# Correct currency code usage to prevent conversion errors

- **Pattern:** `ab-001175` (`ecommerce-payment-security.payment-errors.correct-currency-codes`)
- **Severity:** low
- **Lifecycle:** active
- **Last modified:** 2026-04-18
- **Canonical URL:** https://auditbuffet.com/patterns/ab-001175
- **License:** CC-BY-4.0 — attribute to AuditBuffet Pattern Catalog (https://auditbuffet.com/patterns/ab-001175)

## Why it matters

Payment providers require amounts in the smallest currency unit — cents for USD, pence for GBP — but zero-decimal currencies (JPY, KRW, VND, and 13 others) are already expressed in their smallest unit and must not be multiplied by 100. A checkout that applies `price * 100` to a JPY price of ¥1,000 submits a charge for ¥100,000 — a 100x overcharge. CWE-681 (Incorrect Conversion between Numeric Types) captures this integer arithmetic error. The ISO 4217 functional correctness requirement means using the wrong currency code (e.g., `"JP"` instead of `"jpy"`) will also cause the payment to be rejected or processed in the wrong currency. This is a silent production defect: it passes all tests that mock the Stripe API but fails live with real amounts.

## Severity rationale

Low because zero-decimal currency miscalculation causes 100x overcharges on live transactions for affected currencies, and invalid currency codes cause silent payment failures.

## Remediation

Centralize currency-to-amount conversion in a single utility function that handles zero-decimal currencies correctly.

```ts
// lib/currency.ts
const ZERO_DECIMAL = new Set([
  'bif', 'clp', 'gnf', 'jpy', 'kmf', 'krw', 'mga',
  'pyg', 'rwf', 'ugx', 'vnd', 'vuv', 'xaf', 'xof', 'xpf'
])

export function toProviderAmount(amount: number, currency: string): number {
  const code = currency.toLowerCase()
  if (!code.match(/^[a-z]{3}$/)) throw new Error(`Invalid ISO 4217 currency code: ${currency}`)
  return ZERO_DECIMAL.has(code) ? Math.round(amount) : Math.round(amount * 100)
}

// app/api/payment/route.ts
const paymentIntent = await stripe.paymentIntents.create({
  amount: toProviderAmount(priceInLocalUnits, currency),
  currency: currency.toLowerCase(),  // Stripe requires lowercase ISO 4217
})
```

Write a unit test for JPY, KRW, and USD to lock in the conversion logic before any multi-currency work ships.

## Detection

- **ID:** `correct-currency-codes`
- **Severity:** `low`
- **What to look for:** Enumerate all payment provider API calls that specify a currency parameter. For each, verify the currency code uses the ISO 4217 standard three-letter format (e.g., `usd`, `eur`, `gbp`, `jpy`). Count total charge/intent creation calls and check each for correct zero-decimal currency handling — Stripe and most providers expect amounts in the smallest currency unit, but zero-decimal currencies (JPY, KRW, VND) should not be multiplied by 100. Check whether the app hardcodes a single currency or dynamically sets it, and whether dynamic currency values are validated.
- **Pass criteria:** All charge requests use valid ISO 4217 currency codes. No more than 0 charge calls should use invalid currency codes or incorrect zero-decimal handling. Zero-decimal currencies are handled correctly (no multiplication by 100). Currency codes match the actual currency the customer is being charged in. Report the count: "X charge calls found, all use valid ISO 4217 codes."
- **Fail criteria:** Invalid currency codes are used, or zero-decimal currencies (JPY, KRW) are incorrectly multiplied by 100, resulting in charges 100x the intended amount.
- **Skip (N/A) when:** The project is a single-currency implementation targeting only USD (or another single major currency) with no dynamic currency selection.
- **Detail on fail:** Describe the error. Example: `"stripe.paymentIntents.create({ amount: price * 100, currency: 'JPY' }) multiplies JPY by 100 — a ¥1,000 item would charge ¥100,000 because JPY is a zero-decimal currency"`
- **Remediation:** Handle currency unit conversion correctly:

  ```ts
  // Zero-decimal currencies should NOT be multiplied by 100
  const ZERO_DECIMAL_CURRENCIES = new Set(['bif', 'clp', 'gnf', 'jpy', 'kmf', 'krw', 'mga', 'pyg', 'rwf', 'ugx', 'vnd', 'vuv', 'xaf', 'xof', 'xpf'])

  function toProviderAmount(amount: number, currency: string): number {
    const code = currency.toLowerCase()
    return ZERO_DECIMAL_CURRENCIES.has(code) ? Math.round(amount) : Math.round(amount * 100)
  }

  const paymentIntent = await stripe.paymentIntents.create({
    amount: toProviderAmount(priceInLocalUnits, currency),
    currency: currency.toLowerCase(), // Must be lowercase ISO 4217
  })
  ```

## External references

- cwe CWE-681
- iso-25010 functional-suitability.functional-correctness

Taxons: data-integrity

HTML version: https://auditbuffet.com/patterns/ab-001175
