@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,76 @@
|
|
|
1
|
+
import type { EntityManager } from '@mikro-orm/postgresql';
|
|
2
|
+
import type { NumberingSeries, SettingsAdminPort } from '@endora-commerce/contracts';
|
|
3
|
+
import { SalesChannel } from '@endora-commerce/platform/kernel';
|
|
4
|
+
import { type InvoiceKind } from './invoice-number-generator.js';
|
|
5
|
+
/** What this service writes to. `warn` is the report; nothing here blocks boot. */
|
|
6
|
+
export interface NumberingConfigurationLogger {
|
|
7
|
+
info(obj: object, msg: string): void;
|
|
8
|
+
warn(obj: object, msg: string): void;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* The numbering configuration a deployment boots with — feature 078, D-95.3
|
|
12
|
+
* and D-95.4.
|
|
13
|
+
*
|
|
14
|
+
* Two jobs, in this order and for a stated reason:
|
|
15
|
+
*
|
|
16
|
+
* 1. **Grandfather the system-default channel.** The shipped defaults now
|
|
17
|
+
* carry `{channel}`, so a fresh deployment numbers `FV 1/DEFAULT/2026`
|
|
18
|
+
* unless something says otherwise. An accountant expects `FV 1/2026`, and
|
|
19
|
+
* an existing deployment's series must not change shape mid-year, so the
|
|
20
|
+
* system-default channel is pinned to the pre-D-95 pattern as an explicit
|
|
21
|
+
* per-channel value — **only** while the setting is otherwise untouched.
|
|
22
|
+
* The platform picks a default for you and never rewrites your choice.
|
|
23
|
+
* 2. **Report what is left.** Every `(kind, channel)` pair goes through the
|
|
24
|
+
* collision predicate and each colliding pair is logged as a `warn`. It
|
|
25
|
+
* runs *after* step 1 so it never names a pair the same boot has just
|
|
26
|
+
* repaired — a report full of noise is a report nobody reads within a week.
|
|
27
|
+
* It does not block boot: what survives step 1 is a pattern the operator
|
|
28
|
+
* configured themselves, and refusing to start would take down the
|
|
29
|
+
* storefront, the catalogue and the checkout to protect an invoice the
|
|
30
|
+
* issuance refusal already protects.
|
|
31
|
+
*/
|
|
32
|
+
export declare class NumberingConfigurationService {
|
|
33
|
+
private readonly emFactory;
|
|
34
|
+
/**
|
|
35
|
+
* The audited settings write (Constitution XIII), read per call rather than
|
|
36
|
+
* captured: `settings` is non-deactivatable, so the gate has no "no" to
|
|
37
|
+
* give, but the accessor keeps this service from freezing a registration.
|
|
38
|
+
*/
|
|
39
|
+
private readonly settingsAdmin;
|
|
40
|
+
private readonly log;
|
|
41
|
+
constructor(emFactory: () => EntityManager,
|
|
42
|
+
/**
|
|
43
|
+
* The audited settings write (Constitution XIII), read per call rather than
|
|
44
|
+
* captured: `settings` is non-deactivatable, so the gate has no "no" to
|
|
45
|
+
* give, but the accessor keeps this service from freezing a registration.
|
|
46
|
+
*/
|
|
47
|
+
settingsAdmin: () => SettingsAdminPort, log: NumberingConfigurationLogger);
|
|
48
|
+
/** Both jobs, in order. Called once per boot. */
|
|
49
|
+
reconcile(): Promise<void>;
|
|
50
|
+
/**
|
|
51
|
+
* D-95.3 move 3 — pin the system-default channel to the pre-D-95 pattern,
|
|
52
|
+
* once, and only while nobody has configured the setting.
|
|
53
|
+
*
|
|
54
|
+
* "Untouched" is `globalValue === null` **and** no `SettingValue` row for the
|
|
55
|
+
* system-default channel. An operator who wrote a pattern — even one that now
|
|
56
|
+
* collides — keeps it; that case belongs to the report and to the two
|
|
57
|
+
* refusals, never to a silent rewrite.
|
|
58
|
+
*/
|
|
59
|
+
private pinSystemDefaultChannel;
|
|
60
|
+
/** Every `(kind, channel)` series the platform currently resolves. */
|
|
61
|
+
seriesFor(kind: InvoiceKind): Promise<NumberingSeries[]>;
|
|
62
|
+
/**
|
|
63
|
+
* D-95.4 — name every surviving collision, loudly, and do not block boot.
|
|
64
|
+
*
|
|
65
|
+
* The count line is emitted even when the count is zero, so its absence means
|
|
66
|
+
* the report did not run rather than that it found nothing.
|
|
67
|
+
*/
|
|
68
|
+
reportCollisions(): Promise<number>;
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* A channel's display name. `SalesChannel.name` is a per-language map, so a
|
|
72
|
+
* caller that needs one string picks English, then whatever is there, then the
|
|
73
|
+
* code — which is never empty.
|
|
74
|
+
*/
|
|
75
|
+
export declare function channelName(channel: SalesChannel): string;
|
|
76
|
+
//# sourceMappingURL=numbering-configuration.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"numbering-configuration.d.ts","sourceRoot":"","sources":["../../../src/backend/services/numbering-configuration.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,uBAAuB,CAAC;AAC3D,OAAO,KAAK,EAEV,eAAe,EACf,iBAAiB,EAClB,MAAM,4BAA4B,CAAC;AAEpC,OAAO,EAAE,YAAY,EAAE,MAAM,kCAAkC,CAAC;AAKhE,OAAO,EAGL,KAAK,WAAW,EACjB,MAAM,+BAA+B,CAAC;AAWvC,mFAAmF;AACnF,MAAM,WAAW,4BAA4B;IAC3C,IAAI,CAAC,GAAG,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,IAAI,CAAC;IACrC,IAAI,CAAC,GAAG,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,IAAI,CAAC;CACtC;AAED;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,qBAAa,6BAA6B;IAEtC,OAAO,CAAC,QAAQ,CAAC,SAAS;IAC1B;;;;OAIG;IACH,OAAO,CAAC,QAAQ,CAAC,aAAa;IAC9B,OAAO,CAAC,QAAQ,CAAC,GAAG;gBAPH,SAAS,EAAE,MAAM,aAAa;IAC/C;;;;OAIG;IACc,aAAa,EAAE,MAAM,iBAAiB,EACtC,GAAG,EAAE,4BAA4B;IAGpD,iDAAiD;IAC3C,SAAS,IAAI,OAAO,CAAC,IAAI,CAAC;IAKhC;;;;;;;;OAQG;YACW,uBAAuB;IAkErC,sEAAsE;IAChE,SAAS,CAAC,IAAI,EAAE,WAAW,GAAG,OAAO,CAAC,eAAe,EAAE,CAAC;IAqB9D;;;;;OAKG;IACG,gBAAgB,IAAI,OAAO,CAAC,MAAM,CAAC;CAe1C;AAaD;;;;GAIG;AACH,wBAAgB,WAAW,CAAC,OAAO,EAAE,YAAY,GAAG,MAAM,CAGzD"}
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
import { rethrowIfModuleDisabled } from '@endora-commerce/platform/kernel';
|
|
2
|
+
import { SalesChannel } from '@endora-commerce/platform/kernel';
|
|
3
|
+
import { Setting } from '@endora-commerce/platform/kernel';
|
|
4
|
+
import { SettingValue } from '@endora-commerce/platform/kernel';
|
|
5
|
+
import { INVOICES_SETTING_CODES } from '../../manifest.js';
|
|
6
|
+
import { findNumberPatternCollisions } from './invoice-number-collisions.js';
|
|
7
|
+
import { PRE_CHANNEL_DEFAULT_PATTERNS, effectivePattern, } from './invoice-number-generator.js';
|
|
8
|
+
/** The three numbering settings, in the order an operator meets them. */
|
|
9
|
+
const PATTERN_SETTING_CODE = {
|
|
10
|
+
invoice: INVOICES_SETTING_CODES.NUMBERING_INVOICE_PATTERN,
|
|
11
|
+
proforma: INVOICES_SETTING_CODES.NUMBERING_PROFORMA_PATTERN,
|
|
12
|
+
correction: INVOICES_SETTING_CODES.NUMBERING_CORRECTION_PATTERN,
|
|
13
|
+
};
|
|
14
|
+
const KINDS = ['invoice', 'proforma', 'correction'];
|
|
15
|
+
/**
|
|
16
|
+
* The numbering configuration a deployment boots with — feature 078, D-95.3
|
|
17
|
+
* and D-95.4.
|
|
18
|
+
*
|
|
19
|
+
* Two jobs, in this order and for a stated reason:
|
|
20
|
+
*
|
|
21
|
+
* 1. **Grandfather the system-default channel.** The shipped defaults now
|
|
22
|
+
* carry `{channel}`, so a fresh deployment numbers `FV 1/DEFAULT/2026`
|
|
23
|
+
* unless something says otherwise. An accountant expects `FV 1/2026`, and
|
|
24
|
+
* an existing deployment's series must not change shape mid-year, so the
|
|
25
|
+
* system-default channel is pinned to the pre-D-95 pattern as an explicit
|
|
26
|
+
* per-channel value — **only** while the setting is otherwise untouched.
|
|
27
|
+
* The platform picks a default for you and never rewrites your choice.
|
|
28
|
+
* 2. **Report what is left.** Every `(kind, channel)` pair goes through the
|
|
29
|
+
* collision predicate and each colliding pair is logged as a `warn`. It
|
|
30
|
+
* runs *after* step 1 so it never names a pair the same boot has just
|
|
31
|
+
* repaired — a report full of noise is a report nobody reads within a week.
|
|
32
|
+
* It does not block boot: what survives step 1 is a pattern the operator
|
|
33
|
+
* configured themselves, and refusing to start would take down the
|
|
34
|
+
* storefront, the catalogue and the checkout to protect an invoice the
|
|
35
|
+
* issuance refusal already protects.
|
|
36
|
+
*/
|
|
37
|
+
export class NumberingConfigurationService {
|
|
38
|
+
emFactory;
|
|
39
|
+
settingsAdmin;
|
|
40
|
+
log;
|
|
41
|
+
constructor(emFactory,
|
|
42
|
+
/**
|
|
43
|
+
* The audited settings write (Constitution XIII), read per call rather than
|
|
44
|
+
* captured: `settings` is non-deactivatable, so the gate has no "no" to
|
|
45
|
+
* give, but the accessor keeps this service from freezing a registration.
|
|
46
|
+
*/
|
|
47
|
+
settingsAdmin, log) {
|
|
48
|
+
this.emFactory = emFactory;
|
|
49
|
+
this.settingsAdmin = settingsAdmin;
|
|
50
|
+
this.log = log;
|
|
51
|
+
}
|
|
52
|
+
/** Both jobs, in order. Called once per boot. */
|
|
53
|
+
async reconcile() {
|
|
54
|
+
await this.pinSystemDefaultChannel();
|
|
55
|
+
await this.reportCollisions();
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* D-95.3 move 3 — pin the system-default channel to the pre-D-95 pattern,
|
|
59
|
+
* once, and only while nobody has configured the setting.
|
|
60
|
+
*
|
|
61
|
+
* "Untouched" is `globalValue === null` **and** no `SettingValue` row for the
|
|
62
|
+
* system-default channel. An operator who wrote a pattern — even one that now
|
|
63
|
+
* collides — keeps it; that case belongs to the report and to the two
|
|
64
|
+
* refusals, never to a silent rewrite.
|
|
65
|
+
*/
|
|
66
|
+
async pinSystemDefaultChannel() {
|
|
67
|
+
const em = this.emFactory();
|
|
68
|
+
const systemDefault = await em.findOne(SalesChannel, { systemDefault: true });
|
|
69
|
+
if (!systemDefault) {
|
|
70
|
+
// A system-default channel always exists (the boot reconciler owns that
|
|
71
|
+
// invariant). Reporting rather than inventing one: a fabricated channel
|
|
72
|
+
// here would pin a series to a row that is not the platform's default.
|
|
73
|
+
this.log.warn({ module: 'invoices' }, 'invoices: no system-default sales channel, so the pre-D-95 numbering pattern was not pinned');
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
for (const kind of KINDS) {
|
|
77
|
+
const code = PATTERN_SETTING_CODE[kind];
|
|
78
|
+
const setting = await em.findOne(Setting, { code });
|
|
79
|
+
if (!setting)
|
|
80
|
+
continue;
|
|
81
|
+
if (setting.globalValue !== null && setting.globalValue !== undefined)
|
|
82
|
+
continue;
|
|
83
|
+
const existing = await em.findOne(SettingValue, {
|
|
84
|
+
setting,
|
|
85
|
+
salesChannel: systemDefault.id,
|
|
86
|
+
});
|
|
87
|
+
if (existing)
|
|
88
|
+
continue;
|
|
89
|
+
try {
|
|
90
|
+
await this.settingsAdmin().setValueForSubset(code, [systemDefault.code], PRE_CHANNEL_DEFAULT_PATTERNS[kind], null, { actorAdminUserId: null });
|
|
91
|
+
}
|
|
92
|
+
catch (err) {
|
|
93
|
+
// A switched-off `settings` is not a tolerable outcome here — the write
|
|
94
|
+
// is the whole point of the hook — so the gate's answer goes straight
|
|
95
|
+
// back out, before any other test. `ModuleDisabledError` is an
|
|
96
|
+
// `HttpError`, so an `instanceof` or status test would let it through
|
|
97
|
+
// by accident rather than by decision.
|
|
98
|
+
rethrowIfModuleDisabled(err);
|
|
99
|
+
// The narrow tolerance, and it is D-95.4 written as code: another
|
|
100
|
+
// channel already carries the historical pattern as an explicit value,
|
|
101
|
+
// so pinning it here would duplicate it and the module's own write
|
|
102
|
+
// validator refuses. That is a configuration the operator made, and
|
|
103
|
+
// refusing to boot over it would take down the storefront, the
|
|
104
|
+
// catalogue and the checkout to protect an invoice the issuance refusal
|
|
105
|
+
// already protects. The report below names the pair; nothing is
|
|
106
|
+
// silently absorbed.
|
|
107
|
+
this.log.warn({
|
|
108
|
+
module: 'invoices',
|
|
109
|
+
settingCode: code,
|
|
110
|
+
salesChannelCode: systemDefault.code,
|
|
111
|
+
reason: err instanceof Error ? err.message : String(err),
|
|
112
|
+
}, 'invoices: could not pin the system-default channel to its historical numbering pattern');
|
|
113
|
+
continue;
|
|
114
|
+
}
|
|
115
|
+
this.log.info({ module: 'invoices', settingCode: code, salesChannelCode: systemDefault.code }, 'invoices: pinned the system-default channel to its historical numbering pattern');
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
/** Every `(kind, channel)` series the platform currently resolves. */
|
|
119
|
+
async seriesFor(kind) {
|
|
120
|
+
const em = this.emFactory();
|
|
121
|
+
const channels = await em.find(SalesChannel, {});
|
|
122
|
+
const setting = await em.findOne(Setting, { code: PATTERN_SETTING_CODE[kind] });
|
|
123
|
+
const values = setting
|
|
124
|
+
? await em.find(SettingValue, { setting }, { populate: ['salesChannel'] })
|
|
125
|
+
: [];
|
|
126
|
+
const byChannelId = new Map(values.map((value) => [value.salesChannel.id, value.value]));
|
|
127
|
+
const globalValue = setting?.globalValue;
|
|
128
|
+
return channels.map((channel) => {
|
|
129
|
+
const stored = byChannelId.has(channel.id) ? byChannelId.get(channel.id) : globalValue;
|
|
130
|
+
return {
|
|
131
|
+
salesChannelId: channel.id,
|
|
132
|
+
salesChannelCode: channel.code,
|
|
133
|
+
salesChannelName: channelName(channel),
|
|
134
|
+
pattern: effectivePattern(kind, typeof stored === 'string' ? stored : null),
|
|
135
|
+
};
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
/**
|
|
139
|
+
* D-95.4 — name every surviving collision, loudly, and do not block boot.
|
|
140
|
+
*
|
|
141
|
+
* The count line is emitted even when the count is zero, so its absence means
|
|
142
|
+
* the report did not run rather than that it found nothing.
|
|
143
|
+
*/
|
|
144
|
+
async reportCollisions() {
|
|
145
|
+
let total = 0;
|
|
146
|
+
for (const kind of KINDS) {
|
|
147
|
+
const collisions = findNumberPatternCollisions(await this.seriesFor(kind));
|
|
148
|
+
total += collisions.length;
|
|
149
|
+
for (const collision of collisions) {
|
|
150
|
+
this.log.warn(describeCollision(kind, collision), 'invoices: two sales channels can produce the same document number');
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
this.log.info({ module: 'invoices', collidingPairs: total }, 'invoices: numbering pattern collision report');
|
|
154
|
+
return total;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
/** The `warn` payload — both channel codes, both patterns and the proof. */
|
|
158
|
+
function describeCollision(kind, collision) {
|
|
159
|
+
return {
|
|
160
|
+
module: 'invoices',
|
|
161
|
+
kind,
|
|
162
|
+
salesChannelCodes: [collision.a.salesChannelCode, collision.b.salesChannelCode],
|
|
163
|
+
patterns: [collision.a.pattern, collision.b.pattern],
|
|
164
|
+
example: collision.example,
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
/**
|
|
168
|
+
* A channel's display name. `SalesChannel.name` is a per-language map, so a
|
|
169
|
+
* caller that needs one string picks English, then whatever is there, then the
|
|
170
|
+
* code — which is never empty.
|
|
171
|
+
*/
|
|
172
|
+
export function channelName(channel) {
|
|
173
|
+
const names = channel.name ?? {};
|
|
174
|
+
return names['en'] ?? Object.values(names)[0] ?? channel.code;
|
|
175
|
+
}
|
|
176
|
+
//# sourceMappingURL=numbering-configuration.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"numbering-configuration.js","sourceRoot":"","sources":["../../../src/backend/services/numbering-configuration.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { type SettingWriteValidator } from '@endora-commerce/contracts';
|
|
2
|
+
/**
|
|
3
|
+
* `invoices`' answer to "is this numbering configuration legal" — feature 078,
|
|
4
|
+
* D-95.2.
|
|
5
|
+
*
|
|
6
|
+
* `settings` hands over what every channel's value would be after the write;
|
|
7
|
+
* this decides whether two of them can render one string. It refuses a
|
|
8
|
+
* **possible** collision, not an actual one, and that asymmetry with the
|
|
9
|
+
* issuance refusal is the point: at a settings write the operator is
|
|
10
|
+
* configuring and nothing is at stake, so a conservative refusal costs them one
|
|
11
|
+
* edit; at issuance a customer's document is at stake, so only a duplicate the
|
|
12
|
+
* database has actually rejected is refused.
|
|
13
|
+
*
|
|
14
|
+
* The predicate is over the **post-write state**, not over the delta, so
|
|
15
|
+
* re-saving a value that already collides is refused too. That is what an
|
|
16
|
+
* operator needs: the question is whether the configuration they are about to
|
|
17
|
+
* confirm is one the platform can number, not whether they changed anything.
|
|
18
|
+
*
|
|
19
|
+
* Pure: no I/O. Everything it needs is in the projection.
|
|
20
|
+
*/
|
|
21
|
+
export declare function createNumberingPatternValidator(): SettingWriteValidator;
|
|
22
|
+
//# sourceMappingURL=numbering-write-validator.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"numbering-write-validator.d.ts","sourceRoot":"","sources":["../../../src/backend/services/numbering-write-validator.ts"],"names":[],"mappings":"AAAA,OAAO,EAIL,KAAK,qBAAqB,EAC3B,MAAM,4BAA4B,CAAC;AAepC;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAgB,+BAA+B,IAAI,qBAAqB,CAQvE"}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { ERROR_CODES, } from '@endora-commerce/contracts';
|
|
2
|
+
import { HttpError } from '@endora-commerce/platform/http';
|
|
3
|
+
import { INVOICES_SETTING_CODES } from '../../manifest.js';
|
|
4
|
+
import { findNumberPatternCollisions, patternSequenceDefect, } from './invoice-number-collisions.js';
|
|
5
|
+
import { effectivePattern } from './invoice-number-generator.js';
|
|
6
|
+
const KIND_BY_SETTING_CODE = {
|
|
7
|
+
[INVOICES_SETTING_CODES.NUMBERING_INVOICE_PATTERN]: 'invoice',
|
|
8
|
+
[INVOICES_SETTING_CODES.NUMBERING_PROFORMA_PATTERN]: 'proforma',
|
|
9
|
+
[INVOICES_SETTING_CODES.NUMBERING_CORRECTION_PATTERN]: 'correction',
|
|
10
|
+
};
|
|
11
|
+
/**
|
|
12
|
+
* `invoices`' answer to "is this numbering configuration legal" — feature 078,
|
|
13
|
+
* D-95.2.
|
|
14
|
+
*
|
|
15
|
+
* `settings` hands over what every channel's value would be after the write;
|
|
16
|
+
* this decides whether two of them can render one string. It refuses a
|
|
17
|
+
* **possible** collision, not an actual one, and that asymmetry with the
|
|
18
|
+
* issuance refusal is the point: at a settings write the operator is
|
|
19
|
+
* configuring and nothing is at stake, so a conservative refusal costs them one
|
|
20
|
+
* edit; at issuance a customer's document is at stake, so only a duplicate the
|
|
21
|
+
* database has actually rejected is refused.
|
|
22
|
+
*
|
|
23
|
+
* The predicate is over the **post-write state**, not over the delta, so
|
|
24
|
+
* re-saving a value that already collides is refused too. That is what an
|
|
25
|
+
* operator needs: the question is whether the configuration they are about to
|
|
26
|
+
* confirm is one the platform can number, not whether they changed anything.
|
|
27
|
+
*
|
|
28
|
+
* Pure: no I/O. Everything it needs is in the projection.
|
|
29
|
+
*/
|
|
30
|
+
export function createNumberingPatternValidator() {
|
|
31
|
+
return {
|
|
32
|
+
contributorModuleId: 'invoices',
|
|
33
|
+
codes: Object.keys(KIND_BY_SETTING_CODE),
|
|
34
|
+
validate: async (input) => {
|
|
35
|
+
refuseIllegalNumbering(input);
|
|
36
|
+
},
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
function refuseIllegalNumbering(input) {
|
|
40
|
+
const kind = KIND_BY_SETTING_CODE[input.code];
|
|
41
|
+
if (kind === undefined)
|
|
42
|
+
return;
|
|
43
|
+
const series = input.projection.map((channel) => ({
|
|
44
|
+
salesChannelId: channel.salesChannelId,
|
|
45
|
+
salesChannelCode: channel.salesChannelCode,
|
|
46
|
+
salesChannelName: channel.salesChannelName,
|
|
47
|
+
pattern: effectivePattern(kind, typeof channel.value === 'string' ? channel.value : null),
|
|
48
|
+
}));
|
|
49
|
+
// A pattern with no sequence token renders one string for a whole year, so it
|
|
50
|
+
// collides with itself on the second document — a self-collision the pairwise
|
|
51
|
+
// sweep would never look for. Checked on the series this write sets: a
|
|
52
|
+
// platform-wide write reaches every channel without its own row, so it is
|
|
53
|
+
// checked against all of them.
|
|
54
|
+
const targeted = new Set(input.targetedChannelIds);
|
|
55
|
+
const written = targeted.size > 0
|
|
56
|
+
? series.filter((one) => targeted.has(one.salesChannelId))
|
|
57
|
+
: series;
|
|
58
|
+
if (written.some((one) => patternSequenceDefect(one.pattern) !== null)) {
|
|
59
|
+
throw new HttpError(400, ERROR_CODES.INVOICE_NUMBER_PATTERN_COLLIDES, 'This numbering pattern has no sequence token, so every document in this sales ' +
|
|
60
|
+
'channel would be given the same number.', { code: 'no_sequence_token', kind });
|
|
61
|
+
}
|
|
62
|
+
const [collision] = findNumberPatternCollisions(series);
|
|
63
|
+
if (collision === undefined)
|
|
64
|
+
return;
|
|
65
|
+
// Name the channel the operator is *not* editing — that is the one they have
|
|
66
|
+
// to reason about. A platform-wide write targets nobody, so either side is as
|
|
67
|
+
// informative as the other.
|
|
68
|
+
const other = targeted.has(collision.a.salesChannelId) ? collision.b : collision.a;
|
|
69
|
+
throw new HttpError(400, ERROR_CODES.INVOICE_NUMBER_PATTERN_COLLIDES, `This numbering pattern can produce the same document number as sales channel ` +
|
|
70
|
+
`"${other.salesChannelName}".`, {
|
|
71
|
+
code: 'other_channel',
|
|
72
|
+
kind,
|
|
73
|
+
channel: other.salesChannelName,
|
|
74
|
+
channelCode: other.salesChannelCode,
|
|
75
|
+
// Never interpolated into a sentence: a pattern contains `{seq}`, and the
|
|
76
|
+
// envelope discards a translation with an unfilled placeholder whole. It
|
|
77
|
+
// travels here for the admin UI to render outside the message.
|
|
78
|
+
otherPattern: other.pattern,
|
|
79
|
+
example: collision.example,
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
//# sourceMappingURL=numbering-write-validator.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"numbering-write-validator.js","sourceRoot":"","sources":["../../../src/backend/services/numbering-write-validator.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { type SellerCompanyData } from '@endora-commerce/contracts';
|
|
3
|
+
/**
|
|
4
|
+
* The slice of `SettingsService` this module reads through.
|
|
5
|
+
*
|
|
6
|
+
* `salesChannelId` is `string | null`, matching the service (D-41): `null` is a
|
|
7
|
+
* platform-wide read, and it is the only honest thing to pass for an invoice
|
|
8
|
+
* whose order carries no channel. The narrower `string` this used to declare is
|
|
9
|
+
* what pushed `invoices` into the `''` sentinel (issue #103).
|
|
10
|
+
*/
|
|
11
|
+
export interface SettingsReader {
|
|
12
|
+
get<T>(code: string, salesChannelId: string | null, schema: z.ZodType<T>): Promise<T>;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Resolves the seller (own company) data + VAT/NIP from settings for a sales
|
|
16
|
+
* channel and merges them. Throws an actionable 422 when the minimum required
|
|
17
|
+
* fields are missing so issuance never produces an invalid document (FR-006).
|
|
18
|
+
*/
|
|
19
|
+
export declare class SellerSettingsResolver {
|
|
20
|
+
private readonly settings;
|
|
21
|
+
constructor(settings: SettingsReader);
|
|
22
|
+
resolve(salesChannelId: string): Promise<SellerCompanyData>;
|
|
23
|
+
}
|
|
24
|
+
//# sourceMappingURL=seller-settings.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"seller-settings.d.ts","sourceRoot":"","sources":["../../../src/backend/services/seller-settings.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAGxB,OAAO,EAA2B,KAAK,iBAAiB,EAAE,MAAM,4BAA4B,CAAC;AAG7F;;;;;;;GAOG;AACH,MAAM,WAAW,cAAc;IAC7B,GAAG,CAAC,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,cAAc,EAAE,MAAM,GAAG,IAAI,EAAE,MAAM,EAAE,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC,CAAC;CACvF;AAED;;;;GAIG;AACH,qBAAa,sBAAsB;IACrB,OAAO,CAAC,QAAQ,CAAC,QAAQ;gBAAR,QAAQ,EAAE,cAAc;IAE/C,OAAO,CAAC,cAAc,EAAE,MAAM,GAAG,OAAO,CAAC,iBAAiB,CAAC;CA6BlE"}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { HttpError } from '@endora-commerce/platform/http';
|
|
3
|
+
import { ERROR_CODES } from '@endora-commerce/contracts';
|
|
4
|
+
import { sellerCompanyDataSchema } from '@endora-commerce/contracts';
|
|
5
|
+
import { INVOICES_SETTING_CODES } from '../../manifest.js';
|
|
6
|
+
/**
|
|
7
|
+
* Resolves the seller (own company) data + VAT/NIP from settings for a sales
|
|
8
|
+
* channel and merges them. Throws an actionable 422 when the minimum required
|
|
9
|
+
* fields are missing so issuance never produces an invalid document (FR-006).
|
|
10
|
+
*/
|
|
11
|
+
export class SellerSettingsResolver {
|
|
12
|
+
settings;
|
|
13
|
+
constructor(settings) {
|
|
14
|
+
this.settings = settings;
|
|
15
|
+
}
|
|
16
|
+
async resolve(salesChannelId) {
|
|
17
|
+
let taxId = '';
|
|
18
|
+
let raw = {};
|
|
19
|
+
try {
|
|
20
|
+
taxId = await this.settings.get(INVOICES_SETTING_CODES.SELLER_TAX_ID, salesChannelId, z.string());
|
|
21
|
+
}
|
|
22
|
+
catch {
|
|
23
|
+
taxId = '';
|
|
24
|
+
}
|
|
25
|
+
try {
|
|
26
|
+
raw = await this.settings.get(INVOICES_SETTING_CODES.SELLER_COMPANY_DATA, salesChannelId, z.record(z.string(), z.unknown()));
|
|
27
|
+
}
|
|
28
|
+
catch {
|
|
29
|
+
raw = {};
|
|
30
|
+
}
|
|
31
|
+
const parsed = sellerCompanyDataSchema.safeParse({ ...raw, taxId: taxId || raw.taxId });
|
|
32
|
+
if (!parsed.success || !parsed.data.legalName || !parsed.data.taxId) {
|
|
33
|
+
throw new HttpError(422, ERROR_CODES.VALIDATION_FAILED, 'Seller VAT/NIP and company data must be configured before issuing invoices.', { setting: 'invoices.seller.company_data' });
|
|
34
|
+
}
|
|
35
|
+
return parsed.data;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
//# sourceMappingURL=seller-settings.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"seller-settings.js","sourceRoot":"","sources":["../../../src/backend/services/seller-settings.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AACxB,OAAO,EAAE,SAAS,EAAE,MAAM,gCAAgC,CAAC;AAC3D,OAAO,EAAE,WAAW,EAAE,MAAM,4BAA4B,CAAC;AACzD,OAAO,EAAE,uBAAuB,EAA0B,MAAM,4BAA4B,CAAC;AAC7F,OAAO,EAAE,sBAAsB,EAAE,MAAM,mBAAmB,CAAC;AAc3D;;;;GAIG;AACH,MAAM,OAAO,sBAAsB;IACJ;IAA7B,YAA6B,QAAwB;QAAxB,aAAQ,GAAR,QAAQ,CAAgB;IAAG,CAAC;IAEzD,KAAK,CAAC,OAAO,CAAC,cAAsB;QAClC,IAAI,KAAK,GAAG,EAAE,CAAC;QACf,IAAI,GAAG,GAAY,EAAE,CAAC;QACtB,IAAI,CAAC;YACH,KAAK,GAAG,MAAM,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,sBAAsB,CAAC,aAAa,EAAE,cAAc,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,CAAC;QACpG,CAAC;QAAC,MAAM,CAAC;YACP,KAAK,GAAG,EAAE,CAAC;QACb,CAAC;QACD,IAAI,CAAC;YACH,GAAG,GAAG,MAAM,IAAI,CAAC,QAAQ,CAAC,GAAG,CAC3B,sBAAsB,CAAC,mBAAmB,EAC1C,cAAc,EACd,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC,CAAC,OAAO,EAAE,CAAC,CAClC,CAAC;QACJ,CAAC;QAAC,MAAM,CAAC;YACP,GAAG,GAAG,EAAE,CAAC;QACX,CAAC;QAED,MAAM,MAAM,GAAG,uBAAuB,CAAC,SAAS,CAAC,EAAE,GAAI,GAAc,EAAE,KAAK,EAAE,KAAK,IAAK,GAA0B,CAAC,KAAK,EAAE,CAAC,CAAC;QAC5H,IAAI,CAAC,MAAM,CAAC,OAAO,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,SAAS,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,KAAK,EAAE,CAAC;YACpE,MAAM,IAAI,SAAS,CACjB,GAAG,EACH,WAAW,CAAC,iBAAiB,EAC7B,6EAA6E,EAC7E,EAAE,OAAO,EAAE,8BAA8B,EAAE,CAC5C,CAAC;QACJ,CAAC;QACD,OAAO,MAAM,CAAC,IAAI,CAAC;IACrB,CAAC;CACF"}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export interface LedgerNumberingLookup {
|
|
2
|
+
numberingModeFor(salesChannelId: string | null): Promise<'endora' | 'vendor'>;
|
|
3
|
+
activeVendorModuleId(): Promise<string | null>;
|
|
4
|
+
}
|
|
5
|
+
/**
|
|
6
|
+
* Mode B wait: vendor numbering and an active ledger vendor keep the invoice
|
|
7
|
+
* pending so PDF/email wait for applyVendorAssignedNumber.
|
|
8
|
+
*/
|
|
9
|
+
export declare function shouldHoldForVendorNumber(routing: LedgerNumberingLookup | undefined, salesChannelId: string | null): Promise<boolean>;
|
|
10
|
+
//# sourceMappingURL=vendor-number-hold.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"vendor-number-hold.d.ts","sourceRoot":"","sources":["../../../src/backend/services/vendor-number-hold.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,qBAAqB;IACpC,gBAAgB,CAAC,cAAc,EAAE,MAAM,GAAG,IAAI,GAAG,OAAO,CAAC,QAAQ,GAAG,QAAQ,CAAC,CAAC;IAC9E,oBAAoB,IAAI,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAAC;CAChD;AAED;;;GAGG;AACH,wBAAsB,yBAAyB,CAC7C,OAAO,EAAE,qBAAqB,GAAG,SAAS,EAC1C,cAAc,EAAE,MAAM,GAAG,IAAI,GAC5B,OAAO,CAAC,OAAO,CAAC,CAOlB"}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Mode B wait: vendor numbering and an active ledger vendor keep the invoice
|
|
3
|
+
* pending so PDF/email wait for applyVendorAssignedNumber.
|
|
4
|
+
*/
|
|
5
|
+
export async function shouldHoldForVendorNumber(routing, salesChannelId) {
|
|
6
|
+
if (!routing)
|
|
7
|
+
return false;
|
|
8
|
+
const [mode, vendor] = await Promise.all([
|
|
9
|
+
routing.numberingModeFor(salesChannelId),
|
|
10
|
+
routing.activeVendorModuleId(),
|
|
11
|
+
]);
|
|
12
|
+
return mode === 'vendor' && vendor !== null;
|
|
13
|
+
}
|
|
14
|
+
//# sourceMappingURL=vendor-number-hold.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"vendor-number-hold.js","sourceRoot":"","sources":["../../../src/backend/services/vendor-number-hold.ts"],"names":[],"mappings":"AAKA;;;GAGG;AACH,MAAM,CAAC,KAAK,UAAU,yBAAyB,CAC7C,OAA0C,EAC1C,cAA6B;IAE7B,IAAI,CAAC,OAAO;QAAE,OAAO,KAAK,CAAC;IAC3B,MAAM,CAAC,IAAI,EAAE,MAAM,CAAC,GAAG,MAAM,OAAO,CAAC,GAAG,CAAC;QACvC,OAAO,CAAC,gBAAgB,CAAC,cAAc,CAAC;QACxC,OAAO,CAAC,oBAAoB,EAAE;KAC/B,CAAC,CAAC;IACH,OAAO,IAAI,KAAK,QAAQ,IAAI,MAAM,KAAK,IAAI,CAAC;AAC9C,CAAC"}
|