@hartl-services/medusa-base 2.2.1 → 2.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (27) hide show
  1. package/.medusa/server/src/admin/index.js +131 -48
  2. package/.medusa/server/src/admin/index.mjs +131 -48
  3. package/.medusa/server/src/api/admin/draft-orders/[id]/order-confirmation/route.js +23 -0
  4. package/.medusa/server/src/api/middlewares.js +16 -2
  5. package/.medusa/server/src/email/domain/business-keys.js +4 -2
  6. package/.medusa/server/src/email/plugin-features.js +3 -3
  7. package/.medusa/server/src/email/rendering/default-templates.js +37 -2
  8. package/.medusa/server/src/email/rendering/render-email.js +2 -1
  9. package/.medusa/server/src/email/rendering/resolve-shop-configuration.js +5 -1
  10. package/.medusa/server/src/email/types/email-kinds.js +2 -1
  11. package/.medusa/server/src/email/types/template-data.js +12 -1
  12. package/.medusa/server/src/modules/email/migrations/Migration20261001120000.js +16 -0
  13. package/.medusa/server/src/shop-config/build-food-shop-config.js +3 -1
  14. package/.medusa/server/src/subscribers/affiliate-application-submitted.js +26 -0
  15. package/.medusa/server/src/subscribers/order-zero-total-paid.js +19 -0
  16. package/.medusa/server/src/workflows/email/prepare-affiliate-application-email.js +46 -0
  17. package/.medusa/server/src/workflows/email/update-draft-order-confirmation.js +49 -0
  18. package/.medusa/server/src/workflows/hooks/draft-order-confirmation-default.js +39 -0
  19. package/.medusa/server/src/workflows/order/mark-zero-total-order-paid.js +45 -0
  20. package/README.md +93 -8
  21. package/dist/email/types/email-kinds.d.ts +2 -1
  22. package/dist/email/types/email-kinds.js +1 -0
  23. package/dist/email/types/shop-configuration.d.ts +8 -0
  24. package/dist/email/types/template-data.d.ts +18 -0
  25. package/dist/email/types/template-data.js +11 -0
  26. package/dist/shop-config/build-food-shop-config.js +2 -0
  27. package/package.json +2 -2
@@ -0,0 +1,39 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const utils_1 = require("@medusajs/framework/utils");
4
+ const workflows_sdk_1 = require("@medusajs/framework/workflows-sdk");
5
+ const core_flows_1 = require("@medusajs/medusa/core-flows");
6
+ /**
7
+ * A draft order starts without a customer order confirmation: drafts are
8
+ * Admin tasks (sample shipments, manual orders), and the Admin opts in per
9
+ * draft through the "Bestellbestätigung" widget. `createOrderWorkflow` is the
10
+ * workflow behind `POST /admin/draft-orders`; its `no_notification_order`
11
+ * flag is overridden here because the Admin dashboard never sends it.
12
+ */
13
+ core_flows_1.createOrderWorkflow.hooks.orderCreated(async ({ order }, { container }) => {
14
+ const { data } = await container
15
+ .resolve(utils_1.ContainerRegistrationKeys.QUERY)
16
+ .graph({
17
+ entity: "order",
18
+ fields: ["id", "is_draft_order", "no_notification"],
19
+ filters: { id: order.id },
20
+ });
21
+ const created = data[0];
22
+ if (!created?.is_draft_order || created.no_notification === true) {
23
+ return;
24
+ }
25
+ await container
26
+ .resolve(utils_1.Modules.ORDER)
27
+ .updateOrders(created.id, { no_notification: true });
28
+ return new workflows_sdk_1.StepResponse(undefined, {
29
+ id: created.id,
30
+ no_notification: created.no_notification ?? false,
31
+ });
32
+ }, async (previous, { container }) => {
33
+ if (!previous)
34
+ return;
35
+ await container
36
+ .resolve(utils_1.Modules.ORDER)
37
+ .updateOrders(previous.id, { no_notification: previous.no_notification });
38
+ });
39
+ //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiZHJhZnQtb3JkZXItY29uZmlybWF0aW9uLWRlZmF1bHQuanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi8uLi8uLi8uLi8uLi9zcmMvd29ya2Zsb3dzL2hvb2tzL2RyYWZ0LW9yZGVyLWNvbmZpcm1hdGlvbi1kZWZhdWx0LnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiI7O0FBQ0EscURBR2tDO0FBQ2xDLHFFQUFnRTtBQUNoRSw0REFBaUU7QUFhakU7Ozs7OztHQU1HO0FBQ0gsZ0NBQW1CLENBQUMsS0FBSyxDQUFDLFlBQVksQ0FDcEMsS0FBSyxFQUFFLEVBQUUsS0FBSyxFQUFFLEVBQUUsRUFBRSxTQUFTLEVBQUUsRUFBRSxFQUFFO0lBQ2pDLE1BQU0sRUFBRSxJQUFJLEVBQUUsR0FBRyxNQUFNLFNBQVM7U0FDN0IsT0FBTyxDQUFDLGlDQUF5QixDQUFDLEtBQUssQ0FBQztTQUN4QyxLQUFLLENBQUM7UUFDTCxNQUFNLEVBQUUsT0FBTztRQUNmLE1BQU0sRUFBRSxDQUFDLElBQUksRUFBRSxnQkFBZ0IsRUFBRSxpQkFBaUIsQ0FBQztRQUNuRCxPQUFPLEVBQUUsRUFBRSxFQUFFLEVBQUUsS0FBSyxDQUFDLEVBQUUsRUFBRTtLQUMxQixDQUFDLENBQUE7SUFDSixNQUFNLE9BQU8sR0FBRyxJQUFJLENBQUMsQ0FBQyxDQUE2QixDQUFBO0lBQ25ELElBQUksQ0FBQyxPQUFPLEVBQUUsY0FBYyxJQUFJLE9BQU8sQ0FBQyxlQUFlLEtBQUssSUFBSSxFQUFFLENBQUM7UUFDakUsT0FBTTtJQUNSLENBQUM7SUFFRCxNQUFNLFNBQVM7U0FDWixPQUFPLENBQXNCLGVBQU8sQ0FBQyxLQUFLLENBQUM7U0FDM0MsWUFBWSxDQUFDLE9BQU8sQ0FBQyxFQUFFLEVBQUUsRUFBRSxlQUFlLEVBQUUsSUFBSSxFQUFFLENBQUMsQ0FBQTtJQUN0RCxPQUFPLElBQUksNEJBQVksQ0FBa0MsU0FBUyxFQUFFO1FBQ2xFLEVBQUUsRUFBRSxPQUFPLENBQUMsRUFBRTtRQUNkLGVBQWUsRUFBRSxPQUFPLENBQUMsZUFBZSxJQUFJLEtBQUs7S0FDbEQsQ0FBQyxDQUFBO0FBQ0osQ0FBQyxFQUNELEtBQUssRUFBRSxRQUEwQyxFQUFFLEVBQUUsU0FBUyxFQUFFLEVBQUUsRUFBRTtJQUNsRSxJQUFJLENBQUMsUUFBUTtRQUFFLE9BQU07SUFDckIsTUFBTSxTQUFTO1NBQ1osT0FBTyxDQUFzQixlQUFPLENBQUMsS0FBSyxDQUFDO1NBQzNDLFlBQVksQ0FBQyxRQUFRLENBQUMsRUFBRSxFQUFFLEVBQUUsZUFBZSxFQUFFLFFBQVEsQ0FBQyxlQUFlLEVBQUUsQ0FBQyxDQUFBO0FBQzdFLENBQUMsQ0FDRixDQUFBIn0=
@@ -0,0 +1,45 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.markZeroTotalOrderPaidWorkflow = void 0;
4
+ const utils_1 = require("@medusajs/framework/utils");
5
+ const workflows_sdk_1 = require("@medusajs/framework/workflows-sdk");
6
+ const core_flows_1 = require("@medusajs/medusa/core-flows");
7
+ /**
8
+ * An order that costs nothing and has no payment collection (an Admin draft,
9
+ * e.g. a free sample shipment) would stay "Nicht bezahlt" forever: Medusa
10
+ * derives the payment status from payment collections only. A completed
11
+ * 0 € collection makes it "Bezahlt" in the Admin and in database queries,
12
+ * without a payment provider. Orders that already carry a collection (every
13
+ * checkout order) are left alone, so the workflow is safe to run twice.
14
+ */
15
+ exports.markZeroTotalOrderPaidWorkflow = (0, workflows_sdk_1.createWorkflow)("order-mark-zero-total-paid", (input) => {
16
+ const lockKey = (0, workflows_sdk_1.transform)(input, ({ order_id }) => `order-zero-total-paid:${order_id}`);
17
+ (0, core_flows_1.acquireLockStep)({ key: lockKey, timeout: 10, ttl: 60 });
18
+ const { data: orders } = (0, core_flows_1.useQueryGraphStep)({
19
+ entity: "order",
20
+ fields: ["id", "total", "payment_collections.id"],
21
+ filters: { id: input.order_id },
22
+ });
23
+ const needsPayment = (0, workflows_sdk_1.transform)(orders, (rows) => {
24
+ const order = rows[0];
25
+ return (!!order &&
26
+ utils_1.MathBN.eq(utils_1.MathBN.convert(order.total), 0) &&
27
+ !(order.payment_collections ?? []).some(Boolean));
28
+ });
29
+ const created = (0, workflows_sdk_1.when)("order-zero-total-needs-payment", needsPayment, (needed) => needed).then(() => {
30
+ const collections = core_flows_1.createOrderPaymentCollectionWorkflow.runAsStep({
31
+ input: { order_id: input.order_id, amount: 0 },
32
+ });
33
+ const completion = (0, workflows_sdk_1.transform)(collections, (rows) => ({
34
+ selector: { id: rows[0].id },
35
+ update: { status: utils_1.PaymentCollectionStatus.COMPLETED },
36
+ }));
37
+ (0, core_flows_1.updatePaymentCollectionStep)(completion);
38
+ return collections;
39
+ });
40
+ (0, core_flows_1.releaseLockStep)({ key: lockKey });
41
+ return new workflows_sdk_1.WorkflowResponse((0, workflows_sdk_1.transform)({ created }, ({ created }) => ({
42
+ marked_paid: !!created?.length,
43
+ })));
44
+ });
45
+ //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoibWFyay16ZXJvLXRvdGFsLW9yZGVyLXBhaWQuanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi8uLi8uLi8uLi8uLi9zcmMvd29ya2Zsb3dzL29yZGVyL21hcmstemVyby10b3RhbC1vcmRlci1wYWlkLnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiI7OztBQUFBLHFEQUdrQztBQUNsQyxxRUFLMEM7QUFDMUMsNERBTW9DO0FBV3BDOzs7Ozs7O0dBT0c7QUFDVSxRQUFBLDhCQUE4QixHQUFHLElBQUEsOEJBQWMsRUFDMUQsNEJBQTRCLEVBQzVCLENBQUMsS0FBa0MsRUFBRSxFQUFFO0lBQ3JDLE1BQU0sT0FBTyxHQUFHLElBQUEseUJBQVMsRUFDdkIsS0FBSyxFQUNMLENBQUMsRUFBRSxRQUFRLEVBQUUsRUFBRSxFQUFFLENBQUMseUJBQXlCLFFBQVEsRUFBRSxDQUN0RCxDQUFBO0lBQ0QsSUFBQSw0QkFBZSxFQUFDLEVBQUUsR0FBRyxFQUFFLE9BQU8sRUFBRSxPQUFPLEVBQUUsRUFBRSxFQUFFLEdBQUcsRUFBRSxFQUFFLEVBQUUsQ0FBQyxDQUFBO0lBRXZELE1BQU0sRUFBRSxJQUFJLEVBQUUsTUFBTSxFQUFFLEdBQUcsSUFBQSw4QkFBaUIsRUFBQztRQUN6QyxNQUFNLEVBQUUsT0FBTztRQUNmLE1BQU0sRUFBRSxDQUFDLElBQUksRUFBRSxPQUFPLEVBQUUsd0JBQXdCLENBQUM7UUFDakQsT0FBTyxFQUFFLEVBQUUsRUFBRSxFQUFFLEtBQUssQ0FBQyxRQUFRLEVBQUU7S0FDaEMsQ0FBQyxDQUFBO0lBQ0YsTUFBTSxZQUFZLEdBQUcsSUFBQSx5QkFBUyxFQUFDLE1BQU0sRUFBRSxDQUFDLElBQUksRUFBVyxFQUFFO1FBQ3ZELE1BQU0sS0FBSyxHQUFJLElBQTRCLENBQUMsQ0FBQyxDQUFDLENBQUE7UUFDOUMsT0FBTyxDQUNMLENBQUMsQ0FBQyxLQUFLO1lBQ1AsY0FBTSxDQUFDLEVBQUUsQ0FBQyxjQUFNLENBQUMsT0FBTyxDQUFDLEtBQUssQ0FBQyxLQUF3QixDQUFDLEVBQUUsQ0FBQyxDQUFDO1lBQzVELENBQUMsQ0FBQyxLQUFLLENBQUMsbUJBQW1CLElBQUksRUFBRSxDQUFDLENBQUMsSUFBSSxDQUFDLE9BQU8sQ0FBQyxDQUNqRCxDQUFBO0lBQ0gsQ0FBQyxDQUFDLENBQUE7SUFFRixNQUFNLE9BQU8sR0FBRyxJQUFBLG9CQUFJLEVBQ2xCLGdDQUFnQyxFQUNoQyxZQUFZLEVBQ1osQ0FBQyxNQUFNLEVBQUUsRUFBRSxDQUFDLE1BQU0sQ0FDbkIsQ0FBQyxJQUFJLENBQUMsR0FBRyxFQUFFO1FBQ1YsTUFBTSxXQUFXLEdBQUcsaURBQW9DLENBQUMsU0FBUyxDQUFDO1lBQ2pFLEtBQUssRUFBRSxFQUFFLFFBQVEsRUFBRSxLQUFLLENBQUMsUUFBUSxFQUFFLE1BQU0sRUFBRSxDQUFDLEVBQUU7U0FDL0MsQ0FBQyxDQUFBO1FBQ0YsTUFBTSxVQUFVLEdBQUcsSUFBQSx5QkFBUyxFQUFDLFdBQVcsRUFBRSxDQUFDLElBQUksRUFBRSxFQUFFLENBQUMsQ0FBQztZQUNuRCxRQUFRLEVBQUUsRUFBRSxFQUFFLEVBQUUsSUFBSSxDQUFDLENBQUMsQ0FBQyxDQUFDLEVBQUUsRUFBRTtZQUM1QixNQUFNLEVBQUUsRUFBRSxNQUFNLEVBQUUsK0JBQXVCLENBQUMsU0FBUyxFQUFFO1NBQ3RELENBQUMsQ0FBQyxDQUFBO1FBQ0gsSUFBQSx3Q0FBMkIsRUFBQyxVQUFVLENBQUMsQ0FBQTtRQUN2QyxPQUFPLFdBQVcsQ0FBQTtJQUNwQixDQUFDLENBQUMsQ0FBQTtJQUVGLElBQUEsNEJBQWUsRUFBQyxFQUFFLEdBQUcsRUFBRSxPQUFPLEVBQUUsQ0FBQyxDQUFBO0lBQ2pDLE9BQU8sSUFBSSxnQ0FBZ0IsQ0FDekIsSUFBQSx5QkFBUyxFQUNQLEVBQUUsT0FBTyxFQUFFLEVBQ1gsQ0FBQyxFQUFFLE9BQU8sRUFBRSxFQUFnQyxFQUFFLENBQUMsQ0FBQztRQUM5QyxXQUFXLEVBQUUsQ0FBQyxDQUFDLE9BQU8sRUFBRSxNQUFNO0tBQy9CLENBQUMsQ0FDSCxDQUNGLENBQUE7QUFDSCxDQUFDLENBQ0YsQ0FBQSJ9
package/README.md CHANGED
@@ -41,6 +41,9 @@ packing lists) and S3 file storage, and the turnkey `medusa-config.ts` preset
41
41
  repair").
42
42
  - Requires passwords of at least 15 characters for customers and Admin users
43
43
  (see "Password policy").
44
+ - Starts Admin draft orders without a customer order confirmation and marks
45
+ placed 0 € orders without payment as paid (see "Draft orders and 0 €
46
+ orders").
44
47
  - Exports the shop's `medusa-config.ts` helpers from
45
48
  `@hartl-services/medusa-base/config` and Sentry's `register()` from
46
49
  `@hartl-services/medusa-base/instrumentation` (see "Config helpers" and
@@ -739,6 +742,36 @@ links itself:
739
742
  Nothing to configure; `medusa db:migrate` after installing or upgrading to
740
743
  0.4.1 is enough.
741
744
 
745
+ ## Draft orders and 0 € orders
746
+
747
+ Draft orders (Medusa's `@medusajs/draft-order` plugin, loaded by default) are
748
+ the shop's way to enter an order in the Admin: sample shipments, orders taken
749
+ by phone, replacements. Item prices and the shipping amount are set directly
750
+ in the draft; converting it never involves a payment provider.
751
+
752
+ - **Customer order confirmation off by default.** The
753
+ `createOrderWorkflow.hooks.orderCreated` hook
754
+ (`workflows/hooks/draft-order-confirmation-default`) sets Medusa's
755
+ `no_notification` on every new draft, also when an API caller sent
756
+ `no_notification_order: false`. The Admin switches it per draft in the
757
+ **Bestellbestätigung** widget on the draft's detail page
758
+ (`GET`/`POST /admin/draft-orders/:id/order-confirmation`, see the E-Mail
759
+ "Admin API"). On conversion the `order-placed` subscriber records the
760
+ customer confirmation as `suppressed` (`notifications_disabled`); the copy
761
+ to the shop's order address is still sent. The shipment mail follows the
762
+ Admin's "notify customer" choice per shipment, not this switch.
763
+ - **0 € orders count as paid.** Medusa derives an order's payment status from
764
+ its payment collections only, so a converted 0 € draft would stay
765
+ "Nicht bezahlt". The subscriber `order-zero-total-paid` runs
766
+ `order-mark-zero-total-paid` on `order.placed`: an order with a total of 0
767
+ and no payment collection gets a completed 0 € payment collection and reads
768
+ "Bezahlt" (`payment_status: captured`). Orders that already carry a
769
+ collection, i.e. every checkout order, are left alone. In SQL, a paid
770
+ order is one whose linked `payment_collection` (via
771
+ `order_payment_collection`) has `status = 'completed'`.
772
+
773
+ Orders placed before installing this version keep their state.
774
+
742
775
  ## Config helpers
743
776
 
744
777
  `@hartl-services/medusa-base/config` holds the generic environment helpers a
@@ -864,6 +897,10 @@ Options of the `email` namespace:
864
897
  `orderNotificationEmail` mailbox. `verifyAccountUrl` (optional builder,
865
898
  like `passwordResetUrl`; the mail appends `#token=`) is the storefront page
866
899
  that confirms an account, needed while `customer_verification` is on.
900
+ `adminAffiliateApplicationsUrl` (optional builder, receives
901
+ `{ applicationId }`) is the Admin page of the partner applications; the
902
+ notification about a new application shows its "Im Admin prüfen" button
903
+ only with it.
867
904
  `layout` and `renderers` are optional overrides, see "Shop configuration and
868
905
  default templates".
869
906
 
@@ -878,7 +915,7 @@ is off. The former `documentsEnabled` option no longer exists: Documents is
878
915
  always part of the base.
879
916
 
880
917
  The event contracts the module consumes for shipment, cancellation, refund and
881
- affiliate-reminder e-mails come from
918
+ affiliate-reminder and partner-application e-mails come from
882
919
  `@hartl-services/medusa-food-supplements-contracts/events/documents` and
883
920
  `/events/affiliate`, so installing the base does not pull in Affiliate.
884
921
 
@@ -913,10 +950,10 @@ feature is switched on again.
913
950
  | Key | Label | Default | Gates | Keeps running when off |
914
951
  | --------------------- | ---------------------- | ------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -------------------------------------------------------- |
915
952
  | (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 |
916
- | `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 |
953
+ | `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`; route `/admin/draft-orders/:id/order-confirmation` (404) and its draft-order widget | jobs, Admin routes, already created messages |
917
954
  | `account_mails` | Konto-Mails | on | subscribers `password-reset`, `user-invite`, `customer-verification` (the last one only with `customer_verification` on as well) | jobs, Admin routes, already created messages |
918
955
  | `customer_verification` | Kontobestätigung per E-Mail | off | Medusa's e-mail verification of customer logins (see "Kontobestätigung per E-Mail"), route `POST /store/customer-verification/resend` (404 while off), subscriber `customer-verification`; child of `account_mails` | checkout and orders; confirmed accounts stay confirmed, already created messages |
919
- | `affiliate_reminders` | Affiliate-Erinnerungen | on | subscriber `affiliate-consignment-reminder-due` | jobs, Admin routes, already created messages |
956
+ | `affiliate_reminders` | Affiliate-Mails | on | subscribers `affiliate-consignment-reminder-due` (reminders to affiliates) and `affiliate-application-submitted` (notification to the shop about a new partner application) | jobs, Admin routes, already created messages |
920
957
 
921
958
  With the plugin switched off the three jobs return immediately, so nothing
922
959
  is sent, retried or reconciled until it is switched on again; then the jobs
@@ -1043,7 +1080,8 @@ Storefront contract (DTOs, schema and path from the Contracts package,
1043
1080
  ### Ownership
1044
1081
 
1045
1082
  - Documents owns document issuance and publishes immutable document facts.
1046
- - Affiliate owns reminder eligibility and publishes reminder facts.
1083
+ - Affiliate owns reminder eligibility and publishes reminder and application
1084
+ facts.
1047
1085
  - Email resolves recipients, renders content, attaches files, and owns delivery
1048
1086
  status plus the append-only global Terms PDF history.
1049
1087
  - Public HTTP DTOs, schemas, and paths remain in
@@ -1167,6 +1205,28 @@ type Payload = {
1167
1205
  };
1168
1206
  ```
1169
1207
 
1208
+ `affiliate.application_submitted.v1`
1209
+
1210
+ ```ts
1211
+ type Payload = {
1212
+ schema_version: 1;
1213
+ application_id: string;
1214
+ customer_id: string;
1215
+ customer_email: string | null;
1216
+ customer_first_name: string | null;
1217
+ customer_last_name: string | null;
1218
+ company_name: string | null;
1219
+ message: string | null;
1220
+ };
1221
+ ```
1222
+
1223
+ Affiliate publishes it when `POST /store/affiliates/register` has stored an
1224
+ application. Email creates one `affiliate.application_submitted.admin` message
1225
+ per `application_id` (business key
1226
+ `affiliate-application-submitted-admin:<application_id>`, so a replay or a
1227
+ concurrent request never sends a second mail) addressed to
1228
+ `orderNotificationEmail`. The persisted template data are exactly these facts.
1229
+
1170
1230
  `period_start` is a valid `YYYY-MM-01`, `due_date` is a valid `YYYY-MM-DD`, and
1171
1231
  `reminder_key` is exactly
1172
1232
  `affiliate-consignment:<affiliate_id>:<period_start>`. `locale`, when present,
@@ -1177,7 +1237,8 @@ has at most 35 characters. Email runtime-validates every contract before use.
1177
1237
  - The accepted kinds are exactly `order.received`, `order.received.admin`, `order.shipped`,
1178
1238
  `order.canceled`, `order.return_requested`, `order.return_received`,
1179
1239
  `order.refunded`, `auth.password_reset`, `auth.verification`,
1180
- `affiliate.consignment_report_due` and `user.invite`.
1240
+ `affiliate.consignment_report_due`, `affiliate.application_submitted.admin`
1241
+ and `user.invite`.
1181
1242
  - Every automatic message has a deterministic business key.
1182
1243
  - Every placed Order creates independent customer and Shop intents. The customer
1183
1244
  intent honors `no_notification`; the Shop intent always targets the static
@@ -1248,7 +1309,7 @@ an info box lists number, customer, e-mail and order time, followed by an
1248
1309
  button is left out without it) and `timeZone` (IANA; the order time is left
1249
1310
  out without it). `buildFoodShopConfig` sets both (`<MEDUSA_BACKEND_URL>/app/
1250
1311
  orders/<id>` and its `timeZone` option). The layout receives `audience`
1251
- (`"shop"` for this kind and `user.invite`, `"customer"` for all others) and
1312
+ (`"shop"` for this kind, `affiliate.application_submitted.admin` and `user.invite`, `"customer"` for all others) and
1252
1313
  shows the contact line only to customers.
1253
1314
 
1254
1315
  The shipping confirmation (`order.shipped`, one per fulfillment) receives a
@@ -1329,6 +1390,17 @@ Partnerbereich" button and says the mail can be ignored once the report is
1329
1390
  submitted. The button leads to a 404 while the affiliate plugin's
1330
1391
  `partner_portal` toggle is off.
1331
1392
 
1393
+ The partner application notification (`affiliate.application_submitted.admin`)
1394
+ goes to `orderNotificationEmail`, like the order notification, and is gated by
1395
+ the `affiliate_reminders` feature ("Affiliate-Mails"). Its subject is "Neue
1396
+ Partner-Bewerbung von <Firma>" (without a company the applicant's name, then
1397
+ the e-mail address; just "Neue Partner-Bewerbung" if none is known). An info
1398
+ box lists company, name and e-mail (only the present ones), the applicant's
1399
+ message follows as an escaped paragraph, and an "Im Admin prüfen" button links
1400
+ `adminAffiliateApplicationsUrl` (left out without it; `buildFoodShopConfig`
1401
+ sets `<MEDUSA_BACKEND_URL>/app/affiliates/applications`). Its layout audience
1402
+ is `"shop"`. All applicant text is HTML-escaped.
1403
+
1332
1404
  The Admin invite (`user.invite`) has the subject "Einladung ins Admin-Team von
1333
1405
  <displayName>", an "Einladung annehmen" button with the link as copyable text,
1334
1406
  the invite's current expiry ("Die Einladung ist gültig bis 02.10.2026, 01:40
@@ -1430,6 +1502,11 @@ Email exposes authenticated Admin operations at:
1430
1502
  most 5 MiB with its `effective_at` timestamp.
1431
1503
  - `GET /admin/email/terms-versions/:id/download` streams one authenticated,
1432
1504
  non-cacheable PDF download.
1505
+ - `GET /admin/draft-orders/:id/order-confirmation` returns
1506
+ `AdminDraftOrderConfirmationResponse` (`{ draft_order_id,
1507
+ send_order_confirmation }`); `POST` with
1508
+ `{ send_order_confirmation: boolean }` switches it. Both answer 404 for an
1509
+ order that is not (or no longer) a draft and while `order_mails` is off.
1433
1510
 
1434
1511
  List and detail responses expose only safe identifiers and linkage metadata,
1435
1512
  kind/status and safe status codes, resource references, trigger, attempt
@@ -1456,10 +1533,18 @@ composes the base's `PluginPageShell` and `PluginFeatureSections` from
1456
1533
  two operations sections below them, in this order: **AGB/PDF-Versionen** and,
1457
1534
  as the last section, the **E-Mail-Protokoll**. They show Terms PDF and mail
1458
1535
  log state to act on, not configuration the plugin reads at run time. The mail
1459
- log provides bounded server pagination, allowlisted status/kind filters, safe
1460
- message and attempt status, and an explicitly confirmed resend action.
1536
+ log provides bounded server pagination, allowlisted status/kind filters, a
1537
+ search by the exact resource ID (e.g. `order_…`, `cus_…`, as pasted from
1538
+ elsewhere), safe message and attempt status, and an explicitly confirmed
1539
+ resend action.
1461
1540
  Recipient, template, and payload data are intentionally absent from the page.
1462
1541
 
1542
+ The draft-order detail page carries the **Bestellbestätigung** widget (zone
1543
+ `draft_order.details.side.after`, hidden while `order_mails` is off). Its
1544
+ switch "An Kunden senden" starts off and decides whether the customer receives
1545
+ the order confirmation when the draft is converted (see "Draft orders and
1546
+ 0 € orders").
1547
+
1463
1548
  The `AGB/PDF-Versionen` section manages the global Terms PDFs. Admins can
1464
1549
  upload a PDF of at most 5 MiB with its effective date, review versions newest
1465
1550
  first, and download a selected version. Uploads receive a fresh request UUID;
@@ -1,5 +1,5 @@
1
1
  import { z } from "@medusajs/framework/zod";
2
- export declare const EMAIL_KINDS: readonly ["order.received", "order.received.admin", "order.shipped", "order.canceled", "order.return_requested", "order.return_received", "order.refunded", "auth.password_reset", "affiliate.consignment_report_due", "user.invite", "auth.verification"];
2
+ export declare const EMAIL_KINDS: readonly ["order.received", "order.received.admin", "order.shipped", "order.canceled", "order.return_requested", "order.return_received", "order.refunded", "auth.password_reset", "affiliate.consignment_report_due", "affiliate.application_submitted.admin", "user.invite", "auth.verification"];
3
3
  export declare const EmailKindSchema: z.ZodEnum<{
4
4
  "order.received": "order.received";
5
5
  "order.received.admin": "order.received.admin";
@@ -10,6 +10,7 @@ export declare const EmailKindSchema: z.ZodEnum<{
10
10
  "order.refunded": "order.refunded";
11
11
  "auth.password_reset": "auth.password_reset";
12
12
  "affiliate.consignment_report_due": "affiliate.consignment_report_due";
13
+ "affiliate.application_submitted.admin": "affiliate.application_submitted.admin";
13
14
  "user.invite": "user.invite";
14
15
  "auth.verification": "auth.verification";
15
16
  }>;
@@ -12,6 +12,7 @@ exports.EMAIL_KINDS = [
12
12
  "order.refunded",
13
13
  "auth.password_reset",
14
14
  "affiliate.consignment_report_due",
15
+ "affiliate.application_submitted.admin",
15
16
  "user.invite",
16
17
  "auth.verification",
17
18
  ];
@@ -39,6 +39,14 @@ export type ShopEmailConfiguration = Readonly<{
39
39
  adminOrderUrl?: (input: Readonly<{
40
40
  orderId: string;
41
41
  }>) => string;
42
+ /**
43
+ * Link to the partner applications in the Medusa Admin; the shop's
44
+ * notification about a new partner application shows an "Im Admin prüfen"
45
+ * button only when it is set.
46
+ */
47
+ adminAffiliateApplicationsUrl?: (input: Readonly<{
48
+ applicationId: string;
49
+ }>) => string;
42
50
  affiliatePortalUrl: string;
43
51
  displayName: string;
44
52
  inviteUrl: (input: Readonly<{
@@ -362,6 +362,15 @@ export declare const EmailPersistedDataSchemas: {
362
362
  due_date: z.ZodString;
363
363
  locale: z.ZodNullable<z.ZodString>;
364
364
  }, z.core.$strict>;
365
+ readonly "affiliate.application_submitted.admin": z.ZodObject<{
366
+ application_id: z.ZodString;
367
+ customer_id: z.ZodString;
368
+ customer_email: z.ZodNullable<z.ZodString>;
369
+ customer_first_name: z.ZodNullable<z.ZodString>;
370
+ customer_last_name: z.ZodNullable<z.ZodString>;
371
+ company_name: z.ZodNullable<z.ZodString>;
372
+ message: z.ZodNullable<z.ZodString>;
373
+ }, z.core.$strict>;
365
374
  readonly "user.invite": z.ZodObject<{}, z.core.$strict>;
366
375
  readonly "auth.verification": z.ZodObject<{
367
376
  locale: z.ZodNullable<z.ZodString>;
@@ -596,6 +605,15 @@ export declare const EmailRenderDataSchemas: {
596
605
  due_date: z.ZodString;
597
606
  locale: z.ZodNullable<z.ZodString>;
598
607
  }, z.core.$strict>;
608
+ readonly "affiliate.application_submitted.admin": z.ZodObject<{
609
+ application_id: z.ZodString;
610
+ customer_id: z.ZodString;
611
+ customer_email: z.ZodNullable<z.ZodString>;
612
+ customer_first_name: z.ZodNullable<z.ZodString>;
613
+ customer_last_name: z.ZodNullable<z.ZodString>;
614
+ company_name: z.ZodNullable<z.ZodString>;
615
+ message: z.ZodNullable<z.ZodString>;
616
+ }, z.core.$strict>;
599
617
  };
600
618
  export type EmailPersistedDataMap = {
601
619
  readonly [Kind in EmailKind]: Readonly<z.infer<(typeof EmailPersistedDataSchemas)[Kind]>>;
@@ -214,6 +214,17 @@ exports.EmailPersistedDataSchemas = {
214
214
  locale: NullableLocaleSchema,
215
215
  })
216
216
  .strict(),
217
+ "affiliate.application_submitted.admin": zod_1.z
218
+ .object({
219
+ application_id: NonEmptyStringSchema,
220
+ customer_id: NonEmptyStringSchema,
221
+ customer_email: NonEmptyStringSchema.nullable(),
222
+ customer_first_name: NonEmptyStringSchema.nullable(),
223
+ customer_last_name: NonEmptyStringSchema.nullable(),
224
+ company_name: NonEmptyStringSchema.nullable(),
225
+ message: NonEmptyStringSchema.nullable(),
226
+ })
227
+ .strict(),
217
228
  "user.invite": zod_1.z.object({}).strict(),
218
229
  "auth.verification": zod_1.z
219
230
  .object({
@@ -144,6 +144,8 @@ function buildFoodShopConfig(options, environment = process.env) {
144
144
  email: {
145
145
  jwtSecret: env.JWT_SECRET,
146
146
  shop: {
147
+ // The Admin page lists the applications; the id is not part of the URL.
148
+ adminAffiliateApplicationsUrl: () => new URL("/app/affiliates/applications", backendOrigin).toString(),
147
149
  // The Admin dashboard mounts under admin.path ("/app").
148
150
  adminOrderUrl: ({ orderId }) => new URL(`/app/orders/${encodeURIComponent(orderId)}`, backendOrigin).toString(),
149
151
  displayName: options.displayName,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hartl-services/medusa-base",
3
- "version": "2.2.1",
3
+ "version": "2.4.0",
4
4
  "description": "Base plugin for Hartl Services Medusa shops: per-plugin feature toggles with a generic Admin settings page, a Store endpoint for storefronts, enforcement helpers, the complete-cart hook composition, the root redirect, cart-promotion link repair, the built-in E-Mail, Documents and S3 file storage modules, config helpers with the turnkey food-shop preset, and the Sentry logger wrapper and instrumentation.",
5
5
  "license": "MIT",
6
6
  "files": [
@@ -88,7 +88,7 @@
88
88
  "mime-types": "3.0.1",
89
89
  "nodemailer": "6.10.1",
90
90
  "ulid": "^2.4.0",
91
- "@hartl-services/medusa-food-supplements-contracts": "^0.20.0"
91
+ "@hartl-services/medusa-food-supplements-contracts": "^0.24.0"
92
92
  },
93
93
  "devDependencies": {
94
94
  "@medusajs/admin-sdk": "2.19.0",