Seat-change preview: price the change before anything is billed #15

Merged
Cordy merged 3 commits from feat/seats-preview into main 2026-07-31 00:32:19 +00:00
Owner

Dogfood finding from Nikola: the one-click seat change billed the saved payment method instantly with no review — correct Paddle behaviour, wrong customer experience, and easy to trigger accidentally.

New POST /v1/licenses/{id}/seats/preview calls Paddle's subscription-update preview (PATCH /subscriptions/{id}/preview) with the identical body the real change would send — same items, same proration mode — so the price shown is exactly the price charged. Returns chargeNow (action charge/credit, amount in minor units, currency), recurringTotal/recurringCurrency, and nextBilledAt. Nothing is committed or billed by a preview.

Shared validation (session, org-admin, ownership, unrevoked, subscription-backed) extracted into resolveSeatChange; item-body construction into subItemsBody — both endpoints stay behaviourally identical on the guard rails, proven by the test running the same 401/403/404/409 matrix against both.

The test fakes both Paddle paths and asserts the crucial property directly: after a preview, the real subscription PATCH count is zero; after confirm, exactly one, with quantity 65 and prorated_immediately.

Dogfood finding from Nikola: the one-click seat change billed the saved payment method instantly with no review — correct Paddle behaviour, wrong customer experience, and easy to trigger accidentally. New `POST /v1/licenses/{id}/seats/preview` calls Paddle's subscription-update **preview** (`PATCH /subscriptions/{id}/preview`) with the identical body the real change would send — same items, same proration mode — so the price shown is exactly the price charged. Returns `chargeNow` (action charge/credit, amount in minor units, currency), `recurringTotal`/`recurringCurrency`, and `nextBilledAt`. Nothing is committed or billed by a preview. Shared validation (session, org-admin, ownership, unrevoked, subscription-backed) extracted into `resolveSeatChange`; item-body construction into `subItemsBody` — both endpoints stay behaviourally identical on the guard rails, proven by the test running the same 401/403/404/409 matrix against both. The test fakes both Paddle paths and asserts the crucial property directly: after a preview, the real subscription PATCH count is **zero**; after confirm, exactly one, with quantity 65 and `prorated_immediately`.
Cordy merged commit 1e1773d472 into main 2026-07-31 00:32:19 +00:00
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference: Cordy/cairn-license-server#15
No description provided.