First 50 seats always free: Paddle carries paid seats, key carries total cap (#25) #26

Merged
Cordy merged 5 commits from feat/free-tier-stacking into main 2026-08-24 12:42:50 +00:00
Owner

Closes #25.

One conversion boundary: Paddle prices/quantities carry PAID seats; everything else (API, signed key, dashboard, Cairn enforcement) talks TOTAL seats.

  • paddle.go: const freeSeats = 50; paddleTransactionCompleted adds freeSeats to the derived paid quantity (line-item sum or absolute subscription lookup) before refresh/issue. The zero-seat "keep existing" fallback is untouched.
  • seats.go: resolveSeatChange floors at freeSeats+1 (400 "seats must be above the free tier (minimum 51)"); subItemsBody sends seats − freeSeats to Paddle for both preview and the billing PATCH.
  • Tests: purchase quantity 120 → key 170; subscription paid 60 → key 110 (update, renewal, failed-lookup keep); dashboard 65 total → PATCH quantity 15; new TestPaddleQuantityIsPaidSeats (quantity 1 → 51-seat key); floor-rejection test. go vet + full suite green on the runner.

Requires the Paddle price quantity minimum to drop 51 → 1 (max 10000 → 9950) — done in the sandbox alongside this merge; repeat on the live price at launch (#26 swap).

Migration note: keys minted before this change were billed for their full seat count; their next renewal/seat-change re-signs them at quantity+50. Sandbox-only, no live customers.

Closes #25. One conversion boundary: Paddle prices/quantities carry PAID seats; everything else (API, signed key, dashboard, Cairn enforcement) talks TOTAL seats. - `paddle.go`: `const freeSeats = 50`; `paddleTransactionCompleted` adds `freeSeats` to the derived paid quantity (line-item sum or absolute subscription lookup) before refresh/issue. The zero-seat "keep existing" fallback is untouched. - `seats.go`: `resolveSeatChange` floors at `freeSeats+1` (400 "seats must be above the free tier (minimum 51)"); `subItemsBody` sends `seats − freeSeats` to Paddle for both preview and the billing PATCH. - Tests: purchase quantity 120 → key 170; subscription paid 60 → key 110 (update, renewal, failed-lookup keep); dashboard 65 total → PATCH quantity 15; new `TestPaddleQuantityIsPaidSeats` (quantity 1 → 51-seat key); floor-rejection test. `go vet` + full suite green on the runner. Requires the Paddle price quantity minimum to drop 51 → 1 (max 10000 → 9950) — done in the sandbox alongside this merge; repeat on the live price at launch (#26 swap). Migration note: keys minted before this change were billed for their full seat count; their next renewal/seat-change re-signs them at quantity+50. Sandbox-only, no live customers.
Cordy merged commit 023f4eb7fe into main 2026-08-24 12:42:50 +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#26
No description provided.