@things-factory/invoice 10.1.141 → 10.1.145

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.
@@ -2,5 +2,5 @@ import { Invoice } from './invoice';
2
2
  import { InvoiceMutation } from './invoice-mutation';
3
3
  import { InvoiceQuery } from './invoice-query';
4
4
  export declare const entities: (typeof Invoice)[];
5
- export declare const resolvers: (typeof InvoiceQuery | typeof InvoiceMutation)[];
5
+ export declare const resolvers: (typeof InvoiceMutation | typeof InvoiceQuery)[];
6
6
  export declare const subscribers: any[];
@@ -60,12 +60,12 @@ export declare function normalizedHeader(input: Record<string, any>): {
60
60
  direction: any;
61
61
  language: any;
62
62
  supplierName: any;
63
+ buyerName: any;
64
+ issueDate: any;
63
65
  supplierAddress: any;
64
66
  supplierRegistrationNumber: any;
65
- buyerName: any;
66
67
  buyerAddress: any;
67
68
  buyerRegistrationNumber: any;
68
- issueDate: any;
69
69
  purchaseOrder: any;
70
70
  paymentTerms: any;
71
71
  bankDetails: any;
@@ -5,7 +5,7 @@ import { NewInvoice } from './invoice-type';
5
5
  export type InvoiceTransitionGuard = (tx: EntityManager, domain: Domain, invoice: Invoice, action: 'ISSUED' | 'VOID') => Promise<void>;
6
6
  /** An application refuses a transition its own links forbid — Office keeps a linked invoice from being voided. */
7
7
  export declare function registerInvoiceTransitionGuard(guard: InvoiceTransitionGuard): void;
8
- /** Serialize document writes on the domain row; SQLite serializes writes by itself. */
8
+ /** Serialize document writes on the domain row, where the database can hold it (shell `writeLockSupported`). */
9
9
  export declare function lockDocumentDomain(tx: EntityManager, domain: Domain): Promise<void>;
10
10
  export declare function invoiceInDomain(tx: EntityManager, domain: Domain, id: string): Promise<Invoice>;
11
11
  export declare function saveInvoice(tx: EntityManager, domain: Domain, user: any, input: NewInvoice, id?: string): Promise<Invoice>;
@@ -16,12 +16,10 @@ const transitionGuards = new Set();
16
16
  function registerInvoiceTransitionGuard(guard) {
17
17
  transitionGuards.add(guard);
18
18
  }
19
- /** Serialize document writes on the domain row; SQLite serializes writes by itself. */
19
+ /** Serialize document writes on the domain row, where the database can hold it (shell `writeLockSupported`). */
20
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()))
21
+ const found = await tx.getRepository(shell_1.Domain).findOne({ where: { id: domain.id }, ...((0, shell_1.writeLockSupported)(tx) ? { lock: { mode: 'pessimistic_write' } } : {}) });
22
+ if (!found)
25
23
  throw new shell_1.Refusal('invoice-domain-missing', {}, 'Domain not found');
26
24
  }
27
25
  async function invoiceInDomain(tx, domain, id) {
@@ -1 +1 @@
1
- {"version":3,"file":"invoice-service.js","sourceRoot":"","sources":["../../../server/service/invoice/invoice-service.ts"],"names":[],"mappings":";;AAaA,wEAEC;AAGD,gDAIC;AAED,0CAKC;AAED,kCAuCC;AAED,8CAwBC;AAOD,gCA4BC;AAnID,iDAAuD;AAGvD,kEAA6D;AAC7D,+DAA0D;AAC1D,uCAAmE;AACnE,mDAAwI;AAIxI,MAAM,gBAAgB,GAAG,IAAI,GAAG,EAA0B,CAAA;AAE1D,kHAAkH;AAClH,SAAgB,8BAA8B,CAAC,KAA6B;IAC1E,gBAAgB,CAAC,GAAG,CAAC,KAAK,CAAC,CAAA;AAC7B,CAAC;AAED,uFAAuF;AAChF,KAAK,UAAU,kBAAkB,CAAC,EAAiB,EAAE,MAAc;IACxE,MAAM,KAAK,GAAG,EAAE,CAAC,aAAa,CAAC,cAAM,CAAC,CAAC,kBAAkB,CAAC,QAAQ,CAAC,CAAC,KAAK,CAAC,iBAAiB,EAAE,EAAE,EAAE,EAAE,MAAM,CAAC,EAAE,EAAE,CAAC,CAAA;IAC/G,IAAI,CAAC,CAAC,QAAQ,EAAE,gBAAgB,EAAE,OAAO,CAAC,CAAC,QAAQ,CAAC,EAAE,CAAC,UAAU,CAAC,OAAO,CAAC,IAAI,CAAC;QAAE,KAAK,CAAC,OAAO,CAAC,mBAAmB,CAAC,CAAA;IACnH,IAAI,CAAC,CAAC,MAAM,KAAK,CAAC,MAAM,EAAE,CAAC;QAAE,MAAM,IAAI,eAAO,CAAC,wBAAwB,EAAE,EAAE,EAAE,kBAAkB,CAAC,CAAA;AAClG,CAAC;AAEM,KAAK,UAAU,eAAe,CAAC,EAAiB,EAAE,MAAc,EAAE,EAAU;IACjF,MAAM,OAAO,GAAG,MAAM,EAAE,CAAC,aAAa,CAAC,iBAAO,CAAC,CAAC,OAAO,CAAC,EAAE,KAAK,EAAE,EAAE,EAAE,EAAE,MAAM,EAAE,EAAE,EAAE,EAAE,MAAM,CAAC,EAAE,EAAE,EAAE,EAAE,SAAS,EAAE,CAAC,OAAO,EAAE,SAAS,CAAC,EAAE,CAAC,CAAA;IACtI,IAAI,CAAC,OAAO;QAAE,MAAM,IAAI,eAAO,CAAC,mBAAmB,EAAE,EAAE,EAAE,mBAAmB,CAAC,CAAA;IAC7E,OAAO,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,QAAQ,GAAG,CAAC,CAAC,QAAQ,CAAC,CAAA;IACrD,OAAO,OAAO,CAAA;AAChB,CAAC;AAEM,KAAK,UAAU,WAAW,CAAC,EAAiB,EAAE,MAAc,EAAE,IAAS,EAAE,KAAiB,EAAE,EAAW;IAC5G,MAAM,kBAAkB,CAAC,EAAE,EAAE,MAAM,CAAC,CAAA;IACpC,MAAM,GAAG,GAAG,EAAE,CAAC,CAAC,CAAC,MAAM,eAAe,CAAC,EAAE,EAAE,MAAM,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC,SAAS,CAAA;IAClE,IAAI,GAAG;QAAE,IAAA,4BAAY,EAAC,GAAG,CAAC,MAAM,CAAC,CAAA;IAEjC,MAAM,MAAM,GAAG,IAAA,gCAAgB,EAAC,KAAK,CAAC,CAAA;IACtC,IAAI,KAAK,CAAC,OAAO,IAAI,CAAC,IAAA,wBAAQ,EAAC,KAAK,CAAC,OAAO,CAAC;QAAE,MAAM,IAAI,eAAO,CAAC,sBAAsB,EAAE,EAAE,EAAE,6BAA6B,CAAC,CAAA;IAC3H,IAAI,KAAK,CAAC,SAAS,IAAI,KAAK,CAAC,OAAO,IAAI,KAAK,CAAC,OAAO,GAAG,KAAK,CAAC,SAAS;QAAE,MAAM,IAAI,eAAO,CAAC,0BAA0B,EAAE,EAAE,EAAE,8BAA8B,CAAC,CAAA;IAC1J,MAAM,QAAQ,GAAG,KAAK,CAAC,QAAQ,KAAK,yBAAe,CAAC,QAAQ,CAAA;IAC5D,IAAI,QAAQ,IAAI,MAAM,CAAC,SAAS,KAAK,UAAU;QAAE,MAAM,IAAI,eAAO,CAAC,gCAAgC,EAAE,EAAE,EAAE,oDAAoD,CAAC,CAAA;IAE9J,MAAM,UAAU,GAAG,EAAE,CAAC,aAAa,CAAC,iBAAO,CAAC,CAAA;IAC5C,MAAM,SAAS,GAAG,MAAM,UAAU,CAAC,OAAO,CAAC,EAAE,KAAK,EAAE,EAAE,MAAM,EAAE,EAAE,EAAE,EAAE,MAAM,CAAC,EAAE,EAAE,EAAE,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,EAAE,CAAC,CAAA;IAC3G,IAAI,SAAS,IAAI,SAAS,CAAC,EAAE,KAAK,EAAE;QAAE,MAAM,IAAI,eAAO,CAAC,0BAA0B,EAAE,EAAE,EAAE,+BAA+B,CAAC,CAAA;IAExH,MAAM,EAAE,KAAK,EAAE,YAAY,EAAE,SAAS,EAAE,WAAW,EAAE,GAAG,IAAA,iCAAiB,EAAC,KAAK,CAAC,KAAK,EAAE,MAAM,CAAC,QAAQ,EAAE,QAAQ,CAAC,CAAA;IACjH,MAAM,OAAO,GAAG,MAAM,UAAU,CAAC,IAAI,CACnC,UAAU,CAAC,MAAM,CAAC;QAChB,GAAG,GAAG;QACN,GAAG,MAAM;QACT,OAAO,EAAE,KAAK,CAAC,OAAO,IAAI,IAAI;QAC9B,QAAQ,EAAE,QAAQ,CAAC,CAAC,CAAC,yBAAe,CAAC,QAAQ,CAAC,CAAC,CAAC,yBAAe,CAAC,UAAU;QAC1E,YAAY;QACZ,SAAS;QACT,WAAW;QACX,MAAM;QACN,OAAO,EAAE,GAAG,EAAE,OAAO,IAAI,IAAI;QAC7B,OAAO,EAAE,IAAI;QACb,KAAK,EAAE,SAAS;KACjB,CAAC,CACH,CAAA;IAED,MAAM,KAAK,GAAG,EAAE,CAAC,aAAa,CAAC,0BAAW,CAAC,CAAA;IAC3C,IAAI,GAAG;QAAE,MAAM,KAAK,CAAC,UAAU,CAAC,EAAE,OAAO,EAAE,EAAE,EAAE,EAAE,OAAO,CAAC,EAAE,EAAE,EAAE,CAAC,CAAA;IAChE,MAAM,KAAK,CAAC,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,IAAI,CAAC,EAAE,CAAC,KAAK,CAAC,MAAM,CAAC,EAAE,GAAG,IAAI,EAAE,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE,IAAI,EAAE,OAAO,EAAE,IAAI,EAAE,CAAC,CAAC,CAAC,CAAA;IAE7G,MAAM,KAAK,GAAG,MAAM,eAAe,CAAC,EAAE,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,CAAC,CAAA;IAC3D,MAAM,kBAAkB,CAAC,EAAE,EAAE,MAAM,EAAE,IAAI,EAAE,KAAK,EAAE,GAAG,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,SAAS,CAAC,CAAA;IAC9E,OAAO,KAAK,CAAA;AACd,CAAC;AAEM,KAAK,UAAU,iBAAiB,CAAC,EAAiB,EAAE,MAAc,EAAE,IAAS,EAAE,EAAU,EAAE,MAAyB,EAAE,MAAe;IAC1I,MAAM,kBAAkB,CAAC,EAAE,EAAE,MAAM,CAAC,CAAA;IACpC,MAAM,OAAO,GAAG,MAAM,eAAe,CAAC,EAAE,EAAE,MAAM,EAAE,EAAE,CAAC,CAAA;IACrD,KAAK,MAAM,KAAK,IAAI,gBAAgB;QAAE,MAAM,KAAK,CAAC,EAAE,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,CAAC,CAAA;IAE9E,IAAI,MAAM,KAAK,QAAQ,EAAE,CAAC;QACxB,IAAA,4BAAY,EAAC,OAAO,CAAC,MAAM,CAAC,CAAA;QAC5B,IAAI,CAAC,OAAO,CAAC,SAAS,IAAI,CAAC,OAAO,CAAC,OAAO,IAAI,CAAC,OAAO,CAAC,KAAK,CAAC,MAAM,EAAE,CAAC;YACpE,MAAM,IAAI,eAAO,CAAC,wBAAwB,EAAE,EAAE,EAAE,6CAA6C,CAAC,CAAA;QAChG,CAAC;QACD,oGAAoG;QACpG,OAAO,CAAC,WAAW,GAAG,4BAAY,CAAA;QAClC,OAAO,CAAC,cAAc,GAAG,IAAA,8BAAc,EAAC,OAAO,CAAC,QAAQ,CAAC,CAAA;QACzD,OAAO,CAAC,MAAM,GAAG,uBAAa,CAAC,MAAM,CAAA;IACvC,CAAC;SAAM,CAAC;QACN,IAAI,OAAO,CAAC,MAAM,KAAK,uBAAa,CAAC,IAAI,IAAI,CAAC,MAAM,EAAE,IAAI,EAAE;YAAE,MAAM,IAAI,eAAO,CAAC,sBAAsB,EAAE,EAAE,EAAE,sDAAsD,CAAC,CAAA;QACnK,OAAO,CAAC,UAAU,GAAG,MAAM,CAAC,IAAI,EAAE,CAAA;QAClC,OAAO,CAAC,MAAM,GAAG,uBAAa,CAAC,IAAI,CAAA;IACrC,CAAC;IACD,OAAO,CAAC,OAAO,GAAG,IAAI,CAAA;IACtB,MAAM,EAAE,KAAK,EAAE,GAAG,IAAI,EAAE,GAAG,OAAO,CAAA;IAClC,MAAM,EAAE,CAAC,aAAa,CAAC,iBAAO,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAA;IAC1C,MAAM,kBAAkB,CAAC,EAAE,EAAE,MAAM,EAAE,IAAI,EAAE,OAAO,EAAE,MAAM,EAAE,MAAM,CAAC,CAAA;IACnE,OAAO,eAAe,CAAC,EAAE,EAAE,MAAM,EAAE,EAAE,CAAC,CAAA;AACxC,CAAC;AAED,SAAS,kBAAkB,CAAC,EAAiB,EAAE,MAAc,EAAE,IAAS,EAAE,OAAgB,EAAE,MAAc,EAAE,MAAe;IACzH,OAAO,EAAE,CAAC,aAAa,CAAC,4BAAY,CAAC,CAAC,IAAI,CAAC,EAAE,MAAM,EAAE,OAAO,EAAE,KAAK,EAAE,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,IAAI,EAAE,OAAO,EAAE,IAAI,EAAE,QAAQ,EAAE,UAAU,CAAC,OAAO,CAAC,EAAE,CAAC,CAAA;AAC3J,CAAC;AAED,mGAAmG;AACnG,SAAgB,UAAU,CAAC,OAAgB;IACzC,MAAM,IAAI,GAAG;QACX,IAAI;QACJ,GAAG,2BAAW;QACd,QAAQ;QACR,SAAS;QACT,UAAU;QACV,cAAc;QACd,WAAW;QACX,aAAa;QACb,aAAa;QACb,gBAAgB;QAChB,YAAY;KACb,CAAA;IACD,OAAO;QACL,GAAG,MAAM,CAAC,WAAW,CAAC,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC,GAAG,EAAE,OAAO,CAAC,GAAG,CAAC,IAAI,IAAI,CAAC,CAAC,CAAC;QACnE,KAAK,EAAE,OAAO,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,EAAE,QAAQ,EAAE,WAAW,EAAE,QAAQ,EAAE,SAAS,EAAE,QAAQ,EAAE,OAAO,EAAE,OAAO,EAAE,YAAY,EAAE,iBAAiB,EAAE,EAAE,EAAE,CAAC,CAAC;YACzI,QAAQ;YACR,WAAW;YACX,QAAQ;YACR,SAAS;YACT,QAAQ;YACR,OAAO;YACP,OAAO;YACP,YAAY;YACZ,iBAAiB,EAAE,iBAAiB,IAAI,IAAI;SAC7C,CAAC,CAAC;KACJ,CAAA;AACH,CAAC","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"]}
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, writeLockSupported } 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, where the database can hold it (shell `writeLockSupported`). */\nexport async function lockDocumentDomain(tx: EntityManager, domain: Domain) {\n const found = await tx.getRepository(Domain).findOne({ where: { id: domain.id }, ...(writeLockSupported(tx) ? { lock: { mode: 'pessimistic_write' } } : {}) })\n if (!found) 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"]}