- Shipped
- August 27, 2026 at 7:32 PM UTC
- Author
- Kamo
- Commit
- 788a236
Two things the consent ledger needs before an inbound STOP can be recorded tenant-scoped and exactly once. `SmsInboundReceipt` (comms_sms_inbound_receipt) is the idempotency anchor from plan Decision M1. Both inbound rails already dedupe and neither dedupe can cover a keyword: **************** dedupes by asking whether a `VoipMessage` row exists for the carrier's message id, but a keyword message is intercepted and returns BEFORE that row is written — so the second delivery of a carrier retry finds no row and is processed again. Without this table a carrier retrying STOP three times appends three revocation records to a WORM ledger that can never be cleaned up. Unique (ORG_ID, CARRIER_MESSAGE_ID); WORM total-block guards copied from ConsentRecord, deliberately not from AgentOutboundTouch whose append-only is convention with no guard at all. ORG_ID is nullable so an unattributable STOP is still recorded as evidence — and SQL unique constraints do not constrain NULLs, so the repository carries a separate **************** A null passed to the ordinary derived finder renders as `= NULL`, which is never true, so the org-less retry check would have silently passed every time. That hole is documented on the entity; it closes when ADD5/MS3 makes the org resolvable. `ConsentRecord` gains ONE nullable column, `campaign_id`, and ONE index, (organization_id, raw_number, scope). Nothing else moved: the WORM guards, the party-XOR-rawNumber `assertIdentified()` that the DB CHECK mlos_consent_one_identity_chk depends on, the three pre-existing indexes and the nullable organization_id are all unchanged, and `ConsentRecordCampaignKeyTest` is a regression fence around each of them. The new org-scoped finders sit next to the pre-existing org-AGNOSTIC **************** That one is correct for the MLOS global-STOP reading and wrong for MS1, where a homeowner who STOPs Bob's Plumbing must stay reachable by Dave's HVAC. Both are documented at the declaration so picking the wrong one is visible at the call site rather than being a silent cross-tenant suppression. TDD — observed failures, verbatim. Red before implementing (mvn -o -Dmaven.repo.local=<private> test): [ERROR] **************** cannot find symbol [ERROR] symbol: class SmsInboundReceipt [ERROR] location: class **************** (9 occurrences) [ERROR] **************** cannot find symbol [ERROR] **************** cannot find symbol [ERROR] **************** cannot find symbol [INFO] BUILD FAILURE Green after: Tests run: 19, Failures: 0, Errors: 0 (with NotificationConsentTest). Mutation A — dropped `unique = true` from the receipt's index: [ERROR] Tests run: 5, Failures: 1, Errors: 0 <<< FAILURE! -- in **************** java.lang.AssertionError: comms_sms_inbound_receipt needs a UNIQUE index — it is the only thing that stops a carrier retry appending a second revocation record [ERROR] **************** Mutation B — removed the (organization_id, raw_number, scope) index: [ERROR] Tests run: 6, Failures: 1, Errors: 0 <<< FAILURE! -- in **************** [ERROR] **************** [an (organization_id, raw_number, scope) index — the tenant-scoped opt-out read] Both reverted. Full suite after revert: Tests run: 2295, Failures: 0, Errors: 0 — including SharedLibBeanSafetyTest, which the two new classes pass because neither carries a Spring stereotype. DDL HAS NOT LANDED. comms_sms_inbound_receipt does not exist and mlos_consent_record has no campaign_id column. This must not ship before KamoInitializerService applies both and REVOKEs UPDATE/DELETE on the new table (plan Task 5) — entity callbacks do not fire for bulk JPQL or native SQL.