Bug: mid-cycle seat upgrade extends license expiry by a full period #11

Closed
opened 2026-07-30 23:48:37 +00:00 by Cordy · 0 comments
Owner

refresh() (main.go) always pushes expiry one period past max(now, current expiry). Correct for renewals — its original purpose (#3) — but every transaction.completed carrying a known subscription_id goes through the same path, including the prorated transaction Paddle emits when a subscription's quantity is changed mid-cycle.

Consequence on a yearly license: customer six months in upgrades 55→65 seats, pays only the prorated difference, and gets expiry pushed to old-expiry +1 year (+3 days). If they then cancel, they keep a valid 65-seat license for up to ~18 months having paid for 12 plus a proration.

Fix: distinguish transaction origin. Paddle sets origin: "subscription_recurring" on renewals and origin: "subscription_update" on mid-cycle changes. On subscription_update: update seats and re-sign the same license id without touching expiry. On renewals: current behaviour. Needs a test proving an upgrade leaves ExpiresAt unchanged while seats move.

`refresh()` (main.go) always pushes expiry one period past max(now, current expiry). Correct for renewals — its original purpose (#3) — but every `transaction.completed` carrying a known `subscription_id` goes through the same path, including the prorated transaction Paddle emits when a subscription's **quantity is changed mid-cycle**. Consequence on a yearly license: customer six months in upgrades 55→65 seats, pays only the prorated difference, and gets expiry pushed to old-expiry **+1 year** (+3 days). If they then cancel, they keep a valid 65-seat license for up to ~18 months having paid for 12 plus a proration. Fix: distinguish transaction origin. Paddle sets `origin: "subscription_recurring"` on renewals and `origin: "subscription_update"` on mid-cycle changes. On `subscription_update`: update seats and re-sign the same license id **without touching expiry**. On renewals: current behaviour. Needs a test proving an upgrade leaves `ExpiresAt` unchanged while seats move.
Cordy closed this issue 2026-07-30 23:55:36 +00:00
Sign in to join this conversation.
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#11
No description provided.