@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
@@ -55,7 +55,7 @@ export declare const listPluginFeatures: () => Promise<{
55
55
  source: "default" | "stored" | "config" | "none";
56
56
  invalidates: string[];
57
57
  } | {
58
- type: "string" | "boolean" | "url";
58
+ type: "string" | "boolean" | "url" | "image";
59
59
  default: string | number | boolean | null;
60
60
  value: string | number | boolean | null;
61
61
  key: string;
@@ -135,7 +135,7 @@ export declare const retrievePluginFeatures: (pluginKey: string) => Promise<{
135
135
  source: "default" | "stored" | "config" | "none";
136
136
  invalidates: string[];
137
137
  } | {
138
- type: "string" | "boolean" | "url";
138
+ type: "string" | "boolean" | "url" | "image";
139
139
  default: string | number | boolean | null;
140
140
  value: string | number | boolean | null;
141
141
  key: string;
@@ -215,7 +215,7 @@ export declare const updatePluginFeatures: (payload: UpdatePluginFeaturesPayload
215
215
  source: "default" | "stored" | "config" | "none";
216
216
  invalidates: string[];
217
217
  } | {
218
- type: "string" | "boolean" | "url";
218
+ type: "string" | "boolean" | "url" | "image";
219
219
  default: string | number | boolean | null;
220
220
  value: string | number | boolean | null;
221
221
  key: string;
@@ -295,7 +295,7 @@ export declare const updatePluginSettings: (pluginKey: string, payload: UpdatePl
295
295
  source: "default" | "stored" | "config" | "none";
296
296
  invalidates: string[];
297
297
  } | {
298
- type: "string" | "boolean" | "url";
298
+ type: "string" | "boolean" | "url" | "image";
299
299
  default: string | number | boolean | null;
300
300
  value: string | number | boolean | null;
301
301
  key: string;
@@ -59,6 +59,12 @@ export declare const texts: {
59
59
  numberMax: (max: number) => string;
60
60
  numberRange: (min: number, max: number) => string;
61
61
  selectPlaceholder: string;
62
+ imageUpload: string;
63
+ imageReplace: string;
64
+ imageRemove: string;
65
+ imageNone: string;
66
+ imageUploadFailed: string;
67
+ imagePreviewAlt: (label: string) => string;
62
68
  clearHint: string;
63
69
  source: {
64
70
  stored: string;
@@ -59,6 +59,12 @@ export const texts = {
59
59
  numberMax: (max) => `Höchstens ${max}.`,
60
60
  numberRange: (min, max) => `Zwischen ${min} und ${max}.`,
61
61
  selectPlaceholder: "Bitte auswählen",
62
+ imageUpload: "Bild hochladen",
63
+ imageReplace: "Anderes Bild hochladen",
64
+ imageRemove: "Entfernen",
65
+ imageNone: "Kein Bild hinterlegt",
66
+ imageUploadFailed: "Das Bild konnte nicht hochgeladen werden.",
67
+ imagePreviewAlt: (label) => `Vorschau ${label}`,
62
68
  clearHint: "Ein leeres Feld speichern entfernt den gespeicherten Wert (dann gilt Konfiguration oder Standard).",
63
69
  source: {
64
70
  stored: "gespeichert",
@@ -1,3 +1,3 @@
1
- import type { DocumentTemplateInput } from "../types";
2
- import type { DocumentLayout } from "./layout";
3
- export declare const renderFinancialDocument: (input: DocumentTemplateInput, layout: DocumentLayout, timeZone: string) => string;
1
+ import type { FinancialDocumentRenderInput } from "../types";
2
+ import { type DocumentLayout } from "./layout";
3
+ export declare const renderFinancialDocument: (input: FinancialDocumentRenderInput, layout: DocumentLayout, timeZone: string) => string;
@@ -3,20 +3,18 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.renderFinancialDocument = void 0;
4
4
  const format_1 = require("./format");
5
5
  const html_utils_1 = require("./html-utils");
6
+ const layout_1 = require("./layout");
6
7
  const labelsByKind = {
7
8
  cancellation: {
8
- date: "Stornodatum",
9
- recipient: "Stornorechnung an",
9
+ number: "Stornonummer",
10
10
  title: "Stornorechnung",
11
11
  },
12
12
  credit_note: {
13
- date: "Gutschriftsdatum",
14
- recipient: "Gutschrift an",
13
+ number: "Gutschriftnummer",
15
14
  title: "Gutschrift",
16
15
  },
17
16
  invoice: {
18
- date: "Rechnungsdatum",
19
- recipient: "Rechnung an",
17
+ number: "Rechnungsnummer",
20
18
  title: "Rechnung",
21
19
  },
22
20
  };
@@ -26,7 +24,8 @@ const renderAddress = (input) => {
26
24
  return (0, html_utils_1.escapeHtml)(input.order.email);
27
25
  }
28
26
  return [
29
- [address.company, address.first_name, address.last_name]
27
+ address.company,
28
+ [address.first_name, address.last_name]
30
29
  .filter((part) => Boolean(part))
31
30
  .join(" "),
32
31
  address.address_1,
@@ -34,34 +33,43 @@ const renderAddress = (input) => {
34
33
  [address.postal_code, address.city]
35
34
  .filter((part) => Boolean(part))
36
35
  .join(" "),
37
- address.country_code?.toUpperCase(),
36
+ address.country_code ? (0, layout_1.formatCountryName)(address.country_code) : null,
38
37
  ]
39
38
  .filter((part) => Boolean(part))
40
39
  .map(html_utils_1.escapeHtml)
41
40
  .join("<br />");
42
41
  };
43
- const renderLines = (input, timeZone) => input.lines
44
- .map((line) => {
45
- const batchDetails = line.batch_allocations
46
- .map((allocation) => `<div class="batch">Charge: ${(0, html_utils_1.escapeHtml)(allocation.lot_number)} · MHD: ${(0, html_utils_1.escapeHtml)((0, format_1.formatDate)(allocation.expiry_date, { timeZone }))} · ${(0, html_utils_1.escapeHtml)(allocation.quantity)}</div>`)
42
+ /** "Charge: L-1, MHD: 30.06.2027"; the quantity only when a line has several batches. */
43
+ const renderBatchDetails = (line, timeZone) => {
44
+ const showQuantity = line.batch_allocations.length > 1;
45
+ return line.batch_allocations
46
+ .map((allocation) => {
47
+ const parts = [
48
+ `Charge: ${allocation.lot_number}`,
49
+ `MHD: ${(0, format_1.formatDocumentDate)(allocation.expiry_date, timeZone)}`,
50
+ showQuantity ? `Menge: ${allocation.quantity}` : null,
51
+ ].filter((part) => Boolean(part));
52
+ return `<div class="batch">${(0, html_utils_1.escapeHtml)(parts.join(", "))}</div>`;
53
+ })
47
54
  .join("");
48
- return `<tr>
49
- <td>
50
- <strong>${(0, html_utils_1.escapeHtml)(line.title)}</strong>
55
+ };
56
+ const renderLines = (input, timeZone) => input.lines
57
+ .map((line) => `<tr>
58
+ <td class="item">
59
+ <div class="item-title">${(0, html_utils_1.escapeHtml)(line.title)}</div>
51
60
  ${line.sku ? `<div class="sku">SKU: ${(0, html_utils_1.escapeHtml)(line.sku)}</div>` : ""}
52
- ${batchDetails}
61
+ ${renderBatchDetails(line, timeZone)}
53
62
  </td>
54
- <td class="number">${(0, html_utils_1.escapeHtml)(line.quantity)}</td>
55
- <td class="number">${(0, html_utils_1.escapeHtml)((0, format_1.formatMoney)({ amount: line.unit_price.amount, currencyCode: line.unit_price.currency_code }))}</td>
56
- <td class="number">${(0, html_utils_1.escapeHtml)((0, format_1.formatMoney)({ amount: line.total.amount, currencyCode: line.total.currency_code }))}</td>
57
- </tr>`;
58
- })
63
+ <td class="number quantity">${(0, html_utils_1.escapeHtml)(line.quantity)}</td>
64
+ <td class="number">${(0, html_utils_1.renderMoney)(line.unit_price)}</td>
65
+ <td class="number">${(0, html_utils_1.renderMoney)(line.total)}</td>
66
+ </tr>`)
59
67
  .join("");
68
+ const isExemptIntraCommunitySupply = (input) => input.order.tax_compliance?.exemption_wording ===
69
+ "steuerfreie_innergemeinschaftliche_lieferung";
60
70
  const renderTaxCompliance = (input) => {
61
71
  const evidence = input.order.tax_compliance;
62
- if (!evidence ||
63
- evidence.exemption_wording !==
64
- "steuerfreie_innergemeinschaftliche_lieferung") {
72
+ if (!evidence || !isExemptIntraCommunitySupply(input)) {
65
73
  return "";
66
74
  }
67
75
  return `<section class="tax-compliance">
@@ -72,16 +80,18 @@ const renderTaxCompliance = (input) => {
72
80
  </section>`;
73
81
  };
74
82
  const renderDiscountTotal = (input) => {
75
- const { amount, currency_code: currencyCode } = input.totals.discount_total;
76
- const displayAmount = input.document.kind === "invoice" && Number(amount) > 0
77
- ? `-${amount}`
78
- : amount;
79
- return (0, html_utils_1.escapeHtml)((0, format_1.formatMoney)({ amount: displayAmount, currencyCode }));
83
+ const discount = input.totals.discount_total;
84
+ const displayAmount = input.document.kind === "invoice" && Number(discount.amount) > 0
85
+ ? `-${discount.amount}`
86
+ : discount.amount;
87
+ return (0, html_utils_1.renderMoney)(discount, displayAmount);
80
88
  };
81
89
  const formatTaxRate = (rate) => new Intl.NumberFormat(format_1.DEFAULT_FORMAT_LOCALE, {
82
90
  maximumFractionDigits: 2,
83
91
  style: "percent",
84
92
  }).format(rate / 100);
93
+ /** One totals row below the Menge/Einzelpreis columns; the amount sits under "Summe". */
94
+ const renderTotalRow = (label, value, className) => `<tr${className ? ` class="${className}"` : ""}><td class="spacer"></td><td class="label" colspan="2">${(0, html_utils_1.escapeHtml)(label)}</td><td class="number">${value}</td></tr>`;
85
95
  const renderTaxRows = (input) => {
86
96
  const { tax_breakdown: breakdown, tax_total: taxTotal } = input.totals;
87
97
  const rows = breakdown.length > 0
@@ -91,16 +101,17 @@ const renderTaxRows = (input) => {
91
101
  }))
92
102
  : [{ label: "Umsatzsteuer", money: taxTotal }];
93
103
  return rows
94
- .map(({ label, money }) => `<tr><td>${(0, html_utils_1.escapeHtml)(label)}</td><td class="number">${(0, html_utils_1.escapeHtml)((0, format_1.formatMoney)({ amount: money.amount, currencyCode: money.currency_code }))}</td></tr>`)
104
+ .map(({ label, money }) => renderTotalRow(label, (0, html_utils_1.renderMoney)(money)))
95
105
  .join("");
96
106
  };
107
+ /** Only invoices ask for payment; cancellations and credit notes do not. */
108
+ const renderPaymentTerms = (input) => input.document.kind === "invoice" && input.settings.payment_terms
109
+ ? `<section class="payment-terms">${(0, html_utils_1.escapeHtml)(input.settings.payment_terms)}</section>`
110
+ : "";
97
111
  const renderFinancialDocument = (input, layout, timeZone) => {
98
112
  const labels = labelsByKind[input.document.kind];
99
113
  const metadata = [
100
- {
101
- label: labels.date,
102
- value: (0, format_1.formatDate)(input.document.issued_at, { timeZone }),
103
- },
114
+ { label: labels.number, value: input.document.number },
104
115
  {
105
116
  label: "Bestellung",
106
117
  value: (0, html_utils_1.formatOrderReference)(input.order.display_id),
@@ -112,30 +123,37 @@ const renderFinancialDocument = (input, layout, timeZone) => {
112
123
  value: input.document.source_document_number ?? "-",
113
124
  });
114
125
  }
115
- const body = `<section class="recipient">
116
- <h2>${labels.recipient}</h2>
117
- ${renderAddress(input)}
118
- </section>
119
- ${renderTaxCompliance(input)}
120
- <table>
121
- <thead><tr><th>Artikel</th><th class="number">Menge</th><th class="number">Einzelpreis</th><th class="number">Summe</th></tr></thead>
122
- <tbody>${renderLines(input, timeZone)}</tbody>
123
- </table>
124
- <table class="totals">
125
- <tbody>
126
- <tr><td>Zwischensumme</td><td class="number">${(0, html_utils_1.escapeHtml)((0, format_1.formatMoney)({ amount: input.totals.subtotal.amount, currencyCode: input.totals.subtotal.currency_code }))}</td></tr>
127
- <tr><td>Versand</td><td class="number">${(0, html_utils_1.escapeHtml)((0, format_1.formatMoney)({ amount: input.totals.shipping_total.amount, currencyCode: input.totals.shipping_total.currency_code }))}</td></tr>
128
- <tr><td>Rabatt</td><td class="number">${renderDiscountTotal(input)}</td></tr>
126
+ if (input.order.tax_compliance && isExemptIntraCommunitySupply(input)) {
127
+ metadata.push({
128
+ label: "Kunden-UID",
129
+ value: input.order.tax_compliance.customer_vat_number,
130
+ });
131
+ }
132
+ metadata.push({
133
+ label: "Datum",
134
+ value: (0, format_1.formatDocumentDate)(input.document.issued_at, timeZone),
135
+ });
136
+ const { totals } = input;
137
+ const body = `<table class="items financial-items">
138
+ <thead><tr><th class="item">Artikel</th><th class="number quantity">Menge</th><th class="number">Einzelpreis</th><th class="number">Summe</th></tr></thead>
139
+ <tbody class="lines">${renderLines(input, timeZone)}</tbody>
140
+ <tbody class="totals">
141
+ ${renderTotalRow("Zwischensumme", (0, html_utils_1.renderMoney)(totals.subtotal))}
142
+ ${renderTotalRow("Versand", (0, html_utils_1.renderMoney)(totals.shipping_total))}
143
+ ${renderTotalRow("Rabatt", renderDiscountTotal(input))}
129
144
  ${renderTaxRows(input)}
130
- <tr><td>Gesamt</td><td class="number">${(0, html_utils_1.escapeHtml)((0, format_1.formatMoney)({ amount: input.totals.total.amount, currencyCode: input.totals.total.currency_code }))}</td></tr>
145
+ ${renderTotalRow("Gesamt", (0, html_utils_1.renderMoney)(totals.total), "grand-total")}
131
146
  </tbody>
132
- </table>`;
147
+ </table>
148
+ ${renderTaxCompliance(input)}
149
+ ${renderPaymentTerms(input)}`;
133
150
  return layout({
134
151
  body,
135
152
  documentTitle: labels.title,
136
153
  metadata,
137
154
  number: input.document.number,
138
155
  pageTitle: labels.title,
156
+ recipient: renderAddress(input),
139
157
  });
140
158
  };
141
159
  exports.renderFinancialDocument = renderFinancialDocument;
@@ -14,3 +14,17 @@ export type FormatDateOptions = {
14
14
  readonly options?: Omit<Intl.DateTimeFormatOptions, "timeZone">;
15
15
  };
16
16
  export declare const formatDate: (value: string | Date, { locale, options, timeZone }: FormatDateOptions) => string;
17
+ export type MoneyParts = {
18
+ /** Currency symbol or code; empty when the amount could not be formatted. */
19
+ readonly currency: string;
20
+ /** Signed, localized number without the currency. */
21
+ readonly amount: string;
22
+ };
23
+ /**
24
+ * `formatMoney` split into currency and number, so a template can align the
25
+ * currency and the amount separately. Falls back to the whole
26
+ * `formatMoney` text as `amount`.
27
+ */
28
+ export declare const formatMoneyParts: ({ amount, currencyCode }: FormatMoneyInput, { locale }?: FormatMoneyOptions) => MoneyParts;
29
+ /** A document date as "01.01.2027" in the given time zone. */
30
+ export declare const formatDocumentDate: (value: string | Date, timeZone: string) => string;
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.formatDate = exports.formatMoney = exports.DEFAULT_FORMAT_LOCALE = void 0;
3
+ exports.formatDocumentDate = exports.formatMoneyParts = exports.formatDate = exports.formatMoney = exports.DEFAULT_FORMAT_LOCALE = void 0;
4
4
  exports.DEFAULT_FORMAT_LOCALE = "de-AT";
5
5
  const formatMoney = ({ amount, currencyCode }, { locale = exports.DEFAULT_FORMAT_LOCALE } = {}) => {
6
6
  const numericAmount = Number(amount);
@@ -28,3 +28,45 @@ const formatDate = (value, { locale = exports.DEFAULT_FORMAT_LOCALE, options, ti
28
28
  return new Intl.DateTimeFormat(locale, { timeZone, ...options }).format(date);
29
29
  };
30
30
  exports.formatDate = formatDate;
31
+ /**
32
+ * `formatMoney` split into currency and number, so a template can align the
33
+ * currency and the amount separately. Falls back to the whole
34
+ * `formatMoney` text as `amount`.
35
+ */
36
+ const formatMoneyParts = ({ amount, currencyCode }, { locale = exports.DEFAULT_FORMAT_LOCALE } = {}) => {
37
+ const numericAmount = Number(amount);
38
+ const fallback = {
39
+ currency: "",
40
+ amount: (0, exports.formatMoney)({ amount, currencyCode }, { locale }),
41
+ };
42
+ if (!Number.isFinite(numericAmount)) {
43
+ return fallback;
44
+ }
45
+ try {
46
+ const parts = new Intl.NumberFormat(locale, {
47
+ currency: currencyCode.toUpperCase(),
48
+ style: "currency",
49
+ }).formatToParts(numericAmount);
50
+ return {
51
+ currency: parts
52
+ .filter((part) => part.type === "currency")
53
+ .map((part) => part.value)
54
+ .join(""),
55
+ amount: parts
56
+ .filter((part) => part.type !== "currency" &&
57
+ !(part.type === "literal" && !part.value.trim()))
58
+ .map((part) => part.value)
59
+ .join(""),
60
+ };
61
+ }
62
+ catch {
63
+ return fallback;
64
+ }
65
+ };
66
+ exports.formatMoneyParts = formatMoneyParts;
67
+ /** A document date as "01.01.2027" in the given time zone. */
68
+ const formatDocumentDate = (value, timeZone) => (0, exports.formatDate)(value, {
69
+ timeZone,
70
+ options: { day: "2-digit", month: "2-digit", year: "numeric" },
71
+ });
72
+ exports.formatDocumentDate = formatDocumentDate;
@@ -1,11 +1,15 @@
1
1
  import type { DocumentHtmlFactory } from "../types";
2
+ import { type DocumentTheme } from "./layout";
2
3
  /**
3
4
  * The plugin's default document markup (German, locale de-AT). The legal
4
5
  * sender comes with every render input (`input.merchant`, the base plugin's
5
6
  * shop master data). `timeZone` is the IANA time zone in which printed dates
6
7
  * are shown; the plugin passes its resolved `numbering.timeZone` so the date
7
- * on a document matches the period its number was allocated in.
8
+ * on a document matches the period its number was allocated in. `theme`
9
+ * adds a project's CSS and page background (watermark) on top of the default
10
+ * look; the logo comes from the Documents settings (`input.settings`).
8
11
  */
9
- export declare const createDefaultDocumentHtmlFactory: ({ timeZone, }: {
12
+ export declare const createDefaultDocumentHtmlFactory: ({ timeZone, theme, }: {
10
13
  timeZone: string;
14
+ theme?: DocumentTheme;
11
15
  }) => DocumentHtmlFactory;
@@ -10,9 +10,11 @@ const packing_list_1 = require("./packing-list");
10
10
  * sender comes with every render input (`input.merchant`, the base plugin's
11
11
  * shop master data). `timeZone` is the IANA time zone in which printed dates
12
12
  * are shown; the plugin passes its resolved `numbering.timeZone` so the date
13
- * on a document matches the period its number was allocated in.
13
+ * on a document matches the period its number was allocated in. `theme`
14
+ * adds a project's CSS and page background (watermark) on top of the default
15
+ * look; the logo comes from the Documents settings (`input.settings`).
14
16
  */
15
- const createDefaultDocumentHtmlFactory = ({ timeZone, }) => {
17
+ const createDefaultDocumentHtmlFactory = ({ timeZone, theme, }) => {
16
18
  return (input) => {
17
19
  const kind = input.document.kind;
18
20
  switch (kind) {
@@ -20,12 +22,12 @@ const createDefaultDocumentHtmlFactory = ({ timeZone, }) => {
20
22
  case "cancellation":
21
23
  case "credit_note":
22
24
  if ("totals" in input) {
23
- return (0, financial_document_1.renderFinancialDocument)(input, (0, layout_1.createDocumentLayout)(input.merchant, true), timeZone);
25
+ return (0, financial_document_1.renderFinancialDocument)(input, (0, layout_1.createDocumentLayout)(input.merchant, input.settings, true, theme), timeZone);
24
26
  }
25
27
  break;
26
28
  case "packing_list":
27
29
  if ("fulfillment" in input) {
28
- return (0, packing_list_1.renderPackingList)(input, (0, layout_1.createDocumentLayout)(input.merchant, false), timeZone);
30
+ return (0, packing_list_1.renderPackingList)(input, (0, layout_1.createDocumentLayout)(input.merchant, input.settings, false, theme), timeZone);
29
31
  }
30
32
  break;
31
33
  default: {
@@ -1,2 +1,8 @@
1
+ import type { DocumentMoney } from "../types";
1
2
  export declare const escapeHtml: (value: string | number | null | undefined) => string;
2
3
  export declare const formatOrderReference: (displayId: number | null) => string;
4
+ /**
5
+ * An amount as `<span class="money">` with separate currency and amount
6
+ * spans; `amount` overrides the money's own amount (e.g. a negated value).
7
+ */
8
+ export declare const renderMoney: (money: DocumentMoney, amount?: string) => string;
@@ -1,6 +1,7 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.formatOrderReference = exports.escapeHtml = void 0;
3
+ exports.renderMoney = exports.formatOrderReference = exports.escapeHtml = void 0;
4
+ const format_1 = require("./format");
4
5
  const escapeHtml = (value) => String(value ?? "")
5
6
  .replaceAll("&", "&amp;")
6
7
  .replaceAll("<", "&lt;")
@@ -10,3 +11,12 @@ const escapeHtml = (value) => String(value ?? "")
10
11
  exports.escapeHtml = escapeHtml;
11
12
  const formatOrderReference = (displayId) => displayId === null ? "-" : `#${displayId}`;
12
13
  exports.formatOrderReference = formatOrderReference;
14
+ /**
15
+ * An amount as `<span class="money">` with separate currency and amount
16
+ * spans; `amount` overrides the money's own amount (e.g. a negated value).
17
+ */
18
+ const renderMoney = (money, amount = money.amount) => {
19
+ const parts = (0, format_1.formatMoneyParts)({ amount, currencyCode: money.currency_code });
20
+ return `<span class="money"><span class="currency">${(0, exports.escapeHtml)(parts.currency)}</span> <span class="amount">${(0, exports.escapeHtml)(parts.amount)}</span></span>`;
21
+ };
22
+ exports.renderMoney = renderMoney;
@@ -1,4 +1,19 @@
1
- import type { DocumentMerchant } from "../types";
1
+ import type { DocumentRenderSettings, DocumentMerchant } from "../types";
2
+ /**
3
+ * Shop-specific look on top of the default markup. A project passes it to
4
+ * `createDefaultDocumentHtmlFactory` from its own `htmlFactory`, so it keeps
5
+ * the plugin's content and changes only the appearance.
6
+ */
7
+ export type DocumentTheme = {
8
+ /**
9
+ * CSS appended after the default stylesheet; later rules win. A
10
+ * full-page image such as a watermark belongs in `@page { background }`:
11
+ * it covers the whole sheet on every page, while Chromium clips fixed
12
+ * elements at the page margin and sizes an inline `<svg>` in them wrongly
13
+ * from the second page on.
14
+ */
15
+ readonly stylesheet?: string;
16
+ };
2
17
  export type DocumentLayoutInput = {
3
18
  readonly body: string;
4
19
  readonly documentTitle: string;
@@ -8,10 +23,13 @@ export type DocumentLayoutInput = {
8
23
  }[];
9
24
  readonly number: string;
10
25
  readonly pageTitle: string;
26
+ /** Escaped recipient address markup; `null` leaves the block empty. */
27
+ readonly recipient: string | null;
11
28
  };
12
29
  export type DocumentLayout = (input: DocumentLayoutInput) => string;
30
+ export declare const formatCountryName: (countryCode: string) => string;
13
31
  /**
14
32
  * `includeLegalDetails` adds UID, company register, contact and bank details
15
- * to the footer (financial documents); packing lists show only the name.
33
+ * to the footer (financial documents); packing lists show only the sender.
16
34
  */
17
- export declare const createDocumentLayout: (merchant: DocumentMerchant, includeLegalDetails: boolean) => DocumentLayout;
35
+ export declare const createDocumentLayout: (merchant: DocumentMerchant, settings: DocumentRenderSettings, includeLegalDetails: boolean, theme?: DocumentTheme) => DocumentLayout;