@endora-commerce/mod-invoices 0.0.0-stage → 0.100.0
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/LICENSE +21 -0
- package/README.md +59 -2
- package/dist/admin/components/InvoiceSectionTabs.d.ts +22 -0
- package/dist/admin/components/InvoiceSectionTabs.d.ts.map +1 -0
- package/dist/admin/components/InvoiceSectionTabs.js +34 -0
- package/dist/admin/components/InvoiceSectionTabs.js.map +1 -0
- package/dist/admin/index.d.ts +34 -0
- package/dist/admin/index.d.ts.map +1 -0
- package/dist/admin/index.js +69 -0
- package/dist/admin/index.js.map +1 -0
- package/dist/admin/pages/InvoiceDetail.d.ts +10 -0
- package/dist/admin/pages/InvoiceDetail.d.ts.map +1 -0
- package/dist/admin/pages/InvoiceDetail.js +111 -0
- package/dist/admin/pages/InvoiceDetail.js.map +1 -0
- package/dist/admin/pages/InvoiceTemplateEditor.d.ts +11 -0
- package/dist/admin/pages/InvoiceTemplateEditor.d.ts.map +1 -0
- package/dist/admin/pages/InvoiceTemplateEditor.js +188 -0
- package/dist/admin/pages/InvoiceTemplateEditor.js.map +1 -0
- package/dist/admin/pages/InvoiceTemplatesPage.d.ts +10 -0
- package/dist/admin/pages/InvoiceTemplatesPage.d.ts.map +1 -0
- package/dist/admin/pages/InvoiceTemplatesPage.js +60 -0
- package/dist/admin/pages/InvoiceTemplatesPage.js.map +1 -0
- package/dist/admin/pages/InvoicesList.d.ts +9 -0
- package/dist/admin/pages/InvoicesList.d.ts.map +1 -0
- package/dist/admin/pages/InvoicesList.js +116 -0
- package/dist/admin/pages/InvoicesList.js.map +1 -0
- package/dist/admin/templates/described-blocks.d.ts +67 -0
- package/dist/admin/templates/described-blocks.d.ts.map +1 -0
- package/dist/admin/templates/described-blocks.js +83 -0
- package/dist/admin/templates/described-blocks.js.map +1 -0
- package/dist/admin/templates/invoice-builder-plugin.d.ts +4 -0
- package/dist/admin/templates/invoice-builder-plugin.d.ts.map +1 -0
- package/dist/admin/templates/invoice-builder-plugin.js +12 -0
- package/dist/admin/templates/invoice-builder-plugin.js.map +1 -0
- package/dist/admin/templates/invoice-puck-config.d.ts +37 -0
- package/dist/admin/templates/invoice-puck-config.d.ts.map +1 -0
- package/dist/admin/templates/invoice-puck-config.js +733 -0
- package/dist/admin/templates/invoice-puck-config.js.map +1 -0
- package/dist/backend/email-templates/invoice-issued.default.d.ts +10 -0
- package/dist/backend/email-templates/invoice-issued.default.d.ts.map +1 -0
- package/dist/backend/email-templates/invoice-issued.default.js +45 -0
- package/dist/backend/email-templates/invoice-issued.default.js.map +1 -0
- package/dist/backend/entities/invoice-external-attachment.entity.d.ts +20 -0
- package/dist/backend/entities/invoice-external-attachment.entity.d.ts.map +1 -0
- package/dist/backend/entities/invoice-external-attachment.entity.js +77 -0
- package/dist/backend/entities/invoice-external-attachment.entity.js.map +1 -0
- package/dist/backend/entities/invoice-line.entity.d.ts +20 -0
- package/dist/backend/entities/invoice-line.entity.d.ts.map +1 -0
- package/dist/backend/entities/invoice-line.entity.js +82 -0
- package/dist/backend/entities/invoice-line.entity.js.map +1 -0
- package/dist/backend/entities/invoice-number-counter.entity.d.ts +18 -0
- package/dist/backend/entities/invoice-number-counter.entity.d.ts.map +1 -0
- package/dist/backend/entities/invoice-number-counter.entity.js +63 -0
- package/dist/backend/entities/invoice-number-counter.entity.js.map +1 -0
- package/dist/backend/entities/invoice-template.entity.d.ts +22 -0
- package/dist/backend/entities/invoice-template.entity.d.ts.map +1 -0
- package/dist/backend/entities/invoice-template.entity.js +84 -0
- package/dist/backend/entities/invoice-template.entity.js.map +1 -0
- package/dist/backend/entities/invoice.entity.d.ts +64 -0
- package/dist/backend/entities/invoice.entity.d.ts.map +1 -0
- package/dist/backend/entities/invoice.entity.js +202 -0
- package/dist/backend/entities/invoice.entity.js.map +1 -0
- package/dist/backend/index.d.ts +111 -0
- package/dist/backend/index.d.ts.map +1 -0
- package/dist/backend/index.js +291 -0
- package/dist/backend/index.js.map +1 -0
- package/dist/backend/pdf-components/descriptor.d.ts +29 -0
- package/dist/backend/pdf-components/descriptor.d.ts.map +1 -0
- package/dist/backend/pdf-components/descriptor.js +227 -0
- package/dist/backend/pdf-components/descriptor.js.map +1 -0
- package/dist/backend/pdf-components/embed-logo-images.d.ts +35 -0
- package/dist/backend/pdf-components/embed-logo-images.d.ts.map +1 -0
- package/dist/backend/pdf-components/embed-logo-images.js +101 -0
- package/dist/backend/pdf-components/embed-logo-images.js.map +1 -0
- package/dist/backend/pdf-components/sample.d.ts +4 -0
- package/dist/backend/pdf-components/sample.d.ts.map +1 -0
- package/dist/backend/pdf-components/sample.js +75 -0
- package/dist/backend/pdf-components/sample.js.map +1 -0
- package/dist/backend/pdf-components/sections.d.ts +34 -0
- package/dist/backend/pdf-components/sections.d.ts.map +1 -0
- package/dist/backend/pdf-components/sections.js +445 -0
- package/dist/backend/pdf-components/sections.js.map +1 -0
- package/dist/backend/pdf-components/tree-mapper.d.ts +37 -0
- package/dist/backend/pdf-components/tree-mapper.d.ts.map +1 -0
- package/dist/backend/pdf-components/tree-mapper.js +76 -0
- package/dist/backend/pdf-components/tree-mapper.js.map +1 -0
- package/dist/backend/plugin.d.ts +124 -0
- package/dist/backend/plugin.d.ts.map +1 -0
- package/dist/backend/plugin.js +87 -0
- package/dist/backend/plugin.js.map +1 -0
- package/dist/backend/routes.admin.d.ts +41 -0
- package/dist/backend/routes.admin.d.ts.map +1 -0
- package/dist/backend/routes.admin.js +219 -0
- package/dist/backend/routes.admin.js.map +1 -0
- package/dist/backend/routes.customer.d.ts +33 -0
- package/dist/backend/routes.customer.d.ts.map +1 -0
- package/dist/backend/routes.customer.js +124 -0
- package/dist/backend/routes.customer.js.map +1 -0
- package/dist/backend/seeds/generic-invoice-template.d.ts +18 -0
- package/dist/backend/seeds/generic-invoice-template.d.ts.map +1 -0
- package/dist/backend/seeds/generic-invoice-template.js +174 -0
- package/dist/backend/seeds/generic-invoice-template.js.map +1 -0
- package/dist/backend/services/amount-to-words.d.ts +20 -0
- package/dist/backend/services/amount-to-words.d.ts.map +1 -0
- package/dist/backend/services/amount-to-words.js +232 -0
- package/dist/backend/services/amount-to-words.js.map +1 -0
- package/dist/backend/services/auto-issue-reactor.d.ts +53 -0
- package/dist/backend/services/auto-issue-reactor.d.ts.map +1 -0
- package/dist/backend/services/auto-issue-reactor.js +122 -0
- package/dist/backend/services/auto-issue-reactor.js.map +1 -0
- package/dist/backend/services/corrective-invoice.d.ts +38 -0
- package/dist/backend/services/corrective-invoice.d.ts.map +1 -0
- package/dist/backend/services/corrective-invoice.js +257 -0
- package/dist/backend/services/corrective-invoice.js.map +1 -0
- package/dist/backend/services/cross-module-context.d.ts +82 -0
- package/dist/backend/services/cross-module-context.d.ts.map +1 -0
- package/dist/backend/services/cross-module-context.js +93 -0
- package/dist/backend/services/cross-module-context.js.map +1 -0
- package/dist/backend/services/duplicate-number-refusal.d.ts +33 -0
- package/dist/backend/services/duplicate-number-refusal.d.ts.map +1 -0
- package/dist/backend/services/duplicate-number-refusal.js +92 -0
- package/dist/backend/services/duplicate-number-refusal.js.map +1 -0
- package/dist/backend/services/erp-sale-document-write-port.d.ts +37 -0
- package/dist/backend/services/erp-sale-document-write-port.d.ts.map +1 -0
- package/dist/backend/services/erp-sale-document-write-port.js +192 -0
- package/dist/backend/services/erp-sale-document-write-port.js.map +1 -0
- package/dist/backend/services/invoice-attachment-fetch-registry.d.ts +75 -0
- package/dist/backend/services/invoice-attachment-fetch-registry.d.ts.map +1 -0
- package/dist/backend/services/invoice-attachment-fetch-registry.js +81 -0
- package/dist/backend/services/invoice-attachment-fetch-registry.js.map +1 -0
- package/dist/backend/services/invoice-copy-host.service.d.ts +9 -0
- package/dist/backend/services/invoice-copy-host.service.d.ts.map +1 -0
- package/dist/backend/services/invoice-copy-host.service.js +52 -0
- package/dist/backend/services/invoice-copy-host.service.js.map +1 -0
- package/dist/backend/services/invoice-email-dispatch.d.ts +89 -0
- package/dist/backend/services/invoice-email-dispatch.d.ts.map +1 -0
- package/dist/backend/services/invoice-email-dispatch.js +138 -0
- package/dist/backend/services/invoice-email-dispatch.js.map +1 -0
- package/dist/backend/services/invoice-line-builder.d.ts +29 -0
- package/dist/backend/services/invoice-line-builder.d.ts.map +1 -0
- package/dist/backend/services/invoice-line-builder.js +46 -0
- package/dist/backend/services/invoice-line-builder.js.map +1 -0
- package/dist/backend/services/invoice-number-collisions.d.ts +17 -0
- package/dist/backend/services/invoice-number-collisions.d.ts.map +1 -0
- package/dist/backend/services/invoice-number-collisions.js +154 -0
- package/dist/backend/services/invoice-number-collisions.js.map +1 -0
- package/dist/backend/services/invoice-number-generator.d.ts +96 -0
- package/dist/backend/services/invoice-number-generator.d.ts.map +1 -0
- package/dist/backend/services/invoice-number-generator.js +157 -0
- package/dist/backend/services/invoice-number-generator.js.map +1 -0
- package/dist/backend/services/invoice-pdf-block-registry.d.ts +65 -0
- package/dist/backend/services/invoice-pdf-block-registry.d.ts.map +1 -0
- package/dist/backend/services/invoice-pdf-block-registry.js +94 -0
- package/dist/backend/services/invoice-pdf-block-registry.js.map +1 -0
- package/dist/backend/services/invoice-pdf-renderer.d.ts +57 -0
- package/dist/backend/services/invoice-pdf-renderer.d.ts.map +1 -0
- package/dist/backend/services/invoice-pdf-renderer.js +158 -0
- package/dist/backend/services/invoice-pdf-renderer.js.map +1 -0
- package/dist/backend/services/invoice-pdf.d.ts +24 -0
- package/dist/backend/services/invoice-pdf.d.ts.map +1 -0
- package/dist/backend/services/invoice-pdf.js +52 -0
- package/dist/backend/services/invoice-pdf.js.map +1 -0
- package/dist/backend/services/invoice-placement-apply-port.d.ts +29 -0
- package/dist/backend/services/invoice-placement-apply-port.d.ts.map +1 -0
- package/dist/backend/services/invoice-placement-apply-port.js +40 -0
- package/dist/backend/services/invoice-placement-apply-port.js.map +1 -0
- package/dist/backend/services/invoice-read-port.d.ts +44 -0
- package/dist/backend/services/invoice-read-port.d.ts.map +1 -0
- package/dist/backend/services/invoice-read-port.js +95 -0
- package/dist/backend/services/invoice-read-port.js.map +1 -0
- package/dist/backend/services/invoice-service.d.ts +94 -0
- package/dist/backend/services/invoice-service.d.ts.map +1 -0
- package/dist/backend/services/invoice-service.js +374 -0
- package/dist/backend/services/invoice-service.js.map +1 -0
- package/dist/backend/services/invoice-template-service.d.ts +43 -0
- package/dist/backend/services/invoice-template-service.d.ts.map +1 -0
- package/dist/backend/services/invoice-template-service.js +147 -0
- package/dist/backend/services/invoice-template-service.js.map +1 -0
- package/dist/backend/services/numbering-configuration.d.ts +76 -0
- package/dist/backend/services/numbering-configuration.d.ts.map +1 -0
- package/dist/backend/services/numbering-configuration.js +176 -0
- package/dist/backend/services/numbering-configuration.js.map +1 -0
- package/dist/backend/services/numbering-write-validator.d.ts +22 -0
- package/dist/backend/services/numbering-write-validator.d.ts.map +1 -0
- package/dist/backend/services/numbering-write-validator.js +82 -0
- package/dist/backend/services/numbering-write-validator.js.map +1 -0
- package/dist/backend/services/seller-settings.d.ts +24 -0
- package/dist/backend/services/seller-settings.d.ts.map +1 -0
- package/dist/backend/services/seller-settings.js +38 -0
- package/dist/backend/services/seller-settings.js.map +1 -0
- package/dist/backend/services/vendor-number-hold.d.ts +10 -0
- package/dist/backend/services/vendor-number-hold.d.ts.map +1 -0
- package/dist/backend/services/vendor-number-hold.js +14 -0
- package/dist/backend/services/vendor-number-hold.js.map +1 -0
- package/dist/manifest.d.ts +203 -0
- package/dist/manifest.d.ts.map +1 -0
- package/dist/manifest.js +390 -0
- package/dist/manifest.js.map +1 -0
- package/dist/migrations/20260629T125121_invoices_module.d.ts +17 -0
- package/dist/migrations/20260629T125121_invoices_module.d.ts.map +1 -0
- package/dist/migrations/20260629T125121_invoices_module.js +117 -0
- package/dist/migrations/20260629T125121_invoices_module.js.map +1 -0
- package/dist/migrations/20260801T111000_invoices_generic_template_reseed.d.ts +6 -0
- package/dist/migrations/20260801T111000_invoices_generic_template_reseed.d.ts.map +1 -0
- package/dist/migrations/20260801T111000_invoices_generic_template_reseed.js +25 -0
- package/dist/migrations/20260801T111000_invoices_generic_template_reseed.js.map +1 -0
- package/dist/migrations/20260817T201110_invoices_correction_idempotency_key.d.ts +20 -0
- package/dist/migrations/20260817T201110_invoices_correction_idempotency_key.d.ts.map +1 -0
- package/dist/migrations/20260817T201110_invoices_correction_idempotency_key.js +33 -0
- package/dist/migrations/20260817T201110_invoices_correction_idempotency_key.js.map +1 -0
- package/dist/migrations/20260903T101756_invoices_namespace_block_names.d.ts +6 -0
- package/dist/migrations/20260903T101756_invoices_namespace_block_names.d.ts.map +1 -0
- package/dist/migrations/20260903T101756_invoices_namespace_block_names.js +56 -0
- package/dist/migrations/20260903T101756_invoices_namespace_block_names.js.map +1 -0
- package/dist/migrations/20260914T133603_invoices_erp_imported_sale_documents.d.ts +12 -0
- package/dist/migrations/20260914T133603_invoices_erp_imported_sale_documents.d.ts.map +1 -0
- package/dist/migrations/20260914T133603_invoices_erp_imported_sale_documents.js +87 -0
- package/dist/migrations/20260914T133603_invoices_erp_imported_sale_documents.js.map +1 -0
- package/dist/migrations/20260926T230239_invoices_import_identity.d.ts +33 -0
- package/dist/migrations/20260926T230239_invoices_import_identity.d.ts.map +1 -0
- package/dist/migrations/20260926T230239_invoices_import_identity.js +83 -0
- package/dist/migrations/20260926T230239_invoices_import_identity.js.map +1 -0
- package/dist/migrations/index.d.ts +32 -0
- package/dist/migrations/index.d.ts.map +1 -0
- package/dist/migrations/index.js +39 -0
- package/dist/migrations/index.js.map +1 -0
- package/dist/ports/index.d.ts +93 -0
- package/dist/ports/index.d.ts.map +1 -0
- package/dist/ports/index.js +2 -0
- package/dist/ports/index.js.map +1 -0
- package/docs/invoices.md +113 -0
- package/i18n/en.json +45 -0
- package/i18n/pl.json +45 -0
- package/package.json +119 -3
- package/tailwind.css +14 -0
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import type { InvoiceEmailNotSentReason, OrderReadPort, OrderRecord, TransactionalEmailSender } from '@endora-commerce/contracts';
|
|
2
|
+
import type { InvoiceService } from './invoice-service.js';
|
|
3
|
+
import type { InvoicePdfRenderer } from './invoice-pdf-renderer.js';
|
|
4
|
+
import type { SettingsReader } from './seller-settings.js';
|
|
5
|
+
/**
|
|
6
|
+
* Why the invoice e-mail did — or did not — go out (issue #103).
|
|
7
|
+
*
|
|
8
|
+
* `dispatch` used to answer a bare `boolean`, and the two send-on-issue call
|
|
9
|
+
* sites discarded even that. Six unrelated situations therefore looked
|
|
10
|
+
* identical from outside and four of them were silent: no sender wired, no
|
|
11
|
+
* recipient, the operator switched the e-mail off, no template for the code, no
|
|
12
|
+
* transport, and an error the FR-029 `catch` absorbed. An invoice was issued and
|
|
13
|
+
* nothing anywhere said that no message had been sent.
|
|
14
|
+
*
|
|
15
|
+
* The seven reasons live in `@endora-commerce/contracts` since issue #149, because the admin
|
|
16
|
+
* issue route answers with one and an API shape belongs there.
|
|
17
|
+
*/
|
|
18
|
+
export type { InvoiceEmailNotSentReason };
|
|
19
|
+
export type InvoiceEmailDispatchResult = {
|
|
20
|
+
sent: true;
|
|
21
|
+
} | {
|
|
22
|
+
sent: false;
|
|
23
|
+
reason: InvoiceEmailNotSentReason;
|
|
24
|
+
};
|
|
25
|
+
/**
|
|
26
|
+
* Where a failed dispatch is reported. Injectable so a test can read it;
|
|
27
|
+
* defaults to `console.warn`, which is what the rest of this layer uses.
|
|
28
|
+
*/
|
|
29
|
+
export type InvoiceEmailLog = (message: string, context: Record<string, unknown>) => void;
|
|
30
|
+
export interface InvoiceEmailDispatchDeps {
|
|
31
|
+
/**
|
|
32
|
+
* `orders`' published read model (feature 075, Phase C), which replaced this
|
|
33
|
+
* dispatcher's `emFactory` outright: the only thing it ever asked an
|
|
34
|
+
* `EntityManager` for was `em.findOne(Order, …)`, a query against another
|
|
35
|
+
* module's table.
|
|
36
|
+
*/
|
|
37
|
+
orderReadPort: OrderReadPort;
|
|
38
|
+
invoiceService: InvoiceService;
|
|
39
|
+
pdfRenderer: InvoicePdfRenderer;
|
|
40
|
+
settingsService: SettingsReader;
|
|
41
|
+
getSender: () => TransactionalEmailSender | undefined;
|
|
42
|
+
resolveRecipientEmail: (order: OrderRecord) => Promise<string | null>;
|
|
43
|
+
resolveLanguage: (salesChannelId: string | null) => Promise<string>;
|
|
44
|
+
log?: InvoiceEmailLog;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Dispatches the `invoice_issued` transactional email (feature 047, US5).
|
|
48
|
+
* Builds template variables, optionally attaches the rendered PDF (attachment
|
|
49
|
+
* mode) or supplies a storefront download link (link mode), and sends through
|
|
50
|
+
* the transactional_emails sender. Idempotent on `messageId`.
|
|
51
|
+
*
|
|
52
|
+
* **Best-effort, and now audible.** A failure still never throws to the caller
|
|
53
|
+
* — issuance must not be undone (FR-029) — but the tolerance is narrow: a
|
|
54
|
+
* `ModuleDisabledError` is a presence answer and is re-thrown, everything else
|
|
55
|
+
* is named in the result and written to the log. Issue #78 tracks the callers
|
|
56
|
+
* that discard the result; the log is what covers them until it lands.
|
|
57
|
+
*
|
|
58
|
+
* An invoice whose order carries **no sales channel** is an ordinary case, not a
|
|
59
|
+
* failure: every setting below is then read platform-wide (`null`, D-41), and
|
|
60
|
+
* the send names the same `null`.
|
|
61
|
+
*/
|
|
62
|
+
export declare class InvoiceEmailDispatcher {
|
|
63
|
+
private readonly deps;
|
|
64
|
+
private readonly log;
|
|
65
|
+
constructor(deps: InvoiceEmailDispatchDeps);
|
|
66
|
+
dispatch(invoiceId: string, opts?: {
|
|
67
|
+
mode?: 'attachment' | 'link';
|
|
68
|
+
messageId?: string;
|
|
69
|
+
}): Promise<InvoiceEmailDispatchResult>;
|
|
70
|
+
private notSent;
|
|
71
|
+
/**
|
|
72
|
+
* The two conditions that degrade to the module's compiled-in default, and no
|
|
73
|
+
* others: the setting is not registered yet, or it is scoped to a subset of
|
|
74
|
+
* channels this read is not inside. A malformed channel id is a code defect
|
|
75
|
+
* and propagates — the bare `catch` this replaces is precisely what turned the
|
|
76
|
+
* `''` sentinel into a silent "not configured" (D-43, issue #103).
|
|
77
|
+
*/
|
|
78
|
+
private readSetting;
|
|
79
|
+
private resolveMode;
|
|
80
|
+
private resolveBaseUrl;
|
|
81
|
+
/**
|
|
82
|
+
* Whether send-on-issue is enabled for the channel — or, with no channel, for
|
|
83
|
+
* the platform. It used to answer `false` for the no-channel case without
|
|
84
|
+
* reading anything, which is how an invoice issued outside a channel reached
|
|
85
|
+
* the operator with no e-mail and no explanation.
|
|
86
|
+
*/
|
|
87
|
+
sendOnIssueEnabled(channelId: string | null): Promise<boolean>;
|
|
88
|
+
}
|
|
89
|
+
//# sourceMappingURL=invoice-email-dispatch.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"invoice-email-dispatch.d.ts","sourceRoot":"","sources":["../../../src/backend/services/invoice-email-dispatch.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EACV,yBAAyB,EACzB,aAAa,EACb,WAAW,EACX,wBAAwB,EACzB,MAAM,4BAA4B,CAAC;AAMpC,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAC;AAC3D,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,2BAA2B,CAAC;AACpE,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAC;AAG3D;;;;;;;;;;;;GAYG;AACH,YAAY,EAAE,yBAAyB,EAAE,CAAC;AAE1C,MAAM,MAAM,0BAA0B,GAClC;IAAE,IAAI,EAAE,IAAI,CAAA;CAAE,GACd;IAAE,IAAI,EAAE,KAAK,CAAC;IAAC,MAAM,EAAE,yBAAyB,CAAA;CAAE,CAAC;AAEvD;;;GAGG;AACH,MAAM,MAAM,eAAe,GAAG,CAAC,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAAK,IAAI,CAAC;AAE1F,MAAM,WAAW,wBAAwB;IACvC;;;;;OAKG;IACH,aAAa,EAAE,aAAa,CAAC;IAC7B,cAAc,EAAE,cAAc,CAAC;IAC/B,WAAW,EAAE,kBAAkB,CAAC;IAChC,eAAe,EAAE,cAAc,CAAC;IAChC,SAAS,EAAE,MAAM,wBAAwB,GAAG,SAAS,CAAC;IACtD,qBAAqB,EAAE,CAAC,KAAK,EAAE,WAAW,KAAK,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAAC;IACtE,eAAe,EAAE,CAAC,cAAc,EAAE,MAAM,GAAG,IAAI,KAAK,OAAO,CAAC,MAAM,CAAC,CAAC;IACpE,GAAG,CAAC,EAAE,eAAe,CAAC;CACvB;AAED;;;;;;;;;;;;;;;GAeG;AACH,qBAAa,sBAAsB;IAGrB,OAAO,CAAC,QAAQ,CAAC,IAAI;IAFjC,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAkB;gBAET,IAAI,EAAE,wBAAwB;IAIrD,QAAQ,CACZ,SAAS,EAAE,MAAM,EACjB,IAAI,GAAE;QAAE,IAAI,CAAC,EAAE,YAAY,GAAG,MAAM,CAAC;QAAC,SAAS,CAAC,EAAE,MAAM,CAAA;KAAO,GAC9D,OAAO,CAAC,0BAA0B,CAAC;IAoEtC,OAAO,CAAC,OAAO;IAaf;;;;;;OAMG;YACW,WAAW;YAeX,WAAW;YASX,cAAc;IAS5B;;;;;OAKG;IACG,kBAAkB,CAAC,SAAS,EAAE,MAAM,GAAG,IAAI,GAAG,OAAO,CAAC,OAAO,CAAC;CAQrE"}
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { rethrowIfModuleDisabled } from '@endora-commerce/platform/kernel';
|
|
3
|
+
import { SettingNotRegistered, SettingOutOfScopeForChannel, } from '@endora-commerce/platform/kernel';
|
|
4
|
+
import { INVOICES_SETTING_CODES } from '../../manifest.js';
|
|
5
|
+
/**
|
|
6
|
+
* Dispatches the `invoice_issued` transactional email (feature 047, US5).
|
|
7
|
+
* Builds template variables, optionally attaches the rendered PDF (attachment
|
|
8
|
+
* mode) or supplies a storefront download link (link mode), and sends through
|
|
9
|
+
* the transactional_emails sender. Idempotent on `messageId`.
|
|
10
|
+
*
|
|
11
|
+
* **Best-effort, and now audible.** A failure still never throws to the caller
|
|
12
|
+
* — issuance must not be undone (FR-029) — but the tolerance is narrow: a
|
|
13
|
+
* `ModuleDisabledError` is a presence answer and is re-thrown, everything else
|
|
14
|
+
* is named in the result and written to the log. Issue #78 tracks the callers
|
|
15
|
+
* that discard the result; the log is what covers them until it lands.
|
|
16
|
+
*
|
|
17
|
+
* An invoice whose order carries **no sales channel** is an ordinary case, not a
|
|
18
|
+
* failure: every setting below is then read platform-wide (`null`, D-41), and
|
|
19
|
+
* the send names the same `null`.
|
|
20
|
+
*/
|
|
21
|
+
export class InvoiceEmailDispatcher {
|
|
22
|
+
deps;
|
|
23
|
+
log;
|
|
24
|
+
constructor(deps) {
|
|
25
|
+
this.deps = deps;
|
|
26
|
+
this.log = deps.log ?? ((message, context) => console.warn(message, context));
|
|
27
|
+
}
|
|
28
|
+
async dispatch(invoiceId, opts = {}) {
|
|
29
|
+
try {
|
|
30
|
+
const sender = this.deps.getSender();
|
|
31
|
+
if (!sender)
|
|
32
|
+
return this.notSent(invoiceId, 'no_sender');
|
|
33
|
+
const detail = await this.deps.invoiceService.buildDetail(invoiceId);
|
|
34
|
+
if (!detail.orderId)
|
|
35
|
+
return this.notSent(invoiceId, 'invoice_not_found');
|
|
36
|
+
const order = await this.deps.orderReadPort.findById(detail.orderId);
|
|
37
|
+
if (!order)
|
|
38
|
+
return this.notSent(invoiceId, 'invoice_not_found');
|
|
39
|
+
const to = await this.deps.resolveRecipientEmail(order);
|
|
40
|
+
if (!to)
|
|
41
|
+
return this.notSent(invoiceId, 'no_recipient');
|
|
42
|
+
const channelId = detail.salesChannelId;
|
|
43
|
+
const language = await this.deps.resolveLanguage(channelId);
|
|
44
|
+
const mode = opts.mode ?? (await this.resolveMode(channelId));
|
|
45
|
+
const baseUrl = await this.resolveBaseUrl(channelId);
|
|
46
|
+
const downloadUrl = baseUrl
|
|
47
|
+
? `${baseUrl.replace(/\/$/, '')}/orders/${detail.orderId}/invoices/${detail.id}/pdf`
|
|
48
|
+
: '';
|
|
49
|
+
const variables = {
|
|
50
|
+
invoice: {
|
|
51
|
+
number: detail.number,
|
|
52
|
+
total: detail.grossTotal.toFixed(2),
|
|
53
|
+
currency: detail.currency,
|
|
54
|
+
downloadUrl,
|
|
55
|
+
},
|
|
56
|
+
order: { businessId: order.businessId },
|
|
57
|
+
};
|
|
58
|
+
const attachments = mode === 'attachment'
|
|
59
|
+
? [
|
|
60
|
+
{
|
|
61
|
+
filename: `invoice-${detail.number.replace(/\W+/g, '_')}.pdf`,
|
|
62
|
+
content: await this.deps.pdfRenderer.render(detail),
|
|
63
|
+
contentType: 'application/pdf',
|
|
64
|
+
},
|
|
65
|
+
]
|
|
66
|
+
: undefined;
|
|
67
|
+
const outcome = await sender.send({
|
|
68
|
+
code: 'invoice_issued',
|
|
69
|
+
salesChannelId: channelId,
|
|
70
|
+
language,
|
|
71
|
+
to,
|
|
72
|
+
messageId: opts.messageId ?? `invoice_issued:${detail.id}`,
|
|
73
|
+
variables,
|
|
74
|
+
...(attachments ? { attachments } : {}),
|
|
75
|
+
// D-59 — the delivery record has to be findable by the document, not
|
|
76
|
+
// only by the recipient: "was invoice FV 1/2026 delivered" is the
|
|
77
|
+
// support question this whole record exists to answer.
|
|
78
|
+
document: { type: 'invoice', id: detail.id },
|
|
79
|
+
meta: { kind: 'invoice_issued', invoiceId: detail.id, orderId: detail.orderId },
|
|
80
|
+
});
|
|
81
|
+
if (outcome.status !== 'sent')
|
|
82
|
+
return this.notSent(invoiceId, outcome.status);
|
|
83
|
+
return { sent: true };
|
|
84
|
+
}
|
|
85
|
+
catch (error) {
|
|
86
|
+
// A switched-off module is a presence answer about the whole operation,
|
|
87
|
+
// not an e-mail that failed to render; absorbing it would report "sent
|
|
88
|
+
// nothing" where the truthful answer is "this capability is off".
|
|
89
|
+
rethrowIfModuleDisabled(error);
|
|
90
|
+
// Everything else is contained: issuance is committed and must not be
|
|
91
|
+
// undone because the message did not go out (FR-029). It is named, though.
|
|
92
|
+
return this.notSent(invoiceId, 'failed', error);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
notSent(invoiceId, reason, error) {
|
|
96
|
+
this.log('[invoices] the invoice e-mail was not sent', {
|
|
97
|
+
invoiceId,
|
|
98
|
+
reason,
|
|
99
|
+
...(error === undefined ? {} : { error: error instanceof Error ? error.message : error }),
|
|
100
|
+
});
|
|
101
|
+
return { sent: false, reason };
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* The two conditions that degrade to the module's compiled-in default, and no
|
|
105
|
+
* others: the setting is not registered yet, or it is scoped to a subset of
|
|
106
|
+
* channels this read is not inside. A malformed channel id is a code defect
|
|
107
|
+
* and propagates — the bare `catch` this replaces is precisely what turned the
|
|
108
|
+
* `''` sentinel into a silent "not configured" (D-43, issue #103).
|
|
109
|
+
*/
|
|
110
|
+
async readSetting(code, salesChannelId, schema, fallback) {
|
|
111
|
+
try {
|
|
112
|
+
return await this.deps.settingsService.get(code, salesChannelId, schema);
|
|
113
|
+
}
|
|
114
|
+
catch (error) {
|
|
115
|
+
if (error instanceof SettingNotRegistered)
|
|
116
|
+
return fallback;
|
|
117
|
+
if (error instanceof SettingOutOfScopeForChannel)
|
|
118
|
+
return fallback;
|
|
119
|
+
throw error;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
async resolveMode(channelId) {
|
|
123
|
+
return this.readSetting(INVOICES_SETTING_CODES.EMAIL_DELIVERY_MODE, channelId, z.enum(['attachment', 'link']), 'attachment');
|
|
124
|
+
}
|
|
125
|
+
async resolveBaseUrl(channelId) {
|
|
126
|
+
return this.readSetting(INVOICES_SETTING_CODES.STOREFRONT_BASE_URL, channelId, z.string(), '');
|
|
127
|
+
}
|
|
128
|
+
/**
|
|
129
|
+
* Whether send-on-issue is enabled for the channel — or, with no channel, for
|
|
130
|
+
* the platform. It used to answer `false` for the no-channel case without
|
|
131
|
+
* reading anything, which is how an invoice issued outside a channel reached
|
|
132
|
+
* the operator with no e-mail and no explanation.
|
|
133
|
+
*/
|
|
134
|
+
async sendOnIssueEnabled(channelId) {
|
|
135
|
+
return this.readSetting(INVOICES_SETTING_CODES.EMAIL_SEND_ON_ISSUE, channelId, z.boolean(), false);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
//# sourceMappingURL=invoice-email-dispatch.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"invoice-email-dispatch.js","sourceRoot":"","sources":["../../../src/backend/services/invoice-email-dispatch.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAOxB,OAAO,EAAE,uBAAuB,EAAE,MAAM,kCAAkC,CAAC;AAC3E,OAAO,EACL,oBAAoB,EACpB,2BAA2B,GAC5B,MAAM,kCAAkC,CAAC;AAI1C,OAAO,EAAE,sBAAsB,EAAE,MAAM,mBAAmB,CAAC;AA4C3D;;;;;;;;;;;;;;;GAeG;AACH,MAAM,OAAO,sBAAsB;IAGJ;IAFZ,GAAG,CAAkB;IAEtC,YAA6B,IAA8B;QAA9B,SAAI,GAAJ,IAAI,CAA0B;QACzD,IAAI,CAAC,GAAG,GAAG,IAAI,CAAC,GAAG,IAAI,CAAC,CAAC,OAAO,EAAE,OAAO,EAAQ,EAAE,CAAC,OAAO,CAAC,IAAI,CAAC,OAAO,EAAE,OAAO,CAAC,CAAC,CAAC;IACtF,CAAC;IAED,KAAK,CAAC,QAAQ,CACZ,SAAiB,EACjB,OAA6D,EAAE;QAE/D,IAAI,CAAC;YACH,MAAM,MAAM,GAAG,IAAI,CAAC,IAAI,CAAC,SAAS,EAAE,CAAC;YACrC,IAAI,CAAC,MAAM;gBAAE,OAAO,IAAI,CAAC,OAAO,CAAC,SAAS,EAAE,WAAW,CAAC,CAAC;YAEzD,MAAM,MAAM,GAAG,MAAM,IAAI,CAAC,IAAI,CAAC,cAAc,CAAC,WAAW,CAAC,SAAS,CAAC,CAAC;YACrE,IAAI,CAAC,MAAM,CAAC,OAAO;gBAAE,OAAO,IAAI,CAAC,OAAO,CAAC,SAAS,EAAE,mBAAmB,CAAC,CAAC;YACzE,MAAM,KAAK,GAAG,MAAM,IAAI,CAAC,IAAI,CAAC,aAAa,CAAC,QAAQ,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC;YACrE,IAAI,CAAC,KAAK;gBAAE,OAAO,IAAI,CAAC,OAAO,CAAC,SAAS,EAAE,mBAAmB,CAAC,CAAC;YAChE,MAAM,EAAE,GAAG,MAAM,IAAI,CAAC,IAAI,CAAC,qBAAqB,CAAC,KAAK,CAAC,CAAC;YACxD,IAAI,CAAC,EAAE;gBAAE,OAAO,IAAI,CAAC,OAAO,CAAC,SAAS,EAAE,cAAc,CAAC,CAAC;YAExD,MAAM,SAAS,GAAG,MAAM,CAAC,cAAc,CAAC;YACxC,MAAM,QAAQ,GAAG,MAAM,IAAI,CAAC,IAAI,CAAC,eAAe,CAAC,SAAS,CAAC,CAAC;YAC5D,MAAM,IAAI,GAAG,IAAI,CAAC,IAAI,IAAI,CAAC,MAAM,IAAI,CAAC,WAAW,CAAC,SAAS,CAAC,CAAC,CAAC;YAC9D,MAAM,OAAO,GAAG,MAAM,IAAI,CAAC,cAAc,CAAC,SAAS,CAAC,CAAC;YACrD,MAAM,WAAW,GAAG,OAAO;gBACzB,CAAC,CAAC,GAAG,OAAO,CAAC,OAAO,CAAC,KAAK,EAAE,EAAE,CAAC,WAAW,MAAM,CAAC,OAAO,aAAa,MAAM,CAAC,EAAE,MAAM;gBACpF,CAAC,CAAC,EAAE,CAAC;YAEP,MAAM,SAAS,GAA4B;gBACzC,OAAO,EAAE;oBACP,MAAM,EAAE,MAAM,CAAC,MAAM;oBACrB,KAAK,EAAE,MAAM,CAAC,UAAU,CAAC,OAAO,CAAC,CAAC,CAAC;oBACnC,QAAQ,EAAE,MAAM,CAAC,QAAQ;oBACzB,WAAW;iBACZ;gBACD,KAAK,EAAE,EAAE,UAAU,EAAE,KAAK,CAAC,UAAU,EAAE;aACxC,CAAC;YAEF,MAAM,WAAW,GACf,IAAI,KAAK,YAAY;gBACnB,CAAC,CAAC;oBACE;wBACE,QAAQ,EAAE,WAAW,MAAM,CAAC,MAAM,CAAC,OAAO,CAAC,MAAM,EAAE,GAAG,CAAC,MAAM;wBAC7D,OAAO,EAAE,MAAM,IAAI,CAAC,IAAI,CAAC,WAAW,CAAC,MAAM,CAAC,MAAM,CAAC;wBACnD,WAAW,EAAE,iBAAiB;qBAC/B;iBACF;gBACH,CAAC,CAAC,SAAS,CAAC;YAEhB,MAAM,OAAO,GAAG,MAAM,MAAM,CAAC,IAAI,CAAC;gBAChC,IAAI,EAAE,gBAAgB;gBACtB,cAAc,EAAE,SAAS;gBACzB,QAAQ;gBACR,EAAE;gBACF,SAAS,EAAE,IAAI,CAAC,SAAS,IAAI,kBAAkB,MAAM,CAAC,EAAE,EAAE;gBAC1D,SAAS;gBACT,GAAG,CAAC,WAAW,CAAC,CAAC,CAAC,EAAE,WAAW,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;gBACvC,qEAAqE;gBACrE,kEAAkE;gBAClE,uDAAuD;gBACvD,QAAQ,EAAE,EAAE,IAAI,EAAE,SAAS,EAAE,EAAE,EAAE,MAAM,CAAC,EAAE,EAAE;gBAC5C,IAAI,EAAE,EAAE,IAAI,EAAE,gBAAgB,EAAE,SAAS,EAAE,MAAM,CAAC,EAAE,EAAE,OAAO,EAAE,MAAM,CAAC,OAAO,EAAE;aAChF,CAAC,CAAC;YACH,IAAI,OAAO,CAAC,MAAM,KAAK,MAAM;gBAAE,OAAO,IAAI,CAAC,OAAO,CAAC,SAAS,EAAE,OAAO,CAAC,MAAM,CAAC,CAAC;YAC9E,OAAO,EAAE,IAAI,EAAE,IAAI,EAAE,CAAC;QACxB,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,wEAAwE;YACxE,uEAAuE;YACvE,kEAAkE;YAClE,uBAAuB,CAAC,KAAK,CAAC,CAAC;YAC/B,sEAAsE;YACtE,2EAA2E;YAC3E,OAAO,IAAI,CAAC,OAAO,CAAC,SAAS,EAAE,QAAQ,EAAE,KAAK,CAAC,CAAC;QAClD,CAAC;IACH,CAAC;IAEO,OAAO,CACb,SAAiB,EACjB,MAAiC,EACjC,KAAe;QAEf,IAAI,CAAC,GAAG,CAAC,4CAA4C,EAAE;YACrD,SAAS;YACT,MAAM;YACN,GAAG,CAAC,KAAK,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,KAAK,EAAE,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,KAAK,EAAE,CAAC;SAC1F,CAAC,CAAC;QACH,OAAO,EAAE,IAAI,EAAE,KAAK,EAAE,MAAM,EAAE,CAAC;IACjC,CAAC;IAED;;;;;;OAMG;IACK,KAAK,CAAC,WAAW,CACvB,IAAY,EACZ,cAA6B,EAC7B,MAAoB,EACpB,QAAW;QAEX,IAAI,CAAC;YACH,OAAO,MAAM,IAAI,CAAC,IAAI,CAAC,eAAe,CAAC,GAAG,CAAC,IAAI,EAAE,cAAc,EAAE,MAAM,CAAC,CAAC;QAC3E,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,IAAI,KAAK,YAAY,oBAAoB;gBAAE,OAAO,QAAQ,CAAC;YAC3D,IAAI,KAAK,YAAY,2BAA2B;gBAAE,OAAO,QAAQ,CAAC;YAClE,MAAM,KAAK,CAAC;QACd,CAAC;IACH,CAAC;IAEO,KAAK,CAAC,WAAW,CAAC,SAAwB;QAChD,OAAO,IAAI,CAAC,WAAW,CACrB,sBAAsB,CAAC,mBAAmB,EAC1C,SAAS,EACT,CAAC,CAAC,IAAI,CAAC,CAAC,YAAY,EAAE,MAAM,CAAC,CAAC,EAC9B,YAAY,CACb,CAAC;IACJ,CAAC;IAEO,KAAK,CAAC,cAAc,CAAC,SAAwB;QACnD,OAAO,IAAI,CAAC,WAAW,CACrB,sBAAsB,CAAC,mBAAmB,EAC1C,SAAS,EACT,CAAC,CAAC,MAAM,EAAE,EACV,EAAE,CACH,CAAC;IACJ,CAAC;IAED;;;;;OAKG;IACH,KAAK,CAAC,kBAAkB,CAAC,SAAwB;QAC/C,OAAO,IAAI,CAAC,WAAW,CACrB,sBAAsB,CAAC,mBAAmB,EAC1C,SAAS,EACT,CAAC,CAAC,OAAO,EAAE,EACX,KAAK,CACN,CAAC;IACJ,CAAC;CACF"}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { InvoiceLine, VatSummaryRow } from '@endora-commerce/contracts';
|
|
2
|
+
export interface RawOrderLine {
|
|
3
|
+
name: string;
|
|
4
|
+
unit: string;
|
|
5
|
+
quantity: number;
|
|
6
|
+
/** Net unit price. */
|
|
7
|
+
unitNetPrice: number;
|
|
8
|
+
/** Fractional VAT rate, e.g. 0.23. */
|
|
9
|
+
taxRate: number;
|
|
10
|
+
/** Net line value (quantity × unitNetPrice), as stored on the order. */
|
|
11
|
+
netValue: number;
|
|
12
|
+
orderItemId?: string | null;
|
|
13
|
+
}
|
|
14
|
+
export interface BuiltInvoiceTotals {
|
|
15
|
+
lines: Array<InvoiceLine & {
|
|
16
|
+
orderItemId?: string | null;
|
|
17
|
+
}>;
|
|
18
|
+
vatSummary: VatSummaryRow[];
|
|
19
|
+
netTotal: number;
|
|
20
|
+
taxTotal: number;
|
|
21
|
+
grossTotal: number;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Builds immutable invoice line snapshots + the per-rate VAT summary + totals
|
|
25
|
+
* from raw order lines. Pure + deterministic so it can be unit-tested in
|
|
26
|
+
* isolation (SC-003: net + VAT = gross per rate; Σ gross = grand total).
|
|
27
|
+
*/
|
|
28
|
+
export declare function buildInvoiceLines(raw: RawOrderLine[]): BuiltInvoiceTotals;
|
|
29
|
+
//# sourceMappingURL=invoice-line-builder.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"invoice-line-builder.d.ts","sourceRoot":"","sources":["../../../src/backend/services/invoice-line-builder.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,aAAa,EAAE,MAAM,4BAA4B,CAAC;AAO7E,MAAM,WAAW,YAAY;IAC3B,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,MAAM,CAAC;IACjB,sBAAsB;IACtB,YAAY,EAAE,MAAM,CAAC;IACrB,sCAAsC;IACtC,OAAO,EAAE,MAAM,CAAC;IAChB,wEAAwE;IACxE,QAAQ,EAAE,MAAM,CAAC;IACjB,WAAW,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CAC7B;AAED,MAAM,WAAW,kBAAkB;IACjC,KAAK,EAAE,KAAK,CAAC,WAAW,GAAG;QAAE,WAAW,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;KAAE,CAAC,CAAC;IAC5D,UAAU,EAAE,aAAa,EAAE,CAAC;IAC5B,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,MAAM,CAAC;IACjB,UAAU,EAAE,MAAM,CAAC;CACpB;AAED;;;;GAIG;AACH,wBAAgB,iBAAiB,CAAC,GAAG,EAAE,YAAY,EAAE,GAAG,kBAAkB,CAsCzE"}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/** Round to 2 decimals (half-up) for monetary values. */
|
|
2
|
+
function round2(n) {
|
|
3
|
+
return Math.round((n + Number.EPSILON) * 100) / 100;
|
|
4
|
+
}
|
|
5
|
+
/**
|
|
6
|
+
* Builds immutable invoice line snapshots + the per-rate VAT summary + totals
|
|
7
|
+
* from raw order lines. Pure + deterministic so it can be unit-tested in
|
|
8
|
+
* isolation (SC-003: net + VAT = gross per rate; Σ gross = grand total).
|
|
9
|
+
*/
|
|
10
|
+
export function buildInvoiceLines(raw) {
|
|
11
|
+
const lines = raw.map((r, i) => {
|
|
12
|
+
const netValue = round2(r.netValue);
|
|
13
|
+
const grossValue = round2(netValue * (1 + r.taxRate));
|
|
14
|
+
return {
|
|
15
|
+
ordinal: i + 1,
|
|
16
|
+
name: r.name,
|
|
17
|
+
unit: r.unit,
|
|
18
|
+
quantity: r.quantity,
|
|
19
|
+
unitNetPrice: r.unitNetPrice,
|
|
20
|
+
taxRate: r.taxRate,
|
|
21
|
+
netValue,
|
|
22
|
+
grossValue,
|
|
23
|
+
orderItemId: r.orderItemId ?? null,
|
|
24
|
+
};
|
|
25
|
+
});
|
|
26
|
+
// Group by VAT rate for the summary table.
|
|
27
|
+
const byRate = new Map();
|
|
28
|
+
for (const l of lines) {
|
|
29
|
+
const cur = byRate.get(l.taxRate) ?? { net: 0, gross: 0 };
|
|
30
|
+
cur.net += l.netValue;
|
|
31
|
+
cur.gross += l.grossValue;
|
|
32
|
+
byRate.set(l.taxRate, cur);
|
|
33
|
+
}
|
|
34
|
+
const vatSummary = [...byRate.entries()]
|
|
35
|
+
.sort((a, b) => a[0] - b[0])
|
|
36
|
+
.map(([taxRate, v]) => {
|
|
37
|
+
const netTotal = round2(v.net);
|
|
38
|
+
const grossTotal = round2(v.gross);
|
|
39
|
+
return { taxRate, netTotal, vatAmount: round2(grossTotal - netTotal), grossTotal };
|
|
40
|
+
});
|
|
41
|
+
const netTotal = round2(vatSummary.reduce((s, r) => s + r.netTotal, 0));
|
|
42
|
+
const taxTotal = round2(vatSummary.reduce((s, r) => s + r.vatAmount, 0));
|
|
43
|
+
const grossTotal = round2(vatSummary.reduce((s, r) => s + r.grossTotal, 0));
|
|
44
|
+
return { lines, vatSummary, netTotal, taxTotal, grossTotal };
|
|
45
|
+
}
|
|
46
|
+
//# sourceMappingURL=invoice-line-builder.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"invoice-line-builder.js","sourceRoot":"","sources":["../../../src/backend/services/invoice-line-builder.ts"],"names":[],"mappings":"AAEA,yDAAyD;AACzD,SAAS,MAAM,CAAC,CAAS;IACvB,OAAO,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,GAAG,MAAM,CAAC,OAAO,CAAC,GAAG,GAAG,CAAC,GAAG,GAAG,CAAC;AACtD,CAAC;AAuBD;;;;GAIG;AACH,MAAM,UAAU,iBAAiB,CAAC,GAAmB;IACnD,MAAM,KAAK,GAAG,GAAG,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE;QAC7B,MAAM,QAAQ,GAAG,MAAM,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC;QACpC,MAAM,UAAU,GAAG,MAAM,CAAC,QAAQ,GAAG,CAAC,CAAC,GAAG,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC;QACtD,OAAO;YACL,OAAO,EAAE,CAAC,GAAG,CAAC;YACd,IAAI,EAAE,CAAC,CAAC,IAAI;YACZ,IAAI,EAAE,CAAC,CAAC,IAAI;YACZ,QAAQ,EAAE,CAAC,CAAC,QAAQ;YACpB,YAAY,EAAE,CAAC,CAAC,YAAY;YAC5B,OAAO,EAAE,CAAC,CAAC,OAAO;YAClB,QAAQ;YACR,UAAU;YACV,WAAW,EAAE,CAAC,CAAC,WAAW,IAAI,IAAI;SACnC,CAAC;IACJ,CAAC,CAAC,CAAC;IAEH,2CAA2C;IAC3C,MAAM,MAAM,GAAG,IAAI,GAAG,EAA0C,CAAC;IACjE,KAAK,MAAM,CAAC,IAAI,KAAK,EAAE,CAAC;QACtB,MAAM,GAAG,GAAG,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC,OAAO,CAAC,IAAI,EAAE,GAAG,EAAE,CAAC,EAAE,KAAK,EAAE,CAAC,EAAE,CAAC;QAC1D,GAAG,CAAC,GAAG,IAAI,CAAC,CAAC,QAAQ,CAAC;QACtB,GAAG,CAAC,KAAK,IAAI,CAAC,CAAC,UAAU,CAAC;QAC1B,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC,OAAO,EAAE,GAAG,CAAC,CAAC;IAC7B,CAAC;IACD,MAAM,UAAU,GAAoB,CAAC,GAAG,MAAM,CAAC,OAAO,EAAE,CAAC;SACtD,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC;SAC3B,GAAG,CAAC,CAAC,CAAC,OAAO,EAAE,CAAC,CAAC,EAAE,EAAE;QACpB,MAAM,QAAQ,GAAG,MAAM,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC;QAC/B,MAAM,UAAU,GAAG,MAAM,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC;QACnC,OAAO,EAAE,OAAO,EAAE,QAAQ,EAAE,SAAS,EAAE,MAAM,CAAC,UAAU,GAAG,QAAQ,CAAC,EAAE,UAAU,EAAE,CAAC;IACrF,CAAC,CAAC,CAAC;IAEL,MAAM,QAAQ,GAAG,MAAM,CAAC,UAAU,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,GAAG,CAAC,CAAC,QAAQ,EAAE,CAAC,CAAC,CAAC,CAAC;IACxE,MAAM,QAAQ,GAAG,MAAM,CAAC,UAAU,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,GAAG,CAAC,CAAC,SAAS,EAAE,CAAC,CAAC,CAAC,CAAC;IACzE,MAAM,UAAU,GAAG,MAAM,CAAC,UAAU,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,GAAG,CAAC,CAAC,UAAU,EAAE,CAAC,CAAC,CAAC,CAAC;IAE5E,OAAO,EAAE,KAAK,EAAE,UAAU,EAAE,QAAQ,EAAE,QAAQ,EAAE,UAAU,EAAE,CAAC;AAC/D,CAAC"}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { NumberPatternCollision, NumberPatternSequenceDefect, NumberingSeries } from '@endora-commerce/contracts';
|
|
2
|
+
/**
|
|
3
|
+
* `null` when the pattern can produce a distinct number per document.
|
|
4
|
+
*
|
|
5
|
+
* A pattern with no sequence token renders one string for a whole year, so it
|
|
6
|
+
* collides with **itself** on the second document — a self-collision the
|
|
7
|
+
* pairwise sweep would never look for, and a one-condition check.
|
|
8
|
+
*/
|
|
9
|
+
export declare function patternSequenceDefect(pattern: string): NumberPatternSequenceDefect | null;
|
|
10
|
+
/**
|
|
11
|
+
* Every pair in `series` that can render one and the same string.
|
|
12
|
+
*
|
|
13
|
+
* Each series is rendered once (`n × |grid|`) and the pairwise sweep intersects
|
|
14
|
+
* the prepared sets, so the cost is never `n² × |grid|` renders.
|
|
15
|
+
*/
|
|
16
|
+
export declare function findNumberPatternCollisions(series: readonly NumberingSeries[]): NumberPatternCollision[];
|
|
17
|
+
//# sourceMappingURL=invoice-number-collisions.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"invoice-number-collisions.d.ts","sourceRoot":"","sources":["../../../src/backend/services/invoice-number-collisions.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,sBAAsB,EACtB,2BAA2B,EAC3B,eAAe,EAChB,MAAM,4BAA4B,CAAC;AA4CpC;;;;;;GAMG;AACH,wBAAgB,qBAAqB,CAAC,OAAO,EAAE,MAAM,GAAG,2BAA2B,GAAG,IAAI,CAEzF;AAkHD;;;;;GAKG;AACH,wBAAgB,2BAA2B,CACzC,MAAM,EAAE,SAAS,eAAe,EAAE,GACjC,sBAAsB,EAAE,CA0B1B"}
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
import { channelDiscriminator, formatInvoiceNumber } from './invoice-number-generator.js';
|
|
2
|
+
/** How far past the current year the sweep looks. */
|
|
3
|
+
const YEAR_HORIZON = 10;
|
|
4
|
+
/** How far a literal 4-digit year in a pattern may reach and still be probed. */
|
|
5
|
+
const LITERAL_YEAR_HORIZON = 100;
|
|
6
|
+
/** `{seq}` or `{seq:N}` — the tokens that make a pattern able to number a series. */
|
|
7
|
+
const SEQUENCE_TOKEN_RE = /\{seq(?::\d+)?\}/;
|
|
8
|
+
/** Every `{seq:N}` width named by a pattern. */
|
|
9
|
+
const PADDING_TOKEN_RE = /\{seq:(\d+)\}/g;
|
|
10
|
+
/** Literal digit runs, i.e. digits that are not part of a token. */
|
|
11
|
+
const TOKEN_RE = /\{(?:seq(?::\d+)?|channel|YYYY|YY|MM)\}/g;
|
|
12
|
+
/**
|
|
13
|
+
* `null` when the pattern can produce a distinct number per document.
|
|
14
|
+
*
|
|
15
|
+
* A pattern with no sequence token renders one string for a whole year, so it
|
|
16
|
+
* collides with **itself** on the second document — a self-collision the
|
|
17
|
+
* pairwise sweep would never look for, and a one-condition check.
|
|
18
|
+
*/
|
|
19
|
+
export function patternSequenceDefect(pattern) {
|
|
20
|
+
return SEQUENCE_TOKEN_RE.test(pattern) ? null : 'no_sequence_token';
|
|
21
|
+
}
|
|
22
|
+
/** The digit runs a pattern contains as literal text, tokens removed first. */
|
|
23
|
+
function literalDigitRuns(pattern) {
|
|
24
|
+
return pattern.replace(TOKEN_RE, ' ').match(/\d+/g) ?? [];
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* The sequence numbers probed, derived from the compared patterns and channels.
|
|
28
|
+
*
|
|
29
|
+
* `1…20` is where every first-year collision lives. The padding points are
|
|
30
|
+
* where `{seq}` and `{seq:N}` converge. The digit-width sweep is what finds the
|
|
31
|
+
* boundary loss in `FV {channel}{seq}` — `A1` + `2` and `A` + `12` are both
|
|
32
|
+
* `A12` — so it reaches one digit further than the longest compared code.
|
|
33
|
+
*/
|
|
34
|
+
function sequencePoints(series) {
|
|
35
|
+
const points = new Set();
|
|
36
|
+
for (let seq = 1; seq <= 20; seq += 1)
|
|
37
|
+
points.add(seq);
|
|
38
|
+
for (const one of series) {
|
|
39
|
+
for (const match of one.pattern.matchAll(PADDING_TOKEN_RE)) {
|
|
40
|
+
const width = Number(match[1]);
|
|
41
|
+
if (!Number.isFinite(width) || width < 1 || width > 9)
|
|
42
|
+
continue;
|
|
43
|
+
points.add(10 ** (width - 1) - 1);
|
|
44
|
+
points.add(10 ** (width - 1));
|
|
45
|
+
points.add(10 ** width - 1);
|
|
46
|
+
points.add(10 ** width);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
const longestCode = series.reduce((longest, one) => Math.max(longest, one.salesChannelCode.length), 0);
|
|
50
|
+
const widths = 1 + Math.max(6, longestCode);
|
|
51
|
+
for (let k = 1; k <= widths; k += 1) {
|
|
52
|
+
points.add(10 ** (k - 1));
|
|
53
|
+
points.add(10 ** k - 1);
|
|
54
|
+
}
|
|
55
|
+
return [...points].filter((seq) => seq >= 1).sort((a, b) => a - b);
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* The years probed.
|
|
59
|
+
*
|
|
60
|
+
* Never a year before the current one: `issuedAt` is `new Date()` at issuance,
|
|
61
|
+
* so a past year is unreachable and probing one would refuse a configuration
|
|
62
|
+
* that cannot collide. The literal-derived years are the plausible operator
|
|
63
|
+
* mistake — `FV {seq}/2026` typed instead of `FV {seq}/{YYYY}`.
|
|
64
|
+
*/
|
|
65
|
+
function yearPoints(series, currentYear) {
|
|
66
|
+
const points = new Set();
|
|
67
|
+
for (let year = currentYear; year <= currentYear + YEAR_HORIZON; year += 1)
|
|
68
|
+
points.add(year);
|
|
69
|
+
for (const one of series) {
|
|
70
|
+
for (const run of literalDigitRuns(one.pattern)) {
|
|
71
|
+
if (run.length === 4) {
|
|
72
|
+
const year = Number(run);
|
|
73
|
+
if (year >= currentYear && year <= currentYear + LITERAL_YEAR_HORIZON)
|
|
74
|
+
points.add(year);
|
|
75
|
+
}
|
|
76
|
+
else if (run.length === 2) {
|
|
77
|
+
const twoDigit = Number(run);
|
|
78
|
+
// The first year at or after the current one whose `YY` equals the run.
|
|
79
|
+
const candidate = Math.floor(currentYear / 100) * 100 + twoDigit;
|
|
80
|
+
points.add(candidate >= currentYear ? candidate : candidate + 100);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
return [...points].sort((a, b) => a - b);
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* The grid, restricted to the dimensions this pattern actually uses.
|
|
88
|
+
*
|
|
89
|
+
* A pattern without `{MM}` renders the same string for all twelve months, so
|
|
90
|
+
* probing twelve of them is twelve times the work for one `Set` member. The
|
|
91
|
+
* restriction changes no result — it only skips renders whose output is already
|
|
92
|
+
* in the set — and it is what keeps a 50-series sweep inside its budget.
|
|
93
|
+
*/
|
|
94
|
+
function probePoints(pattern, seqs, years) {
|
|
95
|
+
const usesMonth = pattern.includes('{MM}');
|
|
96
|
+
const usesYear = pattern.includes('{YYYY}') || pattern.includes('{YY}');
|
|
97
|
+
const usesSeq = SEQUENCE_TOKEN_RE.test(pattern);
|
|
98
|
+
const seqPoints = usesSeq ? seqs : [1];
|
|
99
|
+
const yearPointsUsed = usesYear ? years : [years[0] ?? new Date().getFullYear()];
|
|
100
|
+
const monthPoints = usesMonth ? [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12] : [1];
|
|
101
|
+
const points = [];
|
|
102
|
+
for (const seq of seqPoints) {
|
|
103
|
+
for (const year of yearPointsUsed) {
|
|
104
|
+
for (const month of monthPoints) {
|
|
105
|
+
// Local time: `formatInvoiceNumber` reads `getFullYear`/`getMonth`.
|
|
106
|
+
points.push({ seq, date: new Date(year, month - 1, 15, 12) });
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
return points;
|
|
111
|
+
}
|
|
112
|
+
/** Every string a series can render over the grid. */
|
|
113
|
+
function renderSet(one, seqs, years) {
|
|
114
|
+
const channel = channelDiscriminator(one.salesChannelCode);
|
|
115
|
+
const rendered = new Set();
|
|
116
|
+
for (const point of probePoints(one.pattern, seqs, years)) {
|
|
117
|
+
rendered.add(formatInvoiceNumber(one.pattern, { seq: point.seq, date: point.date, channel }));
|
|
118
|
+
}
|
|
119
|
+
return rendered;
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* Every pair in `series` that can render one and the same string.
|
|
123
|
+
*
|
|
124
|
+
* Each series is rendered once (`n × |grid|`) and the pairwise sweep intersects
|
|
125
|
+
* the prepared sets, so the cost is never `n² × |grid|` renders.
|
|
126
|
+
*/
|
|
127
|
+
export function findNumberPatternCollisions(series) {
|
|
128
|
+
if (series.length < 2)
|
|
129
|
+
return [];
|
|
130
|
+
const currentYear = new Date().getFullYear();
|
|
131
|
+
const seqs = sequencePoints(series);
|
|
132
|
+
const years = yearPoints(series, currentYear);
|
|
133
|
+
const sets = series.map((one) => renderSet(one, seqs, years));
|
|
134
|
+
const collisions = [];
|
|
135
|
+
for (let i = 0; i < series.length; i += 1) {
|
|
136
|
+
for (let j = i + 1; j < series.length; j += 1) {
|
|
137
|
+
const left = sets[i];
|
|
138
|
+
const right = sets[j];
|
|
139
|
+
const [smaller, larger] = left.size <= right.size ? [left, right] : [right, left];
|
|
140
|
+
let example = null;
|
|
141
|
+
for (const candidate of smaller) {
|
|
142
|
+
if (larger.has(candidate)) {
|
|
143
|
+
example = candidate;
|
|
144
|
+
break;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
if (example !== null) {
|
|
148
|
+
collisions.push({ a: series[i], b: series[j], example });
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
return collisions;
|
|
153
|
+
}
|
|
154
|
+
//# sourceMappingURL=invoice-number-collisions.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"invoice-number-collisions.js","sourceRoot":"","sources":["../../../src/backend/services/invoice-number-collisions.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import type { EntityManager } from '@mikro-orm/postgresql';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
export type InvoiceKind = 'proforma' | 'invoice' | 'correction';
|
|
4
|
+
/**
|
|
5
|
+
* Resolves the format pattern for a (kind, salesChannel) — settings-backed in
|
|
6
|
+
* production, but injectable so the formatter and counter can be unit-tested in
|
|
7
|
+
* isolation.
|
|
8
|
+
*/
|
|
9
|
+
export interface NumberPatternResolver {
|
|
10
|
+
resolvePattern(kind: InvoiceKind, salesChannelId: string): Promise<string>;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* The shipped defaults, which **must** stay identical to the manifest
|
|
14
|
+
* `defaultValue`s (`manifest.ts`): the manifest default is what production
|
|
15
|
+
* resolves, and this map is only reached when the settings read throws.
|
|
16
|
+
*
|
|
17
|
+
* The `/` on both sides of `{channel}` is load-bearing, not cosmetic. A
|
|
18
|
+
* discriminator concatenated straight onto the sequence loses the boundary
|
|
19
|
+
* between the two variable-length fields, so `FV {channel}{seq}/{YYYY}` renders
|
|
20
|
+
* `FV A12/2026` for both (`a1`, seq 2) and (`a`, seq 12) — see D-95.1
|
|
21
|
+
* property 3.
|
|
22
|
+
*/
|
|
23
|
+
declare const DEFAULT_PATTERNS: Record<InvoiceKind, string>;
|
|
24
|
+
/** The pre-D-95 defaults, kept for the system-default channel (D-95.3 move 3). */
|
|
25
|
+
export declare const PRE_CHANNEL_DEFAULT_PATTERNS: Record<InvoiceKind, string>;
|
|
26
|
+
/**
|
|
27
|
+
* The pattern a stored setting value really means: a blank value is not an
|
|
28
|
+
* empty pattern, it is "unset", and it resolves to the default. Two channels
|
|
29
|
+
* can therefore collide while their stored values differ, which is why the
|
|
30
|
+
* collision predicate compares what would be *resolved* and never the raw row.
|
|
31
|
+
*/
|
|
32
|
+
export declare function effectivePattern(kind: InvoiceKind, stored: string | null | undefined): string;
|
|
33
|
+
/** The render context. `channel` is required — see {@link formatInvoiceNumber}. */
|
|
34
|
+
export interface InvoiceNumberContext {
|
|
35
|
+
readonly seq: number;
|
|
36
|
+
readonly date: Date;
|
|
37
|
+
/** The discriminator `{channel}` renders: the channel `code`, uppercased. */
|
|
38
|
+
readonly channel: string;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Render an invoice-number pattern. Supported tokens:
|
|
42
|
+
* {seq} — sequence number
|
|
43
|
+
* {seq:N} — zero-padded to width N
|
|
44
|
+
* {channel} — the sales channel discriminator (feature 078, D-95)
|
|
45
|
+
* {YYYY} — 4-digit year
|
|
46
|
+
* {YY} — 2-digit year
|
|
47
|
+
* {MM} — 2-digit month
|
|
48
|
+
* Everything else is literal (so prefix/suffix are just literal text).
|
|
49
|
+
*
|
|
50
|
+
* `channel` is a **required** member of the context, not an optional one
|
|
51
|
+
* defaulting to `''`: an absent discriminator makes every channel render
|
|
52
|
+
* identically, which is the defect this token exists to remove. A caller that
|
|
53
|
+
* cannot name a channel must fail rather than render.
|
|
54
|
+
*
|
|
55
|
+
* Pure — the collision predicate and the boot report both call it directly.
|
|
56
|
+
*/
|
|
57
|
+
export declare function formatInvoiceNumber(pattern: string, ctx: InvoiceNumberContext): string;
|
|
58
|
+
/**
|
|
59
|
+
* The discriminator a channel code renders as: uppercase, and otherwise
|
|
60
|
+
* verbatim. The code charset is `/^[a-z][a-z0-9_-]*$/`, so uppercasing is
|
|
61
|
+
* injective — no two channels can produce the same discriminator. Stripping or
|
|
62
|
+
* folding any character would break that and is forbidden.
|
|
63
|
+
*/
|
|
64
|
+
export declare function channelDiscriminator(code: string): string;
|
|
65
|
+
/**
|
|
66
|
+
* Draws gap-free, per-(channel, kind, year) sequence numbers under a pessimistic
|
|
67
|
+
* row lock and assembles the human-readable number from the configured pattern.
|
|
68
|
+
*/
|
|
69
|
+
export declare class InvoiceNumberGenerator {
|
|
70
|
+
private readonly patterns?;
|
|
71
|
+
constructor(patterns?: NumberPatternResolver | undefined);
|
|
72
|
+
/** Must run inside an open transaction (`em.transactional(...)`). */
|
|
73
|
+
next(tx: EntityManager, kind: InvoiceKind, salesChannelId: string, issuedAt: Date): Promise<string>;
|
|
74
|
+
/**
|
|
75
|
+
* The `{channel}` discriminator for a channel id, read from the kernel's
|
|
76
|
+
* `sales_channels` row.
|
|
77
|
+
*
|
|
78
|
+
* An absent row **fails** rather than rendering an empty discriminator. A
|
|
79
|
+
* fallback here would put every channel back on one string and restore the
|
|
80
|
+
* defect this feature removes; a `salesChannelId` with no row behind it is a
|
|
81
|
+
* data defect in whatever produced it (`Order.salesChannelId` is non-nullable
|
|
82
|
+
* and a system-default channel always exists), and it is reported, not
|
|
83
|
+
* absorbed.
|
|
84
|
+
*/
|
|
85
|
+
private resolveDiscriminator;
|
|
86
|
+
private resolvePattern;
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Settings-backed pattern resolver: reads `invoices.numbering.<kind>.pattern`
|
|
90
|
+
* per sales channel, falling back to the built-in default.
|
|
91
|
+
*/
|
|
92
|
+
export declare function createSettingsPatternResolver(settingsService: {
|
|
93
|
+
get<T>(code: string, salesChannelId: string, schema: z.ZodType<T>): Promise<T>;
|
|
94
|
+
}): NumberPatternResolver;
|
|
95
|
+
export { DEFAULT_PATTERNS };
|
|
96
|
+
//# sourceMappingURL=invoice-number-generator.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"invoice-number-generator.d.ts","sourceRoot":"","sources":["../../../src/backend/services/invoice-number-generator.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,uBAAuB,CAAC;AAE3D,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAMxB,MAAM,MAAM,WAAW,GAAG,UAAU,GAAG,SAAS,GAAG,YAAY,CAAC;AAEhE;;;;GAIG;AACH,MAAM,WAAW,qBAAqB;IACpC,cAAc,CAAC,IAAI,EAAE,WAAW,EAAE,cAAc,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;CAC5E;AAED;;;;;;;;;;GAUG;AACH,QAAA,MAAM,gBAAgB,EAAE,MAAM,CAAC,WAAW,EAAE,MAAM,CAIjD,CAAC;AAEF,kFAAkF;AAClF,eAAO,MAAM,4BAA4B,EAAE,MAAM,CAAC,WAAW,EAAE,MAAM,CAIpE,CAAC;AAEF;;;;;GAKG;AACH,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,WAAW,EAAE,MAAM,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,GAAG,MAAM,CAE7F;AAED,mFAAmF;AACnF,MAAM,WAAW,oBAAoB;IACnC,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,IAAI,EAAE,IAAI,CAAC;IACpB,6EAA6E;IAC7E,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;CAC1B;AAED;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,mBAAmB,CAAC,OAAO,EAAE,MAAM,EAAE,GAAG,EAAE,oBAAoB,GAAG,MAAM,CAetF;AAED;;;;;GAKG;AACH,wBAAgB,oBAAoB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAEzD;AAED;;;GAGG;AACH,qBAAa,sBAAsB;IACrB,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAC;gBAAT,QAAQ,CAAC,EAAE,qBAAqB,YAAA;IAE7D,qEAAqE;IAC/D,IAAI,CACR,EAAE,EAAE,aAAa,EACjB,IAAI,EAAE,WAAW,EACjB,cAAc,EAAE,MAAM,EACtB,QAAQ,EAAE,IAAI,GACb,OAAO,CAAC,MAAM,CAAC;IAwBlB;;;;;;;;;;OAUG;YACW,oBAAoB;YAapB,cAAc;CAQ7B;AAED;;;GAGG;AACH,wBAAgB,6BAA6B,CAAC,eAAe,EAAE;IAC7D,GAAG,CAAC,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,cAAc,EAAE,MAAM,EAAE,MAAM,EAAE,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC,CAAC;CAChF,GAAG,qBAAqB,CAcxB;AAED,OAAO,EAAE,gBAAgB,EAAE,CAAC"}
|