@happyvertical/smrt-commerce 0.55.14 → 0.55.16

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (70) hide show
  1. package/AGENTS.md +3 -0
  2. package/README.md +29 -0
  3. package/agents/invoice-calculations.md +28 -1
  4. package/agents/payment-allocations.md +64 -0
  5. package/agents/purchasing-ui.md +27 -0
  6. package/dist/allocation-integrity.d.ts +13 -0
  7. package/dist/allocation-integrity.d.ts.map +1 -0
  8. package/dist/billing/payment-attempts.d.ts.map +1 -1
  9. package/dist/chunks/{calculations-DwBf83UE.js → calculations-Bz7RB3qK.js} +36 -2
  10. package/dist/chunks/calculations-Bz7RB3qK.js.map +1 -0
  11. package/dist/chunks/money-DfrRxrel.js +50 -0
  12. package/dist/chunks/money-DfrRxrel.js.map +1 -0
  13. package/dist/chunks/{types-CRnln577.js → types-zVnIjSOk.js} +29 -2
  14. package/dist/chunks/{types-CRnln577.js.map → types-zVnIjSOk.js.map} +1 -1
  15. package/dist/chunks/{units-BhVZswof.js → units-BpZ0_Dpk.js} +2 -2
  16. package/dist/chunks/{units-BhVZswof.js.map → units-BpZ0_Dpk.js.map} +1 -1
  17. package/dist/collections/PaymentAllocationCollection.d.ts +14 -0
  18. package/dist/collections/PaymentAllocationCollection.d.ts.map +1 -1
  19. package/dist/index.d.ts +1 -1
  20. package/dist/index.d.ts.map +1 -1
  21. package/dist/index.js +203 -100
  22. package/dist/index.js.map +1 -1
  23. package/dist/invoices.js +1 -1
  24. package/dist/manifest.json +43 -2
  25. package/dist/models/Contract.d.ts +7 -1
  26. package/dist/models/Contract.d.ts.map +1 -1
  27. package/dist/models/Invoice.d.ts +5 -3
  28. package/dist/models/Invoice.d.ts.map +1 -1
  29. package/dist/models/Payment.d.ts +2 -0
  30. package/dist/models/Payment.d.ts.map +1 -1
  31. package/dist/models/PaymentAllocation.d.ts +7 -33
  32. package/dist/models/PaymentAllocation.d.ts.map +1 -1
  33. package/dist/playground.js +23 -0
  34. package/dist/playground.js.map +1 -1
  35. package/dist/smrt-knowledge.json +79 -8
  36. package/dist/svelte/components/InvoiceEditor.svelte +3 -2
  37. package/dist/svelte/components/InvoiceEditor.svelte.d.ts.map +1 -1
  38. package/dist/svelte/components/InvoiceLineEditor.svelte +4 -0
  39. package/dist/svelte/components/InvoiceLineEditor.svelte.d.ts.map +1 -1
  40. package/dist/svelte/components/PurchaseOrderHeaderForm.svelte +106 -0
  41. package/dist/svelte/components/PurchaseOrderHeaderForm.svelte.d.ts +49 -0
  42. package/dist/svelte/components/PurchaseOrderHeaderForm.svelte.d.ts.map +1 -0
  43. package/dist/svelte/index.d.ts +4 -0
  44. package/dist/svelte/index.d.ts.map +1 -1
  45. package/dist/svelte/index.js +3 -0
  46. package/dist/svelte/invoices/calculations.d.ts +12 -3
  47. package/dist/svelte/invoices/calculations.d.ts.map +1 -1
  48. package/dist/svelte/invoices/calculations.js +40 -1
  49. package/dist/svelte/invoices/messages.d.ts +1 -0
  50. package/dist/svelte/invoices/messages.d.ts.map +1 -1
  51. package/dist/svelte/invoices/messages.js +1 -0
  52. package/dist/svelte/invoices/types.d.ts +3 -2
  53. package/dist/svelte/invoices/types.d.ts.map +1 -1
  54. package/dist/svelte/playground.d.ts +93 -0
  55. package/dist/svelte/playground.d.ts.map +1 -1
  56. package/dist/svelte/playground.js +20 -0
  57. package/dist/svelte/purchasing-i18n.d.ts +9 -0
  58. package/dist/svelte/purchasing-i18n.d.ts.map +1 -1
  59. package/dist/svelte/purchasing-i18n.js +9 -0
  60. package/dist/svelte/purchasing-types.d.ts +13 -0
  61. package/dist/svelte/purchasing-types.d.ts.map +1 -1
  62. package/dist/types/index.d.ts +7 -0
  63. package/dist/types/index.d.ts.map +1 -1
  64. package/dist/ui.d.ts.map +1 -1
  65. package/dist/ui.js +9 -0
  66. package/dist/ui.js.map +1 -1
  67. package/package.json +12 -12
  68. package/dist/chunks/calculations-DwBf83UE.js.map +0 -1
  69. package/dist/chunks/money-WSMvSgWq.js +0 -8
  70. package/dist/chunks/money-WSMvSgWq.js.map +0 -1
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@happyvertical/smrt-commerce",
3
- "version": "0.55.14",
3
+ "version": "0.55.16",
4
4
  "smrtJsdoc": "strict",
5
5
  "description": "Commerce models for the SMRT framework - contracts, fulfillments, payments",
6
6
  "type": "module",
@@ -59,15 +59,15 @@
59
59
  "access": "public"
60
60
  },
61
61
  "dependencies": {
62
- "@happyvertical/accounting": "^0.103.2",
63
- "@happyvertical/payments": "^0.103.2",
64
- "@happyvertical/smrt-core": "0.55.14",
65
- "@happyvertical/smrt-jobs": "0.55.14",
66
- "@happyvertical/smrt-ledgers": "0.55.14",
67
- "@happyvertical/smrt-subscriptions": "0.55.14",
68
- "@happyvertical/smrt-tenancy": "0.55.14",
69
- "@happyvertical/smrt-types": "0.55.14",
70
- "@happyvertical/smrt-ui": "0.55.14"
62
+ "@happyvertical/accounting": "^0.103.4",
63
+ "@happyvertical/payments": "^0.103.4",
64
+ "@happyvertical/smrt-core": "0.55.16",
65
+ "@happyvertical/smrt-jobs": "0.55.16",
66
+ "@happyvertical/smrt-ledgers": "0.55.16",
67
+ "@happyvertical/smrt-subscriptions": "0.55.16",
68
+ "@happyvertical/smrt-tenancy": "0.55.16",
69
+ "@happyvertical/smrt-types": "0.55.16",
70
+ "@happyvertical/smrt-ui": "0.55.16"
71
71
  },
72
72
  "peerDependencies": {
73
73
  "svelte": "^5.57.0"
@@ -78,7 +78,7 @@
78
78
  }
79
79
  },
80
80
  "devDependencies": {
81
- "@happyvertical/smrt-vitest": "0.55.14",
81
+ "@happyvertical/smrt-vitest": "0.55.16",
82
82
  "@sveltejs/package": "^2.5.8",
83
83
  "@sveltejs/vite-plugin-svelte": "^7.3.0",
84
84
  "@testing-library/jest-dom": "^7.0.1",
@@ -109,6 +109,6 @@
109
109
  "test:e2e:quotes": "cd ../.. && node packages/commerce/test-support/quote-browser.mjs",
110
110
  "test:e2e:invoices": "playwright test -c e2e/invoices/playwright.config.ts",
111
111
  "test:e2e:pricing": "cd ../.. && QUOTE_BROWSER_EXTENSION=packages/commerce/test-support/pricing-browser.mjs node packages/commerce/test-support/quote-browser.mjs",
112
- "test:e2e:purchasing": "cd ../.. && QUOTE_BROWSER_EXTENSION=packages/commerce/test-support/purchasing-browser-extension.mjs node packages/commerce/test-support/quote-browser.mjs"
112
+ "test:e2e:purchasing": "cd ../.. && QUOTE_BROWSER_EXTENSION=packages/commerce/test-support/purchasing-browser-extension.mjs node packages/commerce/test-support/quote-browser.mjs && QUOTE_BROWSER_EXTENSION=packages/commerce/test-support/purchasing-header-browser.mjs node packages/commerce/test-support/quote-browser.mjs"
113
113
  }
114
114
  }
@@ -1 +0,0 @@
1
- {"version":3,"file":"calculations-DwBf83UE.js","names":[],"sources":["../../src/svelte/invoices/currency.ts","../../src/svelte/invoices/calculations.ts"],"sourcesContent":["/** Svelte-free currency precision shared by invoice previews and billing. */\n\nimport { ISO_4217_MINOR_UNITS } from '@happyvertical/smrt-ui/currency';\n\nconst CURRENCY_PATTERN = /^[A-Z]{3}$/;\n\n/** Upper-case, validate, and return an ISO 4217-shaped currency code. */\nexport function normalizeCurrency(currency: string): string {\n const code = String(currency ?? '')\n .trim()\n .toUpperCase();\n if (!CURRENCY_PATTERN.test(code)) {\n throw new Error(`Currency '${currency}' must be a three-letter code.`);\n }\n return code;\n}\n\n/**\n * The ISO 4217 minor-unit exponent of a currency (2 for USD and RSD, 0 for\n * JPY, 3 for IQD). Unknown codes and currencies without a minor unit fail\n * closed because assigning either an inferred or invented exponent changes\n * the represented amount.\n */\nexport function currencyMinorUnitExponent(currency: string): number {\n const code = normalizeCurrency(currency);\n const exponent = ISO_4217_MINOR_UNITS.get(code);\n if (exponent === undefined)\n throw new Error(`Unsupported currency '${code}'.`);\n if (exponent === null)\n throw new Error(`Currency '${code}' does not define a minor unit.`);\n return exponent;\n}\n","/** Exact, transport-neutral invoice draft validation shared by browser and server. */\nimport { currencyMinorUnitExponent } from './currency.js';\n\n/** Unparsed editable invoice line; invalid values remain caller-owned. */\nexport interface InvoiceLineDraft {\n /** Stable caller-owned row identity. */\n key: string;\n /** Human-readable item description. */\n description: string;\n /** Optional stock or service code. */\n sku: string;\n /** Nonnegative decimal quantity, at most six decimal places. */\n quantity: string;\n /** Unit price in currency major units. */\n unitPrice: string;\n /** Flat major-unit discount or percentage; unknown drafts remain representable. */\n discountType: string;\n /** Discount in major units or percent according to discountType. */\n discountValue: string;\n /** Inherit the authorized default or use an explicit override. */\n taxMode: string;\n /** Override percent; zero is an explicit exemption, not inheritance. */\n taxRate: string;\n}\n\n/** Editable header and lines; clients never supply authoritative calculated totals. */\nexport interface InvoiceDraftValues {\n /** Customer reference; authorization/existence must be checked by the server. */\n customerId: string;\n /** Calendar issue date, YYYY-MM-DD. */\n issuedOn: string;\n /** Calendar due date, YYYY-MM-DD. */\n dueOn: string;\n /** Uppercase three-letter currency, with Commerce's explicit minor-unit table. */\n currency: string;\n /** Free-form payment terms. */\n paymentTerms: string;\n /** Default tax percent; the server must substitute its authorized rate. */\n taxRate: string;\n /** Caller-owned ordered lines. */\n lines: readonly InvoiceLineDraft[];\n}\n\n/** Fields on which line validation may report an error. */\nexport type InvoiceLineField = keyof InvoiceLineDraft;\n/** Stable validation reasons for caller-localized error messages. */\nexport type InvoiceCalculationError =\n | 'required'\n | 'invalid-decimal'\n | 'precision'\n | 'range'\n | 'invalid-mode'\n | 'discount-exceeds-gross'\n | 'invalid-date'\n | 'duplicate-key'\n | 'invalid-currency';\n/** Currency and server-authorized inherited percentage for a line. */\nexport interface InvoiceCalculationContext {\n /** Uppercase currency code. */\n currency: string;\n /** Default tax percent, for example \"5\". */\n inheritedTaxRate: string;\n}\n/** All monetary outputs are safe integer minor units. */\nexport interface InvoiceCalculatedAmounts {\n /** Rounded quantity times unit price before discounts. */\n grossMinor: number;\n /** Discount rounded to minor units before tax. */\n discountMinor: number;\n /** Gross less discount. */\n subtotalMinor: number;\n /** Rounded tax on the discounted subtotal. */\n taxMinor: number;\n /** Subtotal plus tax. */\n totalMinor: number;\n}\n/** Success or field errors, with no plausible monetary output for an invalid line. */\nexport type InvoiceLineCalculation =\n | ({ valid: true; effectiveTaxRate: string } & InvoiceCalculatedAmounts)\n | {\n valid: false;\n errors: Partial<Record<InvoiceLineField, InvoiceCalculationError>>;\n };\n/** Header error keys include whole-invoice totals and line membership. */\nexport type InvoiceDraftField =\n | Exclude<keyof InvoiceDraftValues, 'lines'>\n | 'lines'\n | 'total';\n/** Aggregate result retains every line result for granular rendering. */\nexport type InvoiceDraftCalculation =\n | ({\n valid: true;\n lineResults: readonly InvoiceLineCalculation[];\n } & InvoiceCalculatedAmounts)\n | {\n valid: false;\n errors: Partial<Record<InvoiceDraftField, InvoiceCalculationError>>;\n lineResults: readonly InvoiceLineCalculation[];\n };\n\ninterface Decimal {\n coefficient: bigint;\n scale: bigint;\n}\nconst MAX_MINOR = BigInt(Number.MAX_SAFE_INTEGER);\nconst MAX_TEXT = 128;\n\nfunction decimal(\n value: string,\n precision: number,\n): Decimal | InvoiceCalculationError {\n if (\n typeof value !== 'string' ||\n value.length > MAX_TEXT ||\n !/^\\d+(?:\\.\\d+)?$/.test(value)\n )\n return 'invalid-decimal';\n const [whole, fraction = ''] = value.split('.');\n if (fraction.length > precision) return 'precision';\n return {\n coefficient: BigInt(whole + fraction),\n scale: 10n ** BigInt(fraction.length),\n };\n}\n\nfunction roundHalfUp(numerator: bigint, denominator: bigint): bigint {\n return (numerator * 2n + denominator) / (denominator * 2n);\n}\n\nfunction minor(\n value: string,\n digits: number,\n): bigint | InvoiceCalculationError {\n const parsed = decimal(value, MAX_TEXT);\n if (typeof parsed === 'string') return parsed;\n const scaled = parsed.coefficient * 10n ** BigInt(digits);\n if (scaled % parsed.scale !== 0n) return 'precision';\n const result = scaled / parsed.scale;\n return result > MAX_MINOR ? 'range' : result;\n}\n\nfunction rate(value: string): Decimal | InvoiceCalculationError {\n const parsed = decimal(value, 6);\n if (typeof parsed === 'string') return parsed;\n return parsed.coefficient > 100n * parsed.scale ? 'range' : parsed;\n}\n\n/** Existing model monetary inputs, independent of currency display precision. */\nexport interface InvoiceMinorLineInput {\n /** Nonnegative quantity with at most six fractional digits. */\n quantity: number;\n /** Signed integer minor units; negative prices support existing credit lines. */\n unitPrice: number;\n /** Nonnegative flat discount in integer minor units. */\n discount: number;\n /** Resolved tax fraction, at most eight fractional digits, in [0, 1]. */\n taxRate: number;\n}\n\nfunction numberDecimal(value: number, precision: number): Decimal {\n if (!Number.isFinite(value) || value < 0 || value > Number.MAX_SAFE_INTEGER)\n throw new RangeError(\n 'Invoice rate or quantity is outside the supported range.',\n );\n // The shortest canonical decimal preserves the caller's Number. toFixed can\n // invent different decimal digits that round back to that same binary value.\n const [mantissa, exponent = '0'] = value.toString().split('e');\n const [whole, fraction = ''] = mantissa.split('.');\n const places = fraction.length - Number(exponent);\n if (places > precision)\n throw new RangeError(\n 'Invoice rate or quantity exceeds the supported decimal precision.',\n );\n const coefficient = BigInt(whole + fraction);\n return places < 0\n ? { coefficient: coefficient * 10n ** BigInt(-places), scale: 1n }\n : { coefficient, scale: 10n ** BigInt(places) };\n}\n\nfunction roundSigned(numerator: bigint, denominator: bigint): bigint {\n // Preserve Math.round's existing credit-line ties toward positive infinity.\n return numerator >= 0n\n ? roundHalfUp(numerator, denominator)\n : -((-numerator * 2n + denominator - 1n) / (denominator * 2n));\n}\n\n/**\n * Exact model calculation, including existing negative-price credit lines.\n * Throws before persistence for unsafe money, invalid quantity, or invalid tax.\n */\nexport function calculateInvoiceMinorLine(\n input: InvoiceMinorLineInput,\n): InvoiceCalculatedAmounts {\n for (const amount of [input.unitPrice, input.discount])\n if (!Number.isSafeInteger(amount))\n throw new RangeError('Invoice money must be safe integer minor units.');\n if (input.discount < 0 || input.taxRate > 1)\n throw new RangeError(\n 'Invoice discount or tax rate is outside the supported range.',\n );\n const quantity = numberDecimal(input.quantity, 6);\n const tax = numberDecimal(input.taxRate, 8);\n const gross = roundSigned(\n quantity.coefficient * BigInt(input.unitPrice),\n quantity.scale,\n );\n const discount = BigInt(input.discount);\n const subtotal = gross - discount;\n const taxMinor = roundSigned(subtotal * tax.coefficient, tax.scale);\n const total = subtotal + taxMinor;\n for (const amount of [gross, subtotal, taxMinor, total])\n if (amount > MAX_MINOR || amount < -MAX_MINOR)\n throw new RangeError(\n 'Invoice line amount exceeds safe integer minor units.',\n );\n return {\n grossMinor: Number(gross),\n discountMinor: input.discount,\n subtotalMinor: Number(subtotal),\n taxMinor: Number(taxMinor),\n totalMinor: Number(total),\n };\n}\n\n/** Validated line fields suitable for the existing model's resolved money columns. */\nexport interface InvoiceResolvedLine extends InvoiceMinorLineInput {\n /** Validated item description. */\n description: string;\n /** Caller stock code. */\n sku: string;\n /** Recomputed total; never copied from a browser total field. */\n amount: number;\n}\n\n/**\n * Resolve a validated editor draft to model fields. Percentage discounts become\n * exact flat minor units; inherited tax becomes the authorized effective rate.\n * Keep the raw draft alongside these fields when reopening its editing modes.\n */\nexport function resolveInvoiceLineDraft(\n line: InvoiceLineDraft,\n context: InvoiceCalculationContext,\n): InvoiceResolvedLine {\n const result = calculateInvoiceLine(line, context);\n if (!result.valid)\n throw new RangeError(\n `Invalid invoice line: ${JSON.stringify(result.errors)}`,\n );\n const quantity = Number(line.quantity);\n const sourceQuantity = decimal(line.quantity, 6);\n const storedQuantity = numberDecimal(quantity, 6);\n if (\n typeof sourceQuantity === 'string' ||\n sourceQuantity.coefficient * storedQuantity.scale !==\n storedQuantity.coefficient * sourceQuantity.scale\n )\n throw new RangeError(\n 'Invoice quantity cannot round-trip through numeric persistence.',\n );\n const [whole, fraction = ''] = result.effectiveTaxRate.split('.');\n const rateDigits = BigInt(whole + fraction)\n .toString()\n .padStart(fraction.length + 3, '0');\n const split = rateDigits.length - fraction.length - 2;\n const taxRate = Number(\n `${rateDigits.slice(0, split)}.${rateDigits.slice(split)}`,\n );\n const unitPrice = minor(\n line.unitPrice,\n currencyMinorUnitExponent(context.currency),\n );\n if (typeof unitPrice !== 'bigint')\n throw new RangeError('Invalid invoice unit price.');\n const resolved = {\n description: line.description,\n sku: line.sku,\n quantity,\n unitPrice: Number(unitPrice),\n discount: result.discountMinor,\n taxRate,\n amount: result.totalMinor,\n };\n const model = calculateInvoiceMinorLine(resolved);\n if (\n model.totalMinor !== result.totalMinor ||\n model.subtotalMinor !== result.subtotalMinor ||\n model.taxMinor !== result.taxMinor\n )\n throw new RangeError('Invoice draft and persisted calculation disagree.');\n return resolved;\n}\n\n/**\n * Validate and calculate one line using decimal integer arithmetic. Round half\n * up separately at gross, percentage discount, and tax boundaries. No rounding\n * is permitted when parsing a price or flat discount into currency minor units.\n */\nexport function calculateInvoiceLine(\n line: InvoiceLineDraft,\n context: InvoiceCalculationContext,\n): InvoiceLineCalculation {\n const errors: Partial<Record<InvoiceLineField, InvoiceCalculationError>> = {};\n if (!line.key?.trim()) errors.key = 'required';\n if (!line.description?.trim()) errors.description = 'required';\n if (!/^[A-Z]{3}$/.test(context.currency))\n return { valid: false, errors: { unitPrice: 'invalid-currency' } };\n const digits = currencyMinorUnitExponent(context.currency);\n const quantity = decimal(line.quantity, 6);\n const price = minor(line.unitPrice, digits);\n if (typeof quantity === 'string') errors.quantity = quantity;\n else if (quantity.coefficient > MAX_MINOR * quantity.scale)\n errors.quantity = 'range';\n else {\n try {\n const persisted = numberDecimal(Number(line.quantity), 6);\n if (\n quantity.coefficient * persisted.scale !==\n persisted.coefficient * quantity.scale\n )\n errors.quantity = 'precision';\n } catch {\n errors.quantity = 'precision';\n }\n }\n if (typeof price === 'string') errors.unitPrice = price;\n if (line.discountType !== 'flat' && line.discountType !== 'percent')\n errors.discountType = 'invalid-mode';\n if (line.taxMode !== 'inherit' && line.taxMode !== 'override')\n errors.taxMode = 'invalid-mode';\n const discount =\n line.discountType === 'percent'\n ? rate(line.discountValue)\n : minor(line.discountValue, digits);\n if (typeof discount === 'string') errors.discountValue = discount;\n const effectiveTaxRate =\n line.taxMode === 'inherit' ? context.inheritedTaxRate : line.taxRate;\n const tax = rate(effectiveTaxRate);\n if (typeof tax === 'string') errors.taxRate = tax;\n if (\n Object.keys(errors).length ||\n typeof quantity === 'string' ||\n typeof price === 'string' ||\n typeof discount === 'string' ||\n typeof tax === 'string'\n )\n return { valid: false, errors };\n const gross = roundHalfUp(quantity.coefficient * price, quantity.scale);\n const reduction =\n typeof discount === 'bigint'\n ? discount\n : roundHalfUp(gross * discount.coefficient, discount.scale * 100n);\n if (gross > MAX_MINOR) return { valid: false, errors: { quantity: 'range' } };\n if (reduction > gross)\n return {\n valid: false,\n errors: { discountValue: 'discount-exceeds-gross' },\n };\n const subtotal = gross - reduction;\n const taxMinor = roundHalfUp(subtotal * tax.coefficient, tax.scale * 100n);\n const total = subtotal + taxMinor;\n if (total > MAX_MINOR) return { valid: false, errors: { taxRate: 'range' } };\n return {\n valid: true,\n effectiveTaxRate,\n grossMinor: Number(gross),\n discountMinor: Number(reduction),\n subtotalMinor: Number(subtotal),\n taxMinor: Number(taxMinor),\n totalMinor: Number(total),\n };\n}\n\nfunction calendarDate(value: string): boolean {\n if (!/^\\d{4}-\\d{2}-\\d{2}$/.test(value) || value.startsWith('0000'))\n return false;\n const date = new Date(`${value}T00:00:00.000Z`);\n return (\n Number.isFinite(date.getTime()) && date.toISOString().slice(0, 10) === value\n );\n}\n\n/** Validate the header and sum validated lines without floating-point arithmetic. */\nexport function calculateInvoiceDraft(\n values: InvoiceDraftValues,\n): InvoiceDraftCalculation {\n const errors: Partial<Record<InvoiceDraftField, InvoiceCalculationError>> =\n {};\n if (!values.customerId?.trim()) errors.customerId = 'required';\n if (!calendarDate(values.issuedOn)) errors.issuedOn = 'invalid-date';\n if (!calendarDate(values.dueOn) || values.dueOn < values.issuedOn)\n errors.dueOn = 'invalid-date';\n if (!/^[A-Z]{3}$/.test(values.currency)) errors.currency = 'invalid-currency';\n const inheritedRate = rate(values.taxRate);\n if (typeof inheritedRate === 'string') errors.taxRate = inheritedRate;\n if (values.lines.length === 0) errors.lines = 'required';\n if (\n new Set(values.lines.map((line) => line.key)).size !== values.lines.length\n )\n errors.lines = 'duplicate-key';\n const lineResults = values.lines.map((line) =>\n calculateInvoiceLine(line, {\n currency: values.currency,\n inheritedTaxRate: values.taxRate,\n }),\n );\n const sums = {\n grossMinor: 0n,\n discountMinor: 0n,\n subtotalMinor: 0n,\n taxMinor: 0n,\n totalMinor: 0n,\n };\n for (const result of lineResults) {\n if (!result.valid) continue;\n for (const key of Object.keys(sums) as Array<keyof typeof sums>)\n sums[key] += BigInt(result[key]);\n }\n if (Object.values(sums).some((value) => value > MAX_MINOR))\n errors.total = 'range';\n if (Object.keys(errors).length || lineResults.some((result) => !result.valid))\n return { valid: false, errors, lineResults };\n return {\n valid: true,\n lineResults,\n grossMinor: Number(sums.grossMinor),\n discountMinor: Number(sums.discountMinor),\n subtotalMinor: Number(sums.subtotalMinor),\n taxMinor: Number(sums.taxMinor),\n totalMinor: Number(sums.totalMinor),\n };\n}\n"],"mappings":"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"}
@@ -1,8 +0,0 @@
1
- //#region src/money.ts
2
- function assertIntegerMinorUnits(context, label, entries) {
3
- for (const [field, value] of entries) if (!Number.isFinite(value) || !Number.isInteger(value)) throw new Error(`${context} ${label}: ${field} must be an integer number of minor units (cents, satoshis) \u2014 got ${value}. Money is exact: $19.99 is 1999, not 19.99.`);
4
- }
5
- //#endregion
6
- export { assertIntegerMinorUnits as t };
7
-
8
- //# sourceMappingURL=money-WSMvSgWq.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"money-WSMvSgWq.js","names":[],"sources":["../../src/money.ts"],"sourcesContent":["/**\n * Money is exact: integer minor units, checked at the model boundary (#2401).\n *\n * Every money column in this package is INTEGER minor units — cents,\n * satoshis — so `$19.99` is `1999`. Before #2401 the models carried\n * `EPSILON = 0.01` tolerances on every comparison, because the values really\n * were floats and `0.1 * 3` really is `0.30000000000000004`. With exact\n * integers those tolerances are not merely unnecessary, they are wrong: a\n * one-cent discrepancy is a one-cent discrepancy, not rounding fuzz.\n *\n * Dropping the tolerances is only safe if the values are *actually* integers,\n * which is what this guard establishes. A fractional major-unit write (`19.99`\n * into a cents column) is the bug the whole convention exists to prevent;\n * PostgreSQL catches it with a `22P02` and SQLite's type affinity does not\n * catch it at all, so the model catches it first, on every engine and every\n * write surface.\n */\n\n/**\n * Throw unless every named value is a finite integer.\n *\n * @param context - Model name for the error message, e.g. `Invoice`.\n * @param label - Row identifier used in the error, e.g. an invoice number.\n * @param entries - `[fieldName, value]` pairs to check.\n * @throws When any value is non-finite or has a fractional part.\n */\nexport function assertIntegerMinorUnits(\n context: string,\n label: string,\n entries: ReadonlyArray<readonly [string, number]>,\n): void {\n for (const [field, value] of entries) {\n if (!Number.isFinite(value) || !Number.isInteger(value)) {\n throw new Error(\n `${context} ${label}: ${field} must be an integer number of minor ` +\n `units (cents, satoshis) — got ${value}. Money is exact: $19.99 is ` +\n '1999, not 19.99.',\n );\n }\n }\n}\n"],"mappings":";AA0BO,SAAS,wBACd,SACA,OACA,SACM;CACN,KAAA,MAAW,CAAC,OAAO,UAAU,SAC3B,IAAI,CAAC,OAAO,SAAS,KAAK,KAAK,CAAC,OAAO,UAAU,KAAK,GACpD,MAAM,IAAI,MACR,GAAG,QAAO,GAAI,MAAK,IAAK,MAAK,yEACM,MAAK,6CAE1C;AAGN"}