169 lines
11 KiB
Markdown
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ý.
|