EzyPayment quản lý subscription như một vòng đời giữa “gói thanh toán”, “người/đối tượng đăng ký”, “sản phẩm cần thanh toán” và “đơn hàng thanh toán”. Subscription không tự trừ tiền trực tiếp; nó theo dõi trạng thái quyền sử dụng, tạo hoặc kích hoạt yêu cầu thanh toán, rồi cập nhật thời hạn dựa trên trạng thái đơn hàng.

Kiến trúc tổng quát

Cơ chế subscription trong EzyPayment xoay quanh 4 khái niệm chính:
  • Payment plan: định nghĩa gói đăng ký, ví dụ loại subscription, chu kỳ, sản phẩm dùng để thanh toán, trạng thái publish.
  • Payment subscription: bản ghi subscription thực tế của một chủ thể, thường là user, đến một item hoặc product cụ thể.
  • Payment order: đơn hàng dùng để thu tiền cho lần kích hoạt hoặc gia hạn subscription.
  • Subscription history: nhật ký snapshot sau mỗi thao tác như tạo mới, cập nhật, yêu cầu thanh toán, gia hạn, từ chối hoặc hết hạn.
Luồng tổng quát:
flowchart TD
    A[Người dùng chọn plan] --> B[Tạo subscription trạng thái WAITING_FOR_PAYMENT]
    B --> C[Người dùng thanh toán payment product]
    C --> D{Đơn hàng được thanh toán?}
    D -- Có --> E[Kích hoạt hoặc gia hạn subscription]
    D -- Không --> F[Từ chối thanh toán, có thể hủy nếu đã hết hạn]
    E --> G[Đặt expiry_at và lịch nhắc thanh toán tiếp theo]
    G --> H[Job nhắc thanh toán trước khi hết hạn]
    H --> I[Gửi event thông báo yêu cầu thanh toán]
    G --> J[Job đánh dấu EXPIRED khi quá hạn]

Payment Plan

Payment plan là template của subscription. Khi tạo subscription từ product, hệ thống kiểm tra plan phải tồn tại và đang ở trạng thái PUBLISHED.
Một plan chứa các thông tin quan trọng:
  • item mà plan áp dụng, ví dụ product cụ thể hoặc plan dùng chung,
  • subscriptionType: RECURRING, ONE_TIME, LIFETIME,
  • paymentProductId: sản phẩm dùng để tạo đơn hàng thanh toán,
  • chu kỳ: periodUnitperiodValue,
  • trial period: hiện được lưu lại nhưng chưa thấy code runtime dùng để tính ngày hết hạn,
  • autoRenew: hiện được lưu lại nhưng chưa thấy logic nền dùng để quyết định có tự gửi yêu cầu gia hạn hay không,
  • status của plan.
Điểm quan trọng: trong code hiện tại, subscriptionTypeautoRenew chủ yếu là dữ liệu mô tả/cấu hình. Logic gia hạn thực tế dựa nhiều hơn vào trạng thái subscription, thời hạn expiryAt, lịch nhắc thanh toán và trạng thái đơn hàng.

Tạo Subscription

Luồng tạo subscription phía web yêu cầu user đã đăng nhập. User gửi productIdplanId.
Hệ thống xử lý như sau:
  • đọc payment plan theo planId,
  • chỉ chấp nhận plan đang PUBLISHED,
  • nếu plan gắn với một product cụ thể, product đó phải khớp với productId trong request,
  • kiểm tra payment product của plan có tồn tại,
  • tạo subscription mới với:
    • chủ thể đăng ký là user hiện tại,
    • item đích là product được đăng ký,
    • plan đích là plan đã chọn,
    • payment product lấy từ plan,
    • chu kỳ và auto-renew lấy từ plan,
    • trạng thái ban đầu là WAITING_FOR_PAYMENT.
Sau khi tạo, response trả về subscriptionIdpaymentProductId. Phần thanh toán sau đó diễn ra qua cơ chế order/payment product của hệ thống ecommerce.

Trạng Thái Subscription

Các trạng thái mặc định của subscription gồm:
  • WAITING_FOR_PAYMENT: đã tạo subscription nhưng chưa thanh toán.
  • ACTIVATED: subscription đang có hiệu lực.
  • INACTIVATED: trạng thái tắt thủ công hoặc mở rộng nghiệp vụ.
  • CANCELLED: subscription bị hủy.
  • EXPIRED: subscription đã hết hạn.
Các hành động được ghi vào lịch sử gồm:
  • CREATE
  • UPDATE
  • REQUEST_PAYMENT
  • RENEW
  • REJECT
  • CANCEL
  • EXPIRE
Mỗi lần có thay đổi quan trọng, hệ thống tạo một bản ghi history. History không chỉ lưu action mà còn copy lại snapshot của subscription tại thời điểm đó: chủ thể đăng ký, item đích, plan, payment product, order liên quan, số lần yêu cầu thanh toán, chu kỳ, trạng thái, ngày bắt đầu, ngày gia hạn, ngày hết hạn và thời điểm thông báo.

Xử Lý Đơn Hàng Thanh Toán

Hệ thống có một appender nền theo dõi lịch sử đơn hàng. Khi đơn hàng chuyển sang trạng thái đã xử lý, cụ thể là PAID hoặc REJECTED, appender tìm subscription liên quan theo hai cách:
  • theo order id đang được lưu là payment request order,
  • hoặc theo order có chứa payment product tương ứng với subscription.
Nếu đơn hàng là PAID, subscription được kích hoạt hoặc gia hạn:
  • đọc periodUnit của subscription,
  • nếu chưa có expiryAt, hoặc expiryAt đã qua, lấy thời điểm hiện tại làm mốc,
  • cộng thêm periodValue theo DAY, WEEK, MONTH hoặc YEAR,
  • chuyển status sang ACTIVATED,
  • nếu chưa có startedAt, đặt startedAt là thời điểm hiện tại,
  • lưu lastPaymentOrderId,
  • lưu oldExpiryAt,
  • cập nhật expiryAt,
  • cập nhật lastRenewedAt,
  • đặt lịch nhắc thanh toán tiếp theo bằng expiryAt - số ngày nhắc trước khi hết hạn,
  • reset paymentRequestCount về 0,
  • ghi history action RENEW.
Nếu đơn hàng bị reject, hệ thống rollback thời hạn về oldExpiryAt nếu có. Nếu thời hạn sau rollback không tồn tại hoặc đã qua, subscription bị chuyển sang CANCELLED và ghi canceledAt. Sau đó ghi history action REJECT.

Nhắc Thanh Toán Gia Hạn

Một appender nền khác quét các subscription đến hạn cần nhắc thanh toán. Điều kiện quét gồm:
  • status nằm trong danh sách được cấu hình cho phép gửi yêu cầu thanh toán,
  • paymentRequestCount nhỏ hơn số lần nhắc tối đa,
  • nextPaymentNotificationTime đã đến hạn.
Giá trị mặc định:
  • nhắc trước khi hết hạn: 7 ngày,
  • số lần nhắc tối đa: 3,
  • khoảng cách giữa các lần nhắc: 3 ngày,
  • status được phép gửi yêu cầu thanh toán: ACTIVATED, EXPIRED.
Khi một subscription được chọn để nhắc:
  • cập nhật lastPaymentNotificationTime,
  • tăng paymentRequestCount,
  • đặt nextPaymentNotificationTime sang lần nhắc tiếp theo,
  • xác định payment product từ plan hoặc từ subscription,
  • có thể tạo order chờ thanh toán trong một số trường hợp,
  • lưu subscription,
  • ghi history action REQUEST_PAYMENT,
  • phát internal event để lớp bên ngoài gửi notification.
Điểm cần lưu ý: code hiện tại phát event thông báo kèm subscription, plan và order nếu có. Việc biến event này thành email, notification, payment link hoặc UI cụ thể nằm ở tầng tích hợp bên ngoài.

Đánh Dấu Hết Hạn

Một appender nền quét subscription đang ACTIVATEDexpiryAt đã qua. Với mỗi bản ghi tìm được, hệ thống:
  • chuyển status sang EXPIRED,
  • cập nhật updatedAt,
  • lưu subscription,
  • ghi history action EXPIRE.
Job này không tự hủy subscription, không xóa dữ liệu và không tự gia hạn. Nó chỉ phản ánh rằng quyền sử dụng đã quá thời hạn.

Cấu Hình

Admin có thể cấu hình các tham số vận hành subscription:
  • số ngày nhắc trước khi subscription hết hạn,
  • số lần gửi yêu cầu thanh toán tối đa,
  • khoảng cách giữa các lần yêu cầu thanh toán,
  • danh sách status được phép gửi yêu cầu thanh toán.
Các giá trị này ảnh hưởng trực tiếp đến job nhắc thanh toán và thời điểm nextPaymentNotificationTime được sinh ra sau mỗi lần gia hạn.

Bảo Mật Và Quyền Truy Cập

Luồng tạo subscription phía web yêu cầu user đã authenticated. User id được lấy từ context đăng nhập, không lấy trực tiếp từ body request.
Các thao tác admin như tạo, sửa, xem danh sách subscription hoặc cập nhật setting yêu cầu admin đã authenticated và có feature phù hợp với nhóm quản trị payment/settings.

Giới Hạn Hiện Tại

Một số điểm quan trọng trong code hiện tại:
  • Trial period được lưu trong plan/subscription nhưng chưa thấy logic dùng trial để tính expiryAt.
  • autoRenew được lưu nhưng chưa thấy logic job kiểm tra autoRenew = YES/NO trước khi gửi yêu cầu thanh toán.
  • subscriptionType được lưu và hiển thị, nhưng luồng gia hạn chính vẫn dựa trên period và trạng thái thanh toán.
  • Subscription không tự thu tiền; nó dựa vào order/payment flow bên ngoài.
  • Việc gửi notification thực tế được tách qua internal event, không nằm trực tiếp trong core subscription flow.
  • Payment request order id có sẵn trong model và được dùng khi xử lý order, nhưng luồng liên kết order nhắc thanh toán với subscription cần được bảo đảm bởi tầng tạo order/tích hợp xung quanh.

Kết Luận

Subscription trong EzyPayment là một state machine gắn với order payment. Hệ thống tạo subscription ở trạng thái chờ thanh toán, kích hoạt hoặc gia hạn khi đơn hàng được trả tiền, nhắc thanh toán trước khi hết hạn, và đánh dấu hết hạn khi quá thời gian sử dụng.
Thiết kế này tách rõ ba phần: dữ liệu plan/subscription, luồng thanh toán bằng order, và các job nền xử lý vòng đời. Nhờ vậy, EzyPayment có thể dùng chung cơ chế ecommerce payment sẵn có, đồng thời vẫn giữ được audit trail đầy đủ cho từng thay đổi của subscription.