Excise digital tax stamps: licensing, serialised marks, duty-gated release, and enforcement traceability - #36
Conversation
Co-Authored-By: Patrick Munis <pmunis@gmail.com>
Co-Authored-By: Patrick Munis <pmunis@gmail.com>
Co-Authored-By: Patrick Munis <pmunis@gmail.com>
Co-Authored-By: Patrick Munis <pmunis@gmail.com>
Co-Authored-By: Patrick Munis <pmunis@gmail.com>
Original prompt from Patrick
|
🤖 Devin AI EngineerI'll be helping with this pull request! Here's what you should know: ✅ I will automatically:
⚙️ Control Options:
|
| const activatedQuantity = marks.filter((mark) => mark.status === "active" || mark.activatedAt !== null).length; | ||
| const retiredQuantity = marks.filter((mark) => mark.status === "retired").length; | ||
| const stillIssuedQuantity = marks.filter((mark) => mark.status === "issued").length; | ||
| const reportedProductionQuantity = reports.reduce((sum, report) => sum + report.quantity, 0); | ||
| const stampVariance = issuedQuantity - activatedQuantity - retiredQuantity - stillIssuedQuantity; |
There was a problem hiding this comment.
🟡 Reconciliation reports false stamp shortfall after retiring an active mark
reconcileOrder counts a stamp as activated when activatedAt is set (excise.ts), but a stamp activated and then retired keeps activatedAt while its status becomes retired, so it is counted in both activatedQuantity and retiredQuantity. stampVariance (excise.ts) then reads negative for a fully accounted order. The analytics endpoint counts activation by current status only (excise.ts) and reports zero for the same data.
| const activatedQuantity = marks.filter((mark) => mark.status === "active" || mark.activatedAt !== null).length; | |
| const retiredQuantity = marks.filter((mark) => mark.status === "retired").length; | |
| const stillIssuedQuantity = marks.filter((mark) => mark.status === "issued").length; | |
| const reportedProductionQuantity = reports.reduce((sum, report) => sum + report.quantity, 0); | |
| const stampVariance = issuedQuantity - activatedQuantity - retiredQuantity - stillIssuedQuantity; | |
| const issuedQuantity = marks.length; | |
| const activatedQuantity = marks.filter((mark) => mark.activatedAt !== null).length; | |
| const currentlyActiveQuantity = marks.filter((mark) => mark.status === "active").length; | |
| const retiredQuantity = marks.filter((mark) => mark.status === "retired").length; | |
| const stillIssuedQuantity = marks.filter((mark) => mark.status === "issued").length; | |
| const reportedProductionQuantity = reports.reduce((sum, report) => sum + report.quantity, 0); | |
| const stampVariance = issuedQuantity - currentlyActiveQuantity - retiredQuantity - stillIssuedQuantity; |
Was this helpful? React with 👍 or 👎 to provide feedback.
There was a problem hiding this comment.
Confirmed, and it's a real one: retireMark sets status = "retired" but leaves activatedAt set, so an activated-then-retired mark is counted in both activatedQuantity and retiredQuantity and stampVariance reads negative on a fully accounted order — while analytics counts activation by current status and reports zero for the same data.
Fixing it slightly differently from the suggestion: the accountability identity moves to mutually exclusive current-status buckets (issued − (active + retired + stillIssued)), and "ever activated" (activatedAt !== null) is kept as the input to productionVariance, since production is reported against activation and retiring the mark afterwards does not unreport it — using currentlyActive there would make productionVariance go negative instead. Both counts are persisted separately and analytics is switched to the same definitions so the two surfaces cannot disagree again, with a behavioural test covering activate → report → retire.
Since this PR is already merged, the fix lands on the follow-on branch (devin/single-window-regulatory-layer) rather than here.
Summary
Adds the excise / digital tax-stamp domain, which the platform previously did not have at all: grepping the repo for the domain returned exactly one line, an ad valorem
exciseRateinside the duty calculation, with no stamp, serial, licensee, facility, SKU or activation concept in the schema or in any of the 104 routers. The comparison this was built from — SICPATRACE Evo, Authentix TransAct, DirectTrace and the EU 2018/574 regulatory floor — is indocs/excise-tax-stamps-parity.md, which defines the eleven capability areas (C1–C11) implemented here and the six innovations (I1–I6).Stacked on #35 (which is stacked on #33); review those first.
The six innovations all exploit the one thing a standalone stamp vendor cannot have: this platform owns the customs side too.
duty_paymentledger entries for the declaration and refuses when they don't covertotalDue, in the same currency. Normally clearance and stamping are two separate systems, which is why the clear-but-never-stamped leak exists.signature_valid_pending_reconciliation, never "genuine product".Fail-closed and honest-failure rules carried over from the audit in #33:
dev/placeholder keys rejected, validated at startup) rather than minting unverifiable marks.unavailableon outage — neverauthenticand neversuspect— and discloses status only, no commercial data.unavailable, notinvalid_signature: our misconfiguration must not read as "this mark is fake".Money-path integrity
payOrderis replay-safe end to end, because a retry after a partial failure was otherwise a second posting of real revenue:The
idempotencyKeyis honoured at the boundary too:tigerbeetle-bridgenow dedupes by key under the store mutex and returns the existing transfer, so concurrent replays cannot both post (that service is still an in-memory simulation of TigerBeetle, as its own comment says — unchanged by this PR).Two defect families from the audit were reintroduced by the first cut of this domain and are fixed here rather than shipped: queries with write side effects (reconciliation, enforcement scan and public verify were
.query()s that inserted rows — themojaloop.getPaymentStatusshape) and a reconciliation variance that was arithmetically meaningless.issued − activated − retired − reportedProductionsubtracts two overlapping quantities, so a perfectly reconciled order reported a large negative variance; it is now two separately meaningful numbers,stampVariance(issued vs activated + retired + still-issued) andproductionVariance(activated vs reported production), each zero on a clean order.Verification
server/excise.behavior.test.tsis caller-level against real Postgres and Redis with only the ledger module stubbed, covering: one transfer across repeatedpayOrdercalls and recovery from an already-posted entry; the currency-mismatch, unsettled-duty and ledger-unavailable release gates; terminal-state re-entry; expired and suspended licences refused at action time; a revoked licence not approvable back to active; idempotent activation; resumable minting and two racing mint calls producing no duplicates; a mark refused a second live parent; both variances zero on a clean order; status-only public verification and the signing-outage path; impossible travel with both scans retained; and eachtraverseSourceoutcome, including a self-filed declaration traversing withactingAgentUserId: null— absent is not unavailable. Go tests cover sequential and concurrent bridge idempotency.Typecheck holds at the repo's 72-error baseline with no diagnostic in a changed file. One failure in
server/payments.test.tsreproduces identically on the base branch and is pre-existing. Not verified: no UI — this PR is server-side only — and the browser subsystem on the build box was unavailable all session, so nothing here has been seen rendered.Link to Devin session: https://app.devin.ai/sessions/e68f0a7bf0e04fb8a3ba32ddb8e1fa23
Requested by: @munisp