1. Bối cảnh doanh nghiệp & phạm vi
Một sàn TMĐT thời trang đa danh mục xử lý trung bình 180.000 đơn hàng/ngày, cao điểm sale 9.9 hoặc 11.11 có thể chạm 1,2 triệu đơn trong 24 giờ. Đội ngũ marketing phát hành liên tục các chương trình khuyến mãi: mã giảm giá phần trăm, mã freeship, mã giảm giá theo ngưỡng đơn hàng, và các chương trình có thể stack (cộng dồn) với nhau theo quy tắc ưu tiên. Sai sót trong tính coupon từng khiến một sàn tương tự lỗ hơn 4 tỷ đồng trong một đợt flash sale do lỗi cho phép cộng dồn hai mã loại trừ lẫn nhau.
Phạm vi bài viết bao trùm toàn bộ vòng đời order-to-cash: khách chọn sản phẩm, áp mã khuyến mãi, đặt hàng, thanh toán qua cổng (VNPay/Momo/thẻ), giao vận, và cuối cùng ghi nhận doanh thu vào sổ cái kế toán. Đây là chuỗi nghiệp vụ xuyên nhiều service (pricing, order, payment, inventory, ledger) nên lỗi ở một khâu có thể lan sang khâu sau, gây lệch số liệu tài chính hoặc bán vượt tồn kho. Ràng buộc tuân thủ bao gồm: không được ghi nhận doanh thu trước khi hàng giao thành công (theo nguyên tắc kế toán dồn tích có điều kiện), và log áp dụng coupon phải lưu tối thiểu 5 năm để phục vụ kiểm toán thuế.
Phạm vi tự động hoá
- Kiểm thử API cho pricing engine: tính giảm giá, stacking, ngưỡng, giới hạn dùng
- Kiểm thử E2E Playwright cho luồng đặt hàng có áp coupon
- Kiểm thử đối soát cuối ngày giữa order, payment, ledger
2. Kiến trúc & luồng nghiệp vụ
Kiến trúc gồm 5 service chính: Pricing Engine (tính giá và giảm giá), Order Service (quản lý vòng đời đơn hàng), Payment Gateway (tích hợp cổng thanh toán bên thứ ba), Inventory Reserve (giữ tồn kho tạm thời trong 15 phút chờ thanh toán), và Ledger/Revenue (ghi nhận bút toán kế toán). Luồng đồng bộ diễn ra khi khách bấm 'Áp dụng mã' — pricing engine phải phản hồi trong dưới 300ms để không ảnh hưởng trải nghiệm; luồng bất đồng bộ diễn ra sau khi thanh toán thành công, hệ thống phát sự kiện OrderPaid để Inventory chốt trừ kho vĩnh viễn và Ledger ghi nhận doanh thu tạm hoãn (deferred revenue) chờ giao hàng.
Điểm khó khi kiểm thử
Điểm khó nhất nằm ở việc pricing engine phải tính đúng thứ tự áp dụng khi có nhiều coupon: giảm giá theo % áp dụng trước, sau đó trừ ngưỡng cố định, cuối cùng mới áp freeship — nếu đổi thứ tự sẽ ra số tiền khác. Ngoài ra, giới hạn dùng coupon (ví dụ 'chỉ 500 lượt đầu') là một tài nguyên chia sẻ (shared counter) dễ bị race condition khi nhiều người dùng bấm cùng lúc trong khung giờ vàng — cần kiểm thử đồng thời (concurrency testing) chứ không chỉ kiểm thử tuần tự. Một khó khăn khác là việc hoàn tiền coupon khi huỷ đơn: ngân sách coupon đã trừ phải được hoàn lại đúng, nếu không sẽ làm cạn quỹ khuyến mãi giả tạo trước hạn.
Trong đợt sale 11.11 năm trước, đội vận hành phát hiện quỹ coupon 'giảm 50k' báo hết ngân sách chỉ sau 20 phút dù giới hạn là 10.000 lượt. Điều tra cho thấy một lỗi retry ở client (người dùng bấm nút 'Áp dụng' nhiều lần do UI phản hồi chậm) đã gửi trùng request, và backend không có idempotency key nên đã trừ ngân sách 3 lần cho cùng một hành động của một người dùng.
3. Mô hình dữ liệu & bất biến (oracle)
Mô hình dữ liệu cốt lõi gồm bảng Order (id, userId, status, subtotal, discountTotal, shippingFee, grandTotal), CouponRedemption (couponId, userId, orderId, amountApplied, redeemedAt), CouponBudget (couponId, totalBudget, usedBudget, usageLimitPerUser), và LedgerEntry (orderId, debitAccount, creditAccount, amount, postedAt). Trạng thái đơn hàng đi theo máy trạng thái hữu hạn: DRAFT → PLACED → PAID → FULFILLED → REVENUE_RECOGNIZED, hoặc bất kỳ trạng thái nào trước FULFILLED có thể chuyển sang CANCELLED kèm theo bù trừ (compensation) tương ứng.
Bất biến nghiệp vụ bắt buộc (oracle)
- Bất biến 1: grandTotal = subtotal − discountTotal + shippingFee, sai lệch phải bằng 0 (không làm tròn quá 1 đồng)
- Bất biến 2: Σ amountApplied của một coupon (usedBudget) không bao giờ vượt totalBudget đã cấu hình
- Bất biến 3: số lượt redeem của 1 user cho 1 coupon ≤ usageLimitPerUser tại mọi thời điểm
- Bất biến 4: mỗi orderId chỉ có đúng 1 trạng thái hiệu lực tại một thời điểm (không tồn tại PAID và CANCELLED đồng thời)
- Bất biến 5: doanh thu chỉ được ghi Ledger khi status = FULFILLED, không sớm hơn
💬 Bình luận (0)
Chưa có bình luận nào. Hãy là người đầu tiên!