Renewal không đồng nghĩa auto-debit
Với PayOS payment link, khách hàng chủ động hoàn tất thanh toán cho từng kỳ. Vì vậy hệ thống không nên hứa “tự động trừ tiền”. Nó cần tạo một renewal flow rõ ràng, nhắc đúng lúc và chỉ kéo dài quyền truy cập sau payment hợp lệ.
active → renewal_due → renewal_checkout_created
→ paid → active(next period)
→ unpaid → past_due → canceled
Dùng thời gian tuyệt đối làm nguồn sự thật
Subscription nên giữ current_period_start, current_period_end, status và tùy chọn cancel_at_period_end. Đừng suy ra kỳ hiện tại từ ngày tạo payment gần nhất; payment có thể đến muộn hoặc được đối soát lại.
Khi tới thời điểm chuẩn bị gia hạn:
- Tạo một Order cho kỳ kế tiếp với snapshot giá.
- Tạo Checkout/PayOS payment link gắn với Order đó.
- Gửi URL gia hạn qua kênh liên lạc đã được người dùng cho phép.
- Sau payment hợp lệ, dịch chuyển period trong một transaction.
Chống tạo nhiều renewal order
Cron có thể chạy lại và nhiều worker có thể chạm cùng subscription. Dùng một khóa duy nhất theo subscription và period:
create unique index renewal_once_per_period
on orders (subscription_id, period_start)
where kind = 'renewal';
Job có thể chạy mỗi giờ, nhưng cùng kỳ chỉ có một logical renewal order. Nếu provider request timeout, retry Checkout bằng cùng idempotency key.
Grace period là quyết định sản phẩm
past_due nên là trạng thái rõ ràng, không phải boolean rải rác. Ví dụ:
- Trước
current_period_end: quyền truy cập bình thường. - Từ period end đến hết 7 ngày grace: cho phép truy cập nhưng hiển thị yêu cầu gia hạn.
- Hết grace chưa thanh toán: chuyển
canceledvà thu hồi grants.
Thời lượng grace cần nhất quán với email, UI và backend authorization. Không để frontend tự tính vì clock hoặc timezone khác nhau.
Thanh toán muộn và race condition
Một payment có thể đến đúng lúc job past-due chạy. Khóa subscription row trong transaction, đọc lại Payment/Order hiện tại, rồi chỉ cancel nếu không có succeeded payment. Completion service cũng cần nhận ra Order đã hoàn tất và trả kết quả idempotent.
Hủy cuối kỳ
Khi người dùng chọn hủy, đặt cancel_at_period_end=true; không thu hồi quyền ngay nếu họ đã trả cho kỳ hiện tại. Job renewal không tạo kỳ mới, và job kết thúc kỳ chuyển subscription sang canceled rồi revoke entitlement.
Điều cần đo
- số subscription sắp tới hạn;
- tỷ lệ tạo renewal checkout;
- tỷ lệ thanh toán trước và trong grace period;
- thời gian từ renewal due đến succeeded payment;
- số subscription bị cancel vì không thanh toán.
Các metric này mô tả sức khỏe renewal tốt hơn tổng số payment, vì chúng cho biết người dùng có hoàn tất kỳ tiếp theo hay không.