@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,133 @@
1
+ import { Migration } from '@mikro-orm/migrations';
2
+ import { randomUUID } from 'crypto';
3
+ import { DEFAULT_HEADER_BLOCK_CODE, defaultHeaderTree, } from '@endora-commerce/email-components/defaults/default-header';
4
+ import { DEFAULT_FOOTER_BLOCK_CODE, defaultFooterTree, envelopeFromTree, } from '@endora-commerce/email-components/defaults/default-footer';
5
+ /**
6
+ * Feature 047 — Transactional Emails.
7
+ *
8
+ * Creates the module schema: email definitions (`transactional_emails`),
9
+ * per-scope/per-language admin customizations (`transactional_email_contents`,
10
+ * with two partial unique indexes since Postgres treats NULL sales_channel_id as
11
+ * distinct), reusable email-safe blocks/templates and their sales-channel
12
+ * bridges, and seeds the system default header/footer blocks (FR-020).
13
+ */
14
+ const DEFAULT_LANGUAGES = ['en-US', 'pl-PL'];
15
+ function jsonbLiteral(value) {
16
+ return JSON.stringify(value).replace(/'/g, "''");
17
+ }
18
+ export class Migration20260629T113442TransactionalEmailsInit extends Migration {
19
+ async up() {
20
+ // --- Definitions + module defaults -------------------------------------
21
+ this.addSql(`
22
+ create table "transactional_emails" (
23
+ "id" uuid not null,
24
+ "code" varchar(160) not null,
25
+ "name" varchar(200) not null,
26
+ "owner_module" varchar(64) not null,
27
+ "description" text null,
28
+ "group_code" varchar(64) null,
29
+ "variables" jsonb not null default '[]',
30
+ "languages" jsonb not null default '[]',
31
+ "default_subject" jsonb not null default '{}',
32
+ "default_content" jsonb not null default '{}',
33
+ "active" boolean not null default true,
34
+ "created_at" timestamptz not null,
35
+ "updated_at" timestamptz not null,
36
+ constraint "transactional_emails_pkey" primary key ("id")
37
+ );
38
+ `);
39
+ this.addSql(`alter table "transactional_emails" add constraint "transactional_emails_code_unique" unique ("code");`);
40
+ // --- Admin customizations (per scope + language) -----------------------
41
+ this.addSql(`
42
+ create table "transactional_email_contents" (
43
+ "id" uuid not null,
44
+ "email_id" uuid not null,
45
+ "sales_channel_id" uuid null,
46
+ "language" varchar(12) not null,
47
+ "subject" text not null,
48
+ "content" jsonb not null default '{}',
49
+ "version" int not null default 1,
50
+ "created_at" timestamptz not null,
51
+ "updated_at" timestamptz not null,
52
+ constraint "transactional_email_contents_pkey" primary key ("id")
53
+ );
54
+ `);
55
+ this.addSql(`alter table "transactional_email_contents" add constraint "tec_email_fk" ` +
56
+ `foreign key ("email_id") references "transactional_emails" ("id") on delete cascade;`);
57
+ this.addSql(`create unique index "tec_global_unique" on "transactional_email_contents" ("email_id", "language") where "sales_channel_id" is null;`);
58
+ this.addSql(`create unique index "tec_channel_unique" on "transactional_email_contents" ("email_id", "sales_channel_id", "language") where "sales_channel_id" is not null;`);
59
+ // --- Reusable blocks + bridge ------------------------------------------
60
+ this.addSql(`
61
+ create table "email_blocks" (
62
+ "id" uuid not null,
63
+ "code" varchar(180) not null,
64
+ "name" varchar(200) not null,
65
+ "description" text null,
66
+ "active" boolean not null default true,
67
+ "content" jsonb not null default '{}',
68
+ "languages" jsonb not null default '[]',
69
+ "is_system" boolean not null default false,
70
+ "version" int not null default 1,
71
+ "created_at" timestamptz not null,
72
+ "updated_at" timestamptz not null,
73
+ constraint "email_blocks_pkey" primary key ("id")
74
+ );
75
+ `);
76
+ this.addSql(`create index "email_blocks_code_idx" on "email_blocks" ("code");`);
77
+ this.addSql(`
78
+ create table "email_block_sales_channels" (
79
+ "block_id" uuid not null,
80
+ "sales_channel_id" uuid not null,
81
+ "code" varchar(180) not null,
82
+ constraint "email_block_sales_channels_pkey" primary key ("block_id", "sales_channel_id")
83
+ );
84
+ `);
85
+ this.addSql(`alter table "email_block_sales_channels" add constraint "ebsc_block_fk" ` +
86
+ `foreign key ("block_id") references "email_blocks" ("id") on delete cascade;`);
87
+ this.addSql(`create unique index "ebsc_channel_code_unique" on "email_block_sales_channels" ("sales_channel_id", "code");`);
88
+ // --- Reusable templates + bridge ---------------------------------------
89
+ this.addSql(`
90
+ create table "email_templates" (
91
+ "id" uuid not null,
92
+ "code" varchar(180) not null,
93
+ "name" varchar(200) not null,
94
+ "description" text null,
95
+ "content" jsonb not null default '{}',
96
+ "languages" jsonb not null default '[]',
97
+ "is_system" boolean not null default false,
98
+ "version" int not null default 1,
99
+ "created_at" timestamptz not null,
100
+ "updated_at" timestamptz not null,
101
+ constraint "email_templates_pkey" primary key ("id")
102
+ );
103
+ `);
104
+ this.addSql(`create index "email_templates_code_idx" on "email_templates" ("code");`);
105
+ this.addSql(`
106
+ create table "email_template_sales_channels" (
107
+ "template_id" uuid not null,
108
+ "sales_channel_id" uuid not null,
109
+ "code" varchar(180) not null,
110
+ constraint "email_template_sales_channels_pkey" primary key ("template_id", "sales_channel_id")
111
+ );
112
+ `);
113
+ this.addSql(`alter table "email_template_sales_channels" add constraint "etsc_template_fk" ` +
114
+ `foreign key ("template_id") references "email_templates" ("id") on delete cascade;`);
115
+ this.addSql(`create unique index "etsc_channel_code_unique" on "email_template_sales_channels" ("sales_channel_id", "code");`);
116
+ // --- Seed system default header + footer blocks (FR-020) ---------------
117
+ const header = envelopeFromTree(defaultHeaderTree(), DEFAULT_LANGUAGES);
118
+ const footer = envelopeFromTree(defaultFooterTree(), DEFAULT_LANGUAGES);
119
+ this.addSql(`insert into "email_blocks" ("id", "code", "name", "active", "content", "languages", "is_system", "version", "created_at", "updated_at") ` +
120
+ `values ('${randomUUID()}', '${DEFAULT_HEADER_BLOCK_CODE}', 'Default header', true, '${jsonbLiteral(header)}'::jsonb, '${jsonbLiteral(DEFAULT_LANGUAGES)}'::jsonb, true, 1, now(), now());`);
121
+ this.addSql(`insert into "email_blocks" ("id", "code", "name", "active", "content", "languages", "is_system", "version", "created_at", "updated_at") ` +
122
+ `values ('${randomUUID()}', '${DEFAULT_FOOTER_BLOCK_CODE}', 'Default footer', true, '${jsonbLiteral(footer)}'::jsonb, '${jsonbLiteral(DEFAULT_LANGUAGES)}'::jsonb, true, 1, now(), now());`);
123
+ }
124
+ async down() {
125
+ this.addSql('drop table if exists "email_template_sales_channels" cascade;');
126
+ this.addSql('drop table if exists "email_templates" cascade;');
127
+ this.addSql('drop table if exists "email_block_sales_channels" cascade;');
128
+ this.addSql('drop table if exists "email_blocks" cascade;');
129
+ this.addSql('drop table if exists "transactional_email_contents" cascade;');
130
+ this.addSql('drop table if exists "transactional_emails" cascade;');
131
+ }
132
+ }
133
+ //# sourceMappingURL=20260629T113442_transactional_emails_init.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"20260629T113442_transactional_emails_init.js","sourceRoot":"","sources":["../../src/migrations/20260629T113442_transactional_emails_init.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,uBAAuB,CAAC;AAClD,OAAO,EAAE,UAAU,EAAE,MAAM,QAAQ,CAAC;AACpC,OAAO,EACL,yBAAyB,EACzB,iBAAiB,GAClB,MAAM,2DAA2D,CAAC;AACnE,OAAO,EACL,yBAAyB,EACzB,iBAAiB,EACjB,gBAAgB,GACjB,MAAM,2DAA2D,CAAC;AAEnE;;;;;;;;GAQG;AACH,MAAM,iBAAiB,GAAG,CAAC,OAAO,EAAE,OAAO,CAAC,CAAC;AAE7C,SAAS,YAAY,CAAC,KAAc;IAClC,OAAO,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC,CAAC,OAAO,CAAC,IAAI,EAAE,IAAI,CAAC,CAAC;AACnD,CAAC;AAED,MAAM,OAAO,+CAAgD,SAAQ,SAAS;IACnE,KAAK,CAAC,EAAE;QACf,0EAA0E;QAC1E,IAAI,CAAC,MAAM,CAAC;;;;;;;;;;;;;;;;;KAiBX,CAAC,CAAC;QACH,IAAI,CAAC,MAAM,CACT,uGAAuG,CACxG,CAAC;QAEF,0EAA0E;QAC1E,IAAI,CAAC,MAAM,CAAC;;;;;;;;;;;;;KAaX,CAAC,CAAC;QACH,IAAI,CAAC,MAAM,CACT,2EAA2E;YACzE,sFAAsF,CACzF,CAAC;QACF,IAAI,CAAC,MAAM,CACT,sIAAsI,CACvI,CAAC;QACF,IAAI,CAAC,MAAM,CACT,+JAA+J,CAChK,CAAC;QAEF,0EAA0E;QAC1E,IAAI,CAAC,MAAM,CAAC;;;;;;;;;;;;;;;KAeX,CAAC,CAAC;QACH,IAAI,CAAC,MAAM,CAAC,kEAAkE,CAAC,CAAC;QAChF,IAAI,CAAC,MAAM,CAAC;;;;;;;KAOX,CAAC,CAAC;QACH,IAAI,CAAC,MAAM,CACT,0EAA0E;YACxE,8EAA8E,CACjF,CAAC;QACF,IAAI,CAAC,MAAM,CACT,8GAA8G,CAC/G,CAAC;QAEF,0EAA0E;QAC1E,IAAI,CAAC,MAAM,CAAC;;;;;;;;;;;;;;KAcX,CAAC,CAAC;QACH,IAAI,CAAC,MAAM,CAAC,wEAAwE,CAAC,CAAC;QACtF,IAAI,CAAC,MAAM,CAAC;;;;;;;KAOX,CAAC,CAAC;QACH,IAAI,CAAC,MAAM,CACT,gFAAgF;YAC9E,oFAAoF,CACvF,CAAC;QACF,IAAI,CAAC,MAAM,CACT,iHAAiH,CAClH,CAAC;QAEF,0EAA0E;QAC1E,MAAM,MAAM,GAAG,gBAAgB,CAAC,iBAAiB,EAAE,EAAE,iBAAiB,CAAC,CAAC;QACxE,MAAM,MAAM,GAAG,gBAAgB,CAAC,iBAAiB,EAAE,EAAE,iBAAiB,CAAC,CAAC;QACxE,IAAI,CAAC,MAAM,CACT,0IAA0I;YACxI,YAAY,UAAU,EAAE,OAAO,yBAAyB,+BAA+B,YAAY,CAAC,MAAM,CAAC,cAAc,YAAY,CAAC,iBAAiB,CAAC,mCAAmC,CAC9L,CAAC;QACF,IAAI,CAAC,MAAM,CACT,0IAA0I;YACxI,YAAY,UAAU,EAAE,OAAO,yBAAyB,+BAA+B,YAAY,CAAC,MAAM,CAAC,cAAc,YAAY,CAAC,iBAAiB,CAAC,mCAAmC,CAC9L,CAAC;IACJ,CAAC;IAEQ,KAAK,CAAC,IAAI;QACjB,IAAI,CAAC,MAAM,CAAC,+DAA+D,CAAC,CAAC;QAC7E,IAAI,CAAC,MAAM,CAAC,iDAAiD,CAAC,CAAC;QAC/D,IAAI,CAAC,MAAM,CAAC,4DAA4D,CAAC,CAAC;QAC1E,IAAI,CAAC,MAAM,CAAC,8CAA8C,CAAC,CAAC;QAC5D,IAAI,CAAC,MAAM,CAAC,8DAA8D,CAAC,CAAC;QAC5E,IAAI,CAAC,MAAM,CAAC,sDAAsD,CAAC,CAAC;IACtE,CAAC;CACF"}
@@ -0,0 +1,6 @@
1
+ import { Migration } from '@mikro-orm/migrations';
2
+ export declare class Migration20260801T111001TransactionalEmailsEmailDefaultsReseed extends Migration {
3
+ up(): Promise<void>;
4
+ down(): Promise<void>;
5
+ }
6
+ //# sourceMappingURL=20260801T111001_transactional_emails_email_defaults_reseed.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"20260801T111001_transactional_emails_email_defaults_reseed.d.ts","sourceRoot":"","sources":["../../src/migrations/20260801T111001_transactional_emails_email_defaults_reseed.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,uBAAuB,CAAC;AAqClD,qBAAa,8DAA+D,SAAQ,SAAS;IAC5E,EAAE,IAAI,OAAO,CAAC,IAAI,CAAC;IAsBnB,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC;CAIrC"}
@@ -0,0 +1,49 @@
1
+ import { Migration } from '@mikro-orm/migrations';
2
+ import { defaultHeaderTree } from '@endora-commerce/email-components/defaults/default-header';
3
+ import { defaultFooterTree, envelopeFromTree, } from '@endora-commerce/email-components/defaults/default-footer';
4
+ /**
5
+ * Reseed this module's own system header/footer block trees from the current
6
+ * `@endora-commerce/email-components` defaults, and clear admin transactional email
7
+ * content overrides so boot reconcile + Reset land on the new simple layouts.
8
+ *
9
+ * Timestamped after FROZEN_THROUGH (feature 065) so it sorts after 100–106. The mis-numbered
10
+ * `Migration099EmailDefaultsReseed` broke `migrator.down()` loops that parse
11
+ * the latest migration ordinal (catalog attributes-migration-parity tests).
12
+ *
13
+ * It reseeded `newsletter`'s two system blocks as well, with two `UPDATE`s
14
+ * against `newsletter_email_blocks`, until
15
+ * `specs/120-migration-closure-bridge-ownership/` FR-019 removed them in place.
16
+ * Nothing replaced them here and nothing should: `newsletter` owns that table,
17
+ * and this module declares `activation: { nonDeactivatable: true }`, so naming
18
+ * `newsletter` in its `dependencies` to make the write legal would turn
19
+ * `newsletter`'s activation control into a dead switch. An instance that omits
20
+ * `newsletter` altogether — which the default module set does, it being the
21
+ * `nonDeactivatable` closure — could not migrate at all while they stood. The
22
+ * refresh of an existing database, if anyone wants one, is `newsletter`'s to
23
+ * ship in a `newsletter`-owned migration that needs no cross-module edge.
24
+ */
25
+ const DEFAULT_LANGUAGES = ['en-US', 'pl-PL'];
26
+ const TE_HEADER = 'default_email_header';
27
+ const TE_FOOTER = 'default_email_footer';
28
+ function jsonbLiteral(value) {
29
+ return JSON.stringify(value).replace(/'/g, "''");
30
+ }
31
+ export class Migration20260801T111001TransactionalEmailsEmailDefaultsReseed extends Migration {
32
+ async up() {
33
+ // Retire the mis-numbered 099 row if a previous deploy already applied it.
34
+ this.addSql(`delete from "mikro_orm_migrations" where "name" = 'Migration099EmailDefaultsReseed';`);
35
+ const header = envelopeFromTree(defaultHeaderTree(), DEFAULT_LANGUAGES);
36
+ const footer = envelopeFromTree(defaultFooterTree(), DEFAULT_LANGUAGES);
37
+ const headerJson = jsonbLiteral(header);
38
+ const footerJson = jsonbLiteral(footer);
39
+ this.addSql(`update "email_blocks" set "content" = '${headerJson}'::jsonb, "version" = "version" + 1, "updated_at" = now() where "code" = '${TE_HEADER}' and "is_system" = true;`);
40
+ this.addSql(`update "email_blocks" set "content" = '${footerJson}'::jsonb, "version" = "version" + 1, "updated_at" = now() where "code" = '${TE_FOOTER}' and "is_system" = true;`);
41
+ // Drop admin overrides so editors see the refreshed module defaults.
42
+ this.addSql('delete from "transactional_email_contents";');
43
+ }
44
+ async down() {
45
+ // Content overrides cannot be restored; block trees stay at the reseeded
46
+ // shape (same as up). No destructive schema change to roll back.
47
+ }
48
+ }
49
+ //# sourceMappingURL=20260801T111001_transactional_emails_email_defaults_reseed.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"20260801T111001_transactional_emails_email_defaults_reseed.js","sourceRoot":"","sources":["../../src/migrations/20260801T111001_transactional_emails_email_defaults_reseed.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,uBAAuB,CAAC;AAClD,OAAO,EAAE,iBAAiB,EAAE,MAAM,2DAA2D,CAAC;AAC9F,OAAO,EACL,iBAAiB,EACjB,gBAAgB,GACjB,MAAM,2DAA2D,CAAC;AAEnE;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,MAAM,iBAAiB,GAAG,CAAC,OAAO,EAAE,OAAO,CAAU,CAAC;AAEtD,MAAM,SAAS,GAAG,sBAAsB,CAAC;AACzC,MAAM,SAAS,GAAG,sBAAsB,CAAC;AAEzC,SAAS,YAAY,CAAC,KAAc;IAClC,OAAO,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC,CAAC,OAAO,CAAC,IAAI,EAAE,IAAI,CAAC,CAAC;AACnD,CAAC;AAED,MAAM,OAAO,8DAA+D,SAAQ,SAAS;IAClF,KAAK,CAAC,EAAE;QACf,2EAA2E;QAC3E,IAAI,CAAC,MAAM,CACT,sFAAsF,CACvF,CAAC;QAEF,MAAM,MAAM,GAAG,gBAAgB,CAAC,iBAAiB,EAAE,EAAE,iBAAiB,CAAC,CAAC;QACxE,MAAM,MAAM,GAAG,gBAAgB,CAAC,iBAAiB,EAAE,EAAE,iBAAiB,CAAC,CAAC;QACxE,MAAM,UAAU,GAAG,YAAY,CAAC,MAAM,CAAC,CAAC;QACxC,MAAM,UAAU,GAAG,YAAY,CAAC,MAAM,CAAC,CAAC;QAExC,IAAI,CAAC,MAAM,CACT,0CAA0C,UAAU,6EAA6E,SAAS,2BAA2B,CACtK,CAAC;QACF,IAAI,CAAC,MAAM,CACT,0CAA0C,UAAU,6EAA6E,SAAS,2BAA2B,CACtK,CAAC;QAEF,qEAAqE;QACrE,IAAI,CAAC,MAAM,CAAC,6CAA6C,CAAC,CAAC;IAC7D,CAAC;IAEQ,KAAK,CAAC,IAAI;QACjB,yEAAyE;QACzE,iEAAiE;IACnE,CAAC;CACF"}
@@ -0,0 +1,6 @@
1
+ import { Migration } from '@mikro-orm/migrations';
2
+ export declare class Migration20260903T101748TransactionalEmailsNamespaceBlockNames extends Migration {
3
+ up(): Promise<void>;
4
+ down(): Promise<void>;
5
+ }
6
+ //# sourceMappingURL=20260903T101748_transactional_emails_namespace_block_names.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"20260903T101748_transactional_emails_namespace_block_names.d.ts","sourceRoot":"","sources":["../../src/migrations/20260903T101748_transactional_emails_namespace_block_names.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,uBAAuB,CAAC;AAmDlD,qBAAa,8DAA+D,SAAQ,SAAS;IAC5E,EAAE,IAAI,OAAO,CAAC,IAAI,CAAC;IAQnB,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC;CAOrC"}
@@ -0,0 +1,60 @@
1
+ import { Migration } from '@mikro-orm/migrations';
2
+ import { FROZEN_BLOCK_RENAMES, FROZEN_BLOCK_RENAMES_INVERSE, applyRenameFunctionSql, createRenameFunctionSql, dropRenameFunctionSql, } from '@endora-commerce/page-builder-core/migration';
3
+ /**
4
+ * Namespace the Page Builder block names stored in `transactional_emails`'s 3 `jsonb` columns
5
+ * — feature 096, T406 (`contracts/block-name-migration.md`).
6
+ *
7
+ * **The rewrite is structural, never textual.** Twelve of the 74 renamed names
8
+ * are ordinary English words that occur throughout shop content — `Row`,
9
+ * `Text`, `Image`, `Map`, `Button` among them — so a text substitution over the
10
+ * column would corrupt a `RawHtml` block's markup and every `alt` attribute in
11
+ * the shop. `pg_temp.rename_block_names_transactional_emails` descends the document and replaces the value of a
12
+ * `type` property in a **node** position and nothing else; the walk is
13
+ * generated from `FROZEN_BLOCK_RENAMES` by
14
+ * `@endora-commerce/page-builder-core/migration`, so five migrations share one
15
+ * definition of what a node is.
16
+ *
17
+ * **This migration belongs to `transactional_emails` because `transactional_emails` owns these tables**,
18
+ * not because it owns the new names (`contracts/block-name-migration.md` §2).
19
+ * Some of the names it writes belong to other modules; that creates no
20
+ * obligation on them and **no new manifest `dependencies` edge**, because a
21
+ * block name is a string value inside a JSONB document — not a foreign key and
22
+ * not a table identifier.
23
+ *
24
+ * **It cannot fail on its input.** A name the map does not hold is left
25
+ * byte-identical: an already-namespaced one and an unrecognised one alike,
26
+ * because the map's domain is bare and its codomain is dotted, so the two sets
27
+ * are disjoint. That is also what makes a second run rewrite nothing — FR-013
28
+ * is a property of the map rather than an outcome a branch has to remember to
29
+ * produce, and no dot test appears in this SQL.
30
+ *
31
+ * `down()` applies the inverse over the identical walk and is **exact for the
32
+ * frozen set**. It is partial by construction: a name with no pre-migration
33
+ * form — a block authored after the upgrade, a third party's `acme.Banner` —
34
+ * has nothing to return to and is left alone. That is correct, and it is why
35
+ * the operator pre-flight (§6) is a backup rather than a `down()`.
36
+ */
37
+ /**
38
+ * Named per module: the five rename migrations may run on one pooled session,
39
+ * and a second `create function` over the same name fails. Each drops its own
40
+ * when it is done, and a rollback removes it with everything else — `pg_temp`
41
+ * DDL is transactional.
42
+ */
43
+ const FN = 'rename_block_names_transactional_emails';
44
+ export class Migration20260903T101748TransactionalEmailsNamespaceBlockNames extends Migration {
45
+ async up() {
46
+ this.addSql(createRenameFunctionSql(FN, FROZEN_BLOCK_RENAMES));
47
+ this.addSql(applyRenameFunctionSql(FN, 'email_templates', 'content'));
48
+ this.addSql(applyRenameFunctionSql(FN, 'email_blocks', 'content'));
49
+ this.addSql(applyRenameFunctionSql(FN, 'transactional_email_contents', 'content'));
50
+ this.addSql(dropRenameFunctionSql(FN));
51
+ }
52
+ async down() {
53
+ this.addSql(createRenameFunctionSql(FN, FROZEN_BLOCK_RENAMES_INVERSE));
54
+ this.addSql(applyRenameFunctionSql(FN, 'email_templates', 'content'));
55
+ this.addSql(applyRenameFunctionSql(FN, 'email_blocks', 'content'));
56
+ this.addSql(applyRenameFunctionSql(FN, 'transactional_email_contents', 'content'));
57
+ this.addSql(dropRenameFunctionSql(FN));
58
+ }
59
+ }
60
+ //# sourceMappingURL=20260903T101748_transactional_emails_namespace_block_names.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"20260903T101748_transactional_emails_namespace_block_names.js","sourceRoot":"","sources":["../../src/migrations/20260903T101748_transactional_emails_namespace_block_names.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,uBAAuB,CAAC;AAClD,OAAO,EACL,oBAAoB,EACpB,4BAA4B,EAC5B,sBAAsB,EACtB,uBAAuB,EACvB,qBAAqB,GACtB,MAAM,8CAA8C,CAAC;AAEtD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAiCG;AACH;;;;;GAKG;AACH,MAAM,EAAE,GAAG,yCAAyC,CAAC;AAErD,MAAM,OAAO,8DAA+D,SAAQ,SAAS;IAClF,KAAK,CAAC,EAAE;QACf,IAAI,CAAC,MAAM,CAAC,uBAAuB,CAAC,EAAE,EAAE,oBAAoB,CAAC,CAAC,CAAC;QAC/D,IAAI,CAAC,MAAM,CAAC,sBAAsB,CAAC,EAAE,EAAE,iBAAiB,EAAE,SAAS,CAAC,CAAC,CAAC;QACtE,IAAI,CAAC,MAAM,CAAC,sBAAsB,CAAC,EAAE,EAAE,cAAc,EAAE,SAAS,CAAC,CAAC,CAAC;QACnE,IAAI,CAAC,MAAM,CAAC,sBAAsB,CAAC,EAAE,EAAE,8BAA8B,EAAE,SAAS,CAAC,CAAC,CAAC;QACnF,IAAI,CAAC,MAAM,CAAC,qBAAqB,CAAC,EAAE,CAAC,CAAC,CAAC;IACzC,CAAC;IAEQ,KAAK,CAAC,IAAI;QACjB,IAAI,CAAC,MAAM,CAAC,uBAAuB,CAAC,EAAE,EAAE,4BAA4B,CAAC,CAAC,CAAC;QACvE,IAAI,CAAC,MAAM,CAAC,sBAAsB,CAAC,EAAE,EAAE,iBAAiB,EAAE,SAAS,CAAC,CAAC,CAAC;QACtE,IAAI,CAAC,MAAM,CAAC,sBAAsB,CAAC,EAAE,EAAE,cAAc,EAAE,SAAS,CAAC,CAAC,CAAC;QACnE,IAAI,CAAC,MAAM,CAAC,sBAAsB,CAAC,EAAE,EAAE,8BAA8B,EAAE,SAAS,CAAC,CAAC,CAAC;QACnF,IAAI,CAAC,MAAM,CAAC,qBAAqB,CAAC,EAAE,CAAC,CAAC,CAAC;IACzC,CAAC;CACF"}
@@ -0,0 +1,29 @@
1
+ /**
2
+ * The `./migrations` subpath — every migration class this module owns, as one
3
+ * ordered `migrations` array.
4
+ *
5
+ * The array is what the platform reads when this module is **installed**:
6
+ * `src/packages/package-runtime.ts` takes `exported['migrations']` and refuses
7
+ * the package outright when it is absent (D-168).
8
+ *
9
+ * Listed in ascending timestamp, which is the order of this module's own
10
+ * migrations and of nothing else (feature 081): a manifest `dependencies` array
11
+ * is the only thing ordering this block against another module's.
12
+ *
13
+ * The **named** exports stay beside the array, and the asymmetry with
14
+ * `./backend` — which publishes an array and no named class (D-168) — is
15
+ * deliberate. `db/migrations-registry.generated.ts` imports each class by name
16
+ * from this specifier, and a migration class name is contract in a way an entity
17
+ * class name is not: `mikro_orm_migrations` persists it, so it is a string every
18
+ * already-migrated database holds.
19
+ *
20
+ * A class that is in neither the array nor the barrel is a migration that does
21
+ * not run: `migration:pending` reports nothing pending and the first symptom is
22
+ * a query against a table nobody created.
23
+ */
24
+ import { Migration20260629T113442TransactionalEmailsInit } from './20260629T113442_transactional_emails_init.js';
25
+ import { Migration20260801T111001TransactionalEmailsEmailDefaultsReseed } from './20260801T111001_transactional_emails_email_defaults_reseed.js';
26
+ import { Migration20260903T101748TransactionalEmailsNamespaceBlockNames } from './20260903T101748_transactional_emails_namespace_block_names.js';
27
+ export declare const migrations: (typeof Migration20260629T113442TransactionalEmailsInit)[];
28
+ export { Migration20260629T113442TransactionalEmailsInit, Migration20260801T111001TransactionalEmailsEmailDefaultsReseed, Migration20260903T101748TransactionalEmailsNamespaceBlockNames, };
29
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/migrations/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAEH,OAAO,EAAE,+CAA+C,EAAE,MAAM,gDAAgD,CAAC;AACjH,OAAO,EAAE,8DAA8D,EAAE,MAAM,iEAAiE,CAAC;AACjJ,OAAO,EAAE,8DAA8D,EAAE,MAAM,iEAAiE,CAAC;AAEjJ,eAAO,MAAM,UAAU,4DAItB,CAAC;AAEF,OAAO,EACL,+CAA+C,EAC/C,8DAA8D,EAC9D,8DAA8D,GAC/D,CAAC"}
@@ -0,0 +1,33 @@
1
+ /**
2
+ * The `./migrations` subpath — every migration class this module owns, as one
3
+ * ordered `migrations` array.
4
+ *
5
+ * The array is what the platform reads when this module is **installed**:
6
+ * `src/packages/package-runtime.ts` takes `exported['migrations']` and refuses
7
+ * the package outright when it is absent (D-168).
8
+ *
9
+ * Listed in ascending timestamp, which is the order of this module's own
10
+ * migrations and of nothing else (feature 081): a manifest `dependencies` array
11
+ * is the only thing ordering this block against another module's.
12
+ *
13
+ * The **named** exports stay beside the array, and the asymmetry with
14
+ * `./backend` — which publishes an array and no named class (D-168) — is
15
+ * deliberate. `db/migrations-registry.generated.ts` imports each class by name
16
+ * from this specifier, and a migration class name is contract in a way an entity
17
+ * class name is not: `mikro_orm_migrations` persists it, so it is a string every
18
+ * already-migrated database holds.
19
+ *
20
+ * A class that is in neither the array nor the barrel is a migration that does
21
+ * not run: `migration:pending` reports nothing pending and the first symptom is
22
+ * a query against a table nobody created.
23
+ */
24
+ import { Migration20260629T113442TransactionalEmailsInit } from './20260629T113442_transactional_emails_init.js';
25
+ import { Migration20260801T111001TransactionalEmailsEmailDefaultsReseed } from './20260801T111001_transactional_emails_email_defaults_reseed.js';
26
+ import { Migration20260903T101748TransactionalEmailsNamespaceBlockNames } from './20260903T101748_transactional_emails_namespace_block_names.js';
27
+ export const migrations = [
28
+ Migration20260629T113442TransactionalEmailsInit,
29
+ Migration20260801T111001TransactionalEmailsEmailDefaultsReseed,
30
+ Migration20260903T101748TransactionalEmailsNamespaceBlockNames,
31
+ ];
32
+ export { Migration20260629T113442TransactionalEmailsInit, Migration20260801T111001TransactionalEmailsEmailDefaultsReseed, Migration20260903T101748TransactionalEmailsNamespaceBlockNames, };
33
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/migrations/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAEH,OAAO,EAAE,+CAA+C,EAAE,MAAM,gDAAgD,CAAC;AACjH,OAAO,EAAE,8DAA8D,EAAE,MAAM,iEAAiE,CAAC;AACjJ,OAAO,EAAE,8DAA8D,EAAE,MAAM,iEAAiE,CAAC;AAEjJ,MAAM,CAAC,MAAM,UAAU,GAAG;IACxB,+CAA+C;IAC/C,8DAA8D;IAC9D,8DAA8D;CAC/D,CAAC;AAEF,OAAO,EACL,+CAA+C,EAC/C,8DAA8D,EAC9D,8DAA8D,GAC/D,CAAC"}
@@ -0,0 +1,176 @@
1
+ ---
2
+ title: transactional_emails
3
+ description: Admin-editable transactional emails — subject, content and look, globally and per sales channel
4
+ ---
5
+
6
+ # `transactional_emails`
7
+
8
+ Admin-editable transactional emails. Lets operators change the **subject**,
9
+ **content**, and **look** of every transactional email the platform sends —
10
+ globally and per **Sales Channel** — from the Admin UI, using an
11
+ email-client-safe WYSIWYG editor consistent with the CMS Page Builder. Each
12
+ email is described by a **definition** registered by the module that owns it
13
+ (orders, returns, organizations, inventory, …), which supplies a default
14
+ subject + default content and the set of variables the business logic
15
+ substitutes at send time.
16
+
17
+ ## Concepts
18
+
19
+ - **Definition** — a registered email identified by a unique `code` (e.g.
20
+ `order_confirmation`). Carries the owning module, declared variables (with
21
+ sample values for preview), supported languages, and the module-provided
22
+ default subject + content. Reconciled into `transactional_emails` at boot;
23
+ orphaned definitions (owning module uninstalled) are pruned.
24
+ - **Content** — the admin's per-scope, per-language customization, stored in
25
+ `transactional_email_contents`. Resolution at send time is
26
+ **per-channel → global → module default**, with a fallback to the channel's
27
+ default language. Absence of a row means "fall back"; **Reset** deletes it.
28
+ - **Blocks & Templates** — reusable email-safe fragments. Blocks are embedded by
29
+ `code` via `EmailInsertBlock`. Email templates remain a separate admin list
30
+ (apply/save outside the canvas); `EmailInsertTemplate` is withdrawn from the
31
+ palette (legacy trees still render). The seeded system blocks
32
+ `default_email_header` (renders branding via `EmailLogo` / `{{var branding.logoUrl}}`) and `default_email_footer`
33
+ are auto-included in the default content.
34
+ - **Branding** — header logo, accent color, and the default header/footer block
35
+ codes, resolved per scope through the Settings module
36
+ (`transactional_emails.*`). The resolved logo is exposed to every email as
37
+ `{{var branding.logoUrl}}`.
38
+ - **Variables** — Magento-2-style directives over the content + subject:
39
+ `{{var path}}`, `{{if path}}…{{/if}}`, `{{for alias in list}}…{{/for}}`. Values
40
+ are HTML-escaped in the HTML body and raw in the plain-text alternative. A
41
+ missing value resolves to empty — an email is never sent with an unresolved
42
+ `{{…}}` and a missing variable never fails a send. The admin editor exposes an
43
+ **Insert variable** picker (subject, plain text fields, and rich text toolbar)
44
+ fed by the email's declared variables plus branding keys.
45
+
46
+ ## Email editor
47
+
48
+ The transactional (and newsletter) editors share `EmailEditorPane`:
49
+
50
+ - Email-safe Puck palette from `@endora-commerce/email-components` (no CMS breakpoints /
51
+ responsive stacking). **Row** opens a column-layout picker (1–6 columns) like
52
+ CMS; columns use a fixed table at send time and the CMS 12-col grid on the
53
+ canvas. **Column** is not listed in the palette (only inside Row). **Table** is
54
+ the data grid (headers + rows, like CMS SimpleTable).
55
+ - The canvas is fixed at mail width (**600px**), with an Outline panel like CMS.
56
+ Use **Preview email** for a modal HTML render with sample variable data and
57
+ **Desktop mail (600px)** / **Narrow (320px)** options (fluid `max-width:600px`
58
+ shell so narrow preview does not overflow).
59
+ **Save as template** / **Apply template** reuse the CMS header actions against
60
+ transactional email templates (`email_templates`).
61
+ - `EmailLogo` shows the branding logo (no URL field). `EmailImage` supports URL or
62
+ asset library, same as CMS. Color fields use the shared CMS color palette.
63
+ Branding logo in Settings (`*_asset_id`) uses the Assets Library picker.
64
+ - `EmailRichText` reuses the CMS Rich Content TipTap editor (plus Variable).
65
+ - `EmailProductCard` picks a catalog product; `EmailOrderSummary` is column/totals
66
+ toggles and appears in the palette only when the email declares `order.items`
67
+ (today: **Order confirmation** only). Order confirmation also exposes labeled
68
+ detail blocks: Order ID, Billing/Shipping address, Summary, Applied discounts,
69
+ Delivery method, Payment method (each gated on its template variable).
70
+ - Content blocks include layout (`EmailSection`, `EmailRow`/`EmailColumn`, table,
71
+ spacer, divider),
72
+ copy (`EmailText`, `EmailRichText`, headings, callout, footer/legal), media
73
+ (`EmailImage`, `EmailLogo`), commerce (`EmailProductCard`, `EmailOrderSummary`
74
+ and the order detail blocks above), `EmailSocial` (icons + labels + per-link
75
+ enable), and `EmailInsertBlock`.
76
+
77
+ ## Rendering
78
+
79
+ Rendering is performed **server-side** by the first-party `@endora-commerce/email-components`
80
+ package (React-free): the Puck content tree is walked and emitted as
81
+ table-based, inline-styled, email-client-safe HTML plus a plain-text
82
+ alternative. The admin editor reuses the same email-safe component palette.
83
+ `EmailRichText` HTML is whitelist-sanitized (`p/strong/em/u/a/ul/ol/li/br`)
84
+ before send; directive markers in text are preserved.
85
+
86
+ ## Registering an email from a module
87
+
88
+ ```ts
89
+ // manifest.ts
90
+ transactionalEmails: [
91
+ { code: 'order_confirmation', name: 'Order confirmation', group: 'orders',
92
+ variables: [{ key: 'order.businessId', label: 'Order number', sampleValue: 'ORD-1042' }] },
93
+ ],
94
+
95
+ // plugin.ts (default subject + content)
96
+ emailDefaultsRegistry.register('order_confirmation', { defaultSubject, defaultContent });
97
+ ```
98
+
99
+ The owning module sends through the `TransactionalEmailSender` port (injected by
100
+ composition), preserving its existing idempotency `messageId`:
101
+
102
+ ```ts
103
+ await sender.send({
104
+ code: 'order_confirmation',
105
+ salesChannelId, language, to,
106
+ messageId: `order_confirmation:${order.id}`,
107
+ variables: { order: { businessId, items: [...] }, customer: { firstName } },
108
+ });
109
+ ```
110
+
111
+ When the sender is not wired, modules fall back to their legacy in-code builders,
112
+ so behavior is unchanged in environments without the module.
113
+
114
+ ## Preview
115
+
116
+ `POST /api/v1/admin/transactional-emails/{code}/preview` renders an email with
117
+ each variable's declared sample value (and any unsaved draft content), returning
118
+ `{ subject, html, text }`. The admin opens the HTML in a new tab.
119
+
120
+ ## Switching an email off
121
+
122
+ The module itself is **non-deactivatable**: every deployment sends account
123
+ verification, invitations and order mail through it, so `/platform/modules`
124
+ renders it locked with that reason rather than as a toggle. The granularity that
125
+ *is* offered is the individual email — the list at `/transactional-emails`
126
+ carries a per-row switch, backed by
127
+ `POST /api/v1/admin/transactional-emails/{code}/activation` with `{ active }`.
128
+ The flip runs through the Command Bus, so it is audited as
129
+ `transactional_email.activation.set` and reversible; it drops no content, no
130
+ override and no per-channel customization. A deactivated email answers
131
+ `{ status: 'deactivated' }` at send time and **no fallback mail goes out**.
132
+
133
+ Emails required to create an account or to get back into one may not be switched
134
+ off at all: today `email_verification` and `organization_invitation`. The
135
+ declaration lives on the owning module's registry entry
136
+ (`EmailDefaults.nonDeactivatable`), not in a list held by this module or by the
137
+ Admin UI, and a refused flip answers `409 TRANSACTIONAL_EMAIL_NOT_DEACTIVATABLE`
138
+ carrying that module's own reason — the same shape the module-level refusal uses.
139
+
140
+ ## Delivery record
141
+
142
+ Every send leaves one row in `email_deliveries`, a table owned by the `email`
143
+ module — the transport is where a message's fate is decided, so it is where the
144
+ record of that fate lives. The row carries the recipient, the email code, the
145
+ sales channel, the message id, the business document the message delivered (an
146
+ invoice, typically), the outcome and the moment it was attempted.
147
+
148
+ The outcome is one of three, and the split is the point of the table:
149
+
150
+ | Status | Means | Typical reason |
151
+ | --- | --- | --- |
152
+ | `sent` | the transport accepted the message | — |
153
+ | `suppressed` | the platform deliberately did not send | `deactivated` (an operator switched this email off), `duplicate_message_id` |
154
+ | `failed` | the message was meant to go out and did not | `transport_error`, `no_transport`, `no_definition` |
155
+
156
+ An operator asking "did the customer get the invoice" therefore gets an answer
157
+ that outlives a log rotation, and one that does not confuse a configuration they
158
+ chose with an outage. This is **best-effort delivery with a durable record**, not
159
+ guaranteed delivery: there is no retry queue and no outbox, a resend stays an
160
+ operator action, and a message lost between the business write and the transport
161
+ call is lost. There is no admin screen over the table yet — it is read from the
162
+ database.
163
+
164
+ ## Permissions
165
+
166
+ - `transactional_emails:read` — view emails, blocks, templates, branding, preview.
167
+ - `transactional_emails:write` — edit content, branding, and manage blocks/templates.
168
+
169
+ ## Registered emails
170
+
171
+ All pre-existing transactional emails route through this mechanism: orders
172
+ (confirmation, comment, reorder, admin-created), returns (authorized, rejected),
173
+ organizations (verification, invitation, new-registration), and inventory
174
+ (low-stock, back-in-stock). Two net-new emails are also registered and dispatched
175
+ via event subscribers: payments (`payment_status_changed`, on payment
176
+ received/failed) and shipments (`shipment_created`, on shipment created).
package/i18n/en.json ADDED
@@ -0,0 +1,92 @@
1
+ {
2
+ "actions.openTransactionalEmails.label": "Transactional emails",
3
+ "actions.openTransactionalEmails.description": "Edit transactional email content, look, blocks, and templates",
4
+ "actions.openEmailTemplates.label": "Email templates",
5
+ "actions.openEmailTemplates.description": "Manage transactional email layout templates",
6
+ "settings.logoAssetId.label": "Email header logo",
7
+ "settings.accentColor.label": "Email accent color",
8
+ "settings.headerBlockCode.label": "Default header block",
9
+ "settings.footerBlockCode.label": "Default footer block",
10
+ "page.title": "Transactional Emails",
11
+ "page.description": "Edit the content and look of transactional emails globally and per sales channel.",
12
+ "list.title": "Email templates",
13
+ "list.empty": "No transactional emails found.",
14
+ "list.search": "Search by name or code…",
15
+ "activation.column": "Status",
16
+ "activation.on": "On",
17
+ "activation.off": "Off",
18
+ "activation.locked": "Always on",
19
+ "activation.action.enable": "Switch on",
20
+ "activation.action.disable": "Switch off",
21
+ "editor.title": "Email",
22
+ "editor.subject": "Subject",
23
+ "editor.content": "Content",
24
+ "editor.save": "Save",
25
+ "editor.reset": "Reset to default",
26
+ "editor.preview": "Preview email",
27
+ "scope.global": "All channels (default)",
28
+ "scope.channel": "Sales channel",
29
+ "blocks.title": "Email blocks",
30
+ "templates.title": "Email templates",
31
+ "branding.title": "Branding",
32
+ "branding.logo": "Header logo",
33
+ "branding.accent": "Accent color",
34
+ "editor.viewport.desktop": "Desktop mail (600px)",
35
+ "editor.viewport.narrow": "Narrow (320px)",
36
+ "editor.variables.insert": "Insert variable",
37
+ "editor.variables.search": "Search variables…",
38
+ "editor.variables.button": "Variable",
39
+ "editor.livePreview": "Live HTML preview",
40
+ "components.EmailSection": "Section",
41
+ "components.EmailLogo": "Logo",
42
+ "components.EmailRichText": "Rich text",
43
+ "components.EmailProductCard": "Product card",
44
+ "components.EmailOrderSummary": "Order summary",
45
+ "components.EmailTable": "Table",
46
+ "components.EmailSocial": "Social links",
47
+ "components.EmailCallout": "Callout",
48
+ "components.EmailFooterLegal": "Footer / legal",
49
+ "actions.openEmailBlocks.label": "Email blocks",
50
+ "actions.openEmailBlocks.description": "Reusable email content blocks",
51
+ "nav.transactionalEmails.label": "Transactional Emails",
52
+ "nav.emailBlocks.label": "Email Blocks",
53
+ "nav.emailTemplates.label": "Email Templates",
54
+ "blocks.emailHeading.label": "Heading",
55
+ "blocks.emailHeading.description": "A heading line in an e-mail.",
56
+ "blocks.emailText.label": "Text",
57
+ "blocks.emailText.description": "A paragraph of e-mail-safe text.",
58
+ "blocks.emailRichText.label": "Rich text",
59
+ "blocks.emailRichText.description": "Formatted e-mail text edited in a rich-text editor.",
60
+ "blocks.emailButton.label": "Button",
61
+ "blocks.emailButton.description": "A call-to-action button rendered as an e-mail-safe table.",
62
+ "blocks.emailImage.label": "Image",
63
+ "blocks.emailImage.description": "A single image with alternative text.",
64
+ "blocks.emailLogo.label": "Logo",
65
+ "blocks.emailLogo.description": "The shop logo, taken from the e-mail branding settings.",
66
+ "blocks.emailSocial.label": "Social links",
67
+ "blocks.emailSocial.description": "Links to the shop social profiles.",
68
+ "blocks.emailCallout.label": "Callout",
69
+ "blocks.emailCallout.description": "A highlighted box drawing attention to one message.",
70
+ "blocks.emailFooterLegal.label": "Footer / legal",
71
+ "blocks.emailFooterLegal.description": "The legal footer with company details and an unsubscribe link.",
72
+ "blocks.emailSection.label": "Section",
73
+ "blocks.emailSection.description": "A full-width section that holds rows.",
74
+ "blocks.emailRow.label": "Row",
75
+ "blocks.emailRow.description": "A row that holds e-mail columns.",
76
+ "blocks.emailTable.label": "Table",
77
+ "blocks.emailTable.description": "A table of headers and rows.",
78
+ "blocks.emailDivider.label": "Divider",
79
+ "blocks.emailDivider.description": "A horizontal rule between blocks.",
80
+ "blocks.emailSpacer.label": "Spacer",
81
+ "blocks.emailSpacer.description": "Vertical whitespace between blocks.",
82
+ "blocks.emailInsertBlock.label": "Insert block",
83
+ "blocks.emailInsertBlock.description": "Embeds a reusable e-mail block by its code.",
84
+ "blocks.emailInsertTemplate.label": "Insert template",
85
+ "blocks.emailInsertTemplate.description": "Embeds an e-mail template by its code.",
86
+ "blocks.emailColumn.label": "Column",
87
+ "blocks.emailColumn.description": "A column inside an e-mail row. Insertable only into a row.",
88
+ "blocks.category.content": "Content",
89
+ "blocks.category.layout": "Layout",
90
+ "blocks.category.embeds": "Embeds",
91
+ "blocks.category.internal": "Internal"
92
+ }