Download index.html from getreadystack/currency-minor-unit-lint: direct link, hf CLI and curl.
- Browser
- Download file 39.6 kB
-
https://huggingface.co/spaces/getreadystack/currency-minor-unit-lint/resolve/main/index.html
- Command line
-
hf download hf://spaces/getreadystack/currency-minor-unit-lint/index.html
-
curl -L -o index.html https://huggingface.co/spaces/getreadystack/currency-minor-unit-lint/resolve/main/index.html
39.6 kB
| <html lang="en"><meta charset="utf-8"> | |
| <meta name="viewport" content="width=device-width,initial-scale=1"> | |
| <title>Currency Minor Unit Lint for Payment API Amounts</title> | |
| <meta name="description" content="Finds the money lines that send the wrong amount to a payment API: the x100 that charges 100x in JPY, the toFixed(2) that truncates KWD, and the currency codes that stopped being legal tender."> | |
| <style> | |
| :root{color-scheme:light dark} | |
| body{margin:0;font:16px/1.6 system-ui,-apple-system,"Segoe UI",sans-serif; | |
| background:#fbfbfa;color:#16161d} | |
| @media(prefers-color-scheme:dark){body{background:#14141a;color:#e8e8ea}} | |
| .wrap{max-width:880px;margin:0 auto;padding:28px 20px 64px} | |
| h1{font-size:1.7rem;line-height:1.25;margin:.2em 0 .3em} | |
| p.sub{margin:0 0 1.4em;opacity:.8} | |
| textarea{width:100%;min-height:230px;font:13px/1.5 ui-monospace,Menlo,Consolas,monospace; | |
| padding:12px;border:1px solid #c9c9d1;border-radius:10px;background:transparent;color:inherit; | |
| box-sizing:border-box} | |
| button{font:600 15px/1 inherit;padding:12px 22px;border:0;border-radius:10px; | |
| background:#1d4ed8;color:#fff;cursor:pointer;margin:12px 0} | |
| button:hover{background:#1a43bd} | |
| table{border-collapse:collapse;width:100%;margin-top:14px;font-size:14px} | |
| th,td{border-bottom:1px solid #d8d8e0;padding:8px 6px;text-align:left;vertical-align:top} | |
| th{font-weight:600;opacity:.75} | |
| td.n{width:64px;font:13px ui-monospace,monospace;opacity:.7} | |
| .ok{padding:14px;border-radius:10px;background:#e8f5ec;color:#14532d;margin-top:14px} | |
| @media(prefers-color-scheme:dark){.ok{background:#15321f;color:#bbf7d0}} | |
| .next{margin-top:30px;padding:18px;border:1px solid #d8d8e0;border-radius:12px} | |
| .next h2{font-size:1.05rem;margin:0 0 .5em} | |
| .next ul{margin:.3em 0 1em 1.1em;padding:0} | |
| </style> | |
| <div class="wrap"> | |
| <h1>Currency Minor Unit Lint for Payment API Amounts</h1> | |
| <p class="sub">Finds the money lines that send the wrong amount to a payment API: the x100 that charges 100x in JPY, the toFixed(2) that truncates KWD, and the currency codes that stopped being legal tender.</p> | |
| <textarea id="src" placeholder="Paste the file that builds your payment amounts">// checkout.js - the amounts we send to the payment API | |
| const price = parseFloat(cart.total); // 19.99 | |
| const amount = Math.round(price * 100); // 1999 | |
| charge({ amount, currency: "jpy" }); // Tokyo | |
| charge({ amount, currency: "bgn" }); // Sofia | |
| charge({ amount: 10.99, currency: "kwd" }); // Kuwait City | |
| charge({ amount: total * 100, currency: "isk" }); | |
| const vat = Math.round(price * 0.2 * 100) / 100; | |
| const label = "$" + (amount / 100).toFixed(2); | |
| </textarea> | |
| <button id="go">Check these money lines</button> | |
| <div id="out"></div> | |
| <div class="next"><h2>In the full version</h2><ul><li>Every file in the repository</li><li>A CI gate</li><li>CSV, JSON or HTML report</li><li>Rewrite the retired codes in place</li><li><a href="https://getreadystack.com/api/buy/cl/polar_cl_acLdPf1V4wK36zA20Zdz5ARBivvJMJv1kJ9JI2lqb50">Get the full version - $29 once</a></li></ul></div> | |
| </div> | |
| <script> | |
| var RULES=[{"pattern": "Math\\.round\\s*\\(\\s*[^)]*\\*\\s*100\\s*\\)", "flags": "", "sev": "error", "message": "Math.round(x * 100) hardcodes two decimals: it charges 100x in the 16 zero-decimal currencies (JPY, KRW, VND, XOF, CLP and the rest) and 10x too little in the 3-decimal ones (KWD, BHD, JOD, OMR, TND). Scale by the currency's ISO 4217 exponent."}, {"pattern": "\\bamount\\s*[:=]\\s*[^,;)\\n]*\\*\\s*100\\b", "flags": "", "sev": "error", "message": "The amount field is built with * 100. Payment APIs take an integer of the currency's minor unit, and that unit is not 1/100 for every currency."}, {"pattern": "\\.toFixed\\(\\s*2\\s*\\)", "flags": "", "sev": "error", "message": "toFixed(2) prints 1200.00 for JPY, which has no minor unit at all, and truncates KWD 1.234 to 1.23 - a real 0.004 KWD error on every line. Format with Intl.NumberFormat and the ISO code."}, {"pattern": "\\bamount\\s*[:=]\\s*\\d+\\.\\d+", "flags": "", "sev": "error", "message": "amount is a decimal literal. The API expects an integer of minor units, so 10.99 is read as 10 or rejected outright."}, {"pattern": "parseFloat\\s*\\(\\s*[^)]*(amount|price|total|subtotal|cost|fee)", "flags": "i", "sev": "warn", "message": "parseFloat on money gives a binary float: 0.1 + 0.2 is 0.30000000000000004. Carry money as an integer of minor units, never as a float."}, {"pattern": "([\\\"\\'])bgn\\1", "flags": "", "sev": "error", "replace": "$1eur$1", "message": "BGN: Bulgaria adopted the euro on 2026-01-01 and the lev stopped being legal tender on 2026-02-01. Charge \"eur\" - the fixed conversion was 1 EUR = 1.95583 BGN."}, {"pattern": "\\bBGN\\b", "flags": "", "sev": "error", "replace": "EUR", "message": "BGN: Bulgaria adopted the euro on 2026-01-01 and the lev stopped being legal tender on 2026-02-01. Charge EUR - the fixed conversion was 1 EUR = 1.95583 BGN."}, {"pattern": "([\\\"\\'])hrk\\1", "flags": "", "sev": "error", "replace": "$1eur$1", "message": "HRK was withdrawn when Croatia adopted the euro on 2023-01-01. Charge \"eur\" - the fixed conversion was 1 EUR = 7.53450 HRK."}, {"pattern": "\\bHRK\\b", "flags": "", "sev": "error", "replace": "EUR", "message": "HRK was withdrawn when Croatia adopted the euro on 2023-01-01. Charge EUR - the fixed conversion was 1 EUR = 7.53450 HRK."}, {"pattern": "[\\\"\\'](?:vef|mro|std|sll|zwl|ltl|eek|skk)[\\\"\\']|\\b(?:VEF|MRO|STD|SLL|ZWL|LTL|EEK|SKK)\\b", "flags": "", "sev": "error", "message": "Retired ISO 4217 code. VEF is now VES, MRO is MRU, STD is STN, SLL is SLE, ZWL is ZWG, and LTL, EEK and SKK were replaced by EUR. A retired code is rejected, so the charge never happens."}, {"pattern": "[\\\"\\'](?:isk|ugx)[\\\"\\']|\\b(?:ISK|UGX)\\b", "flags": "", "sev": "warn", "message": "ISK and UGX are zero-decimal, but payment APIs still expect a two-decimal value whose last two digits are 00 - 5 ISK is amount 500. Sending 5 undercharges by 100x."}, {"pattern": "[\\\"\\'](?:bhd|iqd|jod|kwd|lyd|omr|tnd)[\\\"\\']|\\b(?:BHD|IQD|JOD|KWD|LYD|OMR|TND)\\b", "flags": "", "sev": "warn", "message": "3-decimal currency (ISO 4217 exponent 3). The amount is in thousandths and the last digit must be 0: KWD 1.5 is 1500, not 150."}, {"pattern": "[\\\"\\'](?:bif|clp|djf|gnf|jpy|kmf|krw|mga|pyg|rwf|vnd|vuv|xaf|xof|xpf)[\\\"\\']|\\b(?:BIF|CLP|DJF|GNF|JPY|KMF|KRW|MGA|PYG|RWF|VND|VUV|XAF|XOF|XPF)\\b", "flags": "", "sev": "info", "message": "Zero-decimal currency: the amount IS the charge. 500 JPY is amount 500, so any multiplication by 100 on this line charges 100x."}, {"pattern": "[\\\"\\'](?:huf|twd)[\\\"\\']|\\b(?:HUF|TWD)\\b", "flags": "", "sev": "info", "message": "HUF and TWD are charged with two decimals but paid out as zero-decimal: a manual payout amount must be evenly divisible by 100, so a balance of 1045 cannot be paid out in full."}, {"pattern": "[\\\"']\\$[\\\"']\\s*\\+", "flags": "", "sev": "warn", "message": "'$' + amount hardcodes the symbol and the en-US grouping. The same value must read 1.234,56 with the symbol after the number in de-DE. Use Intl.NumberFormat(locale, {style:'currency', currency})."}, {"pattern": "style\\s*:\\s*[\\\"']currency[\\\"'](?![^\\n]*\\bcurrency\\s*:)", "flags": "", "sev": "error", "message": "Intl.NumberFormat with style 'currency' and no currency option throws a TypeError at runtime, not at build time."}, {"pattern": "\\b(tax|vat|gst)\\w*\\s*=\\s*Math\\.round|Math\\.round\\s*\\(\\s*[^)]*(tax|vat|gst)", "flags": "i", "sev": "warn", "message": "Math.round on a float tax amount rounds 1.005 down to 1, because 1.005 is stored as 1.00499999999999989. Round on integer minor units instead."}, {"pattern": "\\b(rate|fx|exchange_rate|exchangeRate)\\s*[:=]\\s*\\d+\\.\\d{3,}", "flags": "i", "sev": "warn", "message": "A hardcoded FX rate. Only the euro conversion rates are legally fixed (1 EUR = 1.95583 BGN and 7.53450 HRK); every other rate moves daily."}, {"pattern": "\\b(cents|minorUnits|minor_units|smallestUnit)\\w*\\s*[:=]\\s*\\d+\\.\\d+", "flags": "i", "sev": "error", "message": "A variable named for minor units is holding a decimal. Minor units are whole numbers by definition, so this value is already wrong before it is sent."}, {"pattern": "\\b(amount|total|price|balance)\\w*\\s*[!=]==?\\s*\\d+\\.\\d+", "flags": "i", "sev": "warn", "message": "Comparing a money float for equality. 0.1 + 0.2 === 0.3 is false in every IEEE-754 language, so this branch silently never runs."}, {"pattern": "parseInt\\s*\\(\\s*[^)]*toFixed", "flags": "", "sev": "warn", "message": "parseInt(x.toFixed(...)) truncates at the first non-digit, so 1.99 becomes 1 - the decimal part is dropped, not scaled."}, {"pattern": "\\/\\s*100\\b", "flags": "", "sev": "info", "message": "Dividing by 100 to display an amount assumes two decimals. For JPY it divides the charge by 100; for KWD it leaves the last digit in the wrong place."}, {"pattern": "\\btoLocaleString\\s*\\(\\s*\\)", "flags": "", "sev": "info", "message": "toLocaleString() with no arguments formats with the machine's locale, so a server in Frankfurt prints a different number than a laptop in Chicago."}, {"pattern": "\\bNumber\\s*\\(\\s*[^)]*(amount|price|total)", "flags": "i", "sev": "info", "message": "Number() on a money string produces a float, the same trap as parseFloat: the value can no longer be summed or compared exactly."}]; | |
| function scan(t){var L=String(t).split(/\r?\n/),h=[],i,r,re; | |
| for(i=0;i<L.length;i++){for(var k=0;k<RULES.length;k++){r=RULES[k]; | |
| try{re=new RegExp(r.pattern,r.flags||'')}catch(e){continue} | |
| if(re.test(L[i]))h.push({line:i+1,msg:r.message})}} | |
| return h} | |
| document.getElementById('go').onclick=function(){ | |
| var t=document.getElementById('src').value,o=document.getElementById('out'); | |
| if(!t.trim()){o.innerHTML='';return} | |
| var h=scan(t); | |
| if(!h.length){o.innerHTML='<div class="ok">No money-line problems in this file.</div>';return} | |
| var s='<table><tr><th>Line</th><th>Finding</th></tr>'; | |
| for(var i=0;i<h.length;i++){s+='<tr><td class="n">'+h[i].line+'</td><td>'+ | |
| String(h[i].msg).replace(/&/g,'&').replace(/</g,'<')+'</td></tr>'} | |
| o.innerHTML=s+'</table>'}; | |
| document.getElementById('go').click(); | |
| </script> | |
| </html> | |
| <section data-yj-install="free" style="margin:2rem auto 1rem;max-width:46rem;padding:1.25rem 1.25rem;border:1px solid #cfe3d6;border-radius:12px;background:#f6fbf8;font:16px/1.6 system-ui,-apple-system,Segoe UI,sans-serif"><h2 style="margin:0 0 .4rem;font-size:1.2rem">Install free</h2><p style="margin:0 0 .8rem;color:#41505f">Free for the file open in your editor - no key, no limit. The workspace sweep and the report ask for a key.</p><a href="https://open-vsx.org/extension/readystack/currency-minor-unit-lint" rel="noopener" data-yj-install-door="ovsx" onclick="try{navigator.sendBeacon('/api/ev',new Blob([JSON.stringify({t:'install',slug:'currency-minor-unit-lint',src:'ovsx',path:location.pathname})],{type:'application/json'}))}catch(e){}" style="display:inline-block;margin:0 .5rem .5rem 0;padding:.6rem 1rem;border-radius:8px;background:#0f7b3f;color:#fff;text-decoration:none;font-weight:600">Open VSX (Cursor, VSCodium)</a><a href="https://www.npmjs.com/package/@readystack/currency-minor-unit-lint" rel="noopener" data-yj-install-door="npm" onclick="try{navigator.sendBeacon('/api/ev',new Blob([JSON.stringify({t:'install',slug:'currency-minor-unit-lint',src:'npm',path:location.pathname})],{type:'application/json'}))}catch(e){}" style="display:inline-block;margin:0 .5rem .5rem 0;padding:.6rem 1rem;border-radius:8px;background:#0f7b3f;color:#fff;text-decoration:none;font-weight:600">npm</a><a href="https://www.npmjs.com/package/@readystack/currency-minor-unit-lint" rel="noopener" data-yj-install-door="mcp" onclick="try{navigator.sendBeacon('/api/ev',new Blob([JSON.stringify({t:'install',slug:'currency-minor-unit-lint',src:'mcp',path:location.pathname})],{type:'application/json'}))}catch(e){}" style="display:inline-block;margin:0 .5rem .5rem 0;padding:.6rem 1rem;border-radius:8px;background:#0f7b3f;color:#fff;text-decoration:none;font-weight:600">MCP server (npx)</a><a href="https://hub.docker.com/r/getreadystack/currency-minor-unit-lint" rel="noopener" data-yj-install-door="docker" onclick="try{navigator.sendBeacon('/api/ev',new Blob([JSON.stringify({t:'install',slug:'currency-minor-unit-lint',src:'docker',path:location.pathname})],{type:'application/json'}))}catch(e){}" style="display:inline-block;margin:0 .5rem .5rem 0;padding:.6rem 1rem;border-radius:8px;background:#0f7b3f;color:#fff;text-decoration:none;font-weight:600">Docker Hub</a><pre style="margin:.5rem 0 0;padding:.6rem .8rem;background:#f3f5f8;border-radius:8px;font-size:.9rem;overflow:auto">npx @readystack/currency-minor-unit-lint <file> | |
| npx @readystack/currency-minor-unit-lint --mcp | |
| docker run --rm -v "$PWD:/w" getreadystack/currency-minor-unit-lint /w</pre><p style="margin:.6rem 0 0;font-size:.92rem;color:#41505f"><a href="https://getreadystack.com/api/buy/cl/polar_cl_l6iN1uWt0FwWu7tBsczD0jWpP2vxFM54Wdwqb3KPi1G" rel="nofollow" style="color:#0f7b3f;text-decoration:underline">Team? One key for every ReadyStack linter, 5 seats, $149 once</a></p></section> | |
| <section data-yj-alert="rule" style="margin:1rem auto 1.5rem;max-width:46rem;padding:1.1rem 1.25rem;border:1px solid #d9dee6;border-radius:12px;background:#fbfcfd;font:15px/1.55 system-ui,-apple-system,Segoe UI,sans-serif"><b style="font-size:1.05rem">Get the full rule table as a PDF</b><div style="opacity:.78;margin:.35rem 0 .8rem">All 24 rules Currency Minor Unit Lint checks, each with its source and date, on printable pages you can hand to your team or auditor. We email it now, and once more only if a rule changes. No newsletter.</div><div style="display:flex;gap:8px;flex-wrap:wrap"><input id="yjae" type="email" placeholder="you@example.com" style="flex:1;min-width:210px;padding:11px 13px;border:1px solid rgba(140,150,165,.34);border-radius:8px;background:#fff;color:inherit"><button id="yjab" style="padding:11px 18px;border-radius:8px;border:0;cursor:pointer;background:#0f7b3f;color:#fff;font:600 15px system-ui,sans-serif">Email me the PDF</button></div><div id="yjao" hidden style="margin-top:.6rem;color:#0f7b3f">Sent. It arrives within the hour - check spam too.</div><script>(function(){var b=document.getElementById("yjab"),e=document.getElementById("yjae");if(!b||!e)return;b.addEventListener("click",function(){var v=e.value.trim();if(!/.+@.+\..+/.test(v))return;try{localStorage.setItem("yj_em","1");}catch(_){}fetch(location.origin+"/api/subscribe",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({email:v,slug:"currency-minor-unit-lint",lang:"en",source:"rule_table"+((b.closest&&b.closest("section")&&b.closest("section").getAttribute("data-yj-moved"))?"_at_result":"")})}).then(function(r){var o=document.getElementById("yjao");o.hidden=false;if(!r.ok){o.textContent="Could not save - please try again.";}}).catch(function(){var o=document.getElementById("yjao");o.hidden=false;o.textContent="Could not save - please try again.";});});})();</script><script>(function(){var e=document.getElementById("yjae"),s=e&&e.closest?e.closest("section"):null,o=document.getElementById("out");if(!s||!o)return;var d=false;function mv(ev){if(d)return;if(ev&&ev.target&&s.contains(ev.target))return;d=true;var b=o;while(b.parentNode&&b.parentNode!==document.body&&b.parentNode.tagName!=="MAIN"&&b.parentNode.tagName!=="ARTICLE")b=b.parentNode;if(!b.parentNode||b===s||b.contains(s))return;try{b.parentNode.insertBefore(s,b.nextSibling);s.setAttribute("data-yj-moved","1");}catch(_){}}document.addEventListener("input",mv,true);document.addEventListener("change",mv,true);})();</script></section> | |
| <section data-yj-cta="paid" style="margin:3rem auto;max-width:46rem;padding:1.5rem 1.25rem;border:1px solid #d8dee9;border-radius:12px;background:#fbfcfe;font:16px/1.6 system-ui,-apple-system,Segoe UI,sans-serif"><h2 style="margin:0 0 .5rem;font-size:1.25rem">Get the complete version <span class="yj-price">$29</span></h2><p style="margin:.25rem 0 1rem;color:#41505f">This page is the working piece. The full pack has everything below.</p><p style="margin:0 0 1rem;color:#41505f">Finds the money lines that send the wrong amount to a payment API: the x100 that charges 100x in JPY, the toFixed(2) that truncates KWD, and the currency codes that stopped being legal tende</p><p style="margin:0 0 1rem;color:#5b6b7c;font-size:.94rem">Payment processors publish a $15.00 fee for every dispute received, and it is not returned when you lose the dispute.</p><a href="https://getreadystack.com/api/buy/cl/polar_cl_acLdPf1V4wK36zA20Zdz5ARBivvJMJv1kJ9JI2lqb50" rel="nofollow" style="display:inline-block;padding:.7rem 1.3rem;border-radius:8px;background:#1b64f2;color:#fff;text-decoration:none;font-weight:600">Buy the full version — $29</a></section> | |
| <!--yj-share-s101--><div style="margin:28px 0 8px;text-align:center"><button id="yjcp" type="button" style="font:inherit;padding:10px 18px;border:1px solid #8884;border-radius:10px;background:transparent;cursor:pointer">Copy link</button><button id="yjimg" type="button" style="font:inherit;padding:10px 18px;margin-left:8px;border:1px solid #8884;border-radius:10px;background:transparent;cursor:pointer">Save result as image</button></div><script>(function(){var b=document.getElementById("yjimg");if(!b)return;b.addEventListener("click",function(){var o=document.getElementById("out");var txt=(o&&(o.innerText||o.textContent)||"").trim();if(!txt){b.textContent="(no result yet)";setTimeout(function(){b.textContent="Save result as image";},1500);return;}var lines=txt.split(String.fromCharCode(10)).filter(function(x){return x.trim();}).slice(0,10);var W=1200,P=56,c=document.createElement("canvas");c.width=W;var lh=38,H=P*2+70+lines.length*lh+60;c.height=H;var x=c.getContext("2d");x.fillStyle="#1a1512";x.fillRect(0,0,W,H);x.fillStyle="#f3e9d2";x.font="600 34px system-ui,Segoe UI,Arial";var t=(document.title||"").split("|")[0].trim();x.fillText(t.slice(0,54),P,P+34);x.font="22px ui-monospace,Menlo,Consolas,monospace";x.fillStyle="#e8dcc4";for(var i=0;i<lines.length;i++){x.fillText(lines[i].slice(0,88),P,P+70+i*lh+22);}x.font="600 22px system-ui,Segoe UI,Arial";x.fillStyle="#c9a227";var d=location.hostname.replace("www.","");x.fillText(d,W-P-x.measureText(d).width,H-P+6);c.toBlob(function(bl){var u=URL.createObjectURL(bl),a=document.createElement("a");a.href=u;a.download=(t||"result").replace(/[^a-z0-9]+/gi,"-").toLowerCase()+"-"+d+".png";document.body.appendChild(a);a.click();setTimeout(function(){URL.revokeObjectURL(u);a.remove();},2000);},"image/png");});})();</script><script>(function(){var b=document.getElementById("yjcp");if(!b)return;var t=b.textContent;b.addEventListener("click",function(){var u=location.href.split("#")[0];function d(){b.textContent="Link copied";setTimeout(function(){b.textContent=t;},1800);}if(navigator.clipboard&&navigator.clipboard.writeText){navigator.clipboard.writeText(u).then(d,function(){prompt("",u);});}else{prompt("",u);}});})();</script><div class="yj-home-find"><div class="yj-lgbar"><a href="/" class="yj-lg on">EN</a><a href="/de/" class="yj-lg">DE</a><a href="/ja/" class="yj-lg">JA</a><a href="/es/" class="yj-lg">ES</a><a href="/pt/" class="yj-lg">PT</a></div><section class="yj-find"><h2>Find a tool</h2><input id="yjq" type="search" autocomplete="off" placeholder="Type a product name, format or task"><div id="yjr"></div></section><style>.yj-lgbar{position:absolute;top:14px;right:18px;display:flex;gap:6px;z-index:60}.yj-lg{font:600 12px/1 system-ui,sans-serif;padding:6px 9px;border-radius:4px;text-decoration:none;color:#8b96a3;border:1px solid rgba(140,150,165,.28)}.yj-lg:hover{color:#e8eaed;border-color:rgba(160,175,195,.6)}.yj-lg.on{color:#0e1116;background:#cfd6de;border-color:#cfd6de}.yj-find{max-width:760px;margin:34px auto 8px;padding:0 20px}.yj-find h2{font:600 20px/1.3 system-ui,sans-serif;margin:0 0 10px}#yjq{width:100%;box-sizing:border-box;padding:13px 15px;font:400 15.5px system-ui,sans-serif;border:1px solid rgba(140,150,165,.34);border-radius:8px;background:rgba(255,255,255,.04);color:inherit}#yjq:focus{outline:none;border-color:#7FD1C4}#yjr{margin-top:10px;display:flex;flex-direction:column;gap:6px}#yjr a{display:flex;justify-content:space-between;gap:12px;padding:11px 14px;border:1px solid rgba(140,150,165,.22);border-radius:6px;text-decoration:none;font:400 14.5px/1.4 system-ui,sans-serif;color:inherit}#yjr a:hover{border-color:#7FD1C4}#yjr .pr{font:600 13px system-ui,sans-serif;color:#5fbf9a;white-space:nowrap}</style><script>(function(){var q=document.getElementById("yjq"),r=document.getElementById("yjr"),D=null;function load(){if(D)return Promise.resolve(D);return fetch("/search-index.json").then(function(x){return x.json();}).then(function(j){D=j;return j;}).catch(function(){D=[];return D;});}function esc(t){return String(t).replace(/[&<>"]/g,function(c){return {"&":"&","<":"<",">":">",'"':"""}[c];});}function run(){var v=q.value.trim().toLowerCase();if(v.length<2){r.innerHTML="";return;}load().then(function(j){var w=v.split(/\s+/),o=[];for(var i=0;i<j.length&&o.length<12;i++){var t=j[i].t.toLowerCase();var ok=true;for(var k=0;k<w.length;k++){if(t.indexOf(w[k])<0){ok=false;break;}}if(ok)o.push(j[i]);}r.innerHTML=o.length?o.map(function(x){return "<a href=\""+esc(x.u)+"\">"+"<span>"+esc(x.t)+"</span>"+(x.p?"<span class=\"pr\">$"+x.p+"</span>":"")+"</a>";}).join(""):"";});}q.addEventListener("input",run);q.addEventListener("focus",load);})();</script></div> | |
| <script data-yj="yj-view-beacon">(function(){try{var p=location.pathname;var k='yjv:'+p;if(sessionStorage.getItem(k))return;sessionStorage.setItem(k,'1');fetch('/api/ev',{method:'POST',keepalive:true,headers:{'Content-Type':'application/json'},body:JSON.stringify({path:p,lang:"en",ref:(document.referrer||''),src:(function(){try{return (new URLSearchParams(location.search).get('ref')||'').replace(/[^a-z_]/g,'').slice(0,12)}catch(e){return ''}})()})}).catch(function(){});}catch(e){}})();</script> | |
| <script data-yj="yj-use-beacon">(function(){try{var o=document.getElementById('out');if(!o)return;var p=location.pathname,k='yju:'+p,sent=false;function fire(){if(sent||sessionStorage.getItem(k))return;if(!(o.textContent||'').replace(/\s+/g,'').length)return;sent=true;sessionStorage.setItem(k,'1');fetch('/api/ev',{method:'POST',keepalive:true,headers:{'Content-Type':'application/json'},body:JSON.stringify({path:p,lang:"en",t:'use',src:(function(){try{return (new URLSearchParams(location.search).get('ref')||'').replace(/[^a-z_]/g,'').slice(0,12)}catch(e){return ''}})()})}).catch(function(){});}var mo=new MutationObserver(function(){fire();if(sent)mo.disconnect();});mo.observe(o,{childList:true,characterData:true,subtree:true});}catch(e){}})();</script> | |
| <section data-yj="yj-facts-v1" style="max-width:920px;margin:48px auto 0;padding:0 20px;font-family:system-ui,sans-serif"><style>.yj-article p{font:400 15.5px/1.7 system-ui,sans-serif;color:#26313b;margin:0 0 12px;max-width:72ch}.yj-article h2,.yj-article h3,.yj-article h4{font:600 17px/1.35 system-ui,sans-serif;color:#1b1f24;margin:22px 0 8px}.yj-article table{border-collapse:collapse;margin:10px 0 16px;font:14px/1.45 system-ui,sans-serif}.yj-article th,.yj-article td{border:1px solid #e2e6ea;padding:6px 10px;text-align:left}.yj-article th{background:#f4f6f8}.yj-article pre{background:#0f1720;color:#e6edf3;padding:12px 14px;border-radius:8px;overflow:auto;font:13px/1.5 ui-monospace,Menlo,monospace}.yj-article code{font:13px ui-monospace,Menlo,monospace;background:#f1f3f5;padding:1px 4px;border-radius:3px}.yj-article pre code{background:none;padding:0}.yj-article ul,.yj-article ol{font:15px/1.6 system-ui,sans-serif;color:#26313b}</style><p style="font:400 13px/1.5 system-ui,sans-serif;color:#6b7680;margin:0 0 14px"><time datetime="2026-09-10">Updated 2026-09-10</time> · ReadyStack</p><script type="application/ld+json">{"@context":"https://schema.org","@type":"WebPage","dateModified":"2026-09-10","author":{"@type":"Organization","name":"ReadyStack"}}</script><h2 style="font:600 21px/1.3 system-ui,sans-serif;margin:0 0 6px;color:#1b1f24">Worked example</h2><p style="font:400 14.5px/1.5 system-ui,sans-serif;margin:0 0 16px;color:#5b6570">Real numbers from this tool, line by line.</p><div class="yj-article"><p><img src="https://getreadystack.com/img/promo/sku31949_result_card.jpg" alt="Six money lines in this checkout file charge the wrong amount, and every test passes" loading="lazy" style="max-width:100%;border-radius:6px"></p> | |
| <p><strong>Six of the money lines below send an amount the payment API reads differently from what the backend engineer in Sofia, Tokyo or Kuwait City meant - and nothing fails, because every one of them is valid code.</strong></p> | |
| <pre><code>line 3 [error] Math.round(x * 100) charges 100x in the 16 zero-decimal currencies (JPY, KRW, VND...) | |
| line 5 [info] Zero-decimal currency: 500 JPY is amount 500 - the * 100 above charges 100x here | |
| line 6 [error] BGN stopped being legal tender on 2026-02-01. Charge EUR (1 EUR = 1.95583 BGN) | |
| line 7 [error] amount is a decimal literal - 10.99 is read as 10 or rejected outright | |
| line 7 [warn] KWD is a 3-decimal currency: 1.5 is 1500, not 150, and the last digit must be 0 | |
| line 8 [warn] ISK needs a two-decimal value ending in 00 - 5 ISK is amount 500 | |
| line 10 [warn] Math.round on a float tax rounds 1.005 down to 1 (it is stored as 1.00499999999999989) | |
| line 11 [error] toFixed(2) prints 1200.00 for JPY and truncates KWD 1.234 to 1.23 | |
| --- 15 findings over 8 lines, from 24 rules ---</code></pre> | |
| <h3>The line every assistant writes</h3> | |
| <pre><code>const amount = Math.round(price * 100);</code></pre> | |
| <p>This is correct for the dollar, the euro and the pound. It is wrong for the sixteen currencies that have no minor unit at all - BIF, CLP, DJF, GNF, JPY, KMF, KRW, MGA, PYG, RWF, UGX, VND, VUV, XAF, XOF and XPF. In those currencies the amount <strong>is</strong> the charge: 500 JPY is the integer 500. Multiply by a hundred and a 5,000 yen order bills 500,000 yen.</p> | |
| <p>It is wrong in the other direction for the currencies with three decimals - BHD, IQD, JOD, KWD, LYD, OMR and TND. Those amounts are in thousandths, and processors additionally require the last digit to be zero. Sending 150 for KWD 1.5 undercharges by a factor of ten on every invoice.</p> | |
| <h3>The code that stopped being true on a date</h3> | |
| <p>Bulgaria adopted the euro on 1 January 2026 at the fixed rate of 1 EUR = 1.95583 BGN. The lev and the euro circulated together through January, and since 1 February 2026 the euro has been the sole legal currency. <code>currency: "bgn"</code> is not a style choice any more; it is a code that a processor will refuse.</p> | |
| <p>Croatia did the same on 1 January 2023 at 1 EUR = 7.53450 HRK. VEF became VES, MRO became MRU, STD became STN, SLL became SLE, ZWL became ZWG. Every one of those is still sitting in somebody's config file, because a retired currency code is a perfectly valid three-character string until the API answers.</p> | |
| <h3>Why the assistant cannot catch this</h3> | |
| <p>Ask a coding assistant whether <code>Math.round(price * 100)</code> is right and it will say yes, because for the currency it was shown most often it is. Ask it whether BGN is a live currency code and it will say yes, because it learned a world in which it was. The blind spot is not carelessness - it is the training date, and it is exactly the same date the code was written on.</p> | |
| <p>Nor does the test suite help. <code>Math.round(19.99 * 100)</code> returns 1999 whatever currency you attach to it. The number is only wrong once it reaches the processor, and by then it is a refund, a dispute and a support thread.</p> | |
| <h3>What the rules check</h3> | |
| <p>Twenty-four rules, each one checkable:</p> | |
| <ul><li>the scaling rules - <code>* 100</code>, <code>/ 100</code>, <code>toFixed(2)</code>, decimal literals in an <code>amount</code> field</li><li>the float rules - <code>parseFloat</code> on money, equality comparisons, <code>parseInt(x.toFixed())</code>, <code>Math.round</code> on a tax line</li><li>the currency rules - zero-decimal, three-decimal, the ISK and UGX trailing zeros, the HUF and TWD payout divisibility</li><li>the dated rules - BGN, HRK and the six other retired ISO 4217 codes, each with its replacement and the day it changed</li><li>the formatting rules - <code>"$" +</code> concatenation, <code>Intl.NumberFormat</code> with <code>style: 'currency'</code> and no currency option, bare <code>toLocaleString()</code></li></ul> | |
| <p>Checking the file you have open is free: all twenty-four rules, every finding, nothing hidden and no watermark. The licence key covers the other job - every file in the repository, a CI gate that fails the build before a wrong amount can merge, a CSV, JSON or HTML report, and rewriting the retired codes in place. $29 once, 7-day full refund.</p></div><h2 style="font:600 19px/1.3 system-ui,sans-serif;margin:30px 0 10px;color:#1b1f24">15 seconds — what it actually does</h2><video controls muted playsinline preload="none" poster="/img/promo/sku31949_result_card.jpg" style="width:100%;max-width:360px;border-radius:10px;display:block;margin:0 auto"><source src="/media/sku31949_short.mp4" type="video/mp4"></video><h2 style="font:600 21px/1.3 system-ui,sans-serif;margin:34px 0 6px;color:#1b1f24">Questions people ask</h2><details style="border-bottom:1px solid #e6e9ed;padding:10px 0"><summary style="cursor:pointer;font:600 15px/1.4 system-ui,sans-serif;color:#1b1f24">What does Currency Minor Unit Lint actually do?</summary><p style="margin:8px 0 2px;font:400 14.5px/1.6 system-ui,sans-serif;color:#33404b">It reads the file you have open and flags every line that builds a money amount the payment API will read differently from what you meant. Each finding gives the line number, the currency, its ISO 4217 exponent and, where a country changed codes, the date. It ships 24 rules.</p></details><details style="border-bottom:1px solid #e6e9ed;padding:10px 0"><summary style="cursor:pointer;font:600 15px/1.4 system-ui,sans-serif;color:#1b1f24">Who is this for?</summary><p style="margin:8px 0 2px;font:400 14.5px/1.6 system-ui,sans-serif;color:#33404b">Backend and platform engineers who write the charge call - the person who owns the repository the day finance asks why a Tokyo customer was billed a hundred times the price. It is aimed at teams selling into Japan, Korea, the Gulf states or the euro countries that recently switched currency codes.</p></details><details style="border-bottom:1px solid #e6e9ed;padding:10px 0"><summary style="cursor:pointer;font:600 15px/1.4 system-ui,sans-serif;color:#1b1f24">Why can I not just ask a chatbot whether my amount code is right?</summary><p style="margin:8px 0 2px;font:400 14.5px/1.6 system-ui,sans-serif;color:#33404b">Because the assistant writes Math.round(price * 100), which is correct for dollars and euros and wrong for the sixteen zero-decimal currencies. And it was trained before Bulgaria adopted the euro on 2026-01-01, so it still confirms BGN as a live currency code - the lev stopped being legal tender on 2026-02-01. A dated table does not have that blind spot.</p></details><details style="border-bottom:1px solid #e6e9ed;padding:10px 0"><summary style="cursor:pointer;font:600 15px/1.4 system-ui,sans-serif;color:#1b1f24">What is free and what needs a licence key?</summary><p style="margin:8px 0 2px;font:400 14.5px/1.6 system-ui,sans-serif;color:#33404b">Checking the open file is free and complete: all 24 rules run, every finding shows the currency and the date, and nothing is hidden or watermarked. The licence covers a different job - scanning every file in the repository, failing a CI build, exporting a report, and rewriting retired codes in place.</p></details><details style="border-bottom:1px solid #e6e9ed;padding:10px 0"><summary style="cursor:pointer;font:600 15px/1.4 system-ui,sans-serif;color:#1b1f24">What does this cost compared with getting it wrong once?</summary><p style="margin:8px 0 2px;font:400 14.5px/1.6 system-ui,sans-serif;color:#33404b">The extension is 29 US dollars once,. Payment processors publish a fee of 15.00 dollars for every dispute received, and that fee is not returned when you lose. One mis-scaled charge produces a refund, a dispute fee and a support thread.</p></details><details style="border-bottom:1px solid #e6e9ed;padding:10px 0"><summary style="cursor:pointer;font:600 15px/1.4 system-ui,sans-serif;color:#1b1f24">Why not just ask ChatGPT or another AI chat?</summary><p style="margin:8px 0 2px;font:400 14.5px/1.6 system-ui,sans-serif;color:#33404b">A general AI chat answers from training data with a cutoff date, cannot read your repository and names no rule version. Currency Minor Unit Lint checks the file you open against 24 rules from a rule set dated 2026-09-20, and points at the exact line. For a filing, an audit or a client you need that dated result on your own files.</p></details><script type="application/ld+json" data-yj="yj-faq-ld">{"@context": "https://schema.org", "@type": "FAQPage", "mainEntity": [{"@type": "Question", "name": "What does Currency Minor Unit Lint actually do?", "acceptedAnswer": {"@type": "Answer", "text": "It reads the file you have open and flags every line that builds a money amount the payment API will read differently from what you meant. Each finding gives the line number, the currency, its ISO 4217 exponent and, where a country changed codes, the date. It ships 24 rules."}}, {"@type": "Question", "name": "Who is this for?", "acceptedAnswer": {"@type": "Answer", "text": "Backend and platform engineers who write the charge call - the person who owns the repository the day finance asks why a Tokyo customer was billed a hundred times the price. It is aimed at teams selling into Japan, Korea, the Gulf states or the euro countries that recently switched currency codes."}}, {"@type": "Question", "name": "Why can I not just ask a chatbot whether my amount code is right?", "acceptedAnswer": {"@type": "Answer", "text": "Because the assistant writes Math.round(price * 100), which is correct for dollars and euros and wrong for the sixteen zero-decimal currencies. And it was trained before Bulgaria adopted the euro on 2026-01-01, so it still confirms BGN as a live currency code - the lev stopped being legal tender on 2026-02-01. A dated table does not have that blind spot."}}, {"@type": "Question", "name": "What is free and what needs a licence key?", "acceptedAnswer": {"@type": "Answer", "text": "Checking the open file is free and complete: all 24 rules run, every finding shows the currency and the date, and nothing is hidden or watermarked. The licence covers a different job - scanning every file in the repository, failing a CI build, exporting a report, and rewriting retired codes in place."}}, {"@type": "Question", "name": "What does this cost compared with getting it wrong once?", "acceptedAnswer": {"@type": "Answer", "text": "The extension is 29 US dollars once,. Payment processors publish a fee of 15.00 dollars for every dispute received, and that fee is not returned when you lose. One mis-scaled charge produces a refund, a dispute fee and a support thread."}}, {"@type": "Question", "name": "Why not just ask ChatGPT or another AI chat?", "acceptedAnswer": {"@type": "Answer", "text": "A general AI chat answers from training data with a cutoff date, cannot read your repository and names no rule version. Currency Minor Unit Lint checks the file you open against 24 rules from a rule set dated 2026-09-20, and points at the exact line. For a filing, an audit or a client you need that dated result on your own files."}}]}</script><div id="yj-ask" style="margin:36px 0 0;padding:18px 18px 16px;border:1px solid #e2e6ea;border-radius:10px;background:#fbfbfc"><h2 style="font:600 19px/1.3 system-ui,sans-serif;margin:0 0 4px;color:#1b1f24">Ask about this tool</h2><p style="font:400 13.5px/1.5 system-ui,sans-serif;margin:0 0 12px;color:#5b6570">One question, answered by the person who built it. Your email only if you want the answer sent.</p><form onsubmit="return yjAsk(event)" style="display:grid;gap:8px"><textarea name="question" required minlength="10" maxlength="2000" rows="3" placeholder="Your question" style="font:14px/1.5 system-ui,sans-serif;padding:10px;border:1px solid #cfd6dd;border-radius:6px"></textarea><div style="display:flex;gap:8px;flex-wrap:wrap"><input type="email" name="email" placeholder="Email (optional)" style="flex:1;min-width:200px;font:14px system-ui,sans-serif;padding:9px 10px;border:1px solid #cfd6dd;border-radius:6px"><button type="submit" style="font:600 14px system-ui,sans-serif;padding:9px 18px;border:0;border-radius:6px;background:#1b1f24;color:#fff;cursor:pointer">Send</button></div><p id="yj-ask-msg" style="font:13px system-ui,sans-serif;margin:2px 0 0;color:#0a6238;min-height:1em"></p></form><script>function yjAsk(e){e.preventDefault();var f=e.target,m=document.getElementById("yj-ask-msg");m.style.color="#0a6238";fetch(location.origin+"/api/ask",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({sku:31949,slug:"currency-minor-unit-lint",lang:"en",question:f.question.value,email:f.email.value,page:location.pathname})}).then(function(r){if(!r.ok)throw 0;m.textContent="Thanks \u2014 noted. Answers land on this page and, if you left an email, in your inbox.";f.question.value="";}).catch(function(){m.style.color="#a8551f";m.textContent="Could not send. Please try again in a minute.";});return false;}</script></div></section><section class="yj-related" style="max-width:920px;margin:56px auto 40px;padding:0 20px"><h2 style="font:600 21px/1.3 system-ui,sans-serif;margin:0 0 6px;color:#1b1f24">Same job, next tool</h2><p style="font:400 14.5px/1.5 system-ui,sans-serif;margin:0 0 20px;color:#5b6570">Free in the browser. The full version is a one-time licence.</p><div style="display:grid;grid-template-columns:repeat(auto-fill,minmax(250px,1fr));gap:14px"><a class="yj-rel-c" href="https://getreadystack.com/tools/angular-removed-api-lint"><span class="yj-rel-t">Angular Removed-API Lint</span><span class="yj-rel-p">$29</span></a><a class="yj-rel-c" href="https://getreadystack.com/tools/cms-0057-api-gate"><span class="yj-rel-t">CMS-0057 API Gate</span><span class="yj-rel-p">$29</span></a><a class="yj-rel-c" href="https://getreadystack.com/tools/play-release-blocker-lint"><span class="yj-rel-t">Google Play Release Blocker Lint - target API 36, Billing 8, 16 KB</span><span class="yj-rel-p">$29</span></a><a class="yj-rel-c" href="https://getreadystack.com/tools/ingress-nginx-retirement-lint"><span class="yj-rel-t">Ingress-NGINX Retirement Lint</span><span class="yj-rel-p">$29</span></a><a class="yj-rel-c" href="https://getreadystack.com/tools/k8s-removed-api-lint"><span class="yj-rel-t">Kubernetes Removed API Lint</span><span class="yj-rel-p">$29</span></a><a class="yj-rel-c" href="https://getreadystack.com/tools/cloud-manifest-audit-kit"><span class="yj-rel-t">Manifest Snippets + Removed-API Audit (8 Sets)</span><span class="yj-rel-p">$29</span></a></div><style>.yj-rel-c{display:flex;flex-direction:column;gap:8px;padding:15px 16px;border:1px solid #e2e6ea;border-radius:6px;text-decoration:none;background:#fff}.yj-rel-c:hover{border-color:#b9c2cb;background:#fafbfc}.yj-rel-t{font:500 14.5px/1.42 system-ui,sans-serif;color:#1b1f24}.yj-rel-p{font:600 13px system-ui,sans-serif;color:#0a6238}</style></section> |