0

[Fintech 101] Xây dựng hệ thống Thanh toán: Từ One-time, Subscription đến Hoàn tiền (Partial Refund)

Xây dựng một hệ thống thanh toán không đơn thuần là gọi một API và nhận kết quả "Success". Nó là sự kết hợp giữa logic nghiệp vụ, tính toàn vẹn dữ liệu và trải nghiệm người dùng.

Dù bạn dùng Stripe, PayPal hay VNPay, bạn đều phải đối mặt với 3 mô hình chính: Thanh toán một lần, Thuê bao định kỳTrả góp.

1. Các mô hình thanh toán phổ biến

A. One-time Payment (Thanh toán 1 lần)

Đây là mô hình đơn giản nhất (ví dụ: mua một ổ bánh mì, một khóa học).

Luồng đi: User chọn hàng -> Thanh toán -> Nhận hàng -> Kết thúc.

Lưu ý: Cần xử lý Idempotency (Tính không thay đổi) để đảm bảo nếu User bấm nút "Thanh toán" 2 lần do mạng lag, họ cũng không bị trừ tiền 2 lần.

B. Subscription (Thanh toán định kỳ)

Mô hình của Netflix, Spotify hay AWS.

  • Cơ chế: User ủy quyền cho hệ thống tự động trừ tiền sau mỗi chu kỳ (tháng/năm).
  • Kỹ thuật: Cần sử dụng Webhooks để lắng nghe sự kiện từ cổng thanh toán (Ví dụ: invoice.paid, subscription.deleted).
  • Thử thách: Xử lý "Dunning" (Khi thẻ của User hết hạn hoặc hết tiền vào ngày gia hạn).

C. Installments (Trả góp)

Chia nhỏ số tiền lớn thành nhiều đợt thanh toán.

  • 2 dạng chính: 1. Trả góp qua thẻ tín dụng: Ngân hàng trả toàn bộ cho bạn, User nợ ngân hàng.
  • BNPL (Buy Now Pay Later): Hệ thống của bạn hoặc bên thứ 3 (như Fundiin, Kredivo) quản lý các đợt thu tiền.

2. Hoàn tiền (Refund) và Hoàn tiền một phần (Partial Refund)

Đây là nơi "cực hình" nhất của Logic thanh toán.

Refund toàn phần

Hủy bỏ toàn bộ giao dịch và trả lại 100% tiền.

  • Trạng thái: Order chuyển từ Paid -> Refunded.

Partial Refund (Hoàn tiền một phần)

Ví dụ: User mua 3 món hàng nhưng muốn trả lại 1 món.

Phức tạp ở chỗ: Bạn phải tính toán lại Thuế (Tax), phí vận chuyển (Shipping fee) và các mã giảm giá (Discount) đã áp dụng.

Logic: Số tiền hoàn lại tối đa không được vượt quá số tiền đã thanh toán trừ đi các khoản đã hoàn trước đó.

Công thức cơ bản

TotalRefundable=InitialAmountAlreadyRefundedAmountTotalRefundable = InitialAmount - AlreadyRefundedAmount

3. Quy trình thiết kế Database chuẩn (Best Practices)

Đừng bao giờ lưu thông tin thanh toán trực tiếp vào bảng Orders. Hãy tách ra để dễ quản lý hoàn tiền và đối soát.

Table Chức năng
Orders Lưu thông tin đơn hàng (Sản phẩm, tổng tiền).
Payments Lưu lịch sử các lần thanh toán (Transaction ID, Gateway, Status).
Subscriptions Lưu chu kỳ, ngày bắt đầu, ngày hết hạn, Plan ID.
Refunds Lưu lịch sử hoàn tiền, lý do hoàn tiền, mã tham chiếu từ ngân hàng.

4. Xử lý sự cố: Webhook là chìa khóa

Khi User thanh toán xong, họ có thể tắt trình duyệt trước khi Redirect về App của bạn.

Giải pháp: Luôn tin tưởng vào Webhook từ cổng thanh toán gửi về Server-to-Server.

Quy trình: 1. Nhận Webhook.

Kiểm tra chữ ký (Signature) để đảm bảo không bị giả mạo.

Kiểm tra trạng thái đơn hàng hiện tại.

Cập nhật Database và gửi thông báo cho User

5. Ví dụ mã giả (Pseudo-code) cho Partial Refund

public function handlePartialRefund($paymentId, $amountToRefund) 
{
    $payment = Payment::findOrFail($paymentId);
    
    // 1. Kiểm tra số tiền còn lại có đủ để hoàn không
    if ($amountToRefund > $payment->getRemainingAmount()) {
        throw new Exception("Số tiền hoàn vượt quá số dư khả dụng.");
    }

    // 2. Gọi API của Cổng thanh toán (Stripe/PayPal/VNPay)
    $response = $this->gateway->refund($payment->transaction_id, $amountToRefund);

    if ($response->isSuccessful()) {
        // 3. Ghi log vào bảng Refunds
        Refund::create([
            'payment_id' => $paymentId,
            'amount' => $amountToRefund,
            'reason' => 'User returned 1 item'
        ]);

        // 4. Cập nhật lại số tiền đã hoàn trong bảng Payments
        $payment->increment('refunded_amount', $amountToRefund);
    }
}

Lời kết

Hệ thống thanh toán yêu cầu sự tỉ mỉ tuyệt đối. Một sai sót nhỏ trong logic hoàn tiền có thể khiến công ty mất tiền hoặc làm khách hàng giận dữ. Hãy luôn viết Unit Test thật kỹ cho các trường hợp biên (Edge cases).


All rights reserved

Viblo
Hãy đăng ký một tài khoản Viblo để nhận được nhiều bài viết thú vị hơn.
Đăng kí