feat: emission v1 + idempotency (Task 5)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
jonatanritter
2026-07-22 18:25:08 -03:00
co-authored by Claude Opus 4.8
parent c903a9ce0e
commit 3aae8b67ef
10 changed files with 1192 additions and 2 deletions
+39
View File
@@ -215,3 +215,42 @@ class FiscalDocument(Base, UUIDPKMixin, TimestampMixin, SoftDeleteMixin):
rejeicao_motivo: Mapped[str | None] = mapped_column(String(500), nullable=True)
protocolo: Mapped[str | None] = mapped_column(String(20), nullable=True)
autorizada_em: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True)
class FiscalIdempotencyKey(Base, UUIDPKMixin, TimestampMixin):
"""Task 5: backs the `Idempotency-Key` contract of `POST /v1/emissoes`
(design spec decision #6) -- a SEPARATE table rather than a column on
`FiscalDocument` (Task 3's table, already shipped/migrated) so this
Task never touches that migration. `UNIQUE (product_id,
idempotency_key)` is the SAME-transaction outbox partner of `emission.
service.emitir_documento`'s number-allocation + document-INSERT commit
(`documents.service.allocate_fiscal_number`'s docstring): this row is
added to the SAME session, in the SAME commit, as the `FiscalDocument`
it points to via `document_id` -- so a rollback (e.g. the idempotency
race below, or a signature failure) undoes the allocated number AND the
document row AND this key together, never just some of the three.
The two-layer idempotency pattern this table exists for: (1) a cheap
pre-check SELECT before doing any real work (the fast path for a
genuine retry); (2) this UNIQUE constraint as the source of truth for
the RACE -- two concurrent requests carrying the SAME `Idempotency-Key`
can both pass the pre-check (`None`) before either commits; the FIRST
to commit wins, the SECOND's commit raises `IntegrityError` here, which
`emission.service` catches and translates into a RE-READ of the
winner's row (converging both callers on the SAME `FiscalDocument`,
never emitting a duplicate NF-e for one logical request).
No soft-delete mixin: an idempotency key's history is permanent by
design -- there is no "un-claim this key" operation."""
__tablename__ = "fiscal_idempotency_keys"
__table_args__ = (
UniqueConstraint(
"product_id", "idempotency_key",
name="uq_fiscal_idempotency_key_product_key",
),
)
product_id: Mapped[uuid.UUID] = mapped_column(ForeignKey("products.id"), nullable=False)
idempotency_key: Mapped[str] = mapped_column(String(255), nullable=False)
document_id: Mapped[uuid.UUID] = mapped_column(ForeignKey("fiscal_documents.id"), nullable=False)