@things-factory/invoice 0.0.0-stage → 10.1.130
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/README.md +21 -2
- package/dist-client/bootstrap.d.ts +1 -0
- package/dist-client/bootstrap.js +5 -0
- package/dist-client/bootstrap.js.map +1 -0
- package/dist-client/document/document-amount.d.ts +6 -0
- package/dist-client/document/document-amount.js +19 -0
- package/dist-client/document/document-amount.js.map +1 -0
- package/dist-client/document/document-form.d.ts +14 -0
- package/dist-client/document/document-form.js +145 -0
- package/dist-client/document/document-form.js.map +1 -0
- package/dist-client/document/document-lines.d.ts +104 -0
- package/dist-client/document/document-lines.js +61 -0
- package/dist-client/document/document-lines.js.map +1 -0
- package/dist-client/document/document-print.d.ts +15 -0
- package/dist-client/document/document-print.js +60 -0
- package/dist-client/document/document-print.js.map +1 -0
- package/dist-client/entity360/invoice-360.d.ts +19 -0
- package/dist-client/entity360/invoice-360.js +116 -0
- package/dist-client/entity360/invoice-360.js.map +1 -0
- package/dist-client/index.d.ts +6 -0
- package/dist-client/index.js +7 -0
- package/dist-client/index.js.map +1 -0
- package/dist-client/invoice-attach.d.ts +19 -0
- package/dist-client/invoice-attach.js +19 -0
- package/dist-client/invoice-attach.js.map +1 -0
- package/dist-client/pages/invoice-editor.d.ts +48 -0
- package/dist-client/pages/invoice-editor.js +341 -0
- package/dist-client/pages/invoice-editor.js.map +1 -0
- package/dist-client/pages/invoice-list.d.ts +39 -0
- package/dist-client/pages/invoice-list.js +125 -0
- package/dist-client/pages/invoice-list.js.map +1 -0
- package/dist-client/route.d.ts +1 -0
- package/dist-client/route.js +11 -0
- package/dist-client/route.js.map +1 -0
- package/dist-client/tsconfig.tsbuildinfo +1 -0
- package/dist-server/index.d.ts +2 -0
- package/dist-server/index.js +6 -0
- package/dist-server/index.js.map +1 -0
- package/dist-server/routes.d.ts +1 -0
- package/dist-server/routes.js +3 -0
- package/dist-server/routes.js.map +1 -0
- package/dist-server/service/index.d.ts +12 -0
- package/dist-server/service/index.js +19 -0
- package/dist-server/service/index.js.map +1 -0
- package/dist-server/service/invoice/commercial-document.d.ts +44 -0
- package/dist-server/service/invoice/commercial-document.js +200 -0
- package/dist-server/service/invoice/commercial-document.js.map +1 -0
- package/dist-server/service/invoice/index.d.ts +6 -0
- package/dist-server/service/invoice/index.js +10 -0
- package/dist-server/service/invoice/index.js.map +1 -0
- package/dist-server/service/invoice/invoice-mutation.d.ts +8 -0
- package/dist-server/service/invoice/invoice-mutation.js +75 -0
- package/dist-server/service/invoice/invoice-mutation.js.map +1 -0
- package/dist-server/service/invoice/invoice-query.d.ts +16 -0
- package/dist-server/service/invoice/invoice-query.js +117 -0
- package/dist-server/service/invoice/invoice-query.js.map +1 -0
- package/dist-server/service/invoice/invoice-rules.d.ts +83 -0
- package/dist-server/service/invoice/invoice-rules.js +202 -0
- package/dist-server/service/invoice/invoice-rules.js.map +1 -0
- package/dist-server/service/invoice/invoice-service.d.ts +26 -0
- package/dist-server/service/invoice/invoice-service.js +133 -0
- package/dist-server/service/invoice/invoice-service.js.map +1 -0
- package/dist-server/service/invoice/invoice-type.d.ts +40 -0
- package/dist-server/service/invoice/invoice-type.js +146 -0
- package/dist-server/service/invoice/invoice-type.js.map +1 -0
- package/dist-server/service/invoice/invoice.d.ts +30 -0
- package/dist-server/service/invoice/invoice.js +108 -0
- package/dist-server/service/invoice/invoice.js.map +1 -0
- package/dist-server/service/invoice-event/index.d.ts +5 -0
- package/dist-server/service/invoice-event/index.js +9 -0
- package/dist-server/service/invoice-event/index.js.map +1 -0
- package/dist-server/service/invoice-event/invoice-event-query.d.ts +9 -0
- package/dist-server/service/invoice-event/invoice-event-query.js +57 -0
- package/dist-server/service/invoice-event/invoice-event-query.js.map +1 -0
- package/dist-server/service/invoice-event/invoice-event.d.ts +22 -0
- package/dist-server/service/invoice-event/invoice-event.js +98 -0
- package/dist-server/service/invoice-event/invoice-event.js.map +1 -0
- package/dist-server/service/invoice-item/index.d.ts +5 -0
- package/dist-server/service/invoice-item/index.js +9 -0
- package/dist-server/service/invoice-item/index.js.map +1 -0
- package/dist-server/service/invoice-item/invoice-item-query.d.ts +8 -0
- package/dist-server/service/invoice-item/invoice-item-query.js +47 -0
- package/dist-server/service/invoice-item/invoice-item-query.js.map +1 -0
- package/dist-server/service/invoice-item/invoice-item.d.ts +19 -0
- package/dist-server/service/invoice-item/invoice-item.js +80 -0
- package/dist-server/service/invoice-item/invoice-item.js.map +1 -0
- package/dist-server/tsconfig.tsbuildinfo +1 -0
- package/package.json +44 -4
- package/things-factory.config.js +11 -0
- package/translations/en.json +81 -0
- package/translations/ko.json +81 -0
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.HEADER_KEYS = exports.MAX_LINES = exports.TAX_ROUNDING = exports.TaxKind = void 0;
|
|
4
|
+
exports.validDay = validDay;
|
|
5
|
+
exports.currencyDigits = currencyDigits;
|
|
6
|
+
exports.calculateDocument = calculateDocument;
|
|
7
|
+
exports.taxGroupsOf = taxGroupsOf;
|
|
8
|
+
exports.normalizedHeader = normalizedHeader;
|
|
9
|
+
exports.requireDraft = requireDraft;
|
|
10
|
+
exports.settlement = settlement;
|
|
11
|
+
exports.calendarDay = calendarDay;
|
|
12
|
+
const shell_1 = require("@things-factory/shell");
|
|
13
|
+
/*
|
|
14
|
+
* Invoice arithmetic and lifecycle rules, shared by the invoice and quotation services and their tests (ADR-0120
|
|
15
|
+
* decision 4).
|
|
16
|
+
*
|
|
17
|
+
* - Lines carry supply and a tax kind and rate; the tax is reckoned once per (kind, rate) group: the group's supply
|
|
18
|
+
* total × rate ÷ 100, rounded half-up to the currency's minor unit. The document tax is the sum of the groups.
|
|
19
|
+
* - Everything is counted in integers: quantities, prices and rates to four decimals, money in minor units.
|
|
20
|
+
* - A received document keeps the tax its sender printed on each line; nothing recomputes it.
|
|
21
|
+
*/
|
|
22
|
+
var TaxKind;
|
|
23
|
+
(function (TaxKind) {
|
|
24
|
+
TaxKind["TAXABLE"] = "TAXABLE";
|
|
25
|
+
TaxKind["ZERO_RATED"] = "ZERO_RATED";
|
|
26
|
+
TaxKind["EXEMPT"] = "EXEMPT";
|
|
27
|
+
})(TaxKind || (exports.TaxKind = TaxKind = {}));
|
|
28
|
+
/** How a document's tax was rounded — written onto the document when it is issued or sent. */
|
|
29
|
+
exports.TAX_ROUNDING = 'HALF_UP';
|
|
30
|
+
exports.MAX_LINES = 500;
|
|
31
|
+
const MAX_MINOR = 10n ** 14n;
|
|
32
|
+
const SCALE = 10000n;
|
|
33
|
+
function validDay(day) {
|
|
34
|
+
return /^\d{4}-\d{2}-\d{2}$/.test(day) && !Number.isNaN(Date.parse(day)) && new Date(day).toISOString().slice(0, 10) === day;
|
|
35
|
+
}
|
|
36
|
+
function currencyDigits(currency) {
|
|
37
|
+
if (!/^[A-Z]{3}$/.test(currency) || !Intl.supportedValuesOf('currency').includes(currency)) {
|
|
38
|
+
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;
|
|
41
|
+
}
|
|
42
|
+
/** A number with at most four decimals, as an integer of ten-thousandths; undefined when it has more. */
|
|
43
|
+
function scaled(value) {
|
|
44
|
+
if (!Number.isFinite(value))
|
|
45
|
+
return undefined;
|
|
46
|
+
const whole = Math.round(value * 10_000);
|
|
47
|
+
return Math.abs(value * 10_000 - whole) > 1e-6 ? undefined : BigInt(whole);
|
|
48
|
+
}
|
|
49
|
+
/** numerator ÷ denominator, rounded half-up; both non-negative. */
|
|
50
|
+
function divideHalfUp(numerator, denominator) {
|
|
51
|
+
return (numerator * 2n + denominator) / (denominator * 2n);
|
|
52
|
+
}
|
|
53
|
+
function minorOf(value, digits) {
|
|
54
|
+
const factor = 10 ** digits;
|
|
55
|
+
const whole = Math.round(value * factor);
|
|
56
|
+
return Math.abs(value * factor - whole) > 1e-6 ? undefined : BigInt(whole);
|
|
57
|
+
}
|
|
58
|
+
const amountOf = (minor, digits) => Number(minor) / 10 ** digits;
|
|
59
|
+
/**
|
|
60
|
+
* The supply of each line, the tax of each (kind, rate) group and the document totals. `received` keeps the line tax
|
|
61
|
+
* the sender printed; otherwise lines carry no tax and the groups reckon it.
|
|
62
|
+
*/
|
|
63
|
+
function calculateDocument(lines, currency, received = false) {
|
|
64
|
+
const digits = currencyDigits(currency);
|
|
65
|
+
const factor = 10n ** BigInt(digits);
|
|
66
|
+
if (!Array.isArray(lines) || !lines.length || lines.length > exports.MAX_LINES) {
|
|
67
|
+
throw new shell_1.Refusal('invoice-items-required', {}, 'An invoice needs 1 to 500 items');
|
|
68
|
+
}
|
|
69
|
+
const groups = new Map();
|
|
70
|
+
const calculated = lines.map((line, position) => {
|
|
71
|
+
const quantity = scaled(line.quantity);
|
|
72
|
+
const unitPrice = scaled(line.unitPrice);
|
|
73
|
+
const rate = scaled(line.taxRate);
|
|
74
|
+
const discount = minorOf(line.discount, digits);
|
|
75
|
+
const kind = line.taxKind;
|
|
76
|
+
if (!line.description?.trim() ||
|
|
77
|
+
quantity === undefined ||
|
|
78
|
+
unitPrice === undefined ||
|
|
79
|
+
rate === undefined ||
|
|
80
|
+
discount === undefined ||
|
|
81
|
+
quantity <= 0n ||
|
|
82
|
+
unitPrice < 0n ||
|
|
83
|
+
discount < 0n ||
|
|
84
|
+
rate < 0n ||
|
|
85
|
+
rate > 100n * SCALE ||
|
|
86
|
+
!Object.values(TaxKind).includes(kind) ||
|
|
87
|
+
(kind !== TaxKind.TAXABLE && rate !== 0n)) {
|
|
88
|
+
throw new shell_1.Refusal('invoice-item-invalid', {}, 'Check description, quantity, price, discount, tax kind and rate');
|
|
89
|
+
}
|
|
90
|
+
const gross = divideHalfUp(quantity * unitPrice * factor, SCALE * SCALE);
|
|
91
|
+
if (discount > gross)
|
|
92
|
+
throw new shell_1.Refusal('invoice-discount-exceeds', {}, 'Discount exceeds the line amount');
|
|
93
|
+
const supply = gross - discount;
|
|
94
|
+
if (supply > MAX_MINOR)
|
|
95
|
+
throw new shell_1.Refusal('invoice-amount-too-large', {}, 'Amount is too large');
|
|
96
|
+
let printed;
|
|
97
|
+
if (received) {
|
|
98
|
+
printed = line.receivedTaxAmount === null || line.receivedTaxAmount === undefined ? undefined : minorOf(line.receivedTaxAmount, digits);
|
|
99
|
+
if (printed === undefined || printed < 0n)
|
|
100
|
+
throw new shell_1.Refusal('invoice-received-tax-required', {}, 'Enter the tax printed on each received line');
|
|
101
|
+
}
|
|
102
|
+
const key = `${kind}:${rate}`;
|
|
103
|
+
const group = groups.get(key) ?? { taxKind: kind, rate, supply: 0n, tax: 0n };
|
|
104
|
+
group.supply += supply;
|
|
105
|
+
if (printed !== undefined)
|
|
106
|
+
group.tax += printed;
|
|
107
|
+
groups.set(key, group);
|
|
108
|
+
return {
|
|
109
|
+
description: line.description.trim(),
|
|
110
|
+
quantity: line.quantity,
|
|
111
|
+
unitPrice: line.unitPrice,
|
|
112
|
+
discount: line.discount,
|
|
113
|
+
taxKind: kind,
|
|
114
|
+
taxRate: line.taxRate,
|
|
115
|
+
supplyAmount: amountOf(supply, digits),
|
|
116
|
+
receivedTaxAmount: printed === undefined ? null : amountOf(printed, digits),
|
|
117
|
+
position
|
|
118
|
+
};
|
|
119
|
+
});
|
|
120
|
+
let supply = 0n;
|
|
121
|
+
let tax = 0n;
|
|
122
|
+
const taxGroups = [...groups.values()].map(group => {
|
|
123
|
+
if (!received)
|
|
124
|
+
group.tax = divideHalfUp(group.supply * group.rate, 100n * SCALE);
|
|
125
|
+
supply += group.supply;
|
|
126
|
+
tax += group.tax;
|
|
127
|
+
return { taxKind: group.taxKind, taxRate: Number(group.rate) / 10_000, supplyAmount: amountOf(group.supply, digits), taxAmount: amountOf(group.tax, digits) };
|
|
128
|
+
});
|
|
129
|
+
if (supply + tax > MAX_MINOR)
|
|
130
|
+
throw new shell_1.Refusal('invoice-amount-too-large', {}, 'Amount is too large');
|
|
131
|
+
return {
|
|
132
|
+
items: calculated,
|
|
133
|
+
taxGroups,
|
|
134
|
+
supplyAmount: amountOf(supply, digits),
|
|
135
|
+
taxAmount: amountOf(tax, digits),
|
|
136
|
+
totalAmount: amountOf(supply + tax, digits)
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
/** The groups of a saved document, read back from its lines. */
|
|
140
|
+
function taxGroupsOf(document) {
|
|
141
|
+
if (!document.items?.length)
|
|
142
|
+
return [];
|
|
143
|
+
return calculateDocument(document.items, document.currency, document.taxBasis === 'RECEIVED').taxGroups;
|
|
144
|
+
}
|
|
145
|
+
/** The fields every commercial document prints in its head, in the order the snapshot keeps them. */
|
|
146
|
+
exports.HEADER_KEYS = [
|
|
147
|
+
'number',
|
|
148
|
+
'direction',
|
|
149
|
+
'currency',
|
|
150
|
+
'language',
|
|
151
|
+
'supplierName',
|
|
152
|
+
'buyerName',
|
|
153
|
+
'issueDate',
|
|
154
|
+
'supplierAddress',
|
|
155
|
+
'supplierRegistrationNumber',
|
|
156
|
+
'buyerAddress',
|
|
157
|
+
'buyerRegistrationNumber',
|
|
158
|
+
'purchaseOrder',
|
|
159
|
+
'paymentTerms',
|
|
160
|
+
'bankDetails',
|
|
161
|
+
'notes',
|
|
162
|
+
'sourceReference'
|
|
163
|
+
];
|
|
164
|
+
/** The head of a document, trimmed and checked. */
|
|
165
|
+
function normalizedHeader(input) {
|
|
166
|
+
const number = String(input.number ?? '').trim();
|
|
167
|
+
const currency = String(input.currency ?? '').trim().toUpperCase();
|
|
168
|
+
if (!number ||
|
|
169
|
+
!String(input.supplierName ?? '').trim() ||
|
|
170
|
+
!String(input.buyerName ?? '').trim() ||
|
|
171
|
+
!['SALES', 'PURCHASE'].includes(input.direction) ||
|
|
172
|
+
!['ko', 'en'].includes(input.language)) {
|
|
173
|
+
throw new shell_1.Refusal('invoice-header-invalid', {}, 'Check number, parties, direction and language');
|
|
174
|
+
}
|
|
175
|
+
if (input.issueDate && !validDay(input.issueDate))
|
|
176
|
+
throw new shell_1.Refusal('invoice-date-invalid', {}, 'Use a valid YYYY-MM-DD date');
|
|
177
|
+
currencyDigits(currency);
|
|
178
|
+
const header = Object.fromEntries(exports.HEADER_KEYS.map(key => [key, input[key] ?? null]));
|
|
179
|
+
return { ...header, number, currency };
|
|
180
|
+
}
|
|
181
|
+
function requireDraft(status) {
|
|
182
|
+
if (status !== 'DRAFT')
|
|
183
|
+
throw new shell_1.Refusal('invoice-draft-required', {}, 'Issued and void invoices cannot be edited');
|
|
184
|
+
}
|
|
185
|
+
/** How much of a total is paid, in the currency's minor unit. */
|
|
186
|
+
function settlement(total, paid, currency = 'KRW') {
|
|
187
|
+
const digits = currencyDigits(currency);
|
|
188
|
+
const totalMinor = Math.round(total * 10 ** digits);
|
|
189
|
+
const paidMinor = Math.round(paid * 10 ** digits);
|
|
190
|
+
return {
|
|
191
|
+
paidAmount: paidMinor / 10 ** digits,
|
|
192
|
+
remainingAmount: Math.max(0, totalMinor - paidMinor) / 10 ** digits,
|
|
193
|
+
paymentStatus: paidMinor >= totalMinor ? 'PAID' : paidMinor <= 0 ? 'UNPAID' : 'PARTIAL'
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
/** The calendar day in a timezone. */
|
|
197
|
+
function calendarDay(timezone, now = new Date()) {
|
|
198
|
+
if (!timezone)
|
|
199
|
+
throw new Error('Domain timezone is required');
|
|
200
|
+
return new Intl.DateTimeFormat('en-CA', { timeZone: timezone, year: 'numeric', month: '2-digit', day: '2-digit' }).format(now);
|
|
201
|
+
}
|
|
202
|
+
//# sourceMappingURL=invoice-rules.js.map
|
|
@@ -0,0 +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"]}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { Domain } from '@things-factory/shell';
|
|
2
|
+
import { EntityManager } from 'typeorm';
|
|
3
|
+
import { Invoice } from './invoice';
|
|
4
|
+
import { NewInvoice } from './invoice-type';
|
|
5
|
+
export type InvoiceTransitionGuard = (tx: EntityManager, domain: Domain, invoice: Invoice, action: 'ISSUED' | 'VOID') => Promise<void>;
|
|
6
|
+
/** An application refuses a transition its own links forbid — Office keeps a linked invoice from being voided. */
|
|
7
|
+
export declare function registerInvoiceTransitionGuard(guard: InvoiceTransitionGuard): void;
|
|
8
|
+
/** Serialize document writes on the domain row; SQLite serializes writes by itself. */
|
|
9
|
+
export declare function lockDocumentDomain(tx: EntityManager, domain: Domain): Promise<void>;
|
|
10
|
+
export declare function invoiceInDomain(tx: EntityManager, domain: Domain, id: string): Promise<Invoice>;
|
|
11
|
+
export declare function saveInvoice(tx: EntityManager, domain: Domain, user: any, input: NewInvoice, id?: string): Promise<Invoice>;
|
|
12
|
+
export declare function transitionInvoice(tx: EntityManager, domain: Domain, user: any, id: string, action: 'ISSUED' | 'VOID', reason?: string): Promise<Invoice>;
|
|
13
|
+
/** The document as printed — what an issued invoice said stays readable whatever changes later. */
|
|
14
|
+
export declare function snapshotOf(invoice: Invoice): {
|
|
15
|
+
items: {
|
|
16
|
+
position: number;
|
|
17
|
+
description: string;
|
|
18
|
+
quantity: number;
|
|
19
|
+
unitPrice: number;
|
|
20
|
+
discount: number;
|
|
21
|
+
taxKind: import("./invoice-rules").TaxKind;
|
|
22
|
+
taxRate: number;
|
|
23
|
+
supplyAmount: number;
|
|
24
|
+
receivedTaxAmount: number;
|
|
25
|
+
}[];
|
|
26
|
+
};
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.registerInvoiceTransitionGuard = registerInvoiceTransitionGuard;
|
|
4
|
+
exports.lockDocumentDomain = lockDocumentDomain;
|
|
5
|
+
exports.invoiceInDomain = invoiceInDomain;
|
|
6
|
+
exports.saveInvoice = saveInvoice;
|
|
7
|
+
exports.transitionInvoice = transitionInvoice;
|
|
8
|
+
exports.snapshotOf = snapshotOf;
|
|
9
|
+
const shell_1 = require("@things-factory/shell");
|
|
10
|
+
const invoice_event_1 = require("../invoice-event/invoice-event");
|
|
11
|
+
const invoice_item_1 = require("../invoice-item/invoice-item");
|
|
12
|
+
const invoice_1 = require("./invoice");
|
|
13
|
+
const invoice_rules_1 = require("./invoice-rules");
|
|
14
|
+
const transitionGuards = new Set();
|
|
15
|
+
/** An application refuses a transition its own links forbid — Office keeps a linked invoice from being voided. */
|
|
16
|
+
function registerInvoiceTransitionGuard(guard) {
|
|
17
|
+
transitionGuards.add(guard);
|
|
18
|
+
}
|
|
19
|
+
/** Serialize document writes on the domain row; SQLite serializes writes by itself. */
|
|
20
|
+
async function lockDocumentDomain(tx, domain) {
|
|
21
|
+
const query = tx.getRepository(shell_1.Domain).createQueryBuilder('domain').where('domain.id = :id', { id: domain.id });
|
|
22
|
+
if (!['sqlite', 'better-sqlite3', 'sqljs'].includes(tx.connection.options.type))
|
|
23
|
+
query.setLock('pessimistic_write');
|
|
24
|
+
if (!(await query.getOne()))
|
|
25
|
+
throw new shell_1.Refusal('invoice-domain-missing', {}, 'Domain not found');
|
|
26
|
+
}
|
|
27
|
+
async function invoiceInDomain(tx, domain, id) {
|
|
28
|
+
const invoice = await tx.getRepository(invoice_1.Invoice).findOne({ where: { id, domain: { id: domain.id } }, relations: ['items', 'creator'] });
|
|
29
|
+
if (!invoice)
|
|
30
|
+
throw new shell_1.Refusal('invoice-not-found', {}, 'Invoice not found');
|
|
31
|
+
invoice.items.sort((a, b) => a.position - b.position);
|
|
32
|
+
return invoice;
|
|
33
|
+
}
|
|
34
|
+
async function saveInvoice(tx, domain, user, input, id) {
|
|
35
|
+
await lockDocumentDomain(tx, domain);
|
|
36
|
+
const old = id ? await invoiceInDomain(tx, domain, id) : undefined;
|
|
37
|
+
if (old)
|
|
38
|
+
(0, invoice_rules_1.requireDraft)(old.status);
|
|
39
|
+
const header = (0, invoice_rules_1.normalizedHeader)(input);
|
|
40
|
+
if (input.dueDate && !(0, invoice_rules_1.validDay)(input.dueDate))
|
|
41
|
+
throw new shell_1.Refusal('invoice-date-invalid', {}, 'Use a valid YYYY-MM-DD date');
|
|
42
|
+
if (input.issueDate && input.dueDate && input.dueDate < input.issueDate)
|
|
43
|
+
throw new shell_1.Refusal('invoice-due-before-issue', {}, 'Due date precedes issue date');
|
|
44
|
+
const received = input.taxBasis === invoice_1.InvoiceTaxBasis.RECEIVED;
|
|
45
|
+
if (received && header.direction !== 'PURCHASE')
|
|
46
|
+
throw new shell_1.Refusal('invoice-received-purchase-only', {}, 'Only a received purchase invoice keeps printed tax');
|
|
47
|
+
const repository = tx.getRepository(invoice_1.Invoice);
|
|
48
|
+
const duplicate = await repository.findOne({ where: { domain: { id: domain.id }, number: header.number } });
|
|
49
|
+
if (duplicate && duplicate.id !== id)
|
|
50
|
+
throw new shell_1.Refusal('invoice-number-duplicate', {}, 'Invoice number already exists');
|
|
51
|
+
const { items, supplyAmount, taxAmount, totalAmount } = (0, invoice_rules_1.calculateDocument)(input.items, header.currency, received);
|
|
52
|
+
const invoice = await repository.save(repository.create({
|
|
53
|
+
...old,
|
|
54
|
+
...header,
|
|
55
|
+
dueDate: input.dueDate ?? null,
|
|
56
|
+
taxBasis: received ? invoice_1.InvoiceTaxBasis.RECEIVED : invoice_1.InvoiceTaxBasis.CALCULATED,
|
|
57
|
+
supplyAmount,
|
|
58
|
+
taxAmount,
|
|
59
|
+
totalAmount,
|
|
60
|
+
domain,
|
|
61
|
+
creator: old?.creator ?? user,
|
|
62
|
+
updater: user,
|
|
63
|
+
items: undefined
|
|
64
|
+
}));
|
|
65
|
+
const lines = tx.getRepository(invoice_item_1.InvoiceItem);
|
|
66
|
+
if (old)
|
|
67
|
+
await lines.softDelete({ invoice: { id: invoice.id } });
|
|
68
|
+
await lines.save(items.map(item => lines.create({ ...item, invoice, domain, creator: user, updater: user })));
|
|
69
|
+
const saved = await invoiceInDomain(tx, domain, invoice.id);
|
|
70
|
+
await recordInvoiceEvent(tx, domain, user, saved, old ? 'UPDATED' : 'CREATED');
|
|
71
|
+
return saved;
|
|
72
|
+
}
|
|
73
|
+
async function transitionInvoice(tx, domain, user, id, action, reason) {
|
|
74
|
+
await lockDocumentDomain(tx, domain);
|
|
75
|
+
const invoice = await invoiceInDomain(tx, domain, id);
|
|
76
|
+
for (const guard of transitionGuards)
|
|
77
|
+
await guard(tx, domain, invoice, action);
|
|
78
|
+
if (action === 'ISSUED') {
|
|
79
|
+
(0, invoice_rules_1.requireDraft)(invoice.status);
|
|
80
|
+
if (!invoice.issueDate || !invoice.dueDate || !invoice.items.length) {
|
|
81
|
+
throw new shell_1.Refusal('invoice-issue-required', {}, 'Issue date, due date and items are required');
|
|
82
|
+
}
|
|
83
|
+
/* What the amounts were reckoned with stays on the document; a later rule never recomputes them. */
|
|
84
|
+
invoice.taxRounding = invoice_rules_1.TAX_ROUNDING;
|
|
85
|
+
invoice.currencyDigits = (0, invoice_rules_1.currencyDigits)(invoice.currency);
|
|
86
|
+
invoice.status = invoice_1.InvoiceStatus.ISSUED;
|
|
87
|
+
}
|
|
88
|
+
else {
|
|
89
|
+
if (invoice.status === invoice_1.InvoiceStatus.VOID || !reason?.trim())
|
|
90
|
+
throw new shell_1.Refusal('invoice-void-invalid', {}, 'A cancellation needs a reason and a non-void invoice');
|
|
91
|
+
invoice.voidReason = reason.trim();
|
|
92
|
+
invoice.status = invoice_1.InvoiceStatus.VOID;
|
|
93
|
+
}
|
|
94
|
+
invoice.updater = user;
|
|
95
|
+
const { items, ...head } = invoice;
|
|
96
|
+
await tx.getRepository(invoice_1.Invoice).save(head);
|
|
97
|
+
await recordInvoiceEvent(tx, domain, user, invoice, action, reason);
|
|
98
|
+
return invoiceInDomain(tx, domain, id);
|
|
99
|
+
}
|
|
100
|
+
function recordInvoiceEvent(tx, domain, user, invoice, action, reason) {
|
|
101
|
+
return tx.getRepository(invoice_event_1.InvoiceEvent).save({ domain, invoice, actor: user, action, reason, creator: user, updater: user, snapshot: snapshotOf(invoice) });
|
|
102
|
+
}
|
|
103
|
+
/** The document as printed — what an issued invoice said stays readable whatever changes later. */
|
|
104
|
+
function snapshotOf(invoice) {
|
|
105
|
+
const keys = [
|
|
106
|
+
'id',
|
|
107
|
+
...invoice_rules_1.HEADER_KEYS,
|
|
108
|
+
'status',
|
|
109
|
+
'dueDate',
|
|
110
|
+
'taxBasis',
|
|
111
|
+
'supplyAmount',
|
|
112
|
+
'taxAmount',
|
|
113
|
+
'totalAmount',
|
|
114
|
+
'taxRounding',
|
|
115
|
+
'currencyDigits',
|
|
116
|
+
'voidReason'
|
|
117
|
+
];
|
|
118
|
+
return {
|
|
119
|
+
...Object.fromEntries(keys.map(key => [key, invoice[key] ?? null])),
|
|
120
|
+
items: invoice.items.map(({ position, description, quantity, unitPrice, discount, taxKind, taxRate, supplyAmount, receivedTaxAmount }) => ({
|
|
121
|
+
position,
|
|
122
|
+
description,
|
|
123
|
+
quantity,
|
|
124
|
+
unitPrice,
|
|
125
|
+
discount,
|
|
126
|
+
taxKind,
|
|
127
|
+
taxRate,
|
|
128
|
+
supplyAmount,
|
|
129
|
+
receivedTaxAmount: receivedTaxAmount ?? null
|
|
130
|
+
}))
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
//# sourceMappingURL=invoice-service.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"invoice-service.js","sourceRoot":"","sources":["../../../server/service/invoice/invoice-service.ts"],"names":[],"mappings":"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","sourcesContent":["import { Domain, Refusal } from '@things-factory/shell'\nimport { EntityManager } from 'typeorm'\n\nimport { InvoiceEvent } from '../invoice-event/invoice-event'\nimport { InvoiceItem } from '../invoice-item/invoice-item'\nimport { Invoice, InvoiceStatus, InvoiceTaxBasis } from './invoice'\nimport { calculateDocument, currencyDigits, HEADER_KEYS, normalizedHeader, requireDraft, TAX_ROUNDING, validDay } from './invoice-rules'\nimport { NewInvoice } from './invoice-type'\n\nexport type InvoiceTransitionGuard = (tx: EntityManager, domain: Domain, invoice: Invoice, action: 'ISSUED' | 'VOID') => Promise<void>\nconst transitionGuards = new Set<InvoiceTransitionGuard>()\n\n/** An application refuses a transition its own links forbid — Office keeps a linked invoice from being voided. */\nexport function registerInvoiceTransitionGuard(guard: InvoiceTransitionGuard) {\n transitionGuards.add(guard)\n}\n\n/** Serialize document writes on the domain row; SQLite serializes writes by itself. */\nexport async function lockDocumentDomain(tx: EntityManager, domain: Domain) {\n const query = tx.getRepository(Domain).createQueryBuilder('domain').where('domain.id = :id', { id: domain.id })\n if (!['sqlite', 'better-sqlite3', 'sqljs'].includes(tx.connection.options.type)) query.setLock('pessimistic_write')\n if (!(await query.getOne())) throw new Refusal('invoice-domain-missing', {}, 'Domain not found')\n}\n\nexport async function invoiceInDomain(tx: EntityManager, domain: Domain, id: string) {\n const invoice = await tx.getRepository(Invoice).findOne({ where: { id, domain: { id: domain.id } }, relations: ['items', 'creator'] })\n if (!invoice) throw new Refusal('invoice-not-found', {}, 'Invoice not found')\n invoice.items.sort((a, b) => a.position - b.position)\n return invoice\n}\n\nexport async function saveInvoice(tx: EntityManager, domain: Domain, user: any, input: NewInvoice, id?: string) {\n await lockDocumentDomain(tx, domain)\n const old = id ? await invoiceInDomain(tx, domain, id) : undefined\n if (old) requireDraft(old.status)\n\n const header = normalizedHeader(input)\n if (input.dueDate && !validDay(input.dueDate)) throw new Refusal('invoice-date-invalid', {}, 'Use a valid YYYY-MM-DD date')\n if (input.issueDate && input.dueDate && input.dueDate < input.issueDate) throw new Refusal('invoice-due-before-issue', {}, 'Due date precedes issue date')\n const received = input.taxBasis === InvoiceTaxBasis.RECEIVED\n if (received && header.direction !== 'PURCHASE') throw new Refusal('invoice-received-purchase-only', {}, 'Only a received purchase invoice keeps printed tax')\n\n const repository = tx.getRepository(Invoice)\n const duplicate = await repository.findOne({ where: { domain: { id: domain.id }, number: header.number } })\n if (duplicate && duplicate.id !== id) throw new Refusal('invoice-number-duplicate', {}, 'Invoice number already exists')\n\n const { items, supplyAmount, taxAmount, totalAmount } = calculateDocument(input.items, header.currency, received)\n const invoice = await repository.save(\n repository.create({\n ...old,\n ...header,\n dueDate: input.dueDate ?? null,\n taxBasis: received ? InvoiceTaxBasis.RECEIVED : InvoiceTaxBasis.CALCULATED,\n supplyAmount,\n taxAmount,\n totalAmount,\n domain,\n creator: old?.creator ?? user,\n updater: user,\n items: undefined\n })\n )\n\n const lines = tx.getRepository(InvoiceItem)\n if (old) await lines.softDelete({ invoice: { id: invoice.id } })\n await lines.save(items.map(item => lines.create({ ...item, invoice, domain, creator: user, updater: user })))\n\n const saved = await invoiceInDomain(tx, domain, invoice.id)\n await recordInvoiceEvent(tx, domain, user, saved, old ? 'UPDATED' : 'CREATED')\n return saved\n}\n\nexport async function transitionInvoice(tx: EntityManager, domain: Domain, user: any, id: string, action: 'ISSUED' | 'VOID', reason?: string) {\n await lockDocumentDomain(tx, domain)\n const invoice = await invoiceInDomain(tx, domain, id)\n for (const guard of transitionGuards) await guard(tx, domain, invoice, action)\n\n if (action === 'ISSUED') {\n requireDraft(invoice.status)\n if (!invoice.issueDate || !invoice.dueDate || !invoice.items.length) {\n throw new Refusal('invoice-issue-required', {}, 'Issue date, due date and items are required')\n }\n /* What the amounts were reckoned with stays on the document; a later rule never recomputes them. */\n invoice.taxRounding = TAX_ROUNDING\n invoice.currencyDigits = currencyDigits(invoice.currency)\n invoice.status = InvoiceStatus.ISSUED\n } else {\n if (invoice.status === InvoiceStatus.VOID || !reason?.trim()) throw new Refusal('invoice-void-invalid', {}, 'A cancellation needs a reason and a non-void invoice')\n invoice.voidReason = reason.trim()\n invoice.status = InvoiceStatus.VOID\n }\n invoice.updater = user\n const { items, ...head } = invoice\n await tx.getRepository(Invoice).save(head)\n await recordInvoiceEvent(tx, domain, user, invoice, action, reason)\n return invoiceInDomain(tx, domain, id)\n}\n\nfunction recordInvoiceEvent(tx: EntityManager, domain: Domain, user: any, invoice: Invoice, action: string, reason?: string) {\n return tx.getRepository(InvoiceEvent).save({ domain, invoice, actor: user, action, reason, creator: user, updater: user, snapshot: snapshotOf(invoice) })\n}\n\n/** The document as printed — what an issued invoice said stays readable whatever changes later. */\nexport function snapshotOf(invoice: Invoice) {\n const keys = [\n 'id',\n ...HEADER_KEYS,\n 'status',\n 'dueDate',\n 'taxBasis',\n 'supplyAmount',\n 'taxAmount',\n 'totalAmount',\n 'taxRounding',\n 'currencyDigits',\n 'voidReason'\n ]\n return {\n ...Object.fromEntries(keys.map(key => [key, invoice[key] ?? null])),\n items: invoice.items.map(({ position, description, quantity, unitPrice, discount, taxKind, taxRate, supplyAmount, receivedTaxAmount }) => ({\n position,\n description,\n quantity,\n unitPrice,\n discount,\n taxKind,\n taxRate,\n supplyAmount,\n receivedTaxAmount: receivedTaxAmount ?? null\n }))\n }\n}\n"]}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { DocumentDirection } from './commercial-document';
|
|
2
|
+
import { Invoice, InvoiceTaxBasis } from './invoice';
|
|
3
|
+
import { TaxKind } from './invoice-rules';
|
|
4
|
+
export declare class DocumentLineInput {
|
|
5
|
+
description: string;
|
|
6
|
+
quantity: number;
|
|
7
|
+
unitPrice: number;
|
|
8
|
+
discount: number;
|
|
9
|
+
taxKind: TaxKind;
|
|
10
|
+
taxRate: number;
|
|
11
|
+
receivedTaxAmount?: number;
|
|
12
|
+
}
|
|
13
|
+
/** The head and lines every commercial document writes; invoice and quotation inputs add their own dates. */
|
|
14
|
+
export declare abstract class DocumentInput {
|
|
15
|
+
number: string;
|
|
16
|
+
direction: DocumentDirection;
|
|
17
|
+
currency: string;
|
|
18
|
+
language: string;
|
|
19
|
+
supplierName: string;
|
|
20
|
+
supplierAddress?: string;
|
|
21
|
+
supplierRegistrationNumber?: string;
|
|
22
|
+
buyerName: string;
|
|
23
|
+
buyerAddress?: string;
|
|
24
|
+
buyerRegistrationNumber?: string;
|
|
25
|
+
issueDate?: string;
|
|
26
|
+
purchaseOrder?: string;
|
|
27
|
+
paymentTerms?: string;
|
|
28
|
+
bankDetails?: string;
|
|
29
|
+
notes?: string;
|
|
30
|
+
sourceReference?: string;
|
|
31
|
+
items: DocumentLineInput[];
|
|
32
|
+
}
|
|
33
|
+
export declare class NewInvoice extends DocumentInput {
|
|
34
|
+
dueDate?: string;
|
|
35
|
+
taxBasis: InvoiceTaxBasis;
|
|
36
|
+
}
|
|
37
|
+
export declare class InvoiceList {
|
|
38
|
+
items: Invoice[];
|
|
39
|
+
total: number;
|
|
40
|
+
}
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.InvoiceList = exports.NewInvoice = exports.DocumentInput = exports.DocumentLineInput = void 0;
|
|
4
|
+
const tslib_1 = require("tslib");
|
|
5
|
+
const type_graphql_1 = require("type-graphql");
|
|
6
|
+
const commercial_document_1 = require("./commercial-document");
|
|
7
|
+
const invoice_1 = require("./invoice");
|
|
8
|
+
const invoice_rules_1 = require("./invoice-rules");
|
|
9
|
+
let DocumentLineInput = class DocumentLineInput {
|
|
10
|
+
};
|
|
11
|
+
exports.DocumentLineInput = DocumentLineInput;
|
|
12
|
+
tslib_1.__decorate([
|
|
13
|
+
(0, type_graphql_1.Field)({ description: 'What the line charges for' }),
|
|
14
|
+
tslib_1.__metadata("design:type", String)
|
|
15
|
+
], DocumentLineInput.prototype, "description", void 0);
|
|
16
|
+
tslib_1.__decorate([
|
|
17
|
+
(0, type_graphql_1.Field)(type => type_graphql_1.Float, { description: 'Quantity' }),
|
|
18
|
+
tslib_1.__metadata("design:type", Number)
|
|
19
|
+
], DocumentLineInput.prototype, "quantity", void 0);
|
|
20
|
+
tslib_1.__decorate([
|
|
21
|
+
(0, type_graphql_1.Field)(type => type_graphql_1.Float, { description: 'Price of one unit' }),
|
|
22
|
+
tslib_1.__metadata("design:type", Number)
|
|
23
|
+
], DocumentLineInput.prototype, "unitPrice", void 0);
|
|
24
|
+
tslib_1.__decorate([
|
|
25
|
+
(0, type_graphql_1.Field)(type => type_graphql_1.Float, { defaultValue: 0, description: 'Discount taken off the line, in the document currency' }),
|
|
26
|
+
tslib_1.__metadata("design:type", Number)
|
|
27
|
+
], DocumentLineInput.prototype, "discount", void 0);
|
|
28
|
+
tslib_1.__decorate([
|
|
29
|
+
(0, type_graphql_1.Field)(type => invoice_rules_1.TaxKind, { defaultValue: invoice_rules_1.TaxKind.TAXABLE, description: 'Taxable, zero-rated or exempt' }),
|
|
30
|
+
tslib_1.__metadata("design:type", String)
|
|
31
|
+
], DocumentLineInput.prototype, "taxKind", void 0);
|
|
32
|
+
tslib_1.__decorate([
|
|
33
|
+
(0, type_graphql_1.Field)(type => type_graphql_1.Float, { defaultValue: 0, description: 'Tax rate in percent; zero unless taxable' }),
|
|
34
|
+
tslib_1.__metadata("design:type", Number)
|
|
35
|
+
], DocumentLineInput.prototype, "taxRate", void 0);
|
|
36
|
+
tslib_1.__decorate([
|
|
37
|
+
(0, type_graphql_1.Field)(type => type_graphql_1.Float, { nullable: true, description: 'Tax the sender printed on the line — only on a received invoice' }),
|
|
38
|
+
tslib_1.__metadata("design:type", Number)
|
|
39
|
+
], DocumentLineInput.prototype, "receivedTaxAmount", void 0);
|
|
40
|
+
exports.DocumentLineInput = DocumentLineInput = tslib_1.__decorate([
|
|
41
|
+
(0, type_graphql_1.InputType)({ description: 'One line of a commercial document to save' })
|
|
42
|
+
], DocumentLineInput);
|
|
43
|
+
/** The head and lines every commercial document writes; invoice and quotation inputs add their own dates. */
|
|
44
|
+
let DocumentInput = class DocumentInput {
|
|
45
|
+
};
|
|
46
|
+
exports.DocumentInput = DocumentInput;
|
|
47
|
+
tslib_1.__decorate([
|
|
48
|
+
(0, type_graphql_1.Field)({ description: 'Document number, unique in the domain' }),
|
|
49
|
+
tslib_1.__metadata("design:type", String)
|
|
50
|
+
], DocumentInput.prototype, "number", void 0);
|
|
51
|
+
tslib_1.__decorate([
|
|
52
|
+
(0, type_graphql_1.Field)(type => commercial_document_1.DocumentDirection, { description: 'SALES for a document the domain sends, PURCHASE for one it receives' }),
|
|
53
|
+
tslib_1.__metadata("design:type", String)
|
|
54
|
+
], DocumentInput.prototype, "direction", void 0);
|
|
55
|
+
tslib_1.__decorate([
|
|
56
|
+
(0, type_graphql_1.Field)({ description: 'ISO 4217 currency code' }),
|
|
57
|
+
tslib_1.__metadata("design:type", String)
|
|
58
|
+
], DocumentInput.prototype, "currency", void 0);
|
|
59
|
+
tslib_1.__decorate([
|
|
60
|
+
(0, type_graphql_1.Field)({ description: 'Language the document is printed in: ko or en' }),
|
|
61
|
+
tslib_1.__metadata("design:type", String)
|
|
62
|
+
], DocumentInput.prototype, "language", void 0);
|
|
63
|
+
tslib_1.__decorate([
|
|
64
|
+
(0, type_graphql_1.Field)({ description: 'Supplier name as printed' }),
|
|
65
|
+
tslib_1.__metadata("design:type", String)
|
|
66
|
+
], DocumentInput.prototype, "supplierName", void 0);
|
|
67
|
+
tslib_1.__decorate([
|
|
68
|
+
(0, type_graphql_1.Field)({ nullable: true, description: 'Supplier address as printed' }),
|
|
69
|
+
tslib_1.__metadata("design:type", String)
|
|
70
|
+
], DocumentInput.prototype, "supplierAddress", void 0);
|
|
71
|
+
tslib_1.__decorate([
|
|
72
|
+
(0, type_graphql_1.Field)({ nullable: true, description: 'Supplier business or tax registration number' }),
|
|
73
|
+
tslib_1.__metadata("design:type", String)
|
|
74
|
+
], DocumentInput.prototype, "supplierRegistrationNumber", void 0);
|
|
75
|
+
tslib_1.__decorate([
|
|
76
|
+
(0, type_graphql_1.Field)({ description: 'Buyer name as printed' }),
|
|
77
|
+
tslib_1.__metadata("design:type", String)
|
|
78
|
+
], DocumentInput.prototype, "buyerName", void 0);
|
|
79
|
+
tslib_1.__decorate([
|
|
80
|
+
(0, type_graphql_1.Field)({ nullable: true, description: 'Buyer address as printed' }),
|
|
81
|
+
tslib_1.__metadata("design:type", String)
|
|
82
|
+
], DocumentInput.prototype, "buyerAddress", void 0);
|
|
83
|
+
tslib_1.__decorate([
|
|
84
|
+
(0, type_graphql_1.Field)({ nullable: true, description: 'Buyer business or tax registration number' }),
|
|
85
|
+
tslib_1.__metadata("design:type", String)
|
|
86
|
+
], DocumentInput.prototype, "buyerRegistrationNumber", void 0);
|
|
87
|
+
tslib_1.__decorate([
|
|
88
|
+
(0, type_graphql_1.Field)({ nullable: true, description: 'Issue date, YYYY-MM-DD' }),
|
|
89
|
+
tslib_1.__metadata("design:type", String)
|
|
90
|
+
], DocumentInput.prototype, "issueDate", void 0);
|
|
91
|
+
tslib_1.__decorate([
|
|
92
|
+
(0, type_graphql_1.Field)({ nullable: true, description: 'Buyer purchase order reference' }),
|
|
93
|
+
tslib_1.__metadata("design:type", String)
|
|
94
|
+
], DocumentInput.prototype, "purchaseOrder", void 0);
|
|
95
|
+
tslib_1.__decorate([
|
|
96
|
+
(0, type_graphql_1.Field)({ nullable: true, description: 'Payment terms as printed' }),
|
|
97
|
+
tslib_1.__metadata("design:type", String)
|
|
98
|
+
], DocumentInput.prototype, "paymentTerms", void 0);
|
|
99
|
+
tslib_1.__decorate([
|
|
100
|
+
(0, type_graphql_1.Field)({ nullable: true, description: 'Bank account to pay into, as printed' }),
|
|
101
|
+
tslib_1.__metadata("design:type", String)
|
|
102
|
+
], DocumentInput.prototype, "bankDetails", void 0);
|
|
103
|
+
tslib_1.__decorate([
|
|
104
|
+
(0, type_graphql_1.Field)({ nullable: true, description: 'Notes printed on the document' }),
|
|
105
|
+
tslib_1.__metadata("design:type", String)
|
|
106
|
+
], DocumentInput.prototype, "notes", void 0);
|
|
107
|
+
tslib_1.__decorate([
|
|
108
|
+
(0, type_graphql_1.Field)({ nullable: true, description: 'Reference to the record the document was made from' }),
|
|
109
|
+
tslib_1.__metadata("design:type", String)
|
|
110
|
+
], DocumentInput.prototype, "sourceReference", void 0);
|
|
111
|
+
tslib_1.__decorate([
|
|
112
|
+
(0, type_graphql_1.Field)(type => [DocumentLineInput], { description: 'Lines in printed order' }),
|
|
113
|
+
tslib_1.__metadata("design:type", Array)
|
|
114
|
+
], DocumentInput.prototype, "items", void 0);
|
|
115
|
+
exports.DocumentInput = DocumentInput = tslib_1.__decorate([
|
|
116
|
+
(0, type_graphql_1.InputType)({ description: 'The printed head and lines of a commercial document' })
|
|
117
|
+
], DocumentInput);
|
|
118
|
+
let NewInvoice = class NewInvoice extends DocumentInput {
|
|
119
|
+
};
|
|
120
|
+
exports.NewInvoice = NewInvoice;
|
|
121
|
+
tslib_1.__decorate([
|
|
122
|
+
(0, type_graphql_1.Field)({ nullable: true, description: 'Payment due date, YYYY-MM-DD' }),
|
|
123
|
+
tslib_1.__metadata("design:type", String)
|
|
124
|
+
], NewInvoice.prototype, "dueDate", void 0);
|
|
125
|
+
tslib_1.__decorate([
|
|
126
|
+
(0, type_graphql_1.Field)(type => invoice_1.InvoiceTaxBasis, { defaultValue: invoice_1.InvoiceTaxBasis.CALCULATED, description: 'Reckoned tax, or the tax a received invoice printed' }),
|
|
127
|
+
tslib_1.__metadata("design:type", String)
|
|
128
|
+
], NewInvoice.prototype, "taxBasis", void 0);
|
|
129
|
+
exports.NewInvoice = NewInvoice = tslib_1.__decorate([
|
|
130
|
+
(0, type_graphql_1.InputType)({ description: 'An invoice draft to create or replace' })
|
|
131
|
+
], NewInvoice);
|
|
132
|
+
let InvoiceList = class InvoiceList {
|
|
133
|
+
};
|
|
134
|
+
exports.InvoiceList = InvoiceList;
|
|
135
|
+
tslib_1.__decorate([
|
|
136
|
+
(0, type_graphql_1.Field)(type => [invoice_1.Invoice], { description: 'Invoices on the page' }),
|
|
137
|
+
tslib_1.__metadata("design:type", Array)
|
|
138
|
+
], InvoiceList.prototype, "items", void 0);
|
|
139
|
+
tslib_1.__decorate([
|
|
140
|
+
(0, type_graphql_1.Field)(type => type_graphql_1.Int, { description: 'Number of invoices matching the list parameters' }),
|
|
141
|
+
tslib_1.__metadata("design:type", Number)
|
|
142
|
+
], InvoiceList.prototype, "total", void 0);
|
|
143
|
+
exports.InvoiceList = InvoiceList = tslib_1.__decorate([
|
|
144
|
+
(0, type_graphql_1.ObjectType)({ description: 'A page of invoices' })
|
|
145
|
+
], InvoiceList);
|
|
146
|
+
//# sourceMappingURL=invoice-type.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"invoice-type.js","sourceRoot":"","sources":["../../../server/service/invoice/invoice-type.ts"],"names":[],"mappings":"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","sourcesContent":["import { Field, Float, InputType, Int, ObjectType } from 'type-graphql'\n\nimport { DocumentDirection } from './commercial-document'\nimport { Invoice, InvoiceTaxBasis } from './invoice'\nimport { TaxKind } from './invoice-rules'\n\n@InputType({ description: 'One line of a commercial document to save' })\nexport class DocumentLineInput {\n @Field({ description: 'What the line charges for' }) description: string\n @Field(type => Float, { description: 'Quantity' }) quantity: number\n @Field(type => Float, { description: 'Price of one unit' }) unitPrice: number\n @Field(type => Float, { defaultValue: 0, description: 'Discount taken off the line, in the document currency' }) discount: number\n @Field(type => TaxKind, { defaultValue: TaxKind.TAXABLE, description: 'Taxable, zero-rated or exempt' }) taxKind: TaxKind\n @Field(type => Float, { defaultValue: 0, description: 'Tax rate in percent; zero unless taxable' }) taxRate: number\n @Field(type => Float, { nullable: true, description: 'Tax the sender printed on the line — only on a received invoice' }) receivedTaxAmount?: number\n}\n\n/** The head and lines every commercial document writes; invoice and quotation inputs add their own dates. */\n@InputType({ description: 'The printed head and lines of a commercial document' })\nexport abstract class DocumentInput {\n @Field({ description: 'Document number, unique in the domain' }) number: string\n @Field(type => DocumentDirection, { description: 'SALES for a document the domain sends, PURCHASE for one it receives' }) direction: DocumentDirection\n @Field({ description: 'ISO 4217 currency code' }) currency: string\n @Field({ description: 'Language the document is printed in: ko or en' }) language: string\n @Field({ description: 'Supplier name as printed' }) supplierName: string\n @Field({ nullable: true, description: 'Supplier address as printed' }) supplierAddress?: string\n @Field({ nullable: true, description: 'Supplier business or tax registration number' }) supplierRegistrationNumber?: string\n @Field({ description: 'Buyer name as printed' }) buyerName: string\n @Field({ nullable: true, description: 'Buyer address as printed' }) buyerAddress?: string\n @Field({ nullable: true, description: 'Buyer business or tax registration number' }) buyerRegistrationNumber?: string\n @Field({ nullable: true, description: 'Issue date, YYYY-MM-DD' }) issueDate?: string\n @Field({ nullable: true, description: 'Buyer purchase order reference' }) purchaseOrder?: string\n @Field({ nullable: true, description: 'Payment terms as printed' }) paymentTerms?: string\n @Field({ nullable: true, description: 'Bank account to pay into, as printed' }) bankDetails?: string\n @Field({ nullable: true, description: 'Notes printed on the document' }) notes?: string\n @Field({ nullable: true, description: 'Reference to the record the document was made from' }) sourceReference?: string\n @Field(type => [DocumentLineInput], { description: 'Lines in printed order' }) items: DocumentLineInput[]\n}\n\n@InputType({ description: 'An invoice draft to create or replace' })\nexport class NewInvoice extends DocumentInput {\n @Field({ nullable: true, description: 'Payment due date, YYYY-MM-DD' }) dueDate?: string\n @Field(type => InvoiceTaxBasis, { defaultValue: InvoiceTaxBasis.CALCULATED, description: 'Reckoned tax, or the tax a received invoice printed' })\n taxBasis: InvoiceTaxBasis\n}\n\n@ObjectType({ description: 'A page of invoices' })\nexport class InvoiceList {\n @Field(type => [Invoice], { description: 'Invoices on the page' }) items: Invoice[]\n @Field(type => Int, { description: 'Number of invoices matching the list parameters' }) total: number\n}\n"]}
|