การออกใบแจ้งหนี้สำหรับการสมัครสมาชิกหลายสกุลเงิน: กฎการปัดเศษที่สอดคล้องกัน
การออกใบแจ้งหนี้สมัครสมาชิกหลายสกุลเงิน: แนวทางปฏิบัติเรื่องการปัดเศษและโมเดลข้อมูลแบบตารางน้อยที่ช่วยให้ยอดรวมตรงกันบนเว็บ โมบาย และการส่งออกบัญชี

ทำไมยอดใบแจ้งหนี้สมัครสมาชิกจึงต่างกันระหว่างระบบ
ปัญหาทั่วไป: เช็คเอาต์บนเว็บแสดงยอดหนึ่ง โมบายแสดงยอดที่ต่างกันเล็กน้อย และการส่งออกบัญชีก็ได้เลขที่สาม แต่ละระบบต่างก็ทำคณิตศาสตร์ที่ "สมเหตุสมผล" แต่ไม่เหมือนกัน
การสมัครสมาชิกทำให้ปัญหานี้รุนแรงขึ้นเพราะการคำนวณซ้ำซ้อน ความต่างเล็กๆ สะสมผ่านการต่ออายุ การปรอชันเมื่ออัปเกรดกลางรอบ เครดิตและการคืนเงิน ค่าธรรมเนียมที่เรียกซ้ำหลังการชำระเงินล้มเหลว และช่วงเวลาบางส่วนตอนเริ่มหรือสิ้นสุดแผน
ความเบี่ยงมักเริ่มจากตัวเลือกเล็กๆ ที่มองไม่เห็นจนถึงจุดหนึ่ง: จะปัดเมื่อไหร่ (ต่อบรรทัดหรือสุดท้าย), ฐานภาษีที่ใช้เป็นอะไร (net vs gross), จะจัดการสกุลเงินที่มีหน่วยย่อย 0 หรือ 3 ทศนิยมหรือไม่, และใช้อัตราแลกเปลี่ยนใด (timestamp ไหน แหล่งไหน ความแม่นยำเท่าไร) หากเว็บปัด 2 ทศนิยมต่อบรรทัดและโมบายปัดเฉพาะยอดรวมสุดท้าย คุณอาจเห็นความต่าง 0.01 แม้จะมีข้อมูลเข้าเหมือนกัน
เป้าหมายฟังดูน่าเบื่อแต่สำคัญ: ใบแจ้งหนี้เดียวกันควรให้ยอดรวมเดียวกันทุกที่ ทุกครั้ง นั่นช่วยให้ลูกค้ามั่นใจ ลดบิลซัพพอร์ต และยืนได้ในการตรวจสอบบัญชี
"สอดคล้อง" หมายความว่าสำหรับ invoice ID และเวอร์ชันหนึ่ง ๆ:
- กฎสกุลเงินและหน่วยย่อยต้องเหมือนกันบนเว็บ โมบาย และการส่งออก
- วิธีการภาษีต้องตรงกัน (รวมถึงว่าจะปัดภาษีต่อบรรทัดหรือทั้งใบ)
- การปัดและการจัดการเศษส่วนต้องตายตัวและทำซ้ำได้
- อัตรา FX ที่ใช้ต้องถูกบันทึกไว้กับใบแจ้งหนี้ (ค่า ความแม่นยำ และเวลาที่มีผล)
ตัวอย่าง: ลูกค้าอัปเกรดจาก EUR 19.99 เป็น EUR 29.99 กลางเดือน ได้ค่าปรอชัน จากนั้นมีเครดิตเล็กน้อย หากระบบหนึ่งปัดแต่ละบรรทัดปรอชันและอีกระบบปัดเฉพาะยอดสุดท้าย การส่งออกใบแจ้งหนี้อาจไม่ตรงกับที่ลูกค้าเห็น แม้ทุกตัวเลขจะดูว่า "ใกล้เคียงพอ"
พื้นฐาน: สกุลเงิน หน่วยย่อย net-tax-gross
ก่อนจะถกอัตราแลกเปลี่ยนหรือกฎการปัดภาษี ให้ล็อกพื้นฐานก่อน หากสิ่งเหล่านี้ยังไม่ชัด ยอดจะต่างกันระหว่างเว็บ โมบาย และการส่งออกบัญชี
แต่ละบรรทัดในใบแจ้งหนี้และยอดรวมควรมีสามจำนวนอย่างชัดเจน: net (ก่อนภาษี), tax, และ gross (net + tax) เลือกอย่างใดอย่างหนึ่งเป็นแหล่งความจริงที่เก็บไว้แล้วคำนวณอีกสองค่าแบบเดียวกันทุกที่ หลายทีมเก็บ net และ tax แล้วคำนวณ gross เป็น net + tax เพราะช่วยการตรวจสอบและการคืนเงินได้สะดวก
ระบุอย่างชัดเจนว่าสำหรับแต่ละจำนวนเป็นสกุลเงินใด ทีมมักสับสนระหว่างสามแนวคิด:
- สกุลเงินบนใบแจ้งหนี้: สกุลที่พิมพ์บนใบและใช้คำนวณยอดรวมทางกฎหมาย
- สกุลที่แสดงต่อผู้ใช้: สิ่งที่แสดงใน UI ขณะดูแผน
- สกุลที่ตัดชำระจริง: สกุลที่ผู้ให้บริการชำระเงินโอนเข้า
สิ่งเหล่านี้อาจเหมือนกันหรือไม่ก็ได้ หากใบแจ้งหนี้เป็น EUR แต่บัตรชำระเข้าบัญชี USD ใบแจ้งหนี้ต้องยังคงสอดคล้องเป็น EUR แม้การฝากธนาคารจะแตกต่าง
ถัดมา ให้ถือว่าค่าเงินเป็นจำนวนเต็มในหน่วยย่อย (เช่น เซนต์) การเก็บ 9.99 เป็นทศนิยมเป็นสาเหตุของปัญหา 9.989999 ต่อมา โดยเฉพาะเมื่อเพิ่มภาษี ส่วนลด ปรอชัน หรือหลายรายการ เก็บเป็น 999 (เซนต์) พร้อมรหัสสกุล แล้วจัดรูปแบบเมื่อแสดงผลเท่านั้น
ท้ายสุด ตัดสินโหมดราคาที่รวมภาษีหรือไม่:
- ราคาไม่รวมภาษี (Tax-exclusive): ราคาคือ net แล้วบวกภาษี
- ราคารวมภาษี (Tax-inclusive): ราคามีภาษีแล้ว ต้องดึงภาษีกลับออกเพื่อหา net
เช็คนิยาม: แผนที่แสดง 10.00 (รวมภาษี 20%) ควรสร้าง gross ที่เก็บในหน่วยย่อยเท่ากันบนเว็บและโมบาย แล้วคำนวณ net และ tax ด้วยกฎเดียวกัน
อัตราแลกเปลี่ยน: จุดที่ต่างกันมักเริ่มต้น
ความต่างของ FX มักเริ่มก่อนกฎภาษีและการปัด สองระบบอาจทั้ง "ถูก" แต่ยังต่างกันเพราะใช้แหล่งต่าง เวลาอ้างอิงต่าง หรือความแม่นยำต่างกัน
ผู้ให้บริการอัตราแลกเปลี่ยนไม่ค่อยเหมือนกัน บางรายให้ mid-market บางรายมี spread บางรายอัพเดตทุกนาที บางรายชั่วโมงหรือรายวัน แม้จากผู้ให้บริการเดียวกัน ระบบหนึ่งอาจปัดอัตราที่ 4 ตำแหน่งทศนิยม ในขณะที่อีกระบบเก็บ 8+ ตำแหน่ง ซึ่งเปลี่ยนยอดเมื่อคุณคูณจำนวนเงินและภาษี
การตัดสินใจที่สำคัญคือ timestamp ของอัตรา: หากคุณคิดเงินเป็น EUR แต่ลูกค้าจ่ายเป็น USD คุณล็อกอัตราเมื่อออกใบแจ้งหนี้หรือเมื่อจับการชำระเงินดี? ทั้งสองแนวทางใช้ได้ แต่การผสมกันระหว่างเว็บ โมบาย และการส่งออกบัญชีรับประกันความไม่ตรงกัน
เมื่อเลือกแล้ว ให้เก็บอัตราที่ใช้ไว้บนใบแจ้งหนี้ อย่าคำนวณใหม่จากอัตรา "ปัจจุบัน" แม้จะสามารถดูอัตราย้อนหลังได้ การแก้ไขของผู้ให้บริการ ความต่างโซนเวลา และการเปลี่ยนความแม่นยำเล็กน้อยจะทำให้ใบแจ้งหนี้เก่าเบี่ยงเมื่อส่งออกหรือสร้าง PDF ใหม่
ตัวอย่างง่าย: คุณออกใบแจ้งหนี้เวลา 23:59 แต่การชำระเงินสำเร็จเวลา 00:02 เวลาทั้งสองมักตกบน "วัน" ของผู้ให้บริการคนละวัน ตารางรายวันอาจให้ตัวเลขต่างกัน
ตัดสินและบันทึกรายละเอียด FX เหล่านี้:
- ผู้ให้บริการและประเภทการอ้างอิง (mid vs รวมค่าธรรมเนียม)
- กฎ timestamp (เวลาที่ออก vs เวลาที่จับการชำระ) และโซนเวลา
- ความแม่นยำที่เก็บสำหรับอัตรา (เช่น 8 ตำแหน่ง)
- จุดที่ปัด (ปัดจำนวนแต่ละบรรทัดที่แปลงแล้วหรือปัดเฉพาะยอดรวมสุดท้าย)
เคสพิเศษที่ต้องจัดการล่วงหน้า: สกุลเงินที่ไม่มีหน่วยย่อย (เช่น JPY), อัตราที่มีความแม่นยำสูงมาก, และการคืนเงิน โดยทั่วไปการคืนเงินควรใช้ค่าอัตรา FX ที่เก็บไว้จากใบแจ้งหนี้ต้นฉบับ มิฉะนั้นจำนวนคืนอาจต่างจากที่ลูกค้าคาดและการส่งออกบัญชีจะแตกต่าง
โมเดลข้อมูลใบแจ้งหนี้ขนาดเล็กที่ยังใช้ได้
ถ้าต้องการให้ใบแจ้งหนี้ตรงกันบนเว็บ โมบาย และการส่งออกบัญชี โมเดลข้อมูลต้องเก็บผลลัพธ์ ไม่ใช่แค่ข้อมูลนำเข้า เป้าหมายเรียบง่าย: ใบแจ้งหนี้เดียวกันควรเรนเดอร์หน่วยย่อยเดียวกันทุกที่ แม้หลายเดือนหลัง
ชุดเอนทิตีเล็กๆ มักพอเพียง:
- 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
กฎสำคัญ: ฟิลด์จำนวนเงินต้องเป็นจำนวนเต็มในหน่วยย่อย เก็บทั้ง unit price และผลลัพธ์บรรทัดที่คำนวณแล้ว นั่นป้องกันการคำนวณใหม่ด้วยกฎการปัดหรือแหล่ง FX ต่างกันภายหลัง
FX ต้องถูกจับลงบนใบแจ้งหนี้ ไม่ใช่อิงจากการคาดเดา แม้คุณจะเก็บตาราง FX ร่วมกัน ใบแจ้งหนี้ควรเก็บ fx_rate_value ที่ใช้ในเวลาสรุป (และแหล่งที่มา) เพื่อให้การส่งออกทำซ้ำตัวเลขได้
คุณต้องมีตารางแยกสำหรับรายละเอียดภาษีเมื่อใบแจ้งหนี้สามารถมีหลายอัตราหรือเขตอำนาจ (เช่น สินค้าผสม EU VAT + ค่าธรรมเนียมท้องถิ่น หรือการเปลี่ยนอัตราตามที่อยู่) ในกรณีนั้นเก็บแถวต่ออัตราภาษีพร้อม taxable_base_minor และ tax_amount_minor
สุดท้าย ถือว่าใบแจ้งหนี้ที่สรุปแล้วเป็นค่าคงที่ บันทึก snapshot ของค่าที่คำนวณเมื่อมันเป็น final และอย่าคำนวณยอดจาก subscription ภายหลัง การเลือกนี้จะขจัดบั๊กส่วนใหญ่ที่ถามว่า "ทำไมสตางค์เปลี่ยน?"
เลือกกลยุทธ์การปัดและปฏิบัติตามอย่างเคร่งครัด
การปัดไม่ใช่รายละเอียดทางคณิตศาสตร์ มันคือกฎผลิตภัณฑ์ ถ้าเว็บของคุณปัดแบบหนึ่ง โมบายอีกแบบ และการส่งออกบัญชีอีกแบบ คุณจะได้ยอดต่างกันแม้ข้อมูลนำเข้าจะเหมือนกัน
มีกลยุทธ์สามแบบหลัก และต่างกันที่จุดที่คุณ "ล็อก" หน่วยย่อย:
- ปัดต่อหน่วย: ปัดราคาต่อหน่วยแต่ละหน่วย (หลังส่วนลด FX ภาษี) ก่อนคูณด้วยปริมาณ
- ปัดต่อบรรทัด: คำนวณยอดบรรทัดเต็ม แล้วปัดครั้งเดียวต่อบรรทัด
- ปัดบนยอดรวมใบแจ้งหนี้: เก็บความแม่นยำเต็มสำหรับทุกบรรทัด แล้วปัดเฉพาะยอดรวมสุดท้าย
สำหรบการสมัครสมาชิก ค่าเริ่มต้นที่ดีคือปัดต่อบรรทัด เพราะคาดเดาง่ายสำหรับลูกค้า (แต่ละบรรทัดดูถูกต้อง), ตรวจสอบได้ง่าย (อธิบายยอดบรรทัดได้) และเสถียรเมื่อมีการต่ออายุ ปัดต่อหน่วยอาจเบี่ยงเมื่อปริมาณเปลี่ยน หรือเมื่อคุณแสดงราคาต่อหน่วยใน UI การปัดเฉพาะยอดรวมมักทำให้เกิดคำถาม "ทำไมบรรทัดรวมแล้วไม่ตรง?" เพราะผลรวมบรรทัดที่มองเห็นอาจไม่ตรงกับยอดที่แสดง
ปัญหาเพนนีคลาสสิกเกิดเมื่อมีหลายรายการเล็กๆ หรือภาษีเป็นเศษส่วน ตัวอย่าง: 20 บรรทัดแต่ละบรรทัดมีเศษปัด 0.004 ปัดต่อบรรทัดอาจกลายเป็น 0.08 ต่างจากการปัดทีเดียวตอนท้าย เมื่อมีการแปลงสกุลเงิน เศษเล็กๆ เหล่านี้เกิดบ่อยขึ้นและสะสมในรายงานรายได้และการส่งออก
ไม่ว่าจะเลือกแบบไหน ให้ทำให้เป็นตายตัว อินพุตชุดเดียวต้องให้ผลลัพธ์ชุดเดียวบนเว็บ โมบาย และการส่งออก:
- ใช้กฎความแม่นยำเดียวกัน (หน่วยย่อยของสกุลเงิน, ทศนิยมที่ใช้กับภาษี, ความแม่นยำของ FX)
- ปัดในขั้นตอนเดียวกัน (หน่วย, บรรทัด, หรือยอดรวม) และตามลำดับเดียวกัน
- ใช้กฎผูกคอเมื่อเท่ากันอย่างเดียวกัน (เช่น half-up vs half-even)
ถ้าคุณมีทั้งเว็บและโมบายที่ทำ flow บิล ให้จดกฎการปัดเป็นสเปกที่ทดสอบได้ ไม่ใช่พฤติกรรม UI
ขั้นตอนทีละขั้น: คำนวณยอดรวมให้ทำซ้ำได้
เพื่อให้เลขเดียวกันบนเว็บ โมบาย และการส่งออกบัญชี ให้มองการคำนวณเหมือนสูตรอาหาร แนวคิดสำคัญ: คำนวณด้วยความแม่นยำสูง แต่เก็บและรวมเฉพาะจำนวนเต็มในสกุลเงินของใบแจ้งหนี้
สูตรที่ทำซ้ำได้
-
เริ่มจากยอด net ของแต่ละบรรทัดด้วยความแม่นยำสูง เก็บทศนิยมเพิ่มเติมขณะคูณปริมาณ ใช้ส่วนลด และ (ถ้าจำเป็น) แปลงสกุลเงิน แล้วปัดครั้งเดียวเป็นหน่วยย่อยของสกุลเงินบนใบแจ้งหนี้โดยใช้กฎที่เลือก เก็บจำนวนเต็มนั้นเป็น line net
-
คำนวณภาษีจาก line net ที่เก็บไว้ (หรือจากยอดรวมกลุ่มภาษีถ้ากฎอนุญาตการรวมกลุ่ม) ใช้กฎปัดเดียวกันและเก็บภาษีเป็นจำนวนเต็มในหน่วยย่อย นี่คือจุดที่ระบบมักเบี่ยง: ฝั่งหนึ่งปัดก่อนภาษี อีกฝั่งปัดหลัง
-
คำนวณ gross ของแต่ละบรรทัดเป็น (net ที่เก็บ + tax ที่เก็บ) ยอดรวมของใบแจ้งหนี้คือผลบวกของหน่วยย่อยที่เก็บไว้ อย่าคำนวณยอดรวมจากค่าทศนิยมเพื่อแสดงผล การแสดงผลและการส่งออกควรอ่านจำนวนเต็มที่เก็บไว้แล้วจัดรูปแบบ
-
หากกฎท้องถิ่นต้องการยอดภาษีระดับใบแจ้งหนี้ คุณอาจต้องแจกจ่ายเศษที่เหลือ ตัวอย่าง: สามบรรทัดมีภาษี 0.01 แต่การปัดระดับใบแจ้งหนี้บอกว่าต้องเป็น 0.02 ตัดสิน tie-break ดีเทอร์มินิสติก (เช่น เพิ่มหรือลด 1 หน่วยย่อยโดยเริ่มจากบรรทัดที่มีฐานที่ใหญ่สุด แล้วเรียงตาม id บรรทัดอย่างเสถียร) เก็บการปรับนั้นเป็นการแก้ไขภาษีเล็กน้อยบนบรรทัดที่ได้รับผลกระทบเพื่อให้ทุกระบบทำซ้ำได้
-
ล็อกใบแจ้งหนี้ หลังการปัดและการแจกจ่ายเศษแล้ว ให้ถือว่าใบแจ้งหนี้เป็น immutable หากราคาสมัครเปลี่ยนหลัง สร้างใบแจ้งหนี้ใหม่หรือเครดิต โน้ต อย่าแก้ตัวเลขเก่า นโยบายเดียวนี้ขจัดบั๊กส่วนใหญ่
เช็คนิยาม: หากแผน EUR 9.99 มี VAT 19% ยอดที่เก็บไว้อาจเป็น net 999 เซนต์, tax 190 เซนต์, gross 1189 เซนต์ ลูกค้าทุกช่องทางควรเรนเดอร์ 11.89 EUR จากจำนวนเต็มเหล่านั้น ไม่ใช่คำนวณ VAT ใหม่บนฝั่งไคลเอนต์
การปัดภาษี: ต่อบรรทัด vs ต่อใบแจ้งหนี้ และการจัดสรรเศษ
การปัดภาษีคือจุดที่คณิตศาสตร์ถูกต้องกลายเป็นใบแจ้งหนี้ที่ไม่ตรงกัน ประเด็นหลักคือ: การปัดก่อนจะเปลี่ยนผลรวมสุดท้าย
ถ้าปัดภาษีต่อบรรทัดแล้วรวม อาจได้ยอดต่างจากการรวมภาษีที่ยังไม่ปัดแล้วปัดทีเดียวท้ายใบแจ้งหนี้ เมื่อมีหลายบรรทัด ช่องว่างจะเพิ่มขึ้น โดยเฉพาะเมื่อหน่วยย่อยและการแปลงสกุลเงินสร้างเศษ
ตัวอย่าง (2 ทศนิยม): สองบรรทัดแต่ละบรรทัดมีฐาน taxable 0.05 และภาษี 10% ภาษีไม่ปัดต่อบรรทัดคือ 0.005 ถ้าปัดต่อบรรทัด แต่ละอันกลายเป็น 0.01 ดังนั้นภาษีรวมเป็น 0.02 แต่ถ้าปัดที่ระดับใบแจ้งหนี้ ยอด taxable รวมเป็น 0.10 ภาษีคือ 0.01 ทั้งสองแบบมีเหตุผลต่างกัน แต่ไม่ตรงกัน
เมื่อต้องแสดงภาษีต่อบรรทัดแต่ยังอยากให้ยอดรวมตรง ให้จัดสรรเศษที่เหลือแบบกำหนดได้:
- คำนวณภาษีไม่ปัดต่อบรรทัด แล้วปัดลงสำหรับแต่ละบรรทัด
- รวมภาษีที่ปัดลงแล้วเปรียบเทียบกับเป้าหมายภาษีระดับใบแจ้งหนี้
- แจกจ่ายหน่วยย่อยที่เหลือให้บรรทัดที่มีเศษทศนิยมมากที่สุดก่อน
- แก้เสมอกันด้วยกฎที่คงที่ (เช่น ตำแหน่งบรรทัด แล้ว id บรรทัดที่แน่นอน)
- อย่าเปลี่ยนการจัดสรรตามการเรียงลำดับใน UI
การส่งออกอาจยังเบี่ยงเมื่อกลุ่มบรรทัดโดยบัญชี (ตามสินค้า อัตราภาษี หรือเขตอำนาจ) เพื่อให้การส่งออกตรงกับยอดใบแจ้งหนี้ ให้จัดสรรเศษภายในแต่ละกลุ่มที่ต้องการก่อน แล้วตรวจสอบว่าผลรวมของกลุ่มตรงกับภาษีและยอดรวมของใบแจ้งหนี้
หากบัญชีต้องการแยกภาษีตามอัตราหรือเขตอำนาจแต่ UI แสดงตัวเลขเดียว ให้เก็บรายละเอียดการแบ่งไว้ (ยอดต่ออัตราหรือเขต + กฎการจัดสรรที่ตรวจสอบได้) UI สามารถแสดงยอดรวมเดียว ในขณะที่การส่งออกมีถังรายละเอียดโดยไม่เปลี่ยนยอดรวม
กรณีมุมที่ควรกำหนดล่วงหน้า
ความไม่ตรงกันส่วนใหญ่เกิดในมุมที่เฉพาะ ให้ตัดสินกฎล่วงหน้าแล้วจะไม่เป็นเรื่องน่าประหลาดใจ
สกุลเงินที่ไม่มีหน่วยย่อยต้องระมัดระวัง JPY และ KRW ไม่มีหน่วยย่อย ดังนั้นทุกขั้นตอนที่สมมติว่า "เซนต์" จะสร้างความต่าง ตัดสินว่าจะปัดต่อบรรทัด ระดับภาษี หรือเฉพาะยอดรวม และให้ทุกไคลเอนต์ใช้การตั้งค่าสกุลเงินเดียวกัน
VAT ข้ามพรมแดนสามารถเปลี่ยนอัตราตามที่อยู่ลูกค้าและหลักฐานที่ยอมรับ (ที่อยู่การออกบิล, IP, หมายเลขภาษี) ส่วนยากไม่ใช่อัตรา แต่เป็นเวลาที่ล็อกมัน ให้เลือกระยะเวลาจุดที่ตัดสิน (checkout, issue date, หรือ service period start) และยึดตามนั้น
ปรอชันคือที่ที่เศษทวีคูณ การอัปเกรดกลางรอบอาจให้จำนวนเช่น 9.3333... ต่อวัน ตัดสินว่าจะปรอชันจาก net ก่อน gross ก่อน หรือฐานบริการก่อน เพราะการเปลี่ยนลำดับจะเปลี่ยนหน่วยย่อยสุดท้าย
จดกฎเหล่านี้เพื่อไม่ให้เปลี่ยนไปตามเวลา:
- การจัดการสกุลเงินไม่มีหน่วยย่อย: วิธีการปัดและจุดที่ใช้
- ภาษีตามที่อยู่: ฟิลด์ไหนตัดสินอัตราและเวลาที่ถูกล็อก
- คณิตศาสตร์การปรอชัน: ฐาน (วัน วินาที หรือเดือน 30 วัน) และจุดปัด
- ส่วนลดและเครดิต: ลดฐานภาษีหรือไม่ และใช้ต่อบรรทัดหรือระดับใบแจ้งหนี้
- ความสมมาตรของการคืนเงิน: การคืนเงินควรสร้างยอดบรรทัดและภาษีเดิม ไม่ใช่คำนวณใหม่ด้วยอัตราปัจจุบัน
การคืนเงินเป็นกับดักสุดท้าย หากใบแจ้งหนี้ต้นฉบับมีเศษ 0.01 จัดสรรไปยังบรรทัดหนึ่ง การคืนเงินควรย้อนการจัดสรรนั้นอย่างแม่นยำ มิฉะนั้นลูกค้าจะเห็นยอดหนึ่งและบัญชีของคุณเห็นอีกยอดหนึ่ง
ข้อผิดพลาดทั่วไปที่ทำให้ใบแจ้งหนี้ไม่ตรงกัน
ส่วนใหญ่ไม่ได้มาจาก "คณิตศาสตร์ยาก" แต่มาจากการเลือกเล็กๆ ที่ไม่สอดคล้องกันในส่วนต่างๆ ของสแต็ก
ปัญหาใหญ่คือการเก็บเงินเป็นจำนวนจริง (float) ค่าอย่าง 19.99 ไม่สามารถแทนค่าได้อย่างแม่นยำในหลายระบบ ความผิดพลาดเล็กๆ สะสมเมื่อรวมบรรทัดหรือคำนวณภาษี เก็บเป็นจำนวนเต็มในหน่วยย่อยพร้อมรหัสสกุลและสเกลหน่วยย่อย
ปัญหาอีกอย่างคือตัวที่คำนวณ FX ตอนส่งออก ลูกค้าจ่ายตามอัตราหนึ่งในเวลาหนึ่ง หากการส่งออกดึง "อัตราเมื่อวันนี้" คุณอาจได้ยอดต่าง แม้ว่าทุกขั้นตอนจะถูกต้อง ให้ถือว่าใบแจ้งหนี้เป็น snapshot: เก็บอัตรา FX ที่ใช้ จำนวนเงินที่แปลงแล้ว และผลการปัด
การปัดต่างกันยังเกิดเมื่อ UI และ backend ปัดในขั้นตอนต่างกัน เช่น backend ปัดภาษีต่อบรรทัด ในขณะที่เว็บ UI ปัดเฉพาะยอดรวม ทั้งสองดูสมเหตุสมผล แต่จะไม่ตรง
ห้าเรื่องที่พบบ่อยที่สุด:
- ใช้ float สำหรับราคา ภาษี ส่วนลด หรือการแปลง FX
- คำนวณ FX ใหม่ตอนส่งออกแทนการใช้ค่าที่เก็บไว้ในใบแจ้งหนี้
- ปัดภาษีต่างกันใน UI และในงาน backend
- ผสมตรรกะราคาที่รวมภาษีและไม่รวมภายในใบแจ้งหนี้เดียวกัน
- ให้โมบายทำคณิตศาสตร์ของตัวเองแทนเรนเดอร์ยอดที่ backend เก็บไว้
เช็คความเป็นจริงง่ายๆ: แอปโมบายแสดงสามรายการที่ EUR 9.99 พร้อมภาษี 20% ถ้าแอปปัดภาษีท้ายแต่ backend ปัดต่อบรรทัด คุณอาจต่าง EUR 0.01 เซ็นต์เดียวก็พอทำให้การกระทบยอดแตกและเกิดบิลซัพพอร์ต
การแก้ไขที่ง่ายที่สุดคือ: คำนวณครั้งเดียวบน backend เก็บ snapshot ใบแจ้งหนี้อย่างครบถ้วน แล้วให้เว็บและโมบายเรนเดอร์ตัวเลขที่เก็บไว้เท่านั้น
เช็คลิสต์ด่วนสำหรับใบแจ้งหนี้ที่สอดคล้อง
เมื่อเลขต่างกันระหว่างเว็บ โมบาย และการส่งออกบัญชี มักไม่ใช่ปัญหาคณิตศาสตร์ แต่เป็นปัญหาการเก็บและการปัด เริ่มจากหลักการว่าไคลเอนต์ควรแสดงสิ่งที่ใบแจ้งหนี้เก็บไว้ ไม่ใช่คำนวณใหม่ Backend ควรเป็นแหล่งความจริงเดียวและทุกช่องทางอ่านค่าจากจำนวนเต็มที่เก็บไว้เดียวกัน
ห้าเช็คที่ป้องกันความไม่ตรงกันได้มาก
- เก็บยอดแต่ละบรรทัดเป็นหน่วยย่อย และทำให้ยอดรวมของใบแจ้งหนี้เท่ากับผลบวกของหน่วยย่อยที่เก็บไว้ อย่าให้เว็บหรือโมบายคำนวณยอดรวมจากราคาต่อหน่วย
- บันทึกอัตรา FX ที่ใช้ (และเวลาที่ใช้) หากต้องสร้างใบแจ้งหนี้ใหม่ ให้ใช้ค่าอัตราที่เก็บไว้เสมอ
- จดกฎการปัดเดียว (จะปัดที่ไหนและอย่างไร) แล้วบังคับใช้ทุกที่: การคำนวณ backend การเรนเดอร์โมบาย การสร้าง PDF และการส่งออกบัญชี
- ทำให้การกระทบยอดภาษีพิสูจน์ได้: ผลรวมของยอดภาษีที่เก็บไว้ (ต่อบรรทัดหรือระดับใบแจ้งหนี้ตามที่เลือก) ต้องเท่ากับยอดภาษีของใบแจ้งหนี้ทุกครั้ง
- ทดสอบการเดินทางกลับ: ส่งออกข้อมูลใบแจ้งหนี้ แล้วนำเข้ากลับ เพื่อตรวจสอบว่ายอดและภาษีตรงกันอย่างเป๊ะ รวมทั้งหน่วยย่อยด้วย
อย่าลืมการคืนเงินและเครดิต
การคืนเงินและเครดิตควรสะท้อนผลการปัดของใบแจ้งหนี้ต้นฉบับ หากใบแจ้งหนี้ต้นฉบับปัดภาษีต่อบรรทัด การคืนเงินควรทำแบบเดียวกันโดยใช้ความแม่นยำของสกุลและอัตรา FX ที่เก็บไว้ มิฉะนั้นเศษเล็กๆ จะปรากฏและสะสม
วิธีปฏิบัติที่ใช้ได้จริงคือเก็บ snapshot การคำนวณที่ชัดเจนกับทุกใบแจ้งหนี้: สกุลเงิน ความแม่นยำของหน่วยย่อย โหมดการปัด อัตรา FX และ timestamp และยอดบรรทัดที่สรุปแล้ว
ตัวอย่าง: ใบแจ้งหนี้หนึ่งฉบับที่ตรงกันบนเว็บ โมบาย และการส่งออก
นี่คือตัวอย่างใบแจ้งหนี้ที่คงที่ทุกที่
สมมติว่าใบแจ้งหนี้ออกเป็น EUR (2 ทศนิยม), VAT 20%, และลูกค้าถูกเรียกเก็บเป็น USD ฝั่ง backend เก็บ snapshot ของ FX: 1 EUR = 1.0857 USD
| Item | Net (EUR) |
|---|---|
| Pro plan (monthly) | 19.99 |
| Extra seats | 10.00 |
| Discount (10% of 29.99, rounded) | -3.00 |
Net total (EUR) = 26.99
VAT 20% (EUR) = 5.40 (เพราะ 26.99 x 0.20 = 5.398 ปัดเป็น 5.40)
Gross total (EUR) = 32.39
ตอนนี้ backend แปลงยอดสกุลชาร์จจากยอด EUR ที่เก็บไว้และ 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
ถ้าคุณเก็บยอดต่อบรรทัดใน USD ด้วย คุณมักจะเจอความต่าง 0.01 เมื่อปัดแต่ละบรรทัดแล้วรวม นั่นคือจุดที่ใบแจ้งหนี้มักเบี่ยง
ทำให้เป็นดีเทอร์มินิสติก: แปลงและปัดแต่ละบรรทัดแล้วแจกจ่ายเซนต์ที่เหลือ (บวกหรือลบ) ตามลำดับคงที่ (เช่น ตาม line_id เพิ่มขึ้น) จนกว่ายอดรวมต่อบรรทัดจะเท่ากับยอดรวม Gross USD ที่ถูกล็อกไว้
เว็บและโมบายควรแสดงยอดบรรทัด ยอดภาษี อัตรา FX และยอดรวมที่ backend เก็บไว้ ไม่ใช่คำนวณใหม่ การส่งออกบัญชีควรส่งตัวเลขที่เก็บไว้พร้อม snapshot ของ FX (อัตรา, เวลา หรือแหล่ง) เพื่อให้บัญชีตรงกับที่ลูกค้าเห็น
ขั้นตอนปฏิบัติถัดไปคือทำให้การคำนวณเป็นบริการร่วมที่ออก snapshot ใบแจ้งหนี้เดียว (บรรทัด ภาษี ยอดรวม FX การปรับปัด) แล้วทุกช่องทางอ่านจาก snapshot นั้น หากคุณกำลังสร้าง flow เหล่านี้บน Koder.ai (koder.ai) การเก็บโมเดล snapshot นี้ไว้ตรงกลางช่วยให้เว็บ โมบาย และการส่งออกสอดคล้องเพราะทุกคนอ่านค่าจากแหล่งเดียวกัน
คำถามที่พบบ่อย
ทำไมการสมัครสมาชิกเดียวกันจึงแสดงยอดต่างกันบนเว็บ โมบาย และการส่งออกบัญชี?
เพราะแต่ละระบบมักเลือกช่วงเวลาที่จะปัด ผลอะไรว่าจะปัด (เช่น ก่อน/หลังภาษี) และความแม่นยำของตัวเลขต่างกัน ความแตกต่างเล็กๆ เหล่านี้จะกลายเป็นช่องว่าง 0.01–0.02 ยูโรโดยเฉพาะเมื่อมีการปรอชัน เครดิต หรือการพยายามเรียกเก็บซ้ำหลายครั้ง
ควรเก็บจำนวนเงินเป็น float หรือ integer?
เก็บจำนวนเงินเป็นจำนวนเต็มในหน่วยย่อย (เช่น เซนต์) พร้อมรหัสสกุลเงิน แล้วค่อยจัดรูปแบบสำหรับการแสดงผล ค่าแบบจำนวนจริง (float) ไม่สามารถแทนค่าทศนิยมหลายหลักได้อย่างแม่นยำ จึงเกิดข้อผิดพลาดเล็กๆ เมื่อรวมรายการหรือคำนวณภาษี
ใบแจ้งหนี้ควรเก็บยอดไหนบ้าง: net, tax หรือ gross?
เลือกหนึ่งค่าเป็นแหล่งความจริงที่เก็บไว้แล้วใช้คำนวณค่าอื่นๆ ให้ทั่ว ระบบส่วนใหญ่เก็บ net และ tax เป็นหน่วยย่อยแล้วคำนวณ gross = net + tax เพราะช่วยเรื่องการคืนเงินและการตรวจสอบบัญชีได้ง่าย
ต่างกันอย่างไรระหว่างสกุลเงินบนใบแจ้งหนี้ สกุลที่แสดง และสกุลที่ชำระ?
สกุลเงินบนใบแจ้งหนี้คือสกุลที่ระบุเป็นทางการบนใบและใช้ในการกระทบยอด ตัวอย่างเช่น สกุลเพื่อแสดงผลคือสิ่งที่ผู้ใช้เห็นขณะดูแผน ส่วนสกุลที่ชำระจริงคือสกุลที่ผู้ให้บริการชำระเงินโอนเข้า บางครั้งทั้งสามแบบต่างกันได้ แต่ใบแจ้งหนี้ต้องสอดคล้องภายในสกุลเงินของมันเอง
จะป้องกันไม่ให้อัตรา FX เปลี่ยนแปลงใบแจ้งหนี้เก่าได้อย่างไร?
อย่าดึงอัตราใหม่ตอนส่งออกหรือสร้าง PDF ใหม่ เก็บอัตรา FX ที่ใช้บนใบแจ้งหนี้ (ค่า ความแม่นยำ ผู้ให้บริการ และเวลาที่มีผล) แล้วใช้ซ้ำเสมอ เพื่อให้ใบแจ้งหนี้เก่าทำซ้ำตัวเลขเดิมได้เป็นเดือนๆ
ควรล็อกอัตรา FX ตอนออกใบแจ้งหนี้หรือเมื่อตัดบัตร?
ให้เลือกกฎเดียว: "ล็อกอัตราที่เวลาที่ออกใบแจ้งหนี้" หรือ "ล็อกอัตราตอนที่ชำระเงินถูกจับ" แล้วใช้กฎเดียวกันทุกที่ การผสมเวลาทำให้เกิดความไม่ตรงกันได้ง่ายโดยเฉพาะรอบเที่ยงคืนหรือข้ามโซนเวลา
กฎการปัดแบบไหนปลอดภัยที่สุดสำหรับการสมัครสมาชิก?
โดยทั่วไป แนะนำให้ปัด ต่อบรรทัด สำหรับการสมัครสมาชิก แล้วเอาผลรวมของบรรทัดที่บันทึกไว้เป็นยอดรวม วิธีนี้อธิบายง่าย ลูกค้าเห็นยอดแต่ละบรรทัดที่สมเหตุสมผล และเสถียรกว่าหากทุกช่องทางใช้กฎเดียวกัน
จัดการการปัดภาษีอย่างไรเพื่อหลีกเลี่ยงยอดไม่ตรงกัน?
เลือกชัดเจนว่าจะปัดภายใต้กฎ per-line หรือ per-invoice แล้วทำให้เป็นไปตามกฎนั้นอย่างเด็ดขาด หากต้องการให้ยอดรวมตรงกับค่าที่ปัดเป็นระดับใบแจ้งหนี้ ให้จัดสรรเศษส่วนที่เหลืออย่างกำหนดได้และเก็บผลลัพธ์ของการจัดสรรนั้นเป็นยอดภาษีต่อบรรทัดในหน่วยย่อย
ทำไมการปรอชันจึงทำให้เกิดความต่างของสตางค์ และจะแก้อย่างไร?
การปรอชันสร้างทศนิยมซ้ำ (เช่น ต่อวัน) ดังนั้นลำดับการคำนวณจึงสำคัญ เลือกวิธีเดียว (เช่น ปรอชัน net ก่อน แล้วคำนวณภาษีจาก net ที่เก็บไว้) ปัดในจุดที่ตกลงกัน แล้วเก็บยอดบรรทัดที่สิ้นสุดเพื่อให้การอัปเกรด/คืนเงินสะท้อนคำนวณเดิม
สถาปัตยกรรมที่ง่ายที่สุดเพื่อให้ใบแจ้งหนี้ตรงกันบนทุกไคลเอนต์คืออะไร?
ให้ backend สร้าง snapshot ใบแจ้งหนี้ที่สิ้นสุด (บรรทัด ภาษี ยอดรวม กฎปัดสกุลเงิน FX) แล้วถือเป็นข้อมูลไม่เปลี่ยนแปลงเมื่อสิ้นสุดการออก จากนั้นเว็บ โมบาย PDF และการส่งออกอ่านค่าจาก snapshot เหล่านั้นแทนการคำนวณใหม่ ซึ่งเป็นแนวทางที่ดีเมื่อสร้าง flow บน Koder.ai