@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.
- package/LICENSE +21 -0
- package/README.md +63 -0
- package/dist/admin/api/transactional-emails-client.d.ts +43 -0
- package/dist/admin/api/transactional-emails-client.d.ts.map +1 -0
- package/dist/admin/api/transactional-emails-client.js +47 -0
- package/dist/admin/api/transactional-emails-client.js.map +1 -0
- package/dist/admin/components/BrandingPanel.d.ts +10 -0
- package/dist/admin/components/BrandingPanel.d.ts.map +1 -0
- package/dist/admin/components/BrandingPanel.js +58 -0
- package/dist/admin/components/BrandingPanel.js.map +1 -0
- package/dist/admin/components/EmailEditorPane.d.ts +7 -0
- package/dist/admin/components/EmailEditorPane.d.ts.map +1 -0
- package/dist/admin/components/EmailEditorPane.js +48 -0
- package/dist/admin/components/EmailEditorPane.js.map +1 -0
- package/dist/admin/index.d.ts +46 -0
- package/dist/admin/index.d.ts.map +1 -0
- package/dist/admin/index.js +92 -0
- package/dist/admin/index.js.map +1 -0
- package/dist/admin/pages/EmailBlockEditorPage.d.ts +16 -0
- package/dist/admin/pages/EmailBlockEditorPage.d.ts.map +1 -0
- package/dist/admin/pages/EmailBlockEditorPage.js +19 -0
- package/dist/admin/pages/EmailBlockEditorPage.js.map +1 -0
- package/dist/admin/pages/EmailBlocksPage.d.ts +8 -0
- package/dist/admin/pages/EmailBlocksPage.d.ts.map +1 -0
- package/dist/admin/pages/EmailBlocksPage.js +55 -0
- package/dist/admin/pages/EmailBlocksPage.js.map +1 -0
- package/dist/admin/pages/EmailEditor.d.ts +8 -0
- package/dist/admin/pages/EmailEditor.d.ts.map +1 -0
- package/dist/admin/pages/EmailEditor.js +126 -0
- package/dist/admin/pages/EmailEditor.js.map +1 -0
- package/dist/admin/pages/EmailFragmentEditor.d.ts +9 -0
- package/dist/admin/pages/EmailFragmentEditor.d.ts.map +1 -0
- package/dist/admin/pages/EmailFragmentEditor.js +93 -0
- package/dist/admin/pages/EmailFragmentEditor.js.map +1 -0
- package/dist/admin/pages/EmailTemplateEditorPage.d.ts +12 -0
- package/dist/admin/pages/EmailTemplateEditorPage.d.ts.map +1 -0
- package/dist/admin/pages/EmailTemplateEditorPage.js +15 -0
- package/dist/admin/pages/EmailTemplateEditorPage.js.map +1 -0
- package/dist/admin/pages/EmailTemplatesPage.d.ts +8 -0
- package/dist/admin/pages/EmailTemplatesPage.d.ts.map +1 -0
- package/dist/admin/pages/EmailTemplatesPage.js +55 -0
- package/dist/admin/pages/EmailTemplatesPage.js.map +1 -0
- package/dist/admin/pages/EmailsList.d.ts +8 -0
- package/dist/admin/pages/EmailsList.d.ts.map +1 -0
- package/dist/admin/pages/EmailsList.js +85 -0
- package/dist/admin/pages/EmailsList.js.map +1 -0
- package/dist/backend/commands/email-activation.commands.d.ts +42 -0
- package/dist/backend/commands/email-activation.commands.d.ts.map +1 -0
- package/dist/backend/commands/email-activation.commands.js +51 -0
- package/dist/backend/commands/email-activation.commands.js.map +1 -0
- package/dist/backend/entities/email-block-sales-channel.entity.d.ts +11 -0
- package/dist/backend/entities/email-block-sales-channel.entity.d.ts.map +1 -0
- package/dist/backend/entities/email-block-sales-channel.entity.js +39 -0
- package/dist/backend/entities/email-block-sales-channel.entity.js.map +1 -0
- package/dist/backend/entities/email-block.entity.d.ts +24 -0
- package/dist/backend/entities/email-block.entity.d.ts.map +1 -0
- package/dist/backend/entities/email-block.entity.js +85 -0
- package/dist/backend/entities/email-block.entity.js.map +1 -0
- package/dist/backend/entities/email-template-sales-channel.entity.d.ts +10 -0
- package/dist/backend/entities/email-template-sales-channel.entity.d.ts.map +1 -0
- package/dist/backend/entities/email-template-sales-channel.entity.js +38 -0
- package/dist/backend/entities/email-template-sales-channel.entity.js.map +1 -0
- package/dist/backend/entities/email-template.entity.d.ts +19 -0
- package/dist/backend/entities/email-template.entity.d.ts.map +1 -0
- package/dist/backend/entities/email-template.entity.js +76 -0
- package/dist/backend/entities/email-template.entity.js.map +1 -0
- package/dist/backend/entities/transactional-email-content.entity.d.ts +25 -0
- package/dist/backend/entities/transactional-email-content.entity.d.ts.map +1 -0
- package/dist/backend/entities/transactional-email-content.entity.js +77 -0
- package/dist/backend/entities/transactional-email-content.entity.js.map +1 -0
- package/dist/backend/entities/transactional-email.entity.d.ts +32 -0
- package/dist/backend/entities/transactional-email.entity.d.ts.map +1 -0
- package/dist/backend/entities/transactional-email.entity.js +100 -0
- package/dist/backend/entities/transactional-email.entity.js.map +1 -0
- package/dist/backend/index.d.ts +105 -0
- package/dist/backend/index.d.ts.map +1 -0
- package/dist/backend/index.js +126 -0
- package/dist/backend/index.js.map +1 -0
- package/dist/backend/plugin.d.ts +49 -0
- package/dist/backend/plugin.d.ts.map +1 -0
- package/dist/backend/plugin.js +54 -0
- package/dist/backend/plugin.js.map +1 -0
- package/dist/backend/routes.admin.d.ts +23 -0
- package/dist/backend/routes.admin.d.ts.map +1 -0
- package/dist/backend/routes.admin.js +133 -0
- package/dist/backend/routes.admin.js.map +1 -0
- package/dist/backend/services/branding.service.d.ts +63 -0
- package/dist/backend/services/branding.service.d.ts.map +1 -0
- package/dist/backend/services/branding.service.js +119 -0
- package/dist/backend/services/branding.service.js.map +1 -0
- package/dist/backend/services/content-resolver.d.ts +28 -0
- package/dist/backend/services/content-resolver.d.ts.map +1 -0
- package/dist/backend/services/content-resolver.js +64 -0
- package/dist/backend/services/content-resolver.js.map +1 -0
- package/dist/backend/services/email-block.service.d.ts +25 -0
- package/dist/backend/services/email-block.service.d.ts.map +1 -0
- package/dist/backend/services/email-block.service.js +162 -0
- package/dist/backend/services/email-block.service.js.map +1 -0
- package/dist/backend/services/email-builder-registry.d.ts +9 -0
- package/dist/backend/services/email-builder-registry.d.ts.map +1 -0
- package/dist/backend/services/email-builder-registry.js +14 -0
- package/dist/backend/services/email-builder-registry.js.map +1 -0
- package/dist/backend/services/email-defaults-registry.d.ts +63 -0
- package/dist/backend/services/email-defaults-registry.d.ts.map +1 -0
- package/dist/backend/services/email-defaults-registry.js +60 -0
- package/dist/backend/services/email-defaults-registry.js.map +1 -0
- package/dist/backend/services/email-template.service.d.ts +23 -0
- package/dist/backend/services/email-template.service.d.ts.map +1 -0
- package/dist/backend/services/email-template.service.js +155 -0
- package/dist/backend/services/email-template.service.js.map +1 -0
- package/dist/backend/services/embed-resolver.d.ts +21 -0
- package/dist/backend/services/embed-resolver.d.ts.map +1 -0
- package/dist/backend/services/embed-resolver.js +68 -0
- package/dist/backend/services/embed-resolver.js.map +1 -0
- package/dist/backend/services/manifest-reconciler.d.ts +28 -0
- package/dist/backend/services/manifest-reconciler.d.ts.map +1 -0
- package/dist/backend/services/manifest-reconciler.js +98 -0
- package/dist/backend/services/manifest-reconciler.js.map +1 -0
- package/dist/backend/services/template-email.d.ts +26 -0
- package/dist/backend/services/template-email.d.ts.map +1 -0
- package/dist/backend/services/template-email.js +69 -0
- package/dist/backend/services/template-email.js.map +1 -0
- package/dist/backend/services/transactional-email.service.d.ts +138 -0
- package/dist/backend/services/transactional-email.service.d.ts.map +1 -0
- package/dist/backend/services/transactional-email.service.js +410 -0
- package/dist/backend/services/transactional-email.service.js.map +1 -0
- package/dist/manifest.d.ts +195 -0
- package/dist/manifest.d.ts.map +1 -0
- package/dist/manifest.js +367 -0
- package/dist/manifest.js.map +1 -0
- package/dist/migrations/20260629T113442_transactional_emails_init.d.ts +6 -0
- package/dist/migrations/20260629T113442_transactional_emails_init.d.ts.map +1 -0
- package/dist/migrations/20260629T113442_transactional_emails_init.js +133 -0
- package/dist/migrations/20260629T113442_transactional_emails_init.js.map +1 -0
- package/dist/migrations/20260801T111001_transactional_emails_email_defaults_reseed.d.ts +6 -0
- package/dist/migrations/20260801T111001_transactional_emails_email_defaults_reseed.d.ts.map +1 -0
- package/dist/migrations/20260801T111001_transactional_emails_email_defaults_reseed.js +49 -0
- package/dist/migrations/20260801T111001_transactional_emails_email_defaults_reseed.js.map +1 -0
- package/dist/migrations/20260903T101748_transactional_emails_namespace_block_names.d.ts +6 -0
- package/dist/migrations/20260903T101748_transactional_emails_namespace_block_names.d.ts.map +1 -0
- package/dist/migrations/20260903T101748_transactional_emails_namespace_block_names.js +60 -0
- package/dist/migrations/20260903T101748_transactional_emails_namespace_block_names.js.map +1 -0
- package/dist/migrations/index.d.ts +29 -0
- package/dist/migrations/index.d.ts.map +1 -0
- package/dist/migrations/index.js +33 -0
- package/dist/migrations/index.js.map +1 -0
- package/docs/transactional-emails.md +176 -0
- package/i18n/en.json +92 -0
- package/i18n/pl.json +92 -0
- package/package.json +113 -0
- 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
|
+
}
|