Lỗi logic mã giảm giá có thể làm sai tổng thanh toán. Tìm hiểu quy tắc chồng, loại trừ và mẫu có thể kiểm thử để tránh giảm hai lần và tổng âm.

Khuyến mãi trông đơn giản cho tới khi bạn đưa chúng vào một quy trình thanh toán thực. Giỏ hàng thay đổi liên tục, nhưng các khoản giảm giá thường được viết như những quy tắc đơn lẻ. Chính khe hở đó là nơi hầu hết các lỗi logic mã giảm giá xuất hiện.
Khó khăn là một quy tắc mới có thể thay đổi tổng tiền ở khắp nơi. Thêm “giảm 10%, nhưng không áp dụng cho hàng giảm giá” và bạn phải quyết định “hàng giảm giá” nghĩa là gì, khi nào kiểm tra, và 10% áp dụng trên khoản nào. Nếu còn khuyến mãi khác cũng chạm tới cùng món hàng, thứ tự áp dụng quan trọng, và thứ tự thay đổi giá.
Nhiều đội cũng trộn toán học với quy tắc nghiệp vụ. Một sửa nhanh như “giới hạn giảm giá bằng tổng phụ” bị sao chép ra ba nơi, rồi bạn có đáp án khác nhau tùy nơi tính tổng (trang giỏ, checkout, hóa đơn, email).
Những thời điểm rủi ro cao là khi hệ thống của bạn tính lại giá:
Một ví dụ nhỏ: khách thêm một bundle, áp mã “$20 off $100”, rồi xóa một món. Nếu mã vẫn “nhớ” tổng phụ cũ, bạn có thể cho $20 khi giỏ chỉ còn $85, hoặc thậm chí làm một dòng hàng âm.
Cuối bài này, bạn sẽ biết cách ngăn hầu hết lỗi khuyến mãi phổ biến: giảm giá hai lần, tổng khác nhau giữa các màn hình, tổng âm, giảm áp dụng cho hàng bị loại trừ, và hoàn tiền không khớp với những gì khách đã thanh toán.
Hầu hết lỗi logic mã giảm giá bắt nguồn từ một câu bị thiếu: những giảm giá nào được phép áp dụng cùng nhau, và theo thứ tự nào. Nếu bạn không thể giải thích quy tắc chồng bằng ngôn ngữ đơn giản, giỏ hàng cuối cùng sẽ làm điều gì đó gây bất ngờ.
Định nghĩa chồng bằng các câu trả lời có/không. Ví dụ: “Một coupon thủ công mỗi đơn hàng. Khuyến mãi tự động vẫn có thể áp dụng trừ khi coupon nói nó chặn chúng.” Một dòng như vậy ngăn các kết hợp ngẫu nhiên dẫn đến giảm hai lần.
Tách giảm theo mặt hàng và giảm theo đơn sớm. Quy tắc theo mặt hàng thay đổi giá sản phẩm cụ thể (ví dụ giảm 20% giày). Quy tắc theo đơn thay đổi tổng (ví dụ $10 off toàn giỏ). Trộn chúng mà không có cấu trúc là cách làm tổng chệch giữa trang sản phẩm, giỏ và checkout.
Quyết định “ưu đãi tốt nhất” nghĩa là gì trước khi bạn code. Nhiều đội chọn “tiết kiệm tối đa”, nhưng điều đó có thể phá vỡ ngưỡng giá sàn. Bạn có thể cần quy tắc như “không bao giờ giảm dưới giá vốn” hoặc “không bao giờ làm phí vận chuyển âm.” Chọn một quy tắc rõ ràng để bộ máy không phải tự suy đoán.
Một thứ tự ưu tiên đơn giản giữ xung đột có thể dự đoán được:
Ví dụ: giỏ có khuyến mãi tự động 10% tất cả mặt hàng, thêm coupon nhập $15 off cho đơn trên $100. Nếu ưu tiên nói tự động trước, bạn có thể rõ ràng trả lời: ngưỡng $100 dùng tổng trước giảm hay sau giảm? Ghi lại, rồi giữ nhất quán khắp nơi.
Khi những lựa chọn này được viết ra, quy tắc chồng coupon của bạn trở thành quy tắc có thể kiểm thử, không phải hành vi ẩn. Đây là cách nhanh nhất tránh lỗi sau này.
Nhiều lỗi bắt đầu khi giảm giá tản mát dưới dạng if-else khắp mã checkout. Cách an toàn hơn là coi mỗi khuyến mãi như dữ liệu với loại, phạm vi và giới hạn rõ ràng. Khi đó toán học giỏ hàng chỉ là bộ đánh giá nhỏ, có thể dự đoán.
Bắt đầu bằng cách đặt tên loại giảm giá, không phải ý tưởng marketing. Hầu hết khuyến mãi phù hợp vài dạng: phần trăm, số tiền cố định, tặng hàng (mua X tặng Y), và miễn phí vận chuyển. Khi bạn thể hiện khuyến mãi bằng một trong các loại này, bạn tránh được các trường hợp đặc biệt khó test.
Tiếp theo, làm rõ phạm vi. Cùng một % hoạt động rất khác tùy vào mục tiêu. Định nghĩa xem nó áp dụng cho toàn đơn, một danh mục, một sản phẩm, một dòng hàng hay vận chuyển. Nếu phạm vi mơ hồ, bạn sẽ vô tình giảm nhầm tổng phụ hoặc giảm hai lần.
Ghi ràng buộc thành trường dữ liệu, không phải comment trong code. Các ràng buộc phổ biến: chi tiêu tối thiểu, chỉ đơn hàng đầu tiên, và khoảng ngày. Cũng ghi rõ cách xử lý với giá sale: chồng lên sale, áp vào giá gốc, hay loại trừ hàng giảm giá.
Một schema gọn có thể gồm:
Cuối cùng, thêm giá sàn mà engine luôn tôn trọng: tổng không bao giờ thấp hơn 0, và nếu doanh nghiệp cần, hàng không bao giờ thấp hơn giá vốn (hoặc mức giá tối thiểu đã định). Xây dựng điều này sẽ ngăn tổng âm và trường hợp “chúng ta trả tiền cho khách” khó chịu.
Nếu bạn prototype engine giảm giá trong Koder.ai, giữ các trường này hiển thị trong chế độ planning để bộ đánh giá luôn đơn giản và có thể test khi thêm nhiều khuyến mãi.
Nhiều lỗi bắt nguồn khi kiểm tra điều kiện đủ điều kiện và phép toán bị trộn lẫn. Mô hình an toàn hơn là hai pha: trước tiên quyết định những gì có thể áp dụng, rồi tính toán số tiền. Tách biệt này giữ quy tắc dễ đọc và giúp ngăn trạng thái xấu (như tổng âm).
Dùng cùng một thứ tự mỗi lần, ngay cả khi khuyến mãi đến từ UI hay API khác nhau. Tính xác định quan trọng vì nó biến “tại sao giỏ thay đổi?” thành một câu hỏi bạn có thể giải thích.
Một luồng đơn giản hoạt động tốt:
Khi áp khuyến mãi, đừng chỉ lưu một “tổng giảm”. Giữ phân tích theo từng dòng và theo đơn để bạn có thể đối chiếu tổng và giải thích.
Tối thiểu, ghi lại:
Ví dụ: giỏ có hai món, một đang sale. Pha 1 đánh dấu mã đủ điều kiện chỉ cho món giá gốc. Pha 2 áp 10% cho dòng đó, để nguyên dòng sale, rồi tính lại tổng đơn từ phân tích dòng để không giảm hai lần vô ý.
Nhiều lỗi bắt đầu khi loại trừ ẩn trong các nhánh đặc biệt kiểu “nếu mã là X thì bỏ qua Y.” Nó hoạt động cho một khuyến mãi, rồi vỡ khi khuyến mãi tiếp theo xuất hiện.
Cách an toàn hơn: giữ một luồng đánh giá duy nhất, và làm cho loại trừ trở thành một tập kiểm tra có thể từ chối một tổ hợp khuyến mãi trước khi bạn tính tiền. Bằng cách đó, giảm không bao giờ áp dụng một nửa.
Thay vì hardcode hành vi, cho mỗi khuyến mãi một “hồ sơ khả năng tương thích” nhỏ, rõ ràng. Ví dụ: loại promo (coupon vs sale tự động), phạm vi (hàng, vận chuyển, đơn), và quy tắc kết hợp.
Hỗ trợ cả hai:
Điểm mấu chốt là engine của bạn hỏi cùng những câu cho mọi promo, rồi quyết định tập hợp có hợp lệ hay không.
Sale tự động thường được áp trước, rồi một coupon đến và lặng lẽ ghi đè. Quyết định trước điều gì nên xảy ra:
Chọn một cách cho mỗi promo và mã hóa nó như một kiểm tra, không phải một đường tính toán thay thế.
Một cách thực tiễn để tránh bất ngờ là kiểm tra tính đối xứng. Nếu “WELCOME10 không kết hợp với FREESHIP” là ý định hai chiều, mã hóa để cả hai chiều đều chặn. Nếu không đối xứng, làm rõ và hiển thị trong dữ liệu.
Ví dụ: sale toàn site 15% đang chạy. Khách nhập coupon 20% dành cho hàng giá gốc. Các kiểm tra nên loại trừ hàng đang sale cho coupon trước khi tính tổng, hơn là giảm chúng rồi cố sửa số về sau.
Nếu bạn xây quy tắc trong nền tảng như Koder.ai, giữ các kiểm tra này như một lớp riêng, có thể test, để thay đổi quy tắc mà không viết lại phần toán học.
Hầu hết tranh chấp khuyến mãi không phải vì con số headline. Chúng xảy ra khi cùng giỏ được tính theo hai cách hơi khác nhau, rồi khách thấy một số ở giỏ và một số khác ở checkout.
Bắt đầu bằng khoá trình tự thao tác. Quyết định, và văn bản hoá, xem giảm theo dòng có xảy ra trước giảm theo đơn không, và vận chuyển nằm đâu. Quy tắc phổ biến: giảm theo mặt hàng trước, rồi giảm theo đơn trên tổng còn lại, rồi giảm vận chuyển cuối cùng. Dù chọn gì, dùng cùng một trình tự ở mọi nơi hiển thị tổng.
Thuế là cái bẫy tiếp theo. Nếu giá đã bao gồm thuế, giảm giá làm giảm phần thuế nữa. Nếu giá chưa bao gồm thuế, thuế tính sau khi giảm. Trộn hai mô hình này ở các phần khác nhau của luồng là một trong những lỗi kinh điển vì hai phép tính đúng vẫn có thể cho kết quả khác nhau nếu giả định cơ sở thuế khác nhau.
Vấn đề làm tròn có vẻ nhỏ nhưng tạo nhiều ticket hỗ trợ. Quyết định làm tròn theo dòng hay theo đơn, và giữ đúng độ chính xác cho tiền tệ. Với coupon phần trăm, làm tròn theo dòng có thể chênh vài xu so với làm tròn theo đơn, đặc biệt với nhiều món giá thấp.
Những trường hợp méo mó đáng xử lý:
Ví dụ cụ thể: coupon giảm 10% theo đơn cộng miễn phí vận chuyển cho đơn trên $50. Nếu coupon áp trước khi kiểm tra ngưỡng, tổng sau giảm có thể < $50 và vận chuyển mất miễn phí. Chọn một cách diễn giải, mã hóa nó, và giữ nhất quán ở giỏ, checkout và trong hoàn tiền.
Hầu hết lỗi xuất hiện khi giỏ được đánh giá qua hơn một đường. Một khuyến mãi có thể áp ở mức dòng hàng ở chỗ này và sau đó lại áp ở mức đơn ở chỗ khác, và cả hai nhìn có vẻ “đúng” cô lập.
Các lỗi thường gặp và nguyên nhân:
Ví dụ: giỏ có hai món, một đủ điều kiện và một bị loại trừ. Nếu engine tính “tổng đủ điều kiện” đúng cho promo phần trăm, nhưng sau đó trừ một khoản cố định từ tổng đơn đầy đủ, món bị loại trừ thực chất vẫn bị giảm.
Mẫu an toàn nhất là tính mỗi promo trên một “khoản đủ điều kiện” rõ ràng và trả về điều chỉnh có giới hạn (không bao giờ dưới 0), cùng với dấu vết rõ ràng những gì nó chạm tới. Nếu bạn sinh engine giảm giá bằng công cụ như Koder.ai, hãy xuất dấu vết dưới dạng dữ liệu để test có thể so sánh chính xác dòng nào đủ điều kiện và tổng phụ nào được dùng.
Nhiều lỗi xuất hiện vì test chỉ kiểm tra tổng cuối cùng. Một bộ test tốt kiểm tra cả tính đủ điều kiện (promo có nên áp không?) và toán học (nó bớt bao nhiêu?), với phân tích đọc được để so sánh theo thời gian.
Bắt đầu với unit test cô lập một quy tắc. Giữ input nhỏ, rồi mở rộng tới kịch bản giỏ đầy đủ.
Sau khi có độ phủ, thêm vài kiểm tra “luôn đúng” bắt lỗi lạ bạn không nghĩ tới:
Giả sử giỏ có 2 mặt hàng: áo $40 (đủ điều kiện) và thẻ quà $30 (bị loại trừ). Vận chuyển $7. Promo 1: “20% off apparel, max $15”, và promo 2: “$10 off orders over $50” không chồng với giảm phần trăm.
Test kịch bản nên khẳng định promo nào thắng (ưu tiên), xác nhận thẻ quà bị loại trừ, và kiểm tra phân bổ chính xác: 20% của $40 là $8, vận chuyển không đổi, tổng cuối đúng. Lưu phân tích đó làm snapshot vàng để refactor sau này không âm thầm đổi promo áp dụng hay bắt đầu giảm các dòng bị loại trừ.
Trước khi phát hành promo mới, chạy qua checklist cuối cùng để bắt những lỗi khách thấy ngay: tổng lạ, thông điệp mơ hồ, và hoàn tiền không khớp. Những kiểm tra này cũng giúp ngăn hầu hết lỗi vì buộc quy tắc phải hành xử giống nhau ở mọi giỏ.
Chạy các kiểm tra này với vài giỏ “khó” đã biết (một mặt hàng, nhiều mặt hàng, thuế hỗn hợp, vận chuyển, và một dòng số lượng lớn). Lưu giỏ để chạy lại khi bạn thay đổi mã định giá.
Nếu bạn xây quy tắc trong trình tạo như Koder.ai, thêm các trường hợp này làm test tự động cạnh định nghĩa quy tắc. Mục tiêu: promo mới nên fail nhanh trong test chứ không phải fail trong giỏ khách.
Đây là giỏ nhỏ phơi bày hầu hết lỗi mà không quá phức tạp.
Giả sử các quy tắc (ghi chính xác như sau vào hệ thống):
Giỏ:
| Line | Price | Notes |
|---|---|---|
| Item A | $60 | full-price, eligible |
| Item B | $40 | full-price, eligible |
| Item C | $30 | sale item, excluded |
| Shipping | $8 | fee |
Promos:
Kiểm tra ngưỡng coupon: hàng đủ điều kiện trước giảm là $60 + $40 = $100, nên coupon có thể áp.
Áp Promo 1 (10% cho hàng đủ điều kiện): $100 x 10% = $10 off. Tổng đủ điều kiện còn $90.
Áp Promo 2 ($15 off): giới hạn là $90, nên áp đủ $15. Tổng đủ điều kiện mới: $75.
Tổng:
Bây giờ đổi một thứ: khách xóa Item B ($40). Hàng đủ điều kiện còn $60, nên coupon không đạt ngưỡng. Chỉ có promo 10% tự động: Item A còn $54, hàng là $54 + $30 = $84, và tổng cuối trở thành $99.36. Đây là kiểu “chỉnh sửa nhỏ” thường làm hỏng giỏ nếu điều kiện và thứ tự không rõ ràng.
Cách nhanh nhất tránh lỗi là coi promo như quy tắc sản phẩm, không phải “một chút toán trong checkout.” Trước khi phát hành, viết spec ngắn ai cũng đọc và đồng ý.
Bao gồm bốn điều, bằng ngôn ngữ đơn giản:
Sau khi phát hành, theo dõi các tổng như theo dõi lỗi. Lỗi giảm giá thường trông như đơn hợp lệ cho đến khi tài chính thấy nó.
Thiết lập giám sát cảnh báo đơn hàng có mẫu bất thường, như tổng gần bằng không, tổng âm, giảm lớn hơn tổng phụ, hoặc bùng phát giỏ “miễn phí 100%”. Chuyển cảnh báo đến cùng nơi bạn gửi lỗi checkout, và giữ playbook ngắn cách vô hiệu hóa promo an toàn.
Để thêm promo mới mà không gây regressions, dùng workflow lặp lại: cập nhật spec trước, mã hóa quy tắc như dữ liệu (không phải nhánh code), thêm test cho vài giỏ “bình thường” và một hai trường hợp khó, rồi chạy toàn bộ bộ test giảm giá trước khi merge.
Nếu muốn triển khai và lặp nhanh hơn, bạn có thể prototype luồng engine promo trong Koder.ai bằng chế độ planning, rồi dùng snapshot và rollback khi tinh chỉnh test. Điều này giúp thử thay đổi quy tắc nhanh mà không mất phiên bản đã biết là an toàn.