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