# ADR-004 — Cơ chế idempotency cho webhook thanh toán + job đối soát bù định kỳ *Tên file: `adr/ADR-004_idempotency-doi-soat-webhook-thanh-toan.md`* | | | |---|---| | **Status** | `Proposed` | | **Date** | 2026-09-15 | | **Người quyết** | SA + Tech Lead (tích hợp/idempotency — `decision-radar.md §5`) | | **Người đề xuất** | SA (qua skill `sa-2-architecture`, hoạt động `adr`) | | **Điểm radar** | **~6/10** | | **Supersedes** | — | | **Superseded by** | — | | **Liên quan** | `ASR-004` · `QAS-014` · `BR-CART-06` (BA) · `DRV-08` · `ARISK-03` · `CMP-11` | > ⚠️ **ADR bất biến sau khi `Accepted`.** Muốn đổi quyết định thì viết ADR mới có > `Supersedes: ADR-004` và đổi trạng thái bản cũ thành `Superseded by`. --- ## 1. Bối cảnh Xác nhận thanh toán qua webhook VNPay/Momo (bên thứ ba) chưa được xác minh khả thi gần thời gian thực (`DRV-08`, `ASM-01` của BA chưa xác minh). Nếu webhook trễ/lỗi/gọi lặp, đơn hàng có thể kẹt "chờ thanh toán" dù tiền đã thu, hoặc bị cập nhật trạng thái sai nếu xử lý webhook không idempotent. `BR-CART-06` (BA) đã đặt quy tắc nghiệp vụ: chỉ cập nhật `Payment.status = success` khi webhook có chữ ký hợp lệ và trong thời hạn — nhưng con số thời hạn cụ thể **chưa chốt** (thuộc phạm vi kỹ thuật của ADR này, không phải quyết định BA). **Ràng buộc đang chi phối:** | Nguồn | Nội dung | |---|---| | `ASR-004` | Webhook phải idempotent + có job đối soát bù | | `BR-CART-06` (BA) | Chỉ cập nhật `Payment.status=success` khi chữ ký hợp lệ + trong hạn | | `QAS-014` | Job đối soát phát hiện lệch trong ≤15 phút *(đề xuất SA, `OQ-021` chưa xác nhận)* | | `ARISK-03` | VNPay/Momo chưa có sandbox thật — thiết kế chưa kiểm chứng | **Cái đã biết chắc / cái còn là giả định:** | Điều | 🟢 Đã kiểm chứng / 🔴 Giả định | Bằng chứng | |---|---|---| | Webhook có thể bị gọi lặp lại (retry của VNPay/Momo theo tài liệu công khai) | 🟡 Ước lượng có cơ sở | Thực hành phổ biến của cổng thanh toán, chưa xác nhận với sandbox thật | | `gateway_transaction_ref` là khoá duy nhất, ổn định qua các lần gọi lặp | 🔴 Giả định | Chưa có tài liệu API/sandbox thật (`CON-04`) | | Tần suất đối soát ≤15 phút đủ nhanh để tránh khiếu nại CSKH | 🔴 Giả định (`ASM-18`) | `OQ-021` — Tech Lead chưa xác nhận | ## 2. Phương án đã cân nhắc ### PA-1 — Chỉ dựa vào webhook, không có job đối soát bù | | | |---|---| | **Mô tả** | Cập nhật `Payment.status` hoàn toàn dựa vào webhook đến; không có cơ chế nào chủ động kiểm tra lại | | **Ưu** | Đơn giản nhất, không cần job chạy định kỳ | | **Nhược** | Nếu webhook mất/trễ vĩnh viễn (lỗi mạng, đối tác không gửi lại), đơn hàng kẹt "chờ thanh toán" vô thời hạn dù tiền đã thu — vi phạm trực tiếp `DRV-08`/`ASR-004` | | **Chi phí đảo ngược** | Cao — phải xây job đối soát sau khi đã có dữ liệu thật bị kẹt, kèm backfill thủ công cho các đơn đã kẹt | ### PA-2 — Webhook idempotent (khoá theo `gateway_transaction_ref`) + job đối soát bù định kỳ *(chọn)* | | | |---|---| | **Mô tả** | Webhook endpoint kiểm tra idempotency key = `gateway_transaction_ref` trước khi ghi; nếu đã xử lý, trả 200 mà không ghi lại. Job đối soát chạy định kỳ (đề xuất ≤15 phút, `OQ-021`), tra cứu API đối tác để so khớp `Payment.status` | | **Ưu** | Chống hiệu ứng phụ khi webhook gọi lặp; phát hiện được các trường hợp webhook mất hoàn toàn (job chủ động tra cứu, không phụ thuộc webhook đến) | | **Nhược** | Thêm một job chạy định kỳ cần giám sát riêng (alerting nếu job tự nó lỗi); cần bảng log webhook đã nhận để so khớp | | **Chi phí đảo ngược** | Trung bình — đổi tần suất/cơ chế đối soát sau khi có dữ liệu thật tốn công viết lại job + backfill, nhưng không ảnh hưởng mô hình dữ liệu cốt lõi | ### PA-3 — Chỉ dùng job polling, không nhận webhook | | | |---|---| | **Mô tả** | Bỏ hẳn endpoint webhook, chỉ dựa vào job định kỳ tra cứu API đối tác để cập nhật trạng thái | | **Ưu** | Không cần lo idempotency cho webhook đến (không có webhook) | | **Nhược** | Độ trễ xác nhận thanh toán phụ thuộc hoàn toàn chu kỳ polling (tối thiểu bằng tần suất job) — tệ hơn nhiều so với gần thời gian thực; tăng số lượng API call tới đối tác (chi phí + rủi ro rate limit) | | **Chi phí đảo ngược** | Trung bình — thêm lại webhook sau này không khó, nhưng đã đánh đổi trải nghiệm người dùng trong thời gian dùng PA-3 | ### Bảng so sánh | Tiêu chí | PA-1 | PA-2 | PA-3 | |---|---|---|---| | Đáp ứng `ASR-004` (idempotent + đối soát bù) | ❌ | ✅ | 🔶 Có đối soát, không idempotent webhook (không có webhook) | | Độ trễ xác nhận thanh toán (`QAS-002`) | Nhanh nhất khi webhook tới đúng hạn, nhưng vô hạn khi mất | Nhanh (webhook) + có lưới an toàn (job) | Chậm nhất (phụ thuộc chu kỳ polling) | | Chi phí vận hành thêm | Thấp nhất | Trung bình (1 job định kỳ) | Trung bình-cao (nhiều API call hơn) | ## 3. Quyết định > **Chọn PA-2 — Webhook idempotent (khoá `gateway_transaction_ref`) + job đối soát bù định kỳ.** **Vì sao:** Đây là phương án duy nhất vừa giữ được độ trễ thấp cho trường hợp bình thường (webhook tới đúng hạn) vừa có lưới an toàn khi webhook trễ/mất — đúng yêu cầu `ASR-004`. **Phạm vi áp dụng:** Toàn bộ luồng xác nhận thanh toán VNPay/Momo (không áp dụng cho COD — không có webhook). ### Điều kiện chuyển `Proposed → Accepted` | Điều kiện | Ai xác nhận | Trạng thái hiện tại | |---|---|---| | Có sandbox VNPay/Momo thật để kiểm chứng hành vi retry webhook | Tech Lead | Chưa có (`ARISK-03`, `CON-04`) | | `OQ-021` — Tech Lead xác nhận tần suất job đối soát cụ thể | Tech Lead | Mở (đề xuất tạm ≤15 phút) | | Không bắt buộc POC theo `decision-radar.md §6` (không phải con số hiệu năng lớn, nhưng cần sandbox thật để xác minh hành vi đối tác) | — | Chờ sandbox | ## 4. Phương án bị loại và lý do | Phương án | Loại vì | Gắn với | Điều kiện nào thì xét lại | |---|---|---|---| | PA-1 — Chỉ dựa vào webhook, không đối soát | Không phát hiện được trường hợp webhook mất hoàn toàn — vi phạm `ASR-004` | `ASR-004`, `DRV-08` | Không xét lại trừ khi VNPay/Momo đảm bảo bằng SLA hợp đồng webhook không bao giờ mất — chưa có tiền lệ ngành | | PA-3 — Chỉ polling, không webhook | Độ trễ xác nhận kém hơn hẳn, tăng API call — vi phạm gián tiếp `QAS-002` (trải nghiệm checkout) | `QAS-002`, `QAS-014` | Nếu VNPay/Momo không hỗ trợ webhook (một số cổng thanh toán khác trong tương lai) — xét lại cho riêng đối tác đó | ## 5. Hệ quả **Hệ quả tích cực** - Chống được hiệu ứng phụ khi webhook gọi lặp (idempotency key) - Có lưới an toàn phát hiện đơn kẹt trong thời gian giới hạn (đề xuất ≤15 phút), giảm rủi ro khiếu nại CSKH (`DRV-08`) **Hệ quả tiêu cực phải sống chung** - Cần bảng log webhook đã nhận (`ReconciliationLog`) — tăng dung lượng lưu trữ, cần retention policy (thuộc `DAT`, hoạt động 5) - Job đối soát tự nó có thể lỗi — cần alerting riêng cho chính job này (không chỉ alerting cho luồng chính) **Cái quyết định này khoá lại** | Muốn đổi về sau thì | Tốn | |---|---| | Đổi khoá idempotency từ `gateway_transaction_ref` sang cơ chế khác | Cần migrate log webhook đã có + rà lại toàn bộ giao dịch lịch sử | **Việc phát sinh** | Việc | Chủ | Hạn | Ghi ở đâu | |---|---|---|---| | Đàm phán sandbox VNPay/Momo | Tech Lead + PO | Trước GĐ2 `ICD` hoàn tất | `ARISK-03` | | Thiết kế bảng `ReconciliationLog` + retention | SA (hoạt động `dat`) | GĐ2 tiếp theo | `DAT_e-commerce` | | Thêm alerting cho chính job đối soát (job lỗi/không chạy) | SA (hoạt động `inf`) | GĐ2 tiếp theo | `INF_e-commerce` | ## 6. Cách kiểm chứng quyết định này được tuân thủ | Cách kiểm | Công cụ | Chạy ở đâu | `FIT` | |---|---|---|---| | Gọi lặp cùng một webhook payload không tạo hiệu ứng phụ (không nhân đôi trạng thái/log) | Integration test | CI + staging trước mỗi release | `FIT-07` (ứng viên) | | Job đối soát phát hiện lệch giả lập trong ≤15 phút | Test giả lập webhook trễ/mất trên staging | Trước go-live | — (cần sandbox thật, `ARISK-03`) | ## 7. Điều kiện xét lại | Dấu hiệu | Ngưỡng | Ai theo dõi | |---|---|---| | Tần suất job đối soát thật khác đề xuất (`OQ-021` trả lời khác ≤15 phút) | Khi Tech Lead xác nhận số khác | Tech Lead | | Sandbox thật cho thấy webhook có hành vi khác giả định (VD không gọi lặp, hoặc gọi lặp với `gateway_transaction_ref` khác nhau) | Khi có sandbox | Tech Lead | ## 8. Tham chiếu - POC: Không bắt buộc — nhưng cần sandbox thật (`ARISK-03`) trước khi kiểm chứng đầy đủ - Bài đo: `FIT-07` (ứng viên GĐ3) - Tài liệu ngoài: `BR_CartCheckout_v1.0.md §1 BR-CART-06` (BA) - Thảo luận: `ASR_e-commerce_v1.0.md §B2 ASR-004` ## 9. Review log (không đổi Status) | Ngày | Người review | Vai trò | Quyết định | Lý do chưa chuyển `Accepted` | |---|---|---|---|---| | 2026-09-15 | Điều phối dự án (thay mặt Tech Lead, chế độ chạy thử) | Tech Lead (ký thay, ngoại lệ `DEC-01`) | `Reviewed (Proposed giữ nguyên)` — nội dung đủ làm cơ sở thiết kế tiếp (`icd`/`dat`/`sec`/`inf`/`fail`) | Chưa có chữ ký thật Tech Lead; cần chạy thử trên sandbox đối tác thanh toán trước khi chốt | > Đây là ghi nhận **review nội dung**, không phải "sign" theo nghĩa gate: `Status` giữ nguyên > `Proposed`. Xem `00-index/ADL_e-commerce.md` (Change Log — Review 2026-09-15) và `ADL §8` > "Việc phải làm" cho điều kiện chuyển `Accepted`. Confidence tổng thể của lượt review: 🔴 — > `AG2` chưa ký.