Renewal does not imply automatic debit
With payment links, a customer actively completes each period's payment. The product should not promise an automatic debit. It should create a clear renewal flow and extend access only after verified payment.
active → renewal_due → renewal_checkout_created
→ paid → active(next period)
→ unpaid → past_due → canceled
Treat absolute timestamps as truth
Store current_period_start, current_period_end, status, and optionally cancel_at_period_end. Do not infer a period from the latest Payment because payments may arrive late or be reconciled later.
At renewal preparation time:
- Create an Order for the next period with a price snapshot.
- Create a Checkout and PayOS payment link for that Order.
- Deliver the URL through a channel the customer has accepted.
- Move the period forward transactionally after verified payment.
Prevent duplicate renewal orders
Cron runs and workers can overlap. Use a unique key per subscription and period:
create unique index renewal_once_per_period
on orders (subscription_id, period_start)
where kind = 'renewal';
The job may run hourly while producing only one logical renewal Order. Retry provider timeouts with the same idempotency key.
Grace period is a product policy
Represent past_due explicitly. For example, keep normal access before period end, show a renewal notice during a seven-day grace period, and cancel plus revoke grants after grace expires. Backend authorization, email, and UI must share the same rule and timezone.
Late payment and cancellation races
A payment can arrive while the past-due job runs. Lock the Subscription row, reread current Order and Payment state, and cancel only if no succeeded payment exists. Completion must also recognize an already-finished Order and return idempotently.
For cancellation at period end, set cancel_at_period_end=true. Keep already-paid access until period end, skip the next renewal, then cancel and revoke grants.
Measure the lifecycle
Track subscriptions approaching renewal, renewal Checkouts created, payment before and during grace, time to successful payment, and cancellations caused by non-payment. These metrics reveal renewal health better than a raw Payment count.