Story 2508: INTAKE billing emission

Why this exists

A firm is charged for a page only when PRAMAAN has actually delivered the full promise on it: extraction, the Deep Read, assertions, the comparator, and the chronology contribution — all succeeded. Charging any earlier bills work the counsel never received (ER-16). This Story adds the ONE place that turns that full-package success into a customer ledger event. Nothing else in the codebase may create an INTAKE charge.

1. The emitter (src/domains/page_billing_ledger/intake_emission.py)

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.

2. Exactly-once under every failure mode

ScenarioResultMechanism
Full-package successONE usage charge, receipt page countemitter → ledger allocator
Redriven/replayed completion (CL-08)the ORIGINAL event ids, no new rowsidempotency root = completion_event_id
Failed package, internal retries (CL-02/ER-16)nothingpackage_succeeded guard
Re-read of an already-intaken Originalnothing newper-receipt usage unique index
Exact duplicate reusing an existing read (CL-04)nothingduplicate_of/family-child guard
Machine-initiated worknothingno intake actor → no charge
Completion transaction fails after emittingzero rows persisted; the redrive commits one chargeemitter runs in the caller's transaction
Unknown contract schema_versiontyped error, nothing billedSUPPORTED_COMPLETION_SCHEMA_VERSIONS guard
All behaviors are proved by 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.

3. Wiring contract for the signal owner

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.

Dependency gap (deliberate, per ticket instruction). The full-INTAKE-package completion signal does not exist yet: Epic-2423 T4b (#2503 comparator) and T4c (#2505 chronology view) are still open, so no code path can yet know that all five stages succeeded. The emitter + contract ship now, proven by tests simulating the completion event; the T4b/T4c landing wires the live call. Until then the ledger correctly records zero customer INTAKE events on staging.
Scope boundary. No new routes, no schema change, no migration. Rate-card resolution currently returns the newest India-firm launch card — a firm commercial profile (region/account type) does not exist yet and is the named seam in resolve_current_rate_card.