@cosmicdrift/kumiko-samples 0.363.0 → 0.364.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 (140) hide show
  1. package/package.json +1 -1
  2. package/packages/bundled-features/package.json +7 -1
  3. package/packages/bundled-features/src/audit/changes.json +6 -0
  4. package/packages/bundled-features/src/audit/handlers/list.query.ts +10 -5
  5. package/packages/bundled-features/src/auth-email-password/bootstrap.ts +29 -8
  6. package/packages/bundled-features/src/auth-email-password/changes.json +37 -0
  7. package/packages/bundled-features/src/auth-email-password/email-rate-limits.ts +16 -0
  8. package/packages/bundled-features/src/auth-email-password/handlers/change-password.write.ts +6 -2
  9. package/packages/bundled-features/src/auth-email-password/handlers/logout.write.ts +7 -2
  10. package/packages/bundled-features/src/auth-email-password/handlers/signup-request.write.ts +2 -1
  11. package/packages/bundled-features/src/auth-email-password/handlers/token-request-handler.ts +2 -1
  12. package/packages/bundled-features/src/auth-email-password/web/auth-gate.tsx +24 -5
  13. package/packages/bundled-features/src/billing-foundation/changes.json +36 -0
  14. package/packages/bundled-features/src/billing-foundation/consumer-protection/confirmation-mail.ts +57 -5
  15. package/packages/bundled-features/src/billing-foundation/consumer-protection/consent-gate.ts +22 -1
  16. package/packages/bundled-features/src/billing-foundation/consumer-protection/mail-notifications.ts +4 -1
  17. package/packages/bundled-features/src/billing-foundation/consumer-protection/register-termination.ts +11 -1
  18. package/packages/bundled-features/src/billing-foundation/consumer-protection/termination-mail.ts +9 -3
  19. package/packages/bundled-features/src/billing-foundation/consumer-protection/termination-receipt-limit.ts +41 -0
  20. package/packages/bundled-features/src/billing-foundation/consumer-protection/termination-record.ts +10 -1
  21. package/packages/bundled-features/src/billing-foundation/consumer-protection/termination-request.ts +35 -19
  22. package/packages/bundled-features/src/billing-foundation/consumer-protection/termination-texts.ts +9 -0
  23. package/packages/bundled-features/src/billing-foundation/consumer-protection/withdrawal-period.ts +35 -0
  24. package/packages/bundled-features/src/billing-foundation/handlers/create-checkout-session.write.ts +11 -3
  25. package/packages/bundled-features/src/billing-foundation/handlers/process-event.write.ts +10 -4
  26. package/packages/bundled-features/src/billing-foundation/i18n.ts +3 -0
  27. package/packages/bundled-features/src/billing-foundation/subscription-tier-sync.ts +17 -5
  28. package/packages/bundled-features/src/billing-foundation/types.ts +7 -2
  29. package/packages/bundled-features/src/cap-counter/calendar-cap-usage-reservation.ts +2 -0
  30. package/packages/bundled-features/src/cap-counter/changes.json +6 -0
  31. package/packages/bundled-features/src/cap-counter/enforce-cap.ts +75 -5
  32. package/packages/bundled-features/src/cap-counter/index.ts +1 -0
  33. package/packages/bundled-features/src/cap-counter/with-cap-enforcement.ts +3 -0
  34. package/packages/bundled-features/src/channel-email/email-channel.ts +17 -4
  35. package/packages/bundled-features/src/config/changes.json +6 -0
  36. package/packages/bundled-features/src/config/handlers/set.write.ts +2 -2
  37. package/packages/bundled-features/src/config/resolver.ts +15 -12
  38. package/packages/bundled-features/src/config/write-helpers.ts +23 -0
  39. package/packages/bundled-features/src/crypto-shredding/changes.json +6 -0
  40. package/packages/bundled-features/src/crypto-shredding/handlers/forget-subject.write.ts +48 -27
  41. package/packages/bundled-features/src/custom-fields/changes.json +6 -0
  42. package/packages/bundled-features/src/custom-fields/handlers/update-tenant-field.write.ts +0 -1
  43. package/packages/bundled-features/src/delivery/attachment-ref.ts +21 -0
  44. package/packages/bundled-features/src/delivery/changes.json +18 -0
  45. package/packages/bundled-features/src/delivery/channel-context.ts +31 -8
  46. package/packages/bundled-features/src/delivery/index.ts +2 -0
  47. package/packages/bundled-features/src/delivery/jobs.ts +51 -14
  48. package/packages/bundled-features/src/delivery/types.ts +7 -1
  49. package/packages/bundled-features/src/file-derivatives/handlers/public-variant.query.ts +1 -1
  50. package/packages/bundled-features/src/file-foundation/changes.json +6 -0
  51. package/packages/bundled-features/src/file-foundation/read-file-ref-content.ts +6 -3
  52. package/packages/bundled-features/src/files-tenant-data/changes.json +6 -0
  53. package/packages/bundled-features/src/files-tenant-data/hooks.ts +4 -0
  54. package/packages/bundled-features/src/inbound-mail-foundation/changes.json +6 -0
  55. package/packages/bundled-features/src/inbound-mail-foundation/oauth-access-token.ts +13 -8
  56. package/packages/bundled-features/src/inbound-mail-foundation/watch-supervisor.ts +3 -1
  57. package/packages/bundled-features/src/jobs/changes.json +19 -0
  58. package/packages/bundled-features/src/jobs/db/queries/stale-run-sweep.ts +12 -4
  59. package/packages/bundled-features/src/jobs/feature.ts +7 -4
  60. package/packages/bundled-features/src/jobs/handlers/list.query.ts +3 -1
  61. package/packages/bundled-features/src/jobs/handlers/stale-run-sweep.job.ts +5 -2
  62. package/packages/bundled-features/src/jobs/job-run-logger.ts +52 -17
  63. package/packages/bundled-features/src/jobs/tenant-job-run-table.ts +7 -2
  64. package/packages/bundled-features/src/ledger/changes.json +7 -0
  65. package/packages/bundled-features/src/ledger/entity.ts +3 -1
  66. package/packages/bundled-features/src/renderer-foundation/types.ts +2 -0
  67. package/packages/bundled-features/src/renderer-pdf/changes.json +12 -0
  68. package/packages/bundled-features/src/renderer-pdf/render-table-pdf.ts +162 -82
  69. package/packages/bundled-features/src/secrets/changes.json +6 -0
  70. package/packages/bundled-features/src/secrets/feature.ts +4 -3
  71. package/packages/bundled-features/src/secrets/secrets-context.ts +23 -3
  72. package/packages/bundled-features/src/shared/changes.json +6 -0
  73. package/packages/bundled-features/src/shared/chunked-entity-migration.ts +4 -1
  74. package/packages/bundled-features/src/shared/mail-template-body.ts +17 -0
  75. package/packages/bundled-features/src/subscription-stripe/changes.json +6 -0
  76. package/packages/bundled-features/src/subscription-stripe/plugin-methods.ts +19 -11
  77. package/packages/bundled-features/src/subscription-stripe/verify-webhook.ts +3 -1
  78. package/packages/bundled-features/src/template-resolver/README.md +4 -2
  79. package/packages/bundled-features/src/template-resolver/api.ts +3 -2
  80. package/packages/bundled-features/src/template-resolver/changes.json +68 -0
  81. package/packages/bundled-features/src/template-resolver/compile-document-body.ts +77 -13
  82. package/packages/bundled-features/src/template-resolver/constants.ts +12 -2
  83. package/packages/bundled-features/src/template-resolver/document-schema.ts +26 -4
  84. package/packages/bundled-features/src/template-resolver/feature.ts +4 -1
  85. package/packages/bundled-features/src/template-resolver/handlers/copy-locale.write.ts +31 -13
  86. package/packages/bundled-features/src/template-resolver/handlers/diff-to-system.query.ts +5 -44
  87. package/packages/bundled-features/src/template-resolver/handlers/discard-draft.write.ts +52 -0
  88. package/packages/bundled-features/src/template-resolver/handlers/export-bundle.query.ts +56 -1
  89. package/packages/bundled-features/src/template-resolver/handlers/find-by-id.query.ts +6 -3
  90. package/packages/bundled-features/src/template-resolver/handlers/layout-source.query.ts +73 -30
  91. package/packages/bundled-features/src/template-resolver/handlers/list.query.ts +14 -11
  92. package/packages/bundled-features/src/template-resolver/handlers/override-diff.ts +49 -0
  93. package/packages/bundled-features/src/template-resolver/handlers/overview.query.ts +90 -17
  94. package/packages/bundled-features/src/template-resolver/handlers/resource-upload.write.ts +1 -1
  95. package/packages/bundled-features/src/template-resolver/handlers/send-test.write.ts +39 -10
  96. package/packages/bundled-features/src/template-resolver/handlers/template-gates.ts +14 -2
  97. package/packages/bundled-features/src/template-resolver/handlers/template-write.ts +54 -4
  98. package/packages/bundled-features/src/template-resolver/handlers/tenant-overrides.query.ts +96 -14
  99. package/packages/bundled-features/src/template-resolver/handlers/toggle-status.write.ts +51 -24
  100. package/packages/bundled-features/src/template-resolver/handlers/upsert-tenant.write.ts +1 -1
  101. package/packages/bundled-features/src/template-resolver/handlers/usage.query.ts +16 -2
  102. package/packages/bundled-features/src/template-resolver/index.ts +12 -1
  103. package/packages/bundled-features/src/template-resolver/layout-appearance.ts +40 -0
  104. package/packages/bundled-features/src/template-resolver/pdf-constants.ts +12 -0
  105. package/packages/bundled-features/src/template-resolver/qualified-names.ts +4 -0
  106. package/packages/bundled-features/src/template-resolver/seeding.ts +25 -4
  107. package/packages/bundled-features/src/template-resolver/store-template-resource.ts +2 -2
  108. package/packages/bundled-features/src/template-resolver/table.ts +13 -1
  109. package/packages/bundled-features/src/template-resolver/template-draft.ts +106 -0
  110. package/packages/bundled-features/src/template-resolver/template-meta.ts +87 -24
  111. package/packages/bundled-features/src/template-resolver/template-preview.ts +159 -56
  112. package/packages/bundled-features/src/template-resolver/template-resources.ts +44 -4
  113. package/packages/bundled-features/src/template-resolver/template-usage.ts +31 -11
  114. package/packages/bundled-features/src/template-resolver/template-validation.ts +1 -0
  115. package/packages/bundled-features/src/template-resolver/web/client-plugin.tsx +12 -8
  116. package/packages/bundled-features/src/template-resolver/web/document-editor-extensions.ts +63 -10
  117. package/packages/bundled-features/src/template-resolver/web/document-editor.tsx +12 -3
  118. package/packages/bundled-features/src/template-resolver/web/document-tiptap-editor.tsx +349 -134
  119. package/packages/bundled-features/src/template-resolver/web/document-toolbar-blocks.ts +11 -0
  120. package/packages/bundled-features/src/template-resolver/web/i18n.ts +25 -4
  121. package/packages/bundled-features/src/template-resolver/web/index.ts +7 -0
  122. package/packages/bundled-features/src/template-resolver/web/placeholder-line-editor.tsx +10 -3
  123. package/packages/bundled-features/src/tier-engine/changes.json +6 -0
  124. package/packages/bundled-features/src/tier-engine/feature.ts +11 -3
  125. package/packages/bundled-features/src/user/changes.json +6 -0
  126. package/packages/bundled-features/src/user/resolve-display-names.ts +8 -15
  127. package/packages/bundled-features/src/user/seeding.ts +19 -5
  128. package/packages/bundled-features/src/user-data-rights/changes.json +6 -0
  129. package/packages/bundled-features/src/user-profile/changes.json +1 -1
  130. package/packages/bundled-features/src/waitlist/options.ts +1 -1
  131. package/packages/bundled-features/src/workflow-runner/changes.json +6 -0
  132. package/packages/bundled-features/src/workflow-runner/event-trigger.ts +2 -1
  133. package/packages/bundled-features/src/workflow-runner/handlers/resume-run.write.ts +7 -1
  134. package/samples/apps/ui-walkthrough/package.json +9 -0
  135. package/samples/apps/ui-walkthrough/playwright.config.ts +40 -7
  136. package/samples/apps/ui-walkthrough/src/app/seed-templates.ts +87 -2
  137. package/samples/apps/ui-walkthrough/src/app/template-seed-data.ts +246 -30
  138. package/samples/apps/ui-walkthrough/src/features/billing/feature.ts +4 -0
  139. package/samples/apps/ui-walkthrough/src/run-config.ts +4 -0
  140. package/samples/apps/use-all-bundled/feature-manifest.json +5 -3
@@ -16,6 +16,9 @@ export type SeedTemplate = {
16
16
  // Document HTML marks a variable as an empty span; only link targets take {{name}}.
17
17
  const v = (name: string): string => `<span data-variable="${name}"></span>`;
18
18
 
19
+ // Resource key of the logo image the invoice mail embeds (uploaded by seed-templates.ts).
20
+ export const LOGO_RESOURCE_KEY = "logo-nordlicht.png";
21
+
19
22
  const AUTH_VARIABLE_SCHEMA = {
20
23
  appName: { type: "text", required: true },
21
24
  actionUrl: { type: "link", required: true },
@@ -44,16 +47,134 @@ const DOCUMENT_LINES_SCHEMA = {
44
47
  },
45
48
  } as const;
46
49
 
50
+ // Same variable names the billing notifications declare; labels follow the template language.
51
+ type InvoiceDocumentLabels = {
52
+ readonly recipientAddress: string;
53
+ readonly invoiceNumber: string;
54
+ readonly invoiceDate: string;
55
+ readonly invoiceTotal: string;
56
+ readonly netTotal: string;
57
+ readonly vatAmount: string;
58
+ readonly dueDate: string;
59
+ readonly lines: string;
60
+ readonly position: string;
61
+ readonly description: string;
62
+ readonly period: string;
63
+ readonly net: string;
64
+ readonly paymentReference: string;
65
+ readonly iban: string;
66
+ };
67
+
68
+ function invoiceDocumentSchema(labels: InvoiceDocumentLabels) {
69
+ return {
70
+ recipientAddress: {
71
+ type: "text",
72
+ required: true,
73
+ label: labels.recipientAddress,
74
+ example: "Familie Yilmaz, Lindenstraße 14, 24103 Kiel",
75
+ },
76
+ invoiceNumber: {
77
+ type: "text",
78
+ required: true,
79
+ label: labels.invoiceNumber,
80
+ example: "RE-2026-0142",
81
+ },
82
+ invoiceDate: { type: "date", required: true, label: labels.invoiceDate, example: "2026-10-01" },
83
+ dueDate: { type: "date", required: true, label: labels.dueDate, example: "2026-10-15" },
84
+ netTotal: {
85
+ type: "money",
86
+ label: labels.netTotal,
87
+ example: { amount: 107900, currency: "EUR" },
88
+ },
89
+ vatAmount: {
90
+ type: "money",
91
+ label: labels.vatAmount,
92
+ example: { amount: 20501, currency: "EUR" },
93
+ },
94
+ invoiceTotal: {
95
+ type: "money",
96
+ required: true,
97
+ label: labels.invoiceTotal,
98
+ example: { amount: 128401, currency: "EUR" },
99
+ },
100
+ lines: {
101
+ type: "list",
102
+ required: true,
103
+ label: labels.lines,
104
+ item: {
105
+ position: { type: "text", label: labels.position },
106
+ description: { type: "text", label: labels.description },
107
+ period: { type: "text", label: labels.period },
108
+ net: { type: "money", label: labels.net },
109
+ },
110
+ example: [
111
+ {
112
+ position: "1",
113
+ description: "Kaltmiete Lindenstraße 14, WE 3",
114
+ period: "Okt 2026",
115
+ net: { amount: 98000, currency: "EUR" },
116
+ },
117
+ {
118
+ position: "2",
119
+ description: "Stellplatz 7",
120
+ period: "Okt 2026",
121
+ net: { amount: 9900, currency: "EUR" },
122
+ },
123
+ ],
124
+ },
125
+ paymentReference: {
126
+ type: "text",
127
+ label: labels.paymentReference,
128
+ example: "RE-2026-0142 / Yilmaz",
129
+ },
130
+ iban: { type: "text", label: labels.iban, example: "DE12 2105 0170 0012 3456 78" },
131
+ } as const;
132
+ }
133
+
134
+ const INVOICE_DOCUMENT_SCHEMA_DE = invoiceDocumentSchema({
135
+ recipientAddress: "Empfängeradresse",
136
+ invoiceNumber: "Rechnungsnummer",
137
+ invoiceDate: "Rechnungsdatum",
138
+ invoiceTotal: "Rechnungsbetrag",
139
+ netTotal: "Nettobetrag",
140
+ vatAmount: "Umsatzsteuer",
141
+ dueDate: "Fälligkeitsdatum",
142
+ lines: "Rechnungspositionen",
143
+ position: "Nr.",
144
+ description: "Bezeichnung",
145
+ period: "Zeitraum",
146
+ net: "Netto",
147
+ paymentReference: "Zahlungsreferenz",
148
+ iban: "IBAN",
149
+ });
150
+
151
+ const INVOICE_DOCUMENT_SCHEMA_EN = invoiceDocumentSchema({
152
+ recipientAddress: "Recipient address",
153
+ invoiceNumber: "Invoice number",
154
+ invoiceDate: "Invoice date",
155
+ invoiceTotal: "Invoice total",
156
+ netTotal: "Net amount",
157
+ vatAmount: "VAT",
158
+ dueDate: "Due date",
159
+ lines: "Invoice lines",
160
+ position: "No.",
161
+ description: "Description",
162
+ period: "Period",
163
+ net: "Net",
164
+ paymentReference: "Payment reference",
165
+ iban: "IBAN",
166
+ });
167
+
47
168
  export const SYSTEM_PDF_LAYOUT: SeedTemplate = {
48
169
  slug: "default",
49
170
  kind: "pdf-layout",
50
171
  locale: "en",
51
172
  title: "Briefpapier",
52
173
  content: [
53
- `<div data-header><p><strong>${v("tenantName")}</strong></p></div>`,
174
+ `<div data-header><table><tbody><tr><td><img data-resource="logo" alt="Nordlicht"></td><td><p data-align="right" data-size="small">Am Hafen 12 · 24103 Kiel</p></td></tr></tbody></table></div>`,
54
175
  "<div data-content></div>",
55
176
  "<div data-signature></div>",
56
- `<div data-footer><p>${v("tenantName")} · Seite ${v("page")} von ${v("pages")}</p></div>`,
177
+ `<div data-footer><p>Seite ${v("page")} von ${v("pages")}</p><p>${v("tenantName")} · HRB 18422 Kiel · IBAN DE12 2105 0170 0012 3456 78</p></div>`,
57
178
  ].join(""),
58
179
  };
59
180
 
@@ -65,14 +186,14 @@ export const TENANT_MAIL_LAYOUT_VERSIONS: readonly string[] = [
65
186
  `<div data-footer><p>${v("tenantName")}</p></div>`,
66
187
  ].join(""),
67
188
  [
68
- `<div data-header><p><strong>${v("tenantName")}</strong></p><p>Ihre Hausverwaltung in Kiel</p></div>`,
189
+ '<div data-header><img data-resource="logo" alt="Nordlicht"></div>',
69
190
  "<div data-content></div>",
70
191
  `<div data-signature><p>Mit freundlichen Grüßen</p><p>${v("signature")}</p><p>${v("tenantName")}</p></div>`,
71
- `<div data-footer><p>${v("imprint")}</p><p><a href="https://nordlicht-immobilien.de/datenschutz">Datenschutz</a> · <a href="{{unsubscribeUrl}}">Abmelden</a></p></div>`,
192
+ `<div data-footer><p>${v("imprint")}</p><p><a href="https://nordlicht-immobilien.de/datenschutz">Datenschutz</a> · <a href="{{unsubscribeUrl}}">Abmeldelink</a></p></div>`,
72
193
  ].join(""),
73
194
  ];
74
195
 
75
- const LINES_TABLE = `<table><tbody><tr><th><p>Position</p></th><th><p>Betrag</p></th></tr><tr data-each="lines" data-repeat-header=""><td><p>${v("description")}</p></td><td><p>${v("amount")}</p></td></tr></tbody></table>`;
196
+ const LINES_TABLE = `<table><tbody><tr><th><p>Position</p></th><th><p data-align="right">Betrag</p></th></tr><tr data-each="lines" data-repeat-header=""><td><p>${v("description")}</p></td><td><p data-align="right">${v("amount")}</p></td></tr><tr><td><p><strong>Gesamt</strong></p></td><td><p data-align="right">${v("invoiceTotal")}</p></td></tr></tbody></table>`;
76
197
 
77
198
  // The registry of the billing notifications declares the variables, so these
78
199
  // templates carry no variableSchema of their own.
@@ -82,14 +203,14 @@ function invoiceIssuedBody(args: {
82
203
  }): string {
83
204
  const due = args.withDueDate ? ` Bitte überweisen Sie den Betrag bis zum ${v("dueDate")}.` : "";
84
205
  return [
85
- "<h2>Ihre Rechnung ist da</h2>",
206
+ `<h2>Ihre Rechnung ${v("invoiceNumber")}</h2>`,
86
207
  `<p>Guten Tag ${v("recipientName")},</p>`,
87
- `<p>${args.intro} <strong>${v("invoiceNumber")}</strong> über <strong>${v("invoiceTotal")}</strong>.${due}</p>`,
88
- '<a data-button href="{{invoiceUrl}}">Rechnung öffnen</a>',
208
+ `<p>${args.intro} ${v("invoiceNumber")} über ${v("invoiceTotal")}.${due} Die Rechnung finden Sie als PDF im Anhang und jederzeit online.</p>`,
209
+ '<a data-button href="{{invoiceUrl}}">Rechnung online ansehen</a>',
89
210
  LINES_TABLE,
90
211
  "<hr>",
91
- "<p>Mit freundlichen Grüßen</p>",
92
- `<p>${v("tenantName")}</p>`,
212
+ "<p>Bei Fragen zur Rechnung antworten Sie einfach auf diese E-Mail.</p>",
213
+ `<p>${v("signature")}</p>`,
93
214
  ].join("");
94
215
  }
95
216
 
@@ -101,6 +222,12 @@ const invoiceIssuedMeta = (subject: string) => ({
101
222
  previewText: "Rechnung {{invoiceNumber}} über {{invoiceTotal}}, online abrufbar",
102
223
  });
103
224
 
225
+ const tenantInvoiceMeta = (subject: string) => ({
226
+ ...invoiceIssuedMeta(subject),
227
+ senderName: "Nordlicht Immobilien GmbH",
228
+ replyTo: "buchhaltung@nordlicht-immobilien.de",
229
+ });
230
+
104
231
  // The platform default has a due date; the tenant drafts below drop it, which
105
232
  // is what blocks publishing in the walkthrough.
106
233
  export const SYSTEM_INVOICE_ISSUED: SeedTemplate = {
@@ -123,7 +250,7 @@ export const INVOICE_ISSUED_DRAFT_VERSIONS: readonly SeedTemplate[] = [
123
250
  intro: "anbei senden wir Ihnen die Rechnung",
124
251
  withDueDate: false,
125
252
  }),
126
- meta: invoiceIssuedMeta("Ihre Rechnung {{invoiceNumber}}"),
253
+ meta: tenantInvoiceMeta("Ihre Rechnung {{invoiceNumber}}"),
127
254
  },
128
255
  {
129
256
  slug: INVOICE_ISSUED_SLUG,
@@ -131,7 +258,7 @@ export const INVOICE_ISSUED_DRAFT_VERSIONS: readonly SeedTemplate[] = [
131
258
  locale: "de",
132
259
  title: "Rechnung versendet",
133
260
  content: invoiceIssuedBody({ intro: "anbei erhalten Sie die Rechnung", withDueDate: false }),
134
- meta: invoiceIssuedMeta("Ihre Rechnung {{invoiceNumber}} von {{tenantName}}"),
261
+ meta: tenantInvoiceMeta("Ihre Rechnung {{invoiceNumber}} von {{tenantName}}"),
135
262
  },
136
263
  ];
137
264
 
@@ -279,7 +406,22 @@ export const SYSTEM_AUTH_MAILS: readonly SeedTemplate[] = [
279
406
  ]),
280
407
  ];
281
408
 
282
- const INVOICE_DOCUMENT_ROWS = `<table><tbody><tr><th><p>Leistung</p></th><th><p>Betrag</p></th></tr><tr data-each="lines" data-repeat-header=""><td><p>${v("description")}</p></td><td><p>${v("amount")}</p></td></tr></tbody></table>`;
409
+ const UTILITY_ROWS = `<table><tbody><tr><th><p>Leistung</p></th><th><p>Betrag</p></th></tr><tr data-each="lines" data-repeat-header=""><td><p>${v("description")}</p></td><td><p>${v("amount")}</p></td></tr></tbody></table>`;
410
+
411
+ const right = (inner: string): string => `<p data-align="right">${inner}</p>`;
412
+
413
+ function invoiceRows(labels: {
414
+ readonly service: string;
415
+ readonly period: string;
416
+ readonly net: string;
417
+ }): string {
418
+ return [
419
+ "<table><tbody>",
420
+ `<tr><th><p>#</p></th><th><p>${labels.service}</p></th><th><p>${labels.period}</p></th><th><p data-align="right">${labels.net}</p></th></tr>`,
421
+ `<tr data-each="lines" data-repeat-header=""><td><p>${v("position")}</p></td><td><p>${v("description")}</p></td><td><p>${v("period")}</p></td><td><p data-align="right">${v("net")}</p></td></tr>`,
422
+ "</tbody></table>",
423
+ ].join("");
424
+ }
283
425
 
284
426
  export const SYSTEM_INVOICE_DOCUMENTS: readonly SeedTemplate[] = [
285
427
  {
@@ -288,16 +430,29 @@ export const SYSTEM_INVOICE_DOCUMENTS: readonly SeedTemplate[] = [
288
430
  locale: "de",
289
431
  title: "Rechnung",
290
432
  content: [
433
+ `<p data-size="small"><u>Nordlicht Immobilien GmbH · Am Hafen 12 · 24103 Kiel</u></p>`,
434
+ `<p>${v("recipientAddress")}</p>`,
435
+ right(`Rechnungsnummer: ${v("invoiceNumber")}`),
436
+ right(`Rechnungsdatum: ${v("invoiceDate")}`),
437
+ right(`Fällig am: ${v("dueDate")}`),
291
438
  `<h1>Rechnung ${v("invoiceNumber")}</h1>`,
292
- `<p>Rechnungsdatum: ${v("invoiceDate")}</p>`,
293
- `<p>Guten Tag ${v("customerName")},</p>`,
294
- "<p>für unsere Leistungen stellen wir Ihnen folgenden Betrag in Rechnung.</p>",
295
- INVOICE_DOCUMENT_ROWS,
296
- `<p><strong>Gesamtbetrag: ${v("total")}</strong></p>`,
297
- "<p>Bitte überweisen Sie den Betrag innerhalb von 14 Tagen auf unser Konto.</p>",
439
+ "<p>Sehr geehrte Damen und Herren,</p>",
440
+ "<p>wir stellen Ihnen die folgenden Leistungen in Rechnung:</p>",
441
+ invoiceRows({ service: "Leistung", period: "Zeitraum", net: "Netto" }),
442
+ right(`Nettobetrag ${v("netTotal")}`),
443
+ right(`Umsatzsteuer 19 % ${v("vatAmount")}`),
444
+ right(`<strong>Rechnungsbetrag ${v("invoiceTotal")}</strong>`),
445
+ `<p>Bitte überweisen Sie den Rechnungsbetrag bis zum ${v("dueDate")} unter Angabe der Referenz ${v("paymentReference")} auf ${v("iban")}.</p>`,
446
+ "<div data-page-break></div>",
447
+ "<p>Zahlungsbedingungen und Hinweise auf Seite 2 …</p>",
298
448
  ].join(""),
299
- meta: { layoutSlug: "default", pageFormat: "A4", fileName: "Rechnung.pdf" },
300
- variableSchema: DOCUMENT_LINES_SCHEMA,
449
+ meta: {
450
+ layoutSlug: "default",
451
+ pageFormat: "A4",
452
+ fileName: "rechnung-{{invoiceNumber}}.pdf",
453
+ metadata: { title: "Rechnung {{invoiceNumber}}" },
454
+ },
455
+ variableSchema: INVOICE_DOCUMENT_SCHEMA_DE,
301
456
  },
302
457
  {
303
458
  slug: "billing:invoice",
@@ -305,16 +460,29 @@ export const SYSTEM_INVOICE_DOCUMENTS: readonly SeedTemplate[] = [
305
460
  locale: "en",
306
461
  title: "Invoice",
307
462
  content: [
463
+ `<p data-size="small"><u>Nordlicht Immobilien GmbH · Am Hafen 12 · 24103 Kiel</u></p>`,
464
+ `<p>${v("recipientAddress")}</p>`,
465
+ right(`Invoice number: ${v("invoiceNumber")}`),
466
+ right(`Invoice date: ${v("invoiceDate")}`),
467
+ right(`Due on: ${v("dueDate")}`),
308
468
  `<h1>Invoice ${v("invoiceNumber")}</h1>`,
309
- `<p>Invoice date: ${v("invoiceDate")}</p>`,
310
- `<p>Dear ${v("customerName")},</p>`,
311
- "<p>we charge you the following amount for our services.</p>",
312
- INVOICE_DOCUMENT_ROWS.replace("Leistung", "Service").replace("Betrag", "Amount"),
313
- `<p><strong>Total: ${v("total")}</strong></p>`,
314
- "<p>Please transfer the amount to our account within 14 days.</p>",
469
+ "<p>Dear Sir or Madam,</p>",
470
+ "<p>we charge you the following services:</p>",
471
+ invoiceRows({ service: "Service", period: "Period", net: "Net" }),
472
+ right(`Net amount ${v("netTotal")}`),
473
+ right(`VAT 19 % ${v("vatAmount")}`),
474
+ right(`<strong>Total ${v("invoiceTotal")}</strong>`),
475
+ `<p>Please transfer the amount by ${v("dueDate")} quoting the reference ${v("paymentReference")} to ${v("iban")}.</p>`,
476
+ "<div data-page-break></div>",
477
+ "<p>Terms of payment and notes on page 2 …</p>",
315
478
  ].join(""),
316
- meta: { layoutSlug: "default", pageFormat: "A4", fileName: "Invoice.pdf" },
317
- variableSchema: DOCUMENT_LINES_SCHEMA,
479
+ meta: {
480
+ layoutSlug: "default",
481
+ pageFormat: "A4",
482
+ fileName: "invoice-{{invoiceNumber}}.pdf",
483
+ metadata: { title: "Invoice {{invoiceNumber}}" },
484
+ },
485
+ variableSchema: INVOICE_DOCUMENT_SCHEMA_EN,
318
486
  },
319
487
  ];
320
488
 
@@ -328,7 +496,7 @@ export const TENANT_UTILITY_STATEMENT: SeedTemplate = {
328
496
  `<p>Abrechnungsdatum: ${v("invoiceDate")}</p>`,
329
497
  `<p>Guten Tag ${v("customerName")},</p>`,
330
498
  "<p>nachfolgend erhalten Sie die Abrechnung der Betriebskosten für den Abrechnungszeitraum.</p>",
331
- INVOICE_DOCUMENT_ROWS,
499
+ UTILITY_ROWS,
332
500
  `<p><strong>Ergebnis: ${v("total")}</strong></p>`,
333
501
  "<p>Ein Guthaben überweisen wir Ihnen innerhalb von 30 Tagen, eine Nachzahlung ist zum Monatsende fällig.</p>",
334
502
  ].join(""),
@@ -352,3 +520,51 @@ export const SYSTEM_TENANCY_CERTIFICATE: SeedTemplate = {
352
520
  invoiceDate: { type: "date", required: true },
353
521
  },
354
522
  };
523
+
524
+ // Two other tenants customise the invoice for the "Anpassungen der Mandanten" drawer: one
525
+ // published copy with a second save, one draft.
526
+ export type TenantInvoiceOverride = {
527
+ readonly tenantId: string;
528
+ readonly tenantKey: string;
529
+ readonly tenantName: string;
530
+ readonly adminEmail: string;
531
+ readonly adminName: string;
532
+ readonly status: "active" | "draft";
533
+ readonly versions: readonly SeedTemplate[];
534
+ };
535
+
536
+ const SYSTEM_INVOICE_DE = SYSTEM_INVOICE_DOCUMENTS[0] as SeedTemplate;
537
+ const INVOICE_INTRO = "<p>wir stellen Ihnen die folgenden Leistungen in Rechnung:</p>";
538
+
539
+ function invoiceWithIntro(sentence: string): SeedTemplate {
540
+ return {
541
+ ...SYSTEM_INVOICE_DE,
542
+ content: SYSTEM_INVOICE_DE.content.replace(INVOICE_INTRO, `<p>${sentence}</p>`),
543
+ };
544
+ }
545
+
546
+ export const TENANT_INVOICE_OVERRIDES: readonly TenantInvoiceOverride[] = [
547
+ {
548
+ tenantId: "00000000-0000-4000-8000-000000000004",
549
+ tenantKey: "kuestenwohnen",
550
+ tenantName: "Küstenwohnen Kiel GmbH",
551
+ adminEmail: "verwaltung@kuestenwohnen-kiel.de",
552
+ adminName: "M. Petersen",
553
+ status: "active",
554
+ versions: [
555
+ invoiceWithIntro("für den genannten Zeitraum stellen wir Ihnen Folgendes in Rechnung:"),
556
+ invoiceWithIntro(
557
+ "für den genannten Zeitraum stellen wir Ihnen Folgendes in Rechnung. Die Nebenkosten rechnen wir jährlich ab.",
558
+ ),
559
+ ],
560
+ },
561
+ {
562
+ tenantId: "00000000-0000-4000-8000-000000000005",
563
+ tenantKey: "hansa-wohnbau",
564
+ tenantName: "Hansa Wohnbau eG",
565
+ adminEmail: "rechnungswesen@hansa-wohnbau.de",
566
+ adminName: "K. Lorenzen",
567
+ status: "draft",
568
+ versions: [invoiceWithIntro("nachfolgend die Abrechnung für Ihre Wohnung:")],
569
+ },
570
+ ];
@@ -10,6 +10,10 @@ const INVOICE_ISSUED_VARIABLES = {
10
10
  required: true,
11
11
  example: "Familie Yilmaz",
12
12
  }),
13
+ recipientEmail: templateVariable.text({
14
+ label: "Empfänger-Adresse",
15
+ example: "yilmaz.familie@example.org",
16
+ }),
13
17
  invoiceNumber: templateVariable.text({
14
18
  label: "Rechnungsnummer",
15
19
  required: true,
@@ -15,6 +15,7 @@ import { createTemplateResolverFeature } from "@cosmicdrift/kumiko-bundled-featu
15
15
  import { createDocumentDesignerFeature } from "@cosmicdrift/kumiko-document-designer";
16
16
  import { localeDe } from "@cosmicdrift/kumiko-locale-de";
17
17
  import { createRendererMailHtmlFeature } from "@cosmicdrift/kumiko-renderer-mail-html";
18
+ import { createRendererPuppeteerClientFeature } from "@cosmicdrift/kumiko-renderer-puppeteer-client";
18
19
  import { TENANT_ADMIN_EMAIL } from "./app/auth-constants";
19
20
  import { billingFeature } from "./features/billing";
20
21
  import { rentalFeature } from "./features/rental";
@@ -35,6 +36,9 @@ export const APP_FEATURES = [
35
36
  // Mounted individually: composeRendererStack() would add a second template-resolver.
36
37
  createRendererFoundationFeature(),
37
38
  createRendererMailHtmlFeature(),
39
+ // The PDF preview of the template editor prints through the puppeteer-service
40
+ // (PUPPETEER_SERVICE_URL / PUPPETEER_AUTH_TOKEN); the e2e config starts one.
41
+ createRendererPuppeteerClientFeature(),
38
42
  createDeliveryFeature(),
39
43
  // In-memory transport: the test mail of the mail editor is rendered and
40
44
  // "sent" for real, nothing leaves the process. Every user resolves to the
@@ -1423,7 +1423,7 @@
1423
1423
  },
1424
1424
  {
1425
1425
  "name": "jobs",
1426
- "description": "Persistence and operator tooling for background jobs registered via `r.job(...)`. Every job execution writes directly into `store_job_runs` (current status + duration) and `store_job_run_logs` (per-line log rows) from the BullMQ callbacks — no event stream in between (#2243). A daily `retention-cleanup` job deletes runs (and their logs) older than `retentionDays`; an hourly `stale-run-sweep` job marks runs stuck at status `running` past `staleRunTimeoutHours` as `failed` (#2246 — a crashed worker never fires the completion callback, so nothing else ever revisits the row). Exposes `jobs:write:trigger` (manual run) and `jobs:write:retry` (operator retry of a failed run), plus `jobs:query:list`, `jobs:query:details`, and `jobs:query:catalog` (manual jobs) for the operator UI. A job that declares `tenantVisibleFailure` also records its last failed attempt per tenant and subject in `store_tenant_job_failures`, which the tenant itself reads through `jobs:query:failures` — a translation key only, never the provider's message (fw#3079). A job that declares `tenantVisibleRun` also keeps its run state per tenant and subject in `store_tenant_job_runs` (queued, running, plus the latest completed or failed run, no payload, error text or logs), which the tenant reads through `jobs:query:tenant-runs` (fw#3616); the stale-run sweep fails its stuck `running` rows and drops its stuck `queued` rows.",
1426
+ "description": "Persistence and operator tooling for background jobs registered via `r.job(...)`. Every job execution writes directly into `store_job_runs` (current status + duration) and `store_job_run_logs` (per-line log rows) from the BullMQ callbacks — no event stream in between (#2243). A daily `retention-cleanup` job deletes runs (and their logs) older than `retentionDays`; an hourly `stale-run-sweep` job marks runs stuck at status `running` past `staleRunTimeoutHours` as `failed` (#2246 — a crashed worker never fires the completion callback, so nothing else ever revisits the row). Exposes `jobs:write:trigger` (manual run) and `jobs:write:retry` (operator retry of a failed run), plus `jobs:query:list`, `jobs:query:details`, and `jobs:query:catalog` (manual jobs) for the operator UI. A job that declares `tenantVisibleFailure` also records its last failed attempt per tenant and subject in `store_tenant_job_failures`, which the tenant itself reads through `jobs:query:failures` — a translation key only, never the provider's message (fw#3079). A job that declares `tenantVisibleRun` also keeps its run state per tenant and subject in `store_tenant_job_runs` (queued, running, plus the latest completed or failed run, no payload, error text or logs), which the tenant reads through `jobs:query:tenant-runs` (fw#3616); the stale-run sweep fails its stuck `running` rows and drops `queued` rows older than `retentionDays`.",
1427
1427
  "toggleableDefault": null,
1428
1428
  "requires": [],
1429
1429
  "optionalRequires": [],
@@ -2170,7 +2170,7 @@
2170
2170
  },
2171
2171
  {
2172
2172
  "name": "renderer-puppeteer-client",
2173
- "description": "A `renderer-foundation` plugin that prints documents to PDF through a separately deployed `puppeteer-service`. A `document-pdf` request names a stored template (`{ slug, locale, variables }`): the plugin resolves the template and its `pdf-layout` (header, signature and footer regions), embeds every image as a data URI, turns repeating table header rows into a real table head and sends the finished HTML to the service, which performs no network fetches itself. The page format, margins, file name pattern and title come from the template meta. `renderAndStorePdf(ctx, ...)` renders and stores the PDF as a file with a `rendered-pdf` record that keeps the template and layout version, so a stored document can always be traced to the text it was created from. The `puppeteer-status` query tells a TenantAdmin whether the service answers; while it does not, rendering fails with a `puppeteer_unavailable` error (HTTP 503) and no PDF is created. `image-snapshot` requests still take ready HTML in `payload.content`. Configure the endpoint and token with `PUPPETEER_SERVICE_URL` and `PUPPETEER_AUTH_TOKEN`; the client retries 5xx and network errors with exponential backoff (default 3 retries), 4xx fails at once. The feature adds the `rendered_pdfs` table, so apps that mount it need a schema migration.",
2173
+ "description": "A `renderer-foundation` plugin that prints documents to PDF through a separately deployed `puppeteer-service`. A `document-pdf` request names a stored template (`{ slug, locale, variables }`): the plugin resolves the template and its `pdf-layout` (header, signature and footer regions), embeds every image as a data URI, turns repeating table header rows into a real table head and sends the finished HTML to the service, which performs no network fetches itself. The page format, margins, file name pattern and title come from the template meta. A write handler renders with `renderPdfBeforeTransaction(ctx, { slug, locale, variables })` as its `beforeTransaction` phase, so the print service call holds no database connection, and stores the result with `storeRenderedPdf(ctx, event.prepared)` in the handler: a file plus a `rendered-pdf` record that keeps the template and layout version, so a stored document can always be traced to the text it was created from. The `puppeteer-status` query tells a TenantAdmin whether the service answers; while it does not, rendering fails with a `puppeteer_unavailable` error (HTTP 503) and no PDF is created. `image-snapshot` requests still take ready HTML in `payload.content`. Configure the endpoint and token with `PUPPETEER_SERVICE_URL` and `PUPPETEER_AUTH_TOKEN`; the client retries 5xx and network errors with exponential backoff (default 3 retries), 4xx fails at once. The feature adds the `rendered_pdfs` table, so apps that mount it need a schema migration.",
2174
2174
  "toggleableDefault": null,
2175
2175
  "requires": [
2176
2176
  "renderer-foundation"
@@ -2563,7 +2563,7 @@
2563
2563
  },
2564
2564
  {
2565
2565
  "name": "template-resolver",
2566
- "description": "Every piece of editable text lives here, in one entity: mail bodies, notification texts, PDF document templates, AI prompts and plain text blocks. What a record is used for is the `kind` (`notification`, `mail`, `mail-layout`, `document-pdf`, `pdf-layout`, `ai-prompt`, `text-block`, `snippet`, `image-snapshot`).\n\nReading is one call — `ctx.templateResolver.resolveTemplate({ tenantId, slug, kind, locale })`. It walks four levels: tenant+locale, system+locale, tenant+fallback-locale, system+fallback-locale. A tenant overrides a system default by simply having its own record; no application code changes. Writing programmatically goes through the `upsertSystem`, `upsertTenant`, `publish` and `archive` write handlers; a SystemAdmin retires or restores a system default with `publishSystem` and `archiveSystem`, and a tenant drops its override with `resetToSystem`.\n\nText an editor should be able to change belongs in a collection, declared at mount: `createTemplateResolverFeature({ collections: [{ id, kind, access: { roles }, nav }] })`. It appears in the navigation, and `access` is part of the mount because a bundled feature cannot know the host's roles. Each collection gets its own `<id>-list` / `<id>-item` / `<id>-set` handlers, so the dispatcher enforces the separation.\n\nHow a collection is edited follows from `contentFormat`: `plain` gives a text area, `rich` a small WYSIWYG (bold, italic, headings, lists, links), `markdown` a text area that stores markdown text (same insertable chips as `plain`). All three offer the collection's `variableSchema` as insertable chips and a preview rendered with sample data — an editor sees what `{{firstName}}` becomes without sending a mail. An app can register its own editor for a format; the last `clientFeature` that registers a format wins (a conflict logs a warning).\n\nA collection is tenant-wide by default. With `ownership: \"user\"` every user keeps their own entries — mail signatures being the obvious case. Those rows live in the separate `user-content-entry` entity and count as user data, so mounting one also requires the `template-resolver-user-data` feature and a migration on the app side.\n\n`validate` dry-runs the write pipeline for a draft (sanitizer, layout rules, placeholders) and reports what the template uses and misses. `export-bundle` / `import-bundle` (and `import-bundle-system`) move one template with all locales and resource files between tenants or environments; `copy-locale` starts a translation from an existing locale. Layouts are structured: a mail or PDF layout is `data-header` / `data-content` / `data-signature` / `data-footer` regions, and mjml layout source is system-only.\n\n`preview` renders a draft (or the stored template) with sample values taken from the `example` of each declared variable, through the same gates and the same mail renderer as a real send, and returns subject, html, `displayHtml` (the html with cid images as data URIs for an editor), text, attachments (inline images and files) and `checks` (images resolved, plain text present, size under 10 MiB, required placeholders used); nothing is saved or sent. `send-test` mails that rendering to the calling user's own address only, subject prefixed with `[TEST]`, at most 10 per user and hour; it needs `delivery`, `channel-email` and `renderer-mail-html` to be mounted.\n\nImages used by a template (`<img data-resource=\"logo\">`) are uploaded with `resourceUpload`, which re-encodes them, stores a file and links it under the key; `resourceRemove` unlinks it. `linkedResources` is written only through these two handlers. With `purpose: \"attachment\"` and a `fileName`, `resourceUpload` stores a PDF, PNG or JPEG as a file attachment of a mail template (original bytes, at most 10) that the mail renderer attaches after the inline images. `resourceLinkPdf` instead links a `document-pdf` template under a key: every mail sent from the template then carries that PDF, rendered at send time with the mail's own variables and locale, under a file name pattern such as `invoice-{{number}}.pdf`; files and linked PDFs share the limit of 10.\n\nReplaces the former `text-content` feature; its blocks now live here as kind `text-block`.",
2566
+ "description": "Every piece of editable text lives here, in one entity: mail bodies, notification texts, PDF document templates, AI prompts and plain text blocks. What a record is used for is the `kind` (`notification`, `mail`, `mail-layout`, `document-pdf`, `pdf-layout`, `ai-prompt`, `text-block`, `snippet`, `image-snapshot`).\n\nReading is one call — `ctx.templateResolver.resolveTemplate({ tenantId, slug, kind, locale })`. It walks four levels: tenant+locale, system+locale, tenant+fallback-locale, system+fallback-locale. A tenant overrides a system default by simply having its own record; no application code changes. Writing programmatically goes through the `upsertSystem`, `upsertTenant`, `publish` and `archive` write handlers; a SystemAdmin retires or restores a system default with `publishSystem` and `archiveSystem`, and a tenant drops its override with `resetToSystem`. Saving a draft over an active template keeps the active version served and stores the edit beside it (column `draft`); `publish` makes it the served version, `discardDraft` / `discardDraftSystem` throws it away.\n\nText an editor should be able to change belongs in a collection, declared at mount: `createTemplateResolverFeature({ collections: [{ id, kind, access: { roles }, nav }] })`. It appears in the navigation, and `access` is part of the mount because a bundled feature cannot know the host's roles. Each collection gets its own `<id>-list` / `<id>-item` / `<id>-set` handlers, so the dispatcher enforces the separation.\n\nHow a collection is edited follows from `contentFormat`: `plain` gives a text area, `rich` a small WYSIWYG (bold, italic, headings, lists, links), `markdown` a text area that stores markdown text (same insertable chips as `plain`). All three offer the collection's `variableSchema` as insertable chips and a preview rendered with sample data — an editor sees what `{{firstName}}` becomes without sending a mail. An app can register its own editor for a format; the last `clientFeature` that registers a format wins (a conflict logs a warning).\n\nA collection is tenant-wide by default. With `ownership: \"user\"` every user keeps their own entries — mail signatures being the obvious case. Those rows live in the separate `user-content-entry` entity and count as user data, so mounting one also requires the `template-resolver-user-data` feature and a migration on the app side.\n\n`validate` dry-runs the write pipeline for a draft (sanitizer, layout rules, placeholders) and reports what the template uses and misses. `export-bundle` / `import-bundle` (and `import-bundle-system`) move one template with all locales and resource files between tenants or environments; `copy-locale` starts a translation from an existing locale. Layouts are structured: a mail or PDF layout is `data-header` / `data-content` / `data-signature` / `data-footer` regions, and mjml layout source is system-only.\n\n`preview` renders a draft (or the stored template) with sample values taken from the `example` of each declared variable, through the same gates and the same mail renderer as a real send, and returns subject, html, `displayHtml` (the html with cid images as data URIs for an editor), text, attachments (inline images and files) and `checks` (images resolved, plain text present, size under 10 MiB, required placeholders used); nothing is saved or sent. `send-test` mails that rendering to the calling user's own address only, subject prefixed with `[TEST]`, at most 10 per user and hour; it needs `delivery`, `channel-email` and `renderer-mail-html` to be mounted.\n\nImages used by a template (`<img data-resource=\"logo\">`) are uploaded with `resourceUpload`, which re-encodes them, stores a file and links it under the key; `resourceRemove` unlinks it. `linkedResources` is written only through these two handlers. With `purpose: \"attachment\"` and a `fileName`, `resourceUpload` stores a PDF, PNG or JPEG as a file attachment of a mail template (original bytes, at most 10) that the mail renderer attaches after the inline images. `resourceLinkPdf` instead links a `document-pdf` template under a key: every mail sent from the template then carries that PDF, rendered at send time with the mail's own variables and locale, under a file name pattern such as `invoice-{{number}}.pdf`; files and linked PDFs share the limit of 10.\n\nReplaces the former `text-content` feature; its blocks now live here as kind `text-block`.",
2567
2567
  "toggleableDefault": null,
2568
2568
  "requires": [],
2569
2569
  "optionalRequires": [],
@@ -2601,6 +2601,8 @@
2601
2601
  "template-resolver:write:archive",
2602
2602
  "template-resolver:write:archive-system",
2603
2603
  "template-resolver:write:copy-locale",
2604
+ "template-resolver:write:discard-draft",
2605
+ "template-resolver:write:discard-draft-system",
2604
2606
  "template-resolver:write:import-bundle",
2605
2607
  "template-resolver:write:import-bundle-system",
2606
2608
  "template-resolver:write:publish",