The invoice reconcile no longer adopts payments the webhook handlers
recorded from event payloads before the invoice read-back existed. Until
now a settled charge with no mapping first looked for a payment of the
same invoice and outcome that was unmapped, or mapped by its payment
intent alone, and gave the charge to the oldest such payment. Now every
settled charge with no mapping is recorded as a payment of its own,
mapped to the charge. An earlier payment with no charge mapping keeps
its times and its mapping, and the charge's new payment sits beside it,
so a member's and an operator's payment list show that attempt twice.
This is accepted because the handlers have recorded payments by charge
since the read-back, so only a database upgraded from before it can hold
such a payment. The console has no check that looks for them: a database
that still holds some is corrected by hand.
Also deleted: the two queries only adoption used,
ListUnmappedPaymentsByInvoice and SetPaymentMappingCharge (sqlc-unused
counts 450 queries, 452 before, none unused), the adoption outcome and
its count, and the payments_adopted attribute of the log line "stripe:
invoice reconciled". Three database error names go with it ("list
payments to adopt", "adopt payment mapping", "map adopted payment");
every other error format and logger message is equal to the parent's.
The payment-intent index stays, and no migration is added.
Behaviour changed on purpose, and only here. The row "earlier payments
adopted" of the invoice payments characterization becomes "earlier
payments kept beside the charges" with the same fixture: its golden file
is replaced and now shows the two earlier payments kept (one mapped by
its intent alone, one unmapped) and a new payment written for each
charge. The row "adoption skips the other outcome" is deleted with its
golden, since the outcome match it pinned is gone. The line
ListUnmappedPaymentsByInvoice leaves two-declines-then-a-success (3
lines), two-charges-in-the-same-second (2), a-charge-without-a-currency
(1) and the sweep's every-kind-of-candidate (1); nothing else in them
moves. TestInvoiceAdoptsEarlierPayments and
TestInvoiceAdoptsDeclinesInOrder are deleted.
seedPayloadProjectedInvoice writes no payment, so
TestInvoiceSweepRepairsWithoutRenumbering starts from an invoice with
none and finds the one payment its charge records. The test kit's
payment mapping now carries a charge id beside the intent, the shape the
reconcile writes; the earlier-payments row builds its intent-only
mapping with a helper that says so. docs/models/payments-billing.md says
what a payment without a Stripe charge is, and docs/stripe.md's upgrade
notes say a payment recorded from an event payload is not matched to a
charge. The OpenSpec change remove-payment-adoption restates the six
requirements of the payment projection spec that promised adoption and
is archived here; openspec validate --specs --strict passes.
How no other output changed: the goldens were rewritten with -update and
read line by line against the code (four files lose lines, one is replaced
by the renamed file and one is deleted; no other golden changed), then the
tests ran again without -update after a database reset and changed
nothing. At the parent, with this commit's tests, goldens and kit copied
over, TestInvoicePaymentConvergence fails in
earlier_payments_kept_beside_the_charges, two_declines_then_a_success,
two_charges_in_the_same_second and a_charge_without_a_currency, and
TestInvoiceSweepRun fails in every_kind_of_candidate, each on the
ListUnmappedPaymentsByInvoice lines or the adopted rows; everything else
passes. Over ./internal/integrations/stripe/..., ./internal/testkit/...
and ./internal/server the names and results at the parent and at the
commit are equal apart from the three deleted tests and subtest and the
renamed one (1793 passing names before, 1790 after, none failing on
either side).
Assertion map. The adoption tests' counts, charge, method, livemode and
kept created_at on adopted rows are adoption's and go with it. The
invoice moving to paid with its number kept and Stripe's issue time over
the backfill stay pinned by TestInvoiceSweepRepairsWithoutRenumbering.
Two rows are NARROWER: the order in which two earlier rows are adopted
is gone (the order of written inserts stays pinned by two declines then
a success and two charges in the same second), and the sweep repair test
no longer asserts a kept created_at on its payment, since the seeded
payment is gone (the invoice's kept created_at is still asserted).
3 breaks on the new side, each caught: a charge handed to an earlier
unmapped payment (the golden row), the same with no search recorded
(the sweep's statement trace), and an intent-only mapping gaining the
charge (the golden row).
Scores, measured with gocognit: convergePayments 11 (11 at the parent),
convergeCharge 2 (4); adoptEarlierPayment (14) is gone. No gocognit
exception is deleted or added.
Unprobed:
- The log line's attribute. No test reads the reconcile's log line.
- The kit's charge id. No test reads the charge of a mapping the kit
writes; the five server tests that use it read the intent, the sync
status and the environment.
- An out-of-band payment followed by a succeeded charge. Adoption gave
such a charge the unmapped payment written for an invoice paid
without a charge; now a second succeeded payment is written beside
it. Stripe does not report a succeeded charge on the payment intent
of an invoice it reported paid without one, so no fixture builds it;
read from the code.
- Other old-version states: an invoice whose failed read stamped its
read time with earlier payments unmatched, and a never-read mapping
of the other Stripe environment. No fixture builds them; a charge
read for any of them takes the insert the new golden row shows.
- Screens, walkthroughs and test/e2e: no template or handler changes
and none reaches the reconcile; not run.