W4 package-complete signal (T4b/T4c owner)
→ IntakePackageCompletion (versioned contract: completion_event_id,
firm/document/job/extraction_version,
package_succeeded, intake actor)
→ emit_intake_usage_for_package(session, completion) # same transaction
package failed? → emit nothing (CL-02)
no human intake actor? → emit nothing (machine work never bills)
page count ← COUNT(documents.document_pages_v2)
for the completed extraction version
rate card ← newest billing.rate_card version
charge → allocate_and_record_usage(...)
idempotency root = completion_event_id
The contractual page count is read from the pages projection of the exact extraction version the package completed on — the same receipt THE READ ran over — so the bill can never disagree with the record. The rate card is resolved at charge time; the ledger row freezes it forever.
One completion → one logical receipt → 1..N bucket-segment rows. The RFD's
atomic allocation (CL-07) legitimately splits a charge across allowance pools (e.g. 7,500
included + 200 overage = 2 rows), ALL sharing the completion's
source_completion_event_id and idempotency root; a redriven completion still yields
exactly the same rows. The contract carries an explicit schema_version (v1); the
emitter rejects unknown versions with the typed error
pramaan.page_billing.unsupported_completion_schema, so a future contract change can
never mis-bill silently.
| Scenario | Result | Mechanism |
|---|---|---|
| Full-package success | ONE usage charge, receipt page count | emitter → ledger allocator |
| Redriven/replayed completion (CL-08) | the ORIGINAL event ids, no new rows | idempotency root = completion_event_id |
| Failed package, internal retries (CL-02/ER-16) | nothing | package_succeeded guard |
| Re-read of an already-intaken Original | nothing new | per-receipt usage unique index |
| Exact duplicate reusing an existing read (CL-04) | nothing | duplicate_of/family-child guard |
| Machine-initiated work | nothing | no intake actor → no charge |
| Completion transaction fails after emitting | zero rows persisted; the redrive commits one charge | emitter runs in the caller's transaction |
Unknown contract schema_version | typed error, nothing billed | SUPPORTED_COMPLETION_SCHEMA_VERSIONS guard |
tests/integration/test_intake_billing_emission.py
against real Postgres (RLS, triggers, unique indexes live), plus DB-free guard tests in
tests/unit/test_intake_emission.py. Vendor costs are separate (Story-2543) and
untouched here.
The W4 completion-signal owner builds an IntakePackageCompletion (schema_version 1)
and MUST call the emitter inside the same transaction that records package
completion: the charge commits atomically with the completion fact and rolls back with it —
proved by the atomicity test (crash after emitting → zero rows; redrive → one charge). The
seam's outbox detail type is reserved as intake_package.completed.
resolve_current_rate_card.