@hartl-services/medusa-base 3.0.0 → 5.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (107) hide show
  1. package/.medusa/server/src/admin/admin-components/features/plugin-features/components/PluginActionsSection.d.ts +1 -1
  2. package/.medusa/server/src/admin/admin-components/features/plugin-features/components/PluginFeaturesCard.d.ts +1 -1
  3. package/.medusa/server/src/admin/admin-components/features/plugin-features/components/PluginHubCard.d.ts +1 -1
  4. package/.medusa/server/src/admin/admin-components/features/plugin-features/components/PluginSettingsForm.d.ts +1 -1
  5. package/.medusa/server/src/admin/admin-components/features/plugin-features/components/PluginStatusChips.d.ts +1 -1
  6. package/.medusa/server/src/admin/admin-components/features/plugin-features/components/ShopMasterDataBanner.d.ts +1 -1
  7. package/.medusa/server/src/admin/admin-components/features/plugin-features/hooks/usePluginFeatureEnabled.d.ts +1 -1
  8. package/.medusa/server/src/admin/admin-components/features/plugin-features/hooks/usePluginSettingsForm.d.ts +1 -1
  9. package/.medusa/server/src/admin/admin-components/features/plugin-features/hooks/usePluginSwitch.d.ts +1 -1
  10. package/.medusa/server/src/admin/admin-components/features/plugin-features/lib/action-runs.d.ts +1 -1
  11. package/.medusa/server/src/admin/admin-components/features/plugin-features/lib/plugin-features-admin-client.d.ts +1 -1
  12. package/.medusa/server/src/admin/admin-components/features/plugin-features/lib/plugin-features-admin-client.js +1 -1
  13. package/.medusa/server/src/admin/admin-components/features/plugin-features/lib/plugin-view.d.ts +1 -1
  14. package/.medusa/server/src/admin/admin-components/features/plugin-features/lib/settings-draft.d.ts +1 -1
  15. package/.medusa/server/src/admin/admin-components/features/plugin-features/lib/settings-layout.d.ts +1 -1
  16. package/.medusa/server/src/admin/index.js +51 -51
  17. package/.medusa/server/src/admin/index.mjs +51 -51
  18. package/.medusa/server/src/api/admin/draft-orders/[id]/order-confirmation/route.js +2 -2
  19. package/.medusa/server/src/api/admin/email/attachments/[id]/mail-kinds/route.js +2 -2
  20. package/.medusa/server/src/api/admin/email/attachments/[id]/route.js +2 -2
  21. package/.medusa/server/src/api/admin/email/attachments/[id]/versions/route.js +2 -2
  22. package/.medusa/server/src/api/admin/email/attachments/route.js +2 -2
  23. package/.medusa/server/src/api/admin/email/messages/[id]/resends/route.js +2 -2
  24. package/.medusa/server/src/api/admin/email/messages/[id]/route.js +2 -2
  25. package/.medusa/server/src/api/admin/email/messages/route.js +2 -2
  26. package/.medusa/server/src/api/admin/email/templates/[kind]/preview/route.js +2 -2
  27. package/.medusa/server/src/api/admin/email/templates/[kind]/route.js +2 -2
  28. package/.medusa/server/src/api/admin/email/templates/[kind]/send-test/route.js +2 -2
  29. package/.medusa/server/src/api/admin/email/templates/[kind]/test-sources/route.js +2 -2
  30. package/.medusa/server/src/api/admin/email/templates/route.js +2 -2
  31. package/.medusa/server/src/api/admin/plugin-features/[plugin_key]/actions/[action_key]/route.js +2 -2
  32. package/.medusa/server/src/api/admin/plugin-features/[plugin_key]/actions/[action_key]/runs/route.js +2 -2
  33. package/.medusa/server/src/api/admin/plugin-features/[plugin_key]/route.js +2 -2
  34. package/.medusa/server/src/api/admin/plugin-features/[plugin_key]/settings/route.js +2 -2
  35. package/.medusa/server/src/api/admin/plugin-features/route.js +2 -2
  36. package/.medusa/server/src/api/admin-first-login-password-policy.js +2 -2
  37. package/.medusa/server/src/api/answer-code-provider-route-not-found.js +2 -2
  38. package/.medusa/server/src/api/check-customer-address-tax-identification.js +3 -3
  39. package/.medusa/server/src/api/code-account-password-routes.js +2 -2
  40. package/.medusa/server/src/api/middlewares.js +9 -9
  41. package/.medusa/server/src/api/password-policy.js +2 -2
  42. package/.medusa/server/src/api/plugin-actions.js +2 -2
  43. package/.medusa/server/src/api/plugin-features-http.js +2 -2
  44. package/.medusa/server/src/api/require-login-code-customer-email.js +2 -2
  45. package/.medusa/server/src/api/store/carts/[id]/completion-check/route.js +2 -2
  46. package/.medusa/server/src/api/store/carts/[id]/tax-identification/route.js +2 -2
  47. package/.medusa/server/src/api/store/customer-auth/password-policy/route.js +2 -2
  48. package/.medusa/server/src/api/store/plugin-features/[plugin_key]/route.js +2 -2
  49. package/.medusa/server/src/api/store/plugin-features/route.js +2 -2
  50. package/.medusa/server/src/customer-auth/application/prepare-code-account-password-reset.js +2 -2
  51. package/.medusa/server/src/customer-auth/application/request-login-code.js +2 -2
  52. package/.medusa/server/src/customer-auth/customer-password-policy.js +2 -2
  53. package/.medusa/server/src/customer-auth/domain/auth-methods.js +2 -2
  54. package/.medusa/server/src/customer-auth/domain/login-code.js +2 -2
  55. package/.medusa/server/src/customer-auth/login-code-state.js +2 -2
  56. package/.medusa/server/src/customer-auth/plugin-features.js +2 -2
  57. package/.medusa/server/src/documents/paths/documents.js +2 -2
  58. package/.medusa/server/src/documents/schemas/documents.js +2 -2
  59. package/.medusa/server/src/email/application/deliver-login-code.js +2 -2
  60. package/.medusa/server/src/email/domain/business-keys.js +2 -2
  61. package/.medusa/server/src/email/templates/blocks/render-document.js +2 -2
  62. package/.medusa/server/src/email/templates/render.js +2 -2
  63. package/.medusa/server/src/email/templates/validate.js +2 -2
  64. package/.medusa/server/src/email/types/template-data.js +2 -2
  65. package/.medusa/server/src/free-shipping/free-shipping-settings.js +38 -0
  66. package/.medusa/server/src/free-shipping/plugin-features.js +45 -0
  67. package/.medusa/server/src/jobs/free-shipping-promotion-sync.js +22 -0
  68. package/.medusa/server/src/modules/documents/service.js +2 -2
  69. package/.medusa/server/src/modules/email/domain/attachment.js +2 -2
  70. package/.medusa/server/src/modules/email/service.js +3 -3
  71. package/.medusa/server/src/modules/free-shipping/index.js +13 -0
  72. package/.medusa/server/src/modules/free-shipping/migrations/Migration20260911114515.js +16 -0
  73. package/.medusa/server/src/modules/free-shipping/migrations/Migration20260928201511.js +19 -0
  74. package/.medusa/server/src/modules/free-shipping/migrations/Migration20260928211756.js +110 -0
  75. package/.medusa/server/src/modules/free-shipping/service.js +17 -0
  76. package/.medusa/server/src/modules/tax-compliance/service.js +2 -2
  77. package/.medusa/server/src/providers/email-otp/service.js +2 -2
  78. package/.medusa/server/src/public/shop-config.js +6 -6
  79. package/.medusa/server/src/shop-config/{build-food-shop-config.js → build-shop-config.js} +14 -18
  80. package/.medusa/server/src/shop-config/options.js +4 -4
  81. package/.medusa/server/src/subscribers/affiliate-application-submitted.js +2 -2
  82. package/.medusa/server/src/subscribers/affiliate-consignment-reminder-due.js +2 -2
  83. package/.medusa/server/src/subscribers/documents-order-cancellation-ready.js +2 -2
  84. package/.medusa/server/src/subscribers/documents-refund-ready.js +2 -2
  85. package/.medusa/server/src/subscribers/documents-shipment-invoice-assignment-decided.js +2 -2
  86. package/.medusa/server/src/subscribers/free-shipping-plugin-features-updated.js +25 -0
  87. package/.medusa/server/src/subscribers/free-shipping-plugin-settings-updated.js +26 -0
  88. package/.medusa/server/src/subscribers/packing-list-allocation-revised.js +2 -2
  89. package/.medusa/server/src/subscribers/tax-compliance-plugin-features-updated.js +2 -2
  90. package/.medusa/server/src/tax-compliance/lib/tax-compliance-state.js +2 -2
  91. package/.medusa/server/src/tax-compliance/plugin-features.js +2 -2
  92. package/.medusa/server/src/workflows/free-shipping/sync-free-shipping-promotions.js +143 -0
  93. package/.medusa/server/src/workflows/hooks/set-free-shipping-promotion-context.js +86 -0
  94. package/README.md +754 -562
  95. package/dist/customer-auth/domain/auth-methods.js +1 -1
  96. package/dist/customer-auth/plugin-features.js +1 -1
  97. package/dist/documents/types/admin-api.d.ts +1 -1
  98. package/dist/documents/types/document-summary.d.ts +1 -1
  99. package/dist/documents/types/store-api.d.ts +1 -1
  100. package/dist/email/types/template-data.js +1 -1
  101. package/dist/public/shop-config.d.ts +4 -4
  102. package/dist/public/shop-config.js +5 -5
  103. package/dist/shop-config/{build-food-shop-config.d.ts → build-shop-config.d.ts} +12 -12
  104. package/dist/shop-config/{build-food-shop-config.js → build-shop-config.js} +13 -17
  105. package/dist/shop-config/options.d.ts +3 -3
  106. package/dist/shop-config/options.js +3 -3
  107. package/package.json +6 -6
package/README.md CHANGED
@@ -1,75 +1,128 @@
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 food 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
 
71
124
  ```bash
72
- pnpm add @hartl-services/medusa-base @hartl-services/medusa-food-supplements-contracts
125
+ pnpm add @hartl-services/medusa-base @hartl-services/medusa-contracts
73
126
  ```
74
127
 
75
128
  ```ts
@@ -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,12 +212,158 @@ 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
- Peer dependencies are the host's Medusa 2.19 packages (`@medusajs/framework`,
137
- `@medusajs/medusa`, `@medusajs/admin-sdk`, `@medusajs/js-sdk`, `@medusajs/ui`, `@medusajs/icons`)
138
- plus React, React Router and TanStack Query in the versions the Medusa 2.19
139
- dashboard uses.
215
+ ### Upgrading to 4.0.0
216
+
217
+ Renames: the Contracts package is
218
+ `@hartl-services/medusa-contracts` (1.0.0, formerly
219
+ `@hartl-services/medusa-food-supplements-contracts`; same subpaths), and the
220
+ Shop-Config-Preset exports are `buildShopConfig`, `defineShopConfig`,
221
+ `ShopConfigOptions`, `ShopConfigInput`, `ShopPlugin` and `ShopModule`
222
+ (formerly `buildFoodShopConfig` etc.). Replace the import names in
223
+ `medusa-config.ts` and the Contracts package name in the shop and its
224
+ Storefront; nothing else changes. The other Hartl Services plugins need the
225
+ versions released with 4.0.0 (peer range `^4`).
226
+
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
140
361
 
141
- ## 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
142
367
 
143
368
  | Method | Path | Auth | Response |
144
369
  |---|---|---|---|
@@ -174,8 +399,8 @@ Store values are effective values only; a storefront uses them to hide
174
399
  features. DTOs, schemas and paths come from the Contracts package:
175
400
 
176
401
  ```ts
177
- import type { StorePluginFeaturesListResponse } from "@hartl-services/medusa-food-supplements-contracts/types/plugin-features-store"
178
- import { pluginFeaturesStorePaths } from "@hartl-services/medusa-food-supplements-contracts/paths/plugin-features"
402
+ import type { StorePluginFeaturesListResponse } from "@hartl-services/medusa-contracts/types/plugin-features-store"
403
+ import { pluginFeaturesStorePaths } from "@hartl-services/medusa-contracts/paths/plugin-features"
179
404
 
180
405
  const { plugins } = await sdk.client.fetch<StorePluginFeaturesListResponse>(
181
406
  pluginFeaturesStorePaths.get.list,
@@ -183,7 +408,7 @@ const { plugins } = await sdk.client.fetch<StorePluginFeaturesListResponse>(
183
408
  )
184
409
  ```
185
410
 
186
- ## Declaring features in a plugin
411
+ ### Declaring features in a plugin
187
412
 
188
413
  A plugin depends on `@hartl-services/medusa-base` (peer dependency), declares
189
414
  its features once and exposes the declaration as `pluginFeatures` on its
@@ -223,7 +448,7 @@ base's own shop master data): the Admin shows no plugin switch, `enabled` is
223
448
  always `true` whatever is stored, and `POST /admin/plugin-features` with
224
449
  `enabled: false` is 400. It cannot be combined with `default_enabled: false`.
225
450
 
226
- ### Presentation fields for the Admin page
451
+ #### Presentation fields for the Admin page
227
452
 
228
453
  All optional; the Admin response always carries them (`null`, `[]` or
229
454
  `"full"` when not declared). Texts are non-empty trimmed strings;
@@ -266,7 +491,7 @@ All optional; the Admin response always carries them (`null`, `[]` or
266
491
  share a two-column row; not for `boolean`) and `section` (sub-heading inside
267
492
  the group, shared by consecutive settings with the same value).
268
493
 
269
- ### Gating routes
494
+ #### Gating routes
270
495
 
271
496
  Put gate entries at the start of the plugin's `routes` array. An entry without
272
497
  `methods` is a prefix match and runs before every method-specific middleware:
@@ -293,7 +518,7 @@ export default defineMiddlewares({
293
518
  A disabled plugin or feature answers 404. With several keys, all must be
294
519
  enabled. The toggles are read once per request.
295
520
 
296
- ### Gating subscribers and jobs
521
+ #### Gating subscribers and jobs
297
522
 
298
523
  Return early:
299
524
 
@@ -314,7 +539,7 @@ To check another plugin without importing it, use its plugin key:
314
539
  the descriptor among the registered modules; an unknown or uninstalled plugin
315
540
  key (and an undeclared feature key) is `false`.
316
541
 
317
- ### Toggle-change event
542
+ #### Toggle-change event
318
543
 
319
544
  After a successful `POST /admin/plugin-features` the base emits
320
545
  `PLUGIN_FEATURES_UPDATED_EVENT` (`plugin-features.updated.v1`) through the
@@ -332,7 +557,7 @@ subscribes to it and filters on its own `plugin_key`:
332
557
  import {
333
558
  PLUGIN_FEATURES_UPDATED_EVENT,
334
559
  PluginFeaturesUpdatedEventSchema,
335
- } from "@hartl-services/medusa-food-supplements-contracts/events/plugin-features"
560
+ } from "@hartl-services/medusa-contracts/events/plugin-features"
336
561
 
337
562
  export default async function onToggleChange({ event, container }: SubscriberArgs<unknown>) {
338
563
  const payload = PluginFeaturesUpdatedEventSchema.parse(event.data)
@@ -346,7 +571,7 @@ export const config: SubscriberConfig = { event: PLUGIN_FEATURES_UPDATED_EVENT }
346
571
  Events can arrive late or twice; re-read the current toggles instead of
347
572
  trusting the payload for anything that writes.
348
573
 
349
- ### Never on the cart path
574
+ #### Never on the cart path
350
575
 
351
576
  Code on the cart and checkout hot path (cart middlewares, cart workflow hooks,
352
577
  `orderCreated` handling, cart subscribers) must never read toggles. It stays
@@ -356,7 +581,7 @@ existing data. Login is not the cart path: the E-Mail plugin's
356
581
  `customer_verification` toggle does switch customer logins, through an
357
582
  in-memory copy of the toggle (see "Kontobestätigung per E-Mail").
358
583
 
359
- ## Runtime settings
584
+ ### Runtime settings
360
585
 
361
586
  Configuration a plugin reads at RUN time (for example an external system's
362
587
  URL and API key) is declared in the descriptor and edited on the plugin's
@@ -450,7 +675,7 @@ class DemoModuleService extends MedusaService({ /* models */ }) {
450
675
  (`plugin-settings.updated.v1`) with `{ plugin_key, version, changed_keys }`
451
676
  (no values). Direct service calls emit nothing.
452
677
 
453
- ### Secrets
678
+ #### Secrets
454
679
 
455
680
  Secrets are stored AES-256-GCM encrypted (`{ enc: "<base64 iv|tag|ciphertext>" }`
456
681
  in `plugin_setting_state.values`) with a key derived by HKDF-SHA256 from the
@@ -465,7 +690,7 @@ applies in the meantime. Secrets saved by an unpublished development build
465
690
  before the key binding existed count as "nicht gesetzt" the same way and must
466
691
  be entered again.
467
692
 
468
- ## Actions
693
+ ### Actions
469
694
 
470
695
  A plugin declares actions as registered workflow ids; the base runs them from
471
696
  the plugin's Admin page:
@@ -519,7 +744,7 @@ Admin user id, `run_id` the recorded run).
519
744
  workflow is not registered is 500 (`UNEXPECTED_STATE` naming the workflow,
520
745
  no run is recorded).
521
746
 
522
- ### Reporting progress
747
+ #### Reporting progress
523
748
 
524
749
  A long-running action reports progress from its workflow; the Admin shows it
525
750
  as a bar ("current / total") with the label:
@@ -548,7 +773,7 @@ characters. Without a `run_id` (the same workflow run from a script) the
548
773
  helper does nothing; invalid progress, an unknown or finished run and a
549
774
  failed write are logged as a warning and dropped, and never fail the action.
550
775
 
551
- ## Shop-Stammdaten
776
+ ### Shop-Stammdaten
552
777
 
553
778
  The base declares its own descriptor on its module service
554
779
  (`plugin_key: "shop"`, label "Shop-Stammdaten", `switchable: false`, no
@@ -616,7 +841,7 @@ Documents plugin into the appearance: the Documents logo, else the E-Mail
616
841
  logo; a `logo` stored in the Shop-Stammdaten by an earlier release is kept
617
842
  only when neither plugin had one.
618
843
 
619
- ## Admin pages
844
+ ### Admin pages
620
845
 
621
846
  - `/settings/hartl-services` (sidebar entry "Hartl Services Plugins") is the
622
847
  hub: a banner for the Shop-Stammdaten (icon, summary, "Nicht konfiguriert"
@@ -645,7 +870,7 @@ only when neither plugin had one.
645
870
  rejects a path outside `/settings/hartl-services/` or under
646
871
  `/settings/hartl-services/plugin/`.
647
872
 
648
- ### Admin components for plugin pages
873
+ #### Admin components for plugin pages
649
874
 
650
875
  `@hartl-services/medusa-base/admin-components` (ESM for the Admin's vite
651
876
  build, with types) exports:
@@ -744,7 +969,7 @@ The consuming plugin lists `@hartl-services/medusa-base` in its
744
969
  external and the shop's Admin resolves one shared copy. The components use
745
970
  the host Admin's React, React Query, React Router and `@medusajs/ui`.
746
971
 
747
- #### Rich-text editor
972
+ ##### Rich-text editor
748
973
 
749
974
  `useRichTextEditor(config, initialContent, deps)` returns the TipTap
750
975
  `Editor | null` (`null` until it exists), `<RichTextEditor editor
@@ -837,7 +1062,7 @@ policy (`link.isAllowedHref`) are read on every use and always see the latest
837
1062
  render, so they need no stable reference. Pass `[]` for an editor that lives as
838
1063
  long as its component.
839
1064
 
840
- ### Server helpers
1065
+ #### Server helpers
841
1066
 
842
1067
  `@hartl-services/medusa-base/lib` (server and isomorphic code; a separate
843
1068
  subpath because unit tests mock `@hartl-services/medusa-base` wholesale)
@@ -883,13 +1108,7 @@ The main entry (`@hartl-services/medusa-base`) additionally exports, next to
883
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` |
884
1109
  | `isBasePluginRegistered(container)` / `resolveBaseModule(container)` | Whether the base module is registered (boot-time check) / its service, or `UNEXPECTED_STATE` |
885
1110
 
886
- `/admin-components` is built (after `medusa plugin:build`) into
887
- `.medusa/server/src/admin/admin-components`, not into `dist`: the host Admin's
888
- Tailwind scans only `.medusa/server/src/admin/**` of a plugin, so the classes of
889
- the shared components are generated there. Do not move the outDir of
890
- `tsconfig.admin-components.json`.
891
-
892
- ## Semantics
1111
+ ### Semantics
893
1112
 
894
1113
  - No stored row: every value is its declared default, `version` is 0.
895
1114
  - `effective(feature) = plugin enabled && configured(feature) && effective(parent)`.
@@ -901,7 +1120,7 @@ the shared components are generated there. Do not move the outDir of
901
1120
  value updates exactly that version. A concurrent change answers 409; the
902
1121
  Admin page reloads and shows a notice.
903
1122
 
904
- ## Complete-cart hook composition
1123
+ ### Complete-cart hook composition
905
1124
 
906
1125
  **Why.** Medusa 2.19.0 allows exactly one handler per workflow hook; a second
907
1126
  registration throws "Cannot define multiple hook handlers" at boot. Several
@@ -986,42 +1205,12 @@ Storefront calls it right before confirming a payment. Plugins that guard
986
1205
  `/store/carts/:id/complete` with a middleware register it for this route too
987
1206
  (the affiliate plugin's registered-email gate does).
988
1207
 
1208
+ ## Built-in features
989
1209
 
990
- ## Sentry logger wrapper
991
-
992
- `@hartl-services/medusa-base/sentry-logger` exports `withSentryErrorReporting(logger)`,
993
- formerly published as `@hartl-services/medusa-sentry-logger`. Medusa's HTTP
994
- error handler catches thrown route and workflow errors and only logs them, so
995
- Sentry's default Node integrations never see them; wrapping the logger reports
996
- every `logger.error(...)` and every `Error` passed to `logger.warn(...)` (at
997
- level `warning`). It is a plain library import for `medusa-config.ts`, not a
998
- Medusa plugin feature, and needs `@sentry/node` (an optional peer dependency):
999
-
1000
- ```ts
1001
- import { logger } from "@medusajs/framework/logger";
1002
- import { withSentryErrorReporting } from "@hartl-services/medusa-base/sentry-logger";
1003
-
1004
- module.exports = defineConfig({
1005
- logger: withSentryErrorReporting(logger),
1006
- // ...
1007
- });
1008
- ```
1009
-
1010
- Initialize Sentry in `instrumentation.ts` as usual; without a `SENTRY_DSN` the
1011
- wrapper is inert.
1012
-
1013
- ## Root redirect
1014
-
1015
- Medusa serves nothing at `/`, so a deployed domain answers it with a 404. The
1016
- base registers `GET /` and answers with a `302` to the shop's `admin.path`
1017
- (read from the config module, fallback `/app`), so opening the bare domain
1018
- lands on the admin dashboard in every shop and in local development without a
1019
- reverse-proxy rule. The redirect is temporary on purpose: browsers cache a
1020
- `301` on the bare domain essentially forever, which would be painful to undo
1021
- should `/` ever get real content. A shop that needs its own `/` defines
1022
- `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.
1023
1212
 
1024
- ## Kunden-Login
1213
+ ### Kunden-Login
1025
1214
 
1026
1215
  Module `customer_auth` (plugin key `customer_auth`, label "Kunden-Login")
1027
1216
  decides which login methods customers may use. The plugin cannot be switched
@@ -1218,233 +1407,57 @@ Storefront contract: DTOs, schemas and paths come from the Contracts package
1218
1407
  read `GET /store/plugin-features/customer_auth` and render only the enabled
1219
1408
  forms.
1220
1409
 
1221
- ## Password policy
1410
+ ### E-Mail
1222
1411
 
1223
- Medusa's `emailpass` provider accepts any non-empty password. The base runs a
1224
- middleware on `POST /auth/:actor_type/emailpass/register` (customer sign-up,
1225
- Admin invite) and `POST /auth/:actor_type/emailpass/update` (password reset).
1226
- It is registered on `/auth/:actor_type/:auth_provider/:action` without
1227
- `methods` and decides on the decoded, lowercased params, because Express
1228
- matches literal path segments against the raw path: `/auth/customer/email%70ass/register`
1229
- and `/auth/Customer/emailpass/register` reach Medusa's handler too, and
1230
- `Customer` counts as the customer actor here.
1231
- A password that breaks the rules of the actor is answered with
1232
- `400 { type: "invalid_data", message }`; the one German message lists every
1233
- unmet rule, e.g. `Das Passwort erfüllt nicht alle Regeln: mindestens 12
1234
- Zeichen, eine Ziffer.` The check runs before Medusa's reset-token check, so a
1235
- 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`.
1236
1418
 
1237
- **Admin users** (every actor type except `customer`) always need at least 15
1238
- characters, counted as Unicode code points, and no composition rules. This
1239
- follows NIST SP 800-63B Rev. 4 (the US guideline for digital identity) for a
1240
- password that is the only factor.
1419
+ #### Configuration
1241
1420
 
1242
- **Customers** follow the settings of the "Kunden-Login" plugin (group
1243
- "Passwort-Regeln", Settings > "Hartl Services Plugins"). They apply only while
1244
- `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:
1245
1426
 
1246
- | Key | Label | Default | Meaning |
1247
- | ----------------------------- | ------------------------------------ | ------- | ------- |
1248
- | `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 |
1249
- | `password_require_mixed_case` | Groß- und Kleinbuchstaben verlangen | off | at least one uppercase and one lowercase letter |
1250
- | `password_require_digit` | Ziffer verlangen | off | at least one digit |
1251
- | `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";
1252
1429
 
1253
- NIST SP 800-63B Rev. 4 advises against composition rules, because they lead to
1254
- predictable passwords such as "Passwort1!"; a longer password protects better.
1255
- The Admin texts say so. A missing or invalid stored value falls back to the
1256
- default of that rule, and if the settings cannot be read at all the default
1257
- policy (15 characters, no composition rules) applies and a warning is logged,
1258
- so a registration is never blocked by a settings outage.
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
+ ```
1259
1459
 
1260
- **Route.** `GET /store/customer-auth/password-policy` (publishable key only)
1261
- answers `{ password_policy: { min_length, require_mixed_case, require_digit,
1262
- require_symbol } }`, the effective customer rules, and 404 while
1263
- `email_password` is off. The storefront shows them in its forms and checks
1264
- them with the same functions as the backend. The types, the path
1265
- (`customerAuthStorePaths.get.passwordPolicy`) and the rules
1266
- (`evaluatePasswordPolicy`, `isPasswordAllowed`, `DEFAULT_CUSTOMER_PASSWORD_POLICY`,
1267
- `ADMIN_PASSWORD_POLICY` in `schemas/password-policy`) come from the Contracts
1268
- package.
1269
-
1270
- **Admin passwords and the shared identity.** An `emailpass` identity is shared
1271
- by all actor types: someone invited as an Admin could register through
1272
- `/auth/customer/emailpass/register` with the (weaker) customer rules, log in at
1273
- `/auth/user/emailpass` and accept the invite with that token, which would bind
1274
- the weak password to an Admin. So the Admin login (`POST` and `GET`
1275
- `/auth/user/emailpass`, decoded and lowercased like above) checks the body
1276
- password against the Admin rules and answers the same `400` when the identity
1277
- has no Admin yet (`app_metadata.user_id` unset). The login of an identity that
1278
- already belongs to an Admin is never checked. Remaining limit, deliberately not
1279
- closed: a customer could register through the Admin path with 15 characters and
1280
- no composition rules although the shop asks for more; that only weakens their
1281
- own password.
1282
-
1283
- Login of a customer is not checked, so existing passwords keep working until their next
1284
- reset, whatever the rules are. Accounts created without HTTP (`medusa user`,
1285
- seed scripts calling the Auth module) are not checked either.
1286
-
1287
- ## Cart-promotion link repair
1288
-
1289
- Medusa's `deletePromotionsWorkflow` only soft-deletes a promotion and leaves
1290
- its `cart_promotion` link rows behind, so every cart that had it applied
1291
- reads back a `null` entry inside `cart.promotions`. The base removes those
1292
- links itself:
1293
-
1294
- - The `deletePromotionsWorkflow.hooks.promotionsDeleted` hook
1295
- (`workflows/hooks/dismiss-deleted-promotion-cart-links`) dismisses the
1296
- links of the deleted promotions and restores them if a later step fails.
1297
- - The migration script `dismiss-orphaned-cart-promotion-links` runs once in
1298
- `medusa db:migrate` and removes the links that already dangle from earlier
1299
- deletions. Medusa tracks migration scripts by file name in
1300
- `script_migrations`, so the name must not change.
1301
-
1302
- Nothing to configure; `medusa db:migrate` after installing or upgrading to
1303
- 0.4.1 is enough.
1304
-
1305
- ## Draft orders and 0 € orders
1306
-
1307
- Draft orders (Medusa's `@medusajs/draft-order` plugin, loaded by default) are
1308
- the shop's way to enter an order in the Admin: sample shipments, orders taken
1309
- by phone, replacements. Item prices and the shipping amount are set directly
1310
- in the draft; converting it never involves a payment provider.
1311
-
1312
- - **Customer order confirmation off by default.** The
1313
- `createOrderWorkflow.hooks.orderCreated` hook
1314
- (`workflows/hooks/draft-order-confirmation-default`) sets Medusa's
1315
- `no_notification` on every new draft, also when an API caller sent
1316
- `no_notification_order: false`. The Admin switches it per draft in the
1317
- **Bestellbestätigung** widget on the draft's detail page
1318
- (`GET`/`POST /admin/draft-orders/:id/order-confirmation`, see the E-Mail
1319
- "Admin API"). On conversion the `order-placed` subscriber records the
1320
- customer confirmation as `suppressed` (`notifications_disabled`); the copy
1321
- to the shop's order address is still sent. The shipment mail follows the
1322
- Admin's "notify customer" choice per shipment, not this switch.
1323
- - **0 € orders count as paid.** Medusa derives an order's payment status from
1324
- its payment collections only, so a converted 0 € draft would stay
1325
- "Nicht bezahlt". The subscriber `order-zero-total-paid` runs
1326
- `order-mark-zero-total-paid` on `order.placed`: an order with a total of 0
1327
- and no payment collection gets a completed 0 € payment collection and reads
1328
- "Bezahlt" (`payment_status: captured`). Orders that already carry a
1329
- collection, i.e. every checkout order, are left alone. In SQL, a paid
1330
- order is one whose linked `payment_collection` (via
1331
- `order_payment_collection`) has `status = 'completed'`.
1332
-
1333
- Orders placed before installing this version keep their state.
1334
-
1335
- ## Config helpers
1336
-
1337
- `@hartl-services/medusa-base/config` holds the generic environment helpers a
1338
- shop's `medusa-config.ts` needs, so every shop fails at startup with the name
1339
- of the missing variable instead of a runtime crash. The environment helpers
1340
- have no dependency on Medusa; the subpath does not load `@sentry/node`.
1341
-
1342
- | Export | Purpose |
1343
- |---|---|
1344
- | `ConfigurationPhase` | `"build" \| "runtime"` |
1345
- | `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` |
1346
- | `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 |
1347
- | `optionalEnvironment`, `integerEnvironment`, `booleanEnvironment` | Trimmed optional string, range-checked integer, strict `"true"`/`"false"` boolean |
1348
- | `allowsInsecureLocalServices(environment, phase)` | `true` for build, development and test |
1349
- | `resolvePublicOrigin(value, name, allowHttp)`, `shopUrl(origin, path)` | Validate a bare HTTP(S) origin; build an absolute URL that cannot leave the origin |
1350
- | `resolveStripe(values)` | Checks the `sk_` key and the `whsec_` webhook secret |
1351
- | `resolveRedisEventBus(environment, phase)` | `{ url } \| undefined`: optional in development, required at runtime elsewhere, never used in a build, authenticated `rediss://` in production |
1352
- | `createEmailNotificationModuleConfig`, `defineShopEmailConfiguration` (deprecated: `createSmtpNotificationModuleConfig`, `resolveSmtp`, `SMTP_ENVIRONMENT_VARIABLES`, `SMTP_BUILD_PLACEHOLDERS`) | Mail notification provider and E-Mail configuration (see "E-Mail") |
1353
- | `createS3CacheStorageConfig`, `createS3CacheFileModuleConfig`, `resolveS3Credentials`, `resolveExpirationStrategy`, `S3_BUILD_PLACEHOLDERS` | S3 file storage (see "Dateispeicher (S3)") |
1354
- | `createDefaultDocumentHtmlFactory`, `DOCUMENTS_BUILD_PLACEHOLDERS` | Documents markup and build placeholder (see "Dokumente") |
1355
-
1356
- `medusa build` loads the configuration without deployment secrets. Each
1357
- plugin's own placeholders (SMTP, S3, PDF executable, ...) live with that
1358
- plugin; the shop merges them:
1359
-
1360
- ```ts
1361
- // medusa-config.ts
1362
- import {
1363
- MEDUSA_BUILD_PLACEHOLDERS,
1364
- requireEnvironment,
1365
- resolveRedisEventBus,
1366
- type ConfigurationPhase,
1367
- } from "@hartl-services/medusa-base/config"
1368
-
1369
- const phase: ConfigurationPhase =
1370
- process.env.MEDUSA_BUILD_PHASE === "true" ? "build" : "runtime"
1371
- const env = requireEnvironment(
1372
- process.env,
1373
- ["DATABASE_URL", "JWT_SECRET", "COOKIE_SECRET"],
1374
- phase,
1375
- { ...MEDUSA_BUILD_PLACEHOLDERS /* , ...other plugins' placeholders */ },
1376
- )
1377
- const redis = resolveRedisEventBus(process.env, phase)
1378
- ```
1379
-
1380
- ## Sentry instrumentation
1381
-
1382
- `@hartl-services/medusa-base/instrumentation` exports `register()`, the hook
1383
- Medusa's start command calls from the shop's `instrumentation.ts`. It runs
1384
- `Sentry.init` with `SENTRY_DSN`, `SENTRY_ENVIRONMENT` (fallback `NODE_ENV`)
1385
- and `tracesSampleRate: 0`; without a `SENTRY_DSN` Sentry stays inert. The
1386
- shop's `instrumentation.ts` is one line:
1387
-
1388
- ```ts
1389
- export { register } from "@hartl-services/medusa-base/instrumentation"
1390
- ```
1391
-
1392
- `@sentry/node` stays an optional peer dependency of the base: a shop that uses
1393
- this export (or `sentry-logger`) lists `@sentry/node` in its own
1394
- `dependencies`.
1395
-
1396
- ## E-Mail
1397
-
1398
- The base owns transactional e-mail for the shop: logical email messages,
1399
- attempts, rendering, the Medusa Notification integration, the SMTP and
1400
- Microsoft 365 transports, retries and e-mail-specific Admin operations. It is
1401
- the `email` module, its notification provider (`providers/smtp`, id `smtp`),
1402
- its subscribers, jobs and
1403
- workflows, and the Admin page `/settings/hartl-services/email`. There is no
1404
- separate e-mail package since 1.0.0.
1405
-
1406
- ### Configuration
1407
-
1408
- The notification provider is a `modules` entry built by
1409
- `createEmailNotificationModuleConfig` (from
1410
- `@hartl-services/medusa-base/config`); which service it sends through and the
1411
- credentials are set in the Admin (see "Versand"). The branding and rendering
1412
- configuration goes into the `email` namespace of the base options:
1413
-
1414
- ```ts
1415
- import { createEmailNotificationModuleConfig } from "@hartl-services/medusa-base/config";
1416
-
1417
- export default defineConfig({
1418
- modules: [
1419
- createEmailNotificationModuleConfig({
1420
- // Permits "Verschlüsselung: Keine" (a local SMTP server); never in production.
1421
- allowInsecure:
1422
- process.env.NODE_ENV === "development" || process.env.NODE_ENV === "test",
1423
- }),
1424
- ],
1425
- plugins: [
1426
- {
1427
- resolve: "@hartl-services/medusa-base",
1428
- options: {
1429
- email: {
1430
- jwtSecret: process.env.JWT_SECRET!,
1431
- shop: {
1432
- displayName: "Mein Shop",
1433
- affiliatePortalUrl: "https://shop.example.com/de/partner",
1434
- inviteUrl: ({ token }) =>
1435
- `https://shop.example.com/app/invite?token=${token}`,
1436
- orderNotificationEmail: "orders@shop.example.com",
1437
- passwordResetUrl: ({ locale }) =>
1438
- `https://shop.example.com/${locale ?? "de"}/passwort-zuruecksetzen`,
1439
- }, // a ShopEmailConfiguration, see `ShopEmailConfiguration` in `@hartl-services/medusa-base/types`
1440
- },
1441
- },
1442
- },
1443
- ],
1444
- });
1445
- ```
1446
-
1447
- Options of the `email` namespace:
1460
+ Options of the `email` namespace:
1448
1461
 
1449
1462
  - `jwtSecret` — used for Email's own tokens; typically the same secret as
1450
1463
  `projectConfig.http.jwtSecret`.
@@ -1455,7 +1468,7 @@ Options of the `email` namespace:
1455
1468
  that confirms an account, needed while `customer_verification` is on.
1456
1469
  `adminPasswordResetUrl` (optional builder, receives `{ token }`) is the
1457
1470
  Medusa Admin's "choose a new password" page; without it an Admin user's
1458
- password reset fails with `render_failed`. `buildFoodShopConfig` sets
1471
+ password reset fails with `render_failed`. `buildShopConfig` sets
1459
1472
  `<MEDUSA_BACKEND_URL>/app/reset-password?token=<token>` (the dashboard reads
1460
1473
  the token from the query).
1461
1474
  `adminAffiliateApplicationsUrl` (optional builder, receives
@@ -1472,22 +1485,17 @@ fact only while the Documents `invoices` feature is on when the message is
1472
1485
  created (with `invoices` off, or the Documents plugin switched off, Documents
1473
1486
  publishes no fact, so the mail is sent without a document). Refund e-mails
1474
1487
  come only from the Documents refund fact, so there are none while `invoices`
1475
- is off. The former `documentsEnabled` option no longer exists: Documents is
1476
- always part of the base.
1488
+ is off.
1477
1489
 
1478
1490
  The event contracts the module consumes for shipment, cancellation, refund and
1479
1491
  affiliate-reminder and partner-application e-mails come from
1480
- `@hartl-services/medusa-food-supplements-contracts/events/documents` and
1492
+ `@hartl-services/medusa-contracts/events/documents` and
1481
1493
  `/events/affiliate`, so installing the base does not pull in Affiliate.
1482
1494
 
1483
1495
  The provider reads no environment variables (since 2.9.0; the former
1484
1496
  `SMTP_*` variables are ignored).
1485
1497
 
1486
- Set `REDIS_URL` to use `@medusajs/event-bus-redis` instead of Medusa's local
1487
- Event Bus; production requires an authenticated `rediss://` URL. See "Event
1488
- Bus" below for retry/backoff behavior.
1489
-
1490
- ### Versand
1498
+ #### Versand
1491
1499
 
1492
1500
  The page E-Mail → "Versand" (`/settings/hartl-services/email/delivery`, linked
1493
1501
  under "Verwaltung") edits the plugin's settings. It stays reachable while the
@@ -1547,7 +1555,7 @@ to the e-mail of the Shop-Stammdaten, once per client id and expiry date (a
1547
1555
  log warning when the Shop-Stammdaten have no e-mail). A new secret with a new
1548
1556
  date warns again.
1549
1557
 
1550
- ### Feature toggles
1558
+ #### Feature toggles
1551
1559
 
1552
1560
  The E-Mail module declares its toggles to `@hartl-services/medusa-base` under the
1553
1561
  plugin key `email`. An Admin switches them under Settings → "Hartl Services
@@ -1583,7 +1591,7 @@ rows are dimmed and their switches disabled then. All E-Mail sub-pages
1583
1591
  switch and show `PluginFeatureDisabledNotice` while it is off (the backend
1584
1592
  answers `404` for `/admin/email/*` then).
1585
1593
 
1586
- ### Kontobestätigung per E-Mail
1594
+ #### Kontobestätigung per E-Mail
1587
1595
 
1588
1596
  Feature `customer_verification` (child of `account_mails`, default **off**)
1589
1597
  makes customers confirm their e-mail address through a link before their
@@ -1658,11 +1666,12 @@ the toggle itself and follow at once. A failed read keeps the last value and
1658
1666
  logs a warning; before the first successful read the copy is off. The copy
1659
1667
  lives on `globalThis` because the preset (`dist`) and the plugin code Medusa
1660
1668
  loads (`.medusa/server`) are compiled separately. Only the preset
1661
- (`buildFoodShopConfig` / `defineFoodShopConfig`) sets the getter; a shop with a
1669
+ (`buildShopConfig` / `defineShopConfig`) sets the getter; a shop with a
1662
1670
  hand-written config must set `authVerificationsPerActor` itself (a fixed list,
1663
1671
  changed by a restart) for the toggle to have any effect on logins.
1664
1672
 
1665
- 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.
1666
1675
 
1667
1676
  Things to know when switching it on:
1668
1677
 
@@ -1688,7 +1697,7 @@ Storefront contract (DTOs, schema and path from the Contracts package,
1688
1697
  - Serve `/<cc>/verify-account`, read `token` from `location.hash`, confirm it
1689
1698
  and then send the customer to the login.
1690
1699
 
1691
- ### Ownership
1700
+ #### Ownership
1692
1701
 
1693
1702
  - Documents owns document issuance and publishes immutable document facts.
1694
1703
  - Affiliate owns reminder eligibility and publishes reminder and application
@@ -1697,14 +1706,14 @@ Storefront contract (DTOs, schema and path from the Contracts package,
1697
1706
  status plus the attachment library (documents with append-only PDF
1698
1707
  versions).
1699
1708
  - Public HTTP DTOs, schemas, and paths remain in
1700
- `@hartl-services/medusa-food-supplements-contracts`.
1709
+ `@hartl-services/medusa-contracts`.
1701
1710
 
1702
1711
  The base's `/types` and `/config` exports are dedicated server integration
1703
1712
  surfaces for the backend. The Documents and Affiliate event contracts live in
1704
1713
  the Contracts package (`events/documents`, `events/affiliate`). The base exports no DML models, services,
1705
1714
  workflows, migrations, or generated output as a consumer contract.
1706
1715
 
1707
- ### Event Bus
1716
+ #### Event Bus
1708
1717
 
1709
1718
  The backend owns Event Bus selection. Development without `REDIS_URL` leaves
1710
1719
  Medusa's local Event Bus active. Configuring `REDIS_URL` registers
@@ -1718,7 +1727,7 @@ for at most one hour and 1,000 entries. There is no custom Event Bus wrapper,
1718
1727
  startup capability probe, or purge job. Events are at-least-once facts and can
1719
1728
  be replayed, delayed, or delivered out of order.
1720
1729
 
1721
- ### Event Contracts
1730
+ #### Event Contracts
1722
1731
 
1723
1732
  All payload objects are strict. Unless noted otherwise, every string shown
1724
1733
  below is trimmed and non-empty. Native event names and shapes are fixed to the
@@ -1844,7 +1853,7 @@ concurrent request never sends a second mail) addressed to
1844
1853
  `affiliate-consignment:<affiliate_id>:<period_start>`. `locale`, when present,
1845
1854
  has at most 35 characters. Email runtime-validates every contract before use.
1846
1855
 
1847
- ### Invariants
1856
+ #### Invariants
1848
1857
 
1849
1858
  - The accepted kinds are exactly `order.received`, `order.received.admin`, `order.shipped`,
1850
1859
  `order.canceled`, `order.return_requested`, `order.return_received`,
@@ -1898,7 +1907,7 @@ has at most 35 characters. Email runtime-validates every contract before use.
1898
1907
 
1899
1908
  Read this section and the central design before changing Email domain behavior.
1900
1909
 
1901
- ### Shop configuration and default templates
1910
+ #### Shop configuration and default templates
1902
1911
 
1903
1912
  The E-Mail module ships a neutral German layout in grey tones (table-based,
1904
1913
  inline styles) and one renderer per accepted kind; the colors of the
@@ -1935,7 +1944,7 @@ an info box lists number, customer, e-mail and order time, followed by an
1935
1944
  "Im Admin öffnen" button and the items, totals and addresses. Two optional
1936
1945
  `ShopEmailConfiguration` fields drive it: `adminOrderUrl({ orderId })` (the
1937
1946
  button is left out without it) and `timeZone` (IANA; the order time is left
1938
- out without it). `buildFoodShopConfig` sets both (`<MEDUSA_BACKEND_URL>/app/
1947
+ out without it). `buildShopConfig` sets both (`<MEDUSA_BACKEND_URL>/app/
1939
1948
  orders/<id>` and its `timeZone` option). The layout receives `audience`
1940
1949
  (`"shop"` for this kind, `affiliate.application_submitted.admin`, `system.credential_expiry.admin`, `user.invite` and `auth.admin_password_reset`, `"customer"` for all others) and
1941
1950
  shows the contact line only to customers.
@@ -2047,7 +2056,7 @@ Partner-Bewerbung von <Firma>" (without a company the applicant's name, then
2047
2056
  the e-mail address; just "Neue Partner-Bewerbung" if none is known). An info
2048
2057
  box lists company, name and e-mail (only the present ones), the applicant's
2049
2058
  message follows as an escaped paragraph, and an "Im Admin prüfen" button links
2050
- `adminAffiliateApplicationsUrl` (left out without it; `buildFoodShopConfig`
2059
+ `adminAffiliateApplicationsUrl` (left out without it; `buildShopConfig`
2051
2060
  sets `<MEDUSA_BACKEND_URL>/app/affiliates/applications`). Its layout audience
2052
2061
  is `"shop"`. All applicant text is HTML-escaped.
2053
2062
 
@@ -2084,7 +2093,7 @@ configuration (blank `displayName`, a non-function `layout` or renderer, an
2084
2093
  unknown renderer kind) fails at startup and not at the first send. Custom
2085
2094
  renderers must escape dynamic values in `bodyHtml` themselves.
2086
2095
 
2087
- ### Deprecated SMTP helpers
2096
+ #### Deprecated SMTP helpers
2088
2097
 
2089
2098
  `createSmtpNotificationModuleConfig(smtp)`, `resolveSmtp`,
2090
2099
  `SMTP_ENVIRONMENT_VARIABLES` and `SMTP_BUILD_PLACEHOLDERS` remain exported
@@ -2093,7 +2102,7 @@ given to `createSmtpNotificationModuleConfig` only fill SMTP settings that are
2093
2102
  not stored in the Admin (the Admin status does not count them as configured).
2094
2103
  Use `createEmailNotificationModuleConfig` and the "Versand" page instead.
2095
2104
 
2096
- ### Delivery Semantics
2105
+ #### Delivery Semantics
2097
2106
 
2098
2107
  For ordinary durable messages, only characterized connection failures,
2099
2108
  temporary SMTP rejections, and temporary attachment access failures are
@@ -2119,7 +2128,7 @@ data remain ephemeral; both kinds are excluded from the generic retry job and
2119
2128
  Admin resend, and retryable handling stays bounded by the original Event Bus
2120
2129
  event.
2121
2130
 
2122
- ### Admin API
2131
+ #### Admin API
2123
2132
 
2124
2133
  Email exposes authenticated Admin operations at:
2125
2134
 
@@ -2190,7 +2199,7 @@ a different UUID represents a separate, conscious Admin decision. The Admin UI
2190
2199
  therefore retains a UUID while retrying an ambiguous failed request and creates
2191
2200
  a new UUID only after a new confirmation.
2192
2201
 
2193
- ### Admin UI
2202
+ #### Admin UI
2194
2203
 
2195
2204
  The E-Mail module has an Admin area under `/settings/hartl-services/email`
2196
2205
  (**Settings → Hartl Services Plugins → E-Mail → Öffnen**; the descriptor's
@@ -2239,7 +2248,7 @@ request UUID; downloads use the configured authenticated Admin SDK and never
2239
2248
  expose storage file IDs or URLs in the UI. There is no delete: removing every
2240
2249
  assigned mail lets a document rest.
2241
2250
 
2242
- ### Mail templates
2251
+ #### Mail templates
2243
2252
 
2244
2253
  All 13 mails (`EMAIL_EDITABLE_KINDS`) are block templates an Admin edits
2245
2254
  from the group pages of the E-Mail area (Bestell-, Konto- and Affiliate-Mails
@@ -2340,7 +2349,7 @@ and then each changed mail kind on its own.
2340
2349
  bzw. ist beschädigt.“), any other read failure „Anhang „X“ konnte
2341
2350
  gerade nicht geladen werden.“
2342
2351
 
2343
- ### Mail attachments
2352
+ #### Mail attachments
2344
2353
 
2345
2354
  The global Terms PDFs (`AGB`), the EU legal-guarantee notice or a flyer are
2346
2355
  documents of the attachment library. A document has a name, the customer
@@ -2355,8 +2364,8 @@ Admin consumers import wire DTOs and paths only from the Contracts package and
2355
2364
  reuse their configured Medusa SDK:
2356
2365
 
2357
2366
  ```ts
2358
- import type { ListAdminEmailMessagesResponse } from "@hartl-services/medusa-food-supplements-contracts/types/email-admin";
2359
- import { emailAdminPaths } from "@hartl-services/medusa-food-supplements-contracts/paths/email";
2367
+ import type { ListAdminEmailMessagesResponse } from "@hartl-services/medusa-contracts/types/email-admin";
2368
+ import { emailAdminPaths } from "@hartl-services/medusa-contracts/paths/email";
2360
2369
 
2361
2370
  const response = await sdk.client.fetch<ListAdminEmailMessagesResponse>(
2362
2371
  emailAdminPaths.get.list,
@@ -2366,15 +2375,11 @@ const response = await sdk.client.fetch<ListAdminEmailMessagesResponse>(
2366
2375
  Do not import E-Mail module implementations, generated `.medusa` files, or
2367
2376
  duplicate runtime schemas in an Admin consumer. The base's own exports remain
2368
2377
  internal server and generated Admin integration surfaces; Email HTTP contracts
2369
- are published only by `@hartl-services/medusa-food-supplements-contracts`.
2378
+ are published only by `@hartl-services/medusa-contracts`.
2370
2379
 
2371
- ### Development and Rollout
2380
+ #### Rollout
2372
2381
 
2373
- For local development, provide the seven SMTP variables and omit `REDIS_URL` to
2374
- use Medusa's local Event Bus. Set `REDIS_URL` only when intentionally exercising
2375
- the direct Redis provider. The backend HTTP runner ignores ambient Redis and
2376
- SMTP settings and injects an isolated Redis container, unique queue, and
2377
- token-protected SMTP fixture for every child.
2382
+ Local development notes: see "Development".
2378
2383
 
2379
2384
  This architecture replaces the old Documents and Affiliate delivery baselines
2380
2385
  without a backfill or compatibility layer. Existing local databases from those
@@ -2390,7 +2395,7 @@ production business data exists; a populated database requires a separately
2390
2395
  reviewed migration. No old delivery rows or queued jobs are migrated or purged
2391
2396
  by Email.
2392
2397
 
2393
- ## Dokumente
2398
+ ### Dokumente
2394
2399
 
2395
2400
  The base issues order documents: one total invoice per order after the final
2396
2401
  shipment and one negative cancellation document for each eligible payment
@@ -2398,10 +2403,9 @@ refund. An Admin user can regenerate issued documents in place for exceptional
2398
2403
  corrections. The base ships default German document markup, so a new shop needs
2399
2404
  no template code; a project can optionally replace the HTML/CSS factory. PDF
2400
2405
  generation, private file storage, customer authorization and durable
2401
- document-readiness facts stay inside the `documents` module. There is no
2402
- separate documents package since 1.0.0.
2406
+ document-readiness facts stay inside the `documents` module.
2403
2407
 
2404
- ### What it does
2408
+ #### What it does
2405
2409
 
2406
2410
  - Listens to Medusa's `shipment.created` event.
2407
2411
  - Creates an invoice only if the order is fully shipped and not cancelled.
@@ -2468,7 +2472,7 @@ separate documents package since 1.0.0.
2468
2472
  - Reconciles non-canceled physical fulfillments periodically so a lost local
2469
2473
  event can be repaired idempotently, including after shipment.
2470
2474
 
2471
- ### Configuration
2475
+ #### Configuration
2472
2476
 
2473
2477
  The invoice workflow reads the order's frozen B2B evidence from the base's own
2474
2478
  `tax_compliance` module (see "Steuer-Compliance"); register the Tax provider
@@ -2527,7 +2531,7 @@ The `documents` module is always registered with the base (no `documentsEnabled`
2527
2531
  switch); the Admin switches it off at run time through the feature toggles
2528
2532
  below. `medusa db:migrate` creates its tables.
2529
2533
 
2530
- ### The `/config` subpath
2534
+ #### The `/config` subpath
2531
2535
 
2532
2536
  `@hartl-services/medusa-base/config` exports, next to the environment helpers,
2533
2537
  build-time values for `medusa-config.ts` and tests:
@@ -2542,7 +2546,7 @@ build-time values for `medusa-config.ts` and tests:
2542
2546
  The document types (`DocumentMerchantOption`, `DocumentHtmlFactory`,
2543
2547
  `DocumentRenderInput`, ...) come from `@hartl-services/medusa-base/types`.
2544
2548
 
2545
- ### Layout, logo, payment terms and shop theme
2549
+ #### Layout, logo, payment terms and shop theme
2546
2550
 
2547
2551
  The default markup prints an A4 letterhead: logo top left, document title and
2548
2552
  key data (number, order, date, customer UID for exempt B2B supplies) on the
@@ -2585,7 +2589,7 @@ fonts as `data:` URLs; the container's Chromium has only its system fonts. A
2585
2589
  shop passes its factory through the preset option `documentHtmlFactory` (see
2586
2590
  "Shop-Config-Preset").
2587
2591
 
2588
- ### Merchant data
2592
+ #### Merchant data
2589
2593
 
2590
2594
  The legal sender printed on every document comes from the base plugin's shop
2591
2595
  master data (Admin → Settings → Hartl Services Plugins → **Shop-Stammdaten**).
@@ -2617,7 +2621,7 @@ Requirements/couplings:
2617
2621
  - The Tax provider `tp_tax_compliance` of the base's Steuer-Compliance on the
2618
2622
  regions, so orders carry UID-based exemptions.
2619
2623
  - A Chromium/Chrome executable and a Medusa File provider that supports
2620
- private files (see "Development" below for the sandbox/non-root note).
2624
+ private files (see "Shop integration" below for the sandbox/non-root note).
2621
2625
  Documents stores PDFs through `Modules.FILE` and streams them only through
2622
2626
  its own authenticated routes, so it works with any File provider and does
2623
2627
  not need the S3 file storage or its `/files` routes.
@@ -2633,8 +2637,7 @@ Requirements/couplings:
2633
2637
  batch/lot data on invoices and packing lists (see "Optional Batch
2634
2638
  Management integration" below).
2635
2639
 
2636
-
2637
- ### Feature toggles
2640
+ #### Feature toggles
2638
2641
 
2639
2642
  The Documents module declares its toggles to the base under the plugin key
2640
2643
  `documents`. An Admin switches them under Settings → "Hartl
@@ -2672,15 +2675,14 @@ using `PluginFeatureGate` from
2672
2675
  No workflow hook and nothing on the cart, checkout or fulfillment path reads
2673
2676
  a toggle.
2674
2677
 
2675
- ### Shop integration
2678
+ #### Shop integration
2676
2679
 
2677
2680
  The demo shop (`apps/backend`) gets everything from the preset (see
2678
2681
  "Shop-Config-Preset"): `documents.numbering.timeZone` is its `timeZone`
2679
2682
  option, `documents.htmlFactory` its optional `documentHtmlFactory` and
2680
2683
  `documents.pdf.executablePath` the `DOCUMENT_PDF_EXECUTABLE` variable. The
2681
- development UID `ATU00000000` is a placeholder: Documents never prints it,
2682
- and Tax Compliance does not transmit it to FinanzOnline. Enter the real
2683
- 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").
2684
2686
 
2685
2687
  `htmlFactory` is intentionally the only presentation extension point. It gets
2686
2688
  a fully typed value snapshot (`DocumentTemplateInput` or
@@ -2698,7 +2700,7 @@ directory and removes it afterward. The Chromium child is also disconnected
2698
2700
  from the user's session bus and legacy keyring socket, so it cannot access the
2699
2701
  operator's normal browser profile or desktop keyring, including under WSLg.
2700
2702
 
2701
- ### Restore check for private files
2703
+ #### Restore check for private files
2702
2704
 
2703
2705
  After restoring the object store (or the local file directory), check that
2704
2706
  every issued document's PDF can be read through the active Medusa File
@@ -2712,7 +2714,7 @@ pnpm exec medusa exec ./node_modules/@hartl-services/medusa-base/.medusa/server/
2712
2714
  It fails on the first issued document without a stored file and otherwise
2713
2715
  logs the number of verified files and bytes.
2714
2716
 
2715
- ### Clean baseline and first deployment
2717
+ #### Clean baseline and first deployment
2716
2718
 
2717
2719
  The module intentionally contains one generated eight-table baseline,
2718
2720
  `Migration20260827061749`. It removes the former `documents_delivery` table
@@ -2758,7 +2760,7 @@ Apply normal pending module migrations after later changes or upgrades with
2758
2760
  does not require link synchronization by itself; `db:sync-links` above is the
2759
2761
  safe shop-wide first-deployment step for links owned elsewhere.
2760
2762
 
2761
- ### Storefront contract
2763
+ #### Storefront contract
2762
2764
 
2763
2765
  All three endpoints require a customer session or bearer token. There is no
2764
2766
  guest lookup endpoint and no direct File-provider URL.
@@ -2774,8 +2776,8 @@ address/email, file and artifact IDs, leases and resolution state.
2774
2776
  For a typed Storefront integration:
2775
2777
 
2776
2778
  ```ts
2777
- import type { ListStoreDocumentsResponse } from "@hartl-services/medusa-food-supplements-contracts/types/documents-store";
2778
- import { documentsPaths } from "@hartl-services/medusa-food-supplements-contracts/paths/documents";
2779
+ import type { ListStoreDocumentsResponse } from "@hartl-services/medusa-contracts/types/documents-store";
2780
+ import { documentsPaths } from "@hartl-services/medusa-contracts/paths/documents";
2779
2781
 
2780
2782
  const { documents: issuedDocuments } =
2781
2783
  await sdk.client.fetch<ListStoreDocumentsResponse>(
@@ -2791,13 +2793,13 @@ storage URL.
2791
2793
 
2792
2794
  The same Contracts boundary applies to all Documents and Tax Compliance public
2793
2795
  routes: import transport DTOs only from
2794
- `@hartl-services/medusa-food-supplements-contracts/types/<feature>`, paths only from
2795
- `@hartl-services/medusa-food-supplements-contracts/paths/<feature>`, and call the
2796
+ `@hartl-services/medusa-contracts/types/<feature>`, paths only from
2797
+ `@hartl-services/medusa-contracts/paths/<feature>`, and call the
2796
2798
  Storefront's configured Medusa SDK. A consumer that needs a runtime guard may
2797
- opt in to `@hartl-services/medusa-food-supplements-contracts/schemas/<feature>`; it must
2799
+ opt in to `@hartl-services/medusa-contracts/schemas/<feature>`; it must
2798
2800
  not import either the plugin implementation or generated `.medusa` output.
2799
2801
 
2800
- ### Admin order detail
2802
+ #### Admin order detail
2801
2803
 
2802
2804
  When Documents is enabled, the Medusa Admin order detail shows a
2803
2805
  **Dokumente** widget. It lists the issued documents for that order (kind and
@@ -2817,7 +2819,7 @@ The Admin API is deliberately scoped by both order and document ID:
2817
2819
  All authenticated Admin users can inspect order documents; a document from a
2818
2820
  different order returns `404` from the download endpoint.
2819
2821
 
2820
- #### Internal packing lists
2822
+ ##### Internal packing lists
2821
2823
 
2822
2824
  The separate **Packlisten** order widget lists every physical fulfillment in
2823
2825
  creation order, each with a status badge (Nicht erstellt, Ausstehend, Wird
@@ -2841,7 +2843,7 @@ Packing lists never pass through the Documents customer-fact pipeline. They are
2841
2843
  also stored outside `documents_document`, so the authenticated Store document
2842
2844
  queries cannot expose them accidentally.
2843
2845
 
2844
- #### Regenerating an invoice
2846
+ ##### Regenerating an invoice
2845
2847
 
2846
2848
  The widget offers **Rechnung ausstellen** only while the order has no
2847
2849
  invoice; once one exists, every document row has a regenerate icon button
@@ -2862,7 +2864,7 @@ above. A cancellation or future credit note is instead rendered from its
2862
2864
  persisted snapshot, retaining its original amount, source invoice reference,
2863
2865
  number and issue date. It also emits no customer communication event.
2864
2866
 
2865
- #### Financial document numbers
2867
+ ##### Financial document numbers
2866
2868
 
2867
2869
  Documents assigns numbers in these annual ranges:
2868
2870
 
@@ -2885,7 +2887,7 @@ increments a range. A permanently failed document retains its number and
2885
2887
  failure reason as the explanation for that range's gap; reserved numbers are
2886
2888
  never recycled or reassigned.
2887
2889
 
2888
- #### Cancellation documents for payment refunds
2890
+ ##### Cancellation documents for payment refunds
2889
2891
 
2890
2892
  Medusa's refund event identifies the payment, so the plugin enumerates its
2891
2893
  persisted refund records and claims one cancellation document using each
@@ -2907,7 +2909,7 @@ not create a second customer fact. For a canceled Order, individual Refund
2907
2909
  facts are suppressed in favor of the authoritative Order-cancellation
2908
2910
  resolution and its sorted artifact list.
2909
2911
 
2910
- ### Optional Batch Management integration
2912
+ #### Optional Batch Management integration
2911
2913
 
2912
2914
  If `@hartl-services/medusa-batch-management` version `0.0.3` or later is registered, Documents detects
2913
2915
  its `getOrderDocumentBatchSnapshot(orderId)` capability structurally. For every
@@ -2938,11 +2940,11 @@ list revision and serves the newest ready PDF. Returns do not rewrite the
2938
2940
  historical pick instruction. Fulfillment cancellation marks the packing list as
2939
2941
  canceled and prevents a concurrent render from being published as active.
2940
2942
 
2941
- ### Document facts and artifacts
2943
+ #### Document facts and artifacts
2942
2944
 
2943
2945
  Documents owns no Notification or delivery state. Its integration boundary is
2944
2946
  the strict versioned event contract exported from
2945
- `@hartl-services/medusa-food-supplements-contracts/events/documents`. Shipment assignments, standalone Refund resolutions and Order-
2947
+ `@hartl-services/medusa-contracts/events/documents`. Shipment assignments, standalone Refund resolutions and Order-
2946
2948
  cancellation resolutions each carry an `event_emitted_at` marker that is set
2947
2949
  only after successful enqueue.
2948
2950
 
@@ -2959,10 +2961,10 @@ worker has lost its lease, the unreferenced private File is deleted best-effort;
2959
2961
  a hard process exit in that external File/database gap can leave a storage
2960
2962
  orphan but cannot commit another Artifact, carrier or resolution.
2961
2963
 
2962
- #### Exact outgoing event contracts
2964
+ ##### Exact outgoing event contracts
2963
2965
 
2964
2966
  The server-only Contracts subpath
2965
- `@hartl-services/medusa-food-supplements-contracts/events/documents` exports the
2967
+ `@hartl-services/medusa-contracts/events/documents` exports the
2966
2968
  event-name constants,
2967
2969
  strict Zod schemas and inferred payload types for exactly these three events:
2968
2970
 
@@ -3043,8 +3045,6 @@ before the process can persist success, so an ambiguous attempt becomes
3043
3045
  `unknown` and is not automatically sent again. Any later resend is an explicit
3044
3046
  Email Admin action and a new logical message.
3045
3047
 
3046
- ### Development
3047
-
3048
3048
  #### Public Storefront and Admin contracts
3049
3049
 
3050
3050
  The central contracts package exports the Documents Store and Admin DTOs, Zod
@@ -3052,8 +3052,8 @@ schemas and route paths, but no second Storefront client. Consumers use their
3052
3052
  own Medusa SDK instance:
3053
3053
 
3054
3054
  ```ts
3055
- import type { GetStoreDocumentResponse } from "@hartl-services/medusa-food-supplements-contracts/types/documents-store";
3056
- import { documentsPaths } from "@hartl-services/medusa-food-supplements-contracts/paths/documents";
3055
+ import type { GetStoreDocumentResponse } from "@hartl-services/medusa-contracts/types/documents-store";
3056
+ import { documentsPaths } from "@hartl-services/medusa-contracts/paths/documents";
3057
3057
 
3058
3058
  const response = await sdk.client.fetch<GetStoreDocumentResponse>(
3059
3059
  documentsPaths.store.get.retrieve(documentId),
@@ -3061,10 +3061,10 @@ const response = await sdk.client.fetch<GetStoreDocumentResponse>(
3061
3061
  );
3062
3062
  ```
3063
3063
 
3064
- `@hartl-services/medusa-food-supplements-contracts/types/documents-store` and
3065
- `@hartl-services/medusa-food-supplements-contracts/types/documents-admin` contain the
3064
+ `@hartl-services/medusa-contracts/types/documents-store` and
3065
+ `@hartl-services/medusa-contracts/types/documents-admin` contain the
3066
3066
  respective type-only DTOs. Import
3067
- `@hartl-services/medusa-food-supplements-contracts/schemas/documents` only for explicit
3067
+ `@hartl-services/medusa-contracts/schemas/documents` only for explicit
3068
3068
  runtime validation. The common Storefront case imports only types, which are
3069
3069
  erased by TypeScript, and the path module; it never imports the plugin's Medusa
3070
3070
  server code. Store and Admin responses use the same safe `DocumentSummary`
@@ -3076,8 +3076,8 @@ and README note together when introducing another Documents route.
3076
3076
  Packing-list consumers stay Admin-only:
3077
3077
 
3078
3078
  ```ts
3079
- import type { ListAdminOrderPackingListsResponse } from "@hartl-services/medusa-food-supplements-contracts/types/documents-admin";
3080
- import { packingListsPaths } from "@hartl-services/medusa-food-supplements-contracts/paths/documents";
3079
+ import type { ListAdminOrderPackingListsResponse } from "@hartl-services/medusa-contracts/types/documents-admin";
3080
+ import { packingListsPaths } from "@hartl-services/medusa-contracts/paths/documents";
3081
3081
 
3082
3082
  const response = await sdk.client.fetch<ListAdminOrderPackingListsResponse>(
3083
3083
  packingListsPaths.admin.get.listForOrder(orderId),
@@ -3085,16 +3085,15 @@ const response = await sdk.client.fetch<ListAdminOrderPackingListsResponse>(
3085
3085
  );
3086
3086
  ```
3087
3087
 
3088
- ## Steuer-Compliance
3088
+ ### Steuer-Compliance
3089
3089
 
3090
- There is no separate `@hartl-services/medusa-tax-compliance` package since
3091
- 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
3092
3091
  `tp_tax_compliance`, the UID check routes, the order-evidence freeze and the
3093
- apportioned shipping tax) is part of the base. The module owns the durable
3094
- validation evidence; a Storefront never imports its implementation, only
3095
- `@hartl-services/medusa-food-supplements-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`.
3096
3095
 
3097
- ### Configuration
3096
+ #### Configuration
3098
3097
 
3099
3098
  Register the Tax Module provider in `medusa-config.ts` (the Shop-Config-Preset
3100
3099
  does this); the module itself boots with the base and has no options:
@@ -3124,7 +3123,7 @@ The merchant's own country and VAT number come only from the Shop-Stammdaten
3124
3123
  select the qualified VAT validator (currently only the Austrian FinanzOnline
3125
3124
  adapter). While either is missing, carts get ordinary B2C tax.
3126
3125
 
3127
- ### Runtime settings
3126
+ #### FinanzOnline settings
3128
3127
 
3129
3128
  Declared on the Steuer-Compliance descriptor and edited on its Admin page
3130
3129
  (Settings → Hartl Services Plugins → Steuer-Compliance):
@@ -3175,7 +3174,7 @@ qualifizierte UID-Abfrage.").
3175
3174
  answers ordinary B2C rates. The order tax path needs no merchant: it checks
3176
3175
  the evidence frozen under the order ID.
3177
3176
 
3178
- ### Feature toggles
3177
+ #### Feature toggles
3179
3178
 
3180
3179
  Steuer-Compliance declares its switch and three features under the plugin key
3181
3180
  `tax_compliance` (`TAX_COMPLIANCE_PLUGIN_KEY` / `TAX_COMPLIANCE_FEATURES` from
@@ -3214,14 +3213,14 @@ values; the `plugin-features.updated.v1` subscriber and the
3214
3213
  sync (no row) both count as on; when the row cannot be read both count as
3215
3214
  off (ordinary B2C rates, Medusa's shipping tax).
3216
3215
 
3217
- ### UID check in the checkout
3216
+ #### UID check in the checkout
3218
3217
 
3219
3218
  Public Store request, response, schema, and path imports belong to
3220
- `@hartl-services/medusa-food-supplements-contracts`:
3219
+ `@hartl-services/medusa-contracts`:
3221
3220
 
3222
3221
  ```ts
3223
- import type { SubmitTaxIdentificationResponse } from "@hartl-services/medusa-food-supplements-contracts/types/tax-compliance-store";
3224
- import { taxCompliancePaths } from "@hartl-services/medusa-food-supplements-contracts/paths/tax-compliance";
3222
+ import type { SubmitTaxIdentificationResponse } from "@hartl-services/medusa-contracts/types/tax-compliance-store";
3223
+ import { taxCompliancePaths } from "@hartl-services/medusa-contracts/paths/tax-compliance";
3225
3224
 
3226
3225
  const result = await sdk.client.fetch<SubmitTaxIdentificationResponse>(
3227
3226
  taxCompliancePaths.store.carts.taxIdentification(cartId),
@@ -3310,8 +3309,8 @@ GET /store/tax-identification-policy → { merchant_country_code: "at" | nul
3310
3309
  storefront then asks for every country.
3311
3310
 
3312
3311
  ```ts
3313
- import type { GetTaxIdentificationPolicyResponse } from "@hartl-services/medusa-food-supplements-contracts/types/tax-compliance-store";
3314
- import { taxCompliancePaths } from "@hartl-services/medusa-food-supplements-contracts/paths/tax-compliance";
3312
+ import type { GetTaxIdentificationPolicyResponse } from "@hartl-services/medusa-contracts/types/tax-compliance-store";
3313
+ import { taxCompliancePaths } from "@hartl-services/medusa-contracts/paths/tax-compliance";
3315
3314
 
3316
3315
  const { merchant_country_code } = await sdk.client.fetch<GetTaxIdentificationPolicyResponse>(
3317
3316
  taxCompliancePaths.store.taxIdentificationPolicy,
@@ -3324,7 +3323,7 @@ gets 3 s, so the request stays below a storefront's 15 s checkout wait and the
3324
3323
  database transaction holding the UID lock never waits on a hanging
3325
3324
  FinanzOnline call; a timeout answers `unavailable` (`technical_error`).
3326
3325
 
3327
- ### Saved customer addresses
3326
+ #### Saved customer addresses
3328
3327
 
3329
3328
  A saved customer address carries its UID in `metadata.vat_number`
3330
3329
  (`TAX_IDENTIFICATION_METADATA_KEY` in the Contracts). A logged-in customer
@@ -3372,7 +3371,7 @@ facts likewise receive ordinary B2C tax, without a warning. The registry is
3372
3371
  country-based: it currently provides only the Austrian FinanzOnline adapter;
3373
3372
  an unsupported merchant country deliberately resolves to `unavailable`.
3374
3373
 
3375
- ### Frozen order evidence
3374
+ #### Frozen order evidence
3376
3375
 
3377
3376
  Cart tax workflows pass only the opaque current evidence ID and the merchant
3378
3377
  profile (country and UID) to the provider. Order tax workflows use evidence
@@ -3401,19 +3400,104 @@ wording from the completed order (see "Dokumente"). A later cart change or
3401
3400
  validation cannot alter an already issued document; B2C, invalid, ineligible
3402
3401
  and unavailable evidence adds no tax-proof wording.
3403
3402
 
3404
- ## 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)
3405
3489
 
3406
3490
  The base provides a Medusa File Module provider for durable S3-compatible
3407
3491
  asset storage with a local, TTL-based read cache (`providers/s3-cache`), and
3408
3492
  the `/files` gateway routes every provider URL points at. It supports AWS S3,
3409
- MinIO and compatible endpoints through AWS SDK v3. There is no separate S3
3410
- package since 1.0.0; the provider identifier `s3-cache` and the resolve path
3411
- `@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").
3412
3496
 
3413
3497
  S3 is always the source of truth. The local cache is a per-replica performance
3414
3498
  optimization and may be discarded at any time without data loss.
3415
3499
 
3416
- ### Coverage
3500
+ #### Coverage
3417
3501
 
3418
3502
  The provider covers every asset that uses Medusa's native `Modules.FILE`
3419
3503
  service:
@@ -3427,7 +3511,7 @@ It cannot transparently back up a plugin that writes directly to its own local
3427
3511
  directory, own S3 client, or a third-party URL. Such integrations must be
3428
3512
  changed to use `Modules.FILE` or migrated explicitly.
3429
3513
 
3430
- ### Configuration
3514
+ #### Configuration
3431
3515
 
3432
3516
  ```ts
3433
3517
  // medusa-config.ts
@@ -3503,7 +3587,7 @@ options) is optional; see
3503
3587
  [Active S3 options and defaults](#active-s3-options-and-defaults) below. The
3504
3588
  provider has no database migrations to run.
3505
3589
 
3506
- ### Environment helpers
3590
+ #### Environment helpers
3507
3591
 
3508
3592
  `@hartl-services/medusa-base/config` also exports small helpers for reading
3509
3593
  the deployment variables in `medusa-config.ts`:
@@ -3519,7 +3603,7 @@ the deployment variables in `medusa-config.ts`:
3519
3603
  - `resolveExpirationStrategy(process.env, defaultValue)` — reads
3520
3604
  `S3_CACHE_STORAGE_EXPIRATION_STRATEGY` (`"absolute"` or `"sliding"`).
3521
3605
 
3522
- ### Runtime configuration
3606
+ #### Runtime configuration
3523
3607
 
3524
3608
  The demo shop (`apps/backend`) enables S3 Cache Storage as the sole Medusa
3525
3609
  File Module provider through the preset; there is no per-deployment feature
@@ -3539,7 +3623,7 @@ S3 keys unless the matching database and storage rollback is being performed.
3539
3623
  The local provider cannot read S3-backed IDs. Treat a storage-provider change
3540
3624
  as a controlled deployment cutover, not as a runtime storage router.
3541
3625
 
3542
- #### Active S3 options and defaults
3626
+ ##### Active S3 options and defaults
3543
3627
 
3544
3628
  In this demo host, `bucket`, `region`, and the File Gateway base URL are
3545
3629
  required.
@@ -3561,7 +3645,7 @@ URLs may use only HTTP(S), with no credentials, query string, or fragment.
3561
3645
  Prefixes and provider keys reject absolute paths, traversal, control
3562
3646
  characters, and backslashes.
3563
3647
 
3564
- ### Object and access model
3648
+ #### Object and access model
3565
3649
 
3566
3650
  New uploads have opaque keys. The original filename is stored only as encoded
3567
3651
  object metadata:
@@ -3596,7 +3680,7 @@ successful remote write/delete evicts the affected entry only after S3 has
3596
3680
  accepted it. Do not share the cache directory between replicas; use a bounded
3597
3681
  `emptyDir` or per-pod volume and monitor free disk space.
3598
3682
 
3599
- ### Backup and restore
3683
+ #### Backup and restore
3600
3684
 
3601
3685
  A complete restore requires both a PostgreSQL backup and the versioned object
3602
3686
  store. The cache is not a backup and is intentionally excluded.
@@ -3619,7 +3703,7 @@ count and total bytes without modifying data:
3619
3703
  medusa exec ./node_modules/@hartl-services/medusa-base/.medusa/server/src/documents/scripts/verify-documents-file-storage.js
3620
3704
  ```
3621
3705
 
3622
- ### HTTP contract
3706
+ #### HTTP contract
3623
3707
 
3624
3708
  The gateway routes expose `GET /files/:asset` for public immutable files and
3625
3709
  `GET /files/private/:token` for signed private capability URLs. Both call
@@ -3630,126 +3714,234 @@ Paths, query types, and the runtime query schema are exported from the separate
3630
3714
  normally consume the complete URL returned by Medusa and never construct a
3631
3715
  private URL themselves.
3632
3716
 
3633
- ## Shop-Config-Preset
3717
+ ## Shop behaviour and operations
3634
3718
 
3635
- `@hartl-services/medusa-base/shop-config` turns a food shop's configuration
3636
- from about 290 lines into about 30: the plugin list and order, the Redis event
3637
- bus, SMTP notifications, S3 file storage, the tax and Stripe payment providers,
3638
- the Admin path, the Sentry logger bridge and the environment validation are
3639
- assembled there; the shop supplies only its own facts. It reuses the `/config`
3640
- helpers above for environment parsing. It is a separate subpath because it
3641
- loads the Sentry logger wrapper and with it `@sentry/node`; `/config` stays
3642
- free of it.
3719
+ Behaviour the base adds to every shop without a toggle, and the
3720
+ `medusa-config.ts` helpers for operating it.
3643
3721
 
3644
- The preset registers the base (with its E-Mail, Documents and `/files`
3645
- options, and its Tax provider) plus four feature plugins, in this order:
3646
- `medusa-base`, `medusa-free-shipping`, `medusa-batch-management`,
3647
- `medusa-affiliate`, `medusa-payload-sync`.
3722
+ ### Root redirect
3648
3723
 
3649
- ### 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.
3650
3732
 
3651
- ```bash
3652
- pnpm add @hartl-services/medusa-base \
3653
- @hartl-services/medusa-free-shipping \
3654
- @hartl-services/medusa-batch-management \
3655
- @hartl-services/medusa-affiliate \
3656
- @hartl-services/medusa-payload-sync \
3657
- @medusajs/event-bus-redis @medusajs/payment-stripe \
3658
- @sentry/node
3659
- ```
3733
+ ### Cart-promotion link repair
3660
3734
 
3661
- ### 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:
3662
3871
 
3663
3872
  ```ts
3664
3873
  // medusa-config.ts
3665
- import { defineFoodShopConfig } 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"
3666
3880
 
3667
- module.exports = defineFoodShopConfig({
3668
- displayName: "EXAMPLE SHOP",
3669
- orderNotificationEmail: "orders@example.com",
3670
- timeZone: "Europe/Vienna",
3671
- storefrontCountryCode: "de",
3672
- })
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)
3673
3890
  ```
3674
3891
 
3675
- A shop that has to adjust the result (a test harness, for example) builds the
3676
- 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):
3677
3901
 
3678
3902
  ```ts
3679
- import { defineConfig } from "@medusajs/framework/utils"
3680
- import { buildFoodShopConfig } from "@hartl-services/medusa-base/shop-config"
3903
+ import { logger } from "@medusajs/framework/logger";
3904
+ import { withSentryErrorReporting } from "@hartl-services/medusa-base/sentry-logger";
3681
3905
 
3682
- const config = buildFoodShopConfig(options)
3683
- config.plugins = config.plugins.filter(
3684
- (plugin) => plugin.resolve !== "@hartl-services/medusa-batch-management",
3685
- )
3686
- module.exports = defineConfig(config)
3906
+ module.exports = defineConfig({
3907
+ logger: withSentryErrorReporting(logger),
3908
+ // ...
3909
+ });
3687
3910
  ```
3688
3911
 
3689
- ### API
3690
-
3691
- - `defineFoodShopConfig(options, environment = process.env)` returns
3692
- `defineConfig(buildFoodShopConfig(options, environment))`.
3693
- - `buildFoodShopConfig(options, environment = process.env)` returns the plain
3694
- config input (`admin`, `logger`, `projectConfig`, `modules`, `plugins`)
3695
- before `defineConfig`. `modules` and `plugins` are ordinary arrays.
3696
- - Types: `FoodShopConfigOptions`, `FoodShopConfigInput`, `FoodShopPlugin`,
3697
- `FoodShopModule`.
3912
+ Initialize Sentry in `instrumentation.ts` as usual; without a `SENTRY_DSN` the
3913
+ wrapper is inert.
3698
3914
 
3699
- Both functions validate the options first and throw a `TypeError` naming the
3700
- option (`Food shop option "timeZone" must be ...`), so a misconfigured shop
3701
- fails at boot. They then load the `.env` file (unless disabled) and read and
3702
- validate the environment; a missing variable fails with the variable's name.
3915
+ ### Sentry instrumentation
3703
3916
 
3704
- ### 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:
3705
3922
 
3706
- | Option | Meaning |
3707
- | --- | --- |
3708
- | `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"). |
3709
- | `orderNotificationEmail` | Receives order notification e-mails; must look like an e-mail address. |
3710
- | `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. |
3711
- | `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=`. |
3712
- | `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. |
3713
- | `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
+ ```
3714
3926
 
3715
- ### 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`.
3716
3930
 
3717
- Required at runtime (during `medusa build`, with `MEDUSA_BUILD_PHASE=true`, the
3718
- plugins' build placeholders stand in for them):
3931
+ ## Development
3719
3932
 
3720
- - Medusa core: `DATABASE_URL`, `STORE_CORS`, `ADMIN_CORS`, `AUTH_CORS`,
3721
- `JWT_SECRET`, `COOKIE_SECRET`, `MEDUSA_BACKEND_URL`, `STOREFRONT_URL` (plain
3722
- HTTP origins are accepted only in development and test).
3723
- - Redis: `REDIS_URL` (optional in development, where Medusa's local event bus
3724
- is used; required elsewhere, `rediss://` with authentication in production
3725
- unless `REDIS_ALLOW_INSECURE_PRIVATE_NETWORK=true`). Never used during a
3726
- build.
3727
- - SMTP: `SMTP_HOST`, `SMTP_PORT`, `SMTP_SECURE`, `SMTP_USER`, `SMTP_PASSWORD`,
3728
- `SMTP_FROM`, `SMTP_REQUIRE_TLS` (outside development and test either
3729
- `SMTP_SECURE` or `SMTP_REQUIRE_TLS` must be `true`).
3730
- - Stripe: `STRIPE_API_KEY` (`sk_...`), `STRIPE_WEBHOOK_SECRET` (`whsec_...`).
3731
- - S3: `S3_CACHE_STORAGE_BUCKET`, `S3_CACHE_STORAGE_REGION`.
3732
- - Documents: `DOCUMENT_PDF_EXECUTABLE`.
3933
+ ### Local e-mail development
3733
3934
 
3734
- Optional: `S3_CACHE_STORAGE_ENDPOINT`, `S3_CACHE_STORAGE_FORCE_PATH_STYLE`,
3735
- `S3_CACHE_STORAGE_PREFIX`, `S3_CACHE_STORAGE_ACCESS_KEY_ID`,
3736
- `S3_CACHE_STORAGE_SECRET_ACCESS_KEY`, `S3_CACHE_STORAGE_SESSION_TOKEN` (omit
3737
- the keys to use the AWS credential chain), `S3_CACHE_STORAGE_CACHE_DIR`
3738
- (default `./.medusa-s3-cache`), `S3_CACHE_STORAGE_TTL_MS`,
3739
- `S3_CACHE_STORAGE_CLEANUP_INTERVAL_MS`,
3740
- `S3_CACHE_STORAGE_EXPIRATION_STRATEGY`,
3741
- `FILE_GATEWAY_PRIVATE_DOWNLOAD_DURATION_SECONDS`, and `SENTRY_DSN` /
3742
- `SENTRY_ENVIRONMENT` for error tracking (see the base's
3743
- `/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.
3744
3940
 
3745
- ### Fixed configuration
3941
+ ### Build output of `/admin-components`
3746
3942
 
3747
- Not configurable on purpose: the plugin order (base first), the Redis event
3748
- bus job policy (3 attempts, exponential backoff from 1 s), S3 cache defaults
3749
- (15 min cleanup, 1 h TTL, absolute expiration), Stripe immediate capture with
3750
- automatic payment methods, the 24 h session lifetime, Admin path `/app`,
3751
- consignment reminders due on day 10, the Documents module always being part of the base, and
3752
- `http.authVerificationsPerActor.customer` (a getter that follows the E-Mail `customer_verification` toggle, see
3753
- "Kontobestätigung per E-Mail"), `http.authMethodsPerActor` (customers: a getter that follows the `customer_auth` switches; Admin users: `["emailpass"]`) and the auth module entry
3754
- (providers `emailpass` and `email-otp`, the latter with `hmacSecret` = `JWT_SECRET`, the same value as `http.jwtSecret`; see
3755
- "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`.