@things-factory/invoice 10.1.232 → 10.1.234
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-lines.d.ts +1 -1
- package/dist-client/document/document-lines.js +10 -8
- package/dist-client/document/document-lines.js.map +1 -1
- package/dist-client/tsconfig.tsbuildinfo +1 -1
- package/dist-client/viewparts/document-defaults-setting-let.js +2 -2
- package/dist-client/viewparts/document-defaults-setting-let.js.map +1 -1
- package/dist-server/service/document-defaults/document-defaults-resolver.js +3 -2
- package/dist-server/service/document-defaults/document-defaults-resolver.js.map +1 -1
- package/dist-server/service/document-defaults/document-defaults-rules.d.ts +6 -0
- package/dist-server/service/document-defaults/document-defaults-rules.js +18 -6
- package/dist-server/service/document-defaults/document-defaults-rules.js.map +1 -1
- package/dist-server/service/invoice/invoice-rules.js +4 -3
- package/dist-server/service/invoice/invoice-rules.js.map +1 -1
- package/dist-server/tsconfig.tsbuildinfo +1 -1
- package/dist-shared/document-amounts.d.ts +7 -1
- package/dist-shared/document-amounts.js +18 -2
- package/dist-shared/document-amounts.js.map +1 -1
- package/package.json +9 -9
- package/shared/document-amounts.ts +19 -2
- package/translations/en.json +5 -1
- package/translations/es.json +5 -1
- package/translations/ja.json +5 -1
- package/translations/ko.json +5 -1
- package/translations/ms.json +5 -1
- package/translations/zh.json +5 -1
- package/tsconfig.shared.tsbuildinfo +1 -1
|
@@ -92,11 +92,11 @@ let DocumentDefaultsSettingLet = class DocumentDefaultsSettingLet extends locali
|
|
|
92
92
|
${prose('notes', 'field.invoice-notes')}
|
|
93
93
|
<label
|
|
94
94
|
>${i18next.t('field.invoice-defaults-valid-days')}
|
|
95
|
-
<input type="number" min="1" max="365" .value=${String(value.validDays)} @input=${(e) => set({ validDays: Number(e.target.value) })}
|
|
95
|
+
<input type="number" min="1" max="365" .value=${String(value.validDays ?? '')} @input=${(e) => set({ validDays: e.target.value === '' ? null : Number(e.target.value) })}
|
|
96
96
|
/></label>
|
|
97
97
|
<label
|
|
98
98
|
>${i18next.t('field.invoice-defaults-payment-days')}
|
|
99
|
-
<input type="number" min="0" max="365" .value=${String(value.paymentDays)} @input=${(e) => set({ paymentDays: Number(e.target.value) })}
|
|
99
|
+
<input type="number" min="0" max="365" .value=${String(value.paymentDays ?? '')} @input=${(e) => set({ paymentDays: e.target.value === '' ? null : Number(e.target.value) })}
|
|
100
100
|
/></label>
|
|
101
101
|
<div actions>
|
|
102
102
|
<md-outlined-button ?disabled=${this.busy} @click=${() => this.save()}>${i18next.t('button.save')}</md-outlined-button>
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"document-defaults-setting-let.js","sourceRoot":"","sources":["../../client/viewparts/document-defaults-setting-let.ts"],"names":[],"mappings":";AAAA,OAAO,yCAAyC,CAAA;AAChD,OAAO,8BAA8B,CAAA;AAErC,OAAO,GAAG,MAAM,aAAa,CAAA;AAC7B,OAAO,EAAE,GAAG,EAAE,IAAI,EAAE,UAAU,EAAE,OAAO,EAAE,MAAM,KAAK,CAAA;AACpD,OAAO,EAAE,aAAa,EAAE,KAAK,EAAE,MAAM,mBAAmB,CAAA;AAExD,OAAO,EAAE,MAAM,EAAE,MAAM,kBAAkB,CAAA;AACzC,OAAO,EAAE,OAAO,EAAE,QAAQ,EAAE,MAAM,eAAe,CAAA;AACjD,OAAO,EAAE,MAAM,EAAE,MAAM,iBAAiB,CAAA;AACxC,OAAO,EAAE,YAAY,EAAE,MAAM,uCAAuC,CAAA;AAEpE,OAAO,EAAE,wBAAwB,EAAE,gBAAgB,EAAyB,MAAM,+BAA+B,CAAA;AAEjH;;;;;GAKG;AACH,6GAA6G;AAC7G,MAAM,OAAO,GAAG,CAAC,EAAE,YAAY,EAAE,0BAA0B,EAAE,eAAe,EAAE,GAAG,QAAQ,EAAoB,EAAE,EAAE,CAAC,QAAQ,CAAA;AAGnH,IAAM,0BAA0B,GAAhC,MAAM,0BAA2B,SAAQ,QAAQ,CAAC,OAAO,CAAC,CAAC,UAAU,CAAC;IAAtE;;QAgDY,SAAI,GAAG,KAAK,CAAA;IA2D/B,CAAC;aA1GQ,WAAM,GAAG;QACd,GAAG,CAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;KA0CF;KACF,AA5CY,CA4CZ;IAKD,KAAK,CAAC,iBAAiB;QACrB,KAAK,CAAC,iBAAiB,EAAE,CAAA;QACzB,IAAI,CAAC,CAAC,MAAM,YAAY,CAAC,EAAE,QAAQ,EAAE,SAAS,EAAE,SAAS,EAAE,UAAU,EAAE,kBAAkB,EAAE,IAAI,EAAE,CAAC,CAAC;YAAE,OAAM;QAC3G,IAAI,CAAC,KAAK,GAAG,CAAC,MAAM,gBAAgB,EAAE,CAAC,IAAI,SAAS,CAAA;IACtD,CAAC;IAED,MAAM;QACJ,MAAM,KAAK,GAAG,IAAI,CAAC,KAAK,CAAA;QACxB,IAAI,CAAC,KAAK;YAAE,OAAO,OAAO,CAAA;QAC1B,MAAM,GAAG,GAAG,CAAC,KAAgC,EAAE,EAAE,CAAC,CAAC,IAAI,CAAC,KAAK,GAAG,EAAE,GAAG,KAAK,EAAE,GAAG,KAAK,EAAE,CAAC,CAAA;QACvF,MAAM,KAAK,GAAG,CAAC,IAA4B,EAAE,KAAa,EAAE,EAAE,CAC5D,IAAI,CAAA,UAAU,OAAO,CAAC,CAAC,CAAC,KAAK,CAAC,iBAAiB,MAAM,CAAC,KAAK,CAAC,IAAI,CAAC,IAAI,EAAE,CAAC,WAAW,CAAC,CAAM,EAAE,EAAE,CAAC,GAAG,CAAC,EAAE,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC,MAAM,CAAC,KAAK,EAAE,CAAC,aAAa,CAAA;QAC7I,MAAM,KAAK,GAAG,CAAC,IAA4B,EAAE,KAAa,EAAE,EAAE,CAC5D,IAAI,CAAA,uBAAuB,OAAO,CAAC,CAAC,CAAC,KAAK,CAAC,oBAAoB,MAAM,CAAC,KAAK,CAAC,IAAI,CAAC,IAAI,EAAE,CAAC,WAAW,CAAC,CAAM,EAAE,EAAE,CAAC,GAAG,CAAC,EAAE,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC,MAAM,CAAC,KAAK,EAAE,CAAC,sBAAsB,CAAA;QAEtK,OAAO,IAAI,CAAA;;6BAEc,OAAO,CAAC,CAAC,CAAC,iCAAiC,CAAC;;YAE7D,KAAK,CAAC,aAAa,EAAE,4BAA4B,CAAC;;eAE/C,OAAO,CAAC,CAAC,CAAC,6BAA6B,CAAC,WAAW,OAAO,CAAC,CAAC,CAAC,kCAAkC,CAAC;+BAChF,KAAK,CAAC,YAAY,CAAC,IAAI,CAAC,IAAI,CAAC,WAAW,CAAC,CAAM,EAAE,EAAE,CAAC,GAAG,CAAC,EAAE,YAAY,EAAE,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,KAAK,CAAC,IAAI,CAAC,EAAE,CAAC;;YAExH,KAAK,CAAC,OAAO,EAAE,qBAAqB,CAAC;;eAElC,OAAO,CAAC,CAAC,CAAC,mCAAmC,CAAC;4DACD,MAAM,CAAC,KAAK,CAAC,SAAS,CAAC,WAAW,CAAC,CAAM,EAAE,EAAE,CAAC,GAAG,CAAC,EAAE,SAAS,EAAE,MAAM,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,EAAE,CAAC;;;
|
|
1
|
+
{"version":3,"file":"document-defaults-setting-let.js","sourceRoot":"","sources":["../../client/viewparts/document-defaults-setting-let.ts"],"names":[],"mappings":"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","sourcesContent":["import '@material/web/button/outlined-button.js'\nimport '@things-factory/setting-base'\n\nimport gql from 'graphql-tag'\nimport { css, html, LitElement, nothing } from 'lit'\nimport { customElement, state } from 'lit/decorators.js'\n\nimport { client } from '@operato/graphql'\nimport { i18next, localize } from '@operato/i18n'\nimport { notify } from '@operato/layout'\nimport { hasPrivilege } from '@things-factory/auth-base/dist-client'\n\nimport { DOCUMENT_DEFAULTS_FIELDS, documentDefaults, type DocumentDefaults } from '../document/document-defaults'\n\n/**\n * The company's own words for every new quotation and invoice (ADR-0170): the account to pay into, the\n * payment terms offered - one a line, the first a new document's - a standing note, how long an offer holds and how\n * many days after its issue an invoice is due. Shown\n * to whoever keeps the invoices; for anyone else the card stays empty.\n */\n/* Our name, number and address come with the defaults but are 「우리 회사」's, kept on its own card (ADR-0176). */\nconst inputOf = ({ supplierName, supplierRegistrationNumber, supplierAddress, ...defaults }: DocumentDefaults) => defaults\n\n@customElement('document-defaults-setting-let')\nexport class DocumentDefaultsSettingLet extends localize(i18next)(LitElement) {\n static styles = [\n css`\n div[content] {\n display: grid;\n grid-template-columns: repeat(auto-fit, minmax(min(240px, 100%), 1fr));\n gap: var(--spacing-medium, 8px) var(--spacing-large, 12px);\n }\n label {\n display: flex;\n flex-direction: column;\n gap: var(--spacing-small, 4px);\n font: var(--label-font);\n color: var(--md-sys-color-on-surface);\n text-transform: var(--label-text-transform);\n }\n label.wide {\n grid-column: 1 / -1;\n }\n small {\n color: var(--md-sys-color-on-surface-variant);\n text-transform: none;\n }\n input,\n textarea {\n box-sizing: border-box;\n width: 100%;\n background-color: var(--input-field-background, var(--md-sys-color-surface-container-highest));\n color: var(--input-field-color, var(--md-sys-color-on-surface));\n border: var(--border-dim-color);\n border-radius: var(--border-radius);\n padding: var(--input-padding);\n font: var(--input-font);\n text-transform: none;\n }\n textarea {\n min-height: 64px;\n resize: vertical;\n }\n div[actions] {\n grid-column: 1 / -1;\n display: flex;\n justify-content: flex-end;\n }\n `\n ]\n\n @state() private value?: DocumentDefaults\n @state() private busy = false\n\n async connectedCallback() {\n super.connectedCallback()\n if (!(await hasPrivilege({ category: 'invoice', privilege: 'mutation', domainOwnerGranted: true }))) return\n this.value = (await documentDefaults()) ?? undefined\n }\n\n render() {\n const value = this.value\n if (!value) return nothing\n const set = (patch: Partial<DocumentDefaults>) => (this.value = { ...value, ...patch })\n const input = (name: keyof DocumentDefaults, label: string) =>\n html`<label>${i18next.t(label)}<input .value=${String(value[name] ?? '')} @input=${(e: any) => set({ [name]: e.target.value })} /></label>`\n const prose = (name: keyof DocumentDefaults, label: string) =>\n html`<label class=\"wide\">${i18next.t(label)}<textarea .value=${String(value[name] ?? '')} @input=${(e: any) => set({ [name]: e.target.value })}></textarea></label>`\n\n return html`\n <setting-let>\n <span slot=\"title\">${i18next.t('title.invoice-document-defaults')}</span>\n <div slot=\"content\" content>\n ${prose('bankDetails', 'field.invoice-bank-details')}\n <label class=\"wide\"\n >${i18next.t('field.invoice-payment-terms')} <small>${i18next.t('text.invoice-defaults-terms-hint')}</small>\n <textarea .value=${value.paymentTerms.join('\\n')} @input=${(e: any) => set({ paymentTerms: e.target.value.split('\\n') })}></textarea>\n </label>\n ${prose('notes', 'field.invoice-notes')}\n <label\n >${i18next.t('field.invoice-defaults-valid-days')}\n <input type=\"number\" min=\"1\" max=\"365\" .value=${String(value.validDays ?? '')} @input=${(e: any) => set({ validDays: e.target.value === '' ? null : Number(e.target.value) })}\n /></label>\n <label\n >${i18next.t('field.invoice-defaults-payment-days')}\n <input type=\"number\" min=\"0\" max=\"365\" .value=${String(value.paymentDays ?? '')} @input=${(e: any) => set({ paymentDays: e.target.value === '' ? null : Number(e.target.value) })}\n /></label>\n <div actions>\n <md-outlined-button ?disabled=${this.busy} @click=${() => this.save()}>${i18next.t('button.save')}</md-outlined-button>\n </div>\n </div>\n </setting-let>\n `\n }\n\n private async save() {\n this.busy = true\n try {\n const response = await client.mutate({\n mutation: gql(`mutation ($defaults: DocumentDefaultsInput!) { updateDocumentDefaults(defaults: $defaults) { ${DOCUMENT_DEFAULTS_FIELDS} } }`),\n variables: { defaults: inputOf(this.value!) }\n })\n if (response.errors?.length) throw response.errors[0]\n this.value = response.data.updateDocumentDefaults\n notify({ message: i18next.t('text.saved') })\n } catch (error: any) {\n notify({ level: 'error', message: error?.message ?? String(error) })\n } finally {\n this.busy = false\n }\n }\n}\n"]}
|
|
@@ -61,11 +61,11 @@ tslib_1.__decorate([
|
|
|
61
61
|
tslib_1.__metadata("design:type", String)
|
|
62
62
|
], DocumentDefaultsInput.prototype, "notes", void 0);
|
|
63
63
|
tslib_1.__decorate([
|
|
64
|
-
(0, type_graphql_1.Field)(type => type_graphql_1.
|
|
64
|
+
(0, type_graphql_1.Field)(type => type_graphql_1.Float, { nullable: true, description: 'Days a quotation holds after its issue day - a whole number from 1 to 365; another is refused naming it.' }),
|
|
65
65
|
tslib_1.__metadata("design:type", Number)
|
|
66
66
|
], DocumentDefaultsInput.prototype, "validDays", void 0);
|
|
67
67
|
tslib_1.__decorate([
|
|
68
|
-
(0, type_graphql_1.Field)(type => type_graphql_1.
|
|
68
|
+
(0, type_graphql_1.Field)(type => type_graphql_1.Float, { nullable: true, description: 'Days an invoice is due after its issue day - a whole number from 0 to 365; another is refused naming it.' }),
|
|
69
69
|
tslib_1.__metadata("design:type", Number)
|
|
70
70
|
], DocumentDefaultsInput.prototype, "paymentDays", void 0);
|
|
71
71
|
exports.DocumentDefaultsInput = DocumentDefaultsInput = tslib_1.__decorate([
|
|
@@ -94,6 +94,7 @@ let DocumentDefaultsResolver = class DocumentDefaultsResolver {
|
|
|
94
94
|
}
|
|
95
95
|
async updateDocumentDefaults(defaults, context) {
|
|
96
96
|
const { domain, tx } = context.state;
|
|
97
|
+
(0, document_defaults_rules_1.checkDocumentDefaults)(defaults);
|
|
97
98
|
const kept = (0, document_defaults_rules_1.documentDefaultsOf)(defaults);
|
|
98
99
|
const repository = tx.getRepository(shell_1.EnvVar);
|
|
99
100
|
const own = await repository.findOne({ where: { domain: { id: domain.id }, name: document_defaults_rules_1.DOCUMENT_DEFAULTS_ATTRIBUTE } });
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"document-defaults-resolver.js","sourceRoot":"","sources":["../../../server/service/document-defaults/document-defaults-resolver.ts"],"names":[],"mappings":";;;;AAAA,+
|
|
1
|
+
{"version":3,"file":"document-defaults-resolver.js","sourceRoot":"","sources":["../../../server/service/document-defaults/document-defaults-resolver.ts"],"names":[],"mappings":"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","sourcesContent":["import { Arg, Ctx, Directive, Field, Float, InputType, Int, Mutation, ObjectType, Query, Resolver, UseMiddleware } from 'type-graphql'\nimport { In } from 'typeorm'\n\nimport { printedCompanyOf } from '@things-factory/biz-base'\nimport { dedupByClosestDomain, EnvVar, getDataSource, getDomainIdsWithAncestors, SpeakRefusals } from '@things-factory/shell'\n\nimport { checkDocumentDefaults, DOCUMENT_DEFAULTS_ATTRIBUTE, documentDefaultsOf, storedDocumentDefaults } from './document-defaults-rules'\n\n@ObjectType({ description: \"The company's own words every new quotation and invoice starts with (ADR-0170).\" })\nexport class DocumentDefaults {\n @Field({ description: \"Our name as a supplier - 「우리 회사」's (biz-base, ADR-0176).\" }) supplierName: string\n @Field({ description: \"Our business registration number - 「우리 회사」's.\" }) supplierRegistrationNumber: string\n @Field({ description: \"Our address - 「우리 회사」's.\" }) supplierAddress: string\n @Field({ description: 'The account to pay into, as printed.' }) bankDetails: string\n @Field(type => [String], { description: 'The payment terms the company offers - the first is a new document\\'s, the others a click away.' }) paymentTerms: string[]\n @Field({ description: 'A note every new document carries.' }) notes: string\n @Field(type => Int, { description: 'Days a quotation holds after its issue day.' }) validDays: number\n @Field(type => Int, { description: 'Days an invoice is due after its issue day.' }) paymentDays: number\n}\n\n@InputType({ description: \"The company's own words for new documents, whole - what is left out is emptied. Our name, number and address are kept as 「우리 회사」.\" })\nexport class DocumentDefaultsInput {\n @Field({ nullable: true, description: 'The account to pay into, as printed.' }) bankDetails?: string\n @Field(type => [String], { nullable: true, description: 'The payment terms the company offers, in order.' }) paymentTerms?: string[]\n @Field({ nullable: true, description: 'A note every new document carries.' }) notes?: string\n @Field(type => Float, { nullable: true, description: 'Days a quotation holds after its issue day - a whole number from 1 to 365; another is refused naming it.' }) validDays?: number\n @Field(type => Float, { nullable: true, description: 'Days an invoice is due after its issue day - a whole number from 0 to 365; another is refused naming it.' }) paymentDays?: number\n}\n\n/** Our side of a sales document - 「우리 회사」 as biz-base keeps it (ADR-0176 decision 4). */\nasync function ourSide(domain: any, manager: any) {\n const company = await printedCompanyOf(domain, { manager })\n return { supplierName: company.name, supplierRegistrationNumber: company.brn, supplierAddress: company.address }\n}\n\n/** The attribute as this domain sees it - its own, else its closest ancestor's. */\nasync function storedValue(domain: any, manager: any): Promise<string | undefined> {\n const ids = await getDomainIdsWithAncestors(domain)\n const all: EnvVar[] = await manager.getRepository(EnvVar).find({ where: { domain: { id: In(ids) }, name: DOCUMENT_DEFAULTS_ATTRIBUTE, active: true } })\n return dedupByClosestDomain(all, ids, one => one.name)[0]?.value\n}\n\n/**\n * The company's words for new quotations and invoices (ADR-0170). Read by whoever writes either document\n * (`invoice-published`); changed by whoever keeps the invoices (`invoice:mutation`), on the Settings screen.\n */\n@Resolver()\n@UseMiddleware(SpeakRefusals)\nexport class DocumentDefaultsResolver {\n @Directive('@privilege(category: \"invoice-published\", privilege: \"query\", domainOwnerGranted: true)')\n @Query(returns => DocumentDefaults, { description: \"The company's own words every new quotation and invoice starts with.\" })\n async documentDefaults(@Ctx() context: ResolverContext): Promise<DocumentDefaults> {\n const { domain, tx } = context.state as any\n const manager = tx ?? getDataSource().manager\n return { ...(await ourSide(domain, manager)), ...storedDocumentDefaults(await storedValue(domain, manager)) }\n }\n\n @Directive('@transaction')\n @Directive('@privilege(category: \"invoice\", privilege: \"mutation\", domainOwnerGranted: true)')\n @Mutation(returns => DocumentDefaults, { description: \"To set the company's own words for new quotations and invoices, whole.\" })\n async updateDocumentDefaults(@Arg('defaults', type => DocumentDefaultsInput) defaults: DocumentDefaultsInput, @Ctx() context: ResolverContext): Promise<DocumentDefaults> {\n const { domain, tx } = context.state as any\n checkDocumentDefaults(defaults)\n const kept = documentDefaultsOf(defaults)\n const repository = tx.getRepository(EnvVar)\n const own = await repository.findOne({ where: { domain: { id: domain.id }, name: DOCUMENT_DEFAULTS_ATTRIBUTE } })\n const value = JSON.stringify(kept)\n if (own) await repository.save({ ...own, value, active: true })\n else await repository.save({ domain, name: DOCUMENT_DEFAULTS_ATTRIBUTE, value, active: true, description: 'Quotation and invoice defaults (ADR-0170) - set on the Settings screen' })\n return { ...(await ourSide(domain, tx)), ...kept }\n }\n}\n"]}
|
|
@@ -18,5 +18,11 @@ export declare const DOCUMENT_DEFAULTS_ATTRIBUTE = "Invoice::documentDefaults";
|
|
|
18
18
|
* from 1 to 365 and the days an invoice is due one from 0 to 365.
|
|
19
19
|
*/
|
|
20
20
|
export declare function documentDefaultsOf(input: any): DocumentDefaultsValue;
|
|
21
|
+
/**
|
|
22
|
+
* What a save of the defaults is refused for, before `documentDefaultsOf` keeps it: days out of their range - kept as
|
|
23
|
+
* thirty, they read as a change the person never made - and more than ten distinct payment terms, past which the
|
|
24
|
+
* rest were dropped. A blank day is left to the default. Pure.
|
|
25
|
+
*/
|
|
26
|
+
export declare function checkDocumentDefaults(input: any): void;
|
|
21
27
|
/** The kept attribute read back - nothing kept, or something unreadable, is the empty defaults. */
|
|
22
28
|
export declare function storedDocumentDefaults(value: string | null | undefined): DocumentDefaultsValue;
|
|
@@ -1,14 +1,10 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
-
/*
|
|
3
|
-
* The company's own words for every quotation and invoice it writes (ADR-0170): the account to pay into, the payment
|
|
4
|
-
* terms it offers (several - one is picked per document), a standing note, and how long an offer holds. Pure - the
|
|
5
|
-
* resolver keeps them as one domain attribute. Our name, number and address are 「우리 회사」's, kept by biz-base
|
|
6
|
-
* (ADR-0176 decision 4) and read beside these.
|
|
7
|
-
*/
|
|
8
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
9
3
|
exports.DOCUMENT_DEFAULTS_ATTRIBUTE = exports.MAX_PAYMENT_TERMS = exports.DEFAULT_PAYMENT_DAYS = exports.DEFAULT_VALID_DAYS = void 0;
|
|
10
4
|
exports.documentDefaultsOf = documentDefaultsOf;
|
|
5
|
+
exports.checkDocumentDefaults = checkDocumentDefaults;
|
|
11
6
|
exports.storedDocumentDefaults = storedDocumentDefaults;
|
|
7
|
+
const shell_1 = require("@things-factory/shell");
|
|
12
8
|
exports.DEFAULT_VALID_DAYS = 30;
|
|
13
9
|
exports.DEFAULT_PAYMENT_DAYS = 30;
|
|
14
10
|
exports.MAX_PAYMENT_TERMS = 10;
|
|
@@ -36,6 +32,22 @@ function documentDefaultsOf(input) {
|
|
|
36
32
|
paymentDays: Number.isFinite(payment) && payment >= 0 && payment <= 365 ? payment : exports.DEFAULT_PAYMENT_DAYS
|
|
37
33
|
};
|
|
38
34
|
}
|
|
35
|
+
/**
|
|
36
|
+
* What a save of the defaults is refused for, before `documentDefaultsOf` keeps it: days out of their range - kept as
|
|
37
|
+
* thirty, they read as a change the person never made - and more than ten distinct payment terms, past which the
|
|
38
|
+
* rest were dropped. A blank day is left to the default. Pure.
|
|
39
|
+
*/
|
|
40
|
+
function checkDocumentDefaults(input) {
|
|
41
|
+
const given = (value) => value !== null && value !== undefined && value !== '';
|
|
42
|
+
const days = (value, min) => Number.isInteger(Number(value)) && Number(value) >= min && Number(value) <= 365;
|
|
43
|
+
if (given(input?.validDays) && !days(input.validDays, 1))
|
|
44
|
+
throw new shell_1.Refusal('invoice-defaults-valid-days', { value: String(input.validDays), min: 1, max: 365 }, 'validDays: a whole number from 1 to 365');
|
|
45
|
+
if (given(input?.paymentDays) && !days(input.paymentDays, 0))
|
|
46
|
+
throw new shell_1.Refusal('invoice-defaults-payment-days', { value: String(input.paymentDays), min: 0, max: 365 }, 'paymentDays: a whole number from 0 to 365');
|
|
47
|
+
const terms = new Set((Array.isArray(input?.paymentTerms) ? input.paymentTerms : []).map(text).filter(Boolean));
|
|
48
|
+
if (terms.size > exports.MAX_PAYMENT_TERMS)
|
|
49
|
+
throw new shell_1.Refusal('invoice-defaults-payment-terms-too-many', { count: terms.size, max: exports.MAX_PAYMENT_TERMS }, `paymentTerms: at most ${exports.MAX_PAYMENT_TERMS}`);
|
|
50
|
+
}
|
|
39
51
|
/** The kept attribute read back - nothing kept, or something unreadable, is the empty defaults. */
|
|
40
52
|
function storedDocumentDefaults(value) {
|
|
41
53
|
try {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"document-defaults-rules.js","sourceRoot":"","sources":["../../../server/service/document-defaults/document-defaults-rules.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"document-defaults-rules.js","sourceRoot":"","sources":["../../../server/service/document-defaults/document-defaults-rules.ts"],"names":[],"mappings":"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","sourcesContent":["import { Refusal } from '@things-factory/shell'\n\n/*\n * The company's own words for every quotation and invoice it writes (ADR-0170): the account to pay into, the payment\n * terms it offers (several - one is picked per document), a standing note, and how long an offer holds. Pure - the\n * resolver keeps them as one domain attribute. Our name, number and address are 「우리 회사」's, kept by biz-base\n * (ADR-0176 decision 4) and read beside these.\n */\n\nexport interface DocumentDefaultsValue {\n bankDetails: string\n /** The payment terms the company offers, in the order they are offered - the first is a new document's. */\n paymentTerms: string[]\n notes: string\n /** Days a quotation holds after its issue day. */\n validDays: number\n /** Days an invoice is due after its issue day - none means due on the day. */\n paymentDays: number\n}\n\nexport const DEFAULT_VALID_DAYS = 30\nexport const DEFAULT_PAYMENT_DAYS = 30\nexport const MAX_PAYMENT_TERMS = 10\n\n/** The attribute they are kept under. */\nexport const DOCUMENT_DEFAULTS_ATTRIBUTE = 'Invoice::documentDefaults'\n\nconst text = (value: unknown) => (typeof value === 'string' ? value.trim() : '')\n\n/**\n * What is kept: words trimmed, terms without blanks or repeats and at most ten, the days an offer holds a whole number\n * from 1 to 365 and the days an invoice is due one from 0 to 365.\n */\nexport function documentDefaultsOf(input: any): DocumentDefaultsValue {\n const terms: string[] = []\n for (const one of Array.isArray(input?.paymentTerms) ? input.paymentTerms : []) {\n const term = text(one)\n if (term && !terms.includes(term) && terms.length < MAX_PAYMENT_TERMS) terms.push(term)\n }\n const days = Math.round(Number(input?.validDays))\n const payment = input?.paymentDays === null || input?.paymentDays === undefined || input?.paymentDays === '' ? NaN : Math.round(Number(input.paymentDays))\n return {\n bankDetails: text(input?.bankDetails),\n paymentTerms: terms,\n notes: text(input?.notes),\n validDays: Number.isFinite(days) && days >= 1 && days <= 365 ? days : DEFAULT_VALID_DAYS,\n paymentDays: Number.isFinite(payment) && payment >= 0 && payment <= 365 ? payment : DEFAULT_PAYMENT_DAYS\n }\n}\n\n/**\n * What a save of the defaults is refused for, before `documentDefaultsOf` keeps it: days out of their range - kept as\n * thirty, they read as a change the person never made - and more than ten distinct payment terms, past which the\n * rest were dropped. A blank day is left to the default. Pure.\n */\nexport function checkDocumentDefaults(input: any): void {\n const given = (value: unknown) => value !== null && value !== undefined && value !== ''\n const days = (value: unknown, min: number) => Number.isInteger(Number(value)) && Number(value) >= min && Number(value) <= 365\n if (given(input?.validDays) && !days(input.validDays, 1))\n throw new Refusal('invoice-defaults-valid-days', { value: String(input.validDays), min: 1, max: 365 }, 'validDays: a whole number from 1 to 365')\n if (given(input?.paymentDays) && !days(input.paymentDays, 0))\n throw new Refusal('invoice-defaults-payment-days', { value: String(input.paymentDays), min: 0, max: 365 }, 'paymentDays: a whole number from 0 to 365')\n const terms = new Set((Array.isArray(input?.paymentTerms) ? input.paymentTerms : []).map(text).filter(Boolean))\n if (terms.size > MAX_PAYMENT_TERMS)\n throw new Refusal('invoice-defaults-payment-terms-too-many', { count: terms.size, max: MAX_PAYMENT_TERMS }, `paymentTerms: at most ${MAX_PAYMENT_TERMS}`)\n}\n\n/** The kept attribute read back - nothing kept, or something unreadable, is the empty defaults. */\nexport function storedDocumentDefaults(value: string | null | undefined): DocumentDefaultsValue {\n try {\n return documentDefaultsOf(value ? JSON.parse(value) : {})\n } catch {\n return documentDefaultsOf({})\n }\n}\n"]}
|
|
@@ -33,6 +33,7 @@ function currencyDigits(currency) {
|
|
|
33
33
|
}
|
|
34
34
|
/** The refusal each problem of a line is - written out whole, so every code this package speaks is read where it is. */
|
|
35
35
|
const LINE_REFUSALS = {
|
|
36
|
+
'item-description-required': row => new shell_1.Refusal('invoice-item-description-required', { row }, `row ${row}: write the item`),
|
|
36
37
|
'item-invalid': () => new shell_1.Refusal('invoice-item-invalid', {}, 'Check description, quantity, price, discount, tax kind and rate'),
|
|
37
38
|
'discount-exceeds': () => new shell_1.Refusal('invoice-discount-exceeds', {}, 'Discount exceeds the line amount'),
|
|
38
39
|
'amount-too-large': () => new shell_1.Refusal('invoice-amount-too-large', {}, 'Amount is too large'),
|
|
@@ -56,9 +57,9 @@ function calculateDocument(lines, currency, received = false, discountAmount) {
|
|
|
56
57
|
/* Saved lines name every value: a blank discount is not taken as none here, as a screen takes it. */
|
|
57
58
|
const named = lines.map(line => ({ ...line, discount: line.discount ?? NaN }));
|
|
58
59
|
const amounts = (0, document_amounts_js_1.documentAmounts)(named, currency, received, discountAmount ?? 0);
|
|
59
|
-
const refused = amounts.lines.
|
|
60
|
-
if (refused)
|
|
61
|
-
throw LINE_REFUSALS[refused.problem]();
|
|
60
|
+
const refused = amounts.lines.findIndex(line => line.problem);
|
|
61
|
+
if (refused >= 0)
|
|
62
|
+
throw LINE_REFUSALS[amounts.lines[refused].problem](refused + 1);
|
|
62
63
|
if (amounts.discountProblem)
|
|
63
64
|
throw DISCOUNT_REFUSALS[amounts.discountProblem]();
|
|
64
65
|
if (amounts.tooLarge)
|
|
@@ -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 { currencyDigits as digitsOf } from '@things-factory/invoice/dist-shared/currency-digits.js'\nimport { documentAmounts, type DiscountProblem, type LineProblem, type TaxGroup } from '@things-factory/invoice/dist-shared/document-amounts.js'\nimport { Refusal } from '@things-factory/shell'\n\n/*\n * Invoice lifecycle rules and the refusals of its arithmetic, shared by the invoice and quotation services and their\n * tests (ADR-0120 decision 4). The arithmetic itself is `shared/document-amounts.ts`, which the screens read too.\n */\nexport type { TaxGroup }\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\n\nexport interface LineValues {\n description: string\n quantity: number\n /** The unit the quantity counts in, as printed - no arithmetic reads it. */\n unit?: string | null\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 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/** The refusal each problem of a line is - written out whole, so every code this package speaks is read where it is. */\nconst LINE_REFUSALS: Record<LineProblem, () => Refusal> = {\n 'item-invalid': () => new Refusal('invoice-item-invalid', {}, 'Check description, quantity, price, discount, tax kind and rate'),\n 'discount-exceeds': () => new Refusal('invoice-discount-exceeds', {}, 'Discount exceeds the line amount'),\n 'amount-too-large': () => new Refusal('invoice-amount-too-large', {}, 'Amount is too large'),\n 'received-tax-required': () => new Refusal('invoice-received-tax-required', {}, 'Enter the tax printed on each received line')\n}\n\n/** The refusal a discount of the whole document that cannot be taken is - written out whole, as the lines' are. */\nconst DISCOUNT_REFUSALS: Record<DiscountProblem, () => Refusal> = {\n 'document-discount-invalid': () => new Refusal('invoice-document-discount-invalid', {}, \"The discount is zero or more and at most the lines' supply\")\n}\n\n/**\n * The supply of each line, the tax of each (kind, rate) group and the document totals - reckoned by\n * `shared/document-amounts.ts`, the screens' own arithmetic; here a line that cannot be reckoned refuses the document,\n * and so does a discount of the whole document below zero or past the lines' supply. `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, discountAmount?: number | null) {\n currencyDigits(currency)\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 /* Saved lines name every value: a blank discount is not taken as none here, as a screen takes it. */\n const named = lines.map(line => ({ ...line, discount: line.discount ?? NaN }))\n const amounts = documentAmounts(named, currency, received, discountAmount ?? 0)\n const refused = amounts.lines.find(line => line.problem)\n if (refused) throw LINE_REFUSALS[refused.problem!]()\n if (amounts.discountProblem) throw DISCOUNT_REFUSALS[amounts.discountProblem]()\n if (amounts.tooLarge) throw new Refusal('invoice-amount-too-large', {}, 'Amount is too large')\n\n return {\n items: lines.map((line, position) => ({\n description: line.description.trim(),\n quantity: line.quantity,\n unit: line.unit?.trim() || null,\n unitPrice: line.unitPrice,\n discount: line.discount,\n taxKind: line.taxKind as TaxKind,\n taxRate: line.taxRate,\n supplyAmount: amounts.lines[position].supplyAmount!,\n receivedTaxAmount: amounts.lines[position].receivedTaxAmount ?? null,\n position\n })),\n taxGroups: amounts.taxGroups,\n lineSupplyAmount: amounts.lineSupplyAmount,\n discountAmount: amounts.discountAmount,\n supplyAmount: amounts.supplyAmount,\n taxAmount: amounts.taxAmount,\n totalAmount: amounts.totalAmount\n }\n}\n\ntype SavedDocument = { currency: string; taxBasis?: string; discountAmount?: number | null; items?: readonly LineValues[] }\n\n/** The groups of a saved document, read back from its lines and its discount. */\nexport function taxGroupsOf(document: SavedDocument): TaxGroup[] {\n if (!document.items?.length) return []\n return calculateDocument(document.items, document.currency, document.taxBasis === 'RECEIVED', document.discountAmount).taxGroups\n}\n\n/** The lines' supply of a saved document before its discount, read back from its lines. */\nexport function lineSupplyOf(document: SavedDocument): number {\n if (!document.items?.length) return 0\n return calculateDocument(document.items, document.currency, document.taxBasis === 'RECEIVED', document.discountAmount).lineSupplyAmount\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"]}
|
|
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 { documentAmounts, type DiscountProblem, type LineProblem, type TaxGroup } from '@things-factory/invoice/dist-shared/document-amounts.js'\nimport { Refusal } from '@things-factory/shell'\n\n/*\n * Invoice lifecycle rules and the refusals of its arithmetic, shared by the invoice and quotation services and their\n * tests (ADR-0120 decision 4). The arithmetic itself is `shared/document-amounts.ts`, which the screens read too.\n */\nexport type { TaxGroup }\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\n\nexport interface LineValues {\n description: string\n quantity: number\n /** The unit the quantity counts in, as printed - no arithmetic reads it. */\n unit?: string | null\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 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/** The refusal each problem of a line is - written out whole, so every code this package speaks is read where it is. */\nconst LINE_REFUSALS: Record<LineProblem, (row: number) => Refusal> = {\n 'item-description-required': row => new Refusal('invoice-item-description-required', { row }, `row ${row}: write the item`),\n 'item-invalid': () => new Refusal('invoice-item-invalid', {}, 'Check description, quantity, price, discount, tax kind and rate'),\n 'discount-exceeds': () => new Refusal('invoice-discount-exceeds', {}, 'Discount exceeds the line amount'),\n 'amount-too-large': () => new Refusal('invoice-amount-too-large', {}, 'Amount is too large'),\n 'received-tax-required': () => new Refusal('invoice-received-tax-required', {}, 'Enter the tax printed on each received line')\n}\n\n/** The refusal a discount of the whole document that cannot be taken is - written out whole, as the lines' are. */\nconst DISCOUNT_REFUSALS: Record<DiscountProblem, () => Refusal> = {\n 'document-discount-invalid': () => new Refusal('invoice-document-discount-invalid', {}, \"The discount is zero or more and at most the lines' supply\")\n}\n\n/**\n * The supply of each line, the tax of each (kind, rate) group and the document totals - reckoned by\n * `shared/document-amounts.ts`, the screens' own arithmetic; here a line that cannot be reckoned refuses the document,\n * and so does a discount of the whole document below zero or past the lines' supply. `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, discountAmount?: number | null) {\n currencyDigits(currency)\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 /* Saved lines name every value: a blank discount is not taken as none here, as a screen takes it. */\n const named = lines.map(line => ({ ...line, discount: line.discount ?? NaN }))\n const amounts = documentAmounts(named, currency, received, discountAmount ?? 0)\n const refused = amounts.lines.findIndex(line => line.problem)\n if (refused >= 0) throw LINE_REFUSALS[amounts.lines[refused].problem!](refused + 1)\n if (amounts.discountProblem) throw DISCOUNT_REFUSALS[amounts.discountProblem]()\n if (amounts.tooLarge) throw new Refusal('invoice-amount-too-large', {}, 'Amount is too large')\n\n return {\n items: lines.map((line, position) => ({\n description: line.description.trim(),\n quantity: line.quantity,\n unit: line.unit?.trim() || null,\n unitPrice: line.unitPrice,\n discount: line.discount,\n taxKind: line.taxKind as TaxKind,\n taxRate: line.taxRate,\n supplyAmount: amounts.lines[position].supplyAmount!,\n receivedTaxAmount: amounts.lines[position].receivedTaxAmount ?? null,\n position\n })),\n taxGroups: amounts.taxGroups,\n lineSupplyAmount: amounts.lineSupplyAmount,\n discountAmount: amounts.discountAmount,\n supplyAmount: amounts.supplyAmount,\n taxAmount: amounts.taxAmount,\n totalAmount: amounts.totalAmount\n }\n}\n\ntype SavedDocument = { currency: string; taxBasis?: string; discountAmount?: number | null; items?: readonly LineValues[] }\n\n/** The groups of a saved document, read back from its lines and its discount. */\nexport function taxGroupsOf(document: SavedDocument): TaxGroup[] {\n if (!document.items?.length) return []\n return calculateDocument(document.items, document.currency, document.taxBasis === 'RECEIVED', document.discountAmount).taxGroups\n}\n\n/** The lines' supply of a saved document before its discount, read back from its lines. */\nexport function lineSupplyOf(document: SavedDocument): number {\n if (!document.items?.length) return 0\n return calculateDocument(document.items, document.currency, document.taxBasis === 'RECEIVED', document.discountAmount).lineSupplyAmount\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"]}
|