वित्त ऐप्स में मुद्रा राउंडिंग: पैसे सुरक्षित रूप से स्टोर करें
वित्त ऐप्स में मुद्रा राउंडिंग एक-सेंट त्रुटियाँ पैदा कर सकती है। integer-cent स्टोरेज, टैक्स राउंडिंग नीतियाँ और वेब/मोबाइल पर सुसंगत डिस्प्ले सीखें।

क्यों एक-सेंट बग होते हैं
एक-सेंट बग वह तरह की गलती है जिसे उपयोगकर्ता तुरंत नोटिस कर लेते हैं। उत्पाद सूची पर कार्ट कुल $19.99 दिखता है, लेकिन चेकआउट पर $20.00 हो जाता है। $14.38 का रिफंड $14.37 के रूप में पहुँचना। एक इनवॉइस लाइन पर "Tax: $1.45" लिखा है, फिर भी ग्रैंड टोटल ऐसा लग रहा है कि टैक्स अलग तरीके से जोड़ा गया था।
ये समस्याएँ आमतौर पर छोटे राउंडिंग अंतर से आती हैं जो जमा हो जाते हैं। पैसा सिर्फ "एक संख्या" नहीं है। उसके नियम होते हैं: किस मुद्रा में कितनी दशमलव जगहें होती हैं, कब आप राउंड करते हैं, और क्या आप पर-लाइन आइटम पर राउंड करते हैं या अंतिम टोटल पर। यदि ऐप किसी एक जगह पर अलग चुनाव करता है, तो एक सेंट गायब या दिखाई दे सकता है।
ये अक्सर कभी-कभी ही दिखते हैं, इसलिए डिबग करना कठिन होता है। एक ही इनपुट डिवाइस या लोकेल सेटिंग्स, ऑपरेशन्स के क्रम, या प्रकारों के बीच रूपांतरण के तरीके के आधार पर अलग सेंट दे सकते हैं।
सामान्य ट्रिगर में शामिल हैं: float के साथ कैलकुलेशन करना और "अंत में" राउंड करना (पर "अंत" हर जगह एक जैसा नहीं होता), एक स्क्रीन पर प्रति आइटम टैक्स लागू करना और दूसरी पर subtotal पर लागू करना, मुद्राओं या एक्सचेंज रेट्स को मिलाना और असंगत स्टेप्स पर राउंड करना, या डिस्प्ले के लिए वैल्यूज़ फॉर्मैट करना और गलती से उन्हें फिर से नंबर के रूप में पार्स कर लेना।
नुकसान सबसे ज़्यादा वहाँ होता है जहाँ भरोसा नाज़ुक होता है और राशियाँ ऑडिट की जाती हैं: चेकआउट टोटल्स, रिफंड, इनवॉइस, सब्सक्रिप्शन, टिप्स, पayouts और खर्च रिपोर्ट। एक-सेंट का mismatch भुगतान विफलता, मिलान की मुश्किलें और सपोर्ट टिकट्स (जैसे "आपका ऐप मुझसे चुरा रहा है") पैदा कर सकता है।
लक्ष्य सरल है: वही इनपुट हर जगह वही सेंट देनी चाहिए। वही आइटम, वही टैक्स, वही डिस्काउंट, वही राउंडिंग नियम—स्क्रीन, डिवाइस, भाषा या एक्सपोर्ट की परवाह किए बिना।
उदाहरण: अगर दो $9.99 आइटम पर 7.25% टैक्स है, तो तय करें कि आप टैक्स पर-आइटम राउंड करेंगे या सबटोटल पर, फिर backend, web UI और mobile app में हर जगह वही करें। सुसंगति "यहाँ यह अलग क्यों है?" क्षण रोकती है।
पैसे के लिए float क्यों जोखिमभरा है
ज़्यादातर प्रोग्रामिंग भाषाएँ float और double को बाइनरी में स्टोर करती हैं। कई दशमलव कीमतें बाइनरी में ठीक से व्यक्त नहीं होतीं, इसलिए जो संख्या आपने सेव की वह अक्सर थोड़ा ऊपर या नीचे होती है।
क्लासिक उदाहरण है 0.1 + 0.2। कई सिस्टम में यह 0.30000000000000004 बन जाता है। यह नुकसानदेह नहीं लगता, लेकिन मनी लॉजिक आम तौर पर एक चेन होती है: आइटम प्राइस, डिस्काउंट, टैक्स, फीस, फिर अंतिम राउंडिंग। छोटे त्रुटियाँ राउंडिंग निर्णय बदल सकती हैं और एक-सेंट का अंतर पैदा कर सकती हैं।
लक्षण जिन पर लोग ध्यान देते हैं जब मनी राउंडिंग गलत हो जाती है:
- लॉग या API रिस्पॉन्स में 9.989999 या 19.9000001 जैसे वैल्यूज़ दिखना।
- कई आइटम जोड़ने पर टोटल्स ड्रिफ्ट करना, भले हर आइटम सही दिखे।
- रिफंड टोटल मूल चार्ज से $0.01 मेल न खाना।
- वही कार्ट टोटल वेब, मोबाइल और बैकएंड में अलग दिखना।
फॉर्मैटिंग अक्सर समस्या छिपा देती है। यदि आप 9.989999 को दो दशमलव के साथ प्रिंट करते हैं तो वह 9.99 दिखता है, इसलिए सब कुछ सही दिखता है। बग बाद में तब दिखता है जब आप कई वैल्यूज़ जोड़ते हैं, टोटल्स की तुलना करते हैं, या टैक्स के बाद राउंड करते हैं। इसलिए टीम्स कभी-कभी इसे शिप कर देती हैं और केवल पेमेंट प्रोवाइडर्स या अकाउंटिंग एक्सपोर्ट्स के साथ मेल खाता हुआ पाए जाने पर ही पता चलता है।
एक सरल नियम: पैसे को फ्लोटिंग-पॉइंट के रूप में स्टोर या सम न करें। पैसे को मुद्रा के minor unit (जैसे सेंट) के integer के रूप में ट्रीट करें, या ऐसा decimal टाइप उपयोग करें जो सटीक दशमलव गणित गारंटी करे।
यदि आप backend, वेब ऐप, या मोबाइल ऐप बना रहे हैं (यहाँ तक कि no-code प्लेटफ़ॉर्म जैसे AppMaster के साथ भी), तो हर जगह यही सिद्धांत रखें: सटीक वैल्यूज़ स्टोर करें, सटीक वैल्यूज़ पर कैलकुलेट करें, और केवल अंत में डिस्प्ले के लिए फॉर्मैट करें।
वास्तविक मुद्राओं के अनुरूप मनी मॉडल चुनें
ज़्यादातर मनी बग गणित होने से पहले ही शुरू होते हैं: डेटा मॉडल उस तरीके से मेल नहीं खाता जिस तरह मुद्रा वास्तव में काम करती है। मॉडल को पहले से सही रखें और राउंडिंग एक नियम बनने लगती है, अनुमान लगाने का मामला नहीं।
सबसे सुरक्षित डिफ़ॉल्ट यह है कि पैसे को मुद्रा के minor unit में integer के रूप में स्टोर करें। USD के लिए इसका मतलब सेंट; EUR के लिए यूरो सेंट। आपका डेटाबेस और कोड ठीक integers संभालता है, और केवल मानव-फ्रेंडली फॉर्मैट करते समय ही "दशमलव जोड़ते" हैं।
हर मुद्रा के पास 2 दशमलव नहीं होते, इसलिए आपका मॉडल currency-aware होना चाहिए। JPY में 0 minor decimals हैं (1 yen सबसे छोटी इकाई है)। BHD आम तौर पर 3 दशमलव उपयोग करता है (1 dinar = 1000 fils)। यदि आप "हर जगह दो दशमलव" हार्डकोड कर देंगे, तो चुपचाप ओवरचार्ज या अंडरचार्ज कर सकते हैं।
एक प्रैक्टिकल मनी रिकॉर्ड में आमतौर पर ये चाहिए:
amount_minor(integer, जैसे $19.99 के लिए 1999)currency_code(string जैसे USD, EUR, JPY)- वैकल्पिक
minor_unitयाscale(0, 2, 3) यदि आपका सिस्टम इसे विश्वसनीय रूप से lookup नहीं कर पाता
हर amount के साथ currency code स्टोर करें, भले ही वही तालिका के अंदर हो। इससे बाद में multi-currency प्राइसिंग, रिफंड या रिपोर्ट जोड़ते समय गलती नहीं होगी।
यह भी तय करें कि कहाँ राउंडिंग की अनुमति है और कहाँ मना है। एक नीति जो अच्छा काम करती है: आंतरिक टोटल्स, एलोकेशंस, लेज़र या चल रहे कनवर्ज़न के भीतर राउंड न करें; केवल परिभाषित सीमाओं पर राउंड करें (जैसे टैक्स स्टेप, डिस्काउंट स्टेप, या अंतिम इनवॉइस लाइन); और हमेशा उपयोग किया गया राउंडिंग मोड लॉग करें (half up, half even, round down) ताकि परिणाम पुनरुत्पादनीय हों।
कदम-दर-कदम: integer-minor-unit मनी लागू करें
अगर आप कम सरप्राइज चाहते हैं, तो एक आंतरिक फॉर्म चुनें और उसे न तोड़ें: राशियाँ मुद्रा के minor unit (अक्सर सेंट) में integers के रूप में स्टोर करें।
इसका मतलब $10.99 1099 बन जाता है currency USD के साथ। जिन मुद्राओं में कोई minor unit नहीं है जैसे JPY, 1,500 yen 1500 जैसी ही रहती है।
विकासशील ऐप के लिए एक सरल इम्प्लीमेंटेशन पथ:
- Database:
amount_minorको 64-bit integer के रूप में और एक currency code के साथ स्टोर करें (जैसेUSD,EUR,JPY). कॉलम का नाम साफ़ रखें ताकि कोई उसे decimal समझ न बैठे। - API contract:
{ amount_minor: 1099, currency: "USD" }भेजें और रिसीव करें। फॉर्मैटेड स्ट्रिंग्स जैसे "$10.99" और JSON floats से बचें। - UI input: यूज़र जो टाइप करता है उसे नंबर न मानकर टेक्स्ट के रूप में ट्रीट करें। उसे normalize करें (spaces ट्रिम करें, एक decimal separator स्वीकार करें), फिर currency के minor-unit digits का उपयोग करके कन्वर्ट करें।
- सारी गणना integers में: टोटल्स, लाइन सम, डिस्काउंट, फीस और टैक्स सभी integers पर ही करें। नियम परिभाषित करें जैसे "percentage discount कैलक्युलेट करके फिर उसे minor units में राउंड किया जाता है" और हर जगह वही लागू करें।
- अंत में ही फॉर्मैट करें: जब आप पैसे दिखाएँ तो
amount_minorको locale और currency नियमों के अनुसार डिस्प्ले स्ट्रिंग में बदलें। कभी भी अपने फॉर्मैटेड आउटपुट को फिर से गणित के लिए पार्स न करें।
एक प्रैक्टिकल पार्सिंग उदाहरण: USD के लिए "12.3" को "12.30" माने और फिर 1230 में कन्वर्ट करें। JPY के लिए upfront decimals को reject करें।
टैक्स, डिस्काउंट और फीस राउंडिंग नियम
ज़्यादातर एक-सेंट विवाद गणित की गलती नहीं होते; वे नीति की गलतियाँ होते हैं। दो सिस्टम दोनों "सही" हो सकते हैं और फिर भी असहमत दिख सकते हैं यदि वे अलग समय पर राउंड करते हैं।
अपनी राउंडिंग नीति लिखें और हर जगह उसका उपयोग करें: गणनाएँ, रसीदें, एक्सपोर्ट और रिफंड। आम विकल्पों में शामिल हैं half-up (0.5 ऊपर जाए) और half-even (0.5 निकटतम सम सेंट पर जाए)। कुछ फीस हमेशा ऊपर (ceiling) राउंड करने की आवश्यकता रखती हैं ताकि आप कभी-अंडरचार्ज न करें।
टोटल्स आमतौर पर कुछ निर्णयों पर निर्भर होते हैं: क्या आप प्रति लाइन आइटम राउंड करेंगे या पूरे इनवॉइस पर, क्या आप नियम मिलाते हैं (उदा., लाइन पर टैक्स परन्तु फीस इनवॉइस पर), और क्या प्राइस टैक्स-इंक्लूसिव हैं (आप नेट और टैक्स बैक-कैल्कुलेट करते हैं) या टैक्स-एक्सक्लूसिव (नेट से टैक्स कैलकुलेट करते हैं)।
डिस्काउंट्स एक और मोड़ जोड़ते हैं। "10% off" जो टैक्स से पहले लागू होता है वह taxable base घटा देता है, जबकि टैक्स के बाद लागू डिस्काउंट ग्राहक के भुगतान को घटाता है पर कुछ अधिकार क्षेत्रों में रिपोर्टेड टैक्स को नहीं बदलता।
एक छोटा उदाहरण दिखाता है कि सख्त नियम क्यों ज़रूरी हैं। दो आइटम $9.99 पर 7.5% टैक्स। यदि आप टैक्स पर-लाइन राउंड करते हैं, तो हर लाइन टैक्स $0.75 होगा (9.99 x 0.075 = 0.74925)। कुल टैक्स $1.50 बनता है। यदि आप इनवॉइस टोटल पर टैक्स करते हैं, तो यहाँ भी $1.50 आएगा, पर प्राइस थोड़े बदलते ही 1-सेंट का अंतर दिखेगा।
नीति को सादे भाषा में लिखें ताकि सपोर्ट और फाइनेंस उसे समझा सकें। फिर टैक्स, फीस, डिस्काउंट और रिफंड के लिए वही हेल्पर पुन: उपयोग करें।
कुलों को ड्रिफ्ट किए बिना करेंसी कन्वर्ज़न
मल्टी-करेंसी गणित वह जगह है जहाँ छोटे राउंडिंग चुनाव धीरे-धीरे टोटल्स बदल देते हैं। लक्ष्य सरल है: एक बार कन्वर्ट करें, जानबूझकर राउंड करें, और मूल तथ्यों को बाद में संजोए रखें।
एक्सचेंज रेट्स को स्पष्ट precision के साथ स्टोर करें। एक आम पैटर्न scaled integer है, जैसे "rate_micro" जहाँ 1.234567 को 1234567 के रूप में 1,000,000 के scale के साथ स्टोर किया जाता है। दूसरा विकल्प fixed decimal टाइप है, पर फिर भी scale फील्ड में लिखें ताकि उसे अनुमान न लगाया जा सके।
रिपोर्टिंग और अकाउंटिंग के लिए एक बेस मुद्रा चुनें (आमतौर पर आपकी कंपनी की मुद्रा)। आने वाली राशियों को लेज़र और एनालिटिक्स के लिए बेस मुद्रा में कन्वर्ट करें, पर साथ में मूल मुद्रा और राशि भी रखें। इस तरह आप बाद में हर नंबर की व्याख्या कर सकते हैं।
ड्रिफ्ट रोकने वाले नियम:
- अकाउंटिंग के लिए एक दिशा में ही कन्वर्ट करें (foreign से base), और वापस-आगे कन्वर्ज़न से बचें।
- राउंडिंग के समय का निर्णय लें: जब लाइन टोटल दिखाना आवश्यक हो तब प्रति लाइन राउंड करें, या जब केवल ग्रैंड टोटल दिखाना हो तब अंत में राउंड करें।
- एक राउंडिंग मोड लगातार उपयोग करें और दस्तावेज़ करें।
- लेनदेन के लिए उपयोग किया गया मूल amount, currency और exact rate स्टोर रखें।
उदाहरण: ग्राहक 19.99 EUR भुगतान करता है, और आप उसे 1999 minor units के रूप में currency=EUR के साथ स्टोर करते हैं। आप चेकआउट पर उपयोग किया गया rate भी स्टोर करते हैं (उदा., EUR से USD में micro units)। आपका लेज़र कन्वर्ट किए हुए USD amount को स्टोर करता है (आपके चुने हुए नियम के अनुसार राउंड किया हुआ), पर रिफंड्स के लिए स्टोर किया गया मूल EUR amount और currency उपयोग करें, न कि USD से फिर पुनर्रूपांतरण। इससे "मुझे 19.98 EUR वापस क्यों मिला?" जैसे टिकट्स नहीं बनेंगे।
डिवाइसों पर फॉर्मैटिंग और डिस्प्ले
अंतिम कदम स्क्रीन है। स्टोरेज में वैल्यू सही होने के बावजूद यदि फॉर्मैटिंग वेब और मोबाइल पर अलग हो तो वह गलत दिख सकती है।
अलग-लोगों के लिए अलग punctuation और सिंबल प्लेसमेंट अपेक्षित होते हैं। उदाहरण के लिए, बहुत से US उपयोगकर्ता $1,234.50 पढ़ते हैं, जबकि यूरोप के कई हिस्सों में वे 1.234,50 € अपेक्षा करते हैं (उसी वैल्यू के लिए अलग सेपरेटर और सिंबल पोज़िशन)। यदि आप फॉर्मैटिंग हार्डकोड करते हैं, तो आप लोगों को कन्फ्यूज़ करेंगे और सपोर्ट का काम बढ़ेगा।
एक नियम रखें: एज पर फॉर्मैट करें, कोर में नहीं। आपकी सत्यता स्रोत होनी चाहिए (currency code, minor units integer)। केवल डिस्प्ले के लिए स्ट्रिंग बनाएं। कभी फॉर्मैटेड स्ट्रिंग को फिर से पैसे में पार्स न करें—यहीं राउंडिंग, ट्रिमिंग और लोकेल आश्चर्य आते हैं।
रिफंड जैसे नेगेटिव अमाउंट्स के लिए एक सुसंगत स्टाइल चुनें और हर जगह उसे अपनाएँ। कुछ सिस्टम -$12.34 दिखाते हैं, कुछ ($12.34)। दोनों ठीक हैं, पर स्क्रीन के बीच स्विचिंग त्रुटि जैसा दिखता है।
एक साधारण cross-device कांट्रैक्ट जो अच्छा काम करता है:
- मुद्रा को ISO code (जैसे USD, EUR) के रूप में रखें, सिर्फ सिंबल के रूप में नहीं।
- डिफ़ॉल्ट रूप से डिवाइस लोकैल का उपयोग करके फॉर्मैट करें, पर इन-ऐप ओवरराइड की अनुमति दें।
- multi-currency स्क्रीन पर राशि के पास currency code दिखाएँ (उदा., 12.34 USD)।
- इनपुट फॉर्मैटिंग को डिस्प्ले फॉर्मैटिंग से अलग रखें।
- राउंडिंग के बाद ही फॉर्मैट करें, अपने मनी नियमों के अनुसार।
उदाहरण: ग्राहक मोबाइल पर 10,00 EUR के रिफंड को देखता है, फिर उसी ऑर्डर को डेस्कटॉप पर खोलता है और -€10 देखता है। यदि आप कोड भी दिखाएँ (10,00 EUR) और नेगेटिव स्टाइल सुसंगत रखें, तो वे यह नहीं सोचेंगे कि यह बदल गया है।
उदाहरण: चेकआउट, टैक्स और रिफंड बिना सरप्राइज के
एक साधारण कार्ट:
- आइटम A: $4.99 (499 cents)
- आइटम B: $2.50 (250 cents)
- आइटम C: $1.20 (120 cents)
Subtotal = 869 cents ($8.69). पहले 10% डिस्काउंट लागू करें: 869 x 10% = 86.9 cents, राउंड करके 87 cents। डिस्काउंट के बाद subtotal = 782 cents ($7.82). अब 8.875% टैक्स लागू करें।
यहाँ राउंडिंग नियम अंतिम पैसा बदल सकते हैं।
यदि आप इनवॉइस टोटल पर टैक्स कैलकुलेट करते हैं: 782 x 8.875% = 69.4025 cents, राउंड करके 69 cents होगा।
यदि आप हर लाइन पर (डिस्काउंट के बाद) टैक्स कैलकुलेट करें और हर लाइन राउंड करें:
- आइटम A: $4.49 टैक्स = 39.84875 cents, राउंड होकर 40
- आइटम B: $2.25 टैक्स = 19.96875 cents, राउंड होकर 20
- आइटम C: $1.08 टैक्स = 9.585 cents, राउंड होकर 10
लाइन टैक्स कुल = 70 cents। वही कार्ट, वही दर, अलग मान्य नियम—1 सेंट का फर्क।
टैक्स के बाद शिपिंग फीस जोड़ें, मान लें 399 cents ($3.99)। कुल $12.50 (इनवॉइस-लेवल टैक्स) या $12.51 (लाइन-लेवल टैक्स) बन सकता है। एक नियम चुनें, दस्तावेज़ करें और सुसंगत रखें।
अब केवल आइटम B रिफंड करें। उसका डिस्काउंटेड प्राइस (225 cents) और उस पर लागू टैक्स रिफंड करें। लाइन-लेवल टैक्स के साथ यह 225 + 20 = 245 cents ($2.45) होगा। आपकी बचे हुए टोटल्स अभी भी ठीक से reconcile होंगे।
किसी भी विवाद को समझाने के लिए प्रत्येक चार्ज और रिफंड के लिए ये वैल्यूज़ लॉग करें:
- प्रति-लाइन net cents, प्रति-लाइन tax cents, और राउंडिंग मोड
- इनवॉइस डिस्काउंट cents और उसे कैसे allocate किया गया
- उपयोग किया गया tax rate और taxable base cents
- shipping/fees cents और क्या वे taxable हैं
- अंतिम total cents और refund cents
मनी गणनाओं का परीक्षण कैसे करें
अधिकांश मनी बग "गणितीय बग" नहीं होते। वे राउंडिंग, क्रम और फॉर्मैटिंग बग होते हैं जो केवल विशिष्ट कार्ट्स या तिथियों पर दिखते हैं। अच्छे टेस्ट उन मामलों को बोइरिंग बना देते हैं।
गोल्डन टेस्ट्स से शुरू करें: निश्चित इनपुट्स के साथ सटीक अपेक्षित आउटपुट्स minor units में रखें (जैसे सेंट)। assertions कड़े रखें। यदि एक आइटम 199 cents है और टैक्स 15 cents है, तो टेस्ट integer वैल्यूज़ चेक करे, फॉर्मैटेड स्ट्रिंग्स नहीं।
एक छोटा गोल्डन सेट बहुत कुछ कवर कर सकता है:
- टैक्स के साथ एकल लाइन आइटम, फिर डिस्काउंट, फिर फी (प्रत्येक मध्यवर्ती राउंडिंग चेक करें)
- कई लाइन आइटम जहाँ टैक्स प्रति लाइन बनाम सबटोटल पर राउंड किया जाता है (अपने चुने नियम को verify करें)
- रिफंड और आंशिक रिफंड (साइन और राउंडिंग दिशा verify करें)
- कन्वर्ज़न राउंड-ट्रिप (A से B और फिर A) के साथ परिभाषित नीति जहाँ राउंडिंग होती है
- एज वैल्यूज़ (1 सेंट आइटम, बड़े क्वांटिटी, बहुत बड़े टोटल्स)
फिर प्रॉपर्टी-आधारित चेक्स (या सरल रैंडमाइज्ड टेस्ट) जोड़ें ताकि आश्चर्य पकड़े जा सकें। एक अपेक्षित संख्या के बजाय invariants assert करें: टोटल्स लाइन टोटल्स के योग के बराबर हों, कभी भी fractional minor units न दिखें, और "total = subtotal + tax + fees - discounts" हमेशा सत्य हो।
क्रॉस-प्लेटफ़ॉर्म टेस्टिंग जरूरी है क्योंकि परिणाम backend और क्लाइंट्स के बीच ड्रिफ्ट कर सकते हैं। यदि आपका Go backend है और Vue वेब ऐप और Kotlin/SwiftUI मोबाइल है, तो हर लेयर में एक जैसे टेस्ट वेक्टर चलाएँ और integer आउटपुट्स की तुलना करें, न कि UI स्ट्रिंग्स।
अंत में, समय-आधारित मामलों का परीक्षण करें। किसी इनवॉइस पर उपयोग किया गया tax rate स्टोर करें और सत्यापित करें कि पुराने इनवॉइसों को वही परिणाम दोहराकर फिर से कैलकुलेट किया जा सकता है भले ही बाद में रेट बदल गए हों। यही वह जगह है जहाँ "पहले मेल खाता था" बग जन्म लेते हैं।
बचने के लिए सामान्य जाल
अधिकांश एक-सेंट बग गणित की गलती नहीं होते; वे नीति की गलतियाँ होते हैं: कोड ठीक वही करता है जो आपने कहा है, बस वही नहीं जो फाइनेंस उम्मीद करता है।
क Guard करने योग्य जाल:
- बहुत जल्दी राउंडिंग करना: यदि आप हर लाइन आइटम को राउंड करते हैं, फिर subtotal राउंड करते हैं, फिर टैक्स राउंड करते हैं, तो टोटल्स ड्रिफ्ट कर सकते हैं। एक नियम तय करें (उदा.: टैक्स पर लाइन बनाम इनवॉइस) और केवल नीति अनुमति देने वाली जगहों पर ही राउंड करें।
- एक ही सम में मुद्राओं को मिलाना: USD और EUR को एक ही "total" फ़ील्ड में जोड़ना तब तक harmless दिखता है जब तक रिफंड, रिपोर्टिंग या reconciliation न हो। राशियों को उनकी मुद्रा टैग के साथ रखें, और cross-currency sum से पहले सहमति वाले रेट से कन्वर्ट करें।
- यूज़र इनपुट को गलत पार्स करना: यूज़र "1,000.50", "1 000,50" या "10.0" टाइप कर सकते हैं। यदि आपका पार्सर एक फ़ॉर्मेट मानता है, तो आप चुपके से 100050 चार्ज कर सकते हैं बजाय 1000.50 के, या trailing zeros खो सकते हैं। इनपुट normalize करें, validate करें, और minor units में स्टोर करें।
- API या DB में फॉर्मैटेड स्ट्रिंग्स का उपयोग करना: "$1,234.56" केवल डिस्प्ले के लिए है। यदि आपकी API उसे स्वीकार करती है, तो दूसरा सिस्टम उसे अलग तरह से पार्स कर सकता है। इंटेजर्स (minor units) और currency कोड पास करें, और प्रत्येक क्लाइंट को लोकली फॉर्मैट करने दें।
- टैक्स नियमों या रेट तालिकाओं का versioning न करना: टैक्स रेट्स बदलते हैं, छूट बदलती है, और राउंडिंग नियम बदलते हैं। यदि आप पुराना रेट ओवरराइट कर देते हैं, तो पुराने इनवॉइस पुनरुत्पादित करना असंभव हो जाएगा। हर कैलकुलेशन के साथ एक संस्करण या effective date स्टोर करें।
एक छोटा वास्तविकता जाँच: सोमवार को बनाया गया चेकआउट पिछले महीने के टैक्स रेट का उपयोग करता है; उपयोगकर्ता शुक्रवार को रिफंड लेता है जब रेट बदल चुका है। यदि आपने टैक्स नियम का संस्करण और मूल राउंडिंग नीति स्टोर नहीं की, तो आपका रिफंड मूल रसीद से मेल नहीं खाएगा।
त्वरित चेकलिस्ट और अगले कदम
यदि आप कम सरप्राइज चाहते हैं, तो पैसे को एक छोटे सिस्टम की तरह मानें जिसमें स्पष्ट इनपुट, नियम और आउटपुट हों। अधिकांश एक-सेंट बग इसलिए रहते हैं क्योंकि किसी ने नहीं लिखा कि राउंडिंग कहाँ अनुमति है।
शिपिंग से पहले चेकलिस्ट:
- डेटाबेस, बिजनेस लॉजिक और API में हर जगह minor units (जैसे integer सेंट) में राशियाँ स्टोर करें।
- सारी गणना integers में करें, और केवल अंत में डिस्प्ले फॉर्मैट में कन्वर्ट करें।
- प्रत्येक गणना (टैक्स, डिस्काउंट, फी, FX) के लिए एक राउंडिंग पॉइंट चुनें और उसे एक स्थान पर लागू करें।
- वेब और मोबाइल पर सही currency नियमों (दशमलव, सेपरेटर, नेगेटिव वैल्यूज़) का उपयोग करके फॉर्मैट करें।
- एज केस के लिए टेस्ट जोड़ें: 0.01, कन्वर्ज़न में रिपीटिंग दशमलव, रिफंड, आंशिक कैप्चर और बड़े बास्केट।
प्रति गणना एक राउंडिंग नीति लिखें। उदाहरण: "डिस्काउंट प्रति लाइन आइटम के लिए निकटतम सेंट पर राउंड करता है; टैक्स इनवॉइस टोटल पर राउंड करता है; रिफंड मूल राउंडिंग पाथ को दोहराते हैं।" इन नीतियों को कोड के पास और टीम डॉक्स में रखें ताकि वे बिखरें नहीं।
हर महत्वपूर्ण मनी स्टेप के लिए हल्के-फुल्के लॉग जोड़ें। इनपुट वैल्यूज़, चुनी गई नीति का नाम, और आउटपुट minor units में कैप्चर करें। जब कोई ग्राहक रिपोर्ट करे "मुझसे एक सेंट अधिक चार्ज हुआ", तो आपके पास एक लाइन हो जो बताती हो कि क्यों।
प्रोडक्शन में लॉजिक बदलने से पहले एक छोटा ऑडिट प्लान करें। वास्तविक ऐतिहासिक ऑर्डर्स के एक सैंपल पर नए लॉजिक के साथ totals फिर से कैलकुलेट करें, पुराने बनाम नए परिणामों की गिनती करें और mismatches को मैन्युअली रिव्यू करें ताकि वे आपकी नई नीति के अनुरूप हों।
यदि आप एक end-to-end फ्लो बनाना चाहते हैं बिना तीन बार वही नियम लिखे, तो AppMaster (appmaster.io) पूरी एप्स के लिए डिज़ाइन किया गया है। आप PostgreSQL में Data Designer के माध्यम से amounts को minor-unit integers के रूप में मॉडल कर सकते हैं, एक बार Business Process में राउंडिंग और टैक्स स्टेप्स लागू कर सकते हैं, और फिर उसी लॉजिक को वेब और native मोबाइल UIs में पुन: उपयोग कर सकते हैं।
सामान्य प्रश्न
अलग-अलग हिस्से जब अलग समय पर या अलग तरीके से राउंड करते हैं तो ये आमतौर पर होते हैं। यदि प्रोडक्ट लिस्ट एक स्टेप पर राउंड करती है और चेकआउट किसी अन्य स्टेप पर, तो वही कार्ट अलग-संगत सेंट पर आ सकता है।
अधिकांश float्स सामान्य दशमलव कीमतों को बाइनरी में ठीक से नहीं दिखा पाते, इसलिए छोटे छुपे हुए अंतर बनते हैं। वे छोटे अंतर बाद में किसी राउंडिंग निर्णय को उलट सकते हैं और एक-सेंट का फर्क पैदा कर सकते हैं।
पैसे को डेटाबेस में currency के minor unit में integer के रूप में स्टोर करें—उदाहरण के लिए USD के लिए सेंट (1999 = $19.99)—और साथ में currency code रखें। गणनाएँ integers में करें और यूज़र को दिखाते समय ही decimal स्ट्रिंग बनाएँ।
दो दशमलव फ़िक्स करने से JPY (0 दशमलव) या BHD (3 दशमलव) जैसी मुद्राओं के लिए गड़बड़ी होगी। हमेशा राशि के साथ currency code रखें और इनपुट पार्स करते और आउटपुट फॉर्मैट करते समय सही minor-unit scale लागू करें।
एक स्पष्ट नियम चुनें और हर जगह लागू करें, जैसे टैक्स प्रति लाइन आइटम राउंड करना या इनवॉइस subtotal पर राउंड करना। महत्वपूर्ण बात है backend, वेब, मोबाइल, exports और रिफंड्स में वही नियम और वही राउंडिंग मोड हर बार उपयोग करना।
क्रम को पहले ही तय करें और इसे नीति मानकर लागू करें। सामान्य डिफ़ॉल्ट यह है कि डिस्काउंट पहले लागू किया जाए (टैक्सेबल बेस घटे), फिर टैक्स लगे—लेकिन अपने व्यवसाय और अधिकार क्षेत्र के नियमों का पालन करें और हर जगह एक जैसा रखें।
एक बार रूपांतरण करें, उपयोग किए गए rate को सेव करें (स्पष्ट precision के साथ), राउंडिंग एक परिभाषित स्टेप पर करें, और रिफंड के लिए हमेशा मूल राशि और मुद्रा का उपयोग करें। बार-बार बैक-एंड और फ्रंट-एंड के बीच रूपांतरण करने से बचें।
फॉर्मेटेड डिस्प्ले स्ट्रिंग्स को नंबर में वापस पार्स न करें, क्योंकि लोकेल सेपरेटर और राउंडिंग मान बदल सकते हैं। संरचित मान पास करें जैसे (amount_minor, currency_code) और UI पर ही लोकैल नियमों के साथ फॉर्मेट करें।
स्टेप्स के लिए निश्चित इनपुट और अपेक्षित आउटपुट वाले “गोल्डन” टेस्ट बनाएँ जो minor units (सेंट) में सख्त assertions करें। फिर invariants के लिए प्रॉपर्टी-आधारित या रैंडमाइज़्ड चेक जोड़ें, जैसे कि टोटल हमेशा लाइन टोटल्स का योग हो।
मनी मथ को एक साझा जगह में केंद्रीकृत करें और हर जगह वही लोगिक री-यूज़ करें ताकि एक जैसे इनपुट हर जगह एक जैसे सेंट दें। AppMaster में व्यावहारिक तरीका है PostgreSQL में amount_minor को integer के रूप में मॉडल करना और rounding और टैक्स लॉजिक को एक Business Process में रखना जिसे वेब और मोबाइल दोनों उपयोग करें।


