Skip to content

ADR-0003. Idempotency phiếu qua partial unique index

FieldValue
StatusAccepted
Date2026-05-06
Decidersfinance-team
Supersedes

Bối cảnh

  • Delivery Kafka là at-least-once; consumer finance commit offset chỉ sau khi handler thành công (autocommit=false). Một crash sau khi post nhưng trước commit gây redeliver.
  • Không có dedup, redeliver sẽ post đôi phiếu và làm hỏng số dư tài khoản.
  • Các source khác nhau cần dedup key khác nhau: một sale order có thể hợp lệ nhận nhiều thanh toán tender tách (dedup theo từng-event payment), trong khi một purchase order nên book đúng một phiếu PAYMENT (dedup theo từng-source chứng từ).
  • Chứng từ đã void không được chặn một re-post hợp lệ sau này của cùng source.

Quyết định

Chúng tôi sẽ thực thi idempotency trong database qua partial unique index trên FinanceVoucher, backed bởi một kiểm tra replay ở mức ứng dụng:

KeyĐiều kiện indexDùng bởi
(merchantId, type, sourceType, sourceId)live, không-VOIDED, sourceType NOT IN (MANUAL, POS_SESSION, SALE_ORDER)dedup theo từng-source (vd PURCHASE_ORDER)
(sourceType, sourceEventUid)live, không-VOIDED, sourceEventUid IS NOT NULLdedup theo từng-event (SALE_ORDER attempt.uid, INVENTORY_ADJUSTMENT inventoryTrackingId)

Trước khi post, FinanceVoucherService.tryIdempotentReplay tra cứu phiếu hiện có theo key áp dụng và, khi trúng, trả về nó (cùng các dòng ledger) thay vì post lại. Index là backstop cứng; kiểm tra ở app tránh phụ thuộc vào round-trip unique-violation. Mọi điều kiện được scope deletedAt IS NULL AND status <> 'VOIDED'.

Hệ quả

ƯuNhược
Redeliver an toàn — không post đôiProducer phải điền đúng key (sourceEventUid cho source theo từng-event; được validate, throw nếu thiếu)
DB là nguồn chân lý ngay cả khi đua giữa các replica consumerHai chiến lược dedup phải suy luận (theo từng-source vs từng-event)
Dòng void/xóa không bao giờ chặn re-post hợp lệĐiều kiện index tinh tế — phải đồng bộ với FinanceVoucherSourceTypes.isDedupable*
Tender tách post một RECEIPT cho mỗi attempt, đúng

Các phương án đã cân nhắc

Phương ánƯuNhượcLý do loại bỏ
Dedup chỉ-app (không index DB)Schema đơn giản hơnĐua giữa các replica consumer có thể post đôiKhông an toàn dưới horizontal scaling
Một dedup key toàn cục cho mọi sourceMột quy tắcPhá vỡ tender tách (một key cho mỗi đơn)Sai cho đơn nhiều-thanh-toán
Store dedup ngoài (Redis set)Tách rờiHạ tầng thêm; cửa sổ nhất quán so với ghi DBPartial index DB là atomic với quá trình post

Tham chiếu

  • packages/core/src/models/schemas/finance/finance-voucher/schema.ts (partial unique index)
  • packages/core/src/models/schemas/finance/finance-voucher/constants.ts (FinanceVoucherSourceTypes.isDedupable, isDedupablePerSourceEvent)
  • packages/core/src/services/finance/finance-voucher.service.ts (tryIdempotentReplay, _lookupExistingForReplay)
  • API Events — Idempotency & Ordering

Proprietary and Confidential. Unauthorized copying, distribution, or use of this software is strictly prohibited.