@things-factory/invoice 10.1.159 → 10.1.165
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.
- package/dist-client/document/document-amount.d.ts +3 -3
- package/dist-client/document/document-amount.js +3 -5
- package/dist-client/document/document-amount.js.map +1 -1
- package/dist-client/tsconfig.tsbuildinfo +1 -1
- package/dist-server/service/invoice/invoice-rules.d.ts +1 -0
- package/dist-server/service/invoice/invoice-rules.js +5 -3
- package/dist-server/service/invoice/invoice-rules.js.map +1 -1
- package/dist-server/tsconfig.tsbuildinfo +1 -1
- package/dist-shared/currency-digits.d.ts +6 -0
- package/dist-shared/currency-digits.js +22 -0
- package/dist-shared/currency-digits.js.map +1 -0
- package/package.json +8 -7
- package/shared/currency-digits.ts +18 -0
- package/translations/ko.json +2 -2
- package/tsconfig.shared.json +18 -0
- package/tsconfig.shared.tsbuildinfo +1 -0
|
@@ -23,6 +23,7 @@ export interface TaxGroup {
|
|
|
23
23
|
taxAmount: number;
|
|
24
24
|
}
|
|
25
25
|
export declare function validDay(day: string): boolean;
|
|
26
|
+
/** Decimal places of a document's currency (`shared/currency-digits.ts`); a code the platform does not know is refused. */
|
|
26
27
|
export declare function currencyDigits(currency: string): number;
|
|
27
28
|
/**
|
|
28
29
|
* The supply of each line, the tax of each (kind, rate) group and the document totals. `received` keeps the line tax
|
|
@@ -9,6 +9,7 @@ exports.normalizedHeader = normalizedHeader;
|
|
|
9
9
|
exports.requireDraft = requireDraft;
|
|
10
10
|
exports.settlement = settlement;
|
|
11
11
|
exports.calendarDay = calendarDay;
|
|
12
|
+
const currency_digits_js_1 = require("@things-factory/invoice/dist-shared/currency-digits.js");
|
|
12
13
|
const shell_1 = require("@things-factory/shell");
|
|
13
14
|
/*
|
|
14
15
|
* Invoice arithmetic and lifecycle rules, shared by the invoice and quotation services and their tests (ADR-0120
|
|
@@ -33,11 +34,12 @@ const SCALE = 10000n;
|
|
|
33
34
|
function validDay(day) {
|
|
34
35
|
return /^\d{4}-\d{2}-\d{2}$/.test(day) && !Number.isNaN(Date.parse(day)) && new Date(day).toISOString().slice(0, 10) === day;
|
|
35
36
|
}
|
|
37
|
+
/** Decimal places of a document's currency (`shared/currency-digits.ts`); a code the platform does not know is refused. */
|
|
36
38
|
function currencyDigits(currency) {
|
|
37
|
-
|
|
39
|
+
const digits = (0, currency_digits_js_1.currencyDigits)(currency);
|
|
40
|
+
if (digits === undefined)
|
|
38
41
|
throw new shell_1.Refusal('invoice-currency-invalid', {}, 'Use a three-letter currency code');
|
|
39
|
-
|
|
40
|
-
return new Intl.NumberFormat('en', { style: 'currency', currency }).resolvedOptions().maximumFractionDigits;
|
|
42
|
+
return digits;
|
|
41
43
|
}
|
|
42
44
|
/** A number with at most four decimals, as an integer of ten-thousandths; undefined when it has more. */
|
|
43
45
|
function scaled(value) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"invoice-rules.js","sourceRoot":"","sources":["../../../server/service/invoice/invoice-rules.ts"],"names":[],"mappings":"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","sourcesContent":["import { Refusal } from '@things-factory/shell'\n\n/*\n * Invoice arithmetic and lifecycle rules, shared by the invoice and quotation services and their tests (ADR-0120\n * decision 4).\n *\n * - Lines carry supply and a tax kind and rate; the tax is reckoned once per (kind, rate) group: the group's supply\n * total × rate ÷ 100, rounded half-up to the currency's minor unit. The document tax is the sum of the groups.\n * - Everything is counted in integers: quantities, prices and rates to four decimals, money in minor units.\n * - A received document keeps the tax its sender printed on each line; nothing recomputes it.\n */\n\nexport enum TaxKind {\n TAXABLE = 'TAXABLE',\n ZERO_RATED = 'ZERO_RATED',\n EXEMPT = 'EXEMPT'\n}\n\n/** How a document's tax was rounded — written onto the document when it is issued or sent. */\nexport const TAX_ROUNDING = 'HALF_UP'\n\nexport const MAX_LINES = 500\nconst MAX_MINOR = 10n ** 14n\nconst SCALE = 10_000n\n\nexport interface LineValues {\n description: string\n quantity: number\n unitPrice: number\n discount: number\n taxKind: TaxKind | string\n taxRate: number\n /** The tax the sender printed on the line — only on a received document. */\n receivedTaxAmount?: number | null\n}\n\nexport interface TaxGroup {\n taxKind: string\n taxRate: number\n supplyAmount: number\n taxAmount: number\n}\n\nexport function validDay(day: string): boolean {\n return /^\\d{4}-\\d{2}-\\d{2}$/.test(day) && !Number.isNaN(Date.parse(day)) && new Date(day).toISOString().slice(0, 10) === day\n}\n\nexport function currencyDigits(currency: string): number {\n if (!/^[A-Z]{3}$/.test(currency) || !Intl.supportedValuesOf('currency').includes(currency)) {\n throw new Refusal('invoice-currency-invalid', {}, 'Use a three-letter currency code')\n }\n return new Intl.NumberFormat('en', { style: 'currency', currency }).resolvedOptions().maximumFractionDigits\n}\n\n/** A number with at most four decimals, as an integer of ten-thousandths; undefined when it has more. */\nfunction scaled(value: number): bigint | undefined {\n if (!Number.isFinite(value)) return undefined\n const whole = Math.round(value * 10_000)\n return Math.abs(value * 10_000 - whole) > 1e-6 ? undefined : BigInt(whole)\n}\n\n/** numerator ÷ denominator, rounded half-up; both non-negative. */\nfunction divideHalfUp(numerator: bigint, denominator: bigint): bigint {\n return (numerator * 2n + denominator) / (denominator * 2n)\n}\n\nfunction minorOf(value: number, digits: number): bigint | undefined {\n const factor = 10 ** digits\n const whole = Math.round(value * factor)\n return Math.abs(value * factor - whole) > 1e-6 ? undefined : BigInt(whole)\n}\n\nconst amountOf = (minor: bigint, digits: number) => Number(minor) / 10 ** digits\n\n/**\n * The supply of each line, the tax of each (kind, rate) group and the document totals. `received` keeps the line tax\n * the sender printed; otherwise lines carry no tax and the groups reckon it.\n */\nexport function calculateDocument(lines: readonly LineValues[], currency: string, received = false) {\n const digits = currencyDigits(currency)\n const factor = 10n ** BigInt(digits)\n if (!Array.isArray(lines) || !lines.length || lines.length > MAX_LINES) {\n throw new Refusal('invoice-items-required', {}, 'An invoice needs 1 to 500 items')\n }\n\n const groups = new Map<string, { taxKind: string; rate: bigint; supply: bigint; tax: bigint }>()\n const calculated = lines.map((line, position) => {\n const quantity = scaled(line.quantity)\n const unitPrice = scaled(line.unitPrice)\n const rate = scaled(line.taxRate)\n const discount = minorOf(line.discount, digits)\n const kind = line.taxKind as TaxKind\n if (\n !line.description?.trim() ||\n quantity === undefined ||\n unitPrice === undefined ||\n rate === undefined ||\n discount === undefined ||\n quantity <= 0n ||\n unitPrice < 0n ||\n discount < 0n ||\n rate < 0n ||\n rate > 100n * SCALE ||\n !Object.values(TaxKind).includes(kind) ||\n (kind !== TaxKind.TAXABLE && rate !== 0n)\n ) {\n throw new Refusal('invoice-item-invalid', {}, 'Check description, quantity, price, discount, tax kind and rate')\n }\n\n const gross = divideHalfUp(quantity * unitPrice * factor, SCALE * SCALE)\n if (discount > gross) throw new Refusal('invoice-discount-exceeds', {}, 'Discount exceeds the line amount')\n const supply = gross - discount\n if (supply > MAX_MINOR) throw new Refusal('invoice-amount-too-large', {}, 'Amount is too large')\n\n let printed: bigint | undefined\n if (received) {\n printed = line.receivedTaxAmount === null || line.receivedTaxAmount === undefined ? undefined : minorOf(line.receivedTaxAmount, digits)\n if (printed === undefined || printed < 0n) throw new Refusal('invoice-received-tax-required', {}, 'Enter the tax printed on each received line')\n }\n\n const key = `${kind}:${rate}`\n const group = groups.get(key) ?? { taxKind: kind, rate, supply: 0n, tax: 0n }\n group.supply += supply\n if (printed !== undefined) group.tax += printed\n groups.set(key, group)\n\n return {\n description: line.description.trim(),\n quantity: line.quantity,\n unitPrice: line.unitPrice,\n discount: line.discount,\n taxKind: kind,\n taxRate: line.taxRate,\n supplyAmount: amountOf(supply, digits),\n receivedTaxAmount: printed === undefined ? null : amountOf(printed, digits),\n position\n }\n })\n\n let supply = 0n\n let tax = 0n\n const taxGroups: TaxGroup[] = [...groups.values()].map(group => {\n if (!received) group.tax = divideHalfUp(group.supply * group.rate, 100n * SCALE)\n supply += group.supply\n tax += group.tax\n return { taxKind: group.taxKind, taxRate: Number(group.rate) / 10_000, supplyAmount: amountOf(group.supply, digits), taxAmount: amountOf(group.tax, digits) }\n })\n if (supply + tax > MAX_MINOR) throw new Refusal('invoice-amount-too-large', {}, 'Amount is too large')\n\n return {\n items: calculated,\n taxGroups,\n supplyAmount: amountOf(supply, digits),\n taxAmount: amountOf(tax, digits),\n totalAmount: amountOf(supply + tax, digits)\n }\n}\n\n/** The groups of a saved document, read back from its lines. */\nexport function taxGroupsOf(document: { currency: string; taxBasis?: string; items?: readonly LineValues[] }): TaxGroup[] {\n if (!document.items?.length) return []\n return calculateDocument(document.items, document.currency, document.taxBasis === 'RECEIVED').taxGroups\n}\n\n/** The fields every commercial document prints in its head, in the order the snapshot keeps them. */\nexport const HEADER_KEYS = [\n 'number',\n 'direction',\n 'currency',\n 'language',\n 'supplierName',\n 'buyerName',\n 'issueDate',\n 'supplierAddress',\n 'supplierRegistrationNumber',\n 'buyerAddress',\n 'buyerRegistrationNumber',\n 'purchaseOrder',\n 'paymentTerms',\n 'bankDetails',\n 'notes',\n 'sourceReference'\n] as const\n\n/** The head of a document, trimmed and checked. */\nexport function normalizedHeader(input: Record<string, any>) {\n const number = String(input.number ?? '').trim()\n const currency = String(input.currency ?? '').trim().toUpperCase()\n if (\n !number ||\n !String(input.supplierName ?? '').trim() ||\n !String(input.buyerName ?? '').trim() ||\n !['SALES', 'PURCHASE'].includes(input.direction) ||\n !['ko', 'en'].includes(input.language)\n ) {\n throw new Refusal('invoice-header-invalid', {}, 'Check number, parties, direction and language')\n }\n if (input.issueDate && !validDay(input.issueDate)) throw new Refusal('invoice-date-invalid', {}, 'Use a valid YYYY-MM-DD date')\n currencyDigits(currency)\n const header = Object.fromEntries(HEADER_KEYS.map(key => [key, input[key] ?? null])) as Record<(typeof HEADER_KEYS)[number], any>\n return { ...header, number, currency }\n}\n\nexport function requireDraft(status: string) {\n if (status !== 'DRAFT') throw new Refusal('invoice-draft-required', {}, 'Issued and void invoices cannot be edited')\n}\n\n/** How much of a total is paid, in the currency's minor unit. */\nexport function settlement(total: number, paid: number, currency = 'KRW') {\n const digits = currencyDigits(currency)\n const totalMinor = Math.round(total * 10 ** digits)\n const paidMinor = Math.round(paid * 10 ** digits)\n return {\n paidAmount: paidMinor / 10 ** digits,\n remainingAmount: Math.max(0, totalMinor - paidMinor) / 10 ** digits,\n paymentStatus: paidMinor >= totalMinor ? 'PAID' : paidMinor <= 0 ? 'UNPAID' : 'PARTIAL'\n }\n}\n\n/** The calendar day in a timezone. */\nexport function calendarDay(timezone: string, now = new Date()): string {\n if (!timezone) throw new Error('Domain timezone is required')\n return new Intl.DateTimeFormat('en-CA', { timeZone: timezone, year: 'numeric', month: '2-digit', day: '2-digit' }).format(now)\n}\n"]}
|
|
1
|
+
{"version":3,"file":"invoice-rules.js","sourceRoot":"","sources":["../../../server/service/invoice/invoice-rules.ts"],"names":[],"mappings":"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","sourcesContent":["import { currencyDigits as digitsOf } from '@things-factory/invoice/dist-shared/currency-digits.js'\nimport { Refusal } from '@things-factory/shell'\n\n/*\n * Invoice arithmetic and lifecycle rules, shared by the invoice and quotation services and their tests (ADR-0120\n * decision 4).\n *\n * - Lines carry supply and a tax kind and rate; the tax is reckoned once per (kind, rate) group: the group's supply\n * total × rate ÷ 100, rounded half-up to the currency's minor unit. The document tax is the sum of the groups.\n * - Everything is counted in integers: quantities, prices and rates to four decimals, money in minor units.\n * - A received document keeps the tax its sender printed on each line; nothing recomputes it.\n */\n\nexport enum TaxKind {\n TAXABLE = 'TAXABLE',\n ZERO_RATED = 'ZERO_RATED',\n EXEMPT = 'EXEMPT'\n}\n\n/** How a document's tax was rounded — written onto the document when it is issued or sent. */\nexport const TAX_ROUNDING = 'HALF_UP'\n\nexport const MAX_LINES = 500\nconst MAX_MINOR = 10n ** 14n\nconst SCALE = 10_000n\n\nexport interface LineValues {\n description: string\n quantity: number\n unitPrice: number\n discount: number\n taxKind: TaxKind | string\n taxRate: number\n /** The tax the sender printed on the line — only on a received document. */\n receivedTaxAmount?: number | null\n}\n\nexport interface TaxGroup {\n taxKind: string\n taxRate: number\n supplyAmount: number\n taxAmount: number\n}\n\nexport function validDay(day: string): boolean {\n return /^\\d{4}-\\d{2}-\\d{2}$/.test(day) && !Number.isNaN(Date.parse(day)) && new Date(day).toISOString().slice(0, 10) === day\n}\n\n/** Decimal places of a document's currency (`shared/currency-digits.ts`); a code the platform does not know is refused. */\nexport function currencyDigits(currency: string): number {\n const digits = digitsOf(currency)\n if (digits === undefined) throw new Refusal('invoice-currency-invalid', {}, 'Use a three-letter currency code')\n return digits\n}\n\n/** A number with at most four decimals, as an integer of ten-thousandths; undefined when it has more. */\nfunction scaled(value: number): bigint | undefined {\n if (!Number.isFinite(value)) return undefined\n const whole = Math.round(value * 10_000)\n return Math.abs(value * 10_000 - whole) > 1e-6 ? undefined : BigInt(whole)\n}\n\n/** numerator ÷ denominator, rounded half-up; both non-negative. */\nfunction divideHalfUp(numerator: bigint, denominator: bigint): bigint {\n return (numerator * 2n + denominator) / (denominator * 2n)\n}\n\nfunction minorOf(value: number, digits: number): bigint | undefined {\n const factor = 10 ** digits\n const whole = Math.round(value * factor)\n return Math.abs(value * factor - whole) > 1e-6 ? undefined : BigInt(whole)\n}\n\nconst amountOf = (minor: bigint, digits: number) => Number(minor) / 10 ** digits\n\n/**\n * The supply of each line, the tax of each (kind, rate) group and the document totals. `received` keeps the line tax\n * the sender printed; otherwise lines carry no tax and the groups reckon it.\n */\nexport function calculateDocument(lines: readonly LineValues[], currency: string, received = false) {\n const digits = currencyDigits(currency)\n const factor = 10n ** BigInt(digits)\n if (!Array.isArray(lines) || !lines.length || lines.length > MAX_LINES) {\n throw new Refusal('invoice-items-required', {}, 'An invoice needs 1 to 500 items')\n }\n\n const groups = new Map<string, { taxKind: string; rate: bigint; supply: bigint; tax: bigint }>()\n const calculated = lines.map((line, position) => {\n const quantity = scaled(line.quantity)\n const unitPrice = scaled(line.unitPrice)\n const rate = scaled(line.taxRate)\n const discount = minorOf(line.discount, digits)\n const kind = line.taxKind as TaxKind\n if (\n !line.description?.trim() ||\n quantity === undefined ||\n unitPrice === undefined ||\n rate === undefined ||\n discount === undefined ||\n quantity <= 0n ||\n unitPrice < 0n ||\n discount < 0n ||\n rate < 0n ||\n rate > 100n * SCALE ||\n !Object.values(TaxKind).includes(kind) ||\n (kind !== TaxKind.TAXABLE && rate !== 0n)\n ) {\n throw new Refusal('invoice-item-invalid', {}, 'Check description, quantity, price, discount, tax kind and rate')\n }\n\n const gross = divideHalfUp(quantity * unitPrice * factor, SCALE * SCALE)\n if (discount > gross) throw new Refusal('invoice-discount-exceeds', {}, 'Discount exceeds the line amount')\n const supply = gross - discount\n if (supply > MAX_MINOR) throw new Refusal('invoice-amount-too-large', {}, 'Amount is too large')\n\n let printed: bigint | undefined\n if (received) {\n printed = line.receivedTaxAmount === null || line.receivedTaxAmount === undefined ? undefined : minorOf(line.receivedTaxAmount, digits)\n if (printed === undefined || printed < 0n) throw new Refusal('invoice-received-tax-required', {}, 'Enter the tax printed on each received line')\n }\n\n const key = `${kind}:${rate}`\n const group = groups.get(key) ?? { taxKind: kind, rate, supply: 0n, tax: 0n }\n group.supply += supply\n if (printed !== undefined) group.tax += printed\n groups.set(key, group)\n\n return {\n description: line.description.trim(),\n quantity: line.quantity,\n unitPrice: line.unitPrice,\n discount: line.discount,\n taxKind: kind,\n taxRate: line.taxRate,\n supplyAmount: amountOf(supply, digits),\n receivedTaxAmount: printed === undefined ? null : amountOf(printed, digits),\n position\n }\n })\n\n let supply = 0n\n let tax = 0n\n const taxGroups: TaxGroup[] = [...groups.values()].map(group => {\n if (!received) group.tax = divideHalfUp(group.supply * group.rate, 100n * SCALE)\n supply += group.supply\n tax += group.tax\n return { taxKind: group.taxKind, taxRate: Number(group.rate) / 10_000, supplyAmount: amountOf(group.supply, digits), taxAmount: amountOf(group.tax, digits) }\n })\n if (supply + tax > MAX_MINOR) throw new Refusal('invoice-amount-too-large', {}, 'Amount is too large')\n\n return {\n items: calculated,\n taxGroups,\n supplyAmount: amountOf(supply, digits),\n taxAmount: amountOf(tax, digits),\n totalAmount: amountOf(supply + tax, digits)\n }\n}\n\n/** The groups of a saved document, read back from its lines. */\nexport function taxGroupsOf(document: { currency: string; taxBasis?: string; items?: readonly LineValues[] }): TaxGroup[] {\n if (!document.items?.length) return []\n return calculateDocument(document.items, document.currency, document.taxBasis === 'RECEIVED').taxGroups\n}\n\n/** The fields every commercial document prints in its head, in the order the snapshot keeps them. */\nexport const HEADER_KEYS = [\n 'number',\n 'direction',\n 'currency',\n 'language',\n 'supplierName',\n 'buyerName',\n 'issueDate',\n 'supplierAddress',\n 'supplierRegistrationNumber',\n 'buyerAddress',\n 'buyerRegistrationNumber',\n 'purchaseOrder',\n 'paymentTerms',\n 'bankDetails',\n 'notes',\n 'sourceReference'\n] as const\n\n/** The head of a document, trimmed and checked. */\nexport function normalizedHeader(input: Record<string, any>) {\n const number = String(input.number ?? '').trim()\n const currency = String(input.currency ?? '').trim().toUpperCase()\n if (\n !number ||\n !String(input.supplierName ?? '').trim() ||\n !String(input.buyerName ?? '').trim() ||\n !['SALES', 'PURCHASE'].includes(input.direction) ||\n !['ko', 'en'].includes(input.language)\n ) {\n throw new Refusal('invoice-header-invalid', {}, 'Check number, parties, direction and language')\n }\n if (input.issueDate && !validDay(input.issueDate)) throw new Refusal('invoice-date-invalid', {}, 'Use a valid YYYY-MM-DD date')\n currencyDigits(currency)\n const header = Object.fromEntries(HEADER_KEYS.map(key => [key, input[key] ?? null])) as Record<(typeof HEADER_KEYS)[number], any>\n return { ...header, number, currency }\n}\n\nexport function requireDraft(status: string) {\n if (status !== 'DRAFT') throw new Refusal('invoice-draft-required', {}, 'Issued and void invoices cannot be edited')\n}\n\n/** How much of a total is paid, in the currency's minor unit. */\nexport function settlement(total: number, paid: number, currency = 'KRW') {\n const digits = currencyDigits(currency)\n const totalMinor = Math.round(total * 10 ** digits)\n const paidMinor = Math.round(paid * 10 ** digits)\n return {\n paidAmount: paidMinor / 10 ** digits,\n remainingAmount: Math.max(0, totalMinor - paidMinor) / 10 ** digits,\n paymentStatus: paidMinor >= totalMinor ? 'PAID' : paidMinor <= 0 ? 'UNPAID' : 'PARTIAL'\n }\n}\n\n/** The calendar day in a timezone. */\nexport function calendarDay(timezone: string, now = new Date()): string {\n if (!timezone) throw new Error('Domain timezone is required')\n return new Intl.DateTimeFormat('en-CA', { timeZone: timezone, year: 'numeric', month: '2-digit', day: '2-digit' }).format(now)\n}\n"]}
|