@endora-commerce/mod-transactional-emails 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 (151) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +63 -0
  3. package/dist/admin/api/transactional-emails-client.d.ts +43 -0
  4. package/dist/admin/api/transactional-emails-client.d.ts.map +1 -0
  5. package/dist/admin/api/transactional-emails-client.js +47 -0
  6. package/dist/admin/api/transactional-emails-client.js.map +1 -0
  7. package/dist/admin/components/BrandingPanel.d.ts +10 -0
  8. package/dist/admin/components/BrandingPanel.d.ts.map +1 -0
  9. package/dist/admin/components/BrandingPanel.js +58 -0
  10. package/dist/admin/components/BrandingPanel.js.map +1 -0
  11. package/dist/admin/components/EmailEditorPane.d.ts +7 -0
  12. package/dist/admin/components/EmailEditorPane.d.ts.map +1 -0
  13. package/dist/admin/components/EmailEditorPane.js +48 -0
  14. package/dist/admin/components/EmailEditorPane.js.map +1 -0
  15. package/dist/admin/index.d.ts +46 -0
  16. package/dist/admin/index.d.ts.map +1 -0
  17. package/dist/admin/index.js +92 -0
  18. package/dist/admin/index.js.map +1 -0
  19. package/dist/admin/pages/EmailBlockEditorPage.d.ts +16 -0
  20. package/dist/admin/pages/EmailBlockEditorPage.d.ts.map +1 -0
  21. package/dist/admin/pages/EmailBlockEditorPage.js +19 -0
  22. package/dist/admin/pages/EmailBlockEditorPage.js.map +1 -0
  23. package/dist/admin/pages/EmailBlocksPage.d.ts +8 -0
  24. package/dist/admin/pages/EmailBlocksPage.d.ts.map +1 -0
  25. package/dist/admin/pages/EmailBlocksPage.js +55 -0
  26. package/dist/admin/pages/EmailBlocksPage.js.map +1 -0
  27. package/dist/admin/pages/EmailEditor.d.ts +8 -0
  28. package/dist/admin/pages/EmailEditor.d.ts.map +1 -0
  29. package/dist/admin/pages/EmailEditor.js +126 -0
  30. package/dist/admin/pages/EmailEditor.js.map +1 -0
  31. package/dist/admin/pages/EmailFragmentEditor.d.ts +9 -0
  32. package/dist/admin/pages/EmailFragmentEditor.d.ts.map +1 -0
  33. package/dist/admin/pages/EmailFragmentEditor.js +93 -0
  34. package/dist/admin/pages/EmailFragmentEditor.js.map +1 -0
  35. package/dist/admin/pages/EmailTemplateEditorPage.d.ts +12 -0
  36. package/dist/admin/pages/EmailTemplateEditorPage.d.ts.map +1 -0
  37. package/dist/admin/pages/EmailTemplateEditorPage.js +15 -0
  38. package/dist/admin/pages/EmailTemplateEditorPage.js.map +1 -0
  39. package/dist/admin/pages/EmailTemplatesPage.d.ts +8 -0
  40. package/dist/admin/pages/EmailTemplatesPage.d.ts.map +1 -0
  41. package/dist/admin/pages/EmailTemplatesPage.js +55 -0
  42. package/dist/admin/pages/EmailTemplatesPage.js.map +1 -0
  43. package/dist/admin/pages/EmailsList.d.ts +8 -0
  44. package/dist/admin/pages/EmailsList.d.ts.map +1 -0
  45. package/dist/admin/pages/EmailsList.js +85 -0
  46. package/dist/admin/pages/EmailsList.js.map +1 -0
  47. package/dist/backend/commands/email-activation.commands.d.ts +42 -0
  48. package/dist/backend/commands/email-activation.commands.d.ts.map +1 -0
  49. package/dist/backend/commands/email-activation.commands.js +51 -0
  50. package/dist/backend/commands/email-activation.commands.js.map +1 -0
  51. package/dist/backend/entities/email-block-sales-channel.entity.d.ts +11 -0
  52. package/dist/backend/entities/email-block-sales-channel.entity.d.ts.map +1 -0
  53. package/dist/backend/entities/email-block-sales-channel.entity.js +39 -0
  54. package/dist/backend/entities/email-block-sales-channel.entity.js.map +1 -0
  55. package/dist/backend/entities/email-block.entity.d.ts +24 -0
  56. package/dist/backend/entities/email-block.entity.d.ts.map +1 -0
  57. package/dist/backend/entities/email-block.entity.js +85 -0
  58. package/dist/backend/entities/email-block.entity.js.map +1 -0
  59. package/dist/backend/entities/email-template-sales-channel.entity.d.ts +10 -0
  60. package/dist/backend/entities/email-template-sales-channel.entity.d.ts.map +1 -0
  61. package/dist/backend/entities/email-template-sales-channel.entity.js +38 -0
  62. package/dist/backend/entities/email-template-sales-channel.entity.js.map +1 -0
  63. package/dist/backend/entities/email-template.entity.d.ts +19 -0
  64. package/dist/backend/entities/email-template.entity.d.ts.map +1 -0
  65. package/dist/backend/entities/email-template.entity.js +76 -0
  66. package/dist/backend/entities/email-template.entity.js.map +1 -0
  67. package/dist/backend/entities/transactional-email-content.entity.d.ts +25 -0
  68. package/dist/backend/entities/transactional-email-content.entity.d.ts.map +1 -0
  69. package/dist/backend/entities/transactional-email-content.entity.js +77 -0
  70. package/dist/backend/entities/transactional-email-content.entity.js.map +1 -0
  71. package/dist/backend/entities/transactional-email.entity.d.ts +32 -0
  72. package/dist/backend/entities/transactional-email.entity.d.ts.map +1 -0
  73. package/dist/backend/entities/transactional-email.entity.js +100 -0
  74. package/dist/backend/entities/transactional-email.entity.js.map +1 -0
  75. package/dist/backend/index.d.ts +105 -0
  76. package/dist/backend/index.d.ts.map +1 -0
  77. package/dist/backend/index.js +126 -0
  78. package/dist/backend/index.js.map +1 -0
  79. package/dist/backend/plugin.d.ts +49 -0
  80. package/dist/backend/plugin.d.ts.map +1 -0
  81. package/dist/backend/plugin.js +54 -0
  82. package/dist/backend/plugin.js.map +1 -0
  83. package/dist/backend/routes.admin.d.ts +23 -0
  84. package/dist/backend/routes.admin.d.ts.map +1 -0
  85. package/dist/backend/routes.admin.js +133 -0
  86. package/dist/backend/routes.admin.js.map +1 -0
  87. package/dist/backend/services/branding.service.d.ts +63 -0
  88. package/dist/backend/services/branding.service.d.ts.map +1 -0
  89. package/dist/backend/services/branding.service.js +119 -0
  90. package/dist/backend/services/branding.service.js.map +1 -0
  91. package/dist/backend/services/content-resolver.d.ts +28 -0
  92. package/dist/backend/services/content-resolver.d.ts.map +1 -0
  93. package/dist/backend/services/content-resolver.js +64 -0
  94. package/dist/backend/services/content-resolver.js.map +1 -0
  95. package/dist/backend/services/email-block.service.d.ts +25 -0
  96. package/dist/backend/services/email-block.service.d.ts.map +1 -0
  97. package/dist/backend/services/email-block.service.js +162 -0
  98. package/dist/backend/services/email-block.service.js.map +1 -0
  99. package/dist/backend/services/email-builder-registry.d.ts +9 -0
  100. package/dist/backend/services/email-builder-registry.d.ts.map +1 -0
  101. package/dist/backend/services/email-builder-registry.js +14 -0
  102. package/dist/backend/services/email-builder-registry.js.map +1 -0
  103. package/dist/backend/services/email-defaults-registry.d.ts +63 -0
  104. package/dist/backend/services/email-defaults-registry.d.ts.map +1 -0
  105. package/dist/backend/services/email-defaults-registry.js +60 -0
  106. package/dist/backend/services/email-defaults-registry.js.map +1 -0
  107. package/dist/backend/services/email-template.service.d.ts +23 -0
  108. package/dist/backend/services/email-template.service.d.ts.map +1 -0
  109. package/dist/backend/services/email-template.service.js +155 -0
  110. package/dist/backend/services/email-template.service.js.map +1 -0
  111. package/dist/backend/services/embed-resolver.d.ts +21 -0
  112. package/dist/backend/services/embed-resolver.d.ts.map +1 -0
  113. package/dist/backend/services/embed-resolver.js +68 -0
  114. package/dist/backend/services/embed-resolver.js.map +1 -0
  115. package/dist/backend/services/manifest-reconciler.d.ts +28 -0
  116. package/dist/backend/services/manifest-reconciler.d.ts.map +1 -0
  117. package/dist/backend/services/manifest-reconciler.js +98 -0
  118. package/dist/backend/services/manifest-reconciler.js.map +1 -0
  119. package/dist/backend/services/template-email.d.ts +26 -0
  120. package/dist/backend/services/template-email.d.ts.map +1 -0
  121. package/dist/backend/services/template-email.js +69 -0
  122. package/dist/backend/services/template-email.js.map +1 -0
  123. package/dist/backend/services/transactional-email.service.d.ts +138 -0
  124. package/dist/backend/services/transactional-email.service.d.ts.map +1 -0
  125. package/dist/backend/services/transactional-email.service.js +410 -0
  126. package/dist/backend/services/transactional-email.service.js.map +1 -0
  127. package/dist/manifest.d.ts +195 -0
  128. package/dist/manifest.d.ts.map +1 -0
  129. package/dist/manifest.js +367 -0
  130. package/dist/manifest.js.map +1 -0
  131. package/dist/migrations/20260629T113442_transactional_emails_init.d.ts +6 -0
  132. package/dist/migrations/20260629T113442_transactional_emails_init.d.ts.map +1 -0
  133. package/dist/migrations/20260629T113442_transactional_emails_init.js +133 -0
  134. package/dist/migrations/20260629T113442_transactional_emails_init.js.map +1 -0
  135. package/dist/migrations/20260801T111001_transactional_emails_email_defaults_reseed.d.ts +6 -0
  136. package/dist/migrations/20260801T111001_transactional_emails_email_defaults_reseed.d.ts.map +1 -0
  137. package/dist/migrations/20260801T111001_transactional_emails_email_defaults_reseed.js +49 -0
  138. package/dist/migrations/20260801T111001_transactional_emails_email_defaults_reseed.js.map +1 -0
  139. package/dist/migrations/20260903T101748_transactional_emails_namespace_block_names.d.ts +6 -0
  140. package/dist/migrations/20260903T101748_transactional_emails_namespace_block_names.d.ts.map +1 -0
  141. package/dist/migrations/20260903T101748_transactional_emails_namespace_block_names.js +60 -0
  142. package/dist/migrations/20260903T101748_transactional_emails_namespace_block_names.js.map +1 -0
  143. package/dist/migrations/index.d.ts +29 -0
  144. package/dist/migrations/index.d.ts.map +1 -0
  145. package/dist/migrations/index.js +33 -0
  146. package/dist/migrations/index.js.map +1 -0
  147. package/docs/transactional-emails.md +176 -0
  148. package/i18n/en.json +92 -0
  149. package/i18n/pl.json +92 -0
  150. package/package.json +113 -0
  151. package/tailwind.css +14 -0
@@ -0,0 +1,155 @@
1
+ /**
2
+ * EmailTemplateService — feature 047 (US3). CRUD for reusable email-safe
3
+ * templates. Same surface as EmailBlockService without an `active` flag.
4
+ */
5
+ import { ERROR_CODES, } from '@endora-commerce/contracts';
6
+ import { EMAIL_SAFE_COMPONENT_NAMES } from '@endora-commerce/email-components/schema/component-types';
7
+ import { walkUnknownComponents } from '@endora-commerce/email-components/tree/walk-embeds';
8
+ import { HttpError } from '@endora-commerce/platform/http';
9
+ import { recordAuditFromContext } from '@endora-commerce/platform/commands';
10
+ import { EmailTemplate } from '../entities/email-template.entity.js';
11
+ import { EmailTemplateSalesChannel } from '../entities/email-template-sales-channel.entity.js';
12
+ const KNOWN = new Set(EMAIL_SAFE_COMPONENT_NAMES);
13
+ function validateTree(content) {
14
+ const unknown = walkUnknownComponents(content, KNOWN);
15
+ if (unknown.size > 0) {
16
+ throw new HttpError(400, ERROR_CODES.VALIDATION_FAILED, `Non-email-safe components: ${[...unknown].join(', ')}`);
17
+ }
18
+ }
19
+ export class EmailTemplateService {
20
+ emFactory;
21
+ auditLog;
22
+ constructor(emFactory, auditLog) {
23
+ this.emFactory = emFactory;
24
+ this.auditLog = auditLog;
25
+ }
26
+ #audit(em, action, objectId, stateBefore, stateAfter) {
27
+ if (this.auditLog) {
28
+ recordAuditFromContext(this.auditLog, em, {
29
+ action,
30
+ objectType: 'email_template',
31
+ objectId,
32
+ stateBefore,
33
+ stateAfter,
34
+ });
35
+ }
36
+ }
37
+ async channelIds(em, templateId) {
38
+ const rows = await em.find(EmailTemplateSalesChannel, { templateId });
39
+ return rows.map((r) => r.salesChannelId);
40
+ }
41
+ async summary(em, t) {
42
+ const ids = await this.channelIds(em, t.id);
43
+ return {
44
+ id: t.id,
45
+ code: t.code,
46
+ name: t.name,
47
+ isSystem: t.isSystem,
48
+ scope: ids.length > 0 ? 'channel' : 'global',
49
+ languages: t.languages,
50
+ version: t.version,
51
+ };
52
+ }
53
+ async list(salesChannelId) {
54
+ const em = this.emFactory();
55
+ const templates = await em.find(EmailTemplate, {}, { orderBy: { name: 'asc' } });
56
+ const out = [];
57
+ for (const t of templates) {
58
+ const s = await this.summary(em, t);
59
+ if (salesChannelId && s.scope === 'channel') {
60
+ const ids = await this.channelIds(em, t.id);
61
+ if (!ids.includes(salesChannelId))
62
+ continue;
63
+ }
64
+ out.push(s);
65
+ }
66
+ return out;
67
+ }
68
+ async loadOrThrow(em, id) {
69
+ const t = await em.findOne(EmailTemplate, { id });
70
+ if (!t)
71
+ throw new HttpError(404, ERROR_CODES.NOT_FOUND, `Email template ${id} not found.`);
72
+ return t;
73
+ }
74
+ async get(id) {
75
+ const em = this.emFactory();
76
+ const t = await this.loadOrThrow(em, id);
77
+ const ids = await this.channelIds(em, t.id);
78
+ return {
79
+ ...(await this.summary(em, t)),
80
+ description: t.description,
81
+ content: t.content.languages ?? {},
82
+ salesChannelIds: ids,
83
+ };
84
+ }
85
+ async create(req) {
86
+ const em = this.emFactory();
87
+ const existing = await em.findOne(EmailTemplate, { code: req.code });
88
+ if (existing)
89
+ throw new HttpError(409, ERROR_CODES.VERSION_CONFLICT, `Template code "${req.code}" already exists.`);
90
+ const tpl = em.create(EmailTemplate, {
91
+ code: req.code,
92
+ name: req.name,
93
+ description: req.description ?? null,
94
+ content: { schema_version: 1, languages: {} },
95
+ languages: req.languages,
96
+ isSystem: false,
97
+ version: 1,
98
+ });
99
+ await em.persistAndFlush(tpl);
100
+ for (const scId of req.salesChannelIds ?? []) {
101
+ em.persist(em.create(EmailTemplateSalesChannel, { templateId: tpl.id, salesChannelId: scId, code: tpl.code }));
102
+ }
103
+ this.#audit(em, 'email_template.create', tpl.id, null, { code: tpl.code, name: tpl.name });
104
+ await em.flush();
105
+ return this.get(tpl.id);
106
+ }
107
+ async patch(id, req) {
108
+ const em = this.emFactory();
109
+ const t = await this.loadOrThrow(em, id);
110
+ if (req.expectedVersion !== undefined && req.expectedVersion !== t.version) {
111
+ throw new HttpError(409, ERROR_CODES.VERSION_CONFLICT, 'Template was modified by someone else.');
112
+ }
113
+ if (req.name !== undefined)
114
+ t.name = req.name;
115
+ if (req.description !== undefined)
116
+ t.description = req.description;
117
+ if (req.salesChannelIds !== undefined && !t.isSystem) {
118
+ const current = await em.find(EmailTemplateSalesChannel, { templateId: t.id });
119
+ await em.removeAndFlush(current);
120
+ for (const scId of req.salesChannelIds) {
121
+ em.persist(em.create(EmailTemplateSalesChannel, { templateId: t.id, salesChannelId: scId, code: t.code }));
122
+ }
123
+ }
124
+ this.#audit(em, 'email_template.update', t.id, null, { code: t.code, name: t.name });
125
+ await em.flush();
126
+ return this.get(id);
127
+ }
128
+ async setContent(id, language, req) {
129
+ validateTree(req.content);
130
+ const em = this.emFactory();
131
+ const t = await this.loadOrThrow(em, id);
132
+ if (req.expectedVersion !== t.version) {
133
+ throw new HttpError(409, ERROR_CODES.VERSION_CONFLICT, 'Template was modified by someone else.');
134
+ }
135
+ const env = t.content ?? {};
136
+ const languages = { ...(env.languages ?? {}), [language]: req.content };
137
+ t.content = { schema_version: env.schema_version ?? 1, languages };
138
+ if (!t.languages.includes(language))
139
+ t.languages = [...t.languages, language];
140
+ t.version += 1;
141
+ this.#audit(em, 'email_template.set_content', t.id, null, { language, version: t.version });
142
+ await em.flush();
143
+ return this.get(id);
144
+ }
145
+ async delete(id) {
146
+ const em = this.emFactory();
147
+ const t = await this.loadOrThrow(em, id);
148
+ if (t.isSystem)
149
+ throw new HttpError(409, ERROR_CODES.VALIDATION_FAILED, 'System templates cannot be deleted.');
150
+ const bridges = await em.find(EmailTemplateSalesChannel, { templateId: t.id });
151
+ this.#audit(em, 'email_template.delete', t.id, { code: t.code }, null);
152
+ await em.removeAndFlush([...bridges, t]);
153
+ }
154
+ }
155
+ //# sourceMappingURL=email-template.service.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"email-template.service.js","sourceRoot":"","sources":["../../../src/backend/services/email-template.service.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,21 @@
1
+ /**
2
+ * EmbedResolver — feature 047 (R4). Resolves email block/template embeds
3
+ * referenced in a content tree to their single-language Puck trees, preferring
4
+ * channel-scoped rows over global ones. Used by the send + preview render path.
5
+ */
6
+ import type { EntityManager } from '@mikro-orm/postgresql';
7
+ import type { PuckDataTree } from '@endora-commerce/email-components/schema/envelope';
8
+ export interface ResolvedEmbeds {
9
+ blocks: Record<string, PuckDataTree>;
10
+ templates: Record<string, PuckDataTree>;
11
+ }
12
+ export declare class EmbedResolver {
13
+ resolve(em: EntityManager, rootTree: PuckDataTree, opts: {
14
+ salesChannelId: string | null;
15
+ language: string;
16
+ fallbackLanguage?: string;
17
+ }): Promise<ResolvedEmbeds>;
18
+ private pickBlock;
19
+ private pickTemplate;
20
+ }
21
+ //# sourceMappingURL=embed-resolver.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"embed-resolver.d.ts","sourceRoot":"","sources":["../../../src/backend/services/embed-resolver.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,uBAAuB,CAAC;AAC3D,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,mDAAmD,CAAC;AAOtF,MAAM,WAAW,cAAc;IAC7B,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,YAAY,CAAC,CAAC;IACrC,SAAS,EAAE,MAAM,CAAC,MAAM,EAAE,YAAY,CAAC,CAAC;CACzC;AAUD,qBAAa,aAAa;IAClB,OAAO,CACX,EAAE,EAAE,aAAa,EACjB,QAAQ,EAAE,YAAY,EACtB,IAAI,EAAE;QAAE,cAAc,EAAE,MAAM,GAAG,IAAI,CAAC;QAAC,QAAQ,EAAE,MAAM,CAAC;QAAC,gBAAgB,CAAC,EAAE,MAAM,CAAA;KAAE,GACnF,OAAO,CAAC,cAAc,CAAC;YAuBZ,SAAS;YAYT,YAAY;CAU3B"}
@@ -0,0 +1,68 @@
1
+ /**
2
+ * EmbedResolver — feature 047 (R4). Resolves email block/template embeds
3
+ * referenced in a content tree to their single-language Puck trees, preferring
4
+ * channel-scoped rows over global ones. Used by the send + preview render path.
5
+ */
6
+ import { walkBlockEmbeds, walkTemplateEmbeds } from '@endora-commerce/email-components/tree/walk-embeds';
7
+ import { EmailBlock } from '../entities/email-block.entity.js';
8
+ import { EmailTemplate } from '../entities/email-template.entity.js';
9
+ import { EmailBlockSalesChannel } from '../entities/email-block-sales-channel.entity.js';
10
+ import { EmailTemplateSalesChannel } from '../entities/email-template-sales-channel.entity.js';
11
+ function treeForLanguage(content, language, fallback) {
12
+ if (!content || typeof content !== 'object')
13
+ return null;
14
+ const langs = content.languages;
15
+ if (!langs)
16
+ return null;
17
+ const tree = langs[language] ?? (fallback ? langs[fallback] : undefined);
18
+ return tree && typeof tree === 'object' ? tree : null;
19
+ }
20
+ export class EmbedResolver {
21
+ async resolve(em, rootTree, opts) {
22
+ const blockCodes = walkBlockEmbeds(rootTree);
23
+ const templateCodes = walkTemplateEmbeds(rootTree);
24
+ const blocks = {};
25
+ const templates = {};
26
+ for (const code of blockCodes) {
27
+ const block = await this.pickBlock(em, code, opts.salesChannelId);
28
+ if (block) {
29
+ const tree = treeForLanguage(block.content, opts.language, opts.fallbackLanguage);
30
+ if (tree)
31
+ blocks[code] = tree;
32
+ }
33
+ }
34
+ for (const code of templateCodes) {
35
+ const tpl = await this.pickTemplate(em, code, opts.salesChannelId);
36
+ if (tpl) {
37
+ const tree = treeForLanguage(tpl.content, opts.language, opts.fallbackLanguage);
38
+ if (tree)
39
+ templates[code] = tree;
40
+ }
41
+ }
42
+ return { blocks, templates };
43
+ }
44
+ async pickBlock(em, code, salesChannelId) {
45
+ if (salesChannelId) {
46
+ const bridge = await em.findOne(EmailBlockSalesChannel, { code, salesChannelId });
47
+ if (bridge) {
48
+ const scoped = await em.findOne(EmailBlock, { id: bridge.blockId });
49
+ if (scoped)
50
+ return scoped;
51
+ }
52
+ }
53
+ // Global block: a block with no bridge rows. Resolve by code, prefer active.
54
+ return em.findOne(EmailBlock, { code, active: true });
55
+ }
56
+ async pickTemplate(em, code, salesChannelId) {
57
+ if (salesChannelId) {
58
+ const bridge = await em.findOne(EmailTemplateSalesChannel, { code, salesChannelId });
59
+ if (bridge) {
60
+ const scoped = await em.findOne(EmailTemplate, { id: bridge.templateId });
61
+ if (scoped)
62
+ return scoped;
63
+ }
64
+ }
65
+ return em.findOne(EmailTemplate, { code });
66
+ }
67
+ }
68
+ //# sourceMappingURL=embed-resolver.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"embed-resolver.js","sourceRoot":"","sources":["../../../src/backend/services/embed-resolver.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,28 @@
1
+ /**
2
+ * TransactionalEmailReconciler — feature 047 (R6/US1/US5).
3
+ *
4
+ * At boot, upserts `transactional_emails` rows from module manifests +
5
+ * registered defaults. Refreshes module-owned columns (name, variables,
6
+ * defaults, ...) but NEVER touches `transactional_email_contents`, so admin
7
+ * customizations are preserved across module-default updates (FR-027). Reports
8
+ * a code collision when two modules declare the same email code (FR-006).
9
+ */
10
+ import type { EntityManager } from '@mikro-orm/postgresql';
11
+ import type { ModuleManifest } from '@endora-commerce/contracts';
12
+ import type { EmailDefaultsRegistry } from './email-defaults-registry.js';
13
+ export declare class TransactionalEmailCodeCollision extends Error {
14
+ constructor(code: string, modules: string[]);
15
+ }
16
+ export interface ReconcileResult {
17
+ created: number;
18
+ updated: number;
19
+ pruned: number;
20
+ }
21
+ export declare class TransactionalEmailReconciler {
22
+ private readonly emFactory;
23
+ private readonly defaults;
24
+ private readonly fallbackLanguages;
25
+ constructor(emFactory: () => EntityManager, defaults: EmailDefaultsRegistry, fallbackLanguages?: readonly string[]);
26
+ reconcile(manifests: ReadonlyArray<ModuleManifest>): Promise<ReconcileResult>;
27
+ }
28
+ //# sourceMappingURL=manifest-reconciler.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"manifest-reconciler.d.ts","sourceRoot":"","sources":["../../../src/backend/services/manifest-reconciler.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,uBAAuB,CAAC;AAC3D,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,4BAA4B,CAAC;AAEjE,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,8BAA8B,CAAC;AAE1E,qBAAa,+BAAgC,SAAQ,KAAK;gBAC5C,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE;CAI5C;AAED,MAAM,WAAW,eAAe;IAC9B,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE,MAAM,CAAC;CAChB;AAED,qBAAa,4BAA4B;IAErC,OAAO,CAAC,QAAQ,CAAC,SAAS;IAC1B,OAAO,CAAC,QAAQ,CAAC,QAAQ;IACzB,OAAO,CAAC,QAAQ,CAAC,iBAAiB;gBAFjB,SAAS,EAAE,MAAM,aAAa,EAC9B,QAAQ,EAAE,qBAAqB,EAC/B,iBAAiB,GAAE,SAAS,MAAM,EAAc;IAG7D,SAAS,CAAC,SAAS,EAAE,aAAa,CAAC,cAAc,CAAC,GAAG,OAAO,CAAC,eAAe,CAAC;CA4EpF"}
@@ -0,0 +1,98 @@
1
+ /**
2
+ * TransactionalEmailReconciler — feature 047 (R6/US1/US5).
3
+ *
4
+ * At boot, upserts `transactional_emails` rows from module manifests +
5
+ * registered defaults. Refreshes module-owned columns (name, variables,
6
+ * defaults, ...) but NEVER touches `transactional_email_contents`, so admin
7
+ * customizations are preserved across module-default updates (FR-027). Reports
8
+ * a code collision when two modules declare the same email code (FR-006).
9
+ */
10
+ import { TransactionalEmail } from '../entities/transactional-email.entity.js';
11
+ export class TransactionalEmailCodeCollision extends Error {
12
+ constructor(code, modules) {
13
+ super(`Transactional email code "${code}" declared by multiple modules: ${modules.join(', ')}`);
14
+ this.name = 'TransactionalEmailCodeCollision';
15
+ }
16
+ }
17
+ export class TransactionalEmailReconciler {
18
+ emFactory;
19
+ defaults;
20
+ fallbackLanguages;
21
+ constructor(emFactory, defaults, fallbackLanguages = ['en-US']) {
22
+ this.emFactory = emFactory;
23
+ this.defaults = defaults;
24
+ this.fallbackLanguages = fallbackLanguages;
25
+ }
26
+ async reconcile(manifests) {
27
+ // command-coverage-ignore: idempotent boot reconciler — seeds/updates the
28
+ // module-declared email blocks/templates from manifests; a system bootstrap,
29
+ // not an operator-initiated write.
30
+ const em = this.emFactory();
31
+ let created = 0;
32
+ let updated = 0;
33
+ // Detect cross-module code collisions first.
34
+ const owners = new Map();
35
+ for (const m of manifests) {
36
+ for (const entry of m.transactionalEmails ?? []) {
37
+ const list = owners.get(entry.code) ?? [];
38
+ list.push(m.id);
39
+ owners.set(entry.code, list);
40
+ }
41
+ }
42
+ for (const [code, mods] of owners) {
43
+ if (mods.length > 1)
44
+ throw new TransactionalEmailCodeCollision(code, mods);
45
+ }
46
+ for (const m of manifests) {
47
+ for (const entry of m.transactionalEmails ?? []) {
48
+ const def = this.defaults.get(entry.code);
49
+ const languages = def
50
+ ? Object.keys(def.defaultContent && typeof def.defaultContent === 'object'
51
+ ? (def.defaultContent.languages ?? {})
52
+ : {})
53
+ : [];
54
+ const resolvedLanguages = languages.length > 0 ? languages : [...this.fallbackLanguages];
55
+ let row = await em.findOne(TransactionalEmail, { code: entry.code });
56
+ if (!row) {
57
+ row = em.create(TransactionalEmail, {
58
+ code: entry.code,
59
+ name: entry.name,
60
+ ownerModule: m.id,
61
+ description: entry.description ?? null,
62
+ groupCode: entry.group ?? null,
63
+ variables: entry.variables ?? [],
64
+ languages: resolvedLanguages,
65
+ defaultSubject: def?.defaultSubject ?? {},
66
+ defaultContent: def?.defaultContent ?? { schema_version: 1, languages: {} },
67
+ active: true,
68
+ });
69
+ created += 1;
70
+ }
71
+ else {
72
+ row.name = entry.name;
73
+ row.ownerModule = m.id;
74
+ row.description = entry.description ?? null;
75
+ row.groupCode = entry.group ?? null;
76
+ row.variables = entry.variables ?? [];
77
+ row.languages = resolvedLanguages;
78
+ if (def) {
79
+ row.defaultSubject = def.defaultSubject;
80
+ row.defaultContent = def.defaultContent;
81
+ }
82
+ updated += 1;
83
+ }
84
+ }
85
+ }
86
+ await em.flush();
87
+ // Prune orphans (FR-028): definitions whose owning module is no longer
88
+ // installed / no longer declares the code. All rows are reconciler-owned, so
89
+ // anything not currently declared is safe to remove; the FK cascade drops
90
+ // the associated admin customizations.
91
+ const declaredCodes = [...owners.keys()];
92
+ const pruned = declaredCodes.length > 0
93
+ ? await em.nativeDelete(TransactionalEmail, { code: { $nin: declaredCodes } })
94
+ : await em.nativeDelete(TransactionalEmail, {});
95
+ return { created, updated, pruned };
96
+ }
97
+ }
98
+ //# sourceMappingURL=manifest-reconciler.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"manifest-reconciler.js","sourceRoot":"","sources":["../../../src/backend/services/manifest-reconciler.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,26 @@
1
+ import type { TransactionalEmailSender } from '@endora-commerce/contracts';
2
+ export interface TemplateEmailDeps {
3
+ getSender?: () => TransactionalEmailSender | undefined;
4
+ resolveScopeSalesChannelId?: () => Promise<string | null>;
5
+ resolveLanguage?: (salesChannelId: string) => Promise<string>;
6
+ }
7
+ export interface TemplateEmail {
8
+ /**
9
+ * `true` means "handled — do not use your legacy in-code builder". That
10
+ * covers a delivered email, one an operator deactivated, and a composition
11
+ * with no transport: in all three the platform decided what to send, and a
12
+ * fallback would either send mail the operator switched off or fail the same
13
+ * way. Only a code with no definition at all answers `false`.
14
+ */
15
+ trySend(input: {
16
+ code: string;
17
+ to: string;
18
+ messageId: string;
19
+ variables: Record<string, unknown>;
20
+ meta?: Record<string, unknown> | undefined;
21
+ }): Promise<boolean>;
22
+ }
23
+ export declare function makeTemplateEmail(deps: TemplateEmailDeps): TemplateEmail;
24
+ /** No-op helper (sender not wired) — always falls back to the legacy builder. */
25
+ export declare const noopTemplateEmail: TemplateEmail;
26
+ //# sourceMappingURL=template-email.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"template-email.d.ts","sourceRoot":"","sources":["../../../src/backend/services/template-email.ts"],"names":[],"mappings":"AAeA,OAAO,KAAK,EAAE,wBAAwB,EAAE,MAAM,4BAA4B,CAAC;AAE3E,MAAM,WAAW,iBAAiB;IAChC,SAAS,CAAC,EAAE,MAAM,wBAAwB,GAAG,SAAS,CAAC;IACvD,0BAA0B,CAAC,EAAE,MAAM,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAAC;IAC1D,eAAe,CAAC,EAAE,CAAC,cAAc,EAAE,MAAM,KAAK,OAAO,CAAC,MAAM,CAAC,CAAC;CAC/D;AAED,MAAM,WAAW,aAAa;IAC5B;;;;;;OAMG;IACH,OAAO,CAAC,KAAK,EAAE;QACb,IAAI,EAAE,MAAM,CAAC;QACb,EAAE,EAAE,MAAM,CAAC;QACX,SAAS,EAAE,MAAM,CAAC;QAClB,SAAS,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;QACnC,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,SAAS,CAAC;KAC5C,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;CACtB;AAED,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,iBAAiB,GAAG,aAAa,CA4CxE;AAED,iFAAiF;AACjF,eAAO,MAAM,iBAAiB,EAAE,aAI/B,CAAC"}
@@ -0,0 +1,69 @@
1
+ // Feature 047 — helper that routes an email through the admin-editable
2
+ // transactional template when a sender is wired, returning false so the caller
3
+ // can fall back to its legacy in-code builder otherwise.
4
+ //
5
+ // Feature 072 (T120) moved this here from `organizations/services/
6
+ // org-template-email.ts`. It never was an organizations concern: every line of
7
+ // it is about the transactional sender and the channel an unscoped email
8
+ // resolves against. `organizations` owned it only because that module needed it
9
+ // first, and `inventory` reached it through a root that imported across the
10
+ // boundary and passed the result down — which stopped being expressible once
11
+ // both modules composed themselves.
12
+ //
13
+ // Emails that are not naturally sales-channel-scoped resolve against the
14
+ // system-default channel; global template editing still applies.
15
+ export function makeTemplateEmail(deps) {
16
+ return {
17
+ async trySend(input) {
18
+ const sender = deps.getSender?.();
19
+ if (!sender)
20
+ return false;
21
+ const salesChannelId = (await deps.resolveScopeSalesChannelId?.()) ?? null;
22
+ // **Not the seventh site of the `''`-sentinel family (issue #103) — do
23
+ // not "fix" this line to pass `null` through.**
24
+ //
25
+ // `TransactionalEmailSendInput.salesChannelId` is `string | null` now, so
26
+ // the compiler would let it. Two things make this branch different from
27
+ // the six that were changed. It is a **producer**, and its `false` is not
28
+ // silence: it means "caller, use your legacy in-code builder", so the
29
+ // message still goes out — the other six turned an absent channel into a
30
+ // failed settings read that a `catch` then reported as "not configured".
31
+ // And the channel it wants is the **system-default** one, which D-47
32
+ // establishes always exists, so this is a near-dead branch rather than a
33
+ // wrong one.
34
+ //
35
+ // Passing `null` here would be a behaviour change for every module that
36
+ // holds `templateEmailPort`: each would start sending the platform-wide
37
+ // template where it sends its own builder's output today. That is a
38
+ // product decision about what mail goes out, not a sentinel repair.
39
+ if (!salesChannelId)
40
+ return false;
41
+ let language = 'en-US';
42
+ if (deps.resolveLanguage) {
43
+ try {
44
+ language = await deps.resolveLanguage(salesChannelId);
45
+ }
46
+ catch {
47
+ language = 'en-US';
48
+ }
49
+ }
50
+ const outcome = await sender.send({
51
+ code: input.code,
52
+ salesChannelId,
53
+ language,
54
+ to: input.to,
55
+ messageId: input.messageId,
56
+ variables: input.variables,
57
+ ...(input.meta ? { meta: input.meta } : {}),
58
+ });
59
+ return outcome.status !== 'no_definition';
60
+ },
61
+ };
62
+ }
63
+ /** No-op helper (sender not wired) — always falls back to the legacy builder. */
64
+ export const noopTemplateEmail = {
65
+ async trySend() {
66
+ return false;
67
+ },
68
+ };
69
+ //# sourceMappingURL=template-email.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"template-email.js","sourceRoot":"","sources":["../../../src/backend/services/template-email.ts"],"names":[],"mappings":"AAAA,uEAAuE;AACvE,+EAA+E;AAC/E,yDAAyD;AACzD,EAAE;AACF,mEAAmE;AACnE,+EAA+E;AAC/E,yEAAyE;AACzE,gFAAgF;AAChF,4EAA4E;AAC5E,6EAA6E;AAC7E,oCAAoC;AACpC,EAAE;AACF,yEAAyE;AACzE,iEAAiE;AA2BjE,MAAM,UAAU,iBAAiB,CAAC,IAAuB;IACvD,OAAO;QACL,KAAK,CAAC,OAAO,CAAC,KAAK;YACjB,MAAM,MAAM,GAAG,IAAI,CAAC,SAAS,EAAE,EAAE,CAAC;YAClC,IAAI,CAAC,MAAM;gBAAE,OAAO,KAAK,CAAC;YAC1B,MAAM,cAAc,GAAG,CAAC,MAAM,IAAI,CAAC,0BAA0B,EAAE,EAAE,CAAC,IAAI,IAAI,CAAC;YAC3E,uEAAuE;YACvE,gDAAgD;YAChD,EAAE;YACF,0EAA0E;YAC1E,wEAAwE;YACxE,0EAA0E;YAC1E,sEAAsE;YACtE,yEAAyE;YACzE,yEAAyE;YACzE,qEAAqE;YACrE,yEAAyE;YACzE,aAAa;YACb,EAAE;YACF,wEAAwE;YACxE,wEAAwE;YACxE,oEAAoE;YACpE,oEAAoE;YACpE,IAAI,CAAC,cAAc;gBAAE,OAAO,KAAK,CAAC;YAClC,IAAI,QAAQ,GAAG,OAAO,CAAC;YACvB,IAAI,IAAI,CAAC,eAAe,EAAE,CAAC;gBACzB,IAAI,CAAC;oBACH,QAAQ,GAAG,MAAM,IAAI,CAAC,eAAe,CAAC,cAAc,CAAC,CAAC;gBACxD,CAAC;gBAAC,MAAM,CAAC;oBACP,QAAQ,GAAG,OAAO,CAAC;gBACrB,CAAC;YACH,CAAC;YACD,MAAM,OAAO,GAAG,MAAM,MAAM,CAAC,IAAI,CAAC;gBAChC,IAAI,EAAE,KAAK,CAAC,IAAI;gBAChB,cAAc;gBACd,QAAQ;gBACR,EAAE,EAAE,KAAK,CAAC,EAAE;gBACZ,SAAS,EAAE,KAAK,CAAC,SAAS;gBAC1B,SAAS,EAAE,KAAK,CAAC,SAAS;gBAC1B,GAAG,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,KAAK,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;aAC5C,CAAC,CAAC;YACH,OAAO,OAAO,CAAC,MAAM,KAAK,eAAe,CAAC;QAC5C,CAAC;KACF,CAAC;AACJ,CAAC;AAED,iFAAiF;AACjF,MAAM,CAAC,MAAM,iBAAiB,GAAkB;IAC9C,KAAK,CAAC,OAAO;QACX,OAAO,KAAK,CAAC;IACf,CAAC;CACF,CAAC"}