Penagihan langganan multi-mata-uang: pendekatan praktis pembulatan dan model tabel minimal untuk menjaga total konsisten di web, mobile, dan ekspor akuntansi.

Satu masalah umum: checkout di web menunjukkan total tertentu, aplikasi mobile menunjukkan total yang sedikit berbeda, dan ekspor akuntansi menghasilkan angka ketiga. Setiap sistem melakukan perhitungan yang “masuk akal,” tapi tidak sama.
Langganan memperburuk ini karena Anda mengulangi perhitungan berulang kali. Perbedaan kecil menumpuk di sepanjang pembaruan, proration ketika seseorang upgrade di tengah siklus, kredit dan pengembalian dana, biaya ulang setelah pembayaran gagal, dan periode parsial di awal atau akhir paket.
Penyimpangan biasanya dimulai dari pilihan-pilihan kecil yang tak terlihat sampai suatu saat terlihat: kapan melakukan pembulatan (per baris atau di akhir), basis pajak mana yang dipakai (net vs gross), bagaimana menangani mata uang dengan 0 atau 3 unit minor desimal, dan kurs FX mana yang diterapkan (timestamp mana, sumber mana, presisi berapa). Jika web membulatkan ke 2 desimal per baris dan mobile hanya membulatkan total akhir, Anda bisa mendapat perbedaan 0,01 meskipun inputnya sama.
Tujuannya membosankan tapi penting: faktur yang sama harus menghasilkan total yang sama di mana pun, setiap saat. Itu menenangkan pelanggan, mengurangi tiket dukungan, dan tahan dalam audit.
"Konsisten" berarti untuk sebuah invoice ID dan versinya:
Contoh: pelanggan upgrade dari EUR 19.99 ke EUR 29.99 di tengah bulan, mendapat biaya prorata, lalu kredit kecil karena downtime. Jika satu sistem membulatkan setiap baris prorata dan sistem lain hanya membulatkan total akhir, ekspor faktur bisa berbeda dari yang dilihat pelanggan, meskipun setiap angka terlihat "cukup dekat."
Sebelum berdebat soal kurs FX atau aturan pembulatan pajak, kunci dulu hal-hal dasar. Jika yang ini kabur, faktur akan menyimpang antara web, mobile, dan ekspor akuntansi.
Setiap baris faktur dan total faktur harus jelas membawa tiga jumlah: net (sebelum pajak), pajak, dan gross (net + pajak). Pilih salah satu sebagai sumber kebenaran untuk penyimpanan dan perhitungan, lalu turunkan yang lain dengan cara yang sama di mana pun. Banyak tim menyimpan net dan pajak, lalu menghitung gross sebagai net + pajak karena itu mempermudah audit dan pengembalian.
Jelaskan secara eksplisit mata uang untuk setiap angka. Tim sering mencampur tiga konsep berbeda:
Ketiganya bisa sama, tetapi tidak harus. Jika faktur Anda dalam EUR tetapi kartu menyelesaikan ke USD, faktur harus konsisten dalam EUR meskipun setoran bank berbeda.
Selanjutnya, perlakukan uang sebagai bilangan bulat dalam unit minor (misalnya, sen). Menyimpan 9.99 sebagai bilangan floating umum menyebabkan masalah 9.989999 nanti, terutama saat menambah pajak, diskon, proration, atau beberapa item. Simpan 999 (sen) dengan kode mata uang, dan hanya format untuk tampilan.
Terakhir, putuskan mode pajak harga Anda:
Pengecekan konkret: sebuah paket yang ditampilkan sebagai 10.00 (tax-inclusive, 20% VAT) harus menghasilkan gross yang sama dalam unit minor yang disimpan di web dan mobile, lalu menurunkan net dan pajak dengan aturan bersama.
Perbedaan FX sering dimulai sebelum aturan pajak dan pembulatan. Dua sistem bisa sama-sama "benar" tapi masih berbeda karena menggunakan sumber, timestamp, atau presisi yang berbeda.
Penyedia kurs jarang cocok persis. Beberapa mengutip mid-market, yang lain termasuk spread. Beberapa memperbarui setiap menit, yang lain setiap jam atau hari. Bahkan dengan penyedia yang sama, satu sistem mungkin membulatkan kurs ke 4 desimal sementara yang lain mempertahankan 8+ desimal, yang mengubah total setelah Anda mengalikan jumlah langganan dan pajak.
Keputusan terpenting adalah apa arti timestamp kurs Anda. Jika Anda menagih dalam EUR tetapi pelanggan membayar dalam USD, apakah Anda mengunci kurs FX saat faktur diterbitkan, atau saat pembayaran ditangkap? Keduanya umum, tetapi mencampurnya antara web, mobile, dan ekspor akuntansi menjamin ketidaksesuaian.
Setelah memilih aturan, simpan nilai kurs tepat yang Anda gunakan di faktur. Jangan menghitung ulang nanti dari kurs "saat ini", bahkan jika Anda bisa melihat kurs historis. Koreksi penyedia, perbedaan zona waktu, dan perubahan presisi kecil akan membuat faktur lama menyimpang selama ekspor atau saat Anda menghasilkan ulang PDF.
Contoh sederhana: Anda mengeluarkan faktur pada 23:59, tapi pembayaran berhasil pada 00:02. Timestamp itu sering jatuh pada "hari" penyedia yang berbeda, sehingga tabel kurs harian dapat menghasilkan angka berbeda.
Putuskan dan dokumentasikan detail FX ini:
Kasus khusus yang perlu ditangani di depan: mata uang tanpa desimal (seperti JPY), kurs berpresisi sangat tinggi, dan pengembalian dana. Pengembalian dana umumnya harus menggunakan kembali kurs FX yang tersimpan pada faktur asli. Jika tidak, jumlah refund bisa berbeda dari yang pelanggan harapkan dan dari yang ditampilkan ekspor akuntansi.
Jika Anda ingin faktur cocok di web, mobile, dan ekspor akuntansi, model data Anda harus menyimpan hasil, bukan hanya input. Tujuannya sederhana: faktur yang sama harus dirender dengan unit minor yang sama di mana pun, bahkan berbulan-bulan kemudian.
Satu set entitas kecil biasanya cukup:
Aturan kunci: field uang harus integer dalam unit minor. Simpan baik harga satuan maupun total baris yang dihitung. Itu mencegah perhitungan ulang nanti dengan aturan pembulatan yang berbeda atau sumber FX yang berbeda.
FX harus ditangkap pada faktur, bukan diinferensi. Bahkan jika Anda menyimpan tabel FX bersama, faktur harus menyimpan fx_rate_value tepat yang digunakan saat finalisasi (plus dari mana asalnya) sehingga ekspor bisa mereproduksi angka yang sama.
Anda hanya memerlukan tabel tax breakdown terpisah ketika satu faktur dapat memiliki beberapa tarif pajak atau yurisdiksi sekaligus (misalnya, item campuran, EU VAT + pungutan lokal, atau perubahan pajak berbasis alamat dalam satu faktur). Simpan satu baris per tarif pajak dengan taxable_base_minor dan tax_amount_minor.
Terakhir, perlakukan faktur yang sudah difinalisasi sebagai immutable. Simpan snapshot nilai yang dihitung pada saat faktur menjadi final, dan jangan pernah menghitung ulang total dari subscription nanti. Pilihan itu saja menghilangkan sebagian besar bug "kenapa sen berubah?".
Pembulatan bukan detail matematika kecil. Itu aturan produk. Jika web Anda membulatkan satu cara, mobile membulatkan lain, dan ekspor akuntansi membulatkan cara ketiga, Anda akan mendapat total berbeda walau input tampak identik.
Ada tiga strategi umum, dan mereka berbeda pada titik di mana Anda "mengunci" unit minor:
Untuk langganan, default yang baik adalah pembulatan per baris. Ini dapat diprediksi oleh pelanggan (setiap baris terlihat benar), mudah diaudit (Anda bisa menjelaskan setiap total baris), dan stabil antar pembaruan. Pembulatan per unit bisa menyimpang saat kuantitas berubah atau saat Anda menampilkan harga satuan di UI. Pembulatan hanya pada total faktur sering menyebabkan tiket "kenapa baris ini tidak cocok?" karena jumlah baris yang terlihat mungkin tidak cocok dengan total yang ditampilkan.
Masalah penny klasik muncul ketika Anda memiliki banyak item kecil atau pajak fraksional. Contoh: 20 baris masing-masing menghasilkan sisa pembulatan 0.004. Jika dibulatkan per baris, itu bisa menjadi 0.08 perbedaan dibandingkan jika dibulatkan hanya di akhir. Dengan konversi FX, sisa kecil ini muncul lebih sering dan bisa terakumulasi seiring waktu dalam ekspor dan laporan pendapatan.
Apapun yang Anda pilih, buatlah deterministik. Input yang sama harus selalu menghasilkan output yang sama di web, mobile, dan ekspor:
Jika Anda membangun alur penagihan di web dan mobile, tulis aturan pembulatan itu sebagai spesifikasi yang dapat diuji, bukan sebagai perilaku UI.
Untuk menjaga angka sama di web, mobile, dan ekspor akuntansi, perlakukan perhitungan seperti resep. Ide kunci: hitung dengan presisi tinggi, tetapi simpan dan jumlahkan hanya integer dalam mata uang faktur.
Mulai dengan setiap jumlah net baris dalam presisi tinggi. Pertahankan desimal ekstra saat mengalikan kuantitas, menerapkan diskon, dan (jika perlu) mengonversi mata uang. Lalu bulatkan sekali ke unit minor mata uang faktur menggunakan aturan yang dipilih. Simpan integer itu sebagai line net.
Hitung pajak dari line net yang disimpan (atau dari subtotal grup pajak jika aturan Anda memungkinkan pengelompokan per tarif). Terapkan aturan pembulatan yang sama dan simpan pajak sebagai integer dalam unit minor. Di sinilah sistem sering menyimpang: satu sisi membulatkan sebelum pajak, sisi lain membulatkan setelah.
Hitung gross setiap baris sebagai (stored net + stored tax). Total faktur adalah jumlah dari minor yang disimpan. Jangan menghitung ulang total dari nilai floating untuk tampilan. Tampilan dan ekspor harus membaca integer yang disimpan dan memformatnya.
Jika aturan lokal Anda mengharuskan total pajak pada tingkat faktur, Anda mungkin perlu mendistribusikan sisa. Contoh: tiga baris masing-masing pajak 0.01 mungkin jumlahnya 0.03, tetapi pembulatan di tingkat faktur mengatakan 0.02. Putuskan tie-break deterministik (misalnya, tambahkan atau kurangi 1 unit minor dimulai dari baris yang memiliki nilai kena pajak terbesar, lalu urut stabil berdasarkan line id). Simpan penyesuaian sebagai koreksi pajak kecil pada baris yang terkena sehingga setiap sistem dapat mereproduksinya.
Kunci faktur. Setelah pembulatan akhir dan distribusi sisa, perlakukan faktur sebagai immutable. Jika harga subscription berubah nanti, buat faktur baru atau nota kredit, tapi jangan menulis ulang angka lama.
Pengecekan konkret: jika paket EUR 9.99 punya VAT 19%, net yang disimpan mungkin 999 sen, pajak 190 sen, gross 1189 sen. Setiap klien harus menampilkan 11.89 EUR dari integer yang disimpan itu, bukan dengan menghitung ulang VAT secara dinamis.
Pembulatan pajak adalah tempat matematika yang benar berubah jadi faktur yang tidak cocok. Inti masalah sederhana: pembulatan lebih awal mengubah jumlah akhir.
Jika Anda membulatkan pajak per baris (atau per kuantitas), lalu menjumlahkannya, Anda bisa mendapatkan total yang berbeda dari jika Anda menjumlahkan pajak tak dibulatkan di seluruh faktur lalu membulatkannya sekali di akhir. Dengan banyak baris, celah itu menumpuk, apalagi ketika unit minor dan konversi FX sudah menciptakan pecahan kecil.
Contoh konkret (2 desimal): dua baris masing-masing mempunyai jumlah kena pajak 0.05 dengan pajak 10%. Pajak tak dibulatkan per baris adalah 0.005. Jika Anda membulatkan per baris, masing-masing menjadi 0.01, jadi total pajak 0.02. Jika Anda membulatkan di tingkat faktur, total kena pajak 0.10, pajak 0.01. Keduanya dapat dibenarkan. Mereka hanya berbeda.
Ketika Anda harus menampilkan pajak per baris tetapi juga perlu total faktur cocok persis, alokasikan sisa pembulatan secara deterministik:
Ekspor masih bisa menyimpang ketika akuntansi mengelompokkan baris (berdasarkan produk, tarif pajak, atau yurisdiksi). Untuk menjaga total ekspor sama dengan total faktur, alokasikan sisa dalam setiap grup yang diperlukan terlebih dulu, lalu verifikasi bahwa total grup bergulir ke total pajak dan gross faktur yang sama.
Jika akuntansi memerlukan pajak terpisah berdasarkan tarif atau yurisdiksi tetapi UI menampilkan satu angka pajak, simpan rincian itu saja (total per tarif atau yurisdiksi plus aturan alokasi yang auditable). UI bisa menampilkan satu total, sementara ekspor membawa bucket rinci tanpa mengubah grand total faktur.
Sebagian besar ketidakcocokan faktur terjadi di sudut-sudut. Putuskan aturannya lebih awal dan mereka berhenti menjadi kejutan.
Mata uang tanpa desimal perlu perhatian khusus. JPY dan KRW tidak punya unit minor, jadi langkah apa pun yang mengasumsikan "sen" akan diam-diam menciptakan perbedaan. Putuskan apakah Anda membulatkan pada setiap baris, pada tingkat pajak, atau hanya pada total akhir, dan pastikan setiap klien menggunakan pengaturan mata uang yang sama.
VAT atau GST lintas batas dapat mengubah tarif pajak berdasarkan lokasi pelanggan dan bukti apa yang Anda terima (alamat penagihan, IP, ID pajak). Bagian rumit bukan tarifnya sendiri, melainkan kapan Anda menguncinya. Pilih titik waktu (checkout, tanggal penerbitan faktur, atau mulai periode layanan) dan patuhi.
Prorata adalah tempat pecahan berlipat ganda. Upgrade di tengah siklus dapat menciptakan jumlah seperti 9.3333... per hari. Putuskan apakah Anda prorate net, gross, atau periode layanan dulu, lalu hitung sisanya. Mengubah urutan mengubah unit minor terakhir.
Tulis aturan-aturan ini sehingga tidak berubah seiring waktu:
Refunds adalah jebakan terakhir. Jika faktur asli punya sisa pembulatan 0.01 yang dialokasikan ke satu baris, refund Anda harus membalikkan alokasi eksak itu. Jika tidak, pelanggan melihat satu total, dan ledger ekspor Anda lain.
Kebanyakan ketidakcocokan faktur bukan disebabkan oleh "matematika berat." Mereka berasal dari pilihan kecil yang tidak konsisten dibuat di berbagai bagian stack.
Satu yang besar adalah menyimpan uang sebagai bilangan floating. Nilai seperti 19.99 tidak dapat direpresentasikan persis di banyak sistem, jadi kesalahan kecil muncul saat Anda menjumlah baris, menerapkan diskon, atau menghitung pajak. Simpan jumlah sebagai integer di unit minor, plus kode mata uang dan skala unit minor.
Masalah umum lain adalah menghitung ulang FX saat ekspor. Pelanggan membayar berdasarkan kurs tertentu pada waktu tertentu. Jika ekspor akuntansi menarik "kurs hari ini," Anda bisa mendapat total berbeda meskipun setiap langkah benar. Perlakukan faktur sebagai snapshot: simpan kurs FX yang digunakan, jumlah yang dikonversi, dan hasil pembulatan.
Perbedaan pembulatan juga muncul ketika UI dan backend membulatkan pada tahap berbeda. Misalnya, backend mungkin membulatkan pajak per baris, sementara UI web hanya membulatkan pada total faktur. Keduanya terlihat masuk akal, tapi tidak akan cocok.
Lima pelanggar berulang menjelaskan sebagian besar celah:
Pengecekan realitas sederhana: sebuah aplikasi mobile menampilkan tiga item EUR 9.99 dengan pajak 20%. Jika app membulatkan pajak di akhir tetapi backend membulatkan per baris, Anda bisa meleset sebesar EUR 0.01. Satu sen itu cukup untuk merusak rekonsiliasi dan memicu tiket dukungan.
Perbaikan paling sederhana: hitung sekali di backend, simpan snapshot faktur lengkap, dan biarkan web serta mobile merender angka yang disimpan itu persis.
Ketika angka berbeda antara web app, mobile app, dan ekspor akuntansi, biasanya bukan masalah matematika. Ini masalah penyimpanan dan pembulatan.
Mulailah dengan prinsip bahwa klien harus menampilkan apa yang disimpan faktur, bukan menghitung ulang. Backend Anda harus sumber kebenaran tunggal, dan setiap kanal harus membaca nilai yang sama.
Refund dan nota kredit harus mencerminkan hasil pembulatan faktur asli. Jika faktur asli membulatkan pajak per baris, refund harus melakukan hal yang sama, menggunakan presisi mata uang dan kurs FX yang disimpan. Jika tidak, sisa kecil bisa muncul dan terakumulasi.
Cara praktis untuk menegakkan ini adalah menyimpan snapshot perhitungan yang jelas dengan setiap faktur: mata uang, presisi unit minor, mode pembulatan, kurs FX dan timestamp, serta minor baris yang sudah difinalisasi.
Berikut satu faktur yang tetap konsisten di mana pun.
Anggap faktur diterbitkan dalam EUR (2 desimal), VAT 20%, dan pelanggan dikenai biaya dalam USD. Backend menyimpan 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 (karena 26.99 x 0.20 = 5.398, dibulatkan ke 5.40)
Gross total (EUR) = 32.39
Sekarang backend menurunkan total mata uang charge dari total EUR yang disimpan dan snapshot FX yang tersimpan:
Jika Anda juga menyimpan jumlah per-baris dalam USD, seringkali Anda akan mendapatkan perbedaan 0.01 ketika Anda membulatkan setiap baris yang dikonversi lalu menjumlahkannya. Di sinilah faktur biasanya menyimpang.
Jadikan deterministik: konversi dan bulatkan setiap baris, lalu distribusikan sisa sen (positif atau negatif) dalam urutan tetap (misalnya berdasarkan line_id naik) sampai jumlah per-baris sama dengan total gross USD yang sudah ditetapkan.
Web dan mobile harus menampilkan total baris yang disimpan di backend, total pajak, kurs FX, dan gross, bukan menghitung ulang. Ekspor akuntansi harus mengeluarkan angka yang sama yang disimpan ditambah snapshot FX (nilai, timestamp atau sumber) sehingga ledger cocok dengan apa yang dilihat pelanggan.
Langkah praktis berikutnya adalah mengimplementasikan perhitungan sebagai satu layanan bersama yang menghasilkan satu snapshot faktur (baris, pajak, total, FX, penyesuaian pembulatan) dan biarkan setiap kanal merender dari sana. Jika Anda membangun alur ini di Koder.ai (koder.ai), model snapshot ini membantu web, mobile, dan ekspor tetap selaras karena semuanya bisa membaca nilai yang sama yang disimpan.
Because each system often makes slightly different choices about when to round, what to round (net vs gross), and which precision to keep for tax and FX. Those tiny differences show up as 0.01–0.02 gaps, especially when proration, credits, and retries repeat the math over time.
Store amounts as integers in minor units (like cents) plus a currency code, and only format them for display. Floating-point values can’t represent many decimals exactly, so small errors appear when you add tax, discounts, or multiple lines.
Pick one as the stored source of truth and derive the others the same way everywhere. A common default is storing net and tax in minor units and computing gross = net + tax, because it makes refunds and audits easier and keeps totals stable.
Invoice currency is what the invoice totals are legally expressed in and what you should reconcile against. Display currency is what you show while browsing prices, and settlement currency is what the payment provider deposits; those can differ without the invoice being “wrong,” as long as the invoice currency calculations stay consistent.
Don’t re-fetch rates during export or PDF regeneration. Store the exact FX rate used on the invoice (value, precision, provider, and effective time), then always reuse it so old invoices reproduce the same numbers months later.
Lock one rule: either “rate at invoice issue time” or “rate at payment capture time,” then apply it everywhere. Mixing timestamps across systems is a common cause of mismatches, especially around midnight or timezone boundaries.
Default to rounding per line for subscription invoices, then sum stored line minors into totals. It’s usually the easiest to explain, avoids “line items don’t add up” support tickets, and stays stable across renewals if every channel uses the same rule.
Choose explicitly between per-line tax rounding and invoice-level tax rounding, then make it deterministic. If you must reconcile to an invoice-level target, allocate the rounding remainder in a fixed way and store the resulting per-line tax minors so every system can display the same outcome.
Proration creates repeating decimals (like daily rates), so the order of operations matters. Pick one method (for example, prorate net first, then compute tax from stored net), round at the agreed step, and store the finalized line minors so upgrades, downgrades, credits, and refunds mirror the original math.
Make the backend produce a finalized invoice snapshot (lines, taxes, totals, currency minor-unit rules, FX snapshot, rounding mode) and treat it as immutable once finalized. Then have web, mobile, PDFs, and exports render those stored integers instead of recomputing; this is also a good pattern when building billing flows on Koder.ai.