8 phút

Hóa đơn đăng ký nhiều tiền tệ: quy tắc làm tròn khớp nhau

Hóa đơn đăng ký nhiều tiền tệ: cách làm tròn thực tế và mô hình tối thiểu để giữ tổng khớp giữa web, mobile và xuất kế toán.

Hóa đơn đăng ký nhiều tiền tệ: quy tắc làm tròn khớp nhau

Tại sao hóa đơn đăng ký lại lệch giữa các hệ thống

Một vấn đề thường gặp: checkout trên web hiện một tổng, app mobile hiện tổng hơi khác, và export kế toán cho ra một con số thứ ba. Mỗi hệ thống đều làm toán "hợp lý", nhưng không phải cùng một phép tính.

Đăng ký làm vấn đề tệ hơn vì bạn lặp lại phép tính nhiều lần. Những khác biệt nhỏ tích lũy qua các lần gia hạn, proration khi nâng cấp giữa chu kỳ, credit và refund, phí retry sau thanh toán thất bại, và các khoảng thời gian không tròn ở đầu hoặc cuối gói.

Sự lệch thường bắt nguồn từ những lựa chọn tí hon mà ban đầu không nhìn thấy cho đến lúc nó gây vấn đề: khi nào làm tròn (mỗi dòng hay cuối cùng), cơ sở tính thuế nào dùng (net hay gross), xử lý tiền tệ có đơn vị nhỏ 0 hay 3 chữ số như thế nào, và tỷ giá FX nào được áp dụng (mốc thời gian nào, nguồn nào, độ chính xác ra sao). Nếu web làm tròn 2 chữ số cho mỗi dòng mà mobile chỉ làm tròn tổng cuối cùng, bạn có thể thấy khác 0.01 ngay cả khi đầu vào giống hệt.

Mục tiêu nghe có vẻ nhàm nhưng quan trọng: cùng một hóa đơn phải cho cùng một tổng ở mọi nơi, mọi lúc. Điều đó giữ khách hàng yên tâm, giảm ticket hỗ trợ, và đứng vững khi kiểm toán.

"Nhất quán" nghĩa là với một invoice ID và phiên bản:

  • Quy tắc tiền tệ và đơn vị nhỏ giống nhau giữa web, mobile và export.
  • Phương pháp thuế khớp (kể cả việc thuế làm tròn theo từng dòng hay theo hóa đơn).
  • Cách làm tròn và xử lý phần dư cố định và có thể lặp lại.
  • Bất cứ tỷ giá FX nào dùng đều được gắn vào hóa đơn (tỷ giá, độ chính xác và thời điểm có hiệu lực).

Ví dụ: khách hàng nâng từ EUR 19.99 lên EUR 29.99 giữa tháng, bị tính proration, rồi có credit nhỏ vì downtime. Nếu một hệ thống làm tròn mỗi dòng proration mà hệ thống khác chỉ làm tròn tổng cuối, export có thể khác với thứ khách hàng thấy, dù mọi số đơn lẻ trông đều "gần đúng".

Những điều cơ bản: tiền tệ, đơn vị nhỏ, net-tax-gross

Trước khi tranh luận về tỷ giá FX hay quy tắc làm tròn thuế, hãy cố định những điều cơ bản. Nếu chúng lỏng lẻo, hóa đơn sẽ lệch giữa web, mobile và export.

Mỗi dòng hóa đơn và tổng hóa đơn nên rõ ràng mang theo ba số: net (trước thuế), tax, và gross (net + tax). Chọn một cái làm nguồn sự thật để lưu và tính, rồi dẫn xuất các giá trị khác theo cùng một cách ở mọi nơi. Nhiều đội lưu net và tax, rồi tính gross = net + tax vì điều đó thuận tiện cho kiểm toán và hoàn tiền.

Ghi rõ tiền tệ của từng con số. Các đội thường lẫn ba khái niệm khác nhau:

  • Invoice currency: tiền tệ pháp lý in trên hóa đơn và dùng cho tổng.
  • Customer display currency: tiền tệ hiển thị trên UI khi người dùng xem gói.
  • Settlement currency: tiền tệ mà nhà xử lý thanh toán chuyển về tài khoản ngân hàng.

Chúng có thể giống nhau nhưng không nhất thiết. Nếu hóa đơn của bạn bằng EUR nhưng thẻ thanh toán được settle bằng USD, hóa đơn vẫn phải nhất quán trong EUR dù khoản nộp ngân hàng khác.

Tiếp theo, xử lý tiền như số nguyên ở đơn vị nhỏ (ví dụ cents). Lưu 9.99 dưới dạng float là cách phổ biến gây ra lỗi 9.989999 sau này, đặc biệt khi thêm thuế, chiết khấu, proration hoặc nhiều mục. Lưu 999 (cents) kèm mã tiền tệ, và chỉ format khi hiển thị.

Cuối cùng, quyết định chế độ định giá có thuế hay không:

  • Tax-exclusive: giá là net, thuế được cộng thêm.
  • Tax-inclusive: giá đã gồm thuế, nên net được suy ra từ giá gồm thuế.

Một kiểm tra cụ thể: một gói hiển thị 10.00 (đã gồm thuế, VAT 20%) nên sinh ra cùng một gross đã lưu ở đơn vị nhỏ trên web và mobile, sau đó dẫn xuất net và tax theo một quy tắc chung.

Tỷ giá FX: nơi thường bắt đầu khác biệt

Khác biệt FX thường bắt đầu trước cả quy tắc thuế và làm tròn. Hai hệ thống có thể đều "đúng" nhưng vẫn không khớp vì dùng nguồn khác nhau, mốc thời gian khác nhau, hoặc độ chính xác khác nhau.

Nhà cung cấp tỷ giá hiếm khi khớp hoàn toàn. Có nơi quote mid-market, có nơi đã bao gồm spread. Một số cập nhật mỗi phút, vài nơi mỗi giờ hoặc mỗi ngày. Dù cùng nhà cung cấp, hệ thống này có thể làm tròn tỷ giá 4 chữ số còn hệ thống kia giữ 8+ chữ số, và điều đó thay đổi tổng khi nhân với tiền đăng ký và thuế.

Quyết định quan trọng nhất là mốc thời gian của tỷ giá nghĩa là gì. Nếu bạn tính phí bằng EUR mà khách thanh toán bằng USD, bạn khóa tỷ giá khi phát hành hóa đơn hay khi capture thanh toán? Cả hai đều phổ biến, nhưng trộn chúng giữa web, mobile và export đảm bảo sẽ mismatch.

Khi đã chọn quy tắc, hãy lưu chính xác tỷ giá bạn dùng trên hóa đơn. Đừng tính lại từ "tỷ giá hiện tại" sau này, dù bạn có thể tra tỷ giá lịch sử. Nhà cung cấp sửa đổi, khác biệt múi giờ, và thay đổi nhỏ về độ chính xác sẽ làm các hóa đơn cũ lệch khi export hoặc tái sinh PDF.

Ví dụ đơn giản: bạn phát hành hóa đơn lúc 23:59, nhưng thanh toán thành công lúc 00:02. Những time-stamp này thường rơi vào "ngày" khác nhau của nhà cung cấp, nên bảng tỷ giá hàng ngày có thể sinh số khác nhau.

Quyết định và ghi lại các chi tiết FX sau:

  • Nhà cung cấp và loại quote (mid-market hay đã bao gồm phí)
  • Quy tắc timestamp (thời điểm phát hành vs thời điểm capture) và múi giờ
  • Độ chính xác đã lưu cho tỷ giá (ví dụ 8 chữ số thập phân)
  • Bước làm tròn (làm tròn số tiền đã chuyển đổi trên mỗi dòng hay chỉ làm tròn tổng cuối)

Các trường hợp đặc biệt cần xử lý sớm: tiền tệ không có chữ số thập phân (như JPY), tỷ giá có độ chính xác rất cao, và các refund. Refund thường nên tái sử dụng tỷ giá đã lưu trên hóa đơn gốc. Nếu không, số tiền refund có thể khác so với kỳ vọng của khách và export kế toán.

Mô hình dữ liệu hóa đơn tối thiểu nhưng hiệu quả

Nếu bạn muốn hóa đơn khớp giữa web, mobile và export kế toán, mô hình dữ liệu phải lưu kết quả, không chỉ đầu vào. Mục tiêu đơn giản: cùng một hóa đơn nên render cùng các đơn vị nhỏ ở mọi nơi, thậm chí vài tháng sau.

Một tập nhỏ các thực thể thường đủ:

  • Subscription: customer_id, plan_id, billing_currency, billing_interval, next_renewal_at, status
  • Invoice: invoice_id, customer_id, subscription_id, issued_at, period_start, period_end, status (draft/final), currency, fx_rate_id (or provider), fx_rate_value, rounding_mode, totals (net_minor, tax_minor, gross_minor)
  • Invoice_line: invoice_id, description, quantity, unit_price_minor, line_net_minor, line_tax_minor, line_gross_minor, tax_rate_id (or tax_code)
  • Payment: invoice_id, paid_at, amount_minor, currency, provider, provider_payment_id, status

Quy tắc then chốt: các trường tiền nên là số nguyên ở đơn vị nhỏ. Lưu cả unit price và các tổng dòng đã tính sẵn. Điều đó ngăn không cho tính lại sau này với quy tắc làm tròn khác hoặc nguồn FX khác.

FX cần được chụp trên hóa đơn, không được suy ra. Dù bạn lưu một bảng FX chung, hóa đơn vẫn nên lưu chính xác fx_rate_value đã dùng vào thời điểm finalize (kèm nguồn) để export có thể tái tạo cùng con số.

Bạn chỉ cần bảng tax breakdown riêng khi một hóa đơn có thể có nhiều mức thuế hoặc nhiều khu vực cùng lúc (ví dụ: nhiều mặt hàng khác nhau, VAT EU + lệ phí địa phương, hoặc thay đổi thuế theo địa chỉ trong cùng hóa đơn). Khi đó lưu một hàng cho mỗi mức thuế với taxable_base_minor và tax_amount_minor.

Cuối cùng, coi hóa đơn đã finalize là bất biến. Lưu một snapshot các giá trị đã tính tại thời điểm nó trở thành final, và không bao giờ tính lại tổng từ subscription sau này. Một lựa chọn này loại bỏ hầu hết lỗi "tại sao cents thay đổi?".

Chọn chiến lược làm tròn và giữ nguyên

Xử lý proration mà không bị trôi xu
Thêm proration, credit và refund sao cho phản chiếu chính xác số tiền từng dòng ban đầu.

Làm tròn không phải là chi tiết toán học. Đó là quy tắc sản phẩm. Nếu web làm tròn một cách, mobile làm khác, và export kế toán lại khác, bạn sẽ có tổng khác ngay cả khi đầu vào giống nhau.

Có ba chiến lược phổ biến, khác nhau ở chỗ bạn "khóa" đơn vị nhỏ:

  • Làm tròn theo đơn vị: làm tròn giá mỗi đơn vị (sau chiết khấu, FX, logic thuế) trước khi nhân với số lượng.
  • Làm tròn theo dòng: tính số tiền dòng đầy đủ, rồi làm tròn một lần cho mỗi dòng.
  • Làm tròn trên tổng hóa đơn: giữ độ chính xác đầy đủ cho mọi dòng, rồi chỉ làm tròn tổng cuối cùng.

Với đăng ký, mặc định tốt là làm tròn theo dòng. Nó dễ dự đoán cho khách (mỗi dòng trông hợp lý), dễ kiểm toán (bạn có thể giải thích tổng từng dòng), và ổn định qua các lần gia hạn. Làm tròn theo đơn vị có thể lệch khi số lượng thay đổi hoặc khi bạn hiển thị giá đơn vị trên UI. Làm tròn chỉ trên tổng hóa đơn thường tạo ra ticket "tại sao các dòng cộng không bằng tổng?" vì tổng các dòng hiển thị không khớp với tổng cuối.

Vấn đề đồng xu kinh điển xuất hiện khi bạn có nhiều mục nhỏ hoặc thuế phân số. Ví dụ: 20 dòng mỗi dòng cho ra phần dư làm tròn 0.004. Làm tròn theo dòng có thể tạo ra khác biệt 0.08 so với làm tròn chỉ ở cuối. Với chuyển đổi FX, các phần thừa nhỏ này xuất hiện nhiều hơn và có thể tích lũy theo thời gian trong export và báo cáo doanh thu.

Dù chọn gì, hãy làm cho nó xác định. Cùng đầu vào phải luôn cho cùng đầu ra giữa web, mobile và export:

  • Dùng cùng quy tắc độ chính xác (đơn vị nhỏ của tiền tệ, số chữ số thập phân cho thuế, độ chính xác FX).
  • Áp dụng làm tròn ở cùng bước (đơn vị, dòng, hoặc tổng), theo cùng thứ tự.
  • Dùng cùng quy tắc xử lý tie (ví dụ half-up hay half-even), ở mọi nơi.

Nếu bạn xây cả flow web và mobile, viết quy tắc làm tròn thành spec có thể kiểm thử, chứ đừng để nó là hành vi UI.

Từng bước: tính tổng theo cách có thể lặp lại

Để giữ cùng số trên web, mobile và export kế toán, coi phép tính như một công thức. Ý chính: tính với độ chính xác cao, nhưng chỉ lưu và cộng các số nguyên ở tiền tệ hóa đơn.

Một công thức có thể lặp lại

  1. Bắt đầu với mỗi dòng item net ở độ chính xác cao. Giữ thêm chữ số thập phân khi nhân với quantity, áp dụng chiết khấu, và (nếu cần) chuyển đổi tiền tệ. Rồi làm tròn một lần sang đơn vị nhỏ của hóa đơn theo quy tắc đã chọn. Lưu số nguyên đó làm line net.

  2. Tính thuế từ line net đã lưu (hoặc từ subtotal nhóm thuế nếu quy tắc cho phép gom nhóm). Áp dụng cùng quy tắc làm tròn và lưu tax là số nguyên ở đơn vị nhỏ. Đây là nơi hệ thống thường lệch: bên này làm tròn trước thuế, bên kia làm tròn sau.

  3. Tính gross mỗi dòng = (net đã lưu + tax đã lưu). Tổng hóa đơn là tổng các số nguyên đã lưu. Đừng tính lại tổng từ giá trị float để hiển thị. UI và export nên đọc các số nguyên đã lưu và format chúng.

  4. Nếu luật địa phương yêu cầu tổng thuế ở cấp hóa đơn, bạn có thể cần phân phối phần dư. Ví dụ: ba dòng mỗi dòng 0.01 thuế có thể cộng thành 0.03, nhưng làm tròn ở cấp hóa đơn cho 0.02. Quyết định quy tắc tie-break xác định (ví dụ: cộng hoặc trừ 1 đơn vị nhỏ bắt đầu từ dòng có cơ sở tính thuế lớn nhất, rồi sắp xếp ổn định theo id dòng). Lưu điều chỉnh như một sửa nhỏ thuế trên các dòng bị ảnh hưởng để mọi hệ thống có thể tái tạo.

  5. Khóa hóa đơn. Sau khi làm tròn cuối cùng và phân phối phần dư, coi hóa đơn là bất biến. Nếu giá subscription thay đổi sau đó, tạo hóa đơn mới hoặc ghi chú credit, nhưng đừng rewrite các con số cũ.

Một kiểm tra cụ thể: nếu gói EUR 9.99 có VAT 19%, giá trị lưu có thể là net 999 cents, tax 190 cents, gross 1189 cents. Mọi client nên render 11.89 EUR từ những số nguyên đã lưu đó, không phải bằng cách tính lại VAT trên fly.

Làm tròn thuế: theo dòng vs theo hóa đơn và phần dư

Làm tròn thuế là nơi toán học đúng biến thành hóa đơn không khớp. Vấn đề lõi đơn giản: làm tròn sớm thay đổi tổng cuối.

Nếu bạn làm tròn thuế theo từng dòng (hoặc theo số lượng), rồi cộng, bạn có thể có tổng khác với việc cộng thuế chưa làm tròn rồi làm tròn một lần ở cuối. Với nhiều dòng, khoảng cách cộng dồn, nhất là khi đơn vị nhỏ và chuyển đổi FX đã tạo sẵn các phân số.

Ví dụ cụ thể (2 chữ số): hai dòng mỗi dòng có taxable 0.05 với thuế 10%. Thuế chưa làm tròn mỗi dòng là 0.005. Nếu làm tròn theo dòng, mỗi dòng thành 0.01, tổng thuế 0.02. Nếu làm tròn ở cấp hóa đơn, tổng taxable là 0.10, thuế là 0.01. Cả hai đều có thể biện hộ. Chỉ là chúng khác nhau.

Khi bạn phải hiển thị thuế theo dòng nhưng đồng thời cần tổng hóa đơn khớp chính xác, phân bổ phần dư làm tròn một cách xác định:

  • Tính thuế chưa làm tròn cho mỗi dòng, rồi làm tròn xuống cho từng dòng.
  • Cộng các thuế đã làm tròn xuống đó và so sánh với mục tiêu thuế (tổng thuế đã được làm tròn ở cấp hóa đơn).
  • Phân phối các đơn vị nhỏ còn lại vào những dòng có phần phân số lớn nhất trước.
  • Thống nhất cách phá vỡ hòa (ví dụ: vị trí dòng, rồi id dòng cố định).
  • Không bao giờ thay đổi phân bổ dựa trên sắp xếp UI.

Export vẫn có thể lệch khi kế toán gom nhóm dòng (theo sản phẩm, theo mức thuế, hay theo khu vực). Để giữ tổng export khớp tổng hóa đơn, phân bổ phần dư trong từng nhóm bắt buộc trước, rồi xác minh tổng nhóm roll-up ra cùng tổng thuế và gross của hóa đơn.

Nếu kế toán yêu cầu tách thuế theo mức hoặc khu vực nhưng UI chỉ hiện một con số, vẫn lưu breakdown (tổng theo mỗi mức/khu vực cộng quy tắc phân bổ dễ kiểm toán). UI có thể hiển thị một tổng duy nhất, trong khi export mang các bucket chi tiết mà không thay đổi grand total.

Trường hợp đặc biệt cần quyết trước

Tạo một dịch vụ toán học thanh toán duy nhất
Xây dựng một dịch vụ tính toán hóa đơn duy nhất để web, mobile và xuất báo cáo đều có thể dùng lại.

Hầu hết mismatched invoice xảy ra ở góc cạnh. Quyết các quy tắc sớm và chúng sẽ không còn là bất ngờ.

Tiền tệ không có chữ số thập phân cần xử lý đặc biệt. JPY và KRW không có đơn vị nhỏ, nên mọi bước giả định "cent" sẽ tạo khác biệt lặng lẽ. Quyết xem bạn làm tròn từng dòng, ở cấp thuế, hay chỉ ở tổng cuối, và đảm bảo mọi client dùng cùng cài đặt tiền tệ.

VAT/GST xuyên biên giới có thể thay đổi mức thuế theo vị trí khách và theo bằng chứng bạn chấp nhận (địa chỉ billing, IP, mã thuế). Phức tạp không phải ở mức thuế mà là thời điểm bạn khóa nó. Chọn thời điểm (checkout, ngày phát hành hóa đơn, hoặc ngày bắt đầu dịch vụ) và bám theo.

Proration là nơi phân số nhân lên. Nâng cấp giữa chu kỳ có thể tạo các giá trị như 9.3333... mỗi ngày. Quyết xem bạn prorate net, hay prorate gross, hoặc tính trước theo khoảng thời gian, rồi tính phần còn lại từ đó. Thay đổi thứ tự sẽ thay đổi đơn vị nhỏ cuối cùng.

Ghi lại các quy tắc này để chúng không bị trôi theo thời gian:

  • Xử lý tiền tệ không có chữ số thập phân: phương pháp làm tròn và khi áp dụng.
  • Thuế theo vị trí: trường nào quyết định mức thuế và khi nào chúng bị đóng băng.
  • Toán proration: cơ sở (ngày, giây, tháng cố định 30 ngày) và điểm làm tròn.
  • Chiết khấu và credit: có giảm cơ sở chịu thuế không, và áp dụng theo dòng hay trên hóa đơn.
  • Tính đối xứng refund: refund nên tái tạo chính xác dòng và thuế của hóa đơn gốc, không tính lại theo tỷ giá hôm nay.

Refunds là cái bẫy cuối cùng. Nếu hóa đơn gốc có 0.01 phần dư làm tròn phân bổ một dòng, refund nên đảo ngược phân bổ chính xác đó. Nếu không, khách thấy một tổng, nhưng sổ sách bạn báo một tổng khác.

Những sai lầm phổ biến gây mismatch hóa đơn

Phần lớn mismatch không do "toán học khó" mà do những lựa chọn nhỏ không nhất quán ở các phần khác nhau của stack.

Một lỗi lớn là lưu tiền bằng float. Giá như 19.99 không thể biểu diễn chính xác trong nhiều hệ thống, nên lỗi nhỏ tích lũy khi cộng dòng, áp chiết khấu, hay tính thuế. Lưu số ở dạng integer trong đơn vị nhỏ, cộng mã tiền tệ và tỷ lệ đơn vị nhỏ.

Một vấn đề khác là tính lại FX khi export. Khách đã trả theo tỷ giá cụ thể tại thời điểm cụ thể. Nếu export của bạn lấy "tỷ giá hôm nay", bạn có thể kết luận tổng khác dù mọi bước lúc đó đều đúng. Coi hóa đơn là một snapshot: lưu tỷ giá FX đã dùng, các số đã chuyển đổi và kết quả làm tròn.

Khác biệt làm tròn cũng xuất hiện khi UI và backend làm tròn ở các bước khác nhau. Ví dụ, backend có thể làm tròn thuế theo dòng, trong khi web UI chỉ làm tròn ở tổng hóa đơn. Cả hai đều có vẻ hợp lý nhưng không khớp.

Năm lỗi lặp lại giải thích hầu hết khoảng cách:

  • Dùng float cho giá, thuế, chiết khấu hoặc chuyển đổi FX
  • Tính lại FX khi export thay vì dùng giá trị đã lưu trên hóa đơn
  • Làm tròn thuế khác nhau giữa UI và job backend
  • Trộn logic tax-included và tax-exclusive trong cùng một hóa đơn
  • Cho phép mobile tự triển khai phép toán thay vì render tổng backend

Một sửa đơn giản nhưng hiệu quả: tính một lần ở backend, lưu snapshot hóa đơn đầy đủ, và để web/mobile render đúng các số đã lưu ấy.

Checklist nhanh để có hóa đơn nhất quán

Giữ web và mobile đồng bộ
Xây dựng API tổng hợp dùng chung để client hiển thị số nguyên đã lưu thay vì tính toán lại.

Khi số khác nhau giữa web, mobile và export kế toán, thường không phải do toán học mà do lưu trữ và làm tròn.

Bắt đầu với nguyên tắc: client nên hiển thị những gì hóa đơn lưu, không tính lại. Backend phải là nguồn sự thật duy nhất, và mọi kênh đều đọc cùng các giá trị đã lưu.

Năm kiểm tra ngăn ngừa hầu hết mismatch

  • Lưu mỗi số dòng ở đơn vị nhỏ, và đảm bảo tổng hóa đơn bằng tổng các đơn vị nhỏ đã lưu. Đừng để web hoặc mobile tính lại tổng từ giá đơn vị.
  • Lưu chính xác tỷ giá FX đã dùng (và thời điểm). Nếu tái sinh hóa đơn, dùng lại tỷ giá đã lưu, không dùng tỷ giá mới.
  • Viết một quy tắc làm tròn duy nhất (ở bước nào và như thế nào) và áp dụng ở mọi nơi: backend, hiển thị mobile, tạo PDF, và export kế toán.
  • Làm cho việc đối chiếu thuế có thể chứng minh: tổng các giá trị thuế đã lưu (theo dòng hoặc theo hóa đơn, tuỳ bạn chọn) phải luôn bằng tổng thuế của hóa đơn.
  • Chạy kiểm tra round-trip: xuất dữ liệu hóa đơn rồi import lại, xác nhận tổng và thuế khớp chính xác, kể cả đơn vị nhỏ.

Đừng quên refunds và credits

Refunds và credit note nên phản chiếu kết quả làm tròn của hóa đơn gốc. Nếu hóa đơn gốc làm tròn thuế theo dòng, refund nên làm giống vậy, dùng cùng độ chính xác tiền tệ và tỷ giá FX đã lưu. Nếu không, các phần dư nhỏ có thể xuất hiện và tích lũy theo thời gian.

Cách thực tế để đảm bảo: lưu một snapshot tính toán rõ ràng kèm mỗi hóa đơn: tiền tệ, độ chính xác đơn vị nhỏ, chế độ làm tròn, tỷ giá FX và timestamp, và các dòng đã hoàn tất.

Ví dụ: một hóa đơn khớp giữa web, mobile và export

Dưới đây là một hóa đơn giữ nguyên mọi nơi.

Giả sử hóa đơn phát hành bằng EUR (2 chữ số), VAT 20%, và khách bị charge bằng USD. Backend lưu snapshot FX: 1 EUR = 1.0857 USD.

ItemNet (EUR)
Pro plan (monthly)19.99
Extra seats10.00
Discount (10% of 29.99, rounded)-3.00

Net total (EUR) = 26.99

VAT 20% (EUR) = 5.40 (vì 26.99 x 0.20 = 5.398, làm tròn thành 5.40)

Gross total (EUR) = 32.39

Bây giờ backend dẫn xuất các tổng tính theo tiền charge từ tổng EUR đã lưu và snapshot FX:

  • Gross (USD) = 32.39 x 1.0857 = 35.17
  • Net (USD) = 26.99 x 1.0857 = 29.30
  • VAT (USD) = 5.40 x 1.0857 = 5.86

Nếu bạn cũng lưu các giá trị theo dòng bằng USD, thường sẽ có 0.01 chênh khi làm tròn từng dòng đã chuyển đổi rồi cộng lại. Đó là nơi hóa đơn thường lệch.

Làm cho nó xác định: chuyển đổi và làm tròn mỗi dòng, rồi phân phối những cent thừa (dương hoặc âm) theo thứ tự cố định (ví dụ theo line_id tăng dần) cho đến khi tổng theo dòng bằng với gross USD đã cố định.

Web và mobile nên hiển thị các tổng dòng đã lưu từ backend, tổng thuế, tỷ giá FX và gross, chứ không phải tính lại. Export kế toán nên xuất cùng các số đã lưu kèm snapshot FX (tỷ giá, timestamp hoặc nguồn) để sổ sách khớp với những gì khách hàng thấy.

Bước thực tế tiếp theo là triển khai phép tính như một dịch vụ dùng chung xuất ra một snapshot hóa đơn duy nhất (dòng, thuế, tổng, FX, điều chỉnh làm tròn) và để mọi kênh render từ đó. Nếu bạn xây các luồng này trên Koder.ai (koder.ai), mô hình snapshot này giúp web, mobile và export giữ đồng bộ vì chúng đều đọc cùng các giá trị đã lưu.

Câu hỏi thường gặp

Why does the same subscription show different totals on web, mobile, and accounting exports?

Bởi vì mỗi hệ thống thường đưa ra những lựa chọn hơi khác nhau về khi nào làm tròn, cái gì được làm tròn (net hay gross), và giữ bao nhiêu chữ số cho thuế và FX. Những khác biệt nhỏ đó dẫn đến chênh 0.01–0.02 EUR/đơn vị, đặc biệt khi có proration, credit và retry lặp lại phép tính theo thời gian.

Should I store money as floats or integers?

Lưu số tiền dưới dạng số nguyên ở đơn vị nhỏ (ví dụ cents) cùng mã tiền tệ, và chỉ định dạng cho hiển thị. Số dấu phẩy động không thể biểu diễn nhiều thập phân chính xác nên sẽ sinh lỗi nhỏ khi cộng thuế, chiết khấu hoặc nhiều dòng.

Which amounts should an invoice store: net, tax, or gross?

Chọn một giá trị làm nguồn sự thật và dẫn xuất các phần còn lại theo cùng một quy tắc ở mọi nơi. Thông thường lưu nettax ở đơn vị nhỏ rồi tính gross = net + tax — dễ xử lý hoàn tiền và kiểm toán hơn và giữ tổng ổn định.

What’s the difference between invoice currency, display currency, and settlement currency?

Invoice currency là tiền tệ mà hóa đơn được biểu thị và dùng để đối chiếu. Display currency là thứ bạn hiển thị khi người dùng xem giá, còn settlement currency là tiền tệ mà nhà xử lý thanh toán nộp vào tài khoản ngân hàng; chúng có thể khác nhau miễn là phép tính trên invoice currency vẫn nhất quán.

How do I prevent FX rates from changing old invoices?

Đừng lấy tỷ giá khi xuất hoặc tái sinh PDF. Lưu chính xác tỷ giá FX đã dùng trên hóa đơn (giá trị, độ chính xác, nhà cung cấp và thời điểm hiệu lực), rồi luôn dùng lại để các hóa đơn cũ tái tạo đúng số liệu sau nhiều tháng.

Should FX be locked at invoice issue time or payment capture time?

Khóa một quy tắc duy nhất: hoặc “tỷ giá tại thời điểm phát hành hóa đơn”, hoặc “tỷ giá tại thời điểm capture thanh toán”, rồi áp dụng thống nhất. Trộn hai mốc thời gian khác nhau giữa các hệ thống là nguyên nhân phổ biến gây lệch, nhất là quanh nửa đêm và khác múi giờ.

What rounding strategy is safest for subscriptions?

Mặc định nên làm tròn theo dòng cho hóa đơn đăng ký: dễ giải thích cho khách, dễ kiểm toán (bạn có thể giải thích tổng mỗi dòng), và ổn định qua các lần gia hạn nếu mọi kênh dùng cùng quy tắc.

How do I handle tax rounding without getting mismatched totals?

Chọn rõ ràng giữa làm tròn thuế theo từng dòng hay theo tổng hóa đơn rồi làm cho nó có thể lặp lại. Nếu cần khớp với mục tiêu tổng hóa đơn, phân phối phần thừa làm tròn một cách cố định và lưu các giá trị thuế sau khi phân bổ để mọi hệ thống tái hiện identically.

Why does proration cause so many “penny” differences, and how do I stop them?

Proration sinh ra thập phân lặp (ví dụ tiền theo ngày), nên thứ tự phép toán quyết định kết quả. Chọn một phương pháp (ví dụ: prorate net trước, rồi tính thuế từ net đã lưu), làm tròn ở bước đã thống nhất, và lưu kết quả dòng đã hoàn tất để upgrade/downgrade/credit/refund phản chiếu chính xác toán học ban đầu.

What’s the simplest architecture to keep invoices consistent across all clients?

Backend tạo một snapshot hóa đơn đã hoàn tất (dòng, thuế, tổng, quy tắc đơn vị nhỏ, snapshot FX, chế độ làm tròn) và coi đó là bất biến sau khi finalize. Web, mobile, PDF và export đều render từ các số nguyên đã lưu thay vì tính lại; đây cũng là mô hình tốt khi xây luồng thanh toán trên Koder.ai.

Related posts