{"version":3,"sources":["../src/money/orgCurrency.ts","../src/money/format.ts","../src/money/parse.ts"],"names":[],"mappings":";;;AAgCO,IAAM,uBAAA,GAA0B;AAkBvC,IAAI,uBAAA,GAAuD,IAAA;AAOpD,SAAS,wBAAwB,QAAA,EAA6C;AACnF,EAAA,uBAAA,GAA0B,QAAA;AAC5B;AAGO,SAAS,uBAAA,GAAuD;AACrE,EAAA,OAAO,uBAAA;AACT;AAOO,SAAS,+BAAA,GAA0C;AACxD,EAAA,OAAO,yBAAyB,IAAA,IAAQ,uBAAA;AAC1C;;;AC1CO,IAAM,qBAAA,GAAwB;AA2C9B,SAAS,WAAA,CAAY,QAAgB,QAAA,EAAgD;AAC1F,EAAA,MAAM,OAAA,GAA8B,OAAO,QAAA,KAAa,QAAA,GAAW,EAAE,YAAA,EAAc,QAAA,EAAS,GAAK,QAAA,IAAY,EAAC;AAC9G,EAAA,MAAM,EAAE,YAAA,EAAc,aAAA,EAAe,MAAA,GAAS,qBAAA,EAAuB,QAAO,GAAI,OAAA;AAIhF,EAAA,MAAM,WAAA,GAAc,YAAA,KAAiB,MAAA,GAAY,uBAAA,EAAwB,GAAI,IAAA;AAC7E,EAAA,MAAM,aAAA,GAAgB,YAAA,IAAgB,WAAA,EAAa,IAAA,IAAQ,uBAAA;AAK3D,EAAA,MAAM,sBAAA,GACJ,aAAA,KAAkB,MAAA,GAAY,aAAA,GAAiB,aAAa,aAAA,IAAiB,IAAA;AAI/E,EAAA,MAAM,cAAA,GAAiB,cAAc,WAAA,EAAY;AAEjD,EAAA,IAAI;AACF,IAAA,MAAM,SAAA,GAAY,IAAI,IAAA,CAAK,YAAA,CAAa,MAAA,EAAQ;AAAA,MAC9C,KAAA,EAAO,UAAA;AAAA,MACP,QAAA,EAAU,cAAA;AAAA;AAAA,MAEV,eAAA,EAAiB,cAAA;AAAA,MACjB,GAAI,0BAA0B,IAAA,GAC1B,EAAE,uBAAuB,sBAAA,EAAwB,qBAAA,EAAuB,sBAAA,EAAuB,GAC/F;AAAC,KACN,CAAA;AACD,IAAA,OAAO,SAAA,CAAU,OAAO,MAAM,CAAA;AAAA,EAChC,CAAA,CAAA,MAAQ;AACN,IAAA,MAAM,iBAAiB,sBAAA,IAA0B,CAAA;AACjD,IAAA,MAAM,SAAS,MAAA,IAAU,cAAA;AAEzB,IAAA,MAAM,IAAA,GAAO,MAAA,GAAS,CAAA,GAAI,GAAA,GAAM,EAAA;AAChC,IAAA,OAAO,CAAA,EAAG,IAAI,CAAA,EAAG,MAAM,CAAA,EAAG,IAAA,CAAK,GAAA,CAAI,MAAM,CAAA,CAAE,OAAA,CAAQ,cAAc,CAAC,CAAA,CAAA;AAAA,EACpE;AACF;AA8BO,SAAS,kBAAA,CAAmB,QAAgB,OAAA,EAA4C;AAC7F,EAAA,MAAM,EAAE,YAAA,EAAc,MAAA,EAAQ,MAAA,GAAS,uBAAsB,GAAI,OAAA;AAEjE,EAAA,MAAM,GAAA,GAAM,iBAAA,CAAkB,YAAA,EAAc,MAAA,EAAQ,MAAM,CAAA;AAC1D,EAAA,MAAM,IAAA,GAAO,MAAA,GAAS,CAAA,GAAI,GAAA,GAAM,EAAA;AAChC,EAAA,MAAM,GAAA,GAAM,IAAA,CAAK,GAAA,CAAI,MAAM,CAAA;AAE3B,EAAA,IAAI,OAAO,GAAA,EAAW;AACpB,IAAA,OAAO,CAAA,EAAG,IAAI,CAAA,EAAG,GAAG,IAAI,GAAA,GAAM,GAAA,EAAW,OAAA,CAAQ,CAAC,CAAC,CAAA,CAAA,CAAA;AAAA,EACrD;AACA,EAAA,IAAI,OAAO,GAAA,EAAO;AAChB,IAAA,OAAO,CAAA,EAAG,IAAI,CAAA,EAAG,GAAG,IAAI,GAAA,GAAM,GAAA,EAAO,OAAA,CAAQ,CAAC,CAAC,CAAA,CAAA,CAAA;AAAA,EACjD;AACA,EAAA,OAAO,CAAA,EAAG,IAAI,CAAA,EAAG,GAAG,GAAG,GAAA,CAAI,OAAA,CAAQ,CAAC,CAAC,CAAA,CAAA;AACvC;AAOO,SAAS,iBAAA,CACd,YAAA,EACA,MAAA,GAAiB,qBAAA,EACjB,MAAA,EACQ;AAER,EAAA,MAAM,cAAA,GAAiB,aAAa,WAAA,EAAY;AAEhD,EAAA,IAAI;AACF,IAAA,MAAM,KAAA,GAAQ,IAAI,IAAA,CAAK,YAAA,CAAa,MAAA,EAAQ;AAAA,MAC1C,KAAA,EAAO,UAAA;AAAA,MACP,QAAA,EAAU,cAAA;AAAA,MACV,eAAA,EAAiB;AAAA,KAClB,CAAA,CAAE,aAAA,CAAc,CAAC,CAAA;AAElB,IAAA,OAAO,KAAA,CAAM,KAAK,CAAC,IAAA,KAAS,KAAK,IAAA,KAAS,UAAU,CAAA,EAAG,KAAA,IAAS,MAAA,IAAU,cAAA;AAAA,EAC5E,CAAA,CAAA,MAAQ;AACN,IAAA,OAAO,MAAA,IAAU,cAAA;AAAA,EACnB;AACF;;;AC3JA,IAAM,aAAA,GAAgB,IAAA;AAStB,IAAM,iBAAA,GAAoB,WAAA;AAuBnB,SAAS,WAAW,KAAA,EAAiD;AAC1E,EAAA,IAAI,KAAA,KAAU,IAAA,IAAQ,KAAA,KAAU,MAAA,EAAW;AACzC,IAAA,OAAO,IAAA;AAAA,EACT;AAEA,EAAA,MAAM,OAAA,GAAU,MAAM,IAAA,EAAK;AAC3B,EAAA,IAAI,OAAA,KAAY,EAAA,IAAM,OAAA,KAAY,KAAA,EAAO;AACvC,IAAA,OAAO,IAAA;AAAA,EACT;AAEA,EAAA,MAAM,OAAA,GAAU,QAAQ,OAAA,CAAQ,aAAA,EAAe,GAAG,CAAA,CAAE,OAAA,CAAQ,mBAAmB,EAAE,CAAA;AACjF,EAAA,MAAM,MAAA,GAAS,MAAA,CAAO,UAAA,CAAW,OAAO,CAAA;AAExC,EAAA,OAAO,MAAA,CAAO,KAAA,CAAM,MAAM,CAAA,GAAI,IAAA,GAAO,MAAA;AACvC","file":"chunk-ZR7TABGT.cjs","sourcesContent":["/**\n * Organisation-driven reporting-currency resolution.\n *\n * Plugin (and host) surfaces render reporting-currency amounts in whatever the\n * organisation has configured as its reporting currency, without threading a\n * currency code through every call site. This module holds the module-level\n * reporting-currency ref, its setter, and the readers with a deterministic\n * fallback constant. It is pure — no React, no host access — so it lives in\n * core-utils behind the `@ethisyscore/core-utils/money` sub-path, mirroring the\n * date org-format seam (`@ethisyscore/core-utils/date/org-format`).\n *\n * The ref is stamped by the surface sync hook — the SDK\n * `useReportingCurrencySync` reads the host\n * `settings:get-organisation-reporting-currency` tool and calls\n * `setOrgReportingCurrency`. When no reporting currency is stamped (a\n * standalone/mock run, an old host, or a failed fetch) resolution falls back to\n * the deterministic UK default `GBP`, NOT the OS locale, so output is\n * machine-stable regardless of the host. This is the exact fail-open contract of\n * the date seam's UK `dd/MM/yyyy` fallback.\n *\n * Deciding WHICH currency an amount is in (its own currency vs the org's\n * reporting currency) and CONVERTING between currencies stay host-aware concerns\n * of the plugin-ui `useCurrency` hook. This seam only answers \"what is the org's\n * reporting currency\" for the pure {@link formatMoney} default path.\n */\n\n/**\n * Deterministic fallback reporting currency when no org currency is stamped.\n * Matches the date seam's UK fallback (`ORG_DATE_FALLBACK_FORMAT`) so money and\n * dates degrade consistently, and so output does not drift with the OS locale of\n * whatever host renders it.\n */\nexport const MONEY_FALLBACK_CURRENCY = \"GBP\";\n\n/** The organisation's reporting currency as stamped by the surface sync hook. */\nexport interface OrgReportingCurrency {\n  /** ISO 4217 code (e.g. `\"GBP\"`, `\"EUR\"`) drives `Intl` currency formatting. */\n  code: string;\n  /**\n   * Host decimal-places override for the currency, or null to use the currency's\n   * ISO default (GBP -> 2, JPY -> 0). Applied by {@link formatMoney} only when the\n   * caller supplies no explicit decimalPlaces.\n   */\n  decimalPlaces: number | null;\n}\n\n// ---------------------------------------------------------------------------\n// Module-level reporting-currency ref\n// ---------------------------------------------------------------------------\n\nlet orgReportingCurrencyRef: OrgReportingCurrency | null = null;\n\n/**\n * Stamps the module-level reporting-currency ref. Called by the surface sync hook\n * each time the organisation's reporting currency resolves. Pass `null` to clear\n * it and fall back to the deterministic {@link MONEY_FALLBACK_CURRENCY}.\n */\nexport function setOrgReportingCurrency(currency: OrgReportingCurrency | null): void {\n  orgReportingCurrencyRef = currency;\n}\n\n/** The org's stamped reporting currency, or null to fall back to the UK default. */\nexport function getOrgReportingCurrency(): OrgReportingCurrency | null {\n  return orgReportingCurrencyRef;\n}\n\n/**\n * The org's effective reporting-currency code: the stamped code when present, else\n * the deterministic {@link MONEY_FALLBACK_CURRENCY} (`GBP`). Never null, so callers\n * always have a well-formed code to format with.\n */\nexport function resolveOrgReportingCurrencyCode(): string {\n  return orgReportingCurrencyRef?.code ?? MONEY_FALLBACK_CURRENCY;\n}\n","/**\n * Framework-agnostic money DISPLAY formatting.\n *\n * The platform stores amounts as a plain number plus a currency (an ISO 4217 code,\n * optionally a symbol and a decimal-places override from the host `Currency` row).\n * This module turns that into a localized string via `Intl.NumberFormat`, with a\n * deterministic fallback so output is machine-stable regardless of the host OS\n * locale - the same principle as the date org-format seam.\n *\n * It is pure - no React, no host access - so it lives in core-utils behind the\n * `@ethisyscore/core-utils/money` sub-path. Resolving WHICH currency to render in\n * (the amount's own currency, or the org's reporting currency) and CONVERTING\n * between currencies are host-aware concerns handled by the plugin-ui `useCurrency`\n * hook, which formats through this module.\n *\n * When no currency is given, `formatMoney` falls back to the organisation's\n * reporting currency via the {@link ./orgCurrency} seam (the same module-ref +\n * fail-open-to-`GBP` mechanism as the date org-format seam), so a plugin can render\n * reporting-currency amounts without threading a code or hardcoding one.\n */\n\nimport {\n  getOrgReportingCurrency,\n  MONEY_FALLBACK_CURRENCY,\n} from \"./orgCurrency\";\n\n/**\n * Deterministic fallback locale. Matches the date seam's UK fallback so money and\n * dates render consistently when no explicit locale is supplied, and so output does\n * not drift with the OS locale of whatever host renders it.\n */\nexport const MONEY_FALLBACK_LOCALE = \"en-GB\";\n\n/** Options for {@link formatMoney}. */\nexport interface FormatMoneyOptions {\n  /**\n   * ISO 4217 code (e.g. `\"GBP\"`, `\"EUR\"`) - drives `Intl` currency formatting.\n   * Optional: when omitted, `formatMoney` resolves the organisation's reporting\n   * currency (see {@link ./orgCurrency}), falling back to `GBP` when none is stamped.\n   */\n  currencyCode?: string;\n  /**\n   * Fraction digits to show. When null/undefined, `Intl`'s per-currency default is\n   * used (GBP -> 2, JPY -> 0). Pass a number to force a currency's decimal-places\n   * override from the host `Currency` row.\n   */\n  decimalPlaces?: number | null;\n  /** BCP-47 locale; defaults to {@link MONEY_FALLBACK_LOCALE} for stable output. */\n  locale?: string;\n  /**\n   * Symbol to use only in the fallback path when `Intl` cannot format the currency\n   * code (e.g. a non-ISO custom code). Ignored on the happy path, where `Intl`\n   * supplies the symbol.\n   */\n  symbol?: string | null;\n}\n\n/**\n * Formats a monetary amount. The currency is resolved in this order:\n *\n * - `formatMoney(amount, \"EUR\")` — a positional ISO code, with the currency's\n *   natural decimals.\n * - `formatMoney(amount, { currencyCode, decimalPlaces, locale, symbol })` — the\n *   options form; any of `currencyCode`/`decimalPlaces` may be omitted.\n * - `formatMoney(amount)` (or an options object without `currencyCode`) — the\n *   organisation's reporting currency stamped via the {@link ./orgCurrency} seam,\n *   including that currency's decimal-places override when the caller gave none.\n *   When no org currency is stamped it falls back to {@link MONEY_FALLBACK_CURRENCY}\n *   (`GBP`), the same deterministic fail-open as the date org-format seam.\n *\n * On an unknown/invalid currency code (which makes `Intl.NumberFormat` throw) it\n * falls back to a symbol/code prefix plus the fixed-decimal amount, so a bad code\n * degrades to a readable string rather than throwing on a render path.\n */\nexport function formatMoney(amount: number, currency?: string | FormatMoneyOptions): string {\n  const options: FormatMoneyOptions = typeof currency === \"string\" ? { currencyCode: currency } : (currency ?? {});\n  const { currencyCode, decimalPlaces, locale = MONEY_FALLBACK_LOCALE, symbol } = options;\n\n  // No explicit code (undefined arg, or an options object without currencyCode)\n  // resolves the org's reporting currency, falling back to GBP when none is stamped.\n  const orgCurrency = currencyCode === undefined ? getOrgReportingCurrency() : null;\n  const effectiveCode = currencyCode ?? orgCurrency?.code ?? MONEY_FALLBACK_CURRENCY;\n  // The org currency's decimal-places override applies ONLY when the caller omitted\n  // decimalPlaces entirely (undefined). An explicit `null` means \"use Intl's\n  // per-currency default\" and must NOT pick up the org override, so distinguish\n  // undefined from null rather than coalescing both with `??`.\n  const effectiveDecimalPlaces =\n    decimalPlaces !== undefined ? decimalPlaces : (orgCurrency?.decimalPlaces ?? null);\n\n  // Intl.NumberFormat requires a well-formed (uppercase) ISO 4217 code; a lowercase\n  // code (e.g. \"usd\") throws and forces the fallback, so normalise up front.\n  const normalizedCode = effectiveCode.toUpperCase();\n\n  try {\n    const formatter = new Intl.NumberFormat(locale, {\n      style: \"currency\",\n      currency: normalizedCode,\n      // Narrow symbol so a clear currency context renders \"$100\", not \"US$100\".\n      currencyDisplay: \"narrowSymbol\",\n      ...(effectiveDecimalPlaces != null\n        ? { minimumFractionDigits: effectiveDecimalPlaces, maximumFractionDigits: effectiveDecimalPlaces }\n        : {}),\n    });\n    return formatter.format(amount);\n  } catch {\n    const fractionDigits = effectiveDecimalPlaces ?? 2;\n    const prefix = symbol ?? normalizedCode;\n    // Keep the minus sign before the symbol/code prefix (\"-£10.00\", not \"£-10.00\").\n    const sign = amount < 0 ? \"-\" : \"\";\n    return `${sign}${prefix}${Math.abs(amount).toFixed(fractionDigits)}`;\n  }\n}\n\n/** Options for {@link formatCompactMoney}. */\nexport interface FormatCompactMoneyOptions {\n  /** ISO 4217 code (e.g. `\"GBP\"`, `\"EUR\"`) - resolves the leading symbol. */\n  currencyCode: string;\n  /**\n   * Fallback symbol used ONLY when `Intl` cannot resolve `currencyCode` (a non-ISO\n   * custom code). For a valid ISO code `Intl`'s own symbol always wins - this option\n   * does not override it. Mirrors {@link FormatMoneyOptions.symbol} and the\n   * {@link getCurrencySymbol} fallback contract.\n   */\n  symbol?: string | null;\n  /** BCP-47 locale used only to resolve the symbol; defaults to {@link MONEY_FALLBACK_LOCALE}. */\n  locale?: string;\n}\n\n/**\n * Formats an amount as an abbreviated currency string for compact display, such\n * as chart axis ticks where space is tight - e.g. `\"£1.3m\"`, `\"£500k\"`, `\"£99\"`.\n *\n * Thresholds on the absolute value: >= 1,000,000 renders in millions with a\n * lowercase `m` and one decimal place; >= 1,000 renders in thousands with a\n * lowercase `k` and no decimals; otherwise the whole amount with no decimals.\n * A leading minus is kept before the symbol (e.g. `\"-£1.3m\"`).\n *\n * Unlike {@link formatMoney} this is NOT org-locale driven beyond resolving the\n * leading symbol via {@link getCurrencySymbol}; the number itself is formatted\n * with fixed abbreviations so axis labels stay short and machine-stable.\n */\nexport function formatCompactMoney(amount: number, options: FormatCompactMoneyOptions): string {\n  const { currencyCode, symbol, locale = MONEY_FALLBACK_LOCALE } = options;\n\n  const sym = getCurrencySymbol(currencyCode, locale, symbol);\n  const sign = amount < 0 ? \"-\" : \"\";\n  const abs = Math.abs(amount);\n\n  if (abs >= 1_000_000) {\n    return `${sign}${sym}${(abs / 1_000_000).toFixed(1)}m`;\n  }\n  if (abs >= 1_000) {\n    return `${sign}${sym}${(abs / 1_000).toFixed(0)}k`;\n  }\n  return `${sign}${sym}${abs.toFixed(0)}`;\n}\n\n/**\n * Resolves the currency symbol for a code in a locale (e.g. `\"GBP\"` -> `\"£\"`),\n * for use in input adornments and labels. Falls back to the supplied `symbol`, then\n * the code itself, when `Intl` cannot resolve it.\n */\nexport function getCurrencySymbol(\n  currencyCode: string,\n  locale: string = MONEY_FALLBACK_LOCALE,\n  symbol?: string | null,\n): string {\n  // Intl needs an uppercase ISO code; a lowercase one throws (see formatMoney).\n  const normalizedCode = currencyCode.toUpperCase();\n\n  try {\n    const parts = new Intl.NumberFormat(locale, {\n      style: \"currency\",\n      currency: normalizedCode,\n      currencyDisplay: \"narrowSymbol\",\n    }).formatToParts(0);\n\n    return parts.find((part) => part.type === \"currency\")?.value ?? symbol ?? normalizedCode;\n  } catch {\n    return symbol ?? normalizedCode;\n  }\n}\n","/**\n * Framework-agnostic money PARSING — the inverse of {@link formatMoney} for form\n * inputs and editable amount fields.\n *\n * A user (or a round-tripped display value) types an amount in the en-GB /\n * dot-decimal display shape — a currency symbol or ISO code prefix, comma grouping\n * separators, stray whitespace, and a dot decimal point (\"£1,234.56\", \"GBP 1,234.56\").\n * This turns that back into a plain number, or null when there is no meaningful value\n * to parse.\n *\n * It is deliberately lenient about the symbol, currency-code prefix and grouping so\n * it tolerates the display forms {@link formatMoney} produces in the en-GB fallback\n * locale, but it is NOT a locale-universal inverse of {@link formatMoney}. Only the\n * dot-decimal shape is understood: a comma is always treated as a grouping separator\n * and dropped, so comma-decimal locales (de-DE `1.234,56`) are OUT OF SCOPE and would\n * mis-parse. The platform stores and edits amounts in the dot-decimal numeric form.\n *\n * Ported from the per-plugin `parseCurrency` helper so consuming plugins drop the\n * local copy.\n */\n\n/**\n * The Unicode minus sign (U+2212) that `Intl.NumberFormat` emits for negatives.\n * It is not the ASCII hyphen-minus, so `parseFloat` would not treat it as a sign;\n * normalise it to `-` before stripping.\n */\nconst UNICODE_MINUS = /−/g;\n\n/**\n * After normalising the minus, everything that is NOT a digit, dot or minus is\n * stripped: currency symbols (£ $ € ¥ ₹ and any other), alpha ISO-code prefixes\n * (the \"GBP \" prefix, and the invalid-code fallback prefix like \"ZZZZ\"), comma\n * grouping separators, and all whitespace (including the non-breaking / narrow\n * no-break spaces `Intl` inserts).\n */\nconst NON_NUMERIC_CHARS = /[^0-9.-]/g;\n\n/**\n * Parses a formatted currency string back to a number. Returns null for\n * null/undefined, an empty/whitespace-only string, or the literal `\"N/A\"`\n * placeholder; normalises the Unicode minus, strips every non-numeric character\n * (currency symbols, alpha currency-code prefixes, grouping commas and whitespace),\n * then `parseFloat`s the remainder, returning null when the result is not a number.\n *\n * Parses only the en-GB / dot-decimal display shape (comma thousands, dot decimal);\n * see the module docs — it is NOT a locale-universal inverse of {@link formatMoney}.\n * A leading minus is preserved, so `\"-£10.00\"` parses to `-10`. Garbage that contains\n * no leading number (`\"abc\"`) yields null.\n *\n * @example\n * parseMoney(\"£1,234.56\")   // 1234.56\n * parseMoney(\"GBP 1,234.56\") // 1234.56\n * parseMoney(\"−£10.00\")     // -10 (Unicode minus U+2212)\n * parseMoney(\"ZZZZ10.00\")   // 10  (invalid-code fallback prefix)\n * parseMoney(\"N/A\")         // null\n * parseMoney(\"\")            // null\n * parseMoney(null)          // null\n */\nexport function parseMoney(value: string | null | undefined): number | null {\n  if (value === null || value === undefined) {\n    return null;\n  }\n\n  const trimmed = value.trim();\n  if (trimmed === \"\" || trimmed === \"N/A\") {\n    return null;\n  }\n\n  const cleaned = trimmed.replace(UNICODE_MINUS, \"-\").replace(NON_NUMERIC_CHARS, \"\");\n  const parsed = Number.parseFloat(cleaned);\n\n  return Number.isNaN(parsed) ? null : parsed;\n}\n"]}