@happyvertical/smrt-commerce 0.55.15 → 0.55.17

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 (53) hide show
  1. package/agents/invoice-calculations.md +28 -1
  2. package/agents/payment-allocations.md +19 -0
  3. package/dist/chunks/{calculations-DwBf83UE.js → calculations-Bz7RB3qK.js} +36 -2
  4. package/dist/chunks/calculations-Bz7RB3qK.js.map +1 -0
  5. package/dist/chunks/{units-BhVZswof.js → units-BpZ0_Dpk.js} +2 -2
  6. package/dist/chunks/{units-BhVZswof.js.map → units-BpZ0_Dpk.js.map} +1 -1
  7. package/dist/index.js +29 -8
  8. package/dist/index.js.map +1 -1
  9. package/dist/invoices.js +1 -1
  10. package/dist/manifest.json +1 -1
  11. package/dist/models/Invoice.d.ts +5 -3
  12. package/dist/models/Invoice.d.ts.map +1 -1
  13. package/dist/smrt-knowledge.json +5 -5
  14. package/dist/svelte/components/CustomerDetail.svelte +4 -1
  15. package/dist/svelte/components/CustomerDirectory.svelte +4 -1
  16. package/dist/svelte/components/CustomerForm.svelte +3 -0
  17. package/dist/svelte/components/InvoiceActions.svelte +4 -1
  18. package/dist/svelte/components/InvoiceAllocationFields.svelte +4 -1
  19. package/dist/svelte/components/InvoiceCard.svelte +12 -9
  20. package/dist/svelte/components/InvoiceEditor.svelte +7 -3
  21. package/dist/svelte/components/InvoiceEditor.svelte.d.ts.map +1 -1
  22. package/dist/svelte/components/InvoiceHeader.svelte +25 -22
  23. package/dist/svelte/components/InvoiceLineEditor.svelte +8 -1
  24. package/dist/svelte/components/InvoiceLineEditor.svelte.d.ts.map +1 -1
  25. package/dist/svelte/components/InvoiceLineItems.svelte +29 -26
  26. package/dist/svelte/components/InvoiceReview.svelte +4 -1
  27. package/dist/svelte/components/InvoiceTotals.svelte +14 -11
  28. package/dist/svelte/components/PartyAddressFields.svelte +3 -0
  29. package/dist/svelte/components/PartyContactFields.svelte +4 -1
  30. package/dist/svelte/components/PricingVersionDecision.svelte +4 -1
  31. package/dist/svelte/components/PricingVersionSummary.svelte +4 -1
  32. package/dist/svelte/components/PurchaseOrderEditor.svelte +4 -1
  33. package/dist/svelte/components/PurchaseOrderHeaderForm.svelte +1 -1
  34. package/dist/svelte/components/PurchaseSourceSelector.svelte +7 -2
  35. package/dist/svelte/components/QuoteEditor.svelte +4 -1
  36. package/dist/svelte/components/QuoteRevisionComparison.svelte +4 -1
  37. package/dist/svelte/components/QuoteRevisionHistory.svelte +4 -1
  38. package/dist/svelte/components/UnbilledItems.svelte +27 -24
  39. package/dist/svelte/components/VendorDetail.svelte +4 -1
  40. package/dist/svelte/components/VendorDirectory.svelte +4 -1
  41. package/dist/svelte/components/VendorForm.svelte +3 -0
  42. package/dist/svelte/invoices/calculations.d.ts +12 -3
  43. package/dist/svelte/invoices/calculations.d.ts.map +1 -1
  44. package/dist/svelte/invoices/calculations.js +40 -1
  45. package/dist/svelte/invoices/messages.d.ts +1 -0
  46. package/dist/svelte/invoices/messages.d.ts.map +1 -1
  47. package/dist/svelte/invoices/messages.js +1 -0
  48. package/dist/svelte/invoices/types.d.ts +3 -2
  49. package/dist/svelte/invoices/types.d.ts.map +1 -1
  50. package/dist/svelte/playground/InvoicePreparationPreview.svelte +6 -1
  51. package/dist/svelte/playground/PartySelectPreview.svelte +4 -1
  52. package/package.json +9 -9
  53. package/dist/chunks/calculations-DwBf83UE.js.map +0 -1
@@ -2,7 +2,7 @@
2
2
 
3
3
  `@happyvertical/smrt-commerce/invoices` is a Svelte-free entry usable by server handlers and browsers. `calculateInvoiceLine`, `calculateInvoiceDraft`, and `resolveInvoiceLineDraft` share exact decimal arithmetic. The UI consumes these helpers for previews; preview results confer no authority.
4
4
 
5
- Drafts retain unparsed text. Quantity accepts nonnegative decimals with at most six fractional digits and must round-trip through the model numeric representation. Prices and flat discounts accept only whole currency minor units expressed in major-unit text. Currency uses Commerce billing's explicit ISO exponent table (0/3/4 exceptions; other uppercase three-letter codes use 2); servers must authorize their supported currency set. No exchange conversion occurs. Rates are percentages from 0 through 100 with at most six fractional digits. `discountType` is `flat` or `percent`; `taxMode` is `inherit` or `override`. An override of `0` is zero tax; an empty override is invalid. Inherited lines ignore their dormant override string.
5
+ Drafts retain unparsed text. Quantity accepts nonnegative decimals with at most six fractional digits and must round-trip through the model numeric representation. Prices and flat discounts accept only whole currency minor units expressed in major-unit text. Currency uses the shared explicit ISO minor-unit table; unknown codes and codes without a minor unit are rejected. Servers must authorize their supported currency set. No exchange conversion occurs. Rates are percentages from 0 through 100 with at most six fractional digits. `discountType` is `flat` or `percent`; `taxMode` is `inherit` or `override`. An override of `0` is zero tax; an empty override is invalid. Inherited lines ignore their dormant override string.
6
6
 
7
7
  Calculation order is quantity × price rounded half up to minor units, discount rounded half up and subtracted, then tax on the discounted subtotal rounded half up. Flat discounts cannot exceed gross. Numeric model quantities and rates use their canonical decimal representation (including supported exponent notation), never a `toFixed` expansion of binary approximation. No floating-point multiplication or epsilon comparison determines money. Unsafe individual or aggregate results are rejected; invalid lines produce errors without monetary totals. Header validation requires customer, real issue/due dates with due date not before issue date, valid currency shape/default rate, at least one line, and unique nonblank row keys.
8
8
 
@@ -15,3 +15,30 @@ Trailing zeroes do not make an exact amount invalid: `100.00` JPY is 100 minor u
15
15
  Parse native form values into the typed draft (never pass unvalidated arbitrary JSON as the typed object), enforce request/row limits, authorize customer and source references in the active tenant, and validate edits against invoice lifecycle state. Replace inherited rate/currency policy with server-authorized context, call the shared validator/resolver, and save resolved rows and invoice within the application's transaction/retry contract. Ignore any client-supplied calculated amount. Default tax remains externally determined; this module is not a tax determination service. Caller-owned extension allocations, budget availability and approval evidence require their own server validation and are not part of line pricing.
16
16
 
17
17
  Changing currency changes the precision and denomination of the entered values, not their numeric text. The UI must not silently convert entered amounts or rewrite invalid drafts. Reopening or changing authorized tax context requires fresh server validation.
18
+
19
+
20
+ ## Explicit tax rounding scope
21
+
22
+ `calculateInvoiceDraft(values, { roundingScope: 'invoice' })` and the shared
23
+ `InvoiceEditor` / `InvoiceLineEditor` `roundingScope` prop accept `line` or
24
+ `invoice`. Omission preserves per-line rounding. The server chooses this option
25
+ from its authorized policy, and supplies it again when rendering rejected forms
26
+ or reopening retained drafts; it is not a client authority field.
27
+
28
+ Invoice scope groups numerically equal effective percentages, including textual
29
+ variants such as `5` and `5.000000`, and rounds each rate group once through
30
+ `calculateCostBillingTax`. Different rates remain independent components; explicit
31
+ zero overrides remain exempt. Quantity and discount rounding are unchanged. Two
32
+ 10-minor lines at 5% therefore have tax 1 in invoice scope and 2 in line scope.
33
+ This is an additive single-rate-per-line editor, not a tax determination or
34
+ compound-tax solver. Invalid scope is a validation error. Drafts still reject
35
+ negative quantities/prices; the signed model calculator is unchanged.
36
+
37
+ `lineResults` and `resolveInvoiceLineDraft` remain standalone per-line
38
+ calculations, useful for validation and existing line-mode model storage. They
39
+ are **not allocations** of invoice-rounded tax: their tax/total fields may not
40
+ sum to the aggregate. In invoice mode the UI displays each line's subtotal and
41
+ an explanation, with tax and total shown only at invoice level. Persist the
42
+ aggregate with the authorized scope and retained draft when using invoice
43
+ rounding; do not reconstruct it by summing standalone resolved line totals.
44
+ No persistence schema or `InvoiceLineItem` tax behavior is changed by this API.
@@ -43,3 +43,22 @@ snapshots fail closed through the same revision writes. SQLite serializes the
43
43
  owning write transaction. DuckDB is not a supported commerce allocation store:
44
44
  the existing declared foreign keys require ON UPDATE CASCADE, which its DDL
45
45
  boundary explicitly refuses; no weaker schema is substituted.
46
+
47
+ ### Undelivered invoice payment progress
48
+
49
+ Allocation may move an undelivered DRAFT invoice to PARTIAL or PAID without a
50
+ customer delivery event. Neither operation invents `sentAt` or `viewedAt`.
51
+ After allocations are removed, reload the parent and call
52
+ `updatePaymentStatus(remainingTotal)` and `save()` within the caller transaction.
53
+ Zero remaining allocations restore DRAFT only for an undelivered invoice;
54
+ a delivered invoice returns to SENT/VIEWED according to its delivery history.
55
+ DRAFT cannot conceal positive allocations. A DRAFT save checks both its supplied
56
+ delivery timestamps and the currently persisted timestamps, so clearing them
57
+ locally during reversal cannot bypass the undelivered requirement. This guard
58
+ does not introduce an immutable lifetime delivery-history record.
59
+
60
+ If a prepaid invoice is actually delivered later, call `markSent()` and save.
61
+ For PARTIAL/PAID invoices it records the first `sentAt` while preserving payment
62
+ status, paid amount, paid date and any existing view timestamp. Repeating this
63
+ call preserves the first send timestamp. It records the caller's delivery
64
+ event; it does not send a customer message or manufacture earlier delivery.
@@ -1,3 +1,4 @@
1
+ import { calculateCostBillingTax } from "../cost-billing.js";
1
2
  import { ISO_4217_MINOR_UNITS } from "@happyvertical/smrt-ui/currency";
2
3
  //#region src/svelte/invoices/currency.ts
3
4
  var CURRENCY_PATTERN = /^[A-Z]{3}$/;
@@ -172,8 +173,10 @@ function calendarDate(value) {
172
173
  const date = /* @__PURE__ */ new Date(`${value}T00:00:00.000Z`);
173
174
  return Number.isFinite(date.getTime()) && date.toISOString().slice(0, 10) === value;
174
175
  }
175
- function calculateInvoiceDraft(values) {
176
+ function calculateInvoiceDraft(values, options = {}) {
176
177
  const errors = {};
178
+ const roundingScope = options.roundingScope === void 0 ? "line" : options.roundingScope;
179
+ if (roundingScope !== "line" && roundingScope !== "invoice") errors.roundingScope = "invalid-mode";
177
180
  if (!values.customerId?.trim()) errors.customerId = "required";
178
181
  if (!calendarDate(values.issuedOn)) errors.issuedOn = "invalid-date";
179
182
  if (!calendarDate(values.dueOn) || values.dueOn < values.issuedOn) errors.dueOn = "invalid-date";
@@ -197,6 +200,37 @@ function calculateInvoiceDraft(values) {
197
200
  if (!result.valid) continue;
198
201
  for (const key of Object.keys(sums)) sums[key] += BigInt(result[key]);
199
202
  }
203
+ if (roundingScope === "invoice" && !Object.keys(errors).length && lineResults.every((result) => result.valid)) {
204
+ const taxable = lineResults.map((result, index) => {
205
+ if (!result.valid) throw new Error("Validated invoice line expected");
206
+ const [whole, fraction = ""] = result.effectiveTaxRate.split(".");
207
+ const digits = fraction.replace(/0+$/, "");
208
+ const percent = `${BigInt(whole)}${digits ? `.${digits}` : ""}`;
209
+ return {
210
+ id: values.lines[index].key,
211
+ amountMinor: result.subtotalMinor,
212
+ category: percent
213
+ };
214
+ });
215
+ try {
216
+ const tax = calculateCostBillingTax({
217
+ lines: taxable,
218
+ rules: [...new Set(taxable.map((row) => row.category))].map((percent) => ({
219
+ category: percent,
220
+ components: [{
221
+ id: percent,
222
+ percent
223
+ }]
224
+ })),
225
+ roundingScope: "invoice"
226
+ });
227
+ sums.taxMinor = BigInt(tax.taxMinor);
228
+ sums.totalMinor = sums.subtotalMinor + sums.taxMinor;
229
+ } catch (cause) {
230
+ if (!(cause instanceof RangeError)) throw cause;
231
+ errors.total = "range";
232
+ }
233
+ }
200
234
  if (Object.values(sums).some((value) => value > MAX_MINOR)) errors.total = "range";
201
235
  if (Object.keys(errors).length || lineResults.some((result) => !result.valid)) return {
202
236
  valid: false,
@@ -216,4 +250,4 @@ function calculateInvoiceDraft(values) {
216
250
  //#endregion
217
251
  export { currencyMinorUnitExponent as a, resolveInvoiceLineDraft as i, calculateInvoiceLine as n, normalizeCurrency as o, calculateInvoiceMinorLine as r, calculateInvoiceDraft as t };
218
252
 
219
- //# sourceMappingURL=calculations-DwBf83UE.js.map
253
+ //# sourceMappingURL=calculations-Bz7RB3qK.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"calculations-Bz7RB3qK.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 { calculateCostBillingTax } from '../../cost-billing.js';\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 | 'roundingScope';\n/** Server-authorized tax rounding; absent means the historical per-line behavior. */\nexport interface InvoiceDraftCalculationOptions {\n /** Invoice scope groups equal effective percentages and rounds each group once. */\n roundingScope?: 'line' | 'invoice';\n}\n/**\n * Aggregate result retains standalone line calculations for validation/audit.\n * With invoice rounding, line tax/total are estimates, NOT allocated invoice\n * amounts, and must not be summed or persisted as authoritative invoice tax.\n */\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 options: InvoiceDraftCalculationOptions = {},\n): InvoiceDraftCalculation {\n const errors: Partial<Record<InvoiceDraftField, InvoiceCalculationError>> =\n {};\n const roundingScope =\n options.roundingScope === undefined ? 'line' : options.roundingScope;\n if (roundingScope !== 'line' && roundingScope !== 'invoice')\n errors.roundingScope = 'invalid-mode';\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 (\n roundingScope === 'invoice' &&\n !Object.keys(errors).length &&\n lineResults.every((result) => result.valid)\n ) {\n // Rates have already passed exact decimal validation. Normalize textual\n // equivalents without numeric conversion before assigning component IDs.\n const taxable = lineResults.map((result, index) => {\n if (!result.valid) throw new Error('Validated invoice line expected');\n const [whole, fraction = ''] = result.effectiveTaxRate.split('.');\n const digits = fraction.replace(/0+$/, '');\n const percent = `${BigInt(whole)}${digits ? `.${digits}` : ''}`;\n return {\n id: values.lines[index].key,\n amountMinor: result.subtotalMinor,\n category: percent,\n };\n });\n try {\n const tax = calculateCostBillingTax({\n lines: taxable,\n rules: [...new Set(taxable.map((row) => row.category))].map(\n (percent) => ({\n category: percent,\n components: [{ id: percent, percent }],\n }),\n ),\n roundingScope: 'invoice',\n });\n sums.taxMinor = BigInt(tax.taxMinor);\n sums.totalMinor = sums.subtotalMinor + sums.taxMinor;\n } catch (cause) {\n if (!(cause instanceof RangeError)) throw cause;\n errors.total = 'range';\n }\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,4 +1,4 @@
1
- import { a as currencyMinorUnitExponent, o as normalizeCurrency } from "./calculations-DwBf83UE.js";
1
+ import { a as currencyMinorUnitExponent, o as normalizeCurrency } from "./calculations-Bz7RB3qK.js";
2
2
  //#region src/billing/units.ts
3
3
  function minorToMajorUnits(amount, currency) {
4
4
  if (!Number.isSafeInteger(amount)) throw new Error(`Amount ${amount} must be a safe integer number of minor units.`);
@@ -42,4 +42,4 @@ function tenantKey(id) {
42
42
  //#endregion
43
43
  export { majorToMinorUnits as a, isUuid as i, deterministicId as n, minorToMajorUnits as o, isTenantId as r, tenantKey as s, canonicalTenantId as t };
44
44
 
45
- //# sourceMappingURL=units-BhVZswof.js.map
45
+ //# sourceMappingURL=units-BpZ0_Dpk.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"units-BhVZswof.js","names":[],"sources":["../../src/billing/units.ts"],"sourcesContent":["/**\n * Money-unit and identity helpers for billing-period close (#3060).\n *\n * Commerce stores money as integer minor units. `@happyvertical/accounting`\n * takes and returns currency *major* units and converts to the provider's own\n * representation itself, so the only conversion this package performs is the\n * ISO 4217 one: minor units ÷ 10^exponent.\n */\n\nimport {\n currencyMinorUnitExponent,\n normalizeCurrency,\n} from '../svelte/invoices/currency.js';\n\nexport {\n currencyMinorUnitExponent,\n normalizeCurrency,\n} from '../svelte/invoices/currency.js';\n\n/** Integer minor units → major units (`1999` USD → `19.99`). */\nexport function minorToMajorUnits(amount: number, currency: string): number {\n if (!Number.isSafeInteger(amount)) {\n throw new Error(\n `Amount ${amount} must be a safe integer number of minor units.`,\n );\n }\n return amount / 10 ** currencyMinorUnitExponent(currency);\n}\n\n/**\n * Major units → integer minor units. Rejects a value that is not a whole\n * number of minor units instead of rounding it: a provider amount that does\n * not round-trip exactly means the two sides disagree about the currency.\n */\nexport function majorToMinorUnits(amount: number, currency: string): number {\n if (!Number.isFinite(amount)) {\n throw new Error(`Provider amount ${amount} is not a finite number.`);\n }\n const scaled = amount * 10 ** currencyMinorUnitExponent(currency);\n const minor = Math.round(scaled);\n if (Math.abs(scaled - minor) > 1e-6 || !Number.isSafeInteger(minor)) {\n throw new Error(\n `Provider amount ${amount} ${normalizeCurrency(currency)} is not a whole number of minor units.`,\n );\n }\n return minor;\n}\n\n/**\n * A stable UUID derived from its parts (SHA-256, version/variant bits set).\n * Deterministic ids are what make every period-close write replay-safe: a\n * retry recomputes the same identity instead of creating a second row.\n */\nexport async function deterministicId(parts: string[]): Promise<string> {\n const bytes = new TextEncoder().encode(JSON.stringify(parts));\n const digest = new Uint8Array(await crypto.subtle.digest('SHA-256', bytes));\n const uuid = digest.slice(0, 16);\n uuid[6] = (uuid[6] & 0x0f) | 0x50;\n uuid[8] = (uuid[8] & 0x3f) | 0x80;\n const hex = Array.from(uuid, (byte) => byte.toString(16).padStart(2, '0'));\n const joined = hex.join('');\n return [\n joined.slice(0, 8),\n joined.slice(8, 12),\n joined.slice(12, 16),\n joined.slice(16, 20),\n joined.slice(20),\n ].join('-');\n}\n\nconst UUID_PATTERN =\n /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;\n\n/** Validate and lower-case a tenant UUID. */\nexport function canonicalTenantId(id: string, label = 'Tenant ID'): string {\n if (typeof id !== 'string' || !UUID_PATTERN.test(id)) {\n throw new Error(`${label} must be a UUID.`);\n }\n return id.toLowerCase();\n}\n\n/** Whether `id` is a UUID (any case), such as a billing record id. */\nexport function isUuid(id: string): boolean {\n return typeof id === 'string' && UUID_PATTERN.test(id);\n}\n\n/** Whether `id` is a tenant UUID (any case). */\nexport function isTenantId(id: string): boolean {\n return typeof id === 'string' && UUID_PATTERN.test(id);\n}\n\n/** Lower-case a possibly-empty tenant id for comparison. */\nexport function tenantKey(id: string | null | undefined): string {\n return id ? String(id).toLowerCase() : '';\n}\n"],"mappings":";;AAoBO,SAAS,kBAAkB,QAAgB,UAA0B;CAC1E,IAAI,CAAC,OAAO,cAAc,MAAM,GAC9B,MAAM,IAAI,MACR,UAAU,OAAM,+CAClB;CAEF,OAAO,SAAS,MAAM,0BAA0B,QAAQ;AAC1D;AAOO,SAAS,kBAAkB,QAAgB,UAA0B;CAC1E,IAAI,CAAC,OAAO,SAAS,MAAM,GACzB,MAAM,IAAI,MAAM,mBAAmB,OAAM,yBAA0B;CAErE,MAAM,SAAS,SAAS,MAAM,0BAA0B,QAAQ;CAChE,MAAM,QAAQ,KAAK,MAAM,MAAM;CAC/B,IAAI,KAAK,IAAI,SAAS,KAAK,IAAI,QAAQ,CAAC,OAAO,cAAc,KAAK,GAChE,MAAM,IAAI,MACR,mBAAmB,OAAM,GAAI,kBAAkB,QAAQ,EAAC,uCAC1D;CAEF,OAAO;AACT;AAOA,eAAsB,gBAAgB,OAAkC;CACtE,MAAM,QAAQ,IAAI,YAAY,CAAA,CAAE,OAAO,KAAK,UAAU,KAAK,CAAC;CAE5D,MAAM,OAAO,IADM,WAAW,MAAM,OAAO,OAAO,OAAO,WAAW,KAAK,CAC5D,CAAA,CAAO,MAAM,GAAG,EAAE;CAC/B,KAAK,KAAM,KAAK,KAAK,KAAQ;CAC7B,KAAK,KAAM,KAAK,KAAK,KAAQ;CAE7B,MAAM,SADM,MAAM,KAAK,OAAO,SAAS,KAAK,SAAS,EAAE,CAAA,CAAE,SAAS,GAAG,GAAG,CACzD,CAAA,CAAI,KAAK,EAAE;CAC1B,OAAO;EACL,OAAO,MAAM,GAAG,CAAC;EACjB,OAAO,MAAM,GAAG,EAAE;EAClB,OAAO,MAAM,IAAI,EAAE;EACnB,OAAO,MAAM,IAAI,EAAE;EACnB,OAAO,MAAM,EAAE;CACjB,CAAA,CAAE,KAAK,GAAG;AACZ;AAEA,IAAM,eACJ;AAGK,SAAS,kBAAkB,IAAY,QAAQ,aAAqB;CACzE,IAAI,OAAO,OAAO,YAAY,CAAC,aAAa,KAAK,EAAE,GACjD,MAAM,IAAI,MAAM,GAAG,MAAK,iBAAkB;CAE5C,OAAO,GAAG,YAAY;AACxB;AAGO,SAAS,OAAO,IAAqB;CAC1C,OAAO,OAAO,OAAO,YAAY,aAAa,KAAK,EAAE;AACvD;AAGO,SAAS,WAAW,IAAqB;CAC9C,OAAO,OAAO,OAAO,YAAY,aAAa,KAAK,EAAE;AACvD;AAGO,SAAS,UAAU,IAAuC;CAC/D,OAAO,KAAK,OAAO,EAAE,CAAA,CAAE,YAAY,IAAI;AACzC"}
1
+ {"version":3,"file":"units-BpZ0_Dpk.js","names":[],"sources":["../../src/billing/units.ts"],"sourcesContent":["/**\n * Money-unit and identity helpers for billing-period close (#3060).\n *\n * Commerce stores money as integer minor units. `@happyvertical/accounting`\n * takes and returns currency *major* units and converts to the provider's own\n * representation itself, so the only conversion this package performs is the\n * ISO 4217 one: minor units ÷ 10^exponent.\n */\n\nimport {\n currencyMinorUnitExponent,\n normalizeCurrency,\n} from '../svelte/invoices/currency.js';\n\nexport {\n currencyMinorUnitExponent,\n normalizeCurrency,\n} from '../svelte/invoices/currency.js';\n\n/** Integer minor units → major units (`1999` USD → `19.99`). */\nexport function minorToMajorUnits(amount: number, currency: string): number {\n if (!Number.isSafeInteger(amount)) {\n throw new Error(\n `Amount ${amount} must be a safe integer number of minor units.`,\n );\n }\n return amount / 10 ** currencyMinorUnitExponent(currency);\n}\n\n/**\n * Major units → integer minor units. Rejects a value that is not a whole\n * number of minor units instead of rounding it: a provider amount that does\n * not round-trip exactly means the two sides disagree about the currency.\n */\nexport function majorToMinorUnits(amount: number, currency: string): number {\n if (!Number.isFinite(amount)) {\n throw new Error(`Provider amount ${amount} is not a finite number.`);\n }\n const scaled = amount * 10 ** currencyMinorUnitExponent(currency);\n const minor = Math.round(scaled);\n if (Math.abs(scaled - minor) > 1e-6 || !Number.isSafeInteger(minor)) {\n throw new Error(\n `Provider amount ${amount} ${normalizeCurrency(currency)} is not a whole number of minor units.`,\n );\n }\n return minor;\n}\n\n/**\n * A stable UUID derived from its parts (SHA-256, version/variant bits set).\n * Deterministic ids are what make every period-close write replay-safe: a\n * retry recomputes the same identity instead of creating a second row.\n */\nexport async function deterministicId(parts: string[]): Promise<string> {\n const bytes = new TextEncoder().encode(JSON.stringify(parts));\n const digest = new Uint8Array(await crypto.subtle.digest('SHA-256', bytes));\n const uuid = digest.slice(0, 16);\n uuid[6] = (uuid[6] & 0x0f) | 0x50;\n uuid[8] = (uuid[8] & 0x3f) | 0x80;\n const hex = Array.from(uuid, (byte) => byte.toString(16).padStart(2, '0'));\n const joined = hex.join('');\n return [\n joined.slice(0, 8),\n joined.slice(8, 12),\n joined.slice(12, 16),\n joined.slice(16, 20),\n joined.slice(20),\n ].join('-');\n}\n\nconst UUID_PATTERN =\n /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;\n\n/** Validate and lower-case a tenant UUID. */\nexport function canonicalTenantId(id: string, label = 'Tenant ID'): string {\n if (typeof id !== 'string' || !UUID_PATTERN.test(id)) {\n throw new Error(`${label} must be a UUID.`);\n }\n return id.toLowerCase();\n}\n\n/** Whether `id` is a UUID (any case), such as a billing record id. */\nexport function isUuid(id: string): boolean {\n return typeof id === 'string' && UUID_PATTERN.test(id);\n}\n\n/** Whether `id` is a tenant UUID (any case). */\nexport function isTenantId(id: string): boolean {\n return typeof id === 'string' && UUID_PATTERN.test(id);\n}\n\n/** Lower-case a possibly-empty tenant id for comparison. */\nexport function tenantKey(id: string | null | undefined): string {\n return id ? String(id).toLowerCase() : '';\n}\n"],"mappings":";;AAoBO,SAAS,kBAAkB,QAAgB,UAA0B;CAC1E,IAAI,CAAC,OAAO,cAAc,MAAM,GAC9B,MAAM,IAAI,MACR,UAAU,OAAM,+CAClB;CAEF,OAAO,SAAS,MAAM,0BAA0B,QAAQ;AAC1D;AAOO,SAAS,kBAAkB,QAAgB,UAA0B;CAC1E,IAAI,CAAC,OAAO,SAAS,MAAM,GACzB,MAAM,IAAI,MAAM,mBAAmB,OAAM,yBAA0B;CAErE,MAAM,SAAS,SAAS,MAAM,0BAA0B,QAAQ;CAChE,MAAM,QAAQ,KAAK,MAAM,MAAM;CAC/B,IAAI,KAAK,IAAI,SAAS,KAAK,IAAI,QAAQ,CAAC,OAAO,cAAc,KAAK,GAChE,MAAM,IAAI,MACR,mBAAmB,OAAM,GAAI,kBAAkB,QAAQ,EAAC,uCAC1D;CAEF,OAAO;AACT;AAOA,eAAsB,gBAAgB,OAAkC;CACtE,MAAM,QAAQ,IAAI,YAAY,CAAA,CAAE,OAAO,KAAK,UAAU,KAAK,CAAC;CAE5D,MAAM,OAAO,IADM,WAAW,MAAM,OAAO,OAAO,OAAO,WAAW,KAAK,CAC5D,CAAA,CAAO,MAAM,GAAG,EAAE;CAC/B,KAAK,KAAM,KAAK,KAAK,KAAQ;CAC7B,KAAK,KAAM,KAAK,KAAK,KAAQ;CAE7B,MAAM,SADM,MAAM,KAAK,OAAO,SAAS,KAAK,SAAS,EAAE,CAAA,CAAE,SAAS,GAAG,GAAG,CACzD,CAAA,CAAI,KAAK,EAAE;CAC1B,OAAO;EACL,OAAO,MAAM,GAAG,CAAC;EACjB,OAAO,MAAM,GAAG,EAAE;EAClB,OAAO,MAAM,IAAI,EAAE;EACnB,OAAO,MAAM,IAAI,EAAE;EACnB,OAAO,MAAM,EAAE;CACjB,CAAA,CAAE,KAAK,GAAG;AACZ;AAEA,IAAM,eACJ;AAGK,SAAS,kBAAkB,IAAY,QAAQ,aAAqB;CACzE,IAAI,OAAO,OAAO,YAAY,CAAC,aAAa,KAAK,EAAE,GACjD,MAAM,IAAI,MAAM,GAAG,MAAK,iBAAkB;CAE5C,OAAO,GAAG,YAAY;AACxB;AAGO,SAAS,OAAO,IAAqB;CAC1C,OAAO,OAAO,OAAO,YAAY,aAAa,KAAK,EAAE;AACvD;AAGO,SAAS,WAAW,IAAqB;CAC9C,OAAO,OAAO,OAAO,YAAY,aAAa,KAAK,EAAE;AACvD;AAGO,SAAS,UAAU,IAAuC;CAC/D,OAAO,KAAK,OAAO,EAAE,CAAA,CAAE,YAAY,IAAI;AACzC"}