Files
sys-analysis-design/sa-output/e-commerce/02-architecture/adr/ADR-004_idempotency-doi-soat-webhook-thanh-toan.md
Canhchimlac 343ad8bbbc save
2026-09-15 16:02:30 +07:00

169 lines
11 KiB
Markdown

# 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ý.