@hartl-services/medusa-base 0.4.0 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.medusa/server/src/admin/index.js +1657 -111
- package/.medusa/server/src/admin/index.mjs +1647 -101
- package/.medusa/server/src/api/admin/email/messages/[id]/resends/route.js +24 -0
- package/.medusa/server/src/api/admin/email/messages/[id]/route.js +12 -0
- package/.medusa/server/src/api/admin/email/messages/route.js +12 -0
- package/.medusa/server/src/api/admin/email/terms-versions/[id]/download/route.js +16 -0
- package/.medusa/server/src/api/admin/email/terms-versions/route.js +47 -0
- package/.medusa/server/src/api/admin/orders/[id]/documents/[document_id]/download/route.js +21 -0
- package/.medusa/server/src/api/admin/orders/[id]/documents/[document_id]/regenerate/route.js +38 -0
- package/.medusa/server/src/api/admin/orders/[id]/documents/regenerate/route.js +32 -0
- package/.medusa/server/src/api/admin/orders/[id]/documents/route.js +36 -0
- package/.medusa/server/src/api/admin/orders/[id]/packing-lists/[fulfillment_id]/download/route.js +24 -0
- package/.medusa/server/src/api/admin/orders/[id]/packing-lists/[fulfillment_id]/retry/route.js +44 -0
- package/.medusa/server/src/api/admin/orders/[id]/packing-lists/route.js +82 -0
- package/.medusa/server/src/api/document-download-response.js +16 -0
- package/.medusa/server/src/api/document-response.js +23 -0
- package/.medusa/server/src/api/files/[asset]/route.js +24 -0
- package/.medusa/server/src/api/files/file-response.js +43 -0
- package/.medusa/server/src/api/files/gateway-options.js +31 -0
- package/.medusa/server/src/api/files/private/[token]/route.js +29 -0
- package/.medusa/server/src/api/files/upload/[token]/route.js +66 -0
- package/.medusa/server/src/api/middlewares.js +168 -1
- package/.medusa/server/src/api/packing-list-response.js +22 -0
- package/.medusa/server/src/api/route.js +27 -0
- package/.medusa/server/src/api/store/documents/[id]/download/route.js +21 -0
- package/.medusa/server/src/api/store/documents/[id]/route.js +15 -0
- package/.medusa/server/src/api/store/documents/route.js +11 -0
- package/.medusa/server/src/api/terms-download-response.js +12 -0
- package/.medusa/server/src/documents/config/index.js +18 -0
- package/.medusa/server/src/documents/paths/documents.js +6 -0
- package/.medusa/server/src/documents/paths/index.js +18 -0
- package/.medusa/server/src/documents/plugin-features.js +38 -0
- package/.medusa/server/src/documents/schemas/documents.js +18 -0
- package/.medusa/server/src/documents/schemas/index.js +18 -0
- package/.medusa/server/src/documents/scripts/verify-documents-file-storage.js +55 -0
- package/.medusa/server/src/documents/templates/financial-document.js +160 -0
- package/.medusa/server/src/documents/templates/format.js +73 -0
- package/.medusa/server/src/documents/templates/html-factory.js +42 -0
- package/.medusa/server/src/documents/templates/html-utils.js +23 -0
- package/.medusa/server/src/documents/templates/layout.js +142 -0
- package/.medusa/server/src/documents/templates/packing-list.js +52 -0
- package/.medusa/server/src/documents/types/admin-api.js +3 -0
- package/.medusa/server/src/documents/types/document-summary.js +3 -0
- package/.medusa/server/src/documents/types/documents.js +3 -0
- package/.medusa/server/src/documents/types/html-factory.js +6 -0
- package/.medusa/server/src/documents/types/index.js +22 -0
- package/.medusa/server/src/documents/types/store-api.js +3 -0
- package/.medusa/server/src/documents/utils/document-merchant.js +22 -0
- package/.medusa/server/src/documents/utils/document-render-settings.js +59 -0
- package/.medusa/server/src/documents/utils/html-to-pdf.js +61 -0
- package/.medusa/server/src/documents/utils/private-file-storage.js +64 -0
- package/.medusa/server/src/documents/utils/resolve-services.js +7 -0
- package/.medusa/server/src/email/application/handle-invite-event.js +318 -0
- package/.medusa/server/src/email/application/handle-password-reset-event.js +366 -0
- package/.medusa/server/src/email/config/index.js +84 -0
- package/.medusa/server/src/email/config/notification-module.js +20 -0
- package/.medusa/server/src/email/domain/business-keys.js +50 -0
- package/.medusa/server/src/email/domain/documents-fact-fingerprint.js +7 -0
- package/.medusa/server/src/email/domain/event-schemas.js +36 -0
- package/.medusa/server/src/email/domain/failure-classification.js +53 -0
- package/.medusa/server/src/email/domain/order-summary.js +103 -0
- package/.medusa/server/src/email/domain/refund-summary.js +43 -0
- package/.medusa/server/src/email/domain/return-summary.js +118 -0
- package/.medusa/server/src/email/domain/shipment-summary.js +74 -0
- package/.medusa/server/src/email/plugin-features.js +39 -0
- package/.medusa/server/src/email/rendering/default-templates.js +719 -0
- package/.medusa/server/src/email/rendering/load-email-shop-details.js +36 -0
- package/.medusa/server/src/email/rendering/render-email.js +50 -0
- package/.medusa/server/src/email/rendering/resolve-shop-configuration.js +67 -0
- package/.medusa/server/src/email/types/email-kinds.js +18 -0
- package/.medusa/server/src/email/types/index.js +21 -0
- package/.medusa/server/src/email/types/shop-configuration.js +3 -0
- package/.medusa/server/src/email/types/smtp-configuration.js +3 -0
- package/.medusa/server/src/email/types/template-data.js +232 -0
- package/.medusa/server/src/files/config/index.js +256 -0
- package/.medusa/server/src/files/gateway/index.js +115 -0
- package/.medusa/server/src/files/types/index.js +6 -0
- package/.medusa/server/src/files/types/public.js +6 -0
- package/.medusa/server/src/jobs/document-fact-publication-reconciliation.js +18 -0
- package/.medusa/server/src/jobs/document-resolution-reconciliation.js +20 -0
- package/.medusa/server/src/jobs/packing-list-reconciliation.js +49 -0
- package/.medusa/server/src/jobs/reconcile-email-attempts.js +63 -0
- package/.medusa/server/src/jobs/reconcile-pending-email-messages.js +41 -0
- package/.medusa/server/src/jobs/retry-email-messages.js +27 -0
- package/.medusa/server/src/migration-scripts/dismiss-orphaned-cart-promotion-links.js +26 -0
- package/.medusa/server/src/modules/documents/domain/cancellation-snapshot.js +97 -0
- package/.medusa/server/src/modules/documents/domain/document-number.js +36 -0
- package/.medusa/server/src/modules/documents/domain/document-resolution.js +24 -0
- package/.medusa/server/src/modules/documents/domain/issuance-rules.js +37 -0
- package/.medusa/server/src/modules/documents/domain/merchant.js +86 -0
- package/.medusa/server/src/modules/documents/domain/options.js +40 -0
- package/.medusa/server/src/modules/documents/domain/packing-list.js +90 -0
- package/.medusa/server/src/modules/documents/domain/snapshot-normalization.js +17 -0
- package/.medusa/server/src/modules/documents/domain/tax-breakdown.js +66 -0
- package/.medusa/server/src/modules/documents/index.js +15 -0
- package/.medusa/server/src/modules/documents/loaders/assert-private-file-storage.js +20 -0
- package/.medusa/server/src/modules/documents/migrations/Migration20260827061749.js +75 -0
- package/.medusa/server/src/modules/documents/migrations/Migration20260830111207.js +28 -0
- package/.medusa/server/src/modules/documents/models/document-artifact.js +25 -0
- package/.medusa/server/src/modules/documents/models/document-number-sequence.js +29 -0
- package/.medusa/server/src/modules/documents/models/document.js +82 -0
- package/.medusa/server/src/modules/documents/models/order-cancellation-resolution.js +51 -0
- package/.medusa/server/src/modules/documents/models/packing-list-revision.js +36 -0
- package/.medusa/server/src/modules/documents/models/packing-list.js +27 -0
- package/.medusa/server/src/modules/documents/models/refund-document-resolution.js +66 -0
- package/.medusa/server/src/modules/documents/models/shipment-invoice-assignment.js +51 -0
- package/.medusa/server/src/modules/documents/models/shipment-invoice-coordinator.js +44 -0
- package/.medusa/server/src/modules/documents/service.js +1701 -0
- package/.medusa/server/src/modules/email/domain/attachment.js +25 -0
- package/.medusa/server/src/modules/email/domain/message-state.js +69 -0
- package/.medusa/server/src/modules/email/index.js +13 -0
- package/.medusa/server/src/modules/email/migrations/Migration20260826173422.js +32 -0
- package/.medusa/server/src/modules/email/migrations/Migration20260827172945.js +16 -0
- package/.medusa/server/src/modules/email/migrations/Migration20260827194804.js +22 -0
- package/.medusa/server/src/modules/email/migrations/Migration20260906195205.js +16 -0
- package/.medusa/server/src/modules/email/models/email-attempt.js +56 -0
- package/.medusa/server/src/modules/email/models/email-message.js +71 -0
- package/.medusa/server/src/modules/email/models/email-terms-version.js +37 -0
- package/.medusa/server/src/modules/email/service.js +1285 -0
- package/.medusa/server/src/modules/plugin-features/domain/plugin-settings.js +4 -3
- package/.medusa/server/src/providers/s3-cache/cache/atomic-write.js +25 -0
- package/.medusa/server/src/providers/s3-cache/cache/cache-manager.js +115 -0
- package/.medusa/server/src/providers/s3-cache/cache/keyed-lock.js +28 -0
- package/.medusa/server/src/providers/s3-cache/index.js +8 -0
- package/.medusa/server/src/providers/s3-cache/object-key.js +53 -0
- package/.medusa/server/src/providers/s3-cache/s3-cache-file.js +229 -0
- package/.medusa/server/src/providers/s3-cache/s3-object-store.js +86 -0
- package/.medusa/server/src/providers/smtp/index.js +8 -0
- package/.medusa/server/src/providers/smtp/service.js +236 -0
- package/.medusa/server/src/providers/smtp/types.js +11 -0
- package/.medusa/server/src/public/config.js +186 -0
- package/.medusa/server/src/public/descriptor.js +3 -1
- package/.medusa/server/src/public/gateway.js +19 -0
- package/.medusa/server/src/public/instrumentation.js +56 -0
- package/.medusa/server/src/public/shop-config.js +12 -0
- package/.medusa/server/src/public/shop-master-data.js +2 -2
- package/.medusa/server/src/public/shop-types.js +24 -0
- package/.medusa/server/src/shop-config/build-food-shop-config.js +252 -0
- package/.medusa/server/src/shop-config/options.js +75 -0
- package/.medusa/server/src/subscribers/affiliate-consignment-reminder-due.js +26 -0
- package/.medusa/server/src/subscribers/documents-order-canceled.js +24 -0
- package/.medusa/server/src/subscribers/documents-order-cancellation-ready.js +26 -0
- package/.medusa/server/src/subscribers/documents-refund-ready.js +24 -0
- package/.medusa/server/src/subscribers/documents-shipment-created.js +36 -0
- package/.medusa/server/src/subscribers/documents-shipment-invoice-assignment-decided.js +26 -0
- package/.medusa/server/src/subscribers/email-order-canceled.js +28 -0
- package/.medusa/server/src/subscribers/email-shipment-created.js +32 -0
- package/.medusa/server/src/subscribers/order-placed.js +27 -0
- package/.medusa/server/src/subscribers/packing-list-allocation-revised.js +32 -0
- package/.medusa/server/src/subscribers/packing-list-fulfillment-canceled.js +54 -0
- package/.medusa/server/src/subscribers/packing-list-fulfillment-created.js +31 -0
- package/.medusa/server/src/subscribers/password-reset.js +23 -0
- package/.medusa/server/src/subscribers/payment-refunded.js +38 -0
- package/.medusa/server/src/subscribers/return-received.js +27 -0
- package/.medusa/server/src/subscribers/return-requested.js +27 -0
- package/.medusa/server/src/subscribers/user-invite.js +23 -0
- package/.medusa/server/src/utils/cart-promotion-links.js +101 -0
- package/.medusa/server/src/workflows/documents/issue-cancellation-after-refund.js +246 -0
- package/.medusa/server/src/workflows/documents/issue-document.js +165 -0
- package/.medusa/server/src/workflows/documents/issue-invoice-after-shipment.js +521 -0
- package/.medusa/server/src/workflows/documents/issue-packing-list.js +390 -0
- package/.medusa/server/src/workflows/documents/publish-document-facts.js +53 -0
- package/.medusa/server/src/workflows/documents/reconcile-document-resolutions.js +48 -0
- package/.medusa/server/src/workflows/documents/reconcile-order-cancellation.js +92 -0
- package/.medusa/server/src/workflows/documents/regenerate-issued-document.js +61 -0
- package/.medusa/server/src/workflows/email/dispatch-email-message.js +301 -0
- package/.medusa/server/src/workflows/email/documents-required-facts.js +30 -0
- package/.medusa/server/src/workflows/email/prepare-affiliate-reminder-email.js +58 -0
- package/.medusa/server/src/workflows/email/prepare-order-canceled-email.js +102 -0
- package/.medusa/server/src/workflows/email/prepare-order-received-email.js +82 -0
- package/.medusa/server/src/workflows/email/prepare-refund-email.js +71 -0
- package/.medusa/server/src/workflows/email/prepare-return-email.js +136 -0
- package/.medusa/server/src/workflows/email/prepare-shipment-email.js +115 -0
- package/.medusa/server/src/workflows/email/retry-email-messages.js +31 -0
- package/.medusa/server/src/workflows/hooks/dismiss-deleted-promotion-cart-links.js +23 -0
- package/README.md +1777 -8
- package/dist/admin-components/features/plugin-features/components/ImageSettingInput.d.ts +15 -0
- package/dist/admin-components/features/plugin-features/components/ImageSettingInput.js +42 -0
- package/dist/admin-components/features/plugin-features/components/PluginSettingsForm.js +4 -2
- package/dist/admin-components/features/plugin-features/hooks/usePluginFeatureEnabled.d.ts +1 -1
- package/dist/admin-components/features/plugin-features/hooks/usePluginFeatures.d.ts +4 -4
- package/dist/admin-components/features/plugin-features/hooks/usePluginSettingsForm.d.ts +1 -1
- package/dist/admin-components/features/plugin-features/lib/image-upload.d.ts +6 -0
- package/dist/admin-components/features/plugin-features/lib/image-upload.js +17 -0
- package/dist/admin-components/features/plugin-features/lib/plugin-features-admin-client.d.ts +4 -4
- package/dist/admin-components/i18n/texts.d.ts +6 -0
- package/dist/admin-components/i18n/texts.js +6 -0
- package/dist/documents/config/index.d.ts +11 -0
- package/dist/documents/config/index.js +17 -0
- package/dist/documents/templates/financial-document.d.ts +3 -0
- package/dist/documents/templates/financial-document.js +159 -0
- package/dist/documents/templates/format.d.ts +30 -0
- package/dist/documents/templates/format.js +72 -0
- package/dist/documents/templates/html-factory.d.ts +15 -0
- package/dist/documents/templates/html-factory.js +41 -0
- package/dist/documents/templates/html-utils.d.ts +8 -0
- package/dist/documents/templates/html-utils.js +22 -0
- package/dist/documents/templates/layout.d.ts +35 -0
- package/dist/documents/templates/layout.js +141 -0
- package/dist/documents/templates/packing-list.d.ts +3 -0
- package/dist/documents/templates/packing-list.js +51 -0
- package/dist/documents/types/admin-api.d.ts +1 -0
- package/dist/documents/types/admin-api.js +2 -0
- package/dist/documents/types/document-summary.d.ts +1 -0
- package/dist/documents/types/document-summary.js +2 -0
- package/dist/documents/types/documents.d.ts +191 -0
- package/dist/documents/types/documents.js +2 -0
- package/dist/documents/types/html-factory.d.ts +8 -0
- package/dist/documents/types/html-factory.js +5 -0
- package/dist/documents/types/index.d.ts +5 -0
- package/dist/documents/types/index.js +21 -0
- package/dist/documents/types/store-api.d.ts +1 -0
- package/dist/documents/types/store-api.js +2 -0
- package/dist/email/config/index.d.ts +17 -0
- package/dist/email/config/index.js +83 -0
- package/dist/email/config/notification-module.d.ts +22 -0
- package/dist/email/config/notification-module.js +19 -0
- package/dist/email/types/email-kinds.d.ts +15 -0
- package/dist/email/types/email-kinds.js +17 -0
- package/dist/email/types/index.d.ts +4 -0
- package/dist/email/types/index.js +20 -0
- package/dist/email/types/shop-configuration.d.ts +68 -0
- package/dist/email/types/shop-configuration.js +2 -0
- package/dist/email/types/smtp-configuration.d.ts +9 -0
- package/dist/email/types/smtp-configuration.js +2 -0
- package/dist/email/types/template-data.d.ts +609 -0
- package/dist/email/types/template-data.js +231 -0
- package/dist/files/config/index.d.ts +32 -0
- package/dist/files/config/index.js +255 -0
- package/dist/files/gateway/index.d.ts +30 -0
- package/dist/files/gateway/index.js +114 -0
- package/dist/files/types/index.d.ts +2 -0
- package/dist/files/types/index.js +5 -0
- package/dist/files/types/public.d.ts +90 -0
- package/dist/files/types/public.js +5 -0
- package/dist/modules/documents/domain/merchant.d.ts +22 -0
- package/dist/modules/documents/domain/merchant.js +85 -0
- package/dist/modules/documents/domain/options.d.ts +34 -0
- package/dist/modules/documents/domain/options.js +39 -0
- package/dist/public/config.d.ts +59 -0
- package/dist/public/config.js +185 -0
- package/dist/public/descriptor.js +2 -0
- package/dist/public/gateway.d.ts +2 -0
- package/dist/public/gateway.js +18 -0
- package/dist/public/instrumentation.d.ts +12 -0
- package/dist/public/instrumentation.js +55 -0
- package/dist/public/shop-config.d.ts +8 -0
- package/dist/public/shop-config.js +11 -0
- package/dist/public/shop-master-data.d.ts +1 -1
- package/dist/public/shop-master-data.js +1 -1
- package/dist/public/shop-types.d.ts +7 -0
- package/dist/public/shop-types.js +23 -0
- package/dist/public/types.d.ts +19 -2
- package/dist/shop-config/build-food-shop-config.d.ts +44 -0
- package/dist/shop-config/build-food-shop-config.js +251 -0
- package/dist/shop-config/options.d.ts +48 -0
- package/dist/shop-config/options.js +74 -0
- package/package.json +48 -5
package/README.md
CHANGED
|
@@ -4,7 +4,10 @@ Medusa v2.19 base plugin for Hartl Services shops. Every shop installs it. It
|
|
|
4
4
|
provides per-plugin feature toggles, runtime settings and actions: a generic
|
|
5
5
|
Admin hub under **Settings → Hartl Services Plugins** with one page per
|
|
6
6
|
plugin, a Store endpoint for storefronts and helpers that plugins use to
|
|
7
|
-
enforce their toggles and read their settings.
|
|
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
|
+
(see "E-Mail", "Dokumente", "Dateispeicher (S3)" and "Shop-Config-Preset").
|
|
8
11
|
|
|
9
12
|
## What it does
|
|
10
13
|
|
|
@@ -33,6 +36,19 @@ enforce their toggles and read their settings.
|
|
|
33
36
|
- Owns the `completeCartWorkflow` hooks `validate` and `orderCreated` and runs
|
|
34
37
|
the workflows that plugins declare for them (see "Complete-cart hook
|
|
35
38
|
composition").
|
|
39
|
+
- Redirects `GET /` to the admin dashboard and repairs dangling
|
|
40
|
+
`cart_promotion` links (see "Root redirect" and "Cart-promotion link
|
|
41
|
+
repair").
|
|
42
|
+
- Exports the shop's `medusa-config.ts` helpers from
|
|
43
|
+
`@hartl-services/medusa-base/config` and Sentry's `register()` from
|
|
44
|
+
`@hartl-services/medusa-base/instrumentation` (see "Config helpers" and
|
|
45
|
+
"Sentry instrumentation").
|
|
46
|
+
- Sends the shop's transactional e-mails over SMTP (module `email`, see
|
|
47
|
+
"E-Mail"), issues invoices, cancellation documents and packing lists as
|
|
48
|
+
private PDFs (module `documents`, see "Dokumente") and stores files in
|
|
49
|
+
S3-compatible storage behind the `/files` gateway (see "Dateispeicher (S3)").
|
|
50
|
+
- Builds a food shop's complete `medusa-config.ts` from a few shop facts
|
|
51
|
+
(`@hartl-services/medusa-base/shop-config`, see "Shop-Config-Preset").
|
|
36
52
|
|
|
37
53
|
## Installation in a shop
|
|
38
54
|
|
|
@@ -49,18 +65,46 @@ plugins: [
|
|
|
49
65
|
options: {
|
|
50
66
|
// Optional: key material for secret settings (see "Secrets").
|
|
51
67
|
settingsSecret: process.env.PLUGIN_SETTINGS_SECRET,
|
|
68
|
+
// E-Mail, Documents and the /files gateway: see the sections below
|
|
69
|
+
// (or let the Shop-Config-Preset fill them in).
|
|
70
|
+
// email: { jwtSecret, shop },
|
|
71
|
+
// documents: { merchant, numbering, pdf, htmlFactory },
|
|
72
|
+
// files: { gateway },
|
|
52
73
|
},
|
|
53
74
|
},
|
|
54
75
|
// … all other plugins
|
|
55
76
|
]
|
|
56
77
|
```
|
|
57
78
|
|
|
58
|
-
The
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
`
|
|
79
|
+
The options are `BasePluginOptions`. Medusa hands the same options object to
|
|
80
|
+
every module of the plugin; each module reads only its own namespace:
|
|
81
|
+
|
|
82
|
+
| Option | Read by | Meaning |
|
|
83
|
+
|---|---|---|
|
|
84
|
+
| `settingsSecret` | `plugin_features` module | Key material for stored secret settings. Set it from an environment variable of the shop; unset or empty, the base uses `projectConfig.http.cookieSecret`. |
|
|
85
|
+
| `email` | `email` module | `{ jwtSecret, shop }`: branding, URLs and the JWT secret of the e-mail links (see "E-Mail"). Without `email.shop` the module boots but sends nothing. |
|
|
86
|
+
| `documents` | `documents` module | `{ merchant?, numbering?, pdf?, htmlFactory? }` (see "Dokumente"). Defaults: numbering time zone `Europe/Vienna`, PDF executable `google-chrome`. |
|
|
87
|
+
| `files` | `/files` routes | `{ gateway: { signingSecret } }`, returned by `createS3CacheStorageConfig` (see "Dateispeicher (S3)"). |
|
|
88
|
+
|
|
89
|
+
`options: {}` is valid: the base then only provides the feature toggles,
|
|
90
|
+
settings, actions and hooks, the e-mail module cannot dispatch, documents use
|
|
91
|
+
the defaults and the `/files` routes answer that signing is not configured.
|
|
92
|
+
Run `medusa db:migrate` after installing it (and after upgrading: 0.4.0 adds
|
|
93
|
+
the tables `plugin_setting_state` and `plugin_action_run`; 1.0.0 brings the
|
|
94
|
+
`email` and `documents` tables of the former `@hartl-services/medusa-email` and
|
|
95
|
+
`@hartl-services/medusa-documents` packages with unchanged table and
|
|
96
|
+
migration names, so a shop that used them needs no data migration).
|
|
97
|
+
|
|
98
|
+
Upgrading from the separate packages: remove `@hartl-services/medusa-email`,
|
|
99
|
+
`@hartl-services/medusa-documents`, `@hartl-services/medusa-s3-cache-storage`
|
|
100
|
+
and `@hartl-services/medusa-food-shop-config` from `package.json` and
|
|
101
|
+
`medusa-config.ts`, move their options into the namespaces above, change the
|
|
102
|
+
provider resolve paths to `@hartl-services/medusa-base/providers/smtp` and
|
|
103
|
+
`@hartl-services/medusa-base/providers/s3-cache` (the provider ids `smtp` and
|
|
104
|
+
`s3-cache` are unchanged) and import their `/config`, `/types` and `/gateway`
|
|
105
|
+
exports from the matching `@hartl-services/medusa-base` subpath. The
|
|
106
|
+
`documentsEnabled` option is gone; Documents is always installed, and the
|
|
107
|
+
e-mail module follows the Documents `invoices` toggle.
|
|
64
108
|
|
|
65
109
|
Peer dependencies are the host's Medusa 2.19 packages (`@medusajs/framework`,
|
|
66
110
|
`@medusajs/medusa`, `@medusajs/admin-sdk`, `@medusajs/js-sdk`, `@medusajs/ui`)
|
|
@@ -71,6 +115,7 @@ dashboard uses.
|
|
|
71
115
|
|
|
72
116
|
| Method | Path | Auth | Response |
|
|
73
117
|
|---|---|---|---|
|
|
118
|
+
| `GET` | `/` | none | 302 redirect to the configured `admin.path` (default `/app`) |
|
|
74
119
|
| `GET` | `/store/plugin-features` | publishable key | `StorePluginFeaturesListResponse` |
|
|
75
120
|
| `GET` | `/store/plugin-features/:plugin_key` | publishable key | `StorePluginFeaturesResponse`, 404 for an unknown plugin |
|
|
76
121
|
| `GET` | `/admin/plugin-features` | Admin user | `AdminPluginFeaturesListResponse` |
|
|
@@ -273,7 +318,10 @@ class DemoModuleService extends MedusaService({ /* models */ }) {
|
|
|
273
318
|
|
|
274
319
|
- Types: `string`, `url` (http/https), `number` (finite, optional inclusive
|
|
275
320
|
`min`/`max`), `boolean`, `secret`, `select` (`options: [{ value, label }]`,
|
|
276
|
-
non-empty, unique values; the stored value must be one of them)
|
|
321
|
+
non-empty, unique values; the stored value must be one of them), `image`
|
|
322
|
+
(the http(s) URL of a PNG/JPEG of at most 2 MiB that the Admin uploads
|
|
323
|
+
through Medusa's `/admin/uploads` into the public File Module storage and
|
|
324
|
+
previews; replacing or removing it leaves the old file in storage). Keys
|
|
277
325
|
follow the feature key format and are unique. A `secret` has no default;
|
|
278
326
|
any other default must match its type, options and bounds. `options` exist
|
|
279
327
|
only on `select`, `min`/`max` only on `number`. `required` defaults to
|
|
@@ -634,3 +682,1724 @@ module.exports = defineConfig({
|
|
|
634
682
|
|
|
635
683
|
Initialize Sentry in `instrumentation.ts` as usual; without a `SENTRY_DSN` the
|
|
636
684
|
wrapper is inert.
|
|
685
|
+
|
|
686
|
+
## Root redirect
|
|
687
|
+
|
|
688
|
+
Medusa serves nothing at `/`, so a deployed domain answers it with a 404. The
|
|
689
|
+
base registers `GET /` and answers with a `302` to the shop's `admin.path`
|
|
690
|
+
(read from the config module, fallback `/app`), so opening the bare domain
|
|
691
|
+
lands on the admin dashboard in every shop and in local development without a
|
|
692
|
+
reverse-proxy rule. The redirect is temporary on purpose: browsers cache a
|
|
693
|
+
`301` on the bare domain essentially forever, which would be painful to undo
|
|
694
|
+
should `/` ever get real content. A shop that needs its own `/` defines
|
|
695
|
+
`src/api/route.ts` itself.
|
|
696
|
+
|
|
697
|
+
## Cart-promotion link repair
|
|
698
|
+
|
|
699
|
+
Medusa's `deletePromotionsWorkflow` only soft-deletes a promotion and leaves
|
|
700
|
+
its `cart_promotion` link rows behind, so every cart that had it applied
|
|
701
|
+
reads back a `null` entry inside `cart.promotions`. The base removes those
|
|
702
|
+
links itself:
|
|
703
|
+
|
|
704
|
+
- The `deletePromotionsWorkflow.hooks.promotionsDeleted` hook
|
|
705
|
+
(`workflows/hooks/dismiss-deleted-promotion-cart-links`) dismisses the
|
|
706
|
+
links of the deleted promotions and restores them if a later step fails.
|
|
707
|
+
- The migration script `dismiss-orphaned-cart-promotion-links` runs once in
|
|
708
|
+
`medusa db:migrate` and removes the links that already dangle from earlier
|
|
709
|
+
deletions. Medusa tracks migration scripts by file name in
|
|
710
|
+
`script_migrations`, so the name must not change.
|
|
711
|
+
|
|
712
|
+
Nothing to configure; `medusa db:migrate` after installing or upgrading to
|
|
713
|
+
0.4.1 is enough.
|
|
714
|
+
|
|
715
|
+
## Config helpers
|
|
716
|
+
|
|
717
|
+
`@hartl-services/medusa-base/config` holds the generic environment helpers a
|
|
718
|
+
shop's `medusa-config.ts` needs, so every shop fails at startup with the name
|
|
719
|
+
of the missing variable instead of a runtime crash. The environment helpers
|
|
720
|
+
have no dependency on Medusa; the subpath does not load `@sentry/node`.
|
|
721
|
+
|
|
722
|
+
| Export | Purpose |
|
|
723
|
+
|---|---|
|
|
724
|
+
| `ConfigurationPhase` | `"build" \| "runtime"` |
|
|
725
|
+
| `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` |
|
|
726
|
+
| `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 |
|
|
727
|
+
| `optionalEnvironment`, `integerEnvironment`, `booleanEnvironment` | Trimmed optional string, range-checked integer, strict `"true"`/`"false"` boolean |
|
|
728
|
+
| `allowsInsecureLocalServices(environment, phase)` | `true` for build, development and test |
|
|
729
|
+
| `resolvePublicOrigin(value, name, allowHttp)`, `shopUrl(origin, path)` | Validate a bare HTTP(S) origin; build an absolute URL that cannot leave the origin |
|
|
730
|
+
| `resolveStripe(values)` | Checks the `sk_` key and the `whsec_` webhook secret |
|
|
731
|
+
| `resolveRedisEventBus(environment, phase)` | `{ url } \| undefined`: optional in development, required at runtime elsewhere, never used in a build, authenticated `rediss://` in production |
|
|
732
|
+
| `createSmtpNotificationModuleConfig`, `resolveSmtp`, `SMTP_ENVIRONMENT_VARIABLES`, `SMTP_BUILD_PLACEHOLDERS`, `defineShopEmailConfiguration` | SMTP notification provider and E-Mail configuration (see "E-Mail") |
|
|
733
|
+
| `createS3CacheStorageConfig`, `createS3CacheFileModuleConfig`, `resolveS3Credentials`, `resolveExpirationStrategy`, `S3_BUILD_PLACEHOLDERS` | S3 file storage (see "Dateispeicher (S3)") |
|
|
734
|
+
| `createDefaultDocumentHtmlFactory`, `DOCUMENTS_BUILD_PLACEHOLDERS` | Documents markup and build placeholder (see "Dokumente") |
|
|
735
|
+
|
|
736
|
+
`medusa build` loads the configuration without deployment secrets. Each
|
|
737
|
+
plugin's own placeholders (SMTP, S3, PDF executable, ...) live with that
|
|
738
|
+
plugin; the shop merges them:
|
|
739
|
+
|
|
740
|
+
```ts
|
|
741
|
+
// medusa-config.ts
|
|
742
|
+
import {
|
|
743
|
+
MEDUSA_BUILD_PLACEHOLDERS,
|
|
744
|
+
requireEnvironment,
|
|
745
|
+
resolveRedisEventBus,
|
|
746
|
+
type ConfigurationPhase,
|
|
747
|
+
} from "@hartl-services/medusa-base/config"
|
|
748
|
+
|
|
749
|
+
const phase: ConfigurationPhase =
|
|
750
|
+
process.env.MEDUSA_BUILD_PHASE === "true" ? "build" : "runtime"
|
|
751
|
+
const env = requireEnvironment(
|
|
752
|
+
process.env,
|
|
753
|
+
["DATABASE_URL", "JWT_SECRET", "COOKIE_SECRET"],
|
|
754
|
+
phase,
|
|
755
|
+
{ ...MEDUSA_BUILD_PLACEHOLDERS /* , ...other plugins' placeholders */ },
|
|
756
|
+
)
|
|
757
|
+
const redis = resolveRedisEventBus(process.env, phase)
|
|
758
|
+
```
|
|
759
|
+
|
|
760
|
+
## Sentry instrumentation
|
|
761
|
+
|
|
762
|
+
`@hartl-services/medusa-base/instrumentation` exports `register()`, the hook
|
|
763
|
+
Medusa's start command calls from the shop's `instrumentation.ts`. It runs
|
|
764
|
+
`Sentry.init` with `SENTRY_DSN`, `SENTRY_ENVIRONMENT` (fallback `NODE_ENV`)
|
|
765
|
+
and `tracesSampleRate: 0`; without a `SENTRY_DSN` Sentry stays inert. The
|
|
766
|
+
shop's `instrumentation.ts` is one line:
|
|
767
|
+
|
|
768
|
+
```ts
|
|
769
|
+
export { register } from "@hartl-services/medusa-base/instrumentation"
|
|
770
|
+
```
|
|
771
|
+
|
|
772
|
+
`@sentry/node` stays an optional peer dependency of the base: a shop that uses
|
|
773
|
+
this export (or `sentry-logger`) lists `@sentry/node` in its own
|
|
774
|
+
`dependencies`.
|
|
775
|
+
|
|
776
|
+
## E-Mail
|
|
777
|
+
|
|
778
|
+
The base owns transactional e-mail for the shop: logical email messages,
|
|
779
|
+
attempts, rendering, the Medusa Notification integration, SMTP transport,
|
|
780
|
+
retries and e-mail-specific Admin operations. It is the `email` module, its
|
|
781
|
+
SMTP notification provider (`providers/smtp`), its subscribers, jobs and
|
|
782
|
+
workflows, and the Admin page `/settings/hartl-services/email`. There is no
|
|
783
|
+
separate e-mail package since 1.0.0.
|
|
784
|
+
|
|
785
|
+
### Configuration
|
|
786
|
+
|
|
787
|
+
The SMTP notification provider is a `modules` entry built by
|
|
788
|
+
`createSmtpNotificationModuleConfig` (from
|
|
789
|
+
`@hartl-services/medusa-base/config`); the branding and rendering
|
|
790
|
+
configuration goes into the `email` namespace of the base options:
|
|
791
|
+
|
|
792
|
+
```ts
|
|
793
|
+
import {
|
|
794
|
+
createSmtpNotificationModuleConfig,
|
|
795
|
+
resolveSmtp,
|
|
796
|
+
SMTP_ENVIRONMENT_VARIABLES,
|
|
797
|
+
} from "@hartl-services/medusa-base/config";
|
|
798
|
+
|
|
799
|
+
// Validates the seven SMTP_* variables (see "SMTP environment helpers").
|
|
800
|
+
const smtp = resolveSmtp(
|
|
801
|
+
Object.fromEntries(
|
|
802
|
+
SMTP_ENVIRONMENT_VARIABLES.map((name) => [name, process.env[name] ?? ""]),
|
|
803
|
+
) as Record<(typeof SMTP_ENVIRONMENT_VARIABLES)[number], string>,
|
|
804
|
+
process.env.NODE_ENV === "development" || process.env.NODE_ENV === "test",
|
|
805
|
+
);
|
|
806
|
+
|
|
807
|
+
export default defineConfig({
|
|
808
|
+
modules: [createSmtpNotificationModuleConfig(smtp)],
|
|
809
|
+
plugins: [
|
|
810
|
+
{
|
|
811
|
+
resolve: "@hartl-services/medusa-base",
|
|
812
|
+
options: {
|
|
813
|
+
email: {
|
|
814
|
+
jwtSecret: process.env.JWT_SECRET!,
|
|
815
|
+
shop: {
|
|
816
|
+
displayName: "Mein Shop",
|
|
817
|
+
affiliatePortalUrl: "https://shop.example.com/de/partner",
|
|
818
|
+
inviteUrl: ({ token }) =>
|
|
819
|
+
`https://shop.example.com/app/invite?token=${token}`,
|
|
820
|
+
orderNotificationEmail: "orders@shop.example.com",
|
|
821
|
+
passwordResetUrl: ({ locale }) =>
|
|
822
|
+
`https://shop.example.com/${locale ?? "de"}/passwort-zuruecksetzen`,
|
|
823
|
+
}, // a ShopEmailConfiguration, see `ShopEmailConfiguration` in `@hartl-services/medusa-base/types`
|
|
824
|
+
},
|
|
825
|
+
},
|
|
826
|
+
},
|
|
827
|
+
],
|
|
828
|
+
});
|
|
829
|
+
```
|
|
830
|
+
|
|
831
|
+
Options of the `email` namespace:
|
|
832
|
+
|
|
833
|
+
- `jwtSecret` — used for Email's own tokens; typically the same secret as
|
|
834
|
+
`projectConfig.http.jwtSecret`.
|
|
835
|
+
- `shop` — a `ShopEmailConfiguration` object: `displayName`,
|
|
836
|
+
`affiliatePortalUrl`, the `passwordResetUrl`/`inviteUrl` builders and the
|
|
837
|
+
`orderNotificationEmail` mailbox. `layout` and `renderers` are optional
|
|
838
|
+
overrides, see "Shop configuration and default templates".
|
|
839
|
+
|
|
840
|
+
Without `email.shop` the module still boots, but it cannot dispatch: no mail
|
|
841
|
+
is sent (the dispatch configuration is requested lazily and fails with
|
|
842
|
+
`UNEXPECTED_STATE`). Shipment and cancellation e-mails wait for a Documents
|
|
843
|
+
fact only while the Documents `invoices` feature is on when the message is
|
|
844
|
+
created (with `invoices` off, or the Documents plugin switched off, Documents
|
|
845
|
+
publishes no fact, so the mail is sent without a document). Refund e-mails
|
|
846
|
+
come only from the Documents refund fact, so there are none while `invoices`
|
|
847
|
+
is off. The former `documentsEnabled` option no longer exists: Documents is
|
|
848
|
+
always part of the base.
|
|
849
|
+
|
|
850
|
+
The event contracts the module consumes for shipment, cancellation, refund and
|
|
851
|
+
affiliate-reminder e-mails come from
|
|
852
|
+
`@hartl-services/medusa-food-supplements-contracts/events/documents` and
|
|
853
|
+
`/events/affiliate`, so installing the base does not pull in Affiliate.
|
|
854
|
+
|
|
855
|
+
Environment variables (all seven required outside development/test; at least
|
|
856
|
+
one of `SMTP_SECURE`/`SMTP_REQUIRE_TLS` must be `true` outside
|
|
857
|
+
development/test):
|
|
858
|
+
|
|
859
|
+
```text
|
|
860
|
+
SMTP_HOST=
|
|
861
|
+
SMTP_PORT=
|
|
862
|
+
SMTP_SECURE=
|
|
863
|
+
SMTP_REQUIRE_TLS=
|
|
864
|
+
SMTP_USER=
|
|
865
|
+
SMTP_PASSWORD=
|
|
866
|
+
SMTP_FROM=
|
|
867
|
+
```
|
|
868
|
+
|
|
869
|
+
Set `REDIS_URL` to use `@medusajs/event-bus-redis` instead of Medusa's local
|
|
870
|
+
Event Bus; production requires an authenticated `rediss://` URL. See "Event
|
|
871
|
+
Bus" below for retry/backoff behavior.
|
|
872
|
+
|
|
873
|
+
### Feature toggles
|
|
874
|
+
|
|
875
|
+
The E-Mail module declares its toggles to `@hartl-services/medusa-base` under the
|
|
876
|
+
plugin key `email`. An Admin switches them under Settings → "Hartl Services
|
|
877
|
+
Plugins"; Storefronts read the effective values from
|
|
878
|
+
`GET /store/plugin-features/email`. A feature is only effective while the
|
|
879
|
+
plugin switch (default on) is on. A gated subscriber that finds its feature
|
|
880
|
+
off returns without creating a message; the event is not replayed when the
|
|
881
|
+
feature is switched on again.
|
|
882
|
+
|
|
883
|
+
| Key | Label | Default | Gates | Keeps running when off |
|
|
884
|
+
| --------------------- | ---------------------- | ------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -------------------------------------------------------- |
|
|
885
|
+
| (plugin switch) | E-Mail | on | every route under `/admin/email/*` (mail log, resends, terms versions; 404); jobs `email-reconcile-attempts`, `email-reconcile-pending-messages`, `email-retry-messages`; every feature below | checkout and orders; already queued messages stay stored |
|
|
886
|
+
| `order_mails` | Bestell-Mails | on | subscribers `order-placed`, `email-shipment-created`, `email-order-canceled`, `return-requested`, `return-received`, `documents-shipment-invoice-assignment-decided`, `documents-order-cancellation-ready`, `documents-refund-ready` | jobs, Admin routes, already created messages |
|
|
887
|
+
| `account_mails` | Konto-Mails | on | subscribers `password-reset`, `user-invite` | jobs, Admin routes, already created messages |
|
|
888
|
+
| `affiliate_reminders` | Affiliate-Erinnerungen | on | subscriber `affiliate-consignment-reminder-due` | jobs, Admin routes, already created messages |
|
|
889
|
+
|
|
890
|
+
With the plugin switched off the three jobs return immediately, so nothing
|
|
891
|
+
is sent, retried or reconciled until it is switched on again; then the jobs
|
|
892
|
+
pick up the stored messages (a message whose payload expired in the
|
|
893
|
+
meantime, such as a password-reset link, is not sent). A toggle read
|
|
894
|
+
failure fails the subscriber with its fixed sentinel (the Event Bus retries
|
|
895
|
+
it) or fails the job run.
|
|
896
|
+
|
|
897
|
+
The plugin declares one setting, edited in the Einstellungen section of its
|
|
898
|
+
Admin page: `logo` (type `image`, optional), the PNG/JPG shown on top of every
|
|
899
|
+
mail. It is not a Shop-Stammdaten field; Documents has its own `logo`.
|
|
900
|
+
|
|
901
|
+
Admin surfaces of a disabled feature are hidden. The E-Mail settings page
|
|
902
|
+
keeps `PluginFeatureSections` visible (so the plugin can be switched back on)
|
|
903
|
+
and gates the Terms PDF versions and mail log sections below it on the
|
|
904
|
+
plugin switch, showing `PluginFeatureDisabledNotice` in their place while it
|
|
905
|
+
is off.
|
|
906
|
+
|
|
907
|
+
### Ownership
|
|
908
|
+
|
|
909
|
+
- Documents owns document issuance and publishes immutable document facts.
|
|
910
|
+
- Affiliate owns reminder eligibility and publishes reminder facts.
|
|
911
|
+
- Email resolves recipients, renders content, attaches files, and owns delivery
|
|
912
|
+
status plus the append-only global Terms PDF history.
|
|
913
|
+
- Public HTTP DTOs, schemas, and paths remain in
|
|
914
|
+
`@hartl-services/medusa-food-supplements-contracts`.
|
|
915
|
+
|
|
916
|
+
The base's `/types` and `/config` exports are dedicated server integration
|
|
917
|
+
surfaces for the backend. The Documents and Affiliate event contracts live in
|
|
918
|
+
the Contracts package (`events/documents`, `events/affiliate`). The base exports no DML models, services,
|
|
919
|
+
workflows, migrations, or generated output as a consumer contract.
|
|
920
|
+
|
|
921
|
+
### Event Bus
|
|
922
|
+
|
|
923
|
+
The backend owns Event Bus selection. Development without `REDIS_URL` leaves
|
|
924
|
+
Medusa's local Event Bus active. Configuring `REDIS_URL` registers
|
|
925
|
+
`@medusajs/event-bus-redis` 2.19.0 directly; production requires an
|
|
926
|
+
authenticated `rediss://` URL unless the operator explicitly accepts private
|
|
927
|
+
network transport risk with `REDIS_ALLOW_INSECURE_PRIVATE_NETWORK=true`.
|
|
928
|
+
|
|
929
|
+
Redis Event Bus jobs have at most three total attempts with exponential backoff
|
|
930
|
+
starting at one second. Completed jobs are removed, while failed jobs are kept
|
|
931
|
+
for at most one hour and 1,000 entries. There is no custom Event Bus wrapper,
|
|
932
|
+
startup capability probe, or purge job. Events are at-least-once facts and can
|
|
933
|
+
be replayed, delayed, or delivered out of order.
|
|
934
|
+
|
|
935
|
+
### Event Contracts
|
|
936
|
+
|
|
937
|
+
All payload objects are strict. Unless noted otherwise, every string shown
|
|
938
|
+
below is trimmed and non-empty. Native event names and shapes are fixed to the
|
|
939
|
+
installed Medusa 2.19 exports:
|
|
940
|
+
|
|
941
|
+
| Event | Payload |
|
|
942
|
+
| ------------------------ | ---------------------------------------------------------------------------------------------- |
|
|
943
|
+
| `order.placed` | `{ id: string }` |
|
|
944
|
+
| `shipment.created` | `{ id: string; no_notification?: boolean }` |
|
|
945
|
+
| `order.canceled` | `{ id: string }` |
|
|
946
|
+
| `order.return_requested` | `{ order_id: string; return_id: string }` |
|
|
947
|
+
| `order.return_received` | `{ order_id: string; return_id: string }` |
|
|
948
|
+
| `auth.password_reset` | `{ entity_id: string; actor_type: string; token: string; metadata?: Record<string, unknown> }` |
|
|
949
|
+
|
|
950
|
+
The shared immutable Documents artifact is:
|
|
951
|
+
|
|
952
|
+
```ts
|
|
953
|
+
type DocumentsEmailArtifact = {
|
|
954
|
+
artifact_id: string;
|
|
955
|
+
document_id: string;
|
|
956
|
+
document_number: string;
|
|
957
|
+
file_id: string;
|
|
958
|
+
filename: string;
|
|
959
|
+
content_type: "application/pdf";
|
|
960
|
+
};
|
|
961
|
+
```
|
|
962
|
+
|
|
963
|
+
The versioned project events are exact integration contracts:
|
|
964
|
+
|
|
965
|
+
`documents.shipment_invoice_assignment.decided.v1`
|
|
966
|
+
|
|
967
|
+
```ts
|
|
968
|
+
type Payload =
|
|
969
|
+
| {
|
|
970
|
+
schema_version: 1;
|
|
971
|
+
order_id: string;
|
|
972
|
+
fulfillment_id: string;
|
|
973
|
+
decision: "non_carrier";
|
|
974
|
+
}
|
|
975
|
+
| {
|
|
976
|
+
schema_version: 1;
|
|
977
|
+
order_id: string;
|
|
978
|
+
fulfillment_id: string;
|
|
979
|
+
decision: "carrier";
|
|
980
|
+
invoice_fulfillment_id: string;
|
|
981
|
+
invoice_artifact: DocumentsEmailArtifact;
|
|
982
|
+
};
|
|
983
|
+
```
|
|
984
|
+
|
|
985
|
+
For `carrier`, `invoice_fulfillment_id` must equal `fulfillment_id`.
|
|
986
|
+
|
|
987
|
+
`documents.order_cancellation_documents.ready.v1`
|
|
988
|
+
|
|
989
|
+
```ts
|
|
990
|
+
type Payload = {
|
|
991
|
+
schema_version: 1;
|
|
992
|
+
order_id: string;
|
|
993
|
+
expected_refund_ids: string[];
|
|
994
|
+
attachments: Array<{
|
|
995
|
+
payment_id: string;
|
|
996
|
+
refund_id: string;
|
|
997
|
+
artifact: DocumentsEmailArtifact;
|
|
998
|
+
}>;
|
|
999
|
+
};
|
|
1000
|
+
```
|
|
1001
|
+
|
|
1002
|
+
`expected_refund_ids` must be sorted and unique. Attachments must be sorted by
|
|
1003
|
+
unique `refund_id`, and every attachment refund must be expected.
|
|
1004
|
+
|
|
1005
|
+
`documents.refund_document.ready.v1`
|
|
1006
|
+
|
|
1007
|
+
```ts
|
|
1008
|
+
type Payload = {
|
|
1009
|
+
schema_version: 1;
|
|
1010
|
+
order_id: string;
|
|
1011
|
+
payment_id: string;
|
|
1012
|
+
refund_id: string;
|
|
1013
|
+
artifact: DocumentsEmailArtifact | null;
|
|
1014
|
+
};
|
|
1015
|
+
```
|
|
1016
|
+
|
|
1017
|
+
`affiliate.consignment_report_reminder_due.v1`
|
|
1018
|
+
|
|
1019
|
+
```ts
|
|
1020
|
+
type Payload = {
|
|
1021
|
+
schema_version: 1;
|
|
1022
|
+
reminder_key: string;
|
|
1023
|
+
affiliate_id: string;
|
|
1024
|
+
customer_id: string | null;
|
|
1025
|
+
affiliate_display_name: string;
|
|
1026
|
+
period_start: string;
|
|
1027
|
+
due_date: string;
|
|
1028
|
+
locale: string | null;
|
|
1029
|
+
trigger: "scheduled" | "manual";
|
|
1030
|
+
};
|
|
1031
|
+
```
|
|
1032
|
+
|
|
1033
|
+
`period_start` is a valid `YYYY-MM-01`, `due_date` is a valid `YYYY-MM-DD`, and
|
|
1034
|
+
`reminder_key` is exactly
|
|
1035
|
+
`affiliate-consignment:<affiliate_id>:<period_start>`. `locale`, when present,
|
|
1036
|
+
has at most 35 characters. Email runtime-validates every contract before use.
|
|
1037
|
+
|
|
1038
|
+
### Invariants
|
|
1039
|
+
|
|
1040
|
+
- The accepted kinds are exactly `order.received`, `order.received.admin`, `order.shipped`,
|
|
1041
|
+
`order.canceled`, `order.return_requested`, `order.return_received`,
|
|
1042
|
+
`order.refunded`, `auth.password_reset`, and
|
|
1043
|
+
`affiliate.consignment_report_due`.
|
|
1044
|
+
- Every automatic message has a deterministic business key.
|
|
1045
|
+
- Every placed Order creates independent customer and Shop intents. The customer
|
|
1046
|
+
intent honors `no_notification`; the Shop intent always targets the static
|
|
1047
|
+
role mailbox selected by the Shop configuration.
|
|
1048
|
+
- A new customer `order.received` intent snapshots the newest global Terms PDF
|
|
1049
|
+
whose `effective_at` is not later than the authoritative `order.created_at`.
|
|
1050
|
+
If no version is effective, the customer message is intentionally created
|
|
1051
|
+
without Terms. The Shop `order.received.admin` intent never receives it.
|
|
1052
|
+
- Terms versions and their private files are immutable. Event replays, transport
|
|
1053
|
+
retries, and explicit Admin resends reuse the original message snapshot, so a
|
|
1054
|
+
later or backdated upload never changes an existing message.
|
|
1055
|
+
- Event payloads are runtime-validated before use and may be replayed, delayed,
|
|
1056
|
+
or delivered out of order.
|
|
1057
|
+
- With the Documents `invoices` feature on, shipment and
|
|
1058
|
+
cancellation messages remain pending until both their native Medusa fact
|
|
1059
|
+
and immutable Documents fact arrive. The required facts are fixed when the
|
|
1060
|
+
message is created; later facts reuse them.
|
|
1061
|
+
- Shipment `no_notification` suppresses the complete correlated message,
|
|
1062
|
+
including any invoice attachment; refund email has no Documents-disabled
|
|
1063
|
+
native fallback.
|
|
1064
|
+
- Recipients, rendered content, attachment bytes, SMTP responses, credentials,
|
|
1065
|
+
reset tokens, and reset URLs are never logged.
|
|
1066
|
+
- Reset tokens and reset URLs are never persisted by Email or Notification.
|
|
1067
|
+
- Password-reset messages cannot be retried by the generic retry job or resent
|
|
1068
|
+
by an Admin.
|
|
1069
|
+
- Unknown transport outcomes are never automatically resent.
|
|
1070
|
+
- Shop branding is the configured `displayName` plus the `logo` setting of
|
|
1071
|
+
the E-Mail plugin and the company details of the Shop-Stammdaten; a shop overrides the default layout or
|
|
1072
|
+
single renderers in its own configuration, not in the plugin.
|
|
1073
|
+
|
|
1074
|
+
Read this section and the central design before changing Email domain behavior.
|
|
1075
|
+
|
|
1076
|
+
### Shop configuration and default templates
|
|
1077
|
+
|
|
1078
|
+
The E-Mail module ships a neutral German layout in grey tones (table-based,
|
|
1079
|
+
inline styles) and one renderer per accepted kind. The layout shows, from top
|
|
1080
|
+
to bottom: the `logo` centered (an `image` setting of the E-Mail plugin,
|
|
1081
|
+
edited on its Admin page; without one, the configured `displayName`), a white card with the subject as heading and the
|
|
1082
|
+
renderer's body, a contact line ("Fragen? Dann schreib uns an … oder ruf uns
|
|
1083
|
+
an unter …", only with `email`/`phone`), and a footer with the company details
|
|
1084
|
+
(`legal_name` or `displayName`, address, UID, Firmenbuchnummer and
|
|
1085
|
+
Firmenbuchgericht, website) plus "Diese E-Mail wurde automatisch versendet.".
|
|
1086
|
+
Missing master data fields are left out. Email reads the logo and the
|
|
1087
|
+
Shop-Stammdaten when it sends a mail, so a change applies to the next mail; if
|
|
1088
|
+
a read fails the mail is sent without that part (logo or company details)
|
|
1089
|
+
instead of failing. Besides
|
|
1090
|
+
the layout, `displayName` appears in the password-reset and invite subjects.
|
|
1091
|
+
A custom `layout` receives the same data as `shop` (`EmailShopDetails`, every
|
|
1092
|
+
field optional) next to `bodyHtml`, `preheader` and `subject`. A shop
|
|
1093
|
+
therefore needs only `displayName`, `affiliatePortalUrl`, `inviteUrl`,
|
|
1094
|
+
`passwordResetUrl` and `orderNotificationEmail`.
|
|
1095
|
+
|
|
1096
|
+
The order confirmation (`order.received`) receives an `OrderSummary` as
|
|
1097
|
+
`summary`: the display ID, the items with quantity and gross amount, the
|
|
1098
|
+
shipping method names, subtotal, shipping, discount, included tax and total,
|
|
1099
|
+
plus the billing and shipping address. Email freezes it when the order is
|
|
1100
|
+
placed, so a resend shows the order as it was placed. `summary` is absent for
|
|
1101
|
+
messages stored before the summary existed; a custom renderer must handle
|
|
1102
|
+
that case.
|
|
1103
|
+
|
|
1104
|
+
The shop's order notification (`order.received.admin`) receives the same
|
|
1105
|
+
`summary` (with the order's `email` and `placed_at`). Its subject names the
|
|
1106
|
+
order number and the customer ("Neue Bestellung #1042 von Ada Beispiel"),
|
|
1107
|
+
an info box lists number, customer, e-mail and order time, followed by an
|
|
1108
|
+
"Im Admin öffnen" button and the items, totals and addresses. Two optional
|
|
1109
|
+
`ShopEmailConfiguration` fields drive it: `adminOrderUrl({ orderId })` (the
|
|
1110
|
+
button is left out without it) and `timeZone` (IANA; the order time is left
|
|
1111
|
+
out without it). `buildFoodShopConfig` sets both (`<MEDUSA_BACKEND_URL>/app/
|
|
1112
|
+
orders/<id>` and its `timeZone` option). The layout receives `audience`
|
|
1113
|
+
(`"shop"` for this kind and `user.invite`, `"customer"` for all others) and
|
|
1114
|
+
shows the contact line only to customers.
|
|
1115
|
+
|
|
1116
|
+
The shipping confirmation (`order.shipped`, one per fulfillment) receives a
|
|
1117
|
+
`ShipmentSummary` as `shipment`, frozen when the shipment fact arrives: the
|
|
1118
|
+
display ID, the shipping option name, the tracking numbers and URLs entered
|
|
1119
|
+
at shipping, the parcel's items (title and variant of the order line,
|
|
1120
|
+
shipped quantity), the delivery address and `more_to_follow` (some ordered
|
|
1121
|
+
quantity is not shipped yet). `invoice_attached` is `true` when Documents chose
|
|
1122
|
+
this shipment for the invoice PDF. Its subject is "Deine Bestellung bei
|
|
1123
|
+
<displayName> ist unterwegs"; a single http(s) tracking URL becomes a
|
|
1124
|
+
"Sendung verfolgen" button, several become one link each. A fulfillment that
|
|
1125
|
+
cannot be read is confirmed without `shipment`, like a message stored before
|
|
1126
|
+
the snapshot existed.
|
|
1127
|
+
|
|
1128
|
+
The cancellation confirmation (`order.canceled`) receives the same
|
|
1129
|
+
`OrderSummary` as `summary` plus `canceled_at`, frozen when the cancellation
|
|
1130
|
+
fact arrives, and `cancellation_attached` (`true` when Documents attached at
|
|
1131
|
+
least one cancellation document). Its subject is "Deine Bestellung #1042 bei
|
|
1132
|
+
<displayName> wurde storniert"; an info box lists the order number and the
|
|
1133
|
+
cancellation date (left out without `timeZone`), followed by a fixed refund
|
|
1134
|
+
note and the canceled items and totals without addresses. An order that
|
|
1135
|
+
cannot be summarized is confirmed without `summary` and without its internal
|
|
1136
|
+
ID.
|
|
1137
|
+
|
|
1138
|
+
The return confirmation (`order.return_requested`) receives a `ReturnSummary`
|
|
1139
|
+
as `summary`, frozen when the return is requested: return and order numbers,
|
|
1140
|
+
the return shipping method, the items to send back (title and variant of the
|
|
1141
|
+
order line, quantity, return reason label) and the return address. It is
|
|
1142
|
+
the stock location's address, addressed to the location's company or else
|
|
1143
|
+
the master data's `legal_name`; without a location address it falls back to
|
|
1144
|
+
the master data's company address. Its subject is "Deine Rücksendung zur
|
|
1145
|
+
Bestellung #1042 bei <displayName>"; the mail asks the customer to enclose
|
|
1146
|
+
the return number (`#R5`). A return that cannot be read is confirmed without
|
|
1147
|
+
`summary` and without internal IDs.
|
|
1148
|
+
|
|
1149
|
+
The return receipt (`order.return_received`, sent once the return is
|
|
1150
|
+
completely received) receives a `ReturnReceivedSummary` as `summary`: return
|
|
1151
|
+
and order numbers, `received_at` and the received items (title and variant
|
|
1152
|
+
of the order line, received quantity and the damaged part of it). Its subject
|
|
1153
|
+
is "Deine Rücksendung zur Bestellung #1042 bei <displayName> ist
|
|
1154
|
+
eingetroffen"; the arrival date needs `timeZone`, damaged units are noted per
|
|
1155
|
+
item, and a fixed sentence says that an eventual refund follows on the
|
|
1156
|
+
original payment method, without promising a separate mail (`order.refunded`
|
|
1157
|
+
only exists with Documents).
|
|
1158
|
+
|
|
1159
|
+
The refund confirmation (`order.refunded`, one per non-cancellation refund,
|
|
1160
|
+
Documents only) receives a `RefundSummary` as `summary`, frozen when the
|
|
1161
|
+
Documents refund fact arrives and read through the refund's payment: order
|
|
1162
|
+
number, refunded gross amount and currency, `refunded_at` and the Admin's
|
|
1163
|
+
refund reason label. `cancellation_attached` is `true` when Documents attached
|
|
1164
|
+
a cancellation document. Its subject is "Deine Erstattung zur Bestellung #1042
|
|
1165
|
+
bei <displayName>"; the info box shows number, amount, date (needs
|
|
1166
|
+
`timeZone`) and reason, followed by a fixed note on bank processing times.
|
|
1167
|
+
Without `summary` the mail keeps the intro and the attachment note.
|
|
1168
|
+
|
|
1169
|
+
The password reset (`auth.password_reset`) has the subject "Neues Passwort
|
|
1170
|
+
für dein Kundenkonto bei <displayName>", a "Neues Passwort festlegen" button,
|
|
1171
|
+
the link as copyable text below it, the link's lifetime ("Der Link ist 15
|
|
1172
|
+
Minuten gültig.", computed from the token's `exp` − `iat` and left out
|
|
1173
|
+
without `iat`) and a note that the mail can be ignored. The lifetime is
|
|
1174
|
+
passed as the ephemeral `valid_minutes` next to `reset_url` and is never
|
|
1175
|
+
persisted.
|
|
1176
|
+
|
|
1177
|
+
The affiliate reminder (`affiliate.consignment_report_due`) greets the
|
|
1178
|
+
affiliate by `affiliate_display_name`, names the report month ("September
|
|
1179
|
+
2026", Austrian month names) and the due date ("10.10.2026") as calendar
|
|
1180
|
+
values formatted in UTC so no time zone shifts them, explains that the counted
|
|
1181
|
+
stock determines the billed sales, links `affiliatePortalUrl` as a "Zum
|
|
1182
|
+
Partnerbereich" button and says the mail can be ignored once the report is
|
|
1183
|
+
submitted. The button leads to a 404 while the affiliate plugin's
|
|
1184
|
+
`partner_portal` toggle is off.
|
|
1185
|
+
|
|
1186
|
+
The Admin invite (`user.invite`) has the subject "Einladung ins Admin-Team von
|
|
1187
|
+
<displayName>", an "Einladung annehmen" button with the link as copyable text,
|
|
1188
|
+
the invite's current expiry ("Die Einladung ist gültig bis 02.10.2026, 01:40
|
|
1189
|
+
Uhr.", needs `timeZone`; read from the invite at send time, so a resend shows
|
|
1190
|
+
its new expiry) and a note that the mail can be ignored. The expiry is passed
|
|
1191
|
+
as the ephemeral `expires_at` next to `invite_url`. The invite goes to the
|
|
1192
|
+
shop's team, so its layout audience is `"shop"` (no contact line).
|
|
1193
|
+
|
|
1194
|
+
- `layout` (optional) replaces the default layout completely.
|
|
1195
|
+
- `renderers` (optional, `Partial<EmailRendererMap>`) replaces only the listed
|
|
1196
|
+
kinds; every other kind keeps its default renderer.
|
|
1197
|
+
|
|
1198
|
+
```ts
|
|
1199
|
+
import { defineShopEmailConfiguration } from "@hartl-services/medusa-base/config";
|
|
1200
|
+
|
|
1201
|
+
const shop = defineShopEmailConfiguration({
|
|
1202
|
+
displayName: "Mein Shop",
|
|
1203
|
+
// ...URLs and mailbox as above
|
|
1204
|
+
renderers: {
|
|
1205
|
+
"order.canceled": ({ order_id }) => ({
|
|
1206
|
+
bodyHtml: `<p>Bestellung <strong>${order_id}</strong> storniert.</p>`,
|
|
1207
|
+
subject: "Storniert",
|
|
1208
|
+
text: `Bestellung ${order_id} storniert.`,
|
|
1209
|
+
}),
|
|
1210
|
+
},
|
|
1211
|
+
});
|
|
1212
|
+
```
|
|
1213
|
+
|
|
1214
|
+
The defaults are merged once when the Email module boots, so an invalid
|
|
1215
|
+
configuration (blank `displayName`, a non-function `layout` or renderer, an
|
|
1216
|
+
unknown renderer kind) fails at startup and not at the first send. Custom
|
|
1217
|
+
renderers must escape dynamic values in `bodyHtml` themselves.
|
|
1218
|
+
|
|
1219
|
+
### SMTP environment helpers
|
|
1220
|
+
|
|
1221
|
+
`@hartl-services/medusa-base/config` exports helpers for the SMTP
|
|
1222
|
+
deployment variables:
|
|
1223
|
+
|
|
1224
|
+
- `SMTP_ENVIRONMENT_VARIABLES` — the seven variable names.
|
|
1225
|
+
- `SMTP_BUILD_PLACEHOLDERS` — placeholder values for all seven variables so
|
|
1226
|
+
`medusa build` can load `medusa-config.ts` without deployment secrets. Use
|
|
1227
|
+
them only while building.
|
|
1228
|
+
- `resolveSmtp(values, allowInsecure)` — validates the values and returns the
|
|
1229
|
+
`ResolvedSmtpConfiguration` for `createSmtpNotificationModuleConfig`. It
|
|
1230
|
+
rejects a port outside 1–65535, a non-`"true"`/`"false"` boolean and an
|
|
1231
|
+
invalid `SMTP_FROM`. `allowInsecure` permits plain SMTP (development, test,
|
|
1232
|
+
build); otherwise `SMTP_SECURE` or `SMTP_REQUIRE_TLS` must be `true`.
|
|
1233
|
+
|
|
1234
|
+
Runtime requires `SMTP_HOST`, `SMTP_PORT`, `SMTP_SECURE`, `SMTP_REQUIRE_TLS`,
|
|
1235
|
+
`SMTP_USER`, `SMTP_PASSWORD`, and `SMTP_FROM`. `SMTP_SECURE=true` selects
|
|
1236
|
+
implicit TLS; `SMTP_REQUIRE_TLS=true` requires STARTTLS. Outside development
|
|
1237
|
+
and test, at least one must be true. Certificate verification is never
|
|
1238
|
+
disabled. Development and test may set both to `false` for an isolated local
|
|
1239
|
+
SMTP server. SMTP endpoints and credentials never belong in the shop
|
|
1240
|
+
configuration.
|
|
1241
|
+
|
|
1242
|
+
### Delivery Semantics
|
|
1243
|
+
|
|
1244
|
+
For ordinary durable messages, only characterized connection failures,
|
|
1245
|
+
temporary SMTP rejections, and temporary attachment access failures are
|
|
1246
|
+
automatically retried. There are at most three total SMTP attempts, scheduled
|
|
1247
|
+
after one and five minutes. Permanent rejection and invalid content become
|
|
1248
|
+
terminal `failed`.
|
|
1249
|
+
|
|
1250
|
+
`unknown` means SMTP may have accepted a message but Email cannot prove the
|
|
1251
|
+
outcome. A persisted successful Medusa Notification is accepted as proof of
|
|
1252
|
+
delivery; otherwise an expired attempt lease or uncharacterized transport
|
|
1253
|
+
result becomes terminal `unknown` and is never retried automatically. An Admin
|
|
1254
|
+
may consciously resend it as a new logical message.
|
|
1255
|
+
|
|
1256
|
+
Business keys deduplicate logical automatic messages, attempt claims serialize
|
|
1257
|
+
workers, and each attempt has its own Notification idempotency key. These
|
|
1258
|
+
controls do not promise exact-once SMTP: a server can accept mail before the
|
|
1259
|
+
process persists success, and a conscious resend can produce a duplicate.
|
|
1260
|
+
|
|
1261
|
+
Email transport retries and Event Bus retries are separate. Once an ordinary
|
|
1262
|
+
message outcome is persisted, its subscriber completes and the Email retry job
|
|
1263
|
+
owns any `retry_pending` work. Password-reset token and URL data remain
|
|
1264
|
+
ephemeral; password reset is excluded from the generic retry job and Admin
|
|
1265
|
+
resend, and retryable handling stays bounded by the original Event Bus event.
|
|
1266
|
+
|
|
1267
|
+
### Admin API
|
|
1268
|
+
|
|
1269
|
+
Email exposes authenticated Admin operations at:
|
|
1270
|
+
|
|
1271
|
+
- `GET /admin/email/messages` lists safe message summaries. Pagination is
|
|
1272
|
+
bounded and deterministic; `kind` and `status` accept only the published
|
|
1273
|
+
allowlists.
|
|
1274
|
+
- `GET /admin/email/messages/:id` returns the safe message detail and delivery
|
|
1275
|
+
attempts.
|
|
1276
|
+
- `POST /admin/email/messages/:id/resends` makes one explicit resend decision
|
|
1277
|
+
using a request UUID.
|
|
1278
|
+
- `GET /admin/email/terms-versions` lists safe Terms version metadata in newest
|
|
1279
|
+
effective order.
|
|
1280
|
+
- `POST /admin/email/terms-versions` idempotently stores one private PDF of at
|
|
1281
|
+
most 5 MiB with its `effective_at` timestamp.
|
|
1282
|
+
- `GET /admin/email/terms-versions/:id/download` streams one authenticated,
|
|
1283
|
+
non-cacheable PDF download.
|
|
1284
|
+
|
|
1285
|
+
List and detail responses expose only safe identifiers and linkage metadata,
|
|
1286
|
+
kind/status and safe status codes, resource references, trigger, attempt
|
|
1287
|
+
counts, and timestamps. They never expose recipients, rendered content,
|
|
1288
|
+
template data, payloads, reset tokens, transport responses, or credentials.
|
|
1289
|
+
Terms responses expose filename, effective timestamp, byte size, SHA-256, and
|
|
1290
|
+
creation time, but never request IDs, file IDs, storage keys, URLs, or content.
|
|
1291
|
+
|
|
1292
|
+
An Admin resend is accepted only for a `sent`, `unknown`, or terminal `failed`
|
|
1293
|
+
source. Password-reset messages can never use the generic resend operation.
|
|
1294
|
+
The authenticated Admin actor is derived by the server and is not accepted
|
|
1295
|
+
from the request body. Replaying the same request UUID returns the same resend;
|
|
1296
|
+
a different UUID represents a separate, conscious Admin decision. The Admin UI
|
|
1297
|
+
therefore retains a UUID while retrying an ambiguous failed request and creates
|
|
1298
|
+
a new UUID only after a new confirmation.
|
|
1299
|
+
|
|
1300
|
+
### Admin UI
|
|
1301
|
+
|
|
1302
|
+
The E-Mail module has one Admin page, `/settings/hartl-services/email`
|
|
1303
|
+
(**Settings → Hartl Services Plugins → E-Mail → Öffnen**; the descriptor's
|
|
1304
|
+
`admin_page` points the hub there) instead of a top-level sidebar item. It
|
|
1305
|
+
composes the base's `PluginPageShell` and `PluginFeatureSections` from
|
|
1306
|
+
`@hartl-services/medusa-base/admin-components` (the toggles above) and adds
|
|
1307
|
+
two operations sections below them, in this order: **AGB/PDF-Versionen** and,
|
|
1308
|
+
as the last section, the **E-Mail-Protokoll**. They show Terms PDF and mail
|
|
1309
|
+
log state to act on, not configuration the plugin reads at run time. The mail
|
|
1310
|
+
log provides bounded server pagination, allowlisted status/kind filters, safe
|
|
1311
|
+
message and attempt status, and an explicitly confirmed resend action.
|
|
1312
|
+
Recipient, template, and payload data are intentionally absent from the page.
|
|
1313
|
+
|
|
1314
|
+
The `AGB/PDF-Versionen` section manages the global Terms PDFs. Admins can
|
|
1315
|
+
upload a PDF of at most 5 MiB with its effective date, review versions newest
|
|
1316
|
+
first, and download a selected version. Uploads receive a fresh request UUID;
|
|
1317
|
+
downloads use the configured authenticated Admin SDK and never expose storage
|
|
1318
|
+
file IDs or URLs in the UI.
|
|
1319
|
+
|
|
1320
|
+
Admin consumers import wire DTOs and paths only from the Contracts package and
|
|
1321
|
+
reuse their configured Medusa SDK:
|
|
1322
|
+
|
|
1323
|
+
```ts
|
|
1324
|
+
import type { ListAdminEmailMessagesResponse } from "@hartl-services/medusa-food-supplements-contracts/types/email-admin";
|
|
1325
|
+
import { emailAdminPaths } from "@hartl-services/medusa-food-supplements-contracts/paths/email";
|
|
1326
|
+
|
|
1327
|
+
const response = await sdk.client.fetch<ListAdminEmailMessagesResponse>(
|
|
1328
|
+
emailAdminPaths.get.list,
|
|
1329
|
+
);
|
|
1330
|
+
```
|
|
1331
|
+
|
|
1332
|
+
Do not import E-Mail module implementations, generated `.medusa` files, or
|
|
1333
|
+
duplicate runtime schemas in an Admin consumer. The base's own exports remain
|
|
1334
|
+
internal server and generated Admin integration surfaces; Email HTTP contracts
|
|
1335
|
+
are published only by `@hartl-services/medusa-food-supplements-contracts`.
|
|
1336
|
+
|
|
1337
|
+
### Development and Rollout
|
|
1338
|
+
|
|
1339
|
+
For local development, provide the seven SMTP variables and omit `REDIS_URL` to
|
|
1340
|
+
use Medusa's local Event Bus. Set `REDIS_URL` only when intentionally exercising
|
|
1341
|
+
the direct Redis provider. The backend HTTP runner ignores ambient Redis and
|
|
1342
|
+
SMTP settings and injects an isolated Redis container, unique queue, and
|
|
1343
|
+
token-protected SMTP fixture for every child.
|
|
1344
|
+
|
|
1345
|
+
This architecture replaces the old Documents and Affiliate delivery baselines
|
|
1346
|
+
without a backfill or compatibility layer. Existing local databases from those
|
|
1347
|
+
baselines must be wiped once with:
|
|
1348
|
+
|
|
1349
|
+
```bash
|
|
1350
|
+
pnpm --dir apps/backend db:recreate
|
|
1351
|
+
```
|
|
1352
|
+
|
|
1353
|
+
The first production rollout must start from an empty database and run
|
|
1354
|
+
`pnpm --dir apps/backend db:migrate` before accepting traffic. This assumes no
|
|
1355
|
+
production business data exists; a populated database requires a separately
|
|
1356
|
+
reviewed migration. No old delivery rows or queued jobs are migrated or purged
|
|
1357
|
+
by Email.
|
|
1358
|
+
|
|
1359
|
+
## Dokumente
|
|
1360
|
+
|
|
1361
|
+
The base issues order documents: one total invoice per order after the final
|
|
1362
|
+
shipment and one negative cancellation document for each eligible payment
|
|
1363
|
+
refund. An Admin user can regenerate issued documents in place for exceptional
|
|
1364
|
+
corrections. The base ships default German document markup, so a new shop needs
|
|
1365
|
+
no template code; a project can optionally replace the HTML/CSS factory. PDF
|
|
1366
|
+
generation, private file storage, customer authorization and durable
|
|
1367
|
+
document-readiness facts stay inside the `documents` module. There is no
|
|
1368
|
+
separate documents package since 1.0.0.
|
|
1369
|
+
|
|
1370
|
+
### What it does
|
|
1371
|
+
|
|
1372
|
+
- Listens to Medusa's `shipment.created` event.
|
|
1373
|
+
- Creates an invoice only if the order is fully shipped and not cancelled.
|
|
1374
|
+
Partial shipments do not create a document.
|
|
1375
|
+
- Saves the rendered PDF and the exact source data as a document snapshot.
|
|
1376
|
+
- Reads eligible B2B evidence frozen under the order ID from Tax Compliance and
|
|
1377
|
+
persists the customer company, customer UID and exemption wording in that
|
|
1378
|
+
same snapshot. B2C and non-eligible evidence add no tax-proof statement.
|
|
1379
|
+
- Freezes a tax breakdown (`totals.tax_breakdown`) grouped by the order's
|
|
1380
|
+
item and shipping tax-line name and rate, as configured on the Medusa tax
|
|
1381
|
+
region. Amounts are rounded to the currency and add up exactly to the
|
|
1382
|
+
tax total; cancellations scale it proportionally. Snapshots issued before
|
|
1383
|
+
this field existed read as an empty breakdown.
|
|
1384
|
+
- Persists one immutable decision per shipped Fulfillment. A leased, Order-
|
|
1385
|
+
unique coordinator chooses exactly one invoice carrier without assuming
|
|
1386
|
+
event delivery order; all other shipments become `non_carrier`.
|
|
1387
|
+
- Uses a durable per-order issuance key and token-fenced finalization, so
|
|
1388
|
+
duplicate, concurrent or delayed events cannot create a second initial
|
|
1389
|
+
invoice or committed carrier.
|
|
1390
|
+
- Allocates financial document numbers from Documents-owned, transactional
|
|
1391
|
+
annual number ranges. Invoice, cancellation and future credit-note ranges are
|
|
1392
|
+
independent of Medusa Order display IDs and safe across backend workers.
|
|
1393
|
+
- Lets an Admin regenerate the invoice in place with a fresh order and active
|
|
1394
|
+
batch-allocation snapshot.
|
|
1395
|
+
- Listens to Medusa's successful `payment.refunded` event and issues one
|
|
1396
|
+
cancellation document per persisted refund, including partial refunds.
|
|
1397
|
+
- Uses the immutable Refund ID as both cancellation issuance key and resolution
|
|
1398
|
+
identity, so replayed payment events cannot create duplicate cancellation
|
|
1399
|
+
documents or readiness facts.
|
|
1400
|
+
- Reconciles the complete Refund set after Order cancellation, including Medusa
|
|
1401
|
+
bulk refunds that do not emit a singular refund event. A canceled Order with
|
|
1402
|
+
zero Refunds still receives a final empty cancellation resolution.
|
|
1403
|
+
- Publishes strict versioned shipment, Refund and Order-cancellation facts only
|
|
1404
|
+
after their database state and referenced immutable artifacts are committed.
|
|
1405
|
+
Publication markers and periodic reconciliation close the database-to-event
|
|
1406
|
+
enqueue gap.
|
|
1407
|
+
- Exposes only an authenticated customer Store API. A customer can list,
|
|
1408
|
+
inspect and download only documents belonging to their own customer ID.
|
|
1409
|
+
Guest documents deliberately have no Store lookup path.
|
|
1410
|
+
- Adds an Admin order-detail widget. Authenticated Admin users can view the
|
|
1411
|
+
issued documents belonging to the current order as a private inline PDF.
|
|
1412
|
+
- Supports `invoice`, `cancellation` and `credit_note` document kinds.
|
|
1413
|
+
Cancellation issuance is wired to payment refunds; `credit_note` remains a
|
|
1414
|
+
reserved type for a future, distinct business process.
|
|
1415
|
+
- Creates one internal packing list for every physical fulfillment, including
|
|
1416
|
+
optional fulfillment-scoped batch data. Partial fulfillments therefore
|
|
1417
|
+
produce independent packing lists.
|
|
1418
|
+
- Keeps packing lists in a separate Admin-only aggregate. They have no customer
|
|
1419
|
+
owner, Store route, communication artifact or customer-facing Documents event.
|
|
1420
|
+
- Reconciles non-canceled physical fulfillments periodically so a lost local
|
|
1421
|
+
event can be repaired idempotently, including after shipment.
|
|
1422
|
+
|
|
1423
|
+
### Configuration
|
|
1424
|
+
|
|
1425
|
+
`@hartl-services/medusa-tax-compliance` is a required *registered plugin*,
|
|
1426
|
+
intentionally not a package dependency of the base: install it and list it in
|
|
1427
|
+
`medusa-config.ts` yourself. The invoice workflow resolves the
|
|
1428
|
+
`tax_compliance` module with no fallback; a shop that does not register Tax
|
|
1429
|
+
Compliance fails at invoice issuance time (the first `shipment.created`
|
|
1430
|
+
event) with an `UNEXPECTED_STATE` error naming the missing plugin, not at
|
|
1431
|
+
startup.
|
|
1432
|
+
|
|
1433
|
+
```ts
|
|
1434
|
+
export default defineConfig({
|
|
1435
|
+
modules: [
|
|
1436
|
+
{
|
|
1437
|
+
resolve: "@medusajs/medusa/tax",
|
|
1438
|
+
options: {
|
|
1439
|
+
providers: [
|
|
1440
|
+
{
|
|
1441
|
+
resolve:
|
|
1442
|
+
"@hartl-services/medusa-tax-compliance/providers/tax-compliance",
|
|
1443
|
+
},
|
|
1444
|
+
],
|
|
1445
|
+
},
|
|
1446
|
+
},
|
|
1447
|
+
],
|
|
1448
|
+
plugins: [
|
|
1449
|
+
{
|
|
1450
|
+
resolve: "@hartl-services/medusa-base",
|
|
1451
|
+
options: {
|
|
1452
|
+
documents: {
|
|
1453
|
+
numbering: {
|
|
1454
|
+
timeZone: "Europe/Vienna", // default when omitted
|
|
1455
|
+
},
|
|
1456
|
+
pdf: {
|
|
1457
|
+
executablePath: process.env.DOCUMENT_PDF_EXECUTABLE, // default: "google-chrome"
|
|
1458
|
+
},
|
|
1459
|
+
},
|
|
1460
|
+
},
|
|
1461
|
+
},
|
|
1462
|
+
{
|
|
1463
|
+
resolve: "@hartl-services/medusa-tax-compliance",
|
|
1464
|
+
options: {
|
|
1465
|
+
merchant: {
|
|
1466
|
+
countryCode: process.env.MERCHANT_COUNTRY_CODE!,
|
|
1467
|
+
vatNumber: process.env.MERCHANT_VAT_NUMBER!,
|
|
1468
|
+
},
|
|
1469
|
+
},
|
|
1470
|
+
},
|
|
1471
|
+
],
|
|
1472
|
+
});
|
|
1473
|
+
```
|
|
1474
|
+
|
|
1475
|
+
Options of the `documents` namespace (all optional; `{}` is valid):
|
|
1476
|
+
|
|
1477
|
+
- `htmlFactory` — optional. A `(input: DocumentRenderInput) => string |
|
|
1478
|
+
Promise<string>` function that replaces the built-in German markup; the
|
|
1479
|
+
module owns PDF rendering and storage either way. When omitted, the module
|
|
1480
|
+
uses `createDefaultDocumentHtmlFactory({ timeZone })` with the resolved
|
|
1481
|
+
`numbering.timeZone`, so printed dates are shown in the same time zone that
|
|
1482
|
+
picks the numbering period. A value that is not a function is rejected at
|
|
1483
|
+
startup.
|
|
1484
|
+
- `numbering.timeZone` — an IANA time zone used to resolve the annual
|
|
1485
|
+
numbering period and the dates printed by the default markup. Defaults to
|
|
1486
|
+
`"Europe/Vienna"`; a given value (also a blank one) is validated and
|
|
1487
|
+
rejected at startup when invalid.
|
|
1488
|
+
- `pdf.executablePath` — Chromium/Chrome executable path or command name.
|
|
1489
|
+
Defaults to `"google-chrome"`.
|
|
1490
|
+
- `merchant` — optional fallback for the shop master data (see "Merchant
|
|
1491
|
+
data" below), shape `DocumentMerchantOption`:
|
|
1492
|
+
`{ countryCode, legalName, registeredAddress: { address1, address2?, city,
|
|
1493
|
+
countryCode, postalCode }, vatNumber? }`.
|
|
1494
|
+
|
|
1495
|
+
The `documents` module is always registered with the base (no `documentsEnabled`
|
|
1496
|
+
switch); the Admin switches it off at run time through the feature toggles
|
|
1497
|
+
below. `medusa db:migrate` creates its tables.
|
|
1498
|
+
|
|
1499
|
+
### The `/config` subpath
|
|
1500
|
+
|
|
1501
|
+
`@hartl-services/medusa-base/config` exports, next to the environment helpers,
|
|
1502
|
+
build-time values for `medusa-config.ts` and tests:
|
|
1503
|
+
|
|
1504
|
+
- `createDefaultDocumentHtmlFactory({ timeZone, theme? })` — the built-in
|
|
1505
|
+
factory, to use as it is, with a shop theme (see "Layout, logo, payment
|
|
1506
|
+
terms and shop theme" below) or to wrap.
|
|
1507
|
+
- `DOCUMENTS_BUILD_PLACEHOLDERS` — `{ DOCUMENT_PDF_EXECUTABLE: "google-chrome" }`.
|
|
1508
|
+
`medusa build` evaluates `medusa-config.ts` without the runtime-only
|
|
1509
|
+
environment, so spread it under the real environment while building.
|
|
1510
|
+
|
|
1511
|
+
The document types (`DocumentMerchantOption`, `DocumentHtmlFactory`,
|
|
1512
|
+
`DocumentRenderInput`, ...) come from `@hartl-services/medusa-base/types`.
|
|
1513
|
+
|
|
1514
|
+
### Layout, logo, payment terms and shop theme
|
|
1515
|
+
|
|
1516
|
+
The default markup prints an A4 letterhead: logo top left, document title and
|
|
1517
|
+
key data (number, order, date, customer UID for exempt B2B supplies) on the
|
|
1518
|
+
right, the billing address on the left, the item table with batch and
|
|
1519
|
+
best-before date per line, the totals below the price columns and a
|
|
1520
|
+
three-column footer (sender with UID and company register, contact, bank) on
|
|
1521
|
+
every page. Packing lists show only the sender in the footer. Every page
|
|
1522
|
+
carries "Seite x von y" bottom right in an 8 mm bottom page margin
|
|
1523
|
+
(`@page { @bottom-right }`; the page count exists only in page margin boxes).
|
|
1524
|
+
|
|
1525
|
+
Settings under Settings → "Hartl Services Plugins" → **Dokumente**, read on
|
|
1526
|
+
every render and passed as `input.settings` (`DocumentRenderSettings`). Like
|
|
1527
|
+
the merchant data they are not part of the snapshot: a regenerated document
|
|
1528
|
+
shows the current values.
|
|
1529
|
+
|
|
1530
|
+
- **Logo** (`logo`, PNG or JPG): each render downloads it (10 s timeout, at
|
|
1531
|
+
most 2 MiB) and passes it inline as `settings.logo_src` (`data:` URL), so
|
|
1532
|
+
Chromium never waits on the network. Without a logo, or when the download
|
|
1533
|
+
fails (logged as a warning), the document shows the merchant's legal name;
|
|
1534
|
+
a logo never blocks issuance.
|
|
1535
|
+
- **Zahlungskonditionen** (`payment_terms`, text): printed below the totals
|
|
1536
|
+
of invoices only (not on cancellations or credit notes); blank means none.
|
|
1537
|
+
Line breaks in the text are kept.
|
|
1538
|
+
|
|
1539
|
+
**Theme:** `createDefaultDocumentHtmlFactory({ timeZone, theme })` keeps the
|
|
1540
|
+
content and markup and changes only the look. `theme.stylesheet` is CSS
|
|
1541
|
+
appended after the default stylesheet (later rules win; the defaults use the
|
|
1542
|
+
custom properties `--accent`, `--text`, `--muted`, `--line`, `--table-head`,
|
|
1543
|
+
`--page-x`, `--page-top`, `--footer-height`). Put a watermark or other
|
|
1544
|
+
full-page image into `@page { background: ... / 210mm 297mm }`: it covers the
|
|
1545
|
+
whole sheet on every page, whereas Chromium clips fixed elements at the page
|
|
1546
|
+
margin and sizes an inline `<svg>` in them wrongly from the second page on.
|
|
1547
|
+
Restyle the page number with `@page { @bottom-right { ... } }`. Embed web
|
|
1548
|
+
fonts as `data:` URLs; the container's Chromium has only its system fonts. A
|
|
1549
|
+
shop passes its factory through the preset option `documentHtmlFactory` (see
|
|
1550
|
+
"Shop-Config-Preset").
|
|
1551
|
+
|
|
1552
|
+
### Merchant data
|
|
1553
|
+
|
|
1554
|
+
The legal sender printed on every document comes from the base plugin's shop
|
|
1555
|
+
master data (Admin → Settings → Hartl Services Plugins → **Shop-Stammdaten**).
|
|
1556
|
+
Each render reads it and passes it to the `htmlFactory` as `input.merchant`
|
|
1557
|
+
(`DocumentMerchant`: `legal_name`, `address_line1`, `address_line2`,
|
|
1558
|
+
`postal_code`, `city`, `country_code`, `vat_number`, `email`, `phone`,
|
|
1559
|
+
`website`, `iban`, `bic`, `company_register_number`,
|
|
1560
|
+
`company_register_court`; unset optional fields are `null`). Per field, a
|
|
1561
|
+
value stored in the Admin wins over the `merchant` option.
|
|
1562
|
+
|
|
1563
|
+
- `merchant` is not part of the stored snapshot: a regenerated document
|
|
1564
|
+
prints the master data current at regeneration time.
|
|
1565
|
+
- The development UID `ATU00000000` arrives as `vat_number: null` and is
|
|
1566
|
+
never printed.
|
|
1567
|
+
- Without `legal_name`, `address_line1`, `postal_code`, `city` and
|
|
1568
|
+
`country_code` a document cannot be issued: issuance fails with
|
|
1569
|
+
`UNEXPECTED_STATE` ("Shop-Stammdaten unvollständig: …" naming the missing
|
|
1570
|
+
fields). Invoices and cancellations check this before a document number
|
|
1571
|
+
and issue date are reserved, so no number is used up: the invoice workflow
|
|
1572
|
+
fails (the subscriber logs it, the shipment assignment stays pending), a
|
|
1573
|
+
refund resolution is marked failed. A packing list revision is marked
|
|
1574
|
+
failed with the message, which the Admin order widget shows. Once the data
|
|
1575
|
+
is complete, the existing paths issue it again (event redelivery,
|
|
1576
|
+
reconciliation jobs, the Admin issue and retry actions). A missing UID
|
|
1577
|
+
does not block issuance.
|
|
1578
|
+
|
|
1579
|
+
Requirements/couplings:
|
|
1580
|
+
|
|
1581
|
+
- A registered Tax Compliance module (`tax_compliance`) as shown above.
|
|
1582
|
+
- A Chromium/Chrome executable and a Medusa File provider that supports
|
|
1583
|
+
private files (see "Development" below for the sandbox/non-root note).
|
|
1584
|
+
Documents stores PDFs through `Modules.FILE` and streams them only through
|
|
1585
|
+
its own authenticated routes, so it works with any File provider and does
|
|
1586
|
+
not need the S3 file storage or its `/files` routes.
|
|
1587
|
+
**With Medusa's local File provider, set `private_upload_dir` to a
|
|
1588
|
+
directory outside `static/`:** its default stores private files in
|
|
1589
|
+
`static/`, which Medusa serves publicly under `/static`, making every
|
|
1590
|
+
invoice downloadable by its guessable file name. The base plugin still
|
|
1591
|
+
starts in that case (it ships the Documents module to every shop), but logs
|
|
1592
|
+
a warning at startup and refuses to write any document: issuing an invoice
|
|
1593
|
+
or packing list fails until the File Module stores private files outside
|
|
1594
|
+
`static/`.
|
|
1595
|
+
- Optionally, `@hartl-services/medusa-batch-management` 0.0.3+ for
|
|
1596
|
+
batch/lot data on invoices and packing lists (see "Optional Batch
|
|
1597
|
+
Management integration" below).
|
|
1598
|
+
|
|
1599
|
+
|
|
1600
|
+
### Feature toggles
|
|
1601
|
+
|
|
1602
|
+
The Documents module declares its toggles to the base under the plugin key
|
|
1603
|
+
`documents`. An Admin switches them under Settings → "Hartl
|
|
1604
|
+
Services Plugins"; Storefronts read the effective values from
|
|
1605
|
+
`GET /store/plugin-features/documents`. A disabled plugin or feature answers
|
|
1606
|
+
its routes with HTTP 404, and its subscribers and jobs return without doing
|
|
1607
|
+
anything. Store routes answer 404 even without a customer login (the gate runs
|
|
1608
|
+
before the route's authentication); Admin routes still require an Admin login
|
|
1609
|
+
first, because Medusa authenticates every `/admin` request before any plugin
|
|
1610
|
+
middleware (401 without login, 404 afterwards). A feature is only effective while the plugin
|
|
1611
|
+
switch (default on) is on.
|
|
1612
|
+
|
|
1613
|
+
| Key | Label | Default | Gates | Keeps running when off |
|
|
1614
|
+
| --------------- | -------------------- | ------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------- |
|
|
1615
|
+
| (plugin switch) | Dokumente | on | every Documents route (`/store/documents/*`, `/admin/orders/:id/documents/*`, `/admin/orders/:id/packing-lists/*`) and, through the two features, every subscriber and job | checkout, fulfillment and shipping; already issued documents and PDFs stay stored |
|
|
1616
|
+
| `invoices` | Rechnungen & Stornos | on | `/store/documents/*`, `/admin/orders/:id/documents/*`; subscribers `documents-shipment-created`, `documents-order-canceled`, `payment-refunded`; jobs `documents-fact-publication-reconciliation`, `documents-domain-resolution-reconciliation` | packing lists; stored invoices stay stored but are not reachable through the API |
|
|
1617
|
+
| `packing_lists` | Packlisten | on | `/admin/orders/:id/packing-lists/*`; subscribers `packing-list-fulfillment-created`, `packing-list-fulfillment-canceled`, `packing-list-allocation-revised`; job `documents-packing-list-reconciliation` | invoices, cancellations and refund documents |
|
|
1618
|
+
|
|
1619
|
+
**Events are not replayed.** While `invoices` is off, no invoice,
|
|
1620
|
+
cancellation or refund document is issued for new shipments, cancellations
|
|
1621
|
+
or refunds, and nothing is issued later for them either once the feature is
|
|
1622
|
+
switched back on; issue a missing invoice by hand with
|
|
1623
|
+
`POST /admin/orders/:id/documents`. While `packing_lists` is off, no packing
|
|
1624
|
+
list is created, revised or canceled; after switching it back on, the
|
|
1625
|
+
periodic `documents-packing-list-reconciliation` job creates the missing
|
|
1626
|
+
packing lists of non-canceled physical fulfillments (cancellations that
|
|
1627
|
+
happened meanwhile are not replayed).
|
|
1628
|
+
|
|
1629
|
+
Admin surfaces of a disabled feature are hidden. The Admin widgets on the
|
|
1630
|
+
order detail page (`order-documents`, `order-packing-lists`) stay registered
|
|
1631
|
+
but render nothing while their feature (`invoices`, `packing_lists`) is off,
|
|
1632
|
+
using `PluginFeatureGate` from
|
|
1633
|
+
`@hartl-services/medusa-base/admin-components`.
|
|
1634
|
+
|
|
1635
|
+
No workflow hook and nothing on the cart, checkout or fulfillment path reads
|
|
1636
|
+
a toggle.
|
|
1637
|
+
|
|
1638
|
+
### Shop integration
|
|
1639
|
+
|
|
1640
|
+
The demo shop (`apps/backend`) gets everything from the preset (see
|
|
1641
|
+
"Shop-Config-Preset"): `documents.merchant` is its `merchant` option,
|
|
1642
|
+
`documents.numbering.timeZone` its `timeZone` option and
|
|
1643
|
+
`documents.pdf.executablePath` the `DOCUMENT_PDF_EXECUTABLE` variable. The
|
|
1644
|
+
development UID `ATU00000000` is a placeholder: Documents never prints it,
|
|
1645
|
+
and Tax Compliance does not transmit it to FinanzOnline. Enter the real
|
|
1646
|
+
merchant UID in the Shop-Stammdaten before issuing production invoices.
|
|
1647
|
+
|
|
1648
|
+
`htmlFactory` is intentionally the only presentation extension point. It gets
|
|
1649
|
+
a fully typed value snapshot (`DocumentTemplateInput` or
|
|
1650
|
+
`PackingListTemplateInput`) plus `merchant` (see "Merchant data"), never a
|
|
1651
|
+
live Medusa model. Escape every dynamic value in the factory; the HTML is
|
|
1652
|
+
rendered by a headless Chromium process before it is stored as a private PDF.
|
|
1653
|
+
|
|
1654
|
+
The consuming host needs a Chromium/Chrome executable and a Medusa File
|
|
1655
|
+
provider that supports private files. Run the Medusa process as a dedicated
|
|
1656
|
+
non-root user: the PDF renderer keeps Chromium's sandbox enabled and therefore
|
|
1657
|
+
refuses to start as root. Installation and Dokploy runtime checks are
|
|
1658
|
+
documented under `Chromium für Dokument-PDFs` in `apps/backend/README.md`.
|
|
1659
|
+
Each render uses an ephemeral Chrome profile below its private temporary
|
|
1660
|
+
directory and removes it afterward. The Chromium child is also disconnected
|
|
1661
|
+
from the user's session bus and legacy keyring socket, so it cannot access the
|
|
1662
|
+
operator's normal browser profile or desktop keyring, including under WSLg.
|
|
1663
|
+
|
|
1664
|
+
### Restore check for private files
|
|
1665
|
+
|
|
1666
|
+
After restoring the object store (or the local file directory), check that
|
|
1667
|
+
every issued document's PDF can be read through the active Medusa File
|
|
1668
|
+
provider. The read-only script lives in the base but is deliberately not a
|
|
1669
|
+
migration script, so `db:migrate` never runs it:
|
|
1670
|
+
|
|
1671
|
+
```bash
|
|
1672
|
+
pnpm exec medusa exec ./node_modules/@hartl-services/medusa-base/.medusa/server/src/documents/scripts/verify-documents-file-storage.js
|
|
1673
|
+
```
|
|
1674
|
+
|
|
1675
|
+
It fails on the first issued document without a stored file and otherwise
|
|
1676
|
+
logs the number of verified files and bytes.
|
|
1677
|
+
|
|
1678
|
+
### Clean baseline and first deployment
|
|
1679
|
+
|
|
1680
|
+
The module intentionally contains one generated eight-table baseline,
|
|
1681
|
+
`Migration20260827061749`. It removes the former `documents_delivery` table
|
|
1682
|
+
entirely and adds the Artifact, Shipment-assignment/coordinator and Refund-/
|
|
1683
|
+
Order-cancellation resolution tables. Old delivery rows are not migrated:
|
|
1684
|
+
Email now owns logical messages and attempts. The generated snapshot is not
|
|
1685
|
+
edited by hand, while migration-only constraints and indexes remain in the
|
|
1686
|
+
baseline.
|
|
1687
|
+
|
|
1688
|
+
`Migration20260830111207` is the forward migration that adds the
|
|
1689
|
+
`documents_document_number_sequence` table, makes every financial document's
|
|
1690
|
+
number and issue date mandatory, and protects the complete document number with
|
|
1691
|
+
an unconditional unique index. It deliberately does not renumber legacy rows.
|
|
1692
|
+
A disposable local database containing pre-migration Documents rows with null
|
|
1693
|
+
or duplicate numbers must be recreated rather than silently backfilled.
|
|
1694
|
+
|
|
1695
|
+
A disposable local database that has already run an older Documents baseline
|
|
1696
|
+
must be recreated once after stopping the backend and verifying the exact local
|
|
1697
|
+
`DATABASE_URL` target:
|
|
1698
|
+
|
|
1699
|
+
```bash
|
|
1700
|
+
pnpm --dir apps/backend db:recreate
|
|
1701
|
+
```
|
|
1702
|
+
|
|
1703
|
+
This command deletes all data and is restricted to local development. Never use
|
|
1704
|
+
it for staging, shared or production databases; those require an
|
|
1705
|
+
environment-owner-approved reset. A first production installation is supported
|
|
1706
|
+
only on a deliberately empty database. Run:
|
|
1707
|
+
|
|
1708
|
+
```bash
|
|
1709
|
+
pnpm --dir apps/backend db:migrate
|
|
1710
|
+
pnpm --dir apps/backend exec medusa db:sync-links
|
|
1711
|
+
```
|
|
1712
|
+
|
|
1713
|
+
`db:migrate` applies all module baselines and Medusa's pending initial seed once;
|
|
1714
|
+
do not invoke that seed separately. If any production database has already run
|
|
1715
|
+
an older Documents baseline or contains data that must survive, stop: this clean
|
|
1716
|
+
baseline is not a forward migration or backfill and a new forward migration is
|
|
1717
|
+
required instead.
|
|
1718
|
+
|
|
1719
|
+
Apply normal pending module migrations after later changes or upgrades with
|
|
1720
|
+
`pnpm --dir apps/backend db:migrate`. This plugin defines no module links, so it
|
|
1721
|
+
does not require link synchronization by itself; `db:sync-links` above is the
|
|
1722
|
+
safe shop-wide first-deployment step for links owned elsewhere.
|
|
1723
|
+
|
|
1724
|
+
### Storefront contract
|
|
1725
|
+
|
|
1726
|
+
All three endpoints require a customer session or bearer token. There is no
|
|
1727
|
+
guest lookup endpoint and no direct File-provider URL.
|
|
1728
|
+
|
|
1729
|
+
- `GET /store/documents` – document summaries for the authenticated customer.
|
|
1730
|
+
- `GET /store/documents/:id` – one owned document summary.
|
|
1731
|
+
- `GET /store/documents/:id/download` – streams the owned private PDF.
|
|
1732
|
+
|
|
1733
|
+
The public DTO deliberately includes only the document number, kind, issue
|
|
1734
|
+
date, order display ID and total. It excludes the immutable snapshot, recipient
|
|
1735
|
+
address/email, file and artifact IDs, leases and resolution state.
|
|
1736
|
+
|
|
1737
|
+
For a typed Storefront integration:
|
|
1738
|
+
|
|
1739
|
+
```ts
|
|
1740
|
+
import type { ListStoreDocumentsResponse } from "@hartl-services/medusa-food-supplements-contracts/types/documents-store";
|
|
1741
|
+
import { documentsPaths } from "@hartl-services/medusa-food-supplements-contracts/paths/documents";
|
|
1742
|
+
|
|
1743
|
+
const { documents: issuedDocuments } =
|
|
1744
|
+
await sdk.client.fetch<ListStoreDocumentsResponse>(
|
|
1745
|
+
documentsPaths.store.get.list,
|
|
1746
|
+
{ method: "GET" },
|
|
1747
|
+
);
|
|
1748
|
+
const downloadPath = documentsPaths.store.get.download(issuedDocuments[0].id);
|
|
1749
|
+
```
|
|
1750
|
+
|
|
1751
|
+
The Storefront should request `downloadPath` with its authenticated SDK;
|
|
1752
|
+
it is deliberately a protected application route rather than an exposed object
|
|
1753
|
+
storage URL.
|
|
1754
|
+
|
|
1755
|
+
The same Contracts boundary applies to all Documents and Tax Compliance public
|
|
1756
|
+
routes: import transport DTOs only from
|
|
1757
|
+
`@hartl-services/medusa-food-supplements-contracts/types/<feature>`, paths only from
|
|
1758
|
+
`@hartl-services/medusa-food-supplements-contracts/paths/<feature>`, and call the
|
|
1759
|
+
Storefront's configured Medusa SDK. A consumer that needs a runtime guard may
|
|
1760
|
+
opt in to `@hartl-services/medusa-food-supplements-contracts/schemas/<feature>`; it must
|
|
1761
|
+
not import either the plugin implementation or generated `.medusa` output.
|
|
1762
|
+
|
|
1763
|
+
### Admin order detail
|
|
1764
|
+
|
|
1765
|
+
When Documents is enabled, the Medusa Admin order detail shows a
|
|
1766
|
+
**Dokumente** widget. It lists the issued documents for that order and opens a
|
|
1767
|
+
private PDF preview. The widget obtains the PDF through the Medusa JS SDK, so
|
|
1768
|
+
both session and bearer-token Admin authentication work without exposing a
|
|
1769
|
+
File-provider URL.
|
|
1770
|
+
|
|
1771
|
+
The Admin API is deliberately scoped by both order and document ID:
|
|
1772
|
+
|
|
1773
|
+
- `GET /admin/orders/:id/documents`
|
|
1774
|
+
- `POST /admin/orders/:id/documents/regenerate`
|
|
1775
|
+
- `POST /admin/orders/:id/documents/:document_id/regenerate`
|
|
1776
|
+
- `GET /admin/orders/:id/documents/:document_id/download`
|
|
1777
|
+
|
|
1778
|
+
All authenticated Admin users can inspect order documents; a document from a
|
|
1779
|
+
different order returns `404` from the download endpoint.
|
|
1780
|
+
|
|
1781
|
+
#### Internal packing lists
|
|
1782
|
+
|
|
1783
|
+
The separate **Packlisten** order widget lists every physical fulfillment in
|
|
1784
|
+
creation order. A ready entry can be previewed and downloaded as a private PDF;
|
|
1785
|
+
missing or failed entries can be generated again through the same idempotent
|
|
1786
|
+
workflow used by the automatic subscriber.
|
|
1787
|
+
|
|
1788
|
+
- `GET /admin/orders/:id/packing-lists`
|
|
1789
|
+
- `POST /admin/orders/:id/packing-lists/:fulfillment_id/retry`
|
|
1790
|
+
- `GET /admin/orders/:id/packing-lists/:fulfillment_id/download`
|
|
1791
|
+
|
|
1792
|
+
The PDF freezes the fulfillment-item title, SKU, quantity and Medusa Stock
|
|
1793
|
+
Location name. Managed variants with multiple inventory items are listed as
|
|
1794
|
+
their concrete warehouse components. Their quantity is the concrete inventory
|
|
1795
|
+
quantity after Medusa's `required_quantity` multiplier, not necessarily the
|
|
1796
|
+
commercial order-line quantity.
|
|
1797
|
+
|
|
1798
|
+
Packing lists never pass through the Documents customer-fact pipeline. They are
|
|
1799
|
+
also stored outside `documents_document`, so the authenticated Store document
|
|
1800
|
+
queries cannot expose them accidentally.
|
|
1801
|
+
|
|
1802
|
+
#### Regenerating an invoice
|
|
1803
|
+
|
|
1804
|
+
Once an invoice exists, the widget changes **Rechnung ausstellen** to
|
|
1805
|
+
**Rechnung regenerieren**. This action retains the document ID, document number
|
|
1806
|
+
and original issue date, but replaces its persisted PDF and snapshot with a
|
|
1807
|
+
fresh render. A prior artifact that is locked by a committed communication fact
|
|
1808
|
+
and its private File are retained permanently. Only an unlocked superseded
|
|
1809
|
+
artifact File may be deleted on a best-effort basis after the replacement has
|
|
1810
|
+
been stored, so Storefront and Admin continue to show exactly one current PDF.
|
|
1811
|
+
|
|
1812
|
+
Regeneration re-applies the normal completed-shipment and cancellation checks,
|
|
1813
|
+
then reads the current order values and active batch withdrawals. It is a
|
|
1814
|
+
deliberate exception workflow and does not emit a customer communication event.
|
|
1815
|
+
|
|
1816
|
+
The document-specific regeneration route is used by the widget for every
|
|
1817
|
+
issued document. For invoices it has the same live-data behavior described
|
|
1818
|
+
above. A cancellation or future credit note is instead rendered from its
|
|
1819
|
+
persisted snapshot, retaining its original amount, source invoice reference,
|
|
1820
|
+
number and issue date. It also emits no customer communication event.
|
|
1821
|
+
|
|
1822
|
+
#### Financial document numbers
|
|
1823
|
+
|
|
1824
|
+
Documents assigns numbers in these annual ranges:
|
|
1825
|
+
|
|
1826
|
+
```text
|
|
1827
|
+
RE-2026-1
|
|
1828
|
+
ST-2026-1
|
|
1829
|
+
GS-2026-1
|
|
1830
|
+
```
|
|
1831
|
+
|
|
1832
|
+
The prefix identifies the document kind, the year is resolved in the selected
|
|
1833
|
+
shop's configured numbering time zone, and the final integer is allocated by a
|
|
1834
|
+
transactional PostgreSQL counter. A new range starts at `1` for each document
|
|
1835
|
+
kind and year. `order_display_id` remains a separate order reference in the
|
|
1836
|
+
snapshot and PDF; it never determines a financial document number.
|
|
1837
|
+
|
|
1838
|
+
Number and issue date are reserved in the same short transaction that creates
|
|
1839
|
+
the processing document. PDF rendering and File I/O remain outside database
|
|
1840
|
+
transactions. A retry reuses the reserved identity and regeneration never
|
|
1841
|
+
increments a range. A permanently failed document retains its number and
|
|
1842
|
+
failure reason as the explanation for that range's gap; reserved numbers are
|
|
1843
|
+
never recycled or reassigned.
|
|
1844
|
+
|
|
1845
|
+
#### Cancellation documents for payment refunds
|
|
1846
|
+
|
|
1847
|
+
Medusa's refund event identifies the payment, so the plugin enumerates its
|
|
1848
|
+
persisted refund records and claims one cancellation document using each
|
|
1849
|
+
immutable refund ID. This makes delivery at-least-once safe: repeated events
|
|
1850
|
+
or a retry leave an already issued cancellation unchanged.
|
|
1851
|
+
|
|
1852
|
+
When an invoice exists, the cancellation issue date is reserved when Documents
|
|
1853
|
+
starts issuing the cancellation and its amount is negative. Since a payment
|
|
1854
|
+
refund has no line-item allocation, the plugin proportionally derives the
|
|
1855
|
+
original invoice's subtotal and tax from the
|
|
1856
|
+
immutable invoice snapshot; the document has one explicit `Rückerstattung zu
|
|
1857
|
+
Rechnung …` line. The document, immutable artifact and final Refund resolution
|
|
1858
|
+
are committed in one short token-fenced transaction.
|
|
1859
|
+
|
|
1860
|
+
For a non-canceled Refund without an existing invoice, Documents finalizes a
|
|
1861
|
+
`not_applicable` resolution and publishes the Refund fact without an artifact.
|
|
1862
|
+
It does not wait forever for a future invoice, and later invoice issuance does
|
|
1863
|
+
not create a second customer fact. For a canceled Order, individual Refund
|
|
1864
|
+
facts are suppressed in favor of the authoritative Order-cancellation
|
|
1865
|
+
resolution and its sorted artifact list.
|
|
1866
|
+
|
|
1867
|
+
### Optional Batch Management integration
|
|
1868
|
+
|
|
1869
|
+
If `@hartl-services/medusa-batch-management` version `0.0.3` or later is registered, Documents detects
|
|
1870
|
+
its `getOrderDocumentBatchSnapshot(orderId)` capability structurally. For every
|
|
1871
|
+
active withdrawal it prints the persisted **charge/lot**, **MHD** and quantity
|
|
1872
|
+
under the matching invoice line.
|
|
1873
|
+
|
|
1874
|
+
These values are captured at FEFO withdrawal time. Regenerating an invoice
|
|
1875
|
+
reads the active withdrawals again: reverse/correct the order's allocation
|
|
1876
|
+
first, then regenerate to put that corrected allocation on the document.
|
|
1877
|
+
Renaming a Batch master record alone deliberately does not change an already
|
|
1878
|
+
persisted withdrawal snapshot. A shipment received before the matching durable
|
|
1879
|
+
batch-withdrawal run is available fails safely and can be retried. An installed
|
|
1880
|
+
incompatible Batch version fails explicitly before a PDF is issued; upgrade
|
|
1881
|
+
Batch Management and apply its migration first.
|
|
1882
|
+
|
|
1883
|
+
Packing lists use the separate structural capability
|
|
1884
|
+
`getFulfillmentPackingListBatchSnapshot(fulfillmentId)`. It returns only active
|
|
1885
|
+
or already returned immutable LOT snapshots belonging to that exact
|
|
1886
|
+
fulfillment. Return receipts reconstruct the original picked quantity before a
|
|
1887
|
+
delayed first render. Documents validates that every managed fulfillment item's
|
|
1888
|
+
concrete quantity equals the sum of its LOT quantities before rendering. If
|
|
1889
|
+
Batch Management is not registered, the same packing list is issued without LOT
|
|
1890
|
+
rows. An installed but incompatible Batch version fails explicitly.
|
|
1891
|
+
|
|
1892
|
+
A manual allocation reassignment increments the fulfillment run's allocation
|
|
1893
|
+
revision and emits an internal event. Documents stores a new immutable packing
|
|
1894
|
+
list revision and serves the newest ready PDF. Returns do not rewrite the
|
|
1895
|
+
historical pick instruction. Fulfillment cancellation marks the packing list as
|
|
1896
|
+
canceled and prevents a concurrent render from being published as active.
|
|
1897
|
+
|
|
1898
|
+
### Document facts and artifacts
|
|
1899
|
+
|
|
1900
|
+
Documents owns no Notification or delivery state. Its integration boundary is
|
|
1901
|
+
the strict versioned event contract exported from
|
|
1902
|
+
`@hartl-services/medusa-food-supplements-contracts/events/documents`. Shipment assignments, standalone Refund resolutions and Order-
|
|
1903
|
+
cancellation resolutions each carry an `event_emitted_at` marker that is set
|
|
1904
|
+
only after successful enqueue.
|
|
1905
|
+
|
|
1906
|
+
Every customer-facing attachment references an immutable `application/pdf`
|
|
1907
|
+
artifact. The artifact becomes permanently `communication_locked` in the same
|
|
1908
|
+
transaction that finalizes the fact. The publication reconciler first verifies
|
|
1909
|
+
the private File remains readable, and then safely republishes any final fact
|
|
1910
|
+
whose marker is still null. Refunds without an invoice and zero-Refund
|
|
1911
|
+
cancellations are the deliberate attachment-free cases.
|
|
1912
|
+
|
|
1913
|
+
Document issuance and resolution work uses ten-minute token leases. PDF and
|
|
1914
|
+
File I/O runs outside database transactions. If File creation succeeds after a
|
|
1915
|
+
worker has lost its lease, the unreferenced private File is deleted best-effort;
|
|
1916
|
+
a hard process exit in that external File/database gap can leave a storage
|
|
1917
|
+
orphan but cannot commit another Artifact, carrier or resolution.
|
|
1918
|
+
|
|
1919
|
+
#### Exact outgoing event contracts
|
|
1920
|
+
|
|
1921
|
+
The server-only Contracts subpath
|
|
1922
|
+
`@hartl-services/medusa-food-supplements-contracts/events/documents` exports the
|
|
1923
|
+
event-name constants,
|
|
1924
|
+
strict Zod schemas and inferred payload types for exactly these three events:
|
|
1925
|
+
|
|
1926
|
+
```ts
|
|
1927
|
+
type DocumentsEmailArtifact = {
|
|
1928
|
+
artifact_id: string;
|
|
1929
|
+
document_id: string;
|
|
1930
|
+
document_number: string;
|
|
1931
|
+
file_id: string;
|
|
1932
|
+
filename: string;
|
|
1933
|
+
content_type: "application/pdf";
|
|
1934
|
+
};
|
|
1935
|
+
|
|
1936
|
+
type DocumentsOutgoingEmailEvent =
|
|
1937
|
+
| {
|
|
1938
|
+
name: "documents.shipment_invoice_assignment.decided.v1";
|
|
1939
|
+
data:
|
|
1940
|
+
| {
|
|
1941
|
+
schema_version: 1;
|
|
1942
|
+
order_id: string;
|
|
1943
|
+
fulfillment_id: string;
|
|
1944
|
+
decision: "non_carrier";
|
|
1945
|
+
}
|
|
1946
|
+
| {
|
|
1947
|
+
schema_version: 1;
|
|
1948
|
+
order_id: string;
|
|
1949
|
+
fulfillment_id: string;
|
|
1950
|
+
decision: "carrier";
|
|
1951
|
+
invoice_fulfillment_id: string;
|
|
1952
|
+
invoice_artifact: DocumentsEmailArtifact;
|
|
1953
|
+
};
|
|
1954
|
+
}
|
|
1955
|
+
| {
|
|
1956
|
+
name: "documents.order_cancellation_documents.ready.v1";
|
|
1957
|
+
data: {
|
|
1958
|
+
schema_version: 1;
|
|
1959
|
+
order_id: string;
|
|
1960
|
+
expected_refund_ids: string[];
|
|
1961
|
+
attachments: Array<{
|
|
1962
|
+
payment_id: string;
|
|
1963
|
+
refund_id: string;
|
|
1964
|
+
artifact: DocumentsEmailArtifact;
|
|
1965
|
+
}>;
|
|
1966
|
+
};
|
|
1967
|
+
}
|
|
1968
|
+
| {
|
|
1969
|
+
name: "documents.refund_document.ready.v1";
|
|
1970
|
+
data: {
|
|
1971
|
+
schema_version: 1;
|
|
1972
|
+
order_id: string;
|
|
1973
|
+
payment_id: string;
|
|
1974
|
+
refund_id: string;
|
|
1975
|
+
artifact: DocumentsEmailArtifact | null;
|
|
1976
|
+
};
|
|
1977
|
+
};
|
|
1978
|
+
```
|
|
1979
|
+
|
|
1980
|
+
Every `string` above is trimmed and non-empty. For a `carrier`,
|
|
1981
|
+
`invoice_fulfillment_id` must equal `fulfillment_id`. Cancellation
|
|
1982
|
+
`expected_refund_ids` are lexicographically sorted and unique; `attachments`
|
|
1983
|
+
are sorted by unique `refund_id`, and every attached Refund belongs to the
|
|
1984
|
+
expected set. `artifact: null` is the final no-Invoice Refund result. The events
|
|
1985
|
+
contain no recipient, template, Notification ID, SMTP setting or delivery
|
|
1986
|
+
status.
|
|
1987
|
+
|
|
1988
|
+
Documents owns committed facts and their at-least-once publication. The E-Mail
|
|
1989
|
+
module owns recipient resolution, logical message deduplication, templates,
|
|
1990
|
+
attachments at dispatch, Medusa Notification records, SMTP attempts, status and
|
|
1991
|
+
explicit Admin resends. See the central
|
|
1992
|
+
[Email design](../../docs/superpowers/specs/2026-08-26-central-email-plugin-design.md)
|
|
1993
|
+
and
|
|
1994
|
+
[implementation plan](../../docs/superpowers/plans/2026-08-26-central-email-plugin.md).
|
|
1995
|
+
|
|
1996
|
+
Neither event publication nor SMTP promises exactly-once delivery. Publication
|
|
1997
|
+
reconciliation and the Redis Event Bus may deliver a fact more than once; Email
|
|
1998
|
+
deduplicates the logical message by its business key. SMTP may accept a message
|
|
1999
|
+
before the process can persist success, so an ambiguous attempt becomes
|
|
2000
|
+
`unknown` and is not automatically sent again. Any later resend is an explicit
|
|
2001
|
+
Email Admin action and a new logical message.
|
|
2002
|
+
|
|
2003
|
+
### Development
|
|
2004
|
+
|
|
2005
|
+
#### Public Storefront and Admin contracts
|
|
2006
|
+
|
|
2007
|
+
The central contracts package exports the Documents Store and Admin DTOs, Zod
|
|
2008
|
+
schemas and route paths, but no second Storefront client. Consumers use their
|
|
2009
|
+
own Medusa SDK instance:
|
|
2010
|
+
|
|
2011
|
+
```ts
|
|
2012
|
+
import type { GetStoreDocumentResponse } from "@hartl-services/medusa-food-supplements-contracts/types/documents-store";
|
|
2013
|
+
import { documentsPaths } from "@hartl-services/medusa-food-supplements-contracts/paths/documents";
|
|
2014
|
+
|
|
2015
|
+
const response = await sdk.client.fetch<GetStoreDocumentResponse>(
|
|
2016
|
+
documentsPaths.store.get.retrieve(documentId),
|
|
2017
|
+
{ method: "GET" },
|
|
2018
|
+
);
|
|
2019
|
+
```
|
|
2020
|
+
|
|
2021
|
+
`@hartl-services/medusa-food-supplements-contracts/types/documents-store` and
|
|
2022
|
+
`@hartl-services/medusa-food-supplements-contracts/types/documents-admin` contain the
|
|
2023
|
+
respective type-only DTOs. Import
|
|
2024
|
+
`@hartl-services/medusa-food-supplements-contracts/schemas/documents` only for explicit
|
|
2025
|
+
runtime validation. The common Storefront case imports only types, which are
|
|
2026
|
+
erased by TypeScript, and the path module; it never imports the plugin's Medusa
|
|
2027
|
+
server code. Store and Admin responses use the same safe `DocumentSummary`
|
|
2028
|
+
transport DTO, while protected PDF download paths remain binary endpoints.
|
|
2029
|
+
|
|
2030
|
+
Add a DTO schema, central contract type and path entry, HTTP integration test,
|
|
2031
|
+
and README note together when introducing another Documents route.
|
|
2032
|
+
|
|
2033
|
+
Packing-list consumers stay Admin-only:
|
|
2034
|
+
|
|
2035
|
+
```ts
|
|
2036
|
+
import type { ListAdminOrderPackingListsResponse } from "@hartl-services/medusa-food-supplements-contracts/types/documents-admin";
|
|
2037
|
+
import { packingListsPaths } from "@hartl-services/medusa-food-supplements-contracts/paths/documents";
|
|
2038
|
+
|
|
2039
|
+
const response = await sdk.client.fetch<ListAdminOrderPackingListsResponse>(
|
|
2040
|
+
packingListsPaths.admin.get.listForOrder(orderId),
|
|
2041
|
+
{ method: "GET" },
|
|
2042
|
+
);
|
|
2043
|
+
```
|
|
2044
|
+
|
|
2045
|
+
## Dateispeicher (S3)
|
|
2046
|
+
|
|
2047
|
+
The base provides a Medusa File Module provider for durable S3-compatible
|
|
2048
|
+
asset storage with a local, TTL-based read cache (`providers/s3-cache`), and
|
|
2049
|
+
the `/files` gateway routes every provider URL points at. It supports AWS S3,
|
|
2050
|
+
MinIO and compatible endpoints through AWS SDK v3. There is no separate S3
|
|
2051
|
+
package since 1.0.0; the provider identifier `s3-cache` and the resolve path
|
|
2052
|
+
`@hartl-services/medusa-base/providers/s3-cache` replace the old one.
|
|
2053
|
+
|
|
2054
|
+
S3 is always the source of truth. The local cache is a per-replica performance
|
|
2055
|
+
optimization and may be discarded at any time without data loss.
|
|
2056
|
+
|
|
2057
|
+
### Coverage
|
|
2058
|
+
|
|
2059
|
+
The provider covers every asset that uses Medusa's native `Modules.FILE`
|
|
2060
|
+
service:
|
|
2061
|
+
|
|
2062
|
+
- public product images and other Medusa uploads;
|
|
2063
|
+
- private PDF documents of the Documents module, including its
|
|
2064
|
+
authorized download routes and email attachments;
|
|
2065
|
+
- future plugins that use the same File Module.
|
|
2066
|
+
|
|
2067
|
+
It cannot transparently back up a plugin that writes directly to its own local
|
|
2068
|
+
directory, own S3 client, or a third-party URL. Such integrations must be
|
|
2069
|
+
changed to use `Modules.FILE` or migrated explicitly.
|
|
2070
|
+
|
|
2071
|
+
### Configuration
|
|
2072
|
+
|
|
2073
|
+
```ts
|
|
2074
|
+
// medusa-config.ts
|
|
2075
|
+
import { createS3CacheStorageConfig } from "@hartl-services/medusa-base/config";
|
|
2076
|
+
|
|
2077
|
+
const s3CacheStorage = createS3CacheStorageConfig({
|
|
2078
|
+
s3: {
|
|
2079
|
+
bucket: process.env.S3_CACHE_STORAGE_BUCKET,
|
|
2080
|
+
region: process.env.S3_CACHE_STORAGE_REGION,
|
|
2081
|
+
// optional: MinIO or another S3-compatible endpoint
|
|
2082
|
+
endpoint: process.env.S3_CACHE_STORAGE_ENDPOINT,
|
|
2083
|
+
forcePathStyle: process.env.S3_CACHE_STORAGE_ENDPOINT ? true : undefined,
|
|
2084
|
+
// omit entirely to use the AWS SDK credential chain (e.g. an IAM role)
|
|
2085
|
+
credentials: process.env.S3_CACHE_STORAGE_ACCESS_KEY_ID
|
|
2086
|
+
? {
|
|
2087
|
+
accessKeyId: process.env.S3_CACHE_STORAGE_ACCESS_KEY_ID,
|
|
2088
|
+
secretAccessKey: process.env.S3_CACHE_STORAGE_SECRET_ACCESS_KEY,
|
|
2089
|
+
}
|
|
2090
|
+
: undefined,
|
|
2091
|
+
},
|
|
2092
|
+
gateway: {
|
|
2093
|
+
baseUrl: `${process.env.MEDUSA_BACKEND_URL}/files`,
|
|
2094
|
+
signingSecret: process.env.FILE_GATEWAY_SIGNING_SECRET,
|
|
2095
|
+
},
|
|
2096
|
+
});
|
|
2097
|
+
|
|
2098
|
+
export default defineConfig({
|
|
2099
|
+
modules: [s3CacheStorage.fileModule /* , ...other modules */],
|
|
2100
|
+
plugins: [
|
|
2101
|
+
{
|
|
2102
|
+
resolve: "@hartl-services/medusa-base",
|
|
2103
|
+
options: { files: { gateway: s3CacheStorage.gateway } },
|
|
2104
|
+
},
|
|
2105
|
+
/* , ...other plugins */
|
|
2106
|
+
],
|
|
2107
|
+
});
|
|
2108
|
+
```
|
|
2109
|
+
|
|
2110
|
+
S3 Cache Storage needs **both** parts, which `createS3CacheStorageConfig`
|
|
2111
|
+
returns from one validated configuration:
|
|
2112
|
+
|
|
2113
|
+
- `fileModule` goes under `modules`. It registers the S3 provider as Medusa's
|
|
2114
|
+
File Module (`@medusajs/medusa/file`); Medusa accepts File providers only
|
|
2115
|
+
there, not from a plugin.
|
|
2116
|
+
- `gateway` goes into the base options as `files.gateway`. It carries the
|
|
2117
|
+
signing secret of the gateway routes every provider URL points at:
|
|
2118
|
+
`GET /files/:asset`, `GET /files/private/:token` and
|
|
2119
|
+
`PUT /files/upload/:token`. Without it, no public file, signed private
|
|
2120
|
+
download or presigned upload works (the routes answer that signing is not
|
|
2121
|
+
configured).
|
|
2122
|
+
|
|
2123
|
+
Both carry the same signing secret, so the routes accept every URL the
|
|
2124
|
+
provider signs. Upload CORS follows Medusa's `projectConfig.http.adminCors`.
|
|
2125
|
+
|
|
2126
|
+
`createS3CacheFileModuleConfig` remains for Medusa's stock local File Module
|
|
2127
|
+
(`enabled` omitted or `false`); with `enabled: true` it registers only the
|
|
2128
|
+
provider, without the gateway part, so use `createS3CacheStorageConfig`
|
|
2129
|
+
instead. The signing helpers of the routes (`createPrivateFileGatewayUrl`, ...)
|
|
2130
|
+
are exported from `@hartl-services/medusa-base/gateway`.
|
|
2131
|
+
|
|
2132
|
+
Required options and environment variables:
|
|
2133
|
+
|
|
2134
|
+
- `s3.bucket` (`S3_CACHE_STORAGE_BUCKET`) and `s3.region`
|
|
2135
|
+
(`S3_CACHE_STORAGE_REGION`).
|
|
2136
|
+
- `gateway.baseUrl` — the public Medusa origin plus `/files`, derived from
|
|
2137
|
+
`MEDUSA_BACKEND_URL`. It must never point at S3 or MinIO.
|
|
2138
|
+
- `gateway.signingSecret` — a strong, server-only secret used to sign private
|
|
2139
|
+
file and upload URLs. Changing it invalidates every outstanding signed URL.
|
|
2140
|
+
|
|
2141
|
+
Everything else (`s3.endpoint`, `s3.forcePathStyle`, `s3.storagePrefix`,
|
|
2142
|
+
`s3.credentials`, `gateway.privateDownloadDurationSeconds`, and the `cache.*`
|
|
2143
|
+
options) is optional; see
|
|
2144
|
+
[Active S3 options and defaults](#active-s3-options-and-defaults) below. The
|
|
2145
|
+
provider has no database migrations to run.
|
|
2146
|
+
|
|
2147
|
+
### Environment helpers
|
|
2148
|
+
|
|
2149
|
+
`@hartl-services/medusa-base/config` also exports small helpers for reading
|
|
2150
|
+
the deployment variables in `medusa-config.ts`:
|
|
2151
|
+
|
|
2152
|
+
- `S3_BUILD_PLACEHOLDERS` — `{ S3_CACHE_STORAGE_BUCKET, S3_CACHE_STORAGE_REGION }`
|
|
2153
|
+
placeholder values so `medusa build` can load the configuration without
|
|
2154
|
+
deployment secrets. Use them only while building.
|
|
2155
|
+
- `resolveS3Credentials(process.env, phase)` — returns the static key pair
|
|
2156
|
+
from `S3_CACHE_STORAGE_ACCESS_KEY_ID`, `S3_CACHE_STORAGE_SECRET_ACCESS_KEY`
|
|
2157
|
+
and optional `S3_CACHE_STORAGE_SESSION_TOKEN`, or `undefined` for the AWS
|
|
2158
|
+
credential chain (and always during `"build"`). A declared but incomplete
|
|
2159
|
+
pair throws, naming the missing variables.
|
|
2160
|
+
- `resolveExpirationStrategy(process.env, defaultValue)` — reads
|
|
2161
|
+
`S3_CACHE_STORAGE_EXPIRATION_STRATEGY` (`"absolute"` or `"sliding"`).
|
|
2162
|
+
|
|
2163
|
+
### Runtime configuration
|
|
2164
|
+
|
|
2165
|
+
The demo shop (`apps/backend`) enables S3 Cache Storage as the sole Medusa
|
|
2166
|
+
File Module provider through the preset; there is no per-deployment feature
|
|
2167
|
+
flag or local-provider fallback. Dokploy must provide
|
|
2168
|
+
`S3_CACHE_STORAGE_BUCKET`, `S3_CACHE_STORAGE_REGION`, and
|
|
2169
|
+
`MEDUSA_BACKEND_URL`. The backend URL is the public Medusa origin, for
|
|
2170
|
+
example `https://api.example.com`; the preset derives the `/files` gateway
|
|
2171
|
+
path from it. It must never point to S3 or MinIO.
|
|
2172
|
+
|
|
2173
|
+
Set `S3_CACHE_STORAGE_ACCESS_KEY_ID` and
|
|
2174
|
+
`S3_CACHE_STORAGE_SECRET_ACCESS_KEY` together, or omit both to use the AWS
|
|
2175
|
+
credential chain. `S3_CACHE_STORAGE_SESSION_TOKEN` is valid only with that
|
|
2176
|
+
complete static key pair.
|
|
2177
|
+
|
|
2178
|
+
Do not change a production shop from S3 to local storage after it has written
|
|
2179
|
+
S3 keys unless the matching database and storage rollback is being performed.
|
|
2180
|
+
The local provider cannot read S3-backed IDs. Treat a storage-provider change
|
|
2181
|
+
as a controlled deployment cutover, not as a runtime storage router.
|
|
2182
|
+
|
|
2183
|
+
#### Active S3 options and defaults
|
|
2184
|
+
|
|
2185
|
+
In this demo host, `bucket`, `region`, and the File Gateway base URL are
|
|
2186
|
+
required.
|
|
2187
|
+
Credentials are either a complete access-key pair or omitted, in which case the
|
|
2188
|
+
AWS SDK credential chain (for example an IAM role) is used.
|
|
2189
|
+
|
|
2190
|
+
| Option | Default | Meaning |
|
|
2191
|
+
| ---------------------------------------- | -------------------- | ---------------------------------------------------------------------------------- |
|
|
2192
|
+
| `s3.endpoint` | none | Internal HTTP(S) endpoint; use it for MinIO or another S3-compatible service. |
|
|
2193
|
+
| `s3.forcePathStyle` | `false` | Set to `true` for normal MinIO deployments. |
|
|
2194
|
+
| `s3.storagePrefix` | `""` | Environment namespace inside the bucket, for example `production/medusa`. |
|
|
2195
|
+
| `gateway.privateDownloadDurationSeconds` | `3600` | Lifetime of signed Medusa download URLs; `1`–`604800`. |
|
|
2196
|
+
| `cache.dir` | `./.medusa-s3-cache` | Writable local directory belonging to one replica. |
|
|
2197
|
+
| `cache.ttlMs` | `3600000` | Cache freshness window; `0`–`604800000`. |
|
|
2198
|
+
| `cache.expirationStrategy` | `absolute` | `absolute` keeps the original write timestamp; `sliding` refreshes mtime on a hit. |
|
|
2199
|
+
| `cache.cleanupIntervalMs` | `900000` | Startup cleanup plus this unreferenced timer; `1`–`86400000`. |
|
|
2200
|
+
|
|
2201
|
+
URLs may use only HTTP(S), with no credentials, query string, or fragment.
|
|
2202
|
+
Prefixes and provider keys reject absolute paths, traversal, control
|
|
2203
|
+
characters, and backslashes.
|
|
2204
|
+
|
|
2205
|
+
### Object and access model
|
|
2206
|
+
|
|
2207
|
+
New uploads have opaque keys. The original filename is stored only as encoded
|
|
2208
|
+
object metadata:
|
|
2209
|
+
|
|
2210
|
+
```text
|
|
2211
|
+
logical key: public/<ULID>.<ext> or private/<ULID>.<ext>
|
|
2212
|
+
S3 key: <storagePrefix>/<logical key>
|
|
2213
|
+
public URL: <MEDUSA_BACKEND_URL>/files/<ULID>.<ext>
|
|
2214
|
+
private URL: <MEDUSA_BACKEND_URL>/files/private/<signed-token>
|
|
2215
|
+
```
|
|
2216
|
+
|
|
2217
|
+
No provider method returns an S3 URL. Public files receive stable Medusa gateway
|
|
2218
|
+
URLs and `Cache-Control: public, max-age=31536000, immutable`; their keys never
|
|
2219
|
+
change, so browser or an optional CDN in front of Medusa may cache them safely.
|
|
2220
|
+
Generic private files receive short-lived HMAC-signed Medusa URLs with
|
|
2221
|
+
`private, no-store`. Domain files such as invoices continue to use their
|
|
2222
|
+
authenticated Documents routes. Gateway responses use a restrictive sandbox
|
|
2223
|
+
Content Security Policy so active uploaded formats cannot execute under the
|
|
2224
|
+
Medusa API origin.
|
|
2225
|
+
|
|
2226
|
+
The provider sends no S3 ACL parameter. Keep the complete bucket and every
|
|
2227
|
+
prefix private. Only the Medusa runtime receives S3 credentials. Public and
|
|
2228
|
+
private presigned-upload requests receive a short-lived Medusa PUT capability
|
|
2229
|
+
under `/files/upload`; upload and download signatures are not interchangeable.
|
|
2230
|
+
The route accepts at most 100 MiB and allows configured `ADMIN_CORS` origins
|
|
2231
|
+
without exposing S3.
|
|
2232
|
+
|
|
2233
|
+
Backend reads (`getDownloadStream` and `getAsBuffer`) share an atomic cache:
|
|
2234
|
+
cache filenames are SHA-256 hashes, concurrent misses for one object download
|
|
2235
|
+
once per replica, and a failed refresh never serves an expired object. A
|
|
2236
|
+
successful remote write/delete evicts the affected entry only after S3 has
|
|
2237
|
+
accepted it. Do not share the cache directory between replicas; use a bounded
|
|
2238
|
+
`emptyDir` or per-pod volume and monitor free disk space.
|
|
2239
|
+
|
|
2240
|
+
### Backup and restore
|
|
2241
|
+
|
|
2242
|
+
A complete restore requires both a PostgreSQL backup and the versioned object
|
|
2243
|
+
store. The cache is not a backup and is intentionally excluded.
|
|
2244
|
+
|
|
2245
|
+
Before production use, provision a dedicated bucket and prefix per
|
|
2246
|
+
environment, Block Public Access, SSE-S3 or KMS encryption, least-privilege
|
|
2247
|
+
application IAM, versioning, and lifecycle cleanup for old versions/abandoned
|
|
2248
|
+
multipart uploads. For stronger recovery objectives, add cross-account or
|
|
2249
|
+
cross-region replication and rehearse restores.
|
|
2250
|
+
|
|
2251
|
+
To restore on another server, restore the database and bucket/prefix, keep the
|
|
2252
|
+
same File Gateway base URL and signing secret, configure the provider, and let
|
|
2253
|
+
the replica caches start empty.
|
|
2254
|
+
|
|
2255
|
+
The Documents module ships a read-only restore drill. It streams
|
|
2256
|
+
every issued PDF through the active File provider and reports the verified file
|
|
2257
|
+
count and total bytes without modifying data:
|
|
2258
|
+
|
|
2259
|
+
```sh
|
|
2260
|
+
medusa exec ./node_modules/@hartl-services/medusa-base/.medusa/server/src/documents/scripts/verify-documents-file-storage.js
|
|
2261
|
+
```
|
|
2262
|
+
|
|
2263
|
+
### HTTP contract
|
|
2264
|
+
|
|
2265
|
+
The gateway routes expose `GET /files/:asset` for public immutable files and
|
|
2266
|
+
`GET /files/private/:token` for signed private capability URLs. Both call
|
|
2267
|
+
`Modules.FILE.getDownloadStream`, so a fresh local cache entry is always used
|
|
2268
|
+
before S3. The private route validates its query through the Contracts schema.
|
|
2269
|
+
Paths, query types, and the runtime query schema are exported from the separate
|
|
2270
|
+
`/paths/files`, `/types/files`, and `/schemas/files` Contracts subpaths. Clients
|
|
2271
|
+
normally consume the complete URL returned by Medusa and never construct a
|
|
2272
|
+
private URL themselves.
|
|
2273
|
+
|
|
2274
|
+
## Shop-Config-Preset
|
|
2275
|
+
|
|
2276
|
+
`@hartl-services/medusa-base/shop-config` turns a food shop's configuration
|
|
2277
|
+
from about 290 lines into about 30: the plugin list and order, the Redis event
|
|
2278
|
+
bus, SMTP notifications, S3 file storage, the tax and Stripe payment providers,
|
|
2279
|
+
the Admin path, the Sentry logger bridge and the environment validation are
|
|
2280
|
+
assembled there; the shop supplies only its own facts. It reuses the `/config`
|
|
2281
|
+
helpers above for environment parsing. It is a separate subpath because it
|
|
2282
|
+
loads the Sentry logger wrapper and with it `@sentry/node`; `/config` stays
|
|
2283
|
+
free of it.
|
|
2284
|
+
|
|
2285
|
+
The preset registers the base (with its E-Mail, Documents and `/files` options)
|
|
2286
|
+
plus five feature plugins, in this order: `medusa-base`,
|
|
2287
|
+
`medusa-tax-compliance`, `medusa-free-shipping`, `medusa-batch-management`,
|
|
2288
|
+
`medusa-affiliate`, `medusa-payload-sync`.
|
|
2289
|
+
|
|
2290
|
+
### Installation in a shop
|
|
2291
|
+
|
|
2292
|
+
```bash
|
|
2293
|
+
pnpm add @hartl-services/medusa-base \
|
|
2294
|
+
@hartl-services/medusa-tax-compliance \
|
|
2295
|
+
@hartl-services/medusa-free-shipping \
|
|
2296
|
+
@hartl-services/medusa-batch-management \
|
|
2297
|
+
@hartl-services/medusa-affiliate \
|
|
2298
|
+
@hartl-services/medusa-payload-sync \
|
|
2299
|
+
@medusajs/event-bus-redis @medusajs/payment-stripe \
|
|
2300
|
+
@sentry/node
|
|
2301
|
+
```
|
|
2302
|
+
|
|
2303
|
+
### Usage
|
|
2304
|
+
|
|
2305
|
+
```ts
|
|
2306
|
+
// medusa-config.ts
|
|
2307
|
+
import { defineFoodShopConfig } from "@hartl-services/medusa-base/shop-config"
|
|
2308
|
+
|
|
2309
|
+
module.exports = defineFoodShopConfig({
|
|
2310
|
+
merchant: {
|
|
2311
|
+
countryCode: "at",
|
|
2312
|
+
legalName: "Example Shop GmbH",
|
|
2313
|
+
registeredAddress: {
|
|
2314
|
+
address1: "Musterstrasse 1",
|
|
2315
|
+
city: "Wien",
|
|
2316
|
+
countryCode: "at",
|
|
2317
|
+
postalCode: "1010",
|
|
2318
|
+
},
|
|
2319
|
+
vatNumber: "ATU12345678",
|
|
2320
|
+
},
|
|
2321
|
+
displayName: "EXAMPLE SHOP",
|
|
2322
|
+
orderNotificationEmail: "orders@example.com",
|
|
2323
|
+
timeZone: "Europe/Vienna",
|
|
2324
|
+
storefrontCountryCode: "de",
|
|
2325
|
+
})
|
|
2326
|
+
```
|
|
2327
|
+
|
|
2328
|
+
A shop that has to adjust the result (a test harness, for example) builds the
|
|
2329
|
+
plain input, changes it and calls `defineConfig` itself:
|
|
2330
|
+
|
|
2331
|
+
```ts
|
|
2332
|
+
import { defineConfig } from "@medusajs/framework/utils"
|
|
2333
|
+
import { buildFoodShopConfig } from "@hartl-services/medusa-base/shop-config"
|
|
2334
|
+
|
|
2335
|
+
const config = buildFoodShopConfig(options)
|
|
2336
|
+
config.plugins = config.plugins.filter(
|
|
2337
|
+
(plugin) => plugin.resolve !== "@hartl-services/medusa-batch-management",
|
|
2338
|
+
)
|
|
2339
|
+
module.exports = defineConfig(config)
|
|
2340
|
+
```
|
|
2341
|
+
|
|
2342
|
+
### API
|
|
2343
|
+
|
|
2344
|
+
- `defineFoodShopConfig(options, environment = process.env)` returns
|
|
2345
|
+
`defineConfig(buildFoodShopConfig(options, environment))`.
|
|
2346
|
+
- `buildFoodShopConfig(options, environment = process.env)` returns the plain
|
|
2347
|
+
config input (`admin`, `logger`, `projectConfig`, `modules`, `plugins`)
|
|
2348
|
+
before `defineConfig`. `modules` and `plugins` are ordinary arrays.
|
|
2349
|
+
- Types: `FoodShopConfigOptions`, `FoodShopConfigInput`, `FoodShopPlugin`,
|
|
2350
|
+
`FoodShopModule`.
|
|
2351
|
+
|
|
2352
|
+
Both functions validate the options first and throw a `TypeError` naming the
|
|
2353
|
+
option (`Food shop option "timeZone" must be ...`), so a misconfigured shop
|
|
2354
|
+
fails at boot. They then load the `.env` file (unless disabled) and read and
|
|
2355
|
+
validate the environment; a missing variable fails with the variable's name.
|
|
2356
|
+
|
|
2357
|
+
### Options
|
|
2358
|
+
|
|
2359
|
+
| Option | Meaning |
|
|
2360
|
+
| --- | --- |
|
|
2361
|
+
| `merchant` | `DocumentMerchantOption` (`countryCode`, `legalName`, `registeredAddress`, optional `vatNumber`). Fallback for the Shop-Stammdaten (Admin, "Hartl Services Plugins") that documents and tax compliance read per field; `legalName` is also the seeded store name. Country codes are 2-letter lowercase. |
|
|
2362
|
+
| `displayName` | Shop name in the default e-mail templates. |
|
|
2363
|
+
| `orderNotificationEmail` | Receives order notification e-mails; must look like an e-mail address. |
|
|
2364
|
+
| `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. |
|
|
2365
|
+
| `storefrontCountryCode` | 2-letter lowercase country code of the storefront route. Links: `<STOREFRONT_URL>/<cc>/partnerportal` (affiliate portal), `<STOREFRONT_URL>/<cc>/reset-password` (the e-mail plugin appends `#token=`), referral base `<STOREFRONT_URL>/<cc>`. The Admin invite link is `<MEDUSA_BACKEND_URL>/app/invite?token=`. |
|
|
2366
|
+
| `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. |
|
|
2367
|
+
| `loadEnvFile` | Default `true`: calls `loadEnv(NODE_ENV \|\| "development", process.cwd())`. Pass `false` when the environment is already complete (tests, isolated child processes). |
|
|
2368
|
+
|
|
2369
|
+
### Environment variables
|
|
2370
|
+
|
|
2371
|
+
Required at runtime (during `medusa build`, with `MEDUSA_BUILD_PHASE=true`, the
|
|
2372
|
+
plugins' build placeholders stand in for them):
|
|
2373
|
+
|
|
2374
|
+
- Medusa core: `DATABASE_URL`, `STORE_CORS`, `ADMIN_CORS`, `AUTH_CORS`,
|
|
2375
|
+
`JWT_SECRET`, `COOKIE_SECRET`, `MEDUSA_BACKEND_URL`, `STOREFRONT_URL` (plain
|
|
2376
|
+
HTTP origins are accepted only in development and test).
|
|
2377
|
+
- Redis: `REDIS_URL` (optional in development, where Medusa's local event bus
|
|
2378
|
+
is used; required elsewhere, `rediss://` with authentication in production
|
|
2379
|
+
unless `REDIS_ALLOW_INSECURE_PRIVATE_NETWORK=true`). Never used during a
|
|
2380
|
+
build.
|
|
2381
|
+
- SMTP: `SMTP_HOST`, `SMTP_PORT`, `SMTP_SECURE`, `SMTP_USER`, `SMTP_PASSWORD`,
|
|
2382
|
+
`SMTP_FROM`, `SMTP_REQUIRE_TLS` (outside development and test either
|
|
2383
|
+
`SMTP_SECURE` or `SMTP_REQUIRE_TLS` must be `true`).
|
|
2384
|
+
- Stripe: `STRIPE_API_KEY` (`sk_...`), `STRIPE_WEBHOOK_SECRET` (`whsec_...`).
|
|
2385
|
+
- S3: `S3_CACHE_STORAGE_BUCKET`, `S3_CACHE_STORAGE_REGION`.
|
|
2386
|
+
- Documents: `DOCUMENT_PDF_EXECUTABLE`.
|
|
2387
|
+
|
|
2388
|
+
Optional: `S3_CACHE_STORAGE_ENDPOINT`, `S3_CACHE_STORAGE_FORCE_PATH_STYLE`,
|
|
2389
|
+
`S3_CACHE_STORAGE_PREFIX`, `S3_CACHE_STORAGE_ACCESS_KEY_ID`,
|
|
2390
|
+
`S3_CACHE_STORAGE_SECRET_ACCESS_KEY`, `S3_CACHE_STORAGE_SESSION_TOKEN` (omit
|
|
2391
|
+
the keys to use the AWS credential chain), `S3_CACHE_STORAGE_CACHE_DIR`
|
|
2392
|
+
(default `./.medusa-s3-cache`), `S3_CACHE_STORAGE_TTL_MS`,
|
|
2393
|
+
`S3_CACHE_STORAGE_CLEANUP_INTERVAL_MS`,
|
|
2394
|
+
`S3_CACHE_STORAGE_EXPIRATION_STRATEGY`,
|
|
2395
|
+
`FILE_GATEWAY_PRIVATE_DOWNLOAD_DURATION_SECONDS`, and `SENTRY_DSN` /
|
|
2396
|
+
`SENTRY_ENVIRONMENT` for error tracking (see the base's
|
|
2397
|
+
`/instrumentation`).
|
|
2398
|
+
|
|
2399
|
+
### Fixed configuration
|
|
2400
|
+
|
|
2401
|
+
Not configurable on purpose: the plugin order (base first), the Redis event
|
|
2402
|
+
bus job policy (3 attempts, exponential backoff from 1 s), S3 cache defaults
|
|
2403
|
+
(15 min cleanup, 1 h TTL, absolute expiration), Stripe immediate capture with
|
|
2404
|
+
automatic payment methods, the 24 h session lifetime, Admin path `/app`,
|
|
2405
|
+
consignment reminders due on day 10, and the Documents module always being part of the base.
|