@hartl-services/medusa-base 4.0.0 → 5.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,70 +1,123 @@
1
1
  # Medusa Base
2
2
 
3
- Medusa v2.19 base plugin for Hartl Services shops. Every shop installs it. It
4
- provides per-plugin feature toggles, runtime settings and actions: a generic
5
- Admin hub under **Settings → Hartl Services Plugins** with one page per
6
- plugin, a Store endpoint for storefronts and helpers that plugins use to
7
- enforce their toggles and read their settings. Since 1.0.0 it also contains
8
- the shop's transactional E-Mail, order Documents (invoices, cancellations,
9
- packing lists) and S3 file storage, and the turnkey `medusa-config.ts` preset,
10
- and since 3.0.0 the EU tax compliance (UID check, Tax provider) (see "E-Mail",
11
- "Dokumente", "Steuer-Compliance", "Dateispeicher (S3)" and
12
- "Shop-Config-Preset").
13
-
14
- ## What it does
15
-
16
- - Discovers every registered module whose service declares `pluginFeatures`
17
- (see below). No plugin has to register itself anywhere else.
18
- - Stores one row per plugin in `plugin_feature_state` (module
19
- `plugin_features`): the plugin switch, the configured feature values and a
20
- `version` for optimistic concurrency.
21
- - Serves the effective values to storefronts and the configured plus effective
22
- values to the Admin.
23
- - Exports `definePluginFeatures`, `requirePluginFeature` and
24
- `isPluginFeatureEnabled` from `@hartl-services/medusa-base` for plugins.
25
- - Emits `plugin-features.updated.v1` after every saved Admin toggle update
26
- (see "Toggle-change event").
27
- - Stores runtime settings that plugins declare (secrets encrypted), resolves
28
- them with the medusa-config fallback and exports `getPluginSettings` (see
29
- "Runtime settings").
30
- - Runs actions that plugins declare as workflow ids, records every run with
31
- its progress and outcome and rejects a second start while one runs (see
32
- "Actions").
33
- - Holds the shop's master data (Firmenangaben) as its own settings and
34
- exports `getShopMasterData` (see "Shop-Stammdaten").
35
- - Exports Admin components from `@hartl-services/medusa-base/admin-components`
36
- (also `sdk`, `getErrorMessage` and the rich-text editor) and server helpers
37
- from `@hartl-services/medusa-base/lib` (`toWireResponse`) so a plugin composes the base's sections into its own page and hides
38
- widgets of disabled features (see "Admin components for plugin pages").
39
- - Owns the `completeCartWorkflow` hooks `validate` and `orderCreated` and runs
40
- the workflows that plugins declare for them (see "Complete-cart hook
41
- composition").
42
- - Redirects `GET /` to the admin dashboard and repairs dangling
43
- `cart_promotion` links (see "Root redirect" and "Cart-promotion link
44
- repair").
45
- - Requires passwords of at least 15 characters for Admin users and, by
46
- default, customers; shops can set their own customer rules (see "Password
47
- policy").
48
- - Starts Admin draft orders without a customer order confirmation and marks
49
- placed 0 € orders without payment as paid (see "Draft orders and 0 €
50
- orders").
51
- - Exports the shop's `medusa-config.ts` helpers from
3
+ Medusa v2.19 base plugin for Hartl Services shops. Every shop installs it,
4
+ first in its plugin list, because the other Hartl Services plugins read their
5
+ feature toggles through it: it provides per-plugin feature toggles, runtime
6
+ settings and actions, a generic Admin hub under **Settings → Hartl Services
7
+ Plugins** with one page per plugin, a Store endpoint for storefronts and
8
+ helpers that plugins use to enforce their toggles and read their settings.
9
+ Since 1.0.0 it also contains the shop's transactional E-Mail, order Documents
10
+ (invoices, cancellations, packing lists), S3 file storage and the turnkey
11
+ `medusa-config.ts` preset, since 3.0.0 the EU tax compliance (UID check, Tax
12
+ provider) and since 5.0.0 the free-shipping thresholds. The "Overview" lists
13
+ every area with its section; "Upgrading" collects the version notes.
14
+
15
+ ## Table of contents
16
+
17
+ - [Overview](#overview)
18
+ - [Installation in a shop](#installation-in-a-shop)
19
+ - [Upgrading](#upgrading)
20
+ - [Upgrading to 0.4.0](#upgrading-to-040)
21
+ - [Upgrading to 0.4.1](#upgrading-to-041)
22
+ - [Upgrading to 1.0.0](#upgrading-to-100)
23
+ - [Upgrading to 3.0.0](#upgrading-to-300)
24
+ - [Upgrading to 4.0.0](#upgrading-to-400)
25
+ - [Upgrading to 5.0.0](#upgrading-to-500)
26
+ - [Shop-Config-Preset](#shop-config-preset)
27
+ - [Installing the preset](#installing-the-preset)
28
+ - [Usage](#usage)
29
+ - [API](#api)
30
+ - [Options](#options)
31
+ - [Environment variables](#environment-variables)
32
+ - [Fixed configuration](#fixed-configuration)
33
+ - [Plugin platform](#plugin-platform)
34
+ - [HTTP endpoints](#http-endpoints)
35
+ - [Declaring features in a plugin](#declaring-features-in-a-plugin)
36
+ - [Runtime settings](#runtime-settings)
37
+ - [Actions](#actions)
38
+ - [Shop-Stammdaten](#shop-stammdaten)
39
+ - [Admin pages](#admin-pages)
40
+ - [Semantics](#semantics)
41
+ - [Complete-cart hook composition](#complete-cart-hook-composition)
42
+ - [Built-in features](#built-in-features)
43
+ - [Kunden-Login](#kunden-login)
44
+ - [E-Mail](#e-mail)
45
+ - [Dokumente](#dokumente)
46
+ - [Steuer-Compliance](#steuer-compliance)
47
+ - [Versandkostenfrei](#versandkostenfrei)
48
+ - [Dateispeicher (S3)](#dateispeicher-s3)
49
+ - [Shop behaviour and operations](#shop-behaviour-and-operations)
50
+ - [Root redirect](#root-redirect)
51
+ - [Cart-promotion link repair](#cart-promotion-link-repair)
52
+ - [Draft orders and 0 € orders](#draft-orders-and-0--orders)
53
+ - [Password policy](#password-policy)
54
+ - [Config helpers](#config-helpers)
55
+ - [Sentry logger wrapper](#sentry-logger-wrapper)
56
+ - [Sentry instrumentation](#sentry-instrumentation)
57
+ - [Development](#development)
58
+ - [Local e-mail development](#local-e-mail-development)
59
+ - [Build output of `/admin-components`](#build-output-of-admin-components)
60
+
61
+ ## Overview
62
+
63
+ - **Feature toggles** (see "Plugin platform"): discovers every registered
64
+ module whose service declares `pluginFeatures`, so no plugin has to register
65
+ itself anywhere else; stores one row per plugin in `plugin_feature_state`
66
+ (module `plugin_features`) with the plugin switch, the configured feature
67
+ values and a `version` for optimistic concurrency; serves the effective
68
+ values to storefronts and the configured plus effective values to the Admin
69
+ (see "HTTP endpoints"); exports `definePluginFeatures`,
70
+ `requirePluginFeature` and `isPluginFeatureEnabled` from
71
+ `@hartl-services/medusa-base` for plugins (see "Declaring features in a
72
+ plugin") and emits `plugin-features.updated.v1` after every saved Admin
73
+ toggle update (see "Toggle-change event").
74
+ - **Runtime settings**: stores runtime settings that plugins declare (secrets
75
+ encrypted), resolves them with the medusa-config fallback and exports
76
+ `getPluginSettings` (see "Runtime settings").
77
+ - **Actions**: runs actions that plugins declare as workflow ids, records every
78
+ run with its progress and outcome and rejects a second start while one runs
79
+ (see "Actions").
80
+ - **Shop-Stammdaten**: holds the shop's master data (Firmenangaben) as its own
81
+ settings and exports `getShopMasterData` (see "Shop-Stammdaten").
82
+ - **Admin components and server helpers**: exports Admin components from
83
+ `@hartl-services/medusa-base/admin-components` (also `sdk`,
84
+ `getErrorMessage` and the rich-text editor) and server helpers from
85
+ `@hartl-services/medusa-base/lib` (`toWireResponse`) so a plugin composes
86
+ the base's sections into its own page and hides widgets of disabled
87
+ features (see "Admin pages" and "Admin components for plugin pages").
88
+ - **Complete-cart hooks**: owns the `completeCartWorkflow` hooks `validate`
89
+ and `orderCreated` and runs the workflows that plugins declare for them (see
90
+ "Complete-cart hook composition").
91
+ - **Kunden-Login**: declares which login methods customers may use (module
92
+ `customer_auth`: e-mail + password and a one-time code by e-mail), provides
93
+ the `email-otp` auth provider and the code request route (see
94
+ "Kunden-Login"); requires passwords of at least 15 characters for Admin
95
+ users and, by default, customers, and lets shops set their own customer
96
+ rules (see "Password policy").
97
+ - **E-Mail**: sends the shop's transactional e-mails over SMTP or Microsoft
98
+ 365, chosen in the Admin (module `email`, see "E-Mail").
99
+ - **Dokumente**: issues invoices, cancellation documents and packing lists as
100
+ private PDFs (module `documents`, see "Dokumente").
101
+ - **Steuer-Compliance**: checks B2B customers' VAT IDs (FinanzOnline),
102
+ provides the Medusa Tax provider `tp_tax_compliance` for reverse-charge and
103
+ apportioned shipping tax and freezes the tax evidence of an order at
104
+ checkout (module `tax_compliance`, see "Steuer-Compliance").
105
+ - **Versandkostenfrei**: keeps the two automatic free-shipping promotions
106
+ (shop-wide and affiliate threshold) in sync with their Admin settings
107
+ (module `free_shipping`, see "Versandkostenfrei").
108
+ - **Dateispeicher (S3)**: stores files in S3-compatible storage behind the
109
+ `/files` gateway (see "Dateispeicher (S3)").
110
+ - **Shop behaviour**: redirects `GET /` to the admin dashboard, repairs
111
+ dangling `cart_promotion` links, starts Admin draft orders without a
112
+ customer order confirmation and marks placed 0 € orders without payment as
113
+ paid (see "Root redirect", "Cart-promotion link repair" and "Draft orders
114
+ and 0 € orders").
115
+ - **Shop configuration**: builds a shop's complete `medusa-config.ts` from a
116
+ few shop facts (`@hartl-services/medusa-base/shop-config`, see
117
+ "Shop-Config-Preset"); exports the generic `medusa-config.ts` helpers from
52
118
  `@hartl-services/medusa-base/config` and Sentry's `register()` from
53
- `@hartl-services/medusa-base/instrumentation` (see "Config helpers" and
54
- "Sentry instrumentation").
55
- - Sends the shop's transactional e-mails over SMTP or Microsoft 365, chosen in the Admin (module `email`, see
56
- "E-Mail"), issues invoices, cancellation documents and packing lists as
57
- private PDFs (module `documents`, see "Dokumente") and stores files in
58
- S3-compatible storage behind the `/files` gateway (see "Dateispeicher (S3)").
59
- - Checks B2B customers' VAT IDs (FinanzOnline), provides the Medusa Tax
60
- provider `tp_tax_compliance` for reverse-charge and apportioned shipping tax
61
- and freezes the tax evidence of an order at checkout (module
62
- `tax_compliance`, see "Steuer-Compliance").
63
- - Declares which login methods customers may use (module `customer_auth`:
64
- e-mail + password and a one-time code by e-mail), provides the `email-otp`
65
- auth provider and the code request route (see "Kunden-Login").
66
- - Builds a shop's complete `medusa-config.ts` from a few shop facts
67
- (`@hartl-services/medusa-base/shop-config`, see "Shop-Config-Preset").
119
+ `@hartl-services/medusa-base/instrumentation` (see "Config helpers",
120
+ "Sentry logger wrapper" and "Sentry instrumentation").
68
121
 
69
122
  ## Installation in a shop
70
123
 
@@ -105,11 +158,35 @@ every module of the plugin; each module reads only its own namespace:
105
158
  `options: {}` is valid: the base then only provides the feature toggles,
106
159
  settings, actions and hooks, the e-mail module cannot dispatch, documents use
107
160
  the defaults and the `/files` routes answer that signing is not configured.
108
- Run `medusa db:migrate` after installing it (and after upgrading: 0.4.0 adds
109
- the tables `plugin_setting_state` and `plugin_action_run`; 1.0.0 brings the
110
- `email` and `documents` tables of the former `@hartl-services/medusa-email` and
111
- `@hartl-services/medusa-documents` packages with unchanged table and
112
- migration names, so a shop that used them needs no data migration).
161
+ Run `medusa db:migrate` after installing it and after every upgrade (see
162
+ "Upgrading").
163
+
164
+ Peer dependencies are the host's Medusa 2.19 packages (`@medusajs/framework`,
165
+ `@medusajs/medusa`, `@medusajs/admin-sdk`, `@medusajs/js-sdk`, `@medusajs/ui`, `@medusajs/icons`)
166
+ plus React, React Router and TanStack Query in the versions the Medusa 2.19
167
+ dashboard uses.
168
+
169
+ ## Upgrading
170
+
171
+ Run `medusa db:migrate` after every upgrade. The versions below add tables or
172
+ migration scripts, merge former packages into the base or rename exports.
173
+
174
+ ### Upgrading to 0.4.0
175
+
176
+ 0.4.0 adds the tables `plugin_setting_state` and `plugin_action_run`.
177
+
178
+ ### Upgrading to 0.4.1
179
+
180
+ `medusa db:migrate` runs the migration script
181
+ `dismiss-orphaned-cart-promotion-links` once (see "Cart-promotion link
182
+ repair"); nothing to configure.
183
+
184
+ ### Upgrading to 1.0.0
185
+
186
+ 1.0.0 brings the `email` and `documents` tables of the former
187
+ `@hartl-services/medusa-email` and `@hartl-services/medusa-documents` packages
188
+ with unchanged table and migration names, so a shop that used them needs no
189
+ data migration.
113
190
 
114
191
  Upgrading from the separate packages: remove `@hartl-services/medusa-email`,
115
192
  `@hartl-services/medusa-documents`, `@hartl-services/medusa-s3-cache-storage`
@@ -122,7 +199,9 @@ exports from the matching `@hartl-services/medusa-base` subpath. The
122
199
  `documentsEnabled` option is gone; Documents is always installed, and the
123
200
  e-mail module follows the Documents `invoices` toggle.
124
201
 
125
- Upgrading to 3.0.0 (Steuer-Compliance merged): remove
202
+ ### Upgrading to 3.0.0
203
+
204
+ Steuer-Compliance was merged into the base: remove
126
205
  `@hartl-services/medusa-tax-compliance` from `package.json` and from the
127
206
  `plugins` list and change the Tax provider resolve path to
128
207
  `@hartl-services/medusa-base/providers/tax-compliance` (the provider id
@@ -133,7 +212,9 @@ reached `tax_compliance` by container key keep working; the typed helper is
133
212
  Hartl Services plugins need the versions released with 3.0.0 (their peer
134
213
  range is `^3`).
135
214
 
136
- Upgrading to 4.0.0 (renames): the Contracts package is
215
+ ### Upgrading to 4.0.0
216
+
217
+ Renames: the Contracts package is
137
218
  `@hartl-services/medusa-contracts` (1.0.0, formerly
138
219
  `@hartl-services/medusa-food-supplements-contracts`; same subpaths), and the
139
220
  Shop-Config-Preset exports are `buildShopConfig`, `defineShopConfig`,
@@ -143,12 +224,146 @@ Shop-Config-Preset exports are `buildShopConfig`, `defineShopConfig`,
143
224
  Storefront; nothing else changes. The other Hartl Services plugins need the
144
225
  versions released with 4.0.0 (peer range `^4`).
145
226
 
146
- Peer dependencies are the host's Medusa 2.19 packages (`@medusajs/framework`,
147
- `@medusajs/medusa`, `@medusajs/admin-sdk`, `@medusajs/js-sdk`, `@medusajs/ui`, `@medusajs/icons`)
148
- plus React, React Router and TanStack Query in the versions the Medusa 2.19
149
- dashboard uses.
227
+ ### Upgrading to 5.0.0
228
+
229
+ Versandkostenfrei was merged into the base: remove
230
+ `@hartl-services/medusa-free-shipping` from `package.json` and from the
231
+ `plugins` list (the module key `free_shipping`, its migrations, settings,
232
+ promotions, job and subscriber ids are unchanged, so no data migration is
233
+ needed; a shop that keeps the old package registered fails at boot with a
234
+ duplicate `free_shipping` module). The other Hartl Services plugins need the
235
+ versions released with 5.0.0 (peer range `^5`).
236
+
237
+ ## Shop-Config-Preset
238
+
239
+ `@hartl-services/medusa-base/shop-config` turns a shop's configuration
240
+ from about 290 lines into about 30: the plugin list and order, the Redis event
241
+ bus, SMTP notifications, S3 file storage, the tax and Stripe payment providers,
242
+ the Admin path, the Sentry logger bridge and the environment validation are
243
+ assembled there; the shop supplies only its own facts. It reuses the `/config`
244
+ helpers (see "Config helpers") for environment parsing. It is a separate subpath because it
245
+ loads the Sentry logger wrapper and with it `@sentry/node`; `/config` stays
246
+ free of it.
247
+
248
+ The preset registers the base (with its E-Mail, Documents and `/files`
249
+ options, and its Tax provider) plus three feature plugins, in this order:
250
+ `medusa-base`, `medusa-batch-management`,
251
+ `medusa-affiliate`, `medusa-payload-sync`.
252
+
253
+ ### Installing the preset
254
+
255
+ ```bash
256
+ pnpm add @hartl-services/medusa-base \
257
+ @hartl-services/medusa-batch-management \
258
+ @hartl-services/medusa-affiliate \
259
+ @hartl-services/medusa-payload-sync \
260
+ @medusajs/event-bus-redis @medusajs/payment-stripe \
261
+ @sentry/node
262
+ ```
263
+
264
+ ### Usage
265
+
266
+ ```ts
267
+ // medusa-config.ts
268
+ import { defineShopConfig } from "@hartl-services/medusa-base/shop-config"
269
+
270
+ module.exports = defineShopConfig({
271
+ displayName: "EXAMPLE SHOP",
272
+ orderNotificationEmail: "orders@example.com",
273
+ timeZone: "Europe/Vienna",
274
+ storefrontCountryCode: "de",
275
+ })
276
+ ```
277
+
278
+ A shop that has to adjust the result (a test harness, for example) builds the
279
+ plain input, changes it and calls `defineConfig` itself:
280
+
281
+ ```ts
282
+ import { defineConfig } from "@medusajs/framework/utils"
283
+ import { buildShopConfig } from "@hartl-services/medusa-base/shop-config"
284
+
285
+ const config = buildShopConfig(options)
286
+ config.plugins = config.plugins.filter(
287
+ (plugin) => plugin.resolve !== "@hartl-services/medusa-batch-management",
288
+ )
289
+ module.exports = defineConfig(config)
290
+ ```
291
+
292
+ ### API
293
+
294
+ - `defineShopConfig(options, environment = process.env)` returns
295
+ `defineConfig(buildShopConfig(options, environment))`.
296
+ - `buildShopConfig(options, environment = process.env)` returns the plain
297
+ config input (`admin`, `logger`, `projectConfig`, `modules`, `plugins`)
298
+ before `defineConfig`. `modules` and `plugins` are ordinary arrays.
299
+ - Types: `ShopConfigOptions`, `ShopConfigInput`, `ShopPlugin`,
300
+ `ShopModule`.
301
+
302
+ Both functions validate the options first and throw a `TypeError` naming the
303
+ option (`Shop option "timeZone" must be ...`), so a misconfigured shop
304
+ fails at boot. They then load the `.env` file (unless disabled) and read and
305
+ validate the environment; a missing variable fails with the variable's name.
306
+
307
+ ### Options
308
+
309
+ | Option | Meaning |
310
+ | --- | --- |
311
+ | `displayName` | Shop name in the default e-mail templates (a shop's initial seed may use it as store name). Company name, address and UID are no options: documents and tax compliance read them only from the Shop-Stammdaten (Admin, "Hartl Services Plugins"). |
312
+ | `orderNotificationEmail` | Receives order notification e-mails; must look like an e-mail address. |
313
+ | `timeZone` | IANA time zone (validated with `Intl`). Chooses the annual document-number period, the printed dates on documents and the affiliate consignment reminder time zone. |
314
+ | `storefrontCountryCode` | 2-letter lowercase country code of the storefront route. Links: `<STOREFRONT_URL>/<cc>/partnerportal` (affiliate portal), `<STOREFRONT_URL>/<cc>/reset-password` and `<STOREFRONT_URL>/<cc>/verify-account` (the e-mail plugin appends `#token=`), referral base `<STOREFRONT_URL>/<cc>`. The Admin invite link is `<MEDUSA_BACKEND_URL>/app/invite?token=`. |
315
+ | `documentHtmlFactory` | Optional `DocumentHtmlFactory` that replaces the default document markup, typically `createDefaultDocumentHtmlFactory({ timeZone, theme })` from `/config` with the shop's CSS and watermark (see "Layout, logo, payment terms and shop theme"). Must be a function. |
316
+ | `loadEnvFile` | Default `true`: calls `loadEnv(NODE_ENV \|\| "development", process.cwd())`. Pass `false` when the environment is already complete (tests, isolated child processes). |
317
+
318
+ ### Environment variables
319
+
320
+ Required at runtime (during `medusa build`, with `MEDUSA_BUILD_PHASE=true`, the
321
+ plugins' build placeholders stand in for them):
322
+
323
+ - Medusa core: `DATABASE_URL`, `STORE_CORS`, `ADMIN_CORS`, `AUTH_CORS`,
324
+ `JWT_SECRET`, `COOKIE_SECRET`, `MEDUSA_BACKEND_URL`, `STOREFRONT_URL` (plain
325
+ HTTP origins are accepted only in development and test).
326
+ - Redis: `REDIS_URL` (optional in development, where Medusa's local event bus
327
+ is used; required elsewhere, `rediss://` with authentication in production
328
+ unless `REDIS_ALLOW_INSECURE_PRIVATE_NETWORK=true`). Never used during a
329
+ build.
330
+ - SMTP: `SMTP_HOST`, `SMTP_PORT`, `SMTP_SECURE`, `SMTP_USER`, `SMTP_PASSWORD`,
331
+ `SMTP_FROM`, `SMTP_REQUIRE_TLS` (outside development and test either
332
+ `SMTP_SECURE` or `SMTP_REQUIRE_TLS` must be `true`).
333
+ - Stripe: `STRIPE_API_KEY` (`sk_...`), `STRIPE_WEBHOOK_SECRET` (`whsec_...`).
334
+ - S3: `S3_CACHE_STORAGE_BUCKET`, `S3_CACHE_STORAGE_REGION`.
335
+ - Documents: `DOCUMENT_PDF_EXECUTABLE`.
336
+
337
+ Optional: `S3_CACHE_STORAGE_ENDPOINT`, `S3_CACHE_STORAGE_FORCE_PATH_STYLE`,
338
+ `S3_CACHE_STORAGE_PREFIX`, `S3_CACHE_STORAGE_ACCESS_KEY_ID`,
339
+ `S3_CACHE_STORAGE_SECRET_ACCESS_KEY`, `S3_CACHE_STORAGE_SESSION_TOKEN` (omit
340
+ the keys to use the AWS credential chain), `S3_CACHE_STORAGE_CACHE_DIR`
341
+ (default `./.medusa-s3-cache`), `S3_CACHE_STORAGE_TTL_MS`,
342
+ `S3_CACHE_STORAGE_CLEANUP_INTERVAL_MS`,
343
+ `S3_CACHE_STORAGE_EXPIRATION_STRATEGY`,
344
+ `FILE_GATEWAY_PRIVATE_DOWNLOAD_DURATION_SECONDS`, and `SENTRY_DSN` /
345
+ `SENTRY_ENVIRONMENT` for error tracking (see the base's
346
+ `/instrumentation`).
347
+
348
+ ### Fixed configuration
349
+
350
+ Not configurable on purpose: the plugin order (base first), the Redis event
351
+ bus job policy (3 attempts, exponential backoff from 1 s), S3 cache defaults
352
+ (15 min cleanup, 1 h TTL, absolute expiration), Stripe immediate capture with
353
+ automatic payment methods, the 24 h session lifetime, Admin path `/app`,
354
+ consignment reminders due on day 10, the Documents module always being part of the base, and
355
+ `http.authVerificationsPerActor.customer` (a getter that follows the E-Mail `customer_verification` toggle, see
356
+ "Kontobestätigung per E-Mail"), `http.authMethodsPerActor` (customers: a getter that follows the `customer_auth` switches; Admin users: `["emailpass"]`) and the auth module entry
357
+ (providers `emailpass` and `email-otp`, the latter with `hmacSecret` = `JWT_SECRET`, the same value as `http.jwtSecret`; see
358
+ "Kunden-Login").
359
+
360
+ ## Plugin platform
150
361
 
151
- ## HTTP endpoints
362
+ The mechanics every plugin builds on: the HTTP surface, the feature
363
+ declaration, runtime settings, actions, the shop master data, the Admin and
364
+ server helpers, the toggle semantics and the complete-cart hooks.
365
+
366
+ ### HTTP endpoints
152
367
 
153
368
  | Method | Path | Auth | Response |
154
369
  |---|---|---|---|
@@ -193,7 +408,7 @@ const { plugins } = await sdk.client.fetch<StorePluginFeaturesListResponse>(
193
408
  )
194
409
  ```
195
410
 
196
- ## Declaring features in a plugin
411
+ ### Declaring features in a plugin
197
412
 
198
413
  A plugin depends on `@hartl-services/medusa-base` (peer dependency), declares
199
414
  its features once and exposes the declaration as `pluginFeatures` on its
@@ -233,7 +448,7 @@ base's own shop master data): the Admin shows no plugin switch, `enabled` is
233
448
  always `true` whatever is stored, and `POST /admin/plugin-features` with
234
449
  `enabled: false` is 400. It cannot be combined with `default_enabled: false`.
235
450
 
236
- ### Presentation fields for the Admin page
451
+ #### Presentation fields for the Admin page
237
452
 
238
453
  All optional; the Admin response always carries them (`null`, `[]` or
239
454
  `"full"` when not declared). Texts are non-empty trimmed strings;
@@ -276,7 +491,7 @@ All optional; the Admin response always carries them (`null`, `[]` or
276
491
  share a two-column row; not for `boolean`) and `section` (sub-heading inside
277
492
  the group, shared by consecutive settings with the same value).
278
493
 
279
- ### Gating routes
494
+ #### Gating routes
280
495
 
281
496
  Put gate entries at the start of the plugin's `routes` array. An entry without
282
497
  `methods` is a prefix match and runs before every method-specific middleware:
@@ -303,7 +518,7 @@ export default defineMiddlewares({
303
518
  A disabled plugin or feature answers 404. With several keys, all must be
304
519
  enabled. The toggles are read once per request.
305
520
 
306
- ### Gating subscribers and jobs
521
+ #### Gating subscribers and jobs
307
522
 
308
523
  Return early:
309
524
 
@@ -324,7 +539,7 @@ To check another plugin without importing it, use its plugin key:
324
539
  the descriptor among the registered modules; an unknown or uninstalled plugin
325
540
  key (and an undeclared feature key) is `false`.
326
541
 
327
- ### Toggle-change event
542
+ #### Toggle-change event
328
543
 
329
544
  After a successful `POST /admin/plugin-features` the base emits
330
545
  `PLUGIN_FEATURES_UPDATED_EVENT` (`plugin-features.updated.v1`) through the
@@ -356,7 +571,7 @@ export const config: SubscriberConfig = { event: PLUGIN_FEATURES_UPDATED_EVENT }
356
571
  Events can arrive late or twice; re-read the current toggles instead of
357
572
  trusting the payload for anything that writes.
358
573
 
359
- ### Never on the cart path
574
+ #### Never on the cart path
360
575
 
361
576
  Code on the cart and checkout hot path (cart middlewares, cart workflow hooks,
362
577
  `orderCreated` handling, cart subscribers) must never read toggles. It stays
@@ -366,7 +581,7 @@ existing data. Login is not the cart path: the E-Mail plugin's
366
581
  `customer_verification` toggle does switch customer logins, through an
367
582
  in-memory copy of the toggle (see "Kontobestätigung per E-Mail").
368
583
 
369
- ## Runtime settings
584
+ ### Runtime settings
370
585
 
371
586
  Configuration a plugin reads at RUN time (for example an external system's
372
587
  URL and API key) is declared in the descriptor and edited on the plugin's
@@ -460,7 +675,7 @@ class DemoModuleService extends MedusaService({ /* models */ }) {
460
675
  (`plugin-settings.updated.v1`) with `{ plugin_key, version, changed_keys }`
461
676
  (no values). Direct service calls emit nothing.
462
677
 
463
- ### Secrets
678
+ #### Secrets
464
679
 
465
680
  Secrets are stored AES-256-GCM encrypted (`{ enc: "<base64 iv|tag|ciphertext>" }`
466
681
  in `plugin_setting_state.values`) with a key derived by HKDF-SHA256 from the
@@ -475,7 +690,7 @@ applies in the meantime. Secrets saved by an unpublished development build
475
690
  before the key binding existed count as "nicht gesetzt" the same way and must
476
691
  be entered again.
477
692
 
478
- ## Actions
693
+ ### Actions
479
694
 
480
695
  A plugin declares actions as registered workflow ids; the base runs them from
481
696
  the plugin's Admin page:
@@ -529,7 +744,7 @@ Admin user id, `run_id` the recorded run).
529
744
  workflow is not registered is 500 (`UNEXPECTED_STATE` naming the workflow,
530
745
  no run is recorded).
531
746
 
532
- ### Reporting progress
747
+ #### Reporting progress
533
748
 
534
749
  A long-running action reports progress from its workflow; the Admin shows it
535
750
  as a bar ("current / total") with the label:
@@ -558,7 +773,7 @@ characters. Without a `run_id` (the same workflow run from a script) the
558
773
  helper does nothing; invalid progress, an unknown or finished run and a
559
774
  failed write are logged as a warning and dropped, and never fail the action.
560
775
 
561
- ## Shop-Stammdaten
776
+ ### Shop-Stammdaten
562
777
 
563
778
  The base declares its own descriptor on its module service
564
779
  (`plugin_key: "shop"`, label "Shop-Stammdaten", `switchable: false`, no
@@ -626,7 +841,7 @@ Documents plugin into the appearance: the Documents logo, else the E-Mail
626
841
  logo; a `logo` stored in the Shop-Stammdaten by an earlier release is kept
627
842
  only when neither plugin had one.
628
843
 
629
- ## Admin pages
844
+ ### Admin pages
630
845
 
631
846
  - `/settings/hartl-services` (sidebar entry "Hartl Services Plugins") is the
632
847
  hub: a banner for the Shop-Stammdaten (icon, summary, "Nicht konfiguriert"
@@ -655,7 +870,7 @@ only when neither plugin had one.
655
870
  rejects a path outside `/settings/hartl-services/` or under
656
871
  `/settings/hartl-services/plugin/`.
657
872
 
658
- ### Admin components for plugin pages
873
+ #### Admin components for plugin pages
659
874
 
660
875
  `@hartl-services/medusa-base/admin-components` (ESM for the Admin's vite
661
876
  build, with types) exports:
@@ -754,7 +969,7 @@ The consuming plugin lists `@hartl-services/medusa-base` in its
754
969
  external and the shop's Admin resolves one shared copy. The components use
755
970
  the host Admin's React, React Query, React Router and `@medusajs/ui`.
756
971
 
757
- #### Rich-text editor
972
+ ##### Rich-text editor
758
973
 
759
974
  `useRichTextEditor(config, initialContent, deps)` returns the TipTap
760
975
  `Editor | null` (`null` until it exists), `<RichTextEditor editor
@@ -847,7 +1062,7 @@ policy (`link.isAllowedHref`) are read on every use and always see the latest
847
1062
  render, so they need no stable reference. Pass `[]` for an editor that lives as
848
1063
  long as its component.
849
1064
 
850
- ### Server helpers
1065
+ #### Server helpers
851
1066
 
852
1067
  `@hartl-services/medusa-base/lib` (server and isomorphic code; a separate
853
1068
  subpath because unit tests mock `@hartl-services/medusa-base` wholesale)
@@ -893,13 +1108,7 @@ The main entry (`@hartl-services/medusa-base`) additionally exports, next to
893
1108
  | `isOwnPluginEvent(data, pluginKey)` | `true` when the event payload's `plugin_key` is `pluginKey`; never throws. For subscribers of `plugin-features.updated.v1` and `plugin-settings.updated.v1` |
894
1109
  | `isBasePluginRegistered(container)` / `resolveBaseModule(container)` | Whether the base module is registered (boot-time check) / its service, or `UNEXPECTED_STATE` |
895
1110
 
896
- `/admin-components` is built (after `medusa plugin:build`) into
897
- `.medusa/server/src/admin/admin-components`, not into `dist`: the host Admin's
898
- Tailwind scans only `.medusa/server/src/admin/**` of a plugin, so the classes of
899
- the shared components are generated there. Do not move the outDir of
900
- `tsconfig.admin-components.json`.
901
-
902
- ## Semantics
1111
+ ### Semantics
903
1112
 
904
1113
  - No stored row: every value is its declared default, `version` is 0.
905
1114
  - `effective(feature) = plugin enabled && configured(feature) && effective(parent)`.
@@ -911,7 +1120,7 @@ the shared components are generated there. Do not move the outDir of
911
1120
  value updates exactly that version. A concurrent change answers 409; the
912
1121
  Admin page reloads and shows a notice.
913
1122
 
914
- ## Complete-cart hook composition
1123
+ ### Complete-cart hook composition
915
1124
 
916
1125
  **Why.** Medusa 2.19.0 allows exactly one handler per workflow hook; a second
917
1126
  registration throws "Cannot define multiple hook handlers" at boot. Several
@@ -996,42 +1205,12 @@ Storefront calls it right before confirming a payment. Plugins that guard
996
1205
  `/store/carts/:id/complete` with a middleware register it for this route too
997
1206
  (the affiliate plugin's registered-email gate does).
998
1207
 
1208
+ ## Built-in features
999
1209
 
1000
- ## Sentry logger wrapper
1001
-
1002
- `@hartl-services/medusa-base/sentry-logger` exports `withSentryErrorReporting(logger)`,
1003
- formerly published as `@hartl-services/medusa-sentry-logger`. Medusa's HTTP
1004
- error handler catches thrown route and workflow errors and only logs them, so
1005
- Sentry's default Node integrations never see them; wrapping the logger reports
1006
- every `logger.error(...)` and every `Error` passed to `logger.warn(...)` (at
1007
- level `warning`). It is a plain library import for `medusa-config.ts`, not a
1008
- Medusa plugin feature, and needs `@sentry/node` (an optional peer dependency):
1009
-
1010
- ```ts
1011
- import { logger } from "@medusajs/framework/logger";
1012
- import { withSentryErrorReporting } from "@hartl-services/medusa-base/sentry-logger";
1013
-
1014
- module.exports = defineConfig({
1015
- logger: withSentryErrorReporting(logger),
1016
- // ...
1017
- });
1018
- ```
1019
-
1020
- Initialize Sentry in `instrumentation.ts` as usual; without a `SENTRY_DSN` the
1021
- wrapper is inert.
1022
-
1023
- ## Root redirect
1024
-
1025
- Medusa serves nothing at `/`, so a deployed domain answers it with a 404. The
1026
- base registers `GET /` and answers with a `302` to the shop's `admin.path`
1027
- (read from the config module, fallback `/app`), so opening the bare domain
1028
- lands on the admin dashboard in every shop and in local development without a
1029
- reverse-proxy rule. The redirect is temporary on purpose: browsers cache a
1030
- `301` on the bare domain essentially forever, which would be painful to undo
1031
- should `/` ever get real content. A shop that needs its own `/` defines
1032
- `src/api/route.ts` itself.
1210
+ The modules and providers the base ships besides the platform, each with its
1211
+ own configuration, toggles and Admin surface.
1033
1212
 
1034
- ## Kunden-Login
1213
+ ### Kunden-Login
1035
1214
 
1036
1215
  Module `customer_auth` (plugin key `customer_auth`, label "Kunden-Login")
1037
1216
  decides which login methods customers may use. The plugin cannot be switched
@@ -1228,231 +1407,55 @@ Storefront contract: DTOs, schemas and paths come from the Contracts package
1228
1407
  read `GET /store/plugin-features/customer_auth` and render only the enabled
1229
1408
  forms.
1230
1409
 
1231
- ## Password policy
1410
+ ### E-Mail
1232
1411
 
1233
- Medusa's `emailpass` provider accepts any non-empty password. The base runs a
1234
- middleware on `POST /auth/:actor_type/emailpass/register` (customer sign-up,
1235
- Admin invite) and `POST /auth/:actor_type/emailpass/update` (password reset).
1236
- It is registered on `/auth/:actor_type/:auth_provider/:action` without
1237
- `methods` and decides on the decoded, lowercased params, because Express
1238
- matches literal path segments against the raw path: `/auth/customer/email%70ass/register`
1239
- and `/auth/Customer/emailpass/register` reach Medusa's handler too, and
1240
- `Customer` counts as the customer actor here.
1241
- A password that breaks the rules of the actor is answered with
1242
- `400 { type: "invalid_data", message }`; the one German message lists every
1243
- unmet rule, e.g. `Das Passwort erfüllt nicht alle Regeln: mindestens 12
1244
- Zeichen, eine Ziffer.` The check runs before Medusa's reset-token check, so a
1245
- rejected password leaves the single-use reset link valid for a second attempt.
1412
+ The base owns transactional e-mail for the shop: logical email messages,
1413
+ attempts, rendering, the Medusa Notification integration, the SMTP and
1414
+ Microsoft 365 transports, retries and e-mail-specific Admin operations. It is
1415
+ the `email` module, its notification provider (`providers/smtp`, id `smtp`),
1416
+ its subscribers, jobs and
1417
+ workflows, and the Admin page `/settings/hartl-services/email`.
1246
1418
 
1247
- **Admin users** (every actor type except `customer`) always need at least 15
1248
- characters, counted as Unicode code points, and no composition rules. This
1249
- follows NIST SP 800-63B Rev. 4 (the US guideline for digital identity) for a
1250
- password that is the only factor.
1419
+ #### Configuration
1251
1420
 
1252
- **Customers** follow the settings of the "Kunden-Login" plugin (group
1253
- "Passwort-Regeln", Settings > "Hartl Services Plugins"). They apply only while
1254
- `email_password` is on and take effect at once, without a cache:
1421
+ The notification provider is a `modules` entry built by
1422
+ `createEmailNotificationModuleConfig` (from
1423
+ `@hartl-services/medusa-base/config`); which service it sends through and the
1424
+ credentials are set in the Admin (see "Versand"). The branding and rendering
1425
+ configuration goes into the `email` namespace of the base options:
1255
1426
 
1256
- | Key | Label | Default | Meaning |
1257
- | ----------------------------- | ------------------------------------ | ------- | ------- |
1258
- | `password_min_length` | Mindestlänge | 15 | 8 to 128 characters, whole numbers only (`integer`); NIST SP 800-63B Rev. 4 recommends at least 15 when the password is the only factor |
1259
- | `password_require_mixed_case` | Groß- und Kleinbuchstaben verlangen | off | at least one uppercase and one lowercase letter |
1260
- | `password_require_digit` | Ziffer verlangen | off | at least one digit |
1261
- | `password_require_symbol` | Sonderzeichen verlangen | off | at least one punctuation mark or symbol (Unicode category P or S; whitespace, combining marks and invisible characters do not count) |
1427
+ ```ts
1428
+ import { createEmailNotificationModuleConfig } from "@hartl-services/medusa-base/config";
1262
1429
 
1263
- NIST SP 800-63B Rev. 4 advises against composition rules, because they lead to
1264
- predictable passwords such as "Passwort1!"; a longer password protects better.
1265
- The Admin texts say so. A missing or invalid stored value falls back to the
1266
- default of that rule, and if the settings cannot be read at all the default
1267
- policy (15 characters, no composition rules) applies and a warning is logged,
1268
- so a registration is never blocked by a settings outage.
1269
-
1270
- **Route.** `GET /store/customer-auth/password-policy` (publishable key only)
1271
- answers `{ password_policy: { min_length, require_mixed_case, require_digit,
1272
- require_symbol } }`, the effective customer rules, and 404 while
1273
- `email_password` is off. The storefront shows them in its forms and checks
1274
- them with the same functions as the backend. The types, the path
1275
- (`customerAuthStorePaths.get.passwordPolicy`) and the rules
1276
- (`evaluatePasswordPolicy`, `isPasswordAllowed`, `DEFAULT_CUSTOMER_PASSWORD_POLICY`,
1277
- `ADMIN_PASSWORD_POLICY` in `schemas/password-policy`) come from the Contracts
1278
- package.
1279
-
1280
- **Admin passwords and the shared identity.** An `emailpass` identity is shared
1281
- by all actor types: someone invited as an Admin could register through
1282
- `/auth/customer/emailpass/register` with the (weaker) customer rules, log in at
1283
- `/auth/user/emailpass` and accept the invite with that token, which would bind
1284
- the weak password to an Admin. So the Admin login (`POST` and `GET`
1285
- `/auth/user/emailpass`, decoded and lowercased like above) checks the body
1286
- password against the Admin rules and answers the same `400` when the identity
1287
- has no Admin yet (`app_metadata.user_id` unset). The login of an identity that
1288
- already belongs to an Admin is never checked. Remaining limit, deliberately not
1289
- closed: a customer could register through the Admin path with 15 characters and
1290
- no composition rules although the shop asks for more; that only weakens their
1291
- own password.
1292
-
1293
- Login of a customer is not checked, so existing passwords keep working until their next
1294
- reset, whatever the rules are. Accounts created without HTTP (`medusa user`,
1295
- seed scripts calling the Auth module) are not checked either.
1296
-
1297
- ## Cart-promotion link repair
1298
-
1299
- Medusa's `deletePromotionsWorkflow` only soft-deletes a promotion and leaves
1300
- its `cart_promotion` link rows behind, so every cart that had it applied
1301
- reads back a `null` entry inside `cart.promotions`. The base removes those
1302
- links itself:
1303
-
1304
- - The `deletePromotionsWorkflow.hooks.promotionsDeleted` hook
1305
- (`workflows/hooks/dismiss-deleted-promotion-cart-links`) dismisses the
1306
- links of the deleted promotions and restores them if a later step fails.
1307
- - The migration script `dismiss-orphaned-cart-promotion-links` runs once in
1308
- `medusa db:migrate` and removes the links that already dangle from earlier
1309
- deletions. Medusa tracks migration scripts by file name in
1310
- `script_migrations`, so the name must not change.
1311
-
1312
- Nothing to configure; `medusa db:migrate` after installing or upgrading to
1313
- 0.4.1 is enough.
1314
-
1315
- ## Draft orders and 0 € orders
1316
-
1317
- Draft orders (Medusa's `@medusajs/draft-order` plugin, loaded by default) are
1318
- the shop's way to enter an order in the Admin: sample shipments, orders taken
1319
- by phone, replacements. Item prices and the shipping amount are set directly
1320
- in the draft; converting it never involves a payment provider.
1321
-
1322
- - **Customer order confirmation off by default.** The
1323
- `createOrderWorkflow.hooks.orderCreated` hook
1324
- (`workflows/hooks/draft-order-confirmation-default`) sets Medusa's
1325
- `no_notification` on every new draft, also when an API caller sent
1326
- `no_notification_order: false`. The Admin switches it per draft in the
1327
- **Bestellbestätigung** widget on the draft's detail page
1328
- (`GET`/`POST /admin/draft-orders/:id/order-confirmation`, see the E-Mail
1329
- "Admin API"). On conversion the `order-placed` subscriber records the
1330
- customer confirmation as `suppressed` (`notifications_disabled`); the copy
1331
- to the shop's order address is still sent. The shipment mail follows the
1332
- Admin's "notify customer" choice per shipment, not this switch.
1333
- - **0 € orders count as paid.** Medusa derives an order's payment status from
1334
- its payment collections only, so a converted 0 € draft would stay
1335
- "Nicht bezahlt". The subscriber `order-zero-total-paid` runs
1336
- `order-mark-zero-total-paid` on `order.placed`: an order with a total of 0
1337
- and no payment collection gets a completed 0 € payment collection and reads
1338
- "Bezahlt" (`payment_status: captured`). Orders that already carry a
1339
- collection, i.e. every checkout order, are left alone. In SQL, a paid
1340
- order is one whose linked `payment_collection` (via
1341
- `order_payment_collection`) has `status = 'completed'`.
1342
-
1343
- Orders placed before installing this version keep their state.
1344
-
1345
- ## Config helpers
1346
-
1347
- `@hartl-services/medusa-base/config` holds the generic environment helpers a
1348
- shop's `medusa-config.ts` needs, so every shop fails at startup with the name
1349
- of the missing variable instead of a runtime crash. The environment helpers
1350
- have no dependency on Medusa; the subpath does not load `@sentry/node`.
1351
-
1352
- | Export | Purpose |
1353
- |---|---|
1354
- | `ConfigurationPhase` | `"build" \| "runtime"` |
1355
- | `MEDUSA_BUILD_PLACEHOLDERS` | Placeholders for `ADMIN_CORS`, `AUTH_CORS`, `COOKIE_SECRET`, `DATABASE_URL`, `JWT_SECRET`, `MEDUSA_BACKEND_URL`, `STOREFRONT_URL`, `STORE_CORS`, `STRIPE_API_KEY`, `STRIPE_WEBHOOK_SECRET` |
1356
- | `requireEnvironment(environment, names, phase, buildPlaceholders)` | Returns every named value (trimmed) or throws one error listing all missing names; in the build phase `buildPlaceholders` fill in for deployment values |
1357
- | `optionalEnvironment`, `integerEnvironment`, `booleanEnvironment` | Trimmed optional string, range-checked integer, strict `"true"`/`"false"` boolean |
1358
- | `allowsInsecureLocalServices(environment, phase)` | `true` for build, development and test |
1359
- | `resolvePublicOrigin(value, name, allowHttp)`, `shopUrl(origin, path)` | Validate a bare HTTP(S) origin; build an absolute URL that cannot leave the origin |
1360
- | `resolveStripe(values)` | Checks the `sk_` key and the `whsec_` webhook secret |
1361
- | `resolveRedisEventBus(environment, phase)` | `{ url } \| undefined`: optional in development, required at runtime elsewhere, never used in a build, authenticated `rediss://` in production |
1362
- | `createEmailNotificationModuleConfig`, `defineShopEmailConfiguration` (deprecated: `createSmtpNotificationModuleConfig`, `resolveSmtp`, `SMTP_ENVIRONMENT_VARIABLES`, `SMTP_BUILD_PLACEHOLDERS`) | Mail notification provider and E-Mail configuration (see "E-Mail") |
1363
- | `createS3CacheStorageConfig`, `createS3CacheFileModuleConfig`, `resolveS3Credentials`, `resolveExpirationStrategy`, `S3_BUILD_PLACEHOLDERS` | S3 file storage (see "Dateispeicher (S3)") |
1364
- | `createDefaultDocumentHtmlFactory`, `DOCUMENTS_BUILD_PLACEHOLDERS` | Documents markup and build placeholder (see "Dokumente") |
1365
-
1366
- `medusa build` loads the configuration without deployment secrets. Each
1367
- plugin's own placeholders (SMTP, S3, PDF executable, ...) live with that
1368
- plugin; the shop merges them:
1369
-
1370
- ```ts
1371
- // medusa-config.ts
1372
- import {
1373
- MEDUSA_BUILD_PLACEHOLDERS,
1374
- requireEnvironment,
1375
- resolveRedisEventBus,
1376
- type ConfigurationPhase,
1377
- } from "@hartl-services/medusa-base/config"
1378
-
1379
- const phase: ConfigurationPhase =
1380
- process.env.MEDUSA_BUILD_PHASE === "true" ? "build" : "runtime"
1381
- const env = requireEnvironment(
1382
- process.env,
1383
- ["DATABASE_URL", "JWT_SECRET", "COOKIE_SECRET"],
1384
- phase,
1385
- { ...MEDUSA_BUILD_PLACEHOLDERS /* , ...other plugins' placeholders */ },
1386
- )
1387
- const redis = resolveRedisEventBus(process.env, phase)
1388
- ```
1389
-
1390
- ## Sentry instrumentation
1391
-
1392
- `@hartl-services/medusa-base/instrumentation` exports `register()`, the hook
1393
- Medusa's start command calls from the shop's `instrumentation.ts`. It runs
1394
- `Sentry.init` with `SENTRY_DSN`, `SENTRY_ENVIRONMENT` (fallback `NODE_ENV`)
1395
- and `tracesSampleRate: 0`; without a `SENTRY_DSN` Sentry stays inert. The
1396
- shop's `instrumentation.ts` is one line:
1397
-
1398
- ```ts
1399
- export { register } from "@hartl-services/medusa-base/instrumentation"
1400
- ```
1401
-
1402
- `@sentry/node` stays an optional peer dependency of the base: a shop that uses
1403
- this export (or `sentry-logger`) lists `@sentry/node` in its own
1404
- `dependencies`.
1405
-
1406
- ## E-Mail
1407
-
1408
- The base owns transactional e-mail for the shop: logical email messages,
1409
- attempts, rendering, the Medusa Notification integration, the SMTP and
1410
- Microsoft 365 transports, retries and e-mail-specific Admin operations. It is
1411
- the `email` module, its notification provider (`providers/smtp`, id `smtp`),
1412
- its subscribers, jobs and
1413
- workflows, and the Admin page `/settings/hartl-services/email`. There is no
1414
- separate e-mail package since 1.0.0.
1415
-
1416
- ### Configuration
1417
-
1418
- The notification provider is a `modules` entry built by
1419
- `createEmailNotificationModuleConfig` (from
1420
- `@hartl-services/medusa-base/config`); which service it sends through and the
1421
- credentials are set in the Admin (see "Versand"). The branding and rendering
1422
- configuration goes into the `email` namespace of the base options:
1423
-
1424
- ```ts
1425
- import { createEmailNotificationModuleConfig } from "@hartl-services/medusa-base/config";
1426
-
1427
- export default defineConfig({
1428
- modules: [
1429
- createEmailNotificationModuleConfig({
1430
- // Permits "Verschlüsselung: Keine" (a local SMTP server); never in production.
1431
- allowInsecure:
1432
- process.env.NODE_ENV === "development" || process.env.NODE_ENV === "test",
1433
- }),
1434
- ],
1435
- plugins: [
1436
- {
1437
- resolve: "@hartl-services/medusa-base",
1438
- options: {
1439
- email: {
1440
- jwtSecret: process.env.JWT_SECRET!,
1441
- shop: {
1442
- displayName: "Mein Shop",
1443
- affiliatePortalUrl: "https://shop.example.com/de/partner",
1444
- inviteUrl: ({ token }) =>
1445
- `https://shop.example.com/app/invite?token=${token}`,
1446
- orderNotificationEmail: "orders@shop.example.com",
1447
- passwordResetUrl: ({ locale }) =>
1448
- `https://shop.example.com/${locale ?? "de"}/passwort-zuruecksetzen`,
1449
- }, // a ShopEmailConfiguration, see `ShopEmailConfiguration` in `@hartl-services/medusa-base/types`
1450
- },
1451
- },
1452
- },
1453
- ],
1454
- });
1455
- ```
1430
+ export default defineConfig({
1431
+ modules: [
1432
+ createEmailNotificationModuleConfig({
1433
+ // Permits "Verschlüsselung: Keine" (a local SMTP server); never in production.
1434
+ allowInsecure:
1435
+ process.env.NODE_ENV === "development" || process.env.NODE_ENV === "test",
1436
+ }),
1437
+ ],
1438
+ plugins: [
1439
+ {
1440
+ resolve: "@hartl-services/medusa-base",
1441
+ options: {
1442
+ email: {
1443
+ jwtSecret: process.env.JWT_SECRET!,
1444
+ shop: {
1445
+ displayName: "Mein Shop",
1446
+ affiliatePortalUrl: "https://shop.example.com/de/partner",
1447
+ inviteUrl: ({ token }) =>
1448
+ `https://shop.example.com/app/invite?token=${token}`,
1449
+ orderNotificationEmail: "orders@shop.example.com",
1450
+ passwordResetUrl: ({ locale }) =>
1451
+ `https://shop.example.com/${locale ?? "de"}/passwort-zuruecksetzen`,
1452
+ }, // a ShopEmailConfiguration, see `ShopEmailConfiguration` in `@hartl-services/medusa-base/types`
1453
+ },
1454
+ },
1455
+ },
1456
+ ],
1457
+ });
1458
+ ```
1456
1459
 
1457
1460
  Options of the `email` namespace:
1458
1461
 
@@ -1482,8 +1485,7 @@ fact only while the Documents `invoices` feature is on when the message is
1482
1485
  created (with `invoices` off, or the Documents plugin switched off, Documents
1483
1486
  publishes no fact, so the mail is sent without a document). Refund e-mails
1484
1487
  come only from the Documents refund fact, so there are none while `invoices`
1485
- is off. The former `documentsEnabled` option no longer exists: Documents is
1486
- always part of the base.
1488
+ is off.
1487
1489
 
1488
1490
  The event contracts the module consumes for shipment, cancellation, refund and
1489
1491
  affiliate-reminder and partner-application e-mails come from
@@ -1493,11 +1495,7 @@ affiliate-reminder and partner-application e-mails come from
1493
1495
  The provider reads no environment variables (since 2.9.0; the former
1494
1496
  `SMTP_*` variables are ignored).
1495
1497
 
1496
- Set `REDIS_URL` to use `@medusajs/event-bus-redis` instead of Medusa's local
1497
- Event Bus; production requires an authenticated `rediss://` URL. See "Event
1498
- Bus" below for retry/backoff behavior.
1499
-
1500
- ### Versand
1498
+ #### Versand
1501
1499
 
1502
1500
  The page E-Mail → "Versand" (`/settings/hartl-services/email/delivery`, linked
1503
1501
  under "Verwaltung") edits the plugin's settings. It stays reachable while the
@@ -1557,7 +1555,7 @@ to the e-mail of the Shop-Stammdaten, once per client id and expiry date (a
1557
1555
  log warning when the Shop-Stammdaten have no e-mail). A new secret with a new
1558
1556
  date warns again.
1559
1557
 
1560
- ### Feature toggles
1558
+ #### Feature toggles
1561
1559
 
1562
1560
  The E-Mail module declares its toggles to `@hartl-services/medusa-base` under the
1563
1561
  plugin key `email`. An Admin switches them under Settings → "Hartl Services
@@ -1593,7 +1591,7 @@ rows are dimmed and their switches disabled then. All E-Mail sub-pages
1593
1591
  switch and show `PluginFeatureDisabledNotice` while it is off (the backend
1594
1592
  answers `404` for `/admin/email/*` then).
1595
1593
 
1596
- ### Kontobestätigung per E-Mail
1594
+ #### Kontobestätigung per E-Mail
1597
1595
 
1598
1596
  Feature `customer_verification` (child of `account_mails`, default **off**)
1599
1597
  makes customers confirm their e-mail address through a link before their
@@ -1672,7 +1670,8 @@ loads (`.medusa/server`) are compiled separately. Only the preset
1672
1670
  hand-written config must set `authVerificationsPerActor` itself (a fixed list,
1673
1671
  changed by a restart) for the toggle to have any effect on logins.
1674
1672
 
1675
- Login is not on the cart path, so the cart rule above is not affected.
1673
+ Login is not on the cart path, so the rule in "Never on the cart path" is
1674
+ not affected.
1676
1675
 
1677
1676
  Things to know when switching it on:
1678
1677
 
@@ -1698,7 +1697,7 @@ Storefront contract (DTOs, schema and path from the Contracts package,
1698
1697
  - Serve `/<cc>/verify-account`, read `token` from `location.hash`, confirm it
1699
1698
  and then send the customer to the login.
1700
1699
 
1701
- ### Ownership
1700
+ #### Ownership
1702
1701
 
1703
1702
  - Documents owns document issuance and publishes immutable document facts.
1704
1703
  - Affiliate owns reminder eligibility and publishes reminder and application
@@ -1714,7 +1713,7 @@ surfaces for the backend. The Documents and Affiliate event contracts live in
1714
1713
  the Contracts package (`events/documents`, `events/affiliate`). The base exports no DML models, services,
1715
1714
  workflows, migrations, or generated output as a consumer contract.
1716
1715
 
1717
- ### Event Bus
1716
+ #### Event Bus
1718
1717
 
1719
1718
  The backend owns Event Bus selection. Development without `REDIS_URL` leaves
1720
1719
  Medusa's local Event Bus active. Configuring `REDIS_URL` registers
@@ -1728,7 +1727,7 @@ for at most one hour and 1,000 entries. There is no custom Event Bus wrapper,
1728
1727
  startup capability probe, or purge job. Events are at-least-once facts and can
1729
1728
  be replayed, delayed, or delivered out of order.
1730
1729
 
1731
- ### Event Contracts
1730
+ #### Event Contracts
1732
1731
 
1733
1732
  All payload objects are strict. Unless noted otherwise, every string shown
1734
1733
  below is trimmed and non-empty. Native event names and shapes are fixed to the
@@ -1854,7 +1853,7 @@ concurrent request never sends a second mail) addressed to
1854
1853
  `affiliate-consignment:<affiliate_id>:<period_start>`. `locale`, when present,
1855
1854
  has at most 35 characters. Email runtime-validates every contract before use.
1856
1855
 
1857
- ### Invariants
1856
+ #### Invariants
1858
1857
 
1859
1858
  - The accepted kinds are exactly `order.received`, `order.received.admin`, `order.shipped`,
1860
1859
  `order.canceled`, `order.return_requested`, `order.return_received`,
@@ -1908,7 +1907,7 @@ has at most 35 characters. Email runtime-validates every contract before use.
1908
1907
 
1909
1908
  Read this section and the central design before changing Email domain behavior.
1910
1909
 
1911
- ### Shop configuration and default templates
1910
+ #### Shop configuration and default templates
1912
1911
 
1913
1912
  The E-Mail module ships a neutral German layout in grey tones (table-based,
1914
1913
  inline styles) and one renderer per accepted kind; the colors of the
@@ -2094,7 +2093,7 @@ configuration (blank `displayName`, a non-function `layout` or renderer, an
2094
2093
  unknown renderer kind) fails at startup and not at the first send. Custom
2095
2094
  renderers must escape dynamic values in `bodyHtml` themselves.
2096
2095
 
2097
- ### Deprecated SMTP helpers
2096
+ #### Deprecated SMTP helpers
2098
2097
 
2099
2098
  `createSmtpNotificationModuleConfig(smtp)`, `resolveSmtp`,
2100
2099
  `SMTP_ENVIRONMENT_VARIABLES` and `SMTP_BUILD_PLACEHOLDERS` remain exported
@@ -2103,7 +2102,7 @@ given to `createSmtpNotificationModuleConfig` only fill SMTP settings that are
2103
2102
  not stored in the Admin (the Admin status does not count them as configured).
2104
2103
  Use `createEmailNotificationModuleConfig` and the "Versand" page instead.
2105
2104
 
2106
- ### Delivery Semantics
2105
+ #### Delivery Semantics
2107
2106
 
2108
2107
  For ordinary durable messages, only characterized connection failures,
2109
2108
  temporary SMTP rejections, and temporary attachment access failures are
@@ -2129,7 +2128,7 @@ data remain ephemeral; both kinds are excluded from the generic retry job and
2129
2128
  Admin resend, and retryable handling stays bounded by the original Event Bus
2130
2129
  event.
2131
2130
 
2132
- ### Admin API
2131
+ #### Admin API
2133
2132
 
2134
2133
  Email exposes authenticated Admin operations at:
2135
2134
 
@@ -2200,7 +2199,7 @@ a different UUID represents a separate, conscious Admin decision. The Admin UI
2200
2199
  therefore retains a UUID while retrying an ambiguous failed request and creates
2201
2200
  a new UUID only after a new confirmation.
2202
2201
 
2203
- ### Admin UI
2202
+ #### Admin UI
2204
2203
 
2205
2204
  The E-Mail module has an Admin area under `/settings/hartl-services/email`
2206
2205
  (**Settings → Hartl Services Plugins → E-Mail → Öffnen**; the descriptor's
@@ -2249,7 +2248,7 @@ request UUID; downloads use the configured authenticated Admin SDK and never
2249
2248
  expose storage file IDs or URLs in the UI. There is no delete: removing every
2250
2249
  assigned mail lets a document rest.
2251
2250
 
2252
- ### Mail templates
2251
+ #### Mail templates
2253
2252
 
2254
2253
  All 13 mails (`EMAIL_EDITABLE_KINDS`) are block templates an Admin edits
2255
2254
  from the group pages of the E-Mail area (Bestell-, Konto- and Affiliate-Mails
@@ -2350,7 +2349,7 @@ and then each changed mail kind on its own.
2350
2349
  bzw. ist beschädigt.“), any other read failure „Anhang „X“ konnte
2351
2350
  gerade nicht geladen werden.“
2352
2351
 
2353
- ### Mail attachments
2352
+ #### Mail attachments
2354
2353
 
2355
2354
  The global Terms PDFs (`AGB`), the EU legal-guarantee notice or a flyer are
2356
2355
  documents of the attachment library. A document has a name, the customer
@@ -2378,13 +2377,9 @@ duplicate runtime schemas in an Admin consumer. The base's own exports remain
2378
2377
  internal server and generated Admin integration surfaces; Email HTTP contracts
2379
2378
  are published only by `@hartl-services/medusa-contracts`.
2380
2379
 
2381
- ### Development and Rollout
2380
+ #### Rollout
2382
2381
 
2383
- For local development, provide the seven SMTP variables and omit `REDIS_URL` to
2384
- use Medusa's local Event Bus. Set `REDIS_URL` only when intentionally exercising
2385
- the direct Redis provider. The backend HTTP runner ignores ambient Redis and
2386
- SMTP settings and injects an isolated Redis container, unique queue, and
2387
- token-protected SMTP fixture for every child.
2382
+ Local development notes: see "Development".
2388
2383
 
2389
2384
  This architecture replaces the old Documents and Affiliate delivery baselines
2390
2385
  without a backfill or compatibility layer. Existing local databases from those
@@ -2400,7 +2395,7 @@ production business data exists; a populated database requires a separately
2400
2395
  reviewed migration. No old delivery rows or queued jobs are migrated or purged
2401
2396
  by Email.
2402
2397
 
2403
- ## Dokumente
2398
+ ### Dokumente
2404
2399
 
2405
2400
  The base issues order documents: one total invoice per order after the final
2406
2401
  shipment and one negative cancellation document for each eligible payment
@@ -2408,10 +2403,9 @@ refund. An Admin user can regenerate issued documents in place for exceptional
2408
2403
  corrections. The base ships default German document markup, so a new shop needs
2409
2404
  no template code; a project can optionally replace the HTML/CSS factory. PDF
2410
2405
  generation, private file storage, customer authorization and durable
2411
- document-readiness facts stay inside the `documents` module. There is no
2412
- separate documents package since 1.0.0.
2406
+ document-readiness facts stay inside the `documents` module.
2413
2407
 
2414
- ### What it does
2408
+ #### What it does
2415
2409
 
2416
2410
  - Listens to Medusa's `shipment.created` event.
2417
2411
  - Creates an invoice only if the order is fully shipped and not cancelled.
@@ -2478,7 +2472,7 @@ separate documents package since 1.0.0.
2478
2472
  - Reconciles non-canceled physical fulfillments periodically so a lost local
2479
2473
  event can be repaired idempotently, including after shipment.
2480
2474
 
2481
- ### Configuration
2475
+ #### Configuration
2482
2476
 
2483
2477
  The invoice workflow reads the order's frozen B2B evidence from the base's own
2484
2478
  `tax_compliance` module (see "Steuer-Compliance"); register the Tax provider
@@ -2537,7 +2531,7 @@ The `documents` module is always registered with the base (no `documentsEnabled`
2537
2531
  switch); the Admin switches it off at run time through the feature toggles
2538
2532
  below. `medusa db:migrate` creates its tables.
2539
2533
 
2540
- ### The `/config` subpath
2534
+ #### The `/config` subpath
2541
2535
 
2542
2536
  `@hartl-services/medusa-base/config` exports, next to the environment helpers,
2543
2537
  build-time values for `medusa-config.ts` and tests:
@@ -2552,7 +2546,7 @@ build-time values for `medusa-config.ts` and tests:
2552
2546
  The document types (`DocumentMerchantOption`, `DocumentHtmlFactory`,
2553
2547
  `DocumentRenderInput`, ...) come from `@hartl-services/medusa-base/types`.
2554
2548
 
2555
- ### Layout, logo, payment terms and shop theme
2549
+ #### Layout, logo, payment terms and shop theme
2556
2550
 
2557
2551
  The default markup prints an A4 letterhead: logo top left, document title and
2558
2552
  key data (number, order, date, customer UID for exempt B2B supplies) on the
@@ -2595,7 +2589,7 @@ fonts as `data:` URLs; the container's Chromium has only its system fonts. A
2595
2589
  shop passes its factory through the preset option `documentHtmlFactory` (see
2596
2590
  "Shop-Config-Preset").
2597
2591
 
2598
- ### Merchant data
2592
+ #### Merchant data
2599
2593
 
2600
2594
  The legal sender printed on every document comes from the base plugin's shop
2601
2595
  master data (Admin → Settings → Hartl Services Plugins → **Shop-Stammdaten**).
@@ -2627,7 +2621,7 @@ Requirements/couplings:
2627
2621
  - The Tax provider `tp_tax_compliance` of the base's Steuer-Compliance on the
2628
2622
  regions, so orders carry UID-based exemptions.
2629
2623
  - A Chromium/Chrome executable and a Medusa File provider that supports
2630
- private files (see "Development" below for the sandbox/non-root note).
2624
+ private files (see "Shop integration" below for the sandbox/non-root note).
2631
2625
  Documents stores PDFs through `Modules.FILE` and streams them only through
2632
2626
  its own authenticated routes, so it works with any File provider and does
2633
2627
  not need the S3 file storage or its `/files` routes.
@@ -2643,8 +2637,7 @@ Requirements/couplings:
2643
2637
  batch/lot data on invoices and packing lists (see "Optional Batch
2644
2638
  Management integration" below).
2645
2639
 
2646
-
2647
- ### Feature toggles
2640
+ #### Feature toggles
2648
2641
 
2649
2642
  The Documents module declares its toggles to the base under the plugin key
2650
2643
  `documents`. An Admin switches them under Settings → "Hartl
@@ -2682,15 +2675,14 @@ using `PluginFeatureGate` from
2682
2675
  No workflow hook and nothing on the cart, checkout or fulfillment path reads
2683
2676
  a toggle.
2684
2677
 
2685
- ### Shop integration
2678
+ #### Shop integration
2686
2679
 
2687
2680
  The demo shop (`apps/backend`) gets everything from the preset (see
2688
2681
  "Shop-Config-Preset"): `documents.numbering.timeZone` is its `timeZone`
2689
2682
  option, `documents.htmlFactory` its optional `documentHtmlFactory` and
2690
2683
  `documents.pdf.executablePath` the `DOCUMENT_PDF_EXECUTABLE` variable. The
2691
- development UID `ATU00000000` is a placeholder: Documents never prints it,
2692
- and Tax Compliance does not transmit it to FinanzOnline. Enter the real
2693
- merchant UID in the Shop-Stammdaten before issuing production invoices.
2684
+ development UID `ATU00000000` is a placeholder that Documents never prints
2685
+ (see "UID check in the checkout").
2694
2686
 
2695
2687
  `htmlFactory` is intentionally the only presentation extension point. It gets
2696
2688
  a fully typed value snapshot (`DocumentTemplateInput` or
@@ -2708,7 +2700,7 @@ directory and removes it afterward. The Chromium child is also disconnected
2708
2700
  from the user's session bus and legacy keyring socket, so it cannot access the
2709
2701
  operator's normal browser profile or desktop keyring, including under WSLg.
2710
2702
 
2711
- ### Restore check for private files
2703
+ #### Restore check for private files
2712
2704
 
2713
2705
  After restoring the object store (or the local file directory), check that
2714
2706
  every issued document's PDF can be read through the active Medusa File
@@ -2722,7 +2714,7 @@ pnpm exec medusa exec ./node_modules/@hartl-services/medusa-base/.medusa/server/
2722
2714
  It fails on the first issued document without a stored file and otherwise
2723
2715
  logs the number of verified files and bytes.
2724
2716
 
2725
- ### Clean baseline and first deployment
2717
+ #### Clean baseline and first deployment
2726
2718
 
2727
2719
  The module intentionally contains one generated eight-table baseline,
2728
2720
  `Migration20260827061749`. It removes the former `documents_delivery` table
@@ -2768,7 +2760,7 @@ Apply normal pending module migrations after later changes or upgrades with
2768
2760
  does not require link synchronization by itself; `db:sync-links` above is the
2769
2761
  safe shop-wide first-deployment step for links owned elsewhere.
2770
2762
 
2771
- ### Storefront contract
2763
+ #### Storefront contract
2772
2764
 
2773
2765
  All three endpoints require a customer session or bearer token. There is no
2774
2766
  guest lookup endpoint and no direct File-provider URL.
@@ -2807,7 +2799,7 @@ Storefront's configured Medusa SDK. A consumer that needs a runtime guard may
2807
2799
  opt in to `@hartl-services/medusa-contracts/schemas/<feature>`; it must
2808
2800
  not import either the plugin implementation or generated `.medusa` output.
2809
2801
 
2810
- ### Admin order detail
2802
+ #### Admin order detail
2811
2803
 
2812
2804
  When Documents is enabled, the Medusa Admin order detail shows a
2813
2805
  **Dokumente** widget. It lists the issued documents for that order (kind and
@@ -2827,7 +2819,7 @@ The Admin API is deliberately scoped by both order and document ID:
2827
2819
  All authenticated Admin users can inspect order documents; a document from a
2828
2820
  different order returns `404` from the download endpoint.
2829
2821
 
2830
- #### Internal packing lists
2822
+ ##### Internal packing lists
2831
2823
 
2832
2824
  The separate **Packlisten** order widget lists every physical fulfillment in
2833
2825
  creation order, each with a status badge (Nicht erstellt, Ausstehend, Wird
@@ -2851,7 +2843,7 @@ Packing lists never pass through the Documents customer-fact pipeline. They are
2851
2843
  also stored outside `documents_document`, so the authenticated Store document
2852
2844
  queries cannot expose them accidentally.
2853
2845
 
2854
- #### Regenerating an invoice
2846
+ ##### Regenerating an invoice
2855
2847
 
2856
2848
  The widget offers **Rechnung ausstellen** only while the order has no
2857
2849
  invoice; once one exists, every document row has a regenerate icon button
@@ -2872,7 +2864,7 @@ above. A cancellation or future credit note is instead rendered from its
2872
2864
  persisted snapshot, retaining its original amount, source invoice reference,
2873
2865
  number and issue date. It also emits no customer communication event.
2874
2866
 
2875
- #### Financial document numbers
2867
+ ##### Financial document numbers
2876
2868
 
2877
2869
  Documents assigns numbers in these annual ranges:
2878
2870
 
@@ -2895,7 +2887,7 @@ increments a range. A permanently failed document retains its number and
2895
2887
  failure reason as the explanation for that range's gap; reserved numbers are
2896
2888
  never recycled or reassigned.
2897
2889
 
2898
- #### Cancellation documents for payment refunds
2890
+ ##### Cancellation documents for payment refunds
2899
2891
 
2900
2892
  Medusa's refund event identifies the payment, so the plugin enumerates its
2901
2893
  persisted refund records and claims one cancellation document using each
@@ -2917,7 +2909,7 @@ not create a second customer fact. For a canceled Order, individual Refund
2917
2909
  facts are suppressed in favor of the authoritative Order-cancellation
2918
2910
  resolution and its sorted artifact list.
2919
2911
 
2920
- ### Optional Batch Management integration
2912
+ #### Optional Batch Management integration
2921
2913
 
2922
2914
  If `@hartl-services/medusa-batch-management` version `0.0.3` or later is registered, Documents detects
2923
2915
  its `getOrderDocumentBatchSnapshot(orderId)` capability structurally. For every
@@ -2948,7 +2940,7 @@ list revision and serves the newest ready PDF. Returns do not rewrite the
2948
2940
  historical pick instruction. Fulfillment cancellation marks the packing list as
2949
2941
  canceled and prevents a concurrent render from being published as active.
2950
2942
 
2951
- ### Document facts and artifacts
2943
+ #### Document facts and artifacts
2952
2944
 
2953
2945
  Documents owns no Notification or delivery state. Its integration boundary is
2954
2946
  the strict versioned event contract exported from
@@ -2969,7 +2961,7 @@ worker has lost its lease, the unreferenced private File is deleted best-effort;
2969
2961
  a hard process exit in that external File/database gap can leave a storage
2970
2962
  orphan but cannot commit another Artifact, carrier or resolution.
2971
2963
 
2972
- #### Exact outgoing event contracts
2964
+ ##### Exact outgoing event contracts
2973
2965
 
2974
2966
  The server-only Contracts subpath
2975
2967
  `@hartl-services/medusa-contracts/events/documents` exports the
@@ -3053,8 +3045,6 @@ before the process can persist success, so an ambiguous attempt becomes
3053
3045
  `unknown` and is not automatically sent again. Any later resend is an explicit
3054
3046
  Email Admin action and a new logical message.
3055
3047
 
3056
- ### Development
3057
-
3058
3048
  #### Public Storefront and Admin contracts
3059
3049
 
3060
3050
  The central contracts package exports the Documents Store and Admin DTOs, Zod
@@ -3095,16 +3085,15 @@ const response = await sdk.client.fetch<ListAdminOrderPackingListsResponse>(
3095
3085
  );
3096
3086
  ```
3097
3087
 
3098
- ## Steuer-Compliance
3088
+ ### Steuer-Compliance
3099
3089
 
3100
- There is no separate `@hartl-services/medusa-tax-compliance` package since
3101
- 3.0.0: the EU tax compliance (module `tax_compliance`, the Tax Module provider
3090
+ The EU tax compliance (module `tax_compliance`, the Tax Module provider
3102
3091
  `tp_tax_compliance`, the UID check routes, the order-evidence freeze and the
3103
- apportioned shipping tax) is part of the base. The module owns the durable
3104
- validation evidence; a Storefront never imports its implementation, only
3105
- `@hartl-services/medusa-contracts`.
3092
+ apportioned shipping tax) is part of the base since 3.0.0 (see "Upgrading").
3093
+ The module owns the durable validation evidence; a Storefront never imports
3094
+ its implementation, only `@hartl-services/medusa-contracts`.
3106
3095
 
3107
- ### Configuration
3096
+ #### Configuration
3108
3097
 
3109
3098
  Register the Tax Module provider in `medusa-config.ts` (the Shop-Config-Preset
3110
3099
  does this); the module itself boots with the base and has no options:
@@ -3134,7 +3123,7 @@ The merchant's own country and VAT number come only from the Shop-Stammdaten
3134
3123
  select the qualified VAT validator (currently only the Austrian FinanzOnline
3135
3124
  adapter). While either is missing, carts get ordinary B2C tax.
3136
3125
 
3137
- ### Runtime settings
3126
+ #### FinanzOnline settings
3138
3127
 
3139
3128
  Declared on the Steuer-Compliance descriptor and edited on its Admin page
3140
3129
  (Settings → Hartl Services Plugins → Steuer-Compliance):
@@ -3185,7 +3174,7 @@ qualifizierte UID-Abfrage.").
3185
3174
  answers ordinary B2C rates. The order tax path needs no merchant: it checks
3186
3175
  the evidence frozen under the order ID.
3187
3176
 
3188
- ### Feature toggles
3177
+ #### Feature toggles
3189
3178
 
3190
3179
  Steuer-Compliance declares its switch and three features under the plugin key
3191
3180
  `tax_compliance` (`TAX_COMPLIANCE_PLUGIN_KEY` / `TAX_COMPLIANCE_FEATURES` from
@@ -3224,7 +3213,7 @@ values; the `plugin-features.updated.v1` subscriber and the
3224
3213
  sync (no row) both count as on; when the row cannot be read both count as
3225
3214
  off (ordinary B2C rates, Medusa's shipping tax).
3226
3215
 
3227
- ### UID check in the checkout
3216
+ #### UID check in the checkout
3228
3217
 
3229
3218
  Public Store request, response, schema, and path imports belong to
3230
3219
  `@hartl-services/medusa-contracts`:
@@ -3334,7 +3323,7 @@ gets 3 s, so the request stays below a storefront's 15 s checkout wait and the
3334
3323
  database transaction holding the UID lock never waits on a hanging
3335
3324
  FinanzOnline call; a timeout answers `unavailable` (`technical_error`).
3336
3325
 
3337
- ### Saved customer addresses
3326
+ #### Saved customer addresses
3338
3327
 
3339
3328
  A saved customer address carries its UID in `metadata.vat_number`
3340
3329
  (`TAX_IDENTIFICATION_METADATA_KEY` in the Contracts). A logged-in customer
@@ -3382,7 +3371,7 @@ facts likewise receive ordinary B2C tax, without a warning. The registry is
3382
3371
  country-based: it currently provides only the Austrian FinanzOnline adapter;
3383
3372
  an unsupported merchant country deliberately resolves to `unavailable`.
3384
3373
 
3385
- ### Frozen order evidence
3374
+ #### Frozen order evidence
3386
3375
 
3387
3376
  Cart tax workflows pass only the opaque current evidence ID and the merchant
3388
3377
  profile (country and UID) to the provider. Order tax workflows use evidence
@@ -3411,19 +3400,104 @@ wording from the completed order (see "Dokumente"). A later cart change or
3411
3400
  validation cannot alter an already issued document; B2C, invalid, ineligible
3412
3401
  and unavailable evidence adds no tax-proof wording.
3413
3402
 
3414
- ## Dateispeicher (S3)
3403
+ ### Versandkostenfrei
3404
+
3405
+ The shop-wide free-shipping threshold with a separate threshold for
3406
+ affiliates is part of the base since 5.0.0 (module `free_shipping`, see
3407
+ "Upgrading"). It has no Admin page and no HTTP route of its own: its switch
3408
+ and its settings are edited on the generic plugin page **Settings → Hartl
3409
+ Services Plugins → Versandkostenfrei**
3410
+ (`/settings/hartl-services/plugin/free_shipping`).
3411
+
3412
+ #### What it does
3413
+
3414
+ - Keeps two automatic Medusa promotions (`AUTO-FREE-SHIPPING-STANDARD`,
3415
+ `AUTO-FREE-SHIPPING-AFFILIATE`, 100 % off shipping) in sync with the
3416
+ settings and the plugin switch:
3417
+ - right after the settings are saved in the Admin
3418
+ (`plugin-settings.updated.v1` subscriber),
3419
+ - right after the switch is changed in the Admin
3420
+ (`plugin-features.updated.v1` subscriber),
3421
+ - every 15 minutes through the `free-shipping-promotion-sync` scheduled job
3422
+ (worker instances only), which also repairs drift and covers a lost event.
3423
+
3424
+ Every run holds one Locking-module lock (`free-shipping-promotion-sync`,
3425
+ 60 s timeout) around the whole sync, so the job and the two subscribers never
3426
+ interleave their rule changes, and the last run always writes the latest
3427
+ values. The shop's configured Locking provider applies (Medusa's default
3428
+ in-memory provider serializes within one process only; use a shared
3429
+ provider such as `@medusajs/locking-redis` when server and worker are
3430
+ separate processes). The sync writes only differences, so a run against
3431
+ in-sync promotions is read-only. Changed rules are created before outdated
3432
+ ones are deleted, so an automatic promotion never exists without its rules
3433
+ (which would grant free shipping to every cart).
3434
+ - Adds the cart's affiliate qualification to the promotion context (hook on
3435
+ `updateCartPromotionsWorkflow`). This uses the `affiliate` module only if it
3436
+ is registered (`tryResolveOptional`); without the Affiliate plugin, only the
3437
+ default threshold applies.
3438
+
3439
+ #### Settings
3440
+
3441
+ Stored in `plugin_setting_state` under the plugin key `free_shipping`; every
3442
+ setting has a default, so the feature works without ever saving the page.
3443
+
3444
+ | Key | Type | Default | Label | Effect |
3445
+ | ----------------------------------------- | ------- | ------- | ------------------------------------------------------------------------ | --------------------------------------------------------------------------------------- |
3446
+ | `default_threshold` | number ≥ 0 | 50 | Kostenlose Lieferung ab (€) | `item_total` threshold of `AUTO-FREE-SHIPPING-STANDARD` |
3447
+ | `affiliate_threshold` | number ≥ 0 | 100 | Für Affiliates ab (€) | `item_total` threshold of `AUTO-FREE-SHIPPING-AFFILIATE` |
3448
+ | `affiliate_applies_to_account_holders` | boolean | on | Gilt, wenn der Kunde selbst Affiliate ist | the cart's customer is an affiliate → affiliate threshold |
3449
+ | `affiliate_applies_to_referred_customers` | boolean | off | Gilt, wenn der Warenkorb über einen Affiliate-Link oder -Code zugeordnet wurde | the cart is assigned to an affiliate referral → affiliate threshold |
3450
+
3451
+ The base validates the values (a negative threshold is rejected with 400).
3452
+ Server code reads them with `resolveFreeShippingSettings(container)`
3453
+ (`src/free-shipping/free-shipping-settings.ts`, a typed wrapper around
3454
+ `getPluginSettings`). The two boolean rules are evaluated in the cart hook on
3455
+ every promotion computation, so they take effect without a promotion resync;
3456
+ the thresholds reach the promotions through the sync described above.
3457
+
3458
+ The cart hook is fail-safe: if the settings cannot be read, the error is
3459
+ reported with `logger.error` (forwarded to Sentry by the logger wrapper) and
3460
+ the cart is computed with the two rules this process last read successfully,
3461
+ or with their declared defaults (account holders on, referred customers off)
3462
+ when it has not read them yet. The cart request never fails because of it.
3463
+ The job and the subscribers let a read failure propagate (the job runs again,
3464
+ the event is retried).
3465
+
3466
+ #### Feature toggle
3467
+
3468
+ The switch lives under the plugin key `free_shipping`; Storefronts read it
3469
+ from `GET /store/plugin-features/free_shipping`.
3470
+
3471
+ | Key | Label | Default | Gates | Keeps running while off |
3472
+ | --------------- | ----------------- | ------- | --------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------- |
3473
+ | (plugin switch) | Versandkostenfrei | on | status of the promotions `AUTO-FREE-SHIPPING-STANDARD` and `AUTO-FREE-SHIPPING-AFFILIATE` (active while on, inactive while off) | the settings (thresholds stay editable and are synced into the inactive promotions), the `free-shipping-promotion-sync` job (it deactivates the promotions), the promotion-context hook on the cart path |
3474
+
3475
+ There are no sub-features. No route answers 404 because of the switch; the
3476
+ store-visible effect of "off" is that the two automatic promotions are
3477
+ inactive. The switch takes effect immediately on an Admin change and at the
3478
+ latest with the next 15-minute job run otherwise.
3479
+
3480
+ #### Migrations
3481
+
3482
+ The module ships the migrations of the former package unchanged: the 0.2.0
3483
+ migration dropped the old `free_shipping_settings` table and carried stored
3484
+ thresholds and affiliate rules over into `plugin_setting_state` /
3485
+ `plugin_feature_state`. A shop that used the former package keeps its data;
3486
+ `medusa db:migrate` after upgrading is enough.
3487
+
3488
+ ### Dateispeicher (S3)
3415
3489
 
3416
3490
  The base provides a Medusa File Module provider for durable S3-compatible
3417
3491
  asset storage with a local, TTL-based read cache (`providers/s3-cache`), and
3418
3492
  the `/files` gateway routes every provider URL points at. It supports AWS S3,
3419
- MinIO and compatible endpoints through AWS SDK v3. There is no separate S3
3420
- package since 1.0.0; the provider identifier `s3-cache` and the resolve path
3421
- `@hartl-services/medusa-base/providers/s3-cache` replace the old one.
3493
+ MinIO and compatible endpoints through AWS SDK v3. The provider identifier is
3494
+ `s3-cache` and its resolve path `@hartl-services/medusa-base/providers/s3-cache`
3495
+ (part of the base since 1.0.0, see "Upgrading").
3422
3496
 
3423
3497
  S3 is always the source of truth. The local cache is a per-replica performance
3424
3498
  optimization and may be discarded at any time without data loss.
3425
3499
 
3426
- ### Coverage
3500
+ #### Coverage
3427
3501
 
3428
3502
  The provider covers every asset that uses Medusa's native `Modules.FILE`
3429
3503
  service:
@@ -3437,7 +3511,7 @@ It cannot transparently back up a plugin that writes directly to its own local
3437
3511
  directory, own S3 client, or a third-party URL. Such integrations must be
3438
3512
  changed to use `Modules.FILE` or migrated explicitly.
3439
3513
 
3440
- ### Configuration
3514
+ #### Configuration
3441
3515
 
3442
3516
  ```ts
3443
3517
  // medusa-config.ts
@@ -3513,7 +3587,7 @@ options) is optional; see
3513
3587
  [Active S3 options and defaults](#active-s3-options-and-defaults) below. The
3514
3588
  provider has no database migrations to run.
3515
3589
 
3516
- ### Environment helpers
3590
+ #### Environment helpers
3517
3591
 
3518
3592
  `@hartl-services/medusa-base/config` also exports small helpers for reading
3519
3593
  the deployment variables in `medusa-config.ts`:
@@ -3529,7 +3603,7 @@ the deployment variables in `medusa-config.ts`:
3529
3603
  - `resolveExpirationStrategy(process.env, defaultValue)` — reads
3530
3604
  `S3_CACHE_STORAGE_EXPIRATION_STRATEGY` (`"absolute"` or `"sliding"`).
3531
3605
 
3532
- ### Runtime configuration
3606
+ #### Runtime configuration
3533
3607
 
3534
3608
  The demo shop (`apps/backend`) enables S3 Cache Storage as the sole Medusa
3535
3609
  File Module provider through the preset; there is no per-deployment feature
@@ -3549,7 +3623,7 @@ S3 keys unless the matching database and storage rollback is being performed.
3549
3623
  The local provider cannot read S3-backed IDs. Treat a storage-provider change
3550
3624
  as a controlled deployment cutover, not as a runtime storage router.
3551
3625
 
3552
- #### Active S3 options and defaults
3626
+ ##### Active S3 options and defaults
3553
3627
 
3554
3628
  In this demo host, `bucket`, `region`, and the File Gateway base URL are
3555
3629
  required.
@@ -3571,7 +3645,7 @@ URLs may use only HTTP(S), with no credentials, query string, or fragment.
3571
3645
  Prefixes and provider keys reject absolute paths, traversal, control
3572
3646
  characters, and backslashes.
3573
3647
 
3574
- ### Object and access model
3648
+ #### Object and access model
3575
3649
 
3576
3650
  New uploads have opaque keys. The original filename is stored only as encoded
3577
3651
  object metadata:
@@ -3606,7 +3680,7 @@ successful remote write/delete evicts the affected entry only after S3 has
3606
3680
  accepted it. Do not share the cache directory between replicas; use a bounded
3607
3681
  `emptyDir` or per-pod volume and monitor free disk space.
3608
3682
 
3609
- ### Backup and restore
3683
+ #### Backup and restore
3610
3684
 
3611
3685
  A complete restore requires both a PostgreSQL backup and the versioned object
3612
3686
  store. The cache is not a backup and is intentionally excluded.
@@ -3629,7 +3703,7 @@ count and total bytes without modifying data:
3629
3703
  medusa exec ./node_modules/@hartl-services/medusa-base/.medusa/server/src/documents/scripts/verify-documents-file-storage.js
3630
3704
  ```
3631
3705
 
3632
- ### HTTP contract
3706
+ #### HTTP contract
3633
3707
 
3634
3708
  The gateway routes expose `GET /files/:asset` for public immutable files and
3635
3709
  `GET /files/private/:token` for signed private capability URLs. Both call
@@ -3640,126 +3714,234 @@ Paths, query types, and the runtime query schema are exported from the separate
3640
3714
  normally consume the complete URL returned by Medusa and never construct a
3641
3715
  private URL themselves.
3642
3716
 
3643
- ## Shop-Config-Preset
3717
+ ## Shop behaviour and operations
3644
3718
 
3645
- `@hartl-services/medusa-base/shop-config` turns a shop's configuration
3646
- from about 290 lines into about 30: the plugin list and order, the Redis event
3647
- bus, SMTP notifications, S3 file storage, the tax and Stripe payment providers,
3648
- the Admin path, the Sentry logger bridge and the environment validation are
3649
- assembled there; the shop supplies only its own facts. It reuses the `/config`
3650
- helpers above for environment parsing. It is a separate subpath because it
3651
- loads the Sentry logger wrapper and with it `@sentry/node`; `/config` stays
3652
- free of it.
3719
+ Behaviour the base adds to every shop without a toggle, and the
3720
+ `medusa-config.ts` helpers for operating it.
3653
3721
 
3654
- The preset registers the base (with its E-Mail, Documents and `/files`
3655
- options, and its Tax provider) plus four feature plugins, in this order:
3656
- `medusa-base`, `medusa-free-shipping`, `medusa-batch-management`,
3657
- `medusa-affiliate`, `medusa-payload-sync`.
3722
+ ### Root redirect
3658
3723
 
3659
- ### Installation in a shop
3724
+ Medusa serves nothing at `/`, so a deployed domain answers it with a 404. The
3725
+ base registers `GET /` and answers with a `302` to the shop's `admin.path`
3726
+ (read from the config module, fallback `/app`), so opening the bare domain
3727
+ lands on the admin dashboard in every shop and in local development without a
3728
+ reverse-proxy rule. The redirect is temporary on purpose: browsers cache a
3729
+ `301` on the bare domain essentially forever, which would be painful to undo
3730
+ should `/` ever get real content. A shop that needs its own `/` defines
3731
+ `src/api/route.ts` itself.
3660
3732
 
3661
- ```bash
3662
- pnpm add @hartl-services/medusa-base \
3663
- @hartl-services/medusa-free-shipping \
3664
- @hartl-services/medusa-batch-management \
3665
- @hartl-services/medusa-affiliate \
3666
- @hartl-services/medusa-payload-sync \
3667
- @medusajs/event-bus-redis @medusajs/payment-stripe \
3668
- @sentry/node
3669
- ```
3733
+ ### Cart-promotion link repair
3670
3734
 
3671
- ### Usage
3735
+ Medusa's `deletePromotionsWorkflow` only soft-deletes a promotion and leaves
3736
+ its `cart_promotion` link rows behind, so every cart that had it applied
3737
+ reads back a `null` entry inside `cart.promotions`. The base removes those
3738
+ links itself:
3739
+
3740
+ - The `deletePromotionsWorkflow.hooks.promotionsDeleted` hook
3741
+ (`workflows/hooks/dismiss-deleted-promotion-cart-links`) dismisses the
3742
+ links of the deleted promotions and restores them if a later step fails.
3743
+ - The migration script `dismiss-orphaned-cart-promotion-links` runs once in
3744
+ `medusa db:migrate` and removes the links that already dangle from earlier
3745
+ deletions. Medusa tracks migration scripts by file name in
3746
+ `script_migrations`, so the name must not change.
3747
+
3748
+ Nothing to configure; `medusa db:migrate` after installing or upgrading to
3749
+ 0.4.1 is enough.
3750
+
3751
+ ### Draft orders and 0 € orders
3752
+
3753
+ Draft orders (Medusa's `@medusajs/draft-order` plugin, loaded by default) are
3754
+ the shop's way to enter an order in the Admin: sample shipments, orders taken
3755
+ by phone, replacements. Item prices and the shipping amount are set directly
3756
+ in the draft; converting it never involves a payment provider.
3757
+
3758
+ - **Customer order confirmation off by default.** The
3759
+ `createOrderWorkflow.hooks.orderCreated` hook
3760
+ (`workflows/hooks/draft-order-confirmation-default`) sets Medusa's
3761
+ `no_notification` on every new draft, also when an API caller sent
3762
+ `no_notification_order: false`. The Admin switches it per draft in the
3763
+ **Bestellbestätigung** widget on the draft's detail page
3764
+ (`GET`/`POST /admin/draft-orders/:id/order-confirmation`, see the E-Mail
3765
+ "Admin API"). On conversion the `order-placed` subscriber records the
3766
+ customer confirmation as `suppressed` (`notifications_disabled`); the copy
3767
+ to the shop's order address is still sent. The shipment mail follows the
3768
+ Admin's "notify customer" choice per shipment, not this switch.
3769
+ - **0 € orders count as paid.** Medusa derives an order's payment status from
3770
+ its payment collections only, so a converted 0 € draft would stay
3771
+ "Nicht bezahlt". The subscriber `order-zero-total-paid` runs
3772
+ `order-mark-zero-total-paid` on `order.placed`: an order with a total of 0
3773
+ and no payment collection gets a completed 0 € payment collection and reads
3774
+ "Bezahlt" (`payment_status: captured`). Orders that already carry a
3775
+ collection, i.e. every checkout order, are left alone. In SQL, a paid
3776
+ order is one whose linked `payment_collection` (via
3777
+ `order_payment_collection`) has `status = 'completed'`.
3778
+
3779
+ Orders placed before installing this version keep their state.
3780
+
3781
+ ### Password policy
3782
+
3783
+ Medusa's `emailpass` provider accepts any non-empty password. The base runs a
3784
+ middleware on `POST /auth/:actor_type/emailpass/register` (customer sign-up,
3785
+ Admin invite) and `POST /auth/:actor_type/emailpass/update` (password reset).
3786
+ It is registered on `/auth/:actor_type/:auth_provider/:action` without
3787
+ `methods` and decides on the decoded, lowercased params, because Express
3788
+ matches literal path segments against the raw path: `/auth/customer/email%70ass/register`
3789
+ and `/auth/Customer/emailpass/register` reach Medusa's handler too, and
3790
+ `Customer` counts as the customer actor here.
3791
+ A password that breaks the rules of the actor is answered with
3792
+ `400 { type: "invalid_data", message }`; the one German message lists every
3793
+ unmet rule, e.g. `Das Passwort erfüllt nicht alle Regeln: mindestens 12
3794
+ Zeichen, eine Ziffer.` The check runs before Medusa's reset-token check, so a
3795
+ rejected password leaves the single-use reset link valid for a second attempt.
3796
+
3797
+ **Admin users** (every actor type except `customer`) always need at least 15
3798
+ characters, counted as Unicode code points, and no composition rules. This
3799
+ follows NIST SP 800-63B Rev. 4 (the US guideline for digital identity) for a
3800
+ password that is the only factor.
3801
+
3802
+ **Customers** follow the settings of the "Kunden-Login" plugin (group
3803
+ "Passwort-Regeln", Settings > "Hartl Services Plugins"). They apply only while
3804
+ `email_password` is on and take effect at once, without a cache:
3805
+
3806
+ | Key | Label | Default | Meaning |
3807
+ | ----------------------------- | ------------------------------------ | ------- | ------- |
3808
+ | `password_min_length` | Mindestlänge | 15 | 8 to 128 characters, whole numbers only (`integer`); NIST SP 800-63B Rev. 4 recommends at least 15 when the password is the only factor |
3809
+ | `password_require_mixed_case` | Groß- und Kleinbuchstaben verlangen | off | at least one uppercase and one lowercase letter |
3810
+ | `password_require_digit` | Ziffer verlangen | off | at least one digit |
3811
+ | `password_require_symbol` | Sonderzeichen verlangen | off | at least one punctuation mark or symbol (Unicode category P or S; whitespace, combining marks and invisible characters do not count) |
3812
+
3813
+ NIST SP 800-63B Rev. 4 advises against composition rules, because they lead to
3814
+ predictable passwords such as "Passwort1!"; a longer password protects better.
3815
+ The Admin texts say so. A missing or invalid stored value falls back to the
3816
+ default of that rule, and if the settings cannot be read at all the default
3817
+ policy (15 characters, no composition rules) applies and a warning is logged,
3818
+ so a registration is never blocked by a settings outage.
3819
+
3820
+ **Route.** `GET /store/customer-auth/password-policy` (publishable key only)
3821
+ answers `{ password_policy: { min_length, require_mixed_case, require_digit,
3822
+ require_symbol } }`, the effective customer rules, and 404 while
3823
+ `email_password` is off. The storefront shows them in its forms and checks
3824
+ them with the same functions as the backend. The types, the path
3825
+ (`customerAuthStorePaths.get.passwordPolicy`) and the rules
3826
+ (`evaluatePasswordPolicy`, `isPasswordAllowed`, `DEFAULT_CUSTOMER_PASSWORD_POLICY`,
3827
+ `ADMIN_PASSWORD_POLICY` in `schemas/password-policy`) come from the Contracts
3828
+ package.
3829
+
3830
+ **Admin passwords and the shared identity.** An `emailpass` identity is shared
3831
+ by all actor types: someone invited as an Admin could register through
3832
+ `/auth/customer/emailpass/register` with the (weaker) customer rules, log in at
3833
+ `/auth/user/emailpass` and accept the invite with that token, which would bind
3834
+ the weak password to an Admin. So the Admin login (`POST` and `GET`
3835
+ `/auth/user/emailpass`, decoded and lowercased like above) checks the body
3836
+ password against the Admin rules and answers the same `400` when the identity
3837
+ has no Admin yet (`app_metadata.user_id` unset). The login of an identity that
3838
+ already belongs to an Admin is never checked. Remaining limit, deliberately not
3839
+ closed: a customer could register through the Admin path with 15 characters and
3840
+ no composition rules although the shop asks for more; that only weakens their
3841
+ own password.
3842
+
3843
+ Login of a customer is not checked, so existing passwords keep working until their next
3844
+ reset, whatever the rules are. Accounts created without HTTP (`medusa user`,
3845
+ seed scripts calling the Auth module) are not checked either.
3846
+
3847
+ ### Config helpers
3848
+
3849
+ `@hartl-services/medusa-base/config` holds the generic environment helpers a
3850
+ shop's `medusa-config.ts` needs, so every shop fails at startup with the name
3851
+ of the missing variable instead of a runtime crash. The environment helpers
3852
+ have no dependency on Medusa; the subpath does not load `@sentry/node`.
3853
+
3854
+ | Export | Purpose |
3855
+ |---|---|
3856
+ | `ConfigurationPhase` | `"build" \| "runtime"` |
3857
+ | `MEDUSA_BUILD_PLACEHOLDERS` | Placeholders for `ADMIN_CORS`, `AUTH_CORS`, `COOKIE_SECRET`, `DATABASE_URL`, `JWT_SECRET`, `MEDUSA_BACKEND_URL`, `STOREFRONT_URL`, `STORE_CORS`, `STRIPE_API_KEY`, `STRIPE_WEBHOOK_SECRET` |
3858
+ | `requireEnvironment(environment, names, phase, buildPlaceholders)` | Returns every named value (trimmed) or throws one error listing all missing names; in the build phase `buildPlaceholders` fill in for deployment values |
3859
+ | `optionalEnvironment`, `integerEnvironment`, `booleanEnvironment` | Trimmed optional string, range-checked integer, strict `"true"`/`"false"` boolean |
3860
+ | `allowsInsecureLocalServices(environment, phase)` | `true` for build, development and test |
3861
+ | `resolvePublicOrigin(value, name, allowHttp)`, `shopUrl(origin, path)` | Validate a bare HTTP(S) origin; build an absolute URL that cannot leave the origin |
3862
+ | `resolveStripe(values)` | Checks the `sk_` key and the `whsec_` webhook secret |
3863
+ | `resolveRedisEventBus(environment, phase)` | `{ url } \| undefined`: optional in development, required at runtime elsewhere, never used in a build, authenticated `rediss://` in production |
3864
+ | `createEmailNotificationModuleConfig`, `defineShopEmailConfiguration` (deprecated: `createSmtpNotificationModuleConfig`, `resolveSmtp`, `SMTP_ENVIRONMENT_VARIABLES`, `SMTP_BUILD_PLACEHOLDERS`) | Mail notification provider and E-Mail configuration (see "E-Mail") |
3865
+ | `createS3CacheStorageConfig`, `createS3CacheFileModuleConfig`, `resolveS3Credentials`, `resolveExpirationStrategy`, `S3_BUILD_PLACEHOLDERS` | S3 file storage (see "Dateispeicher (S3)") |
3866
+ | `createDefaultDocumentHtmlFactory`, `DOCUMENTS_BUILD_PLACEHOLDERS` | Documents markup and build placeholder (see "Dokumente") |
3867
+
3868
+ `medusa build` loads the configuration without deployment secrets. Each
3869
+ plugin's own placeholders (SMTP, S3, PDF executable, ...) live with that
3870
+ plugin; the shop merges them:
3672
3871
 
3673
3872
  ```ts
3674
3873
  // medusa-config.ts
3675
- import { defineShopConfig } from "@hartl-services/medusa-base/shop-config"
3874
+ import {
3875
+ MEDUSA_BUILD_PLACEHOLDERS,
3876
+ requireEnvironment,
3877
+ resolveRedisEventBus,
3878
+ type ConfigurationPhase,
3879
+ } from "@hartl-services/medusa-base/config"
3676
3880
 
3677
- module.exports = defineShopConfig({
3678
- displayName: "EXAMPLE SHOP",
3679
- orderNotificationEmail: "orders@example.com",
3680
- timeZone: "Europe/Vienna",
3681
- storefrontCountryCode: "de",
3682
- })
3881
+ const phase: ConfigurationPhase =
3882
+ process.env.MEDUSA_BUILD_PHASE === "true" ? "build" : "runtime"
3883
+ const env = requireEnvironment(
3884
+ process.env,
3885
+ ["DATABASE_URL", "JWT_SECRET", "COOKIE_SECRET"],
3886
+ phase,
3887
+ { ...MEDUSA_BUILD_PLACEHOLDERS /* , ...other plugins' placeholders */ },
3888
+ )
3889
+ const redis = resolveRedisEventBus(process.env, phase)
3683
3890
  ```
3684
3891
 
3685
- A shop that has to adjust the result (a test harness, for example) builds the
3686
- plain input, changes it and calls `defineConfig` itself:
3892
+ ### Sentry logger wrapper
3893
+
3894
+ `@hartl-services/medusa-base/sentry-logger` exports `withSentryErrorReporting(logger)`,
3895
+ formerly published as `@hartl-services/medusa-sentry-logger`. Medusa's HTTP
3896
+ error handler catches thrown route and workflow errors and only logs them, so
3897
+ Sentry's default Node integrations never see them; wrapping the logger reports
3898
+ every `logger.error(...)` and every `Error` passed to `logger.warn(...)` (at
3899
+ level `warning`). It is a plain library import for `medusa-config.ts`, not a
3900
+ Medusa plugin feature, and needs `@sentry/node` (an optional peer dependency):
3687
3901
 
3688
3902
  ```ts
3689
- import { defineConfig } from "@medusajs/framework/utils"
3690
- import { buildShopConfig } from "@hartl-services/medusa-base/shop-config"
3903
+ import { logger } from "@medusajs/framework/logger";
3904
+ import { withSentryErrorReporting } from "@hartl-services/medusa-base/sentry-logger";
3691
3905
 
3692
- const config = buildShopConfig(options)
3693
- config.plugins = config.plugins.filter(
3694
- (plugin) => plugin.resolve !== "@hartl-services/medusa-batch-management",
3695
- )
3696
- module.exports = defineConfig(config)
3906
+ module.exports = defineConfig({
3907
+ logger: withSentryErrorReporting(logger),
3908
+ // ...
3909
+ });
3697
3910
  ```
3698
3911
 
3699
- ### API
3700
-
3701
- - `defineShopConfig(options, environment = process.env)` returns
3702
- `defineConfig(buildShopConfig(options, environment))`.
3703
- - `buildShopConfig(options, environment = process.env)` returns the plain
3704
- config input (`admin`, `logger`, `projectConfig`, `modules`, `plugins`)
3705
- before `defineConfig`. `modules` and `plugins` are ordinary arrays.
3706
- - Types: `ShopConfigOptions`, `ShopConfigInput`, `ShopPlugin`,
3707
- `ShopModule`.
3912
+ Initialize Sentry in `instrumentation.ts` as usual; without a `SENTRY_DSN` the
3913
+ wrapper is inert.
3708
3914
 
3709
- Both functions validate the options first and throw a `TypeError` naming the
3710
- option (`Shop option "timeZone" must be ...`), so a misconfigured shop
3711
- fails at boot. They then load the `.env` file (unless disabled) and read and
3712
- validate the environment; a missing variable fails with the variable's name.
3915
+ ### Sentry instrumentation
3713
3916
 
3714
- ### Options
3917
+ `@hartl-services/medusa-base/instrumentation` exports `register()`, the hook
3918
+ Medusa's start command calls from the shop's `instrumentation.ts`. It runs
3919
+ `Sentry.init` with `SENTRY_DSN`, `SENTRY_ENVIRONMENT` (fallback `NODE_ENV`)
3920
+ and `tracesSampleRate: 0`; without a `SENTRY_DSN` Sentry stays inert. The
3921
+ shop's `instrumentation.ts` is one line:
3715
3922
 
3716
- | Option | Meaning |
3717
- | --- | --- |
3718
- | `displayName` | Shop name in the default e-mail templates (a shop's initial seed may use it as store name). Company name, address and UID are no options: documents and tax compliance read them only from the Shop-Stammdaten (Admin, "Hartl Services Plugins"). |
3719
- | `orderNotificationEmail` | Receives order notification e-mails; must look like an e-mail address. |
3720
- | `timeZone` | IANA time zone (validated with `Intl`). Chooses the annual document-number period, the printed dates on documents and the affiliate consignment reminder time zone. |
3721
- | `storefrontCountryCode` | 2-letter lowercase country code of the storefront route. Links: `<STOREFRONT_URL>/<cc>/partnerportal` (affiliate portal), `<STOREFRONT_URL>/<cc>/reset-password` and `<STOREFRONT_URL>/<cc>/verify-account` (the e-mail plugin appends `#token=`), referral base `<STOREFRONT_URL>/<cc>`. The Admin invite link is `<MEDUSA_BACKEND_URL>/app/invite?token=`. |
3722
- | `documentHtmlFactory` | Optional `DocumentHtmlFactory` that replaces the default document markup, typically `createDefaultDocumentHtmlFactory({ timeZone, theme })` from `/config` with the shop's CSS and watermark (see "Layout, logo, payment terms and shop theme"). Must be a function. |
3723
- | `loadEnvFile` | Default `true`: calls `loadEnv(NODE_ENV \|\| "development", process.cwd())`. Pass `false` when the environment is already complete (tests, isolated child processes). |
3923
+ ```ts
3924
+ export { register } from "@hartl-services/medusa-base/instrumentation"
3925
+ ```
3724
3926
 
3725
- ### Environment variables
3927
+ `@sentry/node` stays an optional peer dependency of the base: a shop that uses
3928
+ this export (or `sentry-logger`) lists `@sentry/node` in its own
3929
+ `dependencies`.
3726
3930
 
3727
- Required at runtime (during `medusa build`, with `MEDUSA_BUILD_PHASE=true`, the
3728
- plugins' build placeholders stand in for them):
3931
+ ## Development
3729
3932
 
3730
- - Medusa core: `DATABASE_URL`, `STORE_CORS`, `ADMIN_CORS`, `AUTH_CORS`,
3731
- `JWT_SECRET`, `COOKIE_SECRET`, `MEDUSA_BACKEND_URL`, `STOREFRONT_URL` (plain
3732
- HTTP origins are accepted only in development and test).
3733
- - Redis: `REDIS_URL` (optional in development, where Medusa's local event bus
3734
- is used; required elsewhere, `rediss://` with authentication in production
3735
- unless `REDIS_ALLOW_INSECURE_PRIVATE_NETWORK=true`). Never used during a
3736
- build.
3737
- - SMTP: `SMTP_HOST`, `SMTP_PORT`, `SMTP_SECURE`, `SMTP_USER`, `SMTP_PASSWORD`,
3738
- `SMTP_FROM`, `SMTP_REQUIRE_TLS` (outside development and test either
3739
- `SMTP_SECURE` or `SMTP_REQUIRE_TLS` must be `true`).
3740
- - Stripe: `STRIPE_API_KEY` (`sk_...`), `STRIPE_WEBHOOK_SECRET` (`whsec_...`).
3741
- - S3: `S3_CACHE_STORAGE_BUCKET`, `S3_CACHE_STORAGE_REGION`.
3742
- - Documents: `DOCUMENT_PDF_EXECUTABLE`.
3933
+ ### Local e-mail development
3743
3934
 
3744
- Optional: `S3_CACHE_STORAGE_ENDPOINT`, `S3_CACHE_STORAGE_FORCE_PATH_STYLE`,
3745
- `S3_CACHE_STORAGE_PREFIX`, `S3_CACHE_STORAGE_ACCESS_KEY_ID`,
3746
- `S3_CACHE_STORAGE_SECRET_ACCESS_KEY`, `S3_CACHE_STORAGE_SESSION_TOKEN` (omit
3747
- the keys to use the AWS credential chain), `S3_CACHE_STORAGE_CACHE_DIR`
3748
- (default `./.medusa-s3-cache`), `S3_CACHE_STORAGE_TTL_MS`,
3749
- `S3_CACHE_STORAGE_CLEANUP_INTERVAL_MS`,
3750
- `S3_CACHE_STORAGE_EXPIRATION_STRATEGY`,
3751
- `FILE_GATEWAY_PRIVATE_DOWNLOAD_DURATION_SECONDS`, and `SENTRY_DSN` /
3752
- `SENTRY_ENVIRONMENT` for error tracking (see the base's
3753
- `/instrumentation`).
3935
+ For local development, provide the seven SMTP variables and omit `REDIS_URL` to
3936
+ use Medusa's local Event Bus. Set `REDIS_URL` only when intentionally exercising
3937
+ the direct Redis provider. The backend HTTP runner ignores ambient Redis and
3938
+ SMTP settings and injects an isolated Redis container, unique queue, and
3939
+ token-protected SMTP fixture for every child.
3754
3940
 
3755
- ### Fixed configuration
3941
+ ### Build output of `/admin-components`
3756
3942
 
3757
- Not configurable on purpose: the plugin order (base first), the Redis event
3758
- bus job policy (3 attempts, exponential backoff from 1 s), S3 cache defaults
3759
- (15 min cleanup, 1 h TTL, absolute expiration), Stripe immediate capture with
3760
- automatic payment methods, the 24 h session lifetime, Admin path `/app`,
3761
- consignment reminders due on day 10, the Documents module always being part of the base, and
3762
- `http.authVerificationsPerActor.customer` (a getter that follows the E-Mail `customer_verification` toggle, see
3763
- "Kontobestätigung per E-Mail"), `http.authMethodsPerActor` (customers: a getter that follows the `customer_auth` switches; Admin users: `["emailpass"]`) and the auth module entry
3764
- (providers `emailpass` and `email-otp`, the latter with `hmacSecret` = `JWT_SECRET`, the same value as `http.jwtSecret`; see
3765
- "Kunden-Login").
3943
+ `/admin-components` is built (after `medusa plugin:build`) into
3944
+ `.medusa/server/src/admin/admin-components`, not into `dist`: the host Admin's
3945
+ Tailwind scans only `.medusa/server/src/admin/**` of a plugin, so the classes of
3946
+ the shared components are generated there. Do not move the outDir of
3947
+ `tsconfig.admin-components.json`.