Repository navigation
feat(reporting): add the buyer receipts policy to reconcile - #1773
Conversation
reconcile takes receipts=ReceiptPolicy. Off is the default, and nothing is receipted on read; billing periods then show receipt status owed_by_buyer with the buyer responsible, and the client warns once. In verified mode the SDK verifies each unreceipted consumer_receipt materialization itself, through the built-in manifest inspector or an inspect adapter. A mismatch is receipted rejected with the SDK codes without calling commit; otherwise the commit hook returns CommitResult for an accepted receipt carrying its consumer_commit_ref, defer, or Reject with codes. Adjustments are offered to an adjustments hook and deferred. submit_receipts is the typed manual path over sync_reporting_receipts. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Ladon verdict: Approve
Approve — additive buyer receipts policy on reconcile, correctly signalled and tested.
No blocking (critical/high) findings. gated_paths is false, high_risk is false, no author-team gate, and no prior decision. Two Medium findings exist, which is below the row-8 threshold of three, so the PR falls through to row 9 (approve).
Medium findings (non-blocking):
- src/adcp/reporting/subscriptions/receipts.py:176 — Built-in inspector raises on mismatch instead of producing documented rejected receipt; non-matching materialization fails the whole account walk rather than becoming a rejected receipt, and the behavior is untested (tests use a custom inspect).
- src/adcp/reporting/subscriptions/receipts.py:164 — Rejected receipts never dedup; a persistently-rejected period re-writes a fresh rejected receipt to the seller's billing ledger on every reconcile.
These are worth a follow-up look (especially the billing-ledger re-write), but neither is critical/high and failures fail-closed to non-definitive. Semver signal checked (feat, additive exports + ReconcileReport.receipts default None), call-site updates and write-failure handling (RECEIPT_WRITE_FAILED raised then degraded to an issue) verified.
Medium findings
- src/adcp/reporting/subscriptions/receipts.py:176 — Built-in inspector raises on mismatch instead of producing documented rejected receipt
- src/adcp/reporting/subscriptions/receipts.py:164 — Rejected receipts never dedup; reconcile re-submits them unbounded each run
There was a problem hiding this comment.
Ladon verdict: Approve
Approve — clean delta, prior blocking issues addressed, no new findings.
The reviewer surfaced no findings in this run. The delta correctly carries forward previous subscriptions/unauthorized (losses) on a partial events poll and routes a failed principal read to a reconcile issue (SUBSCRIPTION_PRINCIPAL_UNAVAILABLE) while still raising for explain via the new strict flag. The merge is properly gated behind not report.definitive (events.py:515), so genuine removals on a healthy poll still fire and current-wins merge preserves real state changes. Both get_principal and list_accounts failure paths are tested.
The two prior Medium findings lived in receipts.py and are absent from this delta. No no-auto-approve team match, gated_paths is false, review_decision is APPROVED, high_risk is false. No decision-table trigger fires (rows 1–8 all clear) → row 9 approve.
…r failures The receipts policy now skips a materialization already receipted, rejected included, and gives each receipt a stable id per obligation, revision and materialization, so a refusal is never re-sent on every run. A revision the verifier cannot verify, including a mismatch the built-in inspector refuses, is listed in report.receipts.failed with its code and receipted nothing, instead of failing the account walk. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Ladon verdict: Approve
Approve — clean delta pass, no blocking or medium findings.
Checked:
- events.py:546 — doorbell moved to clear-before-poll (not after ack), correctly closing the lost-wakeup window; a ring landing during poll/yield/ack now survives to the next iteration. Guarded by the new
test_a_ring_during_a_cycle_is_not_swallowed. - receipts.py — stable
_receipt_idper (obligation, revision, materialization) with id-only dedup (L363-371) makes accepted/rejected receipts terminal and idempotent; dedup matches on the three ids regardless of status or prior uuid-format id, so it is backward-compatible. - receipts.py:384-394 — inspect try/except is fail-open-per-revision but visible via
report.failed+ a warning, and does not swallowCancelledError. await inspect(context)still has no explicit timeout, but that line predates this delta and the try/except does not worsen it — noted as non-blocking follow-up.
No public-surface/semver, forward-compat union, import-layering, generated-code, credential, or CI-gate concerns in the diff. Only Low-tier note: the L363-371 dedup re-implements the existing _receipt_targets helper.
Gates: high_risk false, gated_paths false (review_decision APPROVED anyway), no no-auto-approve team match. Prior decision was approve. No critical/high/medium findings → decision table falls through to row 9.
There was a problem hiding this comment.
Ladon verdict: Approve
Approve — additive buyer receipts policy for reconcile, net improvement over prior clean pass.
No critical/high findings. One medium finding, no escalation triggers (high_risk false, gated_paths false with review_decision APPROVED, no team gate), so rows 1–8 do not fire → row 9 approve.
Medium findings
- src/adcp/reporting/subscriptions/receipts.py:215 — Built-in
ManifestReportingInspectorpath records a verbosestr(enum)code inreport.receipts.failed/log instead of the bare value, breaking the documented(reporting_obligation_id, code)contract; the test only exercises the raw-string path so it misses it. Fix is_enum(error.code).
Checked and clean: timeouts (inspector readers carry timeout_seconds, seller write reuses adopter client), idempotent stable receipt ids, re-receipt guards (accepted short-circuit + ids-only rejected guard), mode-off warn-once. The 2→3-tuple _obligations change is private with its single caller updated — no public-surface/semver impact. Non-blocking follow-up: on a sync_reporting_receipts write failure the report is marked non-definitive but submitted/rejected lists are already populated with the un-persisted batch; a caller trusting those lists without checking definitive on a billing path could be misled — worth a look but does not meet the medium bar here as a single surfaced concern.
Medium findings
- src/adcp/reporting/subscriptions/receipts.py:215 — Built-in inspector failure records verbose enum repr in report.receipts.failed
…vision Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Ladon verdict: Approve
Approve — delta since prior approve resolves the one outstanding medium finding with no new issues.
What I checked:
receipts.py:216— the prior Medium finding (verbose enum repr inreport.receipts.failed) is resolved: the inspector error code now resolves via_enum(raw)returning.value, andtest_reporting_subscription_receipts.pyasserts the clean"MANIFEST_DIGEST_MISMATCH".events.py:551-555— newReportingEventConflictErrorguard re-polls on an ack race; optimistic-concurrency loads the advanced snapshot so the next ack succeeds (no busy-loop), andstopis honored at the loop top.- No wire-shape, public-surface, or semver concerns in the diff; both fixes are tested.
Decision table: no critical/high/medium findings remain. gated_paths is false (and review_decision is APPROVED anyway). No high-risk flag, no team gate. Rows 1–8 do not fire → row 9 approve. Prior blocking issue addressed; this is a net improvement.
There was a problem hiding this comment.
Ladon verdict: Approve
Approve — clean subsequent review, prior medium resolved.
This is a follow-up push on a previously-approved PR. The delta is three small, clean changes:
receipts.py:388now uses_enum(raw)(returnsstr(getattr(value,"value",value))), resolving the prior medium finding (verbose enum repr inreport.receipts.failed). The enum's value (e.g.MANIFEST_DIGEST_MISMATCH) is now recorded; plain strings fall through unchanged.events.py:550-555catchesReportingEventConflictErrorand continues, keeping theevents()generator alive after another worker acks first; competing-consumer semantics mean no loss/duplicate on re-poll (recovery="transient" confirms intent).- Both new branches have tests.
Gate checks:
- No critical/high/medium findings (row 1, 4, 5, 6, 8 — no match).
gated_paths: false(row 2 n/a);review_decision: APPROVED.high_risk: false, no deletions (rows 3, 5 n/a).- No no-auto-approve team match (row 7 n/a).
- Falls through to row 9 → approve.
The prior medium was addressed and no new findings surfaced.
There was a problem hiding this comment.
Ladon verdict: Approve
Approve — buyer receipts policy for reporting subscriptions.
Checked:
- Verified-mode correctness: gates on commit + exactly one of resource_reader/inspect; SDK-found mismatches reject without calling commit.
- Idempotency: stable per-(obligation,revision,materialization) receipt ids make once-per-materialization skip and off-mode warn-once hold; tests cover each.
- Largest-file rule on receipts.py (+293): the once-per-materialization guard (L360-371) correctly subsumes _receipt_matches to also skip prior rejections.
- Operational readiness: failed receipt write raises RECEIPT_WRITE_FAILED surfaced as a reconcile issue (definitive=False); verifier exceptions isolated per-revision into report.receipts.failed.
Medium findings
- src/adcp/reporting/subscriptions/receipts.py:234 — commit hook is at-least-once on receipt-write failure; the at-least-once guarantee on this billing/durable path is undocumented.
Decision: no critical/high findings; exactly one medium with no escalation-triggering category (not data-loss/schema/infra); high_risk false; gated_paths false and review_decision APPROVED; no no-auto-approve team. Falls through to row 9. Prior decision was approve. Net: single documentation-gap medium, approvable.
Medium findings
- src/adcp/reporting/subscriptions/receipts.py:234 — commit hook is at-least-once on receipt-write failure; not documented
There was a problem hiding this comment.
Ladon verdict: Approve
Approve.
Additive public surface: adds the buyer receipts policy (ReceiptPolicy/CommitResult/Reject/VerifiedRevision, reconcile(receipts=...), submit_receipts). Correct feat shape, forward-compat and import layering untouched. Verified receipt-id stability (stable per obligation/revision/materialization so resends dedup by id), fail-closed walked/mode gate, per-revision inspect isolation, and the SDK-vs-adapter mismatch asymmetry matching docs.
Medium findings
- src/adcp/reporting/subscriptions/receipts.py:234 — commit hook unwrapped: a raise mid-batch discards the account's already-built receipts and re-commits unreceipted obligations on retry (double-load risk on a billing path); no test covers it.
Decision rationale: no critical/high findings (row 1 N/A). gated_paths false; high_risk false; only a single medium finding with no escalation-triggering category (data-loss/schema/infra N/A — this is a billing-path warn/unwrap concern but does not fall into rows 4/5). No no-auto-approve team match. Fewer than three medium findings. Falls through to row 9 → approve. Prior decision was also approve; this is a net-neutral/improvement path. The single medium on the commit-wrap/retry double-load is worth a follow-up but does not block per the decision table.
Medium findings
- src/adcp/reporting/subscriptions/receipts.py:234 — commit hook unwrapped: a raise discards the account's whole receipt batch and re-commits on retry
…st-once contract A commit hook that raises no longer discards the receipts already built for the account's other revisions: it is listed in report.receipts.failed as COMMIT_FAILED and retried on the next reconcile. The docstring and buyer guide state that commit runs at least once per revision and must be idempotent on revision.resource. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Ladon verdict: Approve
Approve — no blocking or medium findings in this run.
This is a subsequent review. The prior run's single medium finding (receipts.py:234 — unwrapped commit hook discarding the whole account receipt batch on a raise) has been resolved: commit now runs inside a try/except (receipts.py:410-423), so a failure drops only its own obligation into report.failed as COMMIT_FAILED and the rest of the account's batch still commits. Failure is surfaced via structured report.failed and retried with documented idempotency — correct shape, not warn-only-hidden. The events.py generation counter behaves correctly (first joins stay at generation 1; rejoins increment for a fresh account.covered id), confirmed by test. No new Critical/High/Medium findings.
Gate check: gated_paths is false; review_decision is APPROVED; no no-auto-approve team match; high_risk is false. No escalation triggers (no deletions, no schema/data-loss/infra medium). Decision table rows 1–8 do not fire → row 9 approve.
|
TS parity note from adcp-client#3582 (T15, the TS port of this slice):
🤖 Generated with Claude Code |
|
Another parity note, from review of adcp-client#3582. TS now reports a rejected billing revision differently. The policy treats any receipt, rejected included, as final for that materialization and never resends it. The period line, though, kept saying TS now reads:
In Python, TS also bounds the policy's 🤖 Generated with Claude Code |
Slice P14 of the reporting-subscriptions DX plan (
.context/dx-reporting-subscriptions.md§2.10, §6.2). Stacked on #1772 (P13), then #1768 (P10) and #1766 (P11).API
Rules
Off by default. Nothing is receipted on read.
consumer_receipt) periods showreceipt: {"status": "owed_by_buyer"}withresponsible_party: "buyer"; the logic is in P10.report.receiptsisNone.mode="verified"needscommitand exactly one ofresource_reader(the built-inManifestReportingInspector) orinspect. Each account's ledger walk feeds it. For eachconsumer_receiptobligation whose current revision is materialized and not yet receipted:rejectedwith the SDK's codes (ROW_COUNT_MISMATCH,CANONICAL_DIGEST_MISMATCH, …) andcommitis not called.commit(VerifiedRevision)decides:CommitResult(ref)gives anacceptedreceipt withconsumer_commit_ref;"defer"submits nothing;Reject(codes)gives arejectedreceipt with those codes.sync_reporting_receiptsper account, and the ledger is walked again so the lines reflect them.Adjustments are offered to
adjustments(adjustment)and deferred. Billing never auto-accepts. Accepting needs the raw adjustment evidence, so it stays on the manual path.client.reporting.submit_receipts(account_id, receipts=..., adjustment_receipts=...)is the typed manual wrapper oversync_reporting_receipts. It returns item results and raisesReportingSubscriptionErrorwhen the task fails.Tests
tests/test_reporting_subscription_receipts.pyruns over the billing ledger fixture fromtest_reporting_reconciliation.py:defersubmits nothing;Rejectsends your codes.offsubmits nothing.submit_receiptsrecords a receipt and refuses an empty call.The events and reconcile suites are green. ruff and mypy are clean. The buyer guide's Receipts for billing feeds section and the README are updated.
🤖 Generated with Claude Code