From 846944448a13587d9550f64175593f166c6c1e69 Mon Sep 17 00:00:00 2001 From: Tyler Date: Fri, 19 Jun 2026 21:51:43 -0600 Subject: [PATCH] =?UTF-8?q?feat(backend):=20harden=20T5=20=E2=80=94=20dive?= =?UTF-8?q?rgence=20comment=20+=20invariant=20tests?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/src/cyclone/db.py | 6 +++ backend/tests/test_db_models.py | 77 +++++++++++++++++++++++++++++++++ 2 files changed, 83 insertions(+) diff --git a/backend/src/cyclone/db.py b/backend/src/cyclone/db.py index 2596aa7..6085673 100644 --- a/backend/src/cyclone/db.py +++ b/backend/src/cyclone/db.py @@ -233,6 +233,12 @@ class Remittance(Base): String(32), ForeignKey("batches.id", ondelete="CASCADE"), nullable=False ) payer_claim_control_number: Mapped[str] = mapped_column(String(64), nullable=False) + # ORM policy: no ON DELETE (forward FK from Remittance → Claim). A claim + # is unlikely to be deleted in normal flow (audit trail); if it ever is, + # the application layer (T7 reconciliation) must clear this FK. The + # migration predates this rationale; amendment deferred to post-SQLite + # rollout. SQLite without `PRAGMA foreign_keys=ON` does not enforce, + # so the divergence is benign in this engine. claim_id: Mapped[Optional[str]] = mapped_column( String(64), ForeignKey("claims.id"), nullable=True ) diff --git a/backend/tests/test_db_models.py b/backend/tests/test_db_models.py index 8de4975..772e03a 100644 --- a/backend/tests/test_db_models.py +++ b/backend/tests/test_db_models.py @@ -7,6 +7,7 @@ from datetime import date, datetime, timezone from decimal import Decimal import pytest +from sqlalchemy.exc import IntegrityError from cyclone import db from cyclone.db import Base, Batch, CasAdjustment, Claim, ClaimState, Remittance @@ -216,3 +217,79 @@ def test_cas_adjustment_aggregation(): .where(CasAdjustment.remittance_id == "CLP-1") ).scalar_one() assert total == Decimal("62.00") + + +def test_remittance_raw_json_round_trips_dict(): + """JSONText on Remittance.raw_json round-trips a dict, not a string.""" + with db.SessionLocal()() as s: + s.add(Batch( + id="b1", kind="835", input_filename="x", + parsed_at=datetime(2026, 6, 19, tzinfo=timezone.utc), + )) + r = Remittance( + id="CLP-1", batch_id="b1", payer_claim_control_number="CLP-1", + status_code="1", + received_at=datetime(2026, 6, 19, tzinfo=timezone.utc), + raw_json={"x12_segments": ["CLP", "NM1", "SVC"], "version": "005010X221A1"}, + ) + s.add(r) + s.commit() + + with db.SessionLocal()() as s: + loaded = s.get(Remittance, "CLP-1") + assert loaded.raw_json == {"x12_segments": ["CLP", "NM1", "SVC"], "version": "005010X221A1"} + + +def test_remittance_unique_constraint_rejects_duplicate_pcn(): + """UNIQUE (batch_id, payer_claim_control_number) blocks duplicates.""" + with db.SessionLocal()() as s: + s.add(Batch( + id="b1", kind="835", input_filename="x", + parsed_at=datetime(2026, 6, 19, tzinfo=timezone.utc), + )) + s.add(Remittance( + id="CLP-1", batch_id="b1", payer_claim_control_number="SAME", + status_code="1", + received_at=datetime(2026, 6, 19, tzinfo=timezone.utc), + )) + s.commit() + + with db.SessionLocal()() as s: + s.add(Remittance( + id="CLP-2", batch_id="b1", payer_claim_control_number="SAME", + status_code="2", + received_at=datetime(2026, 6, 19, tzinfo=timezone.utc), + )) + with pytest.raises(IntegrityError): + s.commit() + + +def test_remittance_cascade_delete_drops_cas_adjustments(): + """Deleting a Remittance drops its CAS rows via ORM cascade.""" + with db.SessionLocal()() as s: + s.add(Batch( + id="b1", kind="835", input_filename="x", + parsed_at=datetime(2026, 6, 19, tzinfo=timezone.utc), + )) + r = Remittance( + id="CLP-1", batch_id="b1", payer_claim_control_number="CLP-1", + status_code="1", total_paid=Decimal("62.00"), + received_at=datetime(2026, 6, 19, tzinfo=timezone.utc), + ) + s.add(r) + s.flush() + s.add(CasAdjustment(remittance_id="CLP-1", group_code="CO", + reason_code="45", amount=Decimal("30.00"))) + s.commit() + + with db.SessionLocal()() as s: + r = s.get(Remittance, "CLP-1") + s.delete(r) + s.commit() + + with db.SessionLocal()() as s: + from sqlalchemy import select + cas = s.execute( + select(CasAdjustment).where(CasAdjustment.remittance_id == "CLP-1") + ).all() + assert len(cas) == 0