@hartl-services/medusa-base 1.0.0 → 1.1.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 (71) hide show
  1. package/.medusa/server/src/admin/index.js +117 -1
  2. package/.medusa/server/src/admin/index.mjs +118 -2
  3. package/.medusa/server/src/documents/plugin-features.js +15 -1
  4. package/.medusa/server/src/documents/templates/financial-document.js +69 -51
  5. package/.medusa/server/src/documents/templates/format.js +44 -2
  6. package/.medusa/server/src/documents/templates/html-factory.js +7 -5
  7. package/.medusa/server/src/documents/templates/html-utils.js +12 -2
  8. package/.medusa/server/src/documents/templates/layout.js +115 -70
  9. package/.medusa/server/src/documents/templates/packing-list.js +10 -8
  10. package/.medusa/server/src/documents/utils/document-render-settings.js +59 -0
  11. package/.medusa/server/src/email/application/handle-invite-event.js +8 -2
  12. package/.medusa/server/src/email/application/handle-password-reset-event.js +16 -4
  13. package/.medusa/server/src/email/domain/order-summary.js +103 -0
  14. package/.medusa/server/src/email/domain/refund-summary.js +43 -0
  15. package/.medusa/server/src/email/domain/return-summary.js +118 -0
  16. package/.medusa/server/src/email/domain/shipment-summary.js +74 -0
  17. package/.medusa/server/src/email/plugin-features.js +9 -1
  18. package/.medusa/server/src/email/rendering/default-templates.js +693 -65
  19. package/.medusa/server/src/email/rendering/load-email-shop-details.js +36 -0
  20. package/.medusa/server/src/email/rendering/render-email.js +16 -3
  21. package/.medusa/server/src/email/rendering/resolve-shop-configuration.js +20 -1
  22. package/.medusa/server/src/email/types/template-data.js +155 -2
  23. package/.medusa/server/src/modules/plugin-features/domain/plugin-settings.js +4 -3
  24. package/.medusa/server/src/public/descriptor.js +3 -1
  25. package/.medusa/server/src/public/shop-master-data.js +2 -2
  26. package/.medusa/server/src/shop-config/build-food-shop-config.js +5 -1
  27. package/.medusa/server/src/shop-config/options.js +5 -1
  28. package/.medusa/server/src/workflows/documents/issue-document.js +4 -2
  29. package/.medusa/server/src/workflows/documents/issue-packing-list.js +8 -2
  30. package/.medusa/server/src/workflows/email/dispatch-email-message.js +4 -2
  31. package/.medusa/server/src/workflows/email/prepare-order-canceled-email.js +42 -4
  32. package/.medusa/server/src/workflows/email/prepare-order-received-email.js +31 -5
  33. package/.medusa/server/src/workflows/email/prepare-refund-email.js +25 -1
  34. package/.medusa/server/src/workflows/email/prepare-return-email.js +64 -3
  35. package/.medusa/server/src/workflows/email/prepare-shipment-email.js +36 -3
  36. package/README.md +166 -9
  37. package/dist/admin-components/features/plugin-features/components/ImageSettingInput.d.ts +15 -0
  38. package/dist/admin-components/features/plugin-features/components/ImageSettingInput.js +42 -0
  39. package/dist/admin-components/features/plugin-features/components/PluginSettingsForm.js +4 -2
  40. package/dist/admin-components/features/plugin-features/hooks/usePluginFeatureEnabled.d.ts +1 -1
  41. package/dist/admin-components/features/plugin-features/hooks/usePluginFeatures.d.ts +4 -4
  42. package/dist/admin-components/features/plugin-features/hooks/usePluginSettingsForm.d.ts +1 -1
  43. package/dist/admin-components/features/plugin-features/lib/image-upload.d.ts +6 -0
  44. package/dist/admin-components/features/plugin-features/lib/image-upload.js +17 -0
  45. package/dist/admin-components/features/plugin-features/lib/plugin-features-admin-client.d.ts +4 -4
  46. package/dist/admin-components/i18n/texts.d.ts +6 -0
  47. package/dist/admin-components/i18n/texts.js +6 -0
  48. package/dist/documents/templates/financial-document.d.ts +3 -3
  49. package/dist/documents/templates/financial-document.js +68 -50
  50. package/dist/documents/templates/format.d.ts +14 -0
  51. package/dist/documents/templates/format.js +43 -1
  52. package/dist/documents/templates/html-factory.d.ts +6 -2
  53. package/dist/documents/templates/html-factory.js +6 -4
  54. package/dist/documents/templates/html-utils.d.ts +6 -0
  55. package/dist/documents/templates/html-utils.js +11 -1
  56. package/dist/documents/templates/layout.d.ts +21 -3
  57. package/dist/documents/templates/layout.js +114 -69
  58. package/dist/documents/templates/packing-list.js +9 -7
  59. package/dist/documents/types/documents.d.ts +17 -0
  60. package/dist/documents/types/html-factory.d.ts +1 -0
  61. package/dist/email/types/shop-configuration.d.ts +25 -0
  62. package/dist/email/types/template-data.d.ts +509 -0
  63. package/dist/email/types/template-data.js +154 -1
  64. package/dist/public/descriptor.js +2 -0
  65. package/dist/public/shop-master-data.d.ts +1 -1
  66. package/dist/public/shop-master-data.js +1 -1
  67. package/dist/public/types.d.ts +2 -1
  68. package/dist/shop-config/build-food-shop-config.js +4 -0
  69. package/dist/shop-config/options.d.ts +10 -3
  70. package/dist/shop-config/options.js +4 -0
  71. package/package.json +2 -2
@@ -1,96 +1,141 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.createDocumentLayout = void 0;
3
+ exports.createDocumentLayout = exports.formatCountryName = void 0;
4
4
  const html_utils_1 = require("./html-utils");
5
5
  const formatCountryName = (countryCode) => new Intl.DisplayNames("de", { type: "region" }).of(countryCode.toUpperCase()) ?? countryCode.toUpperCase();
6
- const renderMerchantAddress = (merchant) => [
7
- merchant.address_line1,
8
- merchant.address_line2,
9
- [merchant.postal_code, merchant.city].join(" "),
10
- formatCountryName(merchant.country_code),
11
- ]
12
- .filter((part) => Boolean(part))
6
+ exports.formatCountryName = formatCountryName;
7
+ const joinLines = (lines) => lines
8
+ .filter((line) => Boolean(line))
13
9
  .map(html_utils_1.escapeHtml)
14
10
  .join("<br />");
15
- /** Legal footer: name, UID, company register, contact and bank details that are set. */
11
+ const renderFooterColumn = (heading, lines) => {
12
+ const content = joinLines(lines);
13
+ if (!content) {
14
+ return "";
15
+ }
16
+ const title = heading
17
+ ? `<div class="footer-heading">${(0, html_utils_1.escapeHtml)(heading)}</div>`
18
+ : "";
19
+ return `<div class="footer-column">${title}${content}</div>`;
20
+ };
21
+ /**
22
+ * Legal footer in three columns: sender with UID and company register,
23
+ * contact, bank. Packing lists (`includeLegalDetails: false`) show only the
24
+ * sender's name and address.
25
+ */
16
26
  const renderMerchantFooter = (merchant, includeLegalDetails) => {
27
+ const sender = `<div class="footer-column"><strong>${(0, html_utils_1.escapeHtml)(merchant.legal_name)}</strong><br />${joinLines([
28
+ merchant.address_line1,
29
+ merchant.address_line2,
30
+ `${merchant.postal_code} ${merchant.city}`,
31
+ (0, exports.formatCountryName)(merchant.country_code),
32
+ ...(includeLegalDetails
33
+ ? [
34
+ merchant.vat_number ? `UID: ${merchant.vat_number}` : null,
35
+ merchant.company_register_number
36
+ ? `Firmenbuchnummer: ${merchant.company_register_number}`
37
+ : null,
38
+ merchant.company_register_court
39
+ ? `Firmenbuchgericht: ${merchant.company_register_court}`
40
+ : null,
41
+ ]
42
+ : []),
43
+ ])}</div>`;
17
44
  if (!includeLegalDetails) {
18
- return (0, html_utils_1.escapeHtml)(merchant.legal_name);
45
+ return sender;
19
46
  }
20
- const register = [
21
- merchant.company_register_number
22
- ? `Firmenbuchnummer: ${merchant.company_register_number}`
23
- : null,
24
- merchant.company_register_court
25
- ? `Firmenbuchgericht: ${merchant.company_register_court}`
26
- : null,
27
- ];
28
- const lines = [
29
- [
30
- merchant.legal_name,
31
- merchant.vat_number ? `UID: ${merchant.vat_number}` : null,
32
- ...register,
33
- ],
34
- [merchant.email, merchant.phone, merchant.website],
35
- [
36
- merchant.iban ? `IBAN: ${merchant.iban}` : null,
47
+ return [
48
+ sender,
49
+ renderFooterColumn("Kontakt", [
50
+ merchant.phone,
51
+ merchant.email,
52
+ merchant.website,
53
+ ]),
54
+ // The heading names the IBAN; only the BIC keeps its label.
55
+ renderFooterColumn("Bankverbindung", [
56
+ merchant.iban,
37
57
  merchant.bic ? `BIC: ${merchant.bic}` : null,
38
- ],
39
- ];
40
- return lines
41
- .map((parts) => parts
42
- .filter((part) => Boolean(part))
43
- .map(html_utils_1.escapeHtml)
44
- .join(" · "))
45
- .filter(Boolean)
46
- .join("<br />");
58
+ ]),
59
+ ].join("");
47
60
  };
61
+ const renderLetterhead = (merchant, settings) => settings.logo_src
62
+ ? `<img class="logo" src="${(0, html_utils_1.escapeHtml)(settings.logo_src)}" alt="${(0, html_utils_1.escapeHtml)(merchant.legal_name)}" />`
63
+ : `<div class="logo-text">${(0, html_utils_1.escapeHtml)(merchant.legal_name)}</div>`;
48
64
  const renderMetadata = (metadata) => metadata
49
- .map(({ label, value }) => `<div><strong>${(0, html_utils_1.escapeHtml)(label)}:</strong> ${(0, html_utils_1.escapeHtml)(value)}</div>`)
65
+ .map(({ label, value }) => `<tr><th scope="row">${(0, html_utils_1.escapeHtml)(label)}</th><td>${(0, html_utils_1.escapeHtml)(value)}</td></tr>`)
50
66
  .join("");
51
67
  /**
52
- * `includeLegalDetails` adds UID, company register, contact and bank details
53
- * to the footer (financial documents); packing lists show only the name.
68
+ * A4 with only a bottom page margin, which holds the page number
69
+ * (`@bottom-right`; `counter(pages)` works only in page margin boxes). `.page`
70
+ * carries the other margins and repeats them on every printed page
71
+ * (`box-decoration-break: clone`); the fixed footer is positioned in the page
72
+ * area, `--footer-height` reserves its space at the bottom of each page.
54
73
  */
55
- const createDocumentLayout = (merchant, includeLegalDetails) => ({ body, documentTitle, metadata, number, pageTitle }) => `<!doctype html>
56
- <html lang="de">
57
- <head>
58
- <meta charset="utf-8" />
59
- <title>${(0, html_utils_1.escapeHtml)(pageTitle)} ${(0, html_utils_1.escapeHtml)(number)}</title>
60
- <style>
61
- @page { margin: 18mm; }
62
- body { color: #1f2937; font-family: Arial, sans-serif; font-size: 11px; line-height: 1.45; }
63
- h1 { font-size: 24px; margin: 0 0 6px; }
64
- h2 { break-after: avoid; font-size: 12px; margin: 22px 0 6px; page-break-after: avoid; text-transform: uppercase; }
65
- .header { border-bottom: 2px solid #111827; display: flex; gap: 32px; justify-content: space-between; padding-bottom: 16px; }
66
- .document-metadata { display: flex; flex-wrap: wrap; gap: 6px 28px; margin: 18px 0 22px; }
67
- .document-metadata div { min-width: 180px; }
68
- .muted, .sku, .batch { color: #6b7280; }
69
- .batch, .packing-batch { font-size: 10px; margin-top: 2px; }
74
+ const DEFAULT_STYLESHEET = `
75
+ @page { size: A4; margin: 0 0 8mm 0; @bottom-right { color: #6b7280; content: "Seite " counter(page) " von " counter(pages); font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; font-size: 7.5pt; padding-right: 20mm; vertical-align: top; } }
76
+ :root { --accent: #111827; --text: #1f2937; --muted: #6b7280; --line: #d1d5db; --table-head: #f3f4f6; --page-x: 20mm; --page-top: 18mm; --footer-height: 30mm; }
77
+ * { box-sizing: border-box; }
78
+ html, body { margin: 0; padding: 0; }
79
+ body { color: var(--text); font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; font-size: 10pt; line-height: 1.4; -webkit-print-color-adjust: exact; print-color-adjust: exact; }
80
+ .page { -webkit-box-decoration-break: clone; box-decoration-break: clone; padding: var(--page-top) var(--page-x) calc(var(--footer-height) + 8mm); }
81
+ .letterhead { margin-bottom: 14mm; min-height: 16mm; }
82
+ .logo { display: block; height: 22mm; max-width: 90mm; object-fit: contain; object-position: left center; width: auto; }
83
+ .logo-text { color: var(--accent); font-size: 20pt; font-weight: 700; letter-spacing: 0.02em; }
84
+ .document-head { break-inside: avoid; display: flex; gap: 12mm; justify-content: space-between; margin-bottom: 14mm; page-break-inside: avoid; }
85
+ .recipient { flex: 1 1 0; font-style: normal; padding-top: 9mm; }
86
+ .document-info { flex: 0 0 78mm; }
87
+ h1 { color: var(--accent); font-size: 16pt; font-weight: 400; letter-spacing: 0.08em; margin: 0 0 3mm; text-transform: uppercase; }
88
+ h2 { break-after: avoid; font-size: 10pt; margin: 8mm 0 2mm; page-break-after: avoid; }
89
+ .document-metadata { border-collapse: collapse; width: 100%; }
90
+ .document-metadata th, .document-metadata td { border: 0; font-weight: 400; padding: 0; text-align: left; vertical-align: top; }
91
+ .document-metadata td { text-align: right; }
92
+ .muted, .sku, .batch { color: var(--muted); }
93
+ .batch, .packing-batch { font-size: 8pt; margin-top: 1mm; }
70
94
  .packing-batch { display: flex; gap: 12px; justify-content: space-between; }
71
95
  table { border-collapse: collapse; page-break-inside: auto; width: 100%; }
72
96
  thead { display: table-header-group; }
73
97
  tfoot { display: table-footer-group; }
74
98
  tbody { page-break-inside: auto; }
75
99
  tr { break-inside: avoid; page-break-inside: avoid; }
76
- th { background: #f3f4f6; font-size: 10px; text-align: left; text-transform: uppercase; }
77
- th, td { border-bottom: 1px solid #d1d5db; padding: 8px 6px; vertical-align: top; }
100
+ .items th { background: var(--table-head); font-weight: 700; text-align: left; }
101
+ .items th.number { text-align: right; }
102
+ .items th, .items td { padding: 2.5mm 2mm; vertical-align: top; }
103
+ .items tbody.lines td { border-bottom: 1px solid var(--line); }
78
104
  .number { text-align: right; white-space: nowrap; }
79
- .recipient, .tax-compliance, .totals { break-inside: avoid; page-break-inside: avoid; }
80
- .totals { margin-left: auto; margin-top: 16px; width: 260px; }
81
- .totals td { border: 0; padding: 3px 0; }
82
- .totals tr:last-child td { border-top: 2px solid #111827; font-size: 14px; font-weight: 700; padding-top: 7px; }
83
- footer { border-top: 1px solid #d1d5db; break-inside: avoid; color: #6b7280; font-size: 9px; margin-top: 28px; page-break-inside: avoid; padding-top: 10px; }
84
- </style>
105
+ .money { white-space: nowrap; }
106
+ .items tbody.totals td { padding-bottom: 1.5mm; padding-top: 1.5mm; }
107
+ .items tbody.totals tr:first-child td { padding-top: 3mm; }
108
+ .items tbody.totals tr.grand-total td.label, .items tbody.totals tr.grand-total td.number { border-bottom: 1px solid var(--line); border-top: 1px solid var(--line); font-weight: 700; padding-bottom: 2mm; padding-top: 2mm; }
109
+ .payment-terms { margin-top: 10mm; white-space: pre-line; }
110
+ .recipient, .tax-compliance, .payment-terms, .items tbody.totals { break-inside: avoid; page-break-inside: avoid; }
111
+ .page-footer { align-items: flex-start; bottom: 4mm; color: var(--text); display: flex; font-size: 8pt; gap: 8mm; left: var(--page-x); line-height: 1.35; position: fixed; right: var(--page-x); }
112
+ .footer-column { flex: 1 1 0; }
113
+ .footer-heading { color: var(--muted); }
114
+ `;
115
+ /**
116
+ * `includeLegalDetails` adds UID, company register, contact and bank details
117
+ * to the footer (financial documents); packing lists show only the sender.
118
+ */
119
+ const createDocumentLayout = (merchant, settings, includeLegalDetails, theme = {}) => ({ body, documentTitle, metadata, number, pageTitle, recipient }) => `<!doctype html>
120
+ <html lang="de">
121
+ <head>
122
+ <meta charset="utf-8" />
123
+ <title>${(0, html_utils_1.escapeHtml)(pageTitle)} ${(0, html_utils_1.escapeHtml)(number)}</title>
124
+ <style>${DEFAULT_STYLESHEET}${theme.stylesheet ?? ""}</style>
85
125
  </head>
86
126
  <body>
87
- <section class="header">
88
- <div><h1>${(0, html_utils_1.escapeHtml)(documentTitle)}</h1><div class="muted">${(0, html_utils_1.escapeHtml)(number)}</div></div>
89
- <div><strong>${(0, html_utils_1.escapeHtml)(merchant.legal_name)}</strong><br />${renderMerchantAddress(merchant)}</div>
90
- </section>
91
- <section class="document-metadata">${renderMetadata(metadata)}</section>
92
- ${body}
93
- <footer>${renderMerchantFooter(merchant, includeLegalDetails)}</footer>
127
+ <footer class="page-footer">${renderMerchantFooter(merchant, includeLegalDetails)}</footer>
128
+ <main class="page">
129
+ <header class="letterhead">${renderLetterhead(merchant, settings)}</header>
130
+ <section class="document-head">
131
+ <address class="recipient">${recipient ?? ""}</address>
132
+ <div class="document-info">
133
+ <h1>${(0, html_utils_1.escapeHtml)(documentTitle)}</h1>
134
+ <table class="document-metadata"><tbody>${renderMetadata(metadata)}</tbody></table>
135
+ </div>
136
+ </section>
137
+ ${body}
138
+ </main>
94
139
  </body>
95
140
  </html>`;
96
141
  exports.createDocumentLayout = createDocumentLayout;
@@ -10,15 +10,15 @@ const renderBatchAllocations = (line, timeZone) => {
10
10
  return line.batch_allocations
11
11
  .map((allocation) => `<div class="packing-batch">
12
12
  <strong>${(0, html_utils_1.escapeHtml)(allocation.lot_number)}</strong>
13
- <span>MHD: ${(0, format_1.formatDate)(allocation.expiry_date, { timeZone })}</span>
13
+ <span>MHD: ${(0, html_utils_1.escapeHtml)((0, format_1.formatDocumentDate)(allocation.expiry_date, timeZone))}</span>
14
14
  <span>Menge: ${(0, html_utils_1.escapeHtml)(allocation.quantity)}</span>
15
15
  </div>`)
16
16
  .join("");
17
17
  };
18
18
  const renderLines = (input, timeZone) => input.lines
19
19
  .map((line) => `<tr>
20
- <td>
21
- <strong>${(0, html_utils_1.escapeHtml)(line.title)}</strong>
20
+ <td class="item">
21
+ <div class="item-title">${(0, html_utils_1.escapeHtml)(line.title)}</div>
22
22
  ${line.sku ? `<div class="sku">SKU: ${(0, html_utils_1.escapeHtml)(line.sku)}</div>` : ""}
23
23
  </td>
24
24
  <td>${renderBatchAllocations(line, timeZone)}</td>
@@ -26,12 +26,13 @@ const renderLines = (input, timeZone) => input.lines
26
26
  </tr>`)
27
27
  .join("");
28
28
  const renderPackingList = (input, layout, timeZone) => layout({
29
- body: `<table>
30
- <thead><tr><th>Bezeichnung</th><th>Charge</th><th class="number">Benötigte Menge</th></tr></thead>
31
- <tbody>${renderLines(input, timeZone)}</tbody>
29
+ body: `<table class="items packing-items">
30
+ <thead><tr><th class="item">Bezeichnung</th><th>Charge</th><th class="number">Benötigte Menge</th></tr></thead>
31
+ <tbody class="lines">${renderLines(input, timeZone)}</tbody>
32
32
  </table>`,
33
33
  documentTitle: "Packliste",
34
34
  metadata: [
35
+ { label: "Packlistennummer", value: input.document.number },
35
36
  {
36
37
  label: "Bestellung",
37
38
  value: (0, html_utils_1.formatOrderReference)(input.order.display_id),
@@ -39,11 +40,12 @@ const renderPackingList = (input, layout, timeZone) => layout({
39
40
  { label: "Fulfillment", value: input.fulfillment.id },
40
41
  {
41
42
  label: "Erstellungsdatum",
42
- value: (0, format_1.formatDate)(input.document.generated_at, { timeZone }),
43
+ value: (0, format_1.formatDocumentDate)(input.document.generated_at, timeZone),
43
44
  },
44
45
  { label: "Lagerplatz", value: input.fulfillment.location_name },
45
46
  ],
46
47
  number: input.document.number,
47
48
  pageTitle: "Packliste",
49
+ recipient: null,
48
50
  });
49
51
  exports.renderPackingList = renderPackingList;
@@ -161,11 +161,28 @@ export type DocumentMerchantOption = {
161
161
  };
162
162
  vatNumber?: string;
163
163
  };
164
+ /**
165
+ * The Documents settings (Admin -> "Hartl Services Plugins" -> Dokumente) a
166
+ * render needs, read when a document is rendered. Not part of the stored
167
+ * snapshot: a regenerated document shows the current values.
168
+ */
169
+ export type DocumentRenderSettings = {
170
+ /**
171
+ * The uploaded logo as a `data:` URL, ready for `<img src>`; `null` when no
172
+ * logo is set or it could not be loaded (the document then shows the
173
+ * merchant's legal name instead).
174
+ */
175
+ logo_src: string | null;
176
+ /** Payment terms printed below the totals of an invoice; `null` = none. */
177
+ payment_terms: string | null;
178
+ };
164
179
  export type FinancialDocumentRenderInput = DocumentTemplateInput & {
165
180
  merchant: DocumentMerchant;
181
+ settings: DocumentRenderSettings;
166
182
  };
167
183
  export type PackingListRenderInput = PackingListTemplateInput & {
168
184
  merchant: DocumentMerchant;
185
+ settings: DocumentRenderSettings;
169
186
  };
170
187
  export type DocumentRenderInput = FinancialDocumentRenderInput | PackingListRenderInput;
171
188
  export type DocumentPdfOptions = {
@@ -5,3 +5,4 @@ import type { DocumentRenderInput } from "./documents";
5
5
  */
6
6
  export type DocumentHtmlFactory = (input: DocumentRenderInput) => string | Promise<string>;
7
7
  export declare const defineDocumentHtmlFactory: (factory: DocumentHtmlFactory) => DocumentHtmlFactory;
8
+ export type { DocumentTheme } from "../templates/layout";
@@ -1,3 +1,4 @@
1
+ import type { ShopMasterData } from "../../public";
1
2
  import type { EmailKind } from "./email-kinds";
2
3
  import type { EmailRenderDataMap } from "./template-data";
3
4
  export type EmailTemplateContent = Readonly<{
@@ -5,10 +6,22 @@ export type EmailTemplateContent = Readonly<{
5
6
  subject: string;
6
7
  text: string;
7
8
  }>;
9
+ /**
10
+ * The shop's master data ("Shop-Stammdaten") plus the `logo` URL of the
11
+ * E-Mail settings, read when the mail is sent. Every field may be missing; a
12
+ * layout shows what is there instead of failing.
13
+ */
14
+ export type EmailShopDetails = Readonly<ShopMasterData & {
15
+ logo?: string;
16
+ }>;
17
+ /** Who reads a mail: a customer or the shop's own team. */
18
+ export type EmailAudience = "customer" | "shop";
8
19
  export type EmailLayout = (input: Readonly<{
20
+ audience: EmailAudience;
9
21
  bodyHtml: string;
10
22
  preheader: string;
11
23
  subject: string;
24
+ shop: EmailShopDetails;
12
25
  }>) => string;
13
26
  export type EmailRendererMap = {
14
27
  readonly [Kind in EmailKind]: (data: EmailRenderDataMap[Kind]) => EmailTemplateContent;
@@ -19,6 +32,13 @@ export type EmailRendererMap = {
19
32
  * partial `renderers` map replaces only the listed kinds.
20
33
  */
21
34
  export type ShopEmailConfiguration = Readonly<{
35
+ /**
36
+ * Link to an order in the Medusa Admin; the shop's order notification shows
37
+ * an "Im Admin öffnen" button only when it is set.
38
+ */
39
+ adminOrderUrl?: (input: Readonly<{
40
+ orderId: string;
41
+ }>) => string;
22
42
  affiliatePortalUrl: string;
23
43
  displayName: string;
24
44
  inviteUrl: (input: Readonly<{
@@ -30,6 +50,11 @@ export type ShopEmailConfiguration = Readonly<{
30
50
  locale: string | null;
31
51
  }>) => string;
32
52
  renderers?: Partial<EmailRendererMap>;
53
+ /**
54
+ * IANA time zone of dates in the default templates (e.g. the order time in
55
+ * the shop's order notification); without it they are left out.
56
+ */
57
+ timeZone?: string;
33
58
  }>;
34
59
  /** A `ShopEmailConfiguration` with the defaults merged in; used internally. */
35
60
  export type ResolvedShopEmailConfiguration = Readonly<Omit<ShopEmailConfiguration, "layout" | "renderers"> & {