diff --git a/app/models/outbox.py b/app/models/outbox.py index dd7fe35..833a822 100644 --- a/app/models/outbox.py +++ b/app/models/outbox.py @@ -5,7 +5,7 @@ from __future__ import annotations import uuid from datetime import datetime -from sqlalchemy import DateTime, Integer, String, Text, func +from sqlalchemy import DateTime, ForeignKey, Integer, String, Text, UniqueConstraint, func from sqlalchemy.dialects.postgresql import JSONB from sqlalchemy.dialects.postgresql import UUID as PGUUID from sqlalchemy.orm import Mapped, mapped_column @@ -77,3 +77,49 @@ class EventOutbox(Base): failed_at: Mapped[datetime | None] = mapped_column( DateTime(timezone=True), nullable=True, ) + + +class OutboxDelivery(Base): + """Per-consumer delivery status for an event_outbox row (Migration 0075). + + Tracks whether each consumer successfully processed an event; an event is + only 'published' when all mandatory deliveries succeed. + """ + + __tablename__ = "outbox_deliveries" + __table_args__ = ( + UniqueConstraint( + "event_id", "consumer_name", + name="uq_outbox_deliveries_event_consumer", + ), + ) + + id: Mapped[uuid.UUID] = mapped_column( + PGUUID(as_uuid=True), primary_key=True, + server_default=func.gen_random_uuid(), + ) + event_id: Mapped[uuid.UUID] = mapped_column( + PGUUID(as_uuid=True), + ForeignKey("event_outbox.id", ondelete="CASCADE"), + nullable=False, + ) + consumer_name: Mapped[str] = mapped_column(String(150), nullable=False) + status: Mapped[str] = mapped_column( + String(30), nullable=False, server_default="pending", + ) + attempt_count: Mapped[int] = mapped_column( + Integer, nullable=False, server_default="0", + ) + next_attempt_at: Mapped[datetime | None] = mapped_column( + DateTime(timezone=True), nullable=True, + ) + last_error: Mapped[str | None] = mapped_column(Text, nullable=True) + processed_at: Mapped[datetime | None] = mapped_column( + DateTime(timezone=True), nullable=True, + ) + created_at: Mapped[datetime] = mapped_column( + DateTime(timezone=True), nullable=False, server_default=func.now(), + ) + updated_at: Mapped[datetime] = mapped_column( + DateTime(timezone=True), nullable=False, server_default=func.now(), + ) diff --git a/docs/test-bugs.md b/docs/test-bugs.md index 8769cdc..1a9a27a 100644 --- a/docs/test-bugs.md +++ b/docs/test-bugs.md @@ -478,7 +478,7 @@ Playwright E2E: 10 passed, 24 failed (BUG-011/012/013 — Mock-Daten und data-te - **Erwartet:** Alle pytest Tests bestehen - **Tatsächlich:** Mehrere Tests fehlgeschlagen (F..F.F..FF, FFFFFFFF) - **Schweregrad:** Medium -- **Status:** ⏳ Nicht gefixt — muss genauer untersuchen welche Tests fehlschlagen +- **Status:** ✅ Teilweise erledigt 2026-08-24 (Block E/I-C): Outbox-Cluster (12 Failures in test_outbox+test_outbox_phase5) BEHOBEN — Root-Cause exakt bewiesen: create_all-basierte Test-Schema fehlte outbox_deliveries-Tabelle UND den uq_outbox_deliveries_event_consumer UniqueConstraint (ON CONFLICT im Produktionscode schlug fehl). Fix: OutboxDelivery-Model in app/models/outbox.py ergänzt (Migration-0075-konform inkl. UniqueConstraint); Beweistest 23/23 grün. Verbleibende Failures anderer Suiten einzeln triagieren. ### BUG-068: Field-Level Permissions nicht implementiert in contacts routes - **Kategorie:** Rechte-System