@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 @@
|
|
|
1
|
+
{"version":3,"file":"auto-issue-reactor.d.ts","sourceRoot":"","sources":["../../../src/backend/services/auto-issue-reactor.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,uBAAuB,CAAC;AAS3D,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAC;AAC3D,OAAO,KAAK,EAAE,sBAAsB,EAAE,eAAe,EAAE,MAAM,6BAA6B,CAAC;AAC3F,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAC;AAG3D,MAAM,WAAW,oBAAoB;IACnC,SAAS,EAAE,MAAM,aAAa,CAAC;IAC/B,cAAc,EAAE,cAAc,CAAC;IAC/B,eAAe,EAAE,cAAc,CAAC;IAChC,wEAAwE;IACxE,eAAe,CAAC,EAAE,sBAAsB,GAAG,SAAS,CAAC;IACrD;;;;OAIG;IACH,GAAG,CAAC,EAAE,eAAe,CAAC;CACvB;AAKD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+BG;AACH,wBAAgB,sBAAsB,CAAC,IAAI,EAAE,oBAAoB,GAAG;IAClE,oBAAoB,EAAE,CAAC,OAAO,EAAE,OAAO,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;CAC3D,CAgFA"}
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { Invoice } from '../entities/invoice.entity.js';
|
|
3
|
+
import { rethrowIfModuleDisabled } from '@endora-commerce/platform/kernel';
|
|
4
|
+
import { SettingNotRegistered, SettingOutOfScopeForChannel, } from '@endora-commerce/platform/kernel';
|
|
5
|
+
import { INVOICES_SETTING_CODES } from '../../manifest.js';
|
|
6
|
+
/** The message every non-delivery below is written under. */
|
|
7
|
+
const NOT_EMAILED = '[invoices] the auto-issued invoice was not e-mailed';
|
|
8
|
+
/**
|
|
9
|
+
* FR-002 — issue an invoice when an order reaches the configured status.
|
|
10
|
+
*
|
|
11
|
+
* The **subscription** lives in this module's `backend.ts` and goes through
|
|
12
|
+
* `ctx.subscribe` (issue #107). It was a bare `eventBus.on` in the plugin body,
|
|
13
|
+
* and of the twenty-two such registrations this was the heaviest write: a
|
|
14
|
+
* switched-off `invoices` module still issued a numbered legal document, took a
|
|
15
|
+
* number out of the invoice sequence and e-mailed the PDF to the customer, while
|
|
16
|
+
* the admin surface that would have shown it refused.
|
|
17
|
+
*
|
|
18
|
+
* Idempotent on `(orderId, kind='invoice')`, and still best-effort: a failed
|
|
19
|
+
* automatic attempt leaves manual issuance available. It is no longer *silent*
|
|
20
|
+
* (issue #115). This subscriber has no caller to answer, so "observed" means
|
|
21
|
+
* written to the log with the context only the reactor knows — which order
|
|
22
|
+
* triggered the issuance, and which number it drew. Three things were swallowed
|
|
23
|
+
* here and each now has a distinct outcome:
|
|
24
|
+
*
|
|
25
|
+
* - a **non-sent dispatch**: `{ sent, reason }` was discarded exactly as the
|
|
26
|
+
* plugin-body closure it was lifted out of discarded it, so an invoice that
|
|
27
|
+
* was issued and never delivered read as success. The reason is recorded.
|
|
28
|
+
* - a **switched-off module**: the outer `catch {}` absorbed
|
|
29
|
+
* `ModuleDisabledError`, which is fail-closed turned into fail-open. It is
|
|
30
|
+
* re-thrown; the EventBus isolates and logs handler throws, so the presence
|
|
31
|
+
* answer surfaces instead of being confused with a transient failure.
|
|
32
|
+
* - a **defective settings read**: `catch { trigger = '' }` made every failure
|
|
33
|
+
* read as "no trigger configured" — the same `''` sentinel D-43 removed from
|
|
34
|
+
* the dispatcher. Only the two conditions that genuinely mean "not
|
|
35
|
+
* configured" degrade now.
|
|
36
|
+
*
|
|
37
|
+
* An operator who switched send-on-issue off is *not* a loss and is not logged:
|
|
38
|
+
* that is a configured choice, not an undelivered message.
|
|
39
|
+
*/
|
|
40
|
+
export function createAutoIssueReactor(deps) {
|
|
41
|
+
const log = deps.log ?? ((message, context) => console.warn(message, context));
|
|
42
|
+
/**
|
|
43
|
+
* The trigger status, or `''` when the operator has not configured one. The
|
|
44
|
+
* two conditions that degrade, and no others: the setting is not registered
|
|
45
|
+
* yet, or it is scoped to a subset of channels this read is not inside.
|
|
46
|
+
* Anything else — including a `ModuleDisabledError` from the settings port —
|
|
47
|
+
* propagates.
|
|
48
|
+
*/
|
|
49
|
+
async function readTrigger(salesChannelId) {
|
|
50
|
+
try {
|
|
51
|
+
return await deps.settingsService.get(INVOICES_SETTING_CODES.AUTO_ISSUE_TRIGGER_STATUS, salesChannelId, z.string());
|
|
52
|
+
}
|
|
53
|
+
catch (error) {
|
|
54
|
+
if (error instanceof SettingNotRegistered)
|
|
55
|
+
return '';
|
|
56
|
+
if (error instanceof SettingOutOfScopeForChannel)
|
|
57
|
+
return '';
|
|
58
|
+
throw error;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
return {
|
|
62
|
+
async onOrderStatusChanged(payload) {
|
|
63
|
+
const p = payload;
|
|
64
|
+
if (!p.orderId || !p.salesChannelId || !p.to)
|
|
65
|
+
return;
|
|
66
|
+
const trigger = await readTrigger(p.salesChannelId);
|
|
67
|
+
if (!trigger || trigger !== p.to)
|
|
68
|
+
return;
|
|
69
|
+
const em = deps.emFactory();
|
|
70
|
+
const existing = await em.findOne(Invoice, { orderId: p.orderId, kind: 'invoice' });
|
|
71
|
+
if (existing)
|
|
72
|
+
return; // idempotent
|
|
73
|
+
let detail;
|
|
74
|
+
try {
|
|
75
|
+
detail = await deps.invoiceService.issue(p.orderId, 'invoice', {
|
|
76
|
+
issuedBy: 'system',
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
catch (error) {
|
|
80
|
+
// Narrow and after the fact: the order has already moved status and
|
|
81
|
+
// manual issuance stays available, so a failed automatic attempt must
|
|
82
|
+
// not take the subscriber down. A switched-off module is not that — it
|
|
83
|
+
// is a presence answer about the whole operation and is re-thrown.
|
|
84
|
+
rethrowIfModuleDisabled(error);
|
|
85
|
+
log('[invoices] the order was not auto-invoiced', {
|
|
86
|
+
orderId: p.orderId,
|
|
87
|
+
error: error instanceof Error ? error.message : error,
|
|
88
|
+
});
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
const dispatcher = deps.emailDispatcher;
|
|
92
|
+
if (!dispatcher) {
|
|
93
|
+
// Same situation the dispatcher names `no_sender`, one layer earlier:
|
|
94
|
+
// there is no dispatcher to name it, so the reactor does.
|
|
95
|
+
log(NOT_EMAILED, {
|
|
96
|
+
invoiceId: detail.id,
|
|
97
|
+
orderId: p.orderId,
|
|
98
|
+
number: detail.number,
|
|
99
|
+
reason: 'no_sender',
|
|
100
|
+
});
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
if (detail.status === 'pending')
|
|
104
|
+
return;
|
|
105
|
+
if (!(await dispatcher.sendOnIssueEnabled(detail.salesChannelId)))
|
|
106
|
+
return;
|
|
107
|
+
// Nothing catches here: `dispatch` contains its own failures and names
|
|
108
|
+
// them in the result (FR-029), and the one thing it re-throws is a
|
|
109
|
+
// presence answer that must reach the bus.
|
|
110
|
+
const result = await dispatcher.dispatch(detail.id);
|
|
111
|
+
if (result.sent)
|
|
112
|
+
return;
|
|
113
|
+
log(NOT_EMAILED, {
|
|
114
|
+
invoiceId: detail.id,
|
|
115
|
+
orderId: p.orderId,
|
|
116
|
+
number: detail.number,
|
|
117
|
+
reason: result.reason,
|
|
118
|
+
});
|
|
119
|
+
},
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
//# sourceMappingURL=auto-issue-reactor.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"auto-issue-reactor.js","sourceRoot":"","sources":["../../../src/backend/services/auto-issue-reactor.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import type { EntityManager } from '@mikro-orm/postgresql';
|
|
2
|
+
import type { InvoiceNumberGenerator } from './invoice-number-generator.js';
|
|
3
|
+
import type { InvoiceAuditRecorder, InvoiceDomainEventEmitter } from './invoice-service.js';
|
|
4
|
+
import { type LedgerNumberingLookup } from './vendor-number-hold.js';
|
|
5
|
+
import type { CorrectiveInvoiceInput, CorrectiveInvoicePort, CorrectiveInvoiceResult } from '@endora-commerce/contracts';
|
|
6
|
+
/**
|
|
7
|
+
* Invoices-side implementation of the returns module's `CorrectiveInvoicePort`
|
|
8
|
+
* (feature 046 / 047 US3). Creates an `invoices` row of kind `correction`:
|
|
9
|
+
* - number drawn from the correction counter (per channel/year) when a
|
|
10
|
+
* number generator is wired (falls back to a UUID-stamped number otherwise);
|
|
11
|
+
* - references the order's original VAT invoice;
|
|
12
|
+
* - snapshots the corrected lines into `invoice_lines`, each carrying the VAT
|
|
13
|
+
* rate and the unit of the original line it corrects (issues #131, #136.1);
|
|
14
|
+
* - caps the credited total at the original invoice gross (FR-020).
|
|
15
|
+
*
|
|
16
|
+
* **An order that was never invoiced gets no document** (product ruling
|
|
17
|
+
* 2026-08-17, issue #135): there is nothing to correct, so the port answers
|
|
18
|
+
* `{ issued: false, reason: 'order_not_invoiced' }` and the settlement records
|
|
19
|
+
* the refund without a VAT-shaped document standing in for one that never
|
|
20
|
+
* existed.
|
|
21
|
+
*/
|
|
22
|
+
export declare class CorrectiveInvoiceProvider implements CorrectiveInvoicePort {
|
|
23
|
+
private readonly emFactory;
|
|
24
|
+
private readonly numbers?;
|
|
25
|
+
private readonly audit?;
|
|
26
|
+
private readonly events?;
|
|
27
|
+
private readonly ledgerRouting?;
|
|
28
|
+
/**
|
|
29
|
+
* `numbers` is an accessor for the same reason `emFactory` is: the generator
|
|
30
|
+
* is a gated port, so resolving it is a question about `invoices`' effective
|
|
31
|
+
* state whose answer changes while the process runs. A composition root that
|
|
32
|
+
* resolved it while wiring this provider asked at boot, and an operator who
|
|
33
|
+
* had switched `invoices` off took the backend down with it.
|
|
34
|
+
*/
|
|
35
|
+
constructor(emFactory: () => EntityManager, numbers?: (() => InvoiceNumberGenerator) | undefined, audit?: InvoiceAuditRecorder | undefined, events?: InvoiceDomainEventEmitter | undefined, ledgerRouting?: LedgerNumberingLookup | undefined);
|
|
36
|
+
createCorrection(input: CorrectiveInvoiceInput): Promise<CorrectiveInvoiceResult>;
|
|
37
|
+
}
|
|
38
|
+
//# sourceMappingURL=corrective-invoice.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"corrective-invoice.d.ts","sourceRoot":"","sources":["../../../src/backend/services/corrective-invoice.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,uBAAuB,CAAC;AAK3D,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,+BAA+B,CAAC;AAC5E,OAAO,KAAK,EAAE,oBAAoB,EAAE,yBAAyB,EAAE,MAAM,sBAAsB,CAAC;AAC5F,OAAO,EAEL,KAAK,qBAAqB,EAC3B,MAAM,yBAAyB,CAAC;AAMjC,OAAO,KAAK,EACV,sBAAsB,EACtB,qBAAqB,EACrB,uBAAuB,EACxB,MAAM,4BAA4B,CAAC;AAuCpC;;;;;;;;;;;;;;;GAeG;AACH,qBAAa,yBAA0B,YAAW,qBAAqB;IASnE,OAAO,CAAC,QAAQ,CAAC,SAAS;IAC1B,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAC;IACzB,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAC;IACvB,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAC;IACxB,OAAO,CAAC,QAAQ,CAAC,aAAa,CAAC;IAZjC;;;;;;OAMG;gBAEgB,SAAS,EAAE,MAAM,aAAa,EAC9B,OAAO,CAAC,GAAE,MAAM,sBAAsB,aAAA,EACtC,KAAK,CAAC,EAAE,oBAAoB,YAAA,EAC5B,MAAM,CAAC,EAAE,yBAAyB,YAAA,EAClC,aAAa,CAAC,EAAE,qBAAqB,YAAA;IAGlD,gBAAgB,CAAC,KAAK,EAAE,sBAAsB,GAAG,OAAO,CAAC,uBAAuB,CAAC;CA+MxF"}
|
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
import { randomUUID } from 'crypto';
|
|
2
|
+
import { ERROR_CODES } from '@endora-commerce/contracts';
|
|
3
|
+
import { HttpError } from '@endora-commerce/platform/http';
|
|
4
|
+
import { Invoice } from '../entities/invoice.entity.js';
|
|
5
|
+
import { InvoiceLine } from '../entities/invoice-line.entity.js';
|
|
6
|
+
import { shouldHoldForVendorNumber, } from './vendor-number-hold.js';
|
|
7
|
+
import { rethrowIfModuleDisabled } from '@endora-commerce/platform/kernel';
|
|
8
|
+
import { refuseDuplicateInvoiceNumber } from './duplicate-number-refusal.js';
|
|
9
|
+
function round2(n) {
|
|
10
|
+
return Math.round((n + Number.EPSILON) * 100) / 100;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* What a corrective line inherits from the line it corrects (product ruling
|
|
14
|
+
* 2026-08-16, issue #131 and #136.1): the VAT rate and the unit of measure,
|
|
15
|
+
* never the rate in force on the correction date and never a constant.
|
|
16
|
+
*
|
|
17
|
+
* A line naming an order item the original never invoiced is refused —
|
|
18
|
+
* inventing a rate for an added position is worse than refusing it.
|
|
19
|
+
*/
|
|
20
|
+
function mirrorFor(original, byOrderItem, orderItemId) {
|
|
21
|
+
const mirrored = byOrderItem.get(orderItemId);
|
|
22
|
+
if (!mirrored) {
|
|
23
|
+
throw new HttpError(422, ERROR_CODES.VALIDATION_FAILED, 'The corrected line has no counterpart on the original invoice, so its VAT rate is unknown.', { code: 'no_corrected_line', orderItemId, originalInvoiceId: original.id });
|
|
24
|
+
}
|
|
25
|
+
return mirrored;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Invoices-side implementation of the returns module's `CorrectiveInvoicePort`
|
|
29
|
+
* (feature 046 / 047 US3). Creates an `invoices` row of kind `correction`:
|
|
30
|
+
* - number drawn from the correction counter (per channel/year) when a
|
|
31
|
+
* number generator is wired (falls back to a UUID-stamped number otherwise);
|
|
32
|
+
* - references the order's original VAT invoice;
|
|
33
|
+
* - snapshots the corrected lines into `invoice_lines`, each carrying the VAT
|
|
34
|
+
* rate and the unit of the original line it corrects (issues #131, #136.1);
|
|
35
|
+
* - caps the credited total at the original invoice gross (FR-020).
|
|
36
|
+
*
|
|
37
|
+
* **An order that was never invoiced gets no document** (product ruling
|
|
38
|
+
* 2026-08-17, issue #135): there is nothing to correct, so the port answers
|
|
39
|
+
* `{ issued: false, reason: 'order_not_invoiced' }` and the settlement records
|
|
40
|
+
* the refund without a VAT-shaped document standing in for one that never
|
|
41
|
+
* existed.
|
|
42
|
+
*/
|
|
43
|
+
export class CorrectiveInvoiceProvider {
|
|
44
|
+
emFactory;
|
|
45
|
+
numbers;
|
|
46
|
+
audit;
|
|
47
|
+
events;
|
|
48
|
+
ledgerRouting;
|
|
49
|
+
/**
|
|
50
|
+
* `numbers` is an accessor for the same reason `emFactory` is: the generator
|
|
51
|
+
* is a gated port, so resolving it is a question about `invoices`' effective
|
|
52
|
+
* state whose answer changes while the process runs. A composition root that
|
|
53
|
+
* resolved it while wiring this provider asked at boot, and an operator who
|
|
54
|
+
* had switched `invoices` off took the backend down with it.
|
|
55
|
+
*/
|
|
56
|
+
constructor(emFactory, numbers, audit, events, ledgerRouting) {
|
|
57
|
+
this.emFactory = emFactory;
|
|
58
|
+
this.numbers = numbers;
|
|
59
|
+
this.audit = audit;
|
|
60
|
+
this.events = events;
|
|
61
|
+
this.ledgerRouting = ledgerRouting;
|
|
62
|
+
}
|
|
63
|
+
async createCorrection(input) {
|
|
64
|
+
const em = this.emFactory();
|
|
65
|
+
// The same correction, asked for twice (D-91). A settlement attempts every
|
|
66
|
+
// external effect before it writes any state, so a refusal from a later
|
|
67
|
+
// step leaves a retryable case and the retry arrives here with the key of
|
|
68
|
+
// the correction this call already issued. Answering with that document is
|
|
69
|
+
// what keeps one return case to one credit note; the partial unique index
|
|
70
|
+
// on the column is what keeps two concurrent retries to one as well.
|
|
71
|
+
if (input.idempotencyKey) {
|
|
72
|
+
const already = await em.findOne(Invoice, {
|
|
73
|
+
correctionIdempotencyKey: input.idempotencyKey,
|
|
74
|
+
kind: 'correction',
|
|
75
|
+
});
|
|
76
|
+
if (already) {
|
|
77
|
+
return {
|
|
78
|
+
issued: true,
|
|
79
|
+
invoiceId: already.id,
|
|
80
|
+
number: already.number,
|
|
81
|
+
status: already.status,
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
const original = await em.findOne(Invoice, { orderId: input.orderId, kind: 'invoice' });
|
|
86
|
+
// No original, no correction (#135). The caller records the refund on the
|
|
87
|
+
// return case and the payment; only the VAT document is skipped, and the
|
|
88
|
+
// reason is stated so the caller reports it rather than reporting nothing.
|
|
89
|
+
if (!original)
|
|
90
|
+
return { issued: false, reason: 'order_not_invoiced' };
|
|
91
|
+
// A correction is denominated in the corrected document's currency: the cap
|
|
92
|
+
// below compares the credited amount with the original gross, and two
|
|
93
|
+
// amounts in different currencies do not compare (#136.2). Nothing produces
|
|
94
|
+
// a cross-currency credit today — the return case inherits the order's
|
|
95
|
+
// currency — so refusing costs nothing and closes the seam. Conversion is
|
|
96
|
+
// deliberately not attempted here.
|
|
97
|
+
if (input.currency !== original.currency) {
|
|
98
|
+
throw new HttpError(422, ERROR_CODES.VALIDATION_FAILED, `A correction must be issued in the corrected invoice's currency (${original.currency}), not ${input.currency}.`, {
|
|
99
|
+
code: 'correction_currency_mismatch',
|
|
100
|
+
requestedCurrency: input.currency,
|
|
101
|
+
originalCurrency: original.currency,
|
|
102
|
+
originalInvoiceId: original.id,
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
// The correction belongs to the corrected document's channel — that is the
|
|
106
|
+
// numbering series it continues.
|
|
107
|
+
const salesChannelId = original.salesChannelId ?? null;
|
|
108
|
+
// Cap the credited total at the original invoice gross (never credit more
|
|
109
|
+
// than was invoiced).
|
|
110
|
+
const originalGross = Number(original.total);
|
|
111
|
+
const requested = round2(input.total);
|
|
112
|
+
const credited = Math.min(requested, originalGross);
|
|
113
|
+
// A correction mirrors the line it corrects. The link is `orderItemId`:
|
|
114
|
+
// issuance snapshots it on every product line of the original, and a return
|
|
115
|
+
// case item names the same order item.
|
|
116
|
+
const mirrorByOrderItem = new Map();
|
|
117
|
+
const originalLines = await em.find(InvoiceLine, { invoiceId: original.id });
|
|
118
|
+
for (const l of originalLines) {
|
|
119
|
+
if (l.orderItemId)
|
|
120
|
+
mirrorByOrderItem.set(l.orderItemId, { rate: Number(l.taxRate), unit: l.unit });
|
|
121
|
+
}
|
|
122
|
+
// Resolve every line before the transaction opens, so a line that cannot
|
|
123
|
+
// be mirrored refuses without having drawn a correction number.
|
|
124
|
+
//
|
|
125
|
+
// The credited amount is gross (a return credits what was paid, tax
|
|
126
|
+
// included), so the net follows from the mirrored rate rather than the
|
|
127
|
+
// other way round.
|
|
128
|
+
let remaining = credited;
|
|
129
|
+
const snapshots = input.lines.map((l, index) => {
|
|
130
|
+
const mirrored = mirrorFor(original, mirrorByOrderItem, l.orderItemId);
|
|
131
|
+
const gross = round2(Math.min(Math.max(l.amount, 0), remaining));
|
|
132
|
+
remaining = round2(remaining - gross);
|
|
133
|
+
const net = round2(gross / (1 + mirrored.rate));
|
|
134
|
+
const qty = l.quantity || 1;
|
|
135
|
+
return {
|
|
136
|
+
ordinal: index + 1,
|
|
137
|
+
orderItemId: l.orderItemId,
|
|
138
|
+
name: l.productName,
|
|
139
|
+
quantity: qty,
|
|
140
|
+
rate: mirrored.rate,
|
|
141
|
+
unit: mirrored.unit,
|
|
142
|
+
net,
|
|
143
|
+
gross,
|
|
144
|
+
unitNetPrice: net / qty,
|
|
145
|
+
};
|
|
146
|
+
});
|
|
147
|
+
// The document's own totals follow its lines: net + tax = the credited
|
|
148
|
+
// gross, which stays the FR-020 cap.
|
|
149
|
+
const taxTotal = round2(snapshots.reduce((sum, s) => sum + (s.gross - s.net), 0));
|
|
150
|
+
const netTotal = round2(credited - taxTotal);
|
|
151
|
+
const issuedAt = new Date();
|
|
152
|
+
const holdForVendor = await shouldHoldForVendorNumber(this.ledgerRouting, salesChannelId);
|
|
153
|
+
// Captured out of the transaction: see the same note in `InvoiceService`.
|
|
154
|
+
let drawnNumber = null;
|
|
155
|
+
const runCorrection = async (tx) => {
|
|
156
|
+
// Draw from the correction counter (a real numbering series to continue)
|
|
157
|
+
// whenever a generator and a channel are known.
|
|
158
|
+
let number;
|
|
159
|
+
if (this.numbers && salesChannelId) {
|
|
160
|
+
number = await this.numbers().next(tx, 'correction', salesChannelId, issuedAt);
|
|
161
|
+
}
|
|
162
|
+
else {
|
|
163
|
+
number = `KOR-${randomUUID().slice(0, 8).toUpperCase()}`;
|
|
164
|
+
}
|
|
165
|
+
drawnNumber = number;
|
|
166
|
+
const inv = tx.create(Invoice, {
|
|
167
|
+
orderId: input.orderId,
|
|
168
|
+
salesChannelId,
|
|
169
|
+
kind: 'correction',
|
|
170
|
+
number,
|
|
171
|
+
issuedAt,
|
|
172
|
+
// The sale date is a fact about the sale, not about the correction
|
|
173
|
+
// (product ruling 2026-08-17, #136.4): a credit note reports the date
|
|
174
|
+
// the goods were delivered or the service performed, which is the
|
|
175
|
+
// original's sale date. `issuedAt` above is the correction's own issue
|
|
176
|
+
// date and keeps its meaning — do not collapse the two back together.
|
|
177
|
+
saleDate: original.saleDate ?? issuedAt.toISOString().slice(0, 10),
|
|
178
|
+
currency: input.currency,
|
|
179
|
+
netTotal: netTotal.toFixed(2),
|
|
180
|
+
taxTotal: taxTotal.toFixed(2),
|
|
181
|
+
total: credited.toFixed(2),
|
|
182
|
+
paidTotal: '0',
|
|
183
|
+
originalInvoiceId: original.id,
|
|
184
|
+
...(input.idempotencyKey ? { correctionIdempotencyKey: input.idempotencyKey } : {}),
|
|
185
|
+
...(original.buyerSnapshot ? { buyerSnapshot: original.buyerSnapshot } : {}),
|
|
186
|
+
...(original.sellerSnapshot ? { sellerSnapshot: original.sellerSnapshot } : {}),
|
|
187
|
+
issuedBy: 'system',
|
|
188
|
+
status: holdForVendor ? 'pending' : 'ready',
|
|
189
|
+
});
|
|
190
|
+
await tx.persistAndFlush(inv);
|
|
191
|
+
// Snapshot the corrected lines (already capped at the credited remainder).
|
|
192
|
+
for (const s of snapshots) {
|
|
193
|
+
tx.create(InvoiceLine, {
|
|
194
|
+
invoiceId: inv.id,
|
|
195
|
+
ordinal: s.ordinal,
|
|
196
|
+
name: s.name,
|
|
197
|
+
// The unit comes from the original line, so a position invoiced in
|
|
198
|
+
// `opak.` is credited in `opak.` and not in `szt.` (#136.1).
|
|
199
|
+
unit: s.unit,
|
|
200
|
+
quantity: String(s.quantity),
|
|
201
|
+
unitNetPrice: s.unitNetPrice.toFixed(4),
|
|
202
|
+
taxRate: s.rate.toFixed(4),
|
|
203
|
+
netValue: s.net.toFixed(2),
|
|
204
|
+
grossValue: s.gross.toFixed(2),
|
|
205
|
+
orderItemId: s.orderItemId,
|
|
206
|
+
});
|
|
207
|
+
}
|
|
208
|
+
await tx.flush();
|
|
209
|
+
return inv;
|
|
210
|
+
};
|
|
211
|
+
// Feature 078, D-95.2 — a correction draws from the same platform-wide
|
|
212
|
+
// number space, so it is refused the same way. `rethrowIfModuleDisabled` is
|
|
213
|
+
// the **first** line: `ModuleDisabledError` is an `HttpError`, so any other
|
|
214
|
+
// test lets it through by accident rather than by decision, and the
|
|
215
|
+
// callback reaches this module's own gated number generator.
|
|
216
|
+
let invoice;
|
|
217
|
+
try {
|
|
218
|
+
invoice = await em.transactional(runCorrection);
|
|
219
|
+
}
|
|
220
|
+
catch (err) {
|
|
221
|
+
rethrowIfModuleDisabled(err);
|
|
222
|
+
await refuseDuplicateInvoiceNumber({
|
|
223
|
+
error: err,
|
|
224
|
+
drawnNumber,
|
|
225
|
+
salesChannelId,
|
|
226
|
+
emFactory: this.emFactory,
|
|
227
|
+
});
|
|
228
|
+
throw err;
|
|
229
|
+
}
|
|
230
|
+
if (this.audit) {
|
|
231
|
+
await this.audit
|
|
232
|
+
.record({
|
|
233
|
+
action: 'invoice.corrected',
|
|
234
|
+
objectType: 'invoice',
|
|
235
|
+
objectId: invoice.id,
|
|
236
|
+
stateAfter: {
|
|
237
|
+
number: invoice.number,
|
|
238
|
+
orderId: input.orderId,
|
|
239
|
+
originalInvoiceId: original.id,
|
|
240
|
+
credited,
|
|
241
|
+
},
|
|
242
|
+
})
|
|
243
|
+
.catch(() => undefined);
|
|
244
|
+
}
|
|
245
|
+
// Feature 059 — domain event for downstream consumers (e.g. KSeF submission).
|
|
246
|
+
this.events?.emit('invoice.corrected.v1', {
|
|
247
|
+
eventId: randomUUID(),
|
|
248
|
+
occurredAt: new Date().toISOString(),
|
|
249
|
+
invoiceId: invoice.id,
|
|
250
|
+
originalInvoiceId: original.id,
|
|
251
|
+
orderId: input.orderId,
|
|
252
|
+
salesChannelId,
|
|
253
|
+
});
|
|
254
|
+
return { issued: true, invoiceId: invoice.id, number: invoice.number, status: invoice.status };
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
//# sourceMappingURL=corrective-invoice.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"corrective-invoice.js","sourceRoot":"","sources":["../../../src/backend/services/corrective-invoice.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import type { EntityManager } from '@mikro-orm/postgresql';
|
|
2
|
+
import type { AssetReadPort, CustomerAccountReadPort, OrderRecord } from '@endora-commerce/contracts';
|
|
3
|
+
import type { LoadAssetImage } from '../pdf-components/embed-logo-images.js';
|
|
4
|
+
/**
|
|
5
|
+
* The three facts an invoice needs from outside this module
|
|
6
|
+
* (`specs/110-instance-repository/` T118c).
|
|
7
|
+
*
|
|
8
|
+
* All three were `invoicesBridge` members — closures a composition root wrote,
|
|
9
|
+
* once in `backend/src/composition.ts` and once in
|
|
10
|
+
* `backend/test/helpers/test-server.ts`, with the same body in each. They are
|
|
11
|
+
* functions of what they read rather than of a `ModuleContext`, so
|
|
12
|
+
* `backend/index.ts` supplies the real inputs and this file is testable over a
|
|
13
|
+
* stub with nothing composed.
|
|
14
|
+
*
|
|
15
|
+
* The other three members of that bridge needed no mapping at all and are not
|
|
16
|
+
* here: the acting admin and the calling customer are the platform's own
|
|
17
|
+
* `adminContextResolver` and `customerContextResolver`, read from the cradle,
|
|
18
|
+
* and the transactional sender is `transactional_emails`' published
|
|
19
|
+
* `transactionalEmailSenderAccessor`.
|
|
20
|
+
*/
|
|
21
|
+
/**
|
|
22
|
+
* Where the invoice e-mail goes.
|
|
23
|
+
*
|
|
24
|
+
* `customer_accounts` publishes the record; this maps it to the one field the
|
|
25
|
+
* dispatcher needs and answers `null` when no account resolves, which the
|
|
26
|
+
* dispatcher reports as `no_recipient` rather than treating as sent.
|
|
27
|
+
*
|
|
28
|
+
* **No `catch`, deliberately.** `findById` on a `lazyPort` proxy throws
|
|
29
|
+
* `ModuleDisabledError` when its owner is absent, and reading that as "this
|
|
30
|
+
* buyer has no e-mail address" is the fail-open composition checklist item 7
|
|
31
|
+
* refuses. `customer_accounts` declares `nonDeactivatable`, so it cannot happen
|
|
32
|
+
* today; the absence of the `catch` is what keeps it fail-closed if that ever
|
|
33
|
+
* changes.
|
|
34
|
+
*/
|
|
35
|
+
export declare function createRecipientEmailResolver(accounts: Pick<CustomerAccountReadPort, 'findById'>): (order: OrderRecord) => Promise<string | null>;
|
|
36
|
+
/**
|
|
37
|
+
* The language the invoice e-mail is rendered in.
|
|
38
|
+
*
|
|
39
|
+
* A read of the platform's own `SalesChannel` entity through this module's
|
|
40
|
+
* `EntityManager`, which is what both composition roots did and what `orders`
|
|
41
|
+
* and `returns` do at the identical site — resolving the language for a
|
|
42
|
+
* transactional e-mail.
|
|
43
|
+
*
|
|
44
|
+
* **Not `salesChannelResolutionPort`**, though the platform contributes it and
|
|
45
|
+
* its `CachedChannel` carries `defaultLanguage`. That port is the *cache*, and
|
|
46
|
+
* swapping a direct read for a cached one changes what this answers for a
|
|
47
|
+
* channel written outside the invalidation path — a design decision about
|
|
48
|
+
* channel reads rather than a drain's. Constitution XII is untouched either
|
|
49
|
+
* way: this resolves no request channel, it reads one attribute of the channel
|
|
50
|
+
* the invoice already names.
|
|
51
|
+
*
|
|
52
|
+
* `en-US` for an invoice with no channel, or a channel that is not there, is
|
|
53
|
+
* the fallback both roots carried.
|
|
54
|
+
*/
|
|
55
|
+
export declare function createChannelLanguageResolver(emFactory: () => EntityManager): (salesChannelId: string | null) => Promise<string>;
|
|
56
|
+
/**
|
|
57
|
+
* The operator's logo, as bytes pdfmake can inline.
|
|
58
|
+
*
|
|
59
|
+
* Two reads on one port, and the order is the point. `findById` is a metadata
|
|
60
|
+
* question and cheap; `openAssetBytes` moves the object. Asking the cheap one
|
|
61
|
+
* first is what keeps an asset that is not an image — a slot an operator can
|
|
62
|
+
* point at anything — from being streamed into this process before it is
|
|
63
|
+
* refused.
|
|
64
|
+
*
|
|
65
|
+
* **What is left here is this module's rule, and nothing about storage.** The
|
|
66
|
+
* root closure it replaces dispatched on `storageBackend`, knew that `legacy`
|
|
67
|
+
* has no `open`, knew that the locator falls back to `storageUrl`, and drained
|
|
68
|
+
* the stream itself; all four are `assets_library`' now, behind
|
|
69
|
+
* `openAssetBytes`. What cannot go there is which MIME types this consumer can
|
|
70
|
+
* use: pdfmake embeds PNG and JPEG and nothing else, which is
|
|
71
|
+
* `embed-logo-images.ts`' rule one layer down and is a fact about a PDF
|
|
72
|
+
* renderer rather than about an asset library.
|
|
73
|
+
*
|
|
74
|
+
* **No `catch`, and that is a change from the closure.** The root wrapped the
|
|
75
|
+
* storage half in one, because a backend that could not stream had to become an
|
|
76
|
+
* invoice without a logo rather than an error. That degrade is in
|
|
77
|
+
* `openAssetBytes`' return type now, where the owner can tell "this asset is
|
|
78
|
+
* not there" from "the bucket did not answer" — so the only throw that could
|
|
79
|
+
* still reach here is `ModuleDisabledError`, which must not be absorbed.
|
|
80
|
+
*/
|
|
81
|
+
export declare function createAssetImageLoader(assets: Pick<AssetReadPort, 'findById' | 'openAssetBytes'>): LoadAssetImage;
|
|
82
|
+
//# sourceMappingURL=cross-module-context.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cross-module-context.d.ts","sourceRoot":"","sources":["../../../src/backend/services/cross-module-context.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,uBAAuB,CAAC;AAC3D,OAAO,KAAK,EACV,aAAa,EACb,uBAAuB,EACvB,WAAW,EACZ,MAAM,4BAA4B,CAAC;AAEpC,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,wCAAwC,CAAC;AAE7E;;;;;;;;;;;;;;;;GAgBG;AAEH;;;;;;;;;;;;;GAaG;AACH,wBAAgB,4BAA4B,CAC1C,QAAQ,EAAE,IAAI,CAAC,uBAAuB,EAAE,UAAU,CAAC,GAClD,CAAC,KAAK,EAAE,WAAW,KAAK,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAGhD;AAED;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAgB,6BAA6B,CAC3C,SAAS,EAAE,MAAM,aAAa,GAC7B,CAAC,cAAc,EAAE,MAAM,GAAG,IAAI,KAAK,OAAO,CAAC,MAAM,CAAC,CAKpD;AAED;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AACH,wBAAgB,sBAAsB,CACpC,MAAM,EAAE,IAAI,CAAC,aAAa,EAAE,UAAU,GAAG,gBAAgB,CAAC,GACzD,cAAc,CAMhB"}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { SalesChannel } from '@endora-commerce/platform/kernel';
|
|
2
|
+
/**
|
|
3
|
+
* The three facts an invoice needs from outside this module
|
|
4
|
+
* (`specs/110-instance-repository/` T118c).
|
|
5
|
+
*
|
|
6
|
+
* All three were `invoicesBridge` members — closures a composition root wrote,
|
|
7
|
+
* once in `backend/src/composition.ts` and once in
|
|
8
|
+
* `backend/test/helpers/test-server.ts`, with the same body in each. They are
|
|
9
|
+
* functions of what they read rather than of a `ModuleContext`, so
|
|
10
|
+
* `backend/index.ts` supplies the real inputs and this file is testable over a
|
|
11
|
+
* stub with nothing composed.
|
|
12
|
+
*
|
|
13
|
+
* The other three members of that bridge needed no mapping at all and are not
|
|
14
|
+
* here: the acting admin and the calling customer are the platform's own
|
|
15
|
+
* `adminContextResolver` and `customerContextResolver`, read from the cradle,
|
|
16
|
+
* and the transactional sender is `transactional_emails`' published
|
|
17
|
+
* `transactionalEmailSenderAccessor`.
|
|
18
|
+
*/
|
|
19
|
+
/**
|
|
20
|
+
* Where the invoice e-mail goes.
|
|
21
|
+
*
|
|
22
|
+
* `customer_accounts` publishes the record; this maps it to the one field the
|
|
23
|
+
* dispatcher needs and answers `null` when no account resolves, which the
|
|
24
|
+
* dispatcher reports as `no_recipient` rather than treating as sent.
|
|
25
|
+
*
|
|
26
|
+
* **No `catch`, deliberately.** `findById` on a `lazyPort` proxy throws
|
|
27
|
+
* `ModuleDisabledError` when its owner is absent, and reading that as "this
|
|
28
|
+
* buyer has no e-mail address" is the fail-open composition checklist item 7
|
|
29
|
+
* refuses. `customer_accounts` declares `nonDeactivatable`, so it cannot happen
|
|
30
|
+
* today; the absence of the `catch` is what keeps it fail-closed if that ever
|
|
31
|
+
* changes.
|
|
32
|
+
*/
|
|
33
|
+
export function createRecipientEmailResolver(accounts) {
|
|
34
|
+
return async (order) => (await accounts.findById(order.placedByCustomerAccountId))?.email ?? null;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* The language the invoice e-mail is rendered in.
|
|
38
|
+
*
|
|
39
|
+
* A read of the platform's own `SalesChannel` entity through this module's
|
|
40
|
+
* `EntityManager`, which is what both composition roots did and what `orders`
|
|
41
|
+
* and `returns` do at the identical site — resolving the language for a
|
|
42
|
+
* transactional e-mail.
|
|
43
|
+
*
|
|
44
|
+
* **Not `salesChannelResolutionPort`**, though the platform contributes it and
|
|
45
|
+
* its `CachedChannel` carries `defaultLanguage`. That port is the *cache*, and
|
|
46
|
+
* swapping a direct read for a cached one changes what this answers for a
|
|
47
|
+
* channel written outside the invalidation path — a design decision about
|
|
48
|
+
* channel reads rather than a drain's. Constitution XII is untouched either
|
|
49
|
+
* way: this resolves no request channel, it reads one attribute of the channel
|
|
50
|
+
* the invoice already names.
|
|
51
|
+
*
|
|
52
|
+
* `en-US` for an invoice with no channel, or a channel that is not there, is
|
|
53
|
+
* the fallback both roots carried.
|
|
54
|
+
*/
|
|
55
|
+
export function createChannelLanguageResolver(emFactory) {
|
|
56
|
+
return async (salesChannelId) => (salesChannelId
|
|
57
|
+
? (await emFactory().findOne(SalesChannel, { id: salesChannelId }))?.defaultLanguage
|
|
58
|
+
: null) ?? 'en-US';
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* The operator's logo, as bytes pdfmake can inline.
|
|
62
|
+
*
|
|
63
|
+
* Two reads on one port, and the order is the point. `findById` is a metadata
|
|
64
|
+
* question and cheap; `openAssetBytes` moves the object. Asking the cheap one
|
|
65
|
+
* first is what keeps an asset that is not an image — a slot an operator can
|
|
66
|
+
* point at anything — from being streamed into this process before it is
|
|
67
|
+
* refused.
|
|
68
|
+
*
|
|
69
|
+
* **What is left here is this module's rule, and nothing about storage.** The
|
|
70
|
+
* root closure it replaces dispatched on `storageBackend`, knew that `legacy`
|
|
71
|
+
* has no `open`, knew that the locator falls back to `storageUrl`, and drained
|
|
72
|
+
* the stream itself; all four are `assets_library`' now, behind
|
|
73
|
+
* `openAssetBytes`. What cannot go there is which MIME types this consumer can
|
|
74
|
+
* use: pdfmake embeds PNG and JPEG and nothing else, which is
|
|
75
|
+
* `embed-logo-images.ts`' rule one layer down and is a fact about a PDF
|
|
76
|
+
* renderer rather than about an asset library.
|
|
77
|
+
*
|
|
78
|
+
* **No `catch`, and that is a change from the closure.** The root wrapped the
|
|
79
|
+
* storage half in one, because a backend that could not stream had to become an
|
|
80
|
+
* invoice without a logo rather than an error. That degrade is in
|
|
81
|
+
* `openAssetBytes`' return type now, where the owner can tell "this asset is
|
|
82
|
+
* not there" from "the bucket did not answer" — so the only throw that could
|
|
83
|
+
* still reach here is `ModuleDisabledError`, which must not be absorbed.
|
|
84
|
+
*/
|
|
85
|
+
export function createAssetImageLoader(assets) {
|
|
86
|
+
return async (assetId) => {
|
|
87
|
+
const record = await assets.findById(assetId, { liveOnly: true });
|
|
88
|
+
if (!record || !record.mimeType.startsWith('image/'))
|
|
89
|
+
return null;
|
|
90
|
+
return assets.openAssetBytes(assetId);
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
//# sourceMappingURL=cross-module-context.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cross-module-context.js","sourceRoot":"","sources":["../../../src/backend/services/cross-module-context.ts"],"names":[],"mappings":"AAMA,OAAO,EAAE,YAAY,EAAE,MAAM,kCAAkC,CAAC;AAGhE;;;;;;;;;;;;;;;;GAgBG;AAEH;;;;;;;;;;;;;GAaG;AACH,MAAM,UAAU,4BAA4B,CAC1C,QAAmD;IAEnD,OAAO,KAAK,EAAE,KAAK,EAAE,EAAE,CACrB,CAAC,MAAM,QAAQ,CAAC,QAAQ,CAAC,KAAK,CAAC,yBAAyB,CAAC,CAAC,EAAE,KAAK,IAAI,IAAI,CAAC;AAC9E,CAAC;AAED;;;;;;;;;;;;;;;;;;GAkBG;AACH,MAAM,UAAU,6BAA6B,CAC3C,SAA8B;IAE9B,OAAO,KAAK,EAAE,cAAc,EAAE,EAAE,CAC9B,CAAC,cAAc;QACb,CAAC,CAAC,CAAC,MAAM,SAAS,EAAE,CAAC,OAAO,CAAC,YAAY,EAAE,EAAE,EAAE,EAAE,cAAc,EAAE,CAAC,CAAC,EAAE,eAAe;QACpF,CAAC,CAAC,IAAI,CAAC,IAAI,OAAO,CAAC;AACzB,CAAC;AAED;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AACH,MAAM,UAAU,sBAAsB,CACpC,MAA0D;IAE1D,OAAO,KAAK,EAAE,OAAO,EAAE,EAAE;QACvB,MAAM,MAAM,GAAG,MAAM,MAAM,CAAC,QAAQ,CAAC,OAAO,EAAE,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC,CAAC;QAClE,IAAI,CAAC,MAAM,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,UAAU,CAAC,QAAQ,CAAC;YAAE,OAAO,IAAI,CAAC;QAClE,OAAO,MAAM,CAAC,cAAc,CAAC,OAAO,CAAC,CAAC;IACxC,CAAC,CAAC;AACJ,CAAC"}
|