@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,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.EmailRenderDataSchemas = exports.EmailPersistedDataSchemas = void 0;
3
+ exports.EmailRenderDataSchemas = exports.EmailPersistedDataSchemas = exports.RefundSummarySchema = exports.ReturnReceivedSummarySchema = exports.ReturnSummarySchema = exports.ShipmentSummarySchema = exports.OrderSummarySchema = void 0;
4
4
  const zod_1 = require("@medusajs/framework/zod");
5
5
  const affiliate_1 = require("@hartl-services/medusa-food-supplements-contracts/schemas/affiliate");
6
6
  const NonEmptyStringSchema = zod_1.z.string().trim().min(1);
@@ -9,15 +9,146 @@ const DocumentsFactFingerprintSchema = zod_1.z
9
9
  .string()
10
10
  .regex(/^[a-f0-9]{64}$/)
11
11
  .optional();
12
+ const AmountSchema = zod_1.z.string().regex(/^-?\d+(\.\d+)?$/);
13
+ const OptionalTextSchema = zod_1.z.string().nullable();
14
+ const OrderSummaryAddressSchema = zod_1.z
15
+ .object({
16
+ first_name: OptionalTextSchema,
17
+ last_name: OptionalTextSchema,
18
+ company: OptionalTextSchema,
19
+ address_1: OptionalTextSchema,
20
+ address_2: OptionalTextSchema,
21
+ postal_code: OptionalTextSchema,
22
+ city: OptionalTextSchema,
23
+ country_code: OptionalTextSchema,
24
+ })
25
+ .strict();
26
+ /**
27
+ * Snapshot of an order when it was placed. Amounts are Medusa's gross
28
+ * decimal strings: item and shipping rows before discounts, so
29
+ * `original_item_total + original_shipping_total - discount_total` is the
30
+ * `total`, which already contains `tax_total`.
31
+ */
32
+ exports.OrderSummarySchema = zod_1.z
33
+ .object({
34
+ display_id: zod_1.z.number().int().nullable(),
35
+ /** The order's e-mail address; `null` when the order has none. */
36
+ email: OptionalTextSchema,
37
+ /** When the order was placed (ISO 8601). */
38
+ placed_at: zod_1.z.string().datetime(),
39
+ currency_code: NonEmptyStringSchema,
40
+ items: zod_1.z.array(zod_1.z
41
+ .object({
42
+ title: NonEmptyStringSchema,
43
+ variant_title: OptionalTextSchema,
44
+ quantity: zod_1.z.number().positive(),
45
+ original_total: AmountSchema,
46
+ })
47
+ .strict()),
48
+ shipping_method_names: zod_1.z.array(NonEmptyStringSchema),
49
+ original_item_total: AmountSchema,
50
+ original_shipping_total: AmountSchema,
51
+ discount_total: AmountSchema,
52
+ tax_total: AmountSchema,
53
+ total: AmountSchema,
54
+ billing_address: OrderSummaryAddressSchema.nullable(),
55
+ shipping_address: OrderSummaryAddressSchema.nullable(),
56
+ })
57
+ .strict();
58
+ /**
59
+ * Snapshot of one shipment when it was shipped: the parcel's items (title
60
+ * of the order line, shipped quantity), tracking numbers and links as
61
+ * entered at shipping, and whether more of the order is still to be shipped.
62
+ */
63
+ exports.ShipmentSummarySchema = zod_1.z
64
+ .object({
65
+ display_id: zod_1.z.number().int().nullable(),
66
+ shipping_method_name: OptionalTextSchema,
67
+ tracking: zod_1.z.array(zod_1.z
68
+ .object({
69
+ number: OptionalTextSchema,
70
+ url: OptionalTextSchema,
71
+ })
72
+ .strict()),
73
+ items: zod_1.z.array(zod_1.z
74
+ .object({
75
+ title: NonEmptyStringSchema,
76
+ variant_title: OptionalTextSchema,
77
+ quantity: zod_1.z.number().positive(),
78
+ })
79
+ .strict()),
80
+ delivery_address: OrderSummaryAddressSchema.nullable(),
81
+ more_to_follow: zod_1.z.boolean(),
82
+ })
83
+ .strict();
84
+ /**
85
+ * Snapshot of a return when it was requested: the return and order numbers,
86
+ * the items to send back (title of the order line, quantity, reason label),
87
+ * the return shipping option and where to send the parcel.
88
+ */
89
+ exports.ReturnSummarySchema = zod_1.z
90
+ .object({
91
+ display_id: zod_1.z.number().int().nullable(),
92
+ order_display_id: zod_1.z.number().int().nullable(),
93
+ shipping_method_name: OptionalTextSchema,
94
+ items: zod_1.z.array(zod_1.z
95
+ .object({
96
+ title: NonEmptyStringSchema,
97
+ variant_title: OptionalTextSchema,
98
+ quantity: zod_1.z.number().positive(),
99
+ reason: OptionalTextSchema,
100
+ })
101
+ .strict()),
102
+ return_address: OrderSummaryAddressSchema.nullable(),
103
+ })
104
+ .strict();
105
+ /**
106
+ * Snapshot of a return when it was completely received: the return and
107
+ * order numbers, when it arrived and the received items (title of the order
108
+ * line, received quantity and how much of it arrived damaged).
109
+ */
110
+ exports.ReturnReceivedSummarySchema = zod_1.z
111
+ .object({
112
+ display_id: zod_1.z.number().int().nullable(),
113
+ order_display_id: zod_1.z.number().int().nullable(),
114
+ received_at: zod_1.z.string().datetime().nullable(),
115
+ items: zod_1.z.array(zod_1.z
116
+ .object({
117
+ title: NonEmptyStringSchema,
118
+ variant_title: OptionalTextSchema,
119
+ quantity: zod_1.z.number().positive(),
120
+ damaged_quantity: zod_1.z.number().nonnegative(),
121
+ })
122
+ .strict()),
123
+ })
124
+ .strict();
125
+ /**
126
+ * Snapshot of one refund when its Documents fact arrived: the order number,
127
+ * the refunded gross amount, when it was refunded and the refund reason
128
+ * chosen in the Admin.
129
+ */
130
+ exports.RefundSummarySchema = zod_1.z
131
+ .object({
132
+ order_display_id: zod_1.z.number().int().nullable(),
133
+ amount: AmountSchema,
134
+ currency_code: NonEmptyStringSchema,
135
+ refunded_at: zod_1.z.string().datetime().nullable(),
136
+ reason: OptionalTextSchema,
137
+ })
138
+ .strict();
12
139
  exports.EmailPersistedDataSchemas = {
13
140
  "order.received": zod_1.z
14
141
  .object({
15
142
  order_id: NonEmptyStringSchema,
143
+ // Optional: messages stored before the summary existed have none.
144
+ summary: exports.OrderSummarySchema.optional(),
16
145
  })
17
146
  .strict(),
18
147
  "order.received.admin": zod_1.z
19
148
  .object({
20
149
  order_id: NonEmptyStringSchema,
150
+ // Optional: messages stored before the summary existed have none.
151
+ summary: exports.OrderSummarySchema.optional(),
21
152
  })
22
153
  .strict(),
23
154
  "order.shipped": zod_1.z
@@ -25,24 +156,38 @@ exports.EmailPersistedDataSchemas = {
25
156
  order_id: NonEmptyStringSchema,
26
157
  fulfillment_id: NonEmptyStringSchema,
27
158
  documents_fact_fingerprint: DocumentsFactFingerprintSchema,
159
+ // Optional: messages stored before the snapshot existed have none.
160
+ shipment: exports.ShipmentSummarySchema.optional(),
161
+ /** The invoice PDF is attached (Documents chose this shipment). */
162
+ invoice_attached: zod_1.z.boolean().optional(),
28
163
  })
29
164
  .strict(),
30
165
  "order.canceled": zod_1.z
31
166
  .object({
32
167
  order_id: NonEmptyStringSchema,
33
168
  documents_fact_fingerprint: DocumentsFactFingerprintSchema,
169
+ // Optional: messages stored before the snapshot existed have none.
170
+ summary: exports.OrderSummarySchema.optional(),
171
+ /** When the order was canceled (ISO 8601). */
172
+ canceled_at: zod_1.z.string().datetime().optional(),
173
+ /** A cancellation document PDF is attached (from Documents). */
174
+ cancellation_attached: zod_1.z.boolean().optional(),
34
175
  })
35
176
  .strict(),
36
177
  "order.return_requested": zod_1.z
37
178
  .object({
38
179
  order_id: NonEmptyStringSchema,
39
180
  return_id: NonEmptyStringSchema,
181
+ // Optional: messages stored before the snapshot existed have none.
182
+ summary: exports.ReturnSummarySchema.optional(),
40
183
  })
41
184
  .strict(),
42
185
  "order.return_received": zod_1.z
43
186
  .object({
44
187
  order_id: NonEmptyStringSchema,
45
188
  return_id: NonEmptyStringSchema,
189
+ // Optional: messages stored before the snapshot existed have none.
190
+ summary: exports.ReturnReceivedSummarySchema.optional(),
46
191
  })
47
192
  .strict(),
48
193
  "order.refunded": zod_1.z
@@ -50,6 +195,10 @@ exports.EmailPersistedDataSchemas = {
50
195
  order_id: NonEmptyStringSchema,
51
196
  refund_id: NonEmptyStringSchema,
52
197
  documents_fact_fingerprint: DocumentsFactFingerprintSchema,
198
+ // Optional: messages stored before the snapshot existed have none.
199
+ summary: exports.RefundSummarySchema.optional(),
200
+ /** A cancellation document PDF is attached (from Documents). */
201
+ cancellation_attached: zod_1.z.boolean().optional(),
53
202
  })
54
203
  .strict(),
55
204
  "auth.password_reset": zod_1.z
@@ -71,8 +220,12 @@ exports.EmailRenderDataSchemas = {
71
220
  ...exports.EmailPersistedDataSchemas,
72
221
  "auth.password_reset": exports.EmailPersistedDataSchemas["auth.password_reset"].extend({
73
222
  reset_url: zod_1.z.string().url(),
223
+ /** How long the reset link is valid, from the token; `undefined` if unknown. */
224
+ valid_minutes: zod_1.z.number().int().positive().optional(),
74
225
  }),
75
226
  "user.invite": exports.EmailPersistedDataSchemas["user.invite"].extend({
76
227
  invite_url: zod_1.z.string().url(),
228
+ /** When the invite expires (ISO 8601), from the current invite. */
229
+ expires_at: zod_1.z.string().datetime().optional(),
77
230
  }),
78
231
  };
@@ -11,6 +11,7 @@ const SETTING_TYPES = new Set([
11
11
  "boolean",
12
12
  "secret",
13
13
  "select",
14
+ "image",
14
15
  ]);
15
16
  const ACTION_MODES = new Set(["sync", "background"]);
16
17
  const ADMIN_PAGE_PREFIX = "/settings/hartl-services/";
@@ -33,6 +34,7 @@ const defaultMatchesType = (setting) => {
33
34
  case "string":
34
35
  return hasText(value);
35
36
  case "url":
37
+ case "image":
36
38
  return typeof value === "string" && isHttpUrl(value);
37
39
  case "number":
38
40
  return typeof value === "number" && Number.isFinite(value);
@@ -5,7 +5,7 @@ import type { PluginSettingsFallback } from "./types";
5
5
  * on the generic plugin page like any plugin's settings. It cannot be
6
6
  * switched off and declares no features.
7
7
  */
8
- export declare const shopMasterDataDescriptor: import("./types").PluginFeatureDescriptor<string, "legal_name" | "address_line1" | "address_line2" | "postal_code" | "city" | "country_code" | "vat_number" | "email" | "phone" | "website" | "iban" | "bic" | "company_register_number" | "company_register_court", never>;
8
+ export declare const shopMasterDataDescriptor: import("./types").PluginFeatureDescriptor<string, "country_code" | "legal_name" | "address_line1" | "address_line2" | "postal_code" | "city" | "vat_number" | "email" | "phone" | "website" | "iban" | "bic" | "company_register_number" | "company_register_court", never>;
9
9
  /** Keys of the shop master data settings. */
10
10
  export type ShopMasterDataKey = "legal_name" | "address_line1" | "address_line2" | "postal_code" | "city" | "country_code" | "vat_number" | "email" | "phone" | "website" | "iban" | "bic" | "company_register_number" | "company_register_court";
11
11
  /** Every key that has a value (stored > fallback). All values are strings. */
@@ -12,7 +12,7 @@ const settings_1 = require("./settings");
12
12
  exports.shopMasterDataDescriptor = (0, descriptor_1.definePluginFeatures)({
13
13
  plugin_key: "shop",
14
14
  label: "Shop-Stammdaten",
15
- description: "Firmenangaben des Shops für Rechnungen, Dokumente und Steuerprüfungen.",
15
+ description: "Firmenangaben des Shops für E-Mails, Rechnungen, Dokumente und Steuerprüfungen.",
16
16
  switchable: false,
17
17
  features: [],
18
18
  settings: [
@@ -30,7 +30,8 @@ export type PluginFeatureDefinition<K extends string = string> = {
30
30
  readonly default_enabled: boolean;
31
31
  readonly parent_key?: K;
32
32
  };
33
- export type PluginSettingType = "string" | "url" | "number" | "boolean" | "secret" | "select";
33
+ /** `image`: public http(s) URL of an image the Admin uploads (PNG/JPEG). */
34
+ export type PluginSettingType = "string" | "url" | "number" | "boolean" | "secret" | "select" | "image";
34
35
  /** One allowed value of a `"select"` setting. */
35
36
  export type PluginSettingOption = {
36
37
  readonly value: string;
@@ -144,6 +144,8 @@ function buildFoodShopConfig(options, environment = process.env) {
144
144
  email: {
145
145
  jwtSecret: env.JWT_SECRET,
146
146
  shop: {
147
+ // The Admin dashboard mounts under admin.path ("/app").
148
+ adminOrderUrl: ({ orderId }) => new URL(`/app/orders/${encodeURIComponent(orderId)}`, backendOrigin).toString(),
147
149
  displayName: options.displayName,
148
150
  affiliatePortalUrl: (0, config_1.shopUrl)(storefrontOrigin, `${storefrontPath}/partnerportal`),
149
151
  // The Admin dashboard mounts "/invite" under admin.path ("/app").
@@ -155,9 +157,11 @@ function buildFoodShopConfig(options, environment = process.env) {
155
157
  orderNotificationEmail: options.orderNotificationEmail,
156
158
  // The e-mail module appends "#token=<token>".
157
159
  passwordResetUrl: () => (0, config_1.shopUrl)(storefrontOrigin, `${storefrontPath}/reset-password`),
160
+ timeZone: options.timeZone,
158
161
  },
159
162
  },
160
163
  documents: {
164
+ htmlFactory: options.documentHtmlFactory,
161
165
  // Fallback only: the shop master data in the Admin ("Hartl Services
162
166
  // Plugins" -> Shop-Stammdaten) wins per field.
163
167
  merchant,
@@ -1,4 +1,4 @@
1
- import type { DocumentMerchantOption } from "../documents/types";
1
+ import type { DocumentHtmlFactory, DocumentMerchantOption } from "../documents/types";
2
2
  /**
3
3
  * The shop-specific facts of a food shop. Deployment values and secrets are
4
4
  * not options: they come from the environment (see the README).
@@ -16,8 +16,8 @@ export type FoodShopConfigOptions = Readonly<{
16
16
  orderNotificationEmail: string;
17
17
  /**
18
18
  * IANA time zone (e.g. "Europe/Vienna"): chooses the annual document-number
19
- * period, matches the dates printed on documents, and schedules the
20
- * affiliate consignment reminders.
19
+ * period, matches the dates printed on documents and e-mails, and
20
+ * schedules the affiliate consignment reminders.
21
21
  */
22
22
  timeZone: string;
23
23
  /**
@@ -26,6 +26,13 @@ export type FoodShopConfigOptions = Readonly<{
26
26
  * affiliate referral links to `/<code>`.
27
27
  */
28
28
  storefrontCountryCode: string;
29
+ /**
30
+ * Replaces the default document markup (documents `htmlFactory`), e.g.
31
+ * `createDefaultDocumentHtmlFactory({ timeZone, theme })` from
32
+ * `@hartl-services/medusa-base/config` with the shop's own CSS and
33
+ * watermark. Omit it for the plugin's default look.
34
+ */
35
+ documentHtmlFactory?: DocumentHtmlFactory;
29
36
  /**
30
37
  * Load `.env.<NODE_ENV>` from the working directory into `process.env`
31
38
  * like `medusa-config.ts` does by default. Pass `false` when the
@@ -63,6 +63,10 @@ function assertFoodShopConfigOptions(options) {
63
63
  requireEmail(candidate.orderNotificationEmail, "orderNotificationEmail");
64
64
  requireTimeZone(candidate.timeZone, "timeZone");
65
65
  requireCountryCode(candidate.storefrontCountryCode, "storefrontCountryCode");
66
+ if (candidate.documentHtmlFactory !== undefined &&
67
+ typeof candidate.documentHtmlFactory !== "function") {
68
+ fail("documentHtmlFactory", "a function");
69
+ }
66
70
  if (candidate.loadEnvFile !== undefined &&
67
71
  typeof candidate.loadEnvFile !== "boolean") {
68
72
  fail("loadEnvFile", "a boolean");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hartl-services/medusa-base",
3
- "version": "1.0.0",
3
+ "version": "1.1.0",
4
4
  "description": "Base plugin for Hartl Services Medusa shops: per-plugin feature toggles with a generic Admin settings page, a Store endpoint for storefronts, enforcement helpers, the complete-cart hook composition, the root redirect, cart-promotion link repair, the built-in E-Mail, Documents and S3 file storage modules, config helpers with the turnkey food-shop preset, and the Sentry logger wrapper and instrumentation.",
5
5
  "license": "MIT",
6
6
  "files": [
@@ -88,7 +88,7 @@
88
88
  "mime-types": "3.0.1",
89
89
  "nodemailer": "6.10.1",
90
90
  "ulid": "^2.4.0",
91
- "@hartl-services/medusa-food-supplements-contracts": "^0.16.0"
91
+ "@hartl-services/medusa-food-supplements-contracts": "^0.17.0"
92
92
  },
93
93
  "devDependencies": {
94
94
  "@medusajs/admin-sdk": "2.19.0",