Two things the plan's own text asked back. First, the idempotency-key
comment lost its substance when Task 5 rewrote it: the charged amount and
the treatment are in the key because at a rate of nought the two Prices
are the same amount and one key would have Stripe hand back a single
object for both — a maintainer who simplified that key would only
rediscover why from a unique-constraint violation. Restored alongside the
newer sentence about what actually stops a second Price (adoption, not
this key), so the module and plan sides read alike again. Spelled out
"twenty-four hours" in both places in the same comment, matching the
sibling.
Second, nothing pinned identifying: ['plan_price_id'] — the one decision
that makes the plan side different from the module side, because a
family's Product carries a Price for every version, term and treatment,
so plan_family alone cannot say which row a Price belongs to. Added a
test that plants a Price proving only the family, confirmed it fails
when identifying is loosened to ['plan_family'], and reverted.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Same defect, same fix, one difference: a family Product carries every version
and every term, so the amount alone does not say which row a Price belongs to.
plan_price_id in the metadata does, and it is what has to agree before a Price
is taken over.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
A verified EU business outside Austria gets reverse charge — rate 0, no VAT
line, the note on the invoice — and was still charged 214,80 € for a 179,00 €
package, because one Stripe Price carried the domestic gross for everybody.
There is no VAT line on their document to reclaim, so it was a flat 20 %
surcharge on exactly the customers who read their invoices, and the document
then stated the whole 214,80 € as net at 0 %, so they self-accounted their own
VAT on a base a fifth too large.
A Stripe Price can ask who is buying after all: there are two per sellable
thing now, the domestic gross and the bare net, both live on one Product, and
the checkout picks by TaxTreatment. The rule is Austria B2B 20 %, other EU B2B
without VAT — a domestic business still pays the gross, reclaims it as input
tax, and the price on the website is still the price charged for them.
- stripe:sync-catalogue mints and archives both halves of every pair, per plan
and per module; `reverse_charge` on stripe_plan_prices/stripe_addon_prices
says which is which, and joining it on subscriptions.stripe_price_id says
which one a running contract is billed on. A rate change moves the gross
Price and leaves the net one alone, because the net owes nothing to the rate.
- A status that changes after the sale converges: a verification (or a lapse)
makes stripe:reprice-subscriptions move the contract onto the other Price
with PRORATE_NONE, because the term is already paid for. The module items
follow the same way.
- Downstream follows: the invoice total equals what was taken, the proof
register expects the figure this customer is actually charged (so a correct
reverse-charge sale is no longer flagged as a mismatch, and an overcharged
one is), the setup fee obeys the same rule, and the booking page quotes what
it will charge.
- StripeClient gained activatePrice(). Archived Prices were brought back in our
own table and left inactive at Stripe, so a rate that moved and moved back
pointed the catalogue at a Price no checkout could be opened for.
An unverified number is still charged the gross — an unchecked string must
never be a discount — and where the net Price is missing the checkout refuses
rather than reaching for the domestic one.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>