IMPORTANT (F2 review): certificate is per-tenant, matching series (already tenant-scoped) and the GET/DELETE anti-oracle boundary. Emission's _get_live_certificate and the upload-replace pre-check (certificates. service, renamed _get_live_certificate_by_branch -> _get_live_certificate_by_tenant_branch) both omitted tenant_ref -- two tenants of one product reusing branch_ref="matriz" collapsed onto the same slot: B's upload soft-deleted A's still-live certificate, and A's emission went on to sign with B's certificate. Migration 8f1a2c9d4b6e replaces the partial-unique index ix_fiscal_certificates_product_branch_live with ix_fiscal_certificates_product_tenant_branch_live on (product_id, tenant_ref, branch_ref) WHERE deleted_at IS NULL, with a working downgrade. Upload's two-layer defense (pre-check + IntegrityError -> CertificateUploadConflictError) still holds against the new index. Tests: - tests/emission/test_emissao.py:: test_dois_tenants_do_mesmo_produto_reusando_branch_ref_tem_certificados_isolados -- two tenants upload for the same product/branch_ref, both stay live; emission for each signs with its OWN certificate (observable via FIX 1's CNPJ check: without FIX 2, tenant A's emission would 409 emitente_certificate_cnpj_mismatch because the "live" cert would actually be B's). - tests/migrations/test_fiscal_documents_schema.py:: test_two_tenants_can_both_hold_a_live_certificate_for_the_same_branch_ref_on_real_migration -- real alembic upgrade head, raw INSERTs proving both tenants' certs land live. - tests/migrations/test_fiscal_documents_schema.py:: test_two_live_certificates_for_same_product_tenant_branch_violate_unique_index_on_real_migration (renamed from ..._product_branch_...) -- same (product, tenant, branch) still rejects a second live certificate on the real migration. - tests/certificates/test_certificates.py:: test_concurrent_uploads_for_same_product_branch_only_one_wins_the_other_gets_409 updated for the renamed/re-scoped precheck function (still same-tenant race, still 1 winner + 1 CertificateUploadConflictError). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
54 lines
2.0 KiB
Python
54 lines
2.0 KiB
Python
"""fiscal_certificates: partial-unique live index scoped by tenant_ref too
|
|
(FIX 2, F2 review, 2026-07-17-sowai-fiscal-svc-design.md decisão #4) --
|
|
`ix_fiscal_certificates_product_branch_live` (`(product_id, branch_ref)
|
|
WHERE deleted_at IS NULL`) let two DIFFERENT tenants of the SAME product
|
|
reusing an identical opaque `branch_ref` (e.g. both `"matriz"`) collapse
|
|
onto the SAME certificate slot: the second tenant's upload soft-deleted the
|
|
first tenant's still-live certificate as a legitimate "replace" instead of
|
|
a 409 conflict, and emission for the first tenant would go on to sign with
|
|
the second tenant's certificate. Replaces the index with one scoped
|
|
`(product_id, tenant_ref, branch_ref) WHERE deleted_at IS NULL` -- matching
|
|
`fiscal_series`'s own tenant-scoped uniqueness and the GET/DELETE
|
|
certificate lookups, which already filtered by `tenant_ref`.
|
|
|
|
Revision ID: 8f1a2c9d4b6e
|
|
Revises: 30a80fe36910
|
|
Create Date: 2026-07-24 00:00:00.000000
|
|
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
from typing import Sequence, Union
|
|
|
|
import sqlalchemy as sa
|
|
from alembic import op
|
|
|
|
revision: str = "8f1a2c9d4b6e"
|
|
down_revision: Union[str, Sequence[str], None] = "30a80fe36910"
|
|
branch_labels: Union[str, Sequence[str], None] = None
|
|
depends_on: Union[str, Sequence[str], None] = None
|
|
|
|
|
|
def upgrade() -> None:
|
|
op.drop_index("ix_fiscal_certificates_product_branch_live", table_name="fiscal_certificates")
|
|
op.create_index(
|
|
"ix_fiscal_certificates_product_tenant_branch_live",
|
|
"fiscal_certificates",
|
|
["product_id", "tenant_ref", "branch_ref"],
|
|
unique=True,
|
|
postgresql_where=sa.text("deleted_at IS NULL"),
|
|
)
|
|
|
|
|
|
def downgrade() -> None:
|
|
op.drop_index(
|
|
"ix_fiscal_certificates_product_tenant_branch_live", table_name="fiscal_certificates"
|
|
)
|
|
op.create_index(
|
|
"ix_fiscal_certificates_product_branch_live",
|
|
"fiscal_certificates",
|
|
["product_id", "branch_ref"],
|
|
unique=True,
|
|
postgresql_where=sa.text("deleted_at IS NULL"),
|
|
)
|