@webbers/pay-payments-medusa 2.2.51 → 2.2.53

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 (45) hide show
  1. package/.medusa/server/package.json +1 -1
  2. package/.medusa/server/src/admin/index.js +233 -11
  3. package/.medusa/server/src/admin/index.mjs +234 -12
  4. package/.medusa/server/src/api/admin/pay/orders/[id]/direct-debit/exchange/route.d.ts +12 -0
  5. package/.medusa/server/src/api/admin/pay/orders/[id]/direct-debit/exchange/route.d.ts.map +1 -0
  6. package/.medusa/server/src/api/admin/pay/orders/[id]/direct-debit/exchange/route.js +42 -0
  7. package/.medusa/server/src/api/admin/pay/orders/[id]/direct-debit/route.d.ts +9 -0
  8. package/.medusa/server/src/api/admin/pay/orders/[id]/direct-debit/route.d.ts.map +1 -0
  9. package/.medusa/server/src/api/admin/pay/orders/[id]/direct-debit/route.js +19 -0
  10. package/.medusa/server/src/api/admin/pay/orders/[id]/reversal/route.d.ts +7 -0
  11. package/.medusa/server/src/api/admin/pay/orders/[id]/reversal/route.d.ts.map +1 -0
  12. package/.medusa/server/src/api/admin/pay/orders/[id]/reversal/route.js +17 -0
  13. package/.medusa/server/src/api/admin/pay/settings/route.d.ts +12 -0
  14. package/.medusa/server/src/api/admin/pay/settings/route.d.ts.map +1 -0
  15. package/.medusa/server/src/api/admin/pay/settings/route.js +24 -0
  16. package/.medusa/server/src/providers/pay/core/pay-base.d.ts +6 -0
  17. package/.medusa/server/src/providers/pay/core/pay-base.d.ts.map +1 -1
  18. package/.medusa/server/src/providers/pay/core/pay-base.js +24 -1
  19. package/.medusa/server/src/providers/pay/services/pay-direct-debit.d.ts +6 -1
  20. package/.medusa/server/src/providers/pay/services/pay-direct-debit.d.ts.map +1 -1
  21. package/.medusa/server/src/providers/pay/services/pay-direct-debit.js +34 -15
  22. package/.medusa/server/src/providers/pay/types/index.d.ts +6 -5
  23. package/.medusa/server/src/providers/pay/types/index.d.ts.map +1 -1
  24. package/.medusa/server/src/providers/pay/types/index.js +1 -1
  25. package/.medusa/server/src/subscribers/direct-debit-exchange.d.ts.map +1 -1
  26. package/.medusa/server/src/subscribers/direct-debit-exchange.js +96 -24
  27. package/.medusa/server/src/subscribers/payment-failed.d.ts.map +1 -1
  28. package/.medusa/server/src/subscribers/payment-failed.js +24 -4
  29. package/.medusa/server/src/utils/directDebitExchange.d.ts +15 -0
  30. package/.medusa/server/src/utils/directDebitExchange.d.ts.map +1 -1
  31. package/.medusa/server/src/utils/directDebitExchange.js +59 -2
  32. package/.medusa/server/src/utils/directDebitSimulation.d.ts +93 -0
  33. package/.medusa/server/src/utils/directDebitSimulation.d.ts.map +1 -0
  34. package/.medusa/server/src/utils/directDebitSimulation.js +170 -0
  35. package/.medusa/server/src/utils/index.d.ts +2 -0
  36. package/.medusa/server/src/utils/index.d.ts.map +1 -1
  37. package/.medusa/server/src/utils/index.js +3 -1
  38. package/.medusa/server/src/utils/paymentReversal.d.ts +24 -0
  39. package/.medusa/server/src/utils/paymentReversal.d.ts.map +1 -0
  40. package/.medusa/server/src/utils/paymentReversal.js +80 -0
  41. package/.medusa/server/src/utils/reverseCapturedPayment.d.ts +43 -0
  42. package/.medusa/server/src/utils/reverseCapturedPayment.d.ts.map +1 -0
  43. package/.medusa/server/src/utils/reverseCapturedPayment.js +100 -0
  44. package/README.md +51 -0
  45. package/package.json +1 -1
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@webbers/pay-payments-medusa",
3
3
  "displayName": "Medusa v2 Pay. Plugin",
4
- "version": "2.2.51",
4
+ "version": "2.2.53",
5
5
  "description": "The Pay.nl payment provider plugin for Medusa v2",
6
6
  "author": "Webbers B.V. <development@webbers.com>",
7
7
  "license": "MIT",
@@ -1,13 +1,191 @@
1
1
  "use strict";
2
2
  const jsxRuntime = require("react/jsx-runtime");
3
- const ui = require("@medusajs/ui");
4
3
  const adminSdk = require("@medusajs/admin-sdk");
5
- const Medusa = require("@medusajs/js-sdk");
4
+ const ui = require("@medusajs/ui");
6
5
  const reactQuery = require("@tanstack/react-query");
6
+ const Medusa = require("@medusajs/js-sdk");
7
7
  const react = require("react");
8
8
  require("@medusajs/admin-shared");
9
9
  const _interopDefault = (e) => e && e.__esModule ? e : { default: e };
10
10
  const Medusa__default = /* @__PURE__ */ _interopDefault(Medusa);
11
+ const sdk = new Medusa__default.default({
12
+ baseUrl: "/",
13
+ debug: false,
14
+ auth: {
15
+ type: "session"
16
+ }
17
+ });
18
+ const PRESET_ORDER = [
19
+ "pending",
20
+ "sent",
21
+ "collected",
22
+ "storno",
23
+ "failed",
24
+ "declined"
25
+ ];
26
+ const statusColor = (status) => {
27
+ switch (status) {
28
+ case "completed":
29
+ case "authorized":
30
+ return "green";
31
+ case "failed":
32
+ case "canceled":
33
+ return "red";
34
+ case "awaiting":
35
+ return "orange";
36
+ default:
37
+ return "grey";
38
+ }
39
+ };
40
+ const queryKey = (orderId) => ["pay-direct-debit-simulation", orderId];
41
+ const PayDirectDebitSimulatorWidget = ({
42
+ data: order
43
+ }) => {
44
+ const queryClient = reactQuery.useQueryClient();
45
+ const { data: state, isLoading } = reactQuery.useQuery({
46
+ queryKey: queryKey(order.id),
47
+ queryFn: () => sdk.client.fetch(`/admin/pay/orders/${order.id}/direct-debit`)
48
+ });
49
+ const simulate = reactQuery.useMutation({
50
+ mutationFn: (action) => sdk.client.fetch(
51
+ `/admin/pay/orders/${order.id}/direct-debit/exchange`,
52
+ { method: "POST", body: { action } }
53
+ ),
54
+ onSuccess: async (_result, action) => {
55
+ var _a;
56
+ await Promise.all([
57
+ queryClient.invalidateQueries({ queryKey: queryKey(order.id) }),
58
+ // The dashboard's order detail, summary and payments
59
+ queryClient.invalidateQueries({ queryKey: ["orders"] }),
60
+ queryClient.invalidateQueries({ queryKey: ["payment_collections"] })
61
+ ]);
62
+ ui.toast.success(`Simulated "${((_a = state == null ? void 0 : state.presets) == null ? void 0 : _a[action].label) ?? action}"`);
63
+ },
64
+ onError: (error) => {
65
+ ui.toast.error(error.message);
66
+ }
67
+ });
68
+ if (isLoading || !(state == null ? void 0 : state.visible)) {
69
+ return null;
70
+ }
71
+ return /* @__PURE__ */ jsxRuntime.jsxs(ui.Container, { className: "divide-y p-0", children: [
72
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-start justify-between gap-x-4 px-6 py-4", children: [
73
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-col gap-y-1", children: [
74
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: "small", leading: "compact", weight: "plus", children: "Pay. direct debit simulator" }),
75
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: "small", leading: "compact", className: "text-ui-fg-subtle", children: "Test mode. This mandate only exists on this server, pick the exchange Pay. would send for it." })
76
+ ] }),
77
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Badge, { size: "2xsmall", color: statusColor(state.paymentCollectionStatus), children: state.paymentCollectionStatus ?? "unknown" })
78
+ ] }),
79
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-col gap-y-3 px-6 py-4", children: [
80
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center justify-between gap-x-4", children: [
81
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: "small", leading: "compact", className: "text-ui-fg-subtle", children: "Mandate" }),
82
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: "small", leading: "compact", className: "truncate font-mono", children: state.mandateCode })
83
+ ] }),
84
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center justify-between gap-x-4", children: [
85
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: "small", leading: "compact", className: "text-ui-fg-subtle", children: "Payment" }),
86
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: "small", leading: "compact", children: state.captured ? "Captured" : "Not captured" })
87
+ ] })
88
+ ] }),
89
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex flex-col gap-y-2 px-6 py-4", children: PRESET_ORDER.map((key) => {
90
+ var _a;
91
+ const preset = (_a = state.presets) == null ? void 0 : _a[key];
92
+ if (!preset) {
93
+ return null;
94
+ }
95
+ return /* @__PURE__ */ jsxRuntime.jsxs(
96
+ "div",
97
+ {
98
+ className: "flex items-center justify-between gap-x-4",
99
+ children: [
100
+ /* @__PURE__ */ jsxRuntime.jsx(
101
+ ui.Text,
102
+ {
103
+ size: "small",
104
+ leading: "compact",
105
+ className: "text-ui-fg-subtle",
106
+ children: preset.description
107
+ }
108
+ ),
109
+ /* @__PURE__ */ jsxRuntime.jsx(
110
+ ui.Button,
111
+ {
112
+ size: "small",
113
+ variant: "secondary",
114
+ className: "shrink-0",
115
+ disabled: simulate.isPending,
116
+ isLoading: simulate.isPending && simulate.variables === key,
117
+ onClick: () => simulate.mutate(key),
118
+ children: preset.label
119
+ }
120
+ )
121
+ ]
122
+ },
123
+ key
124
+ );
125
+ }) })
126
+ ] });
127
+ };
128
+ adminSdk.defineWidgetConfig({
129
+ zone: "order.details.side.after"
130
+ });
131
+ const KIND_LABELS = {
132
+ chargeback: "Chargeback",
133
+ storno: "Storno",
134
+ failure: "Failed collection"
135
+ };
136
+ const formatAmount = (amount, currency) => {
137
+ if (!currency) {
138
+ return amount.toFixed(2);
139
+ }
140
+ try {
141
+ return new Intl.NumberFormat(void 0, {
142
+ style: "currency",
143
+ currency: currency.toUpperCase()
144
+ }).format(amount);
145
+ } catch {
146
+ return `${amount.toFixed(2)} ${currency.toUpperCase()}`;
147
+ }
148
+ };
149
+ const formatDate = (iso) => iso ? new Intl.DateTimeFormat(void 0, {
150
+ dateStyle: "medium",
151
+ timeStyle: "short"
152
+ }).format(new Date(iso)) : null;
153
+ const PayPaymentReversedWidget = ({
154
+ data: order
155
+ }) => {
156
+ const { data: reversal } = reactQuery.useQuery({
157
+ queryKey: ["pay-payment-reversal", order.id],
158
+ queryFn: () => sdk.client.fetch(`/admin/pay/orders/${order.id}/reversal`)
159
+ });
160
+ if (!(reversal == null ? void 0 : reversal.reversed)) {
161
+ return null;
162
+ }
163
+ const kindLabel = reversal.kind ? KIND_LABELS[reversal.kind] : "Reversal";
164
+ const when = formatDate(reversal.at);
165
+ const returned = formatAmount(reversal.amount, reversal.currency_code);
166
+ const outstanding = formatAmount(reversal.outstanding, reversal.currency_code);
167
+ const stillOwed = reversal.outstanding > 0;
168
+ return /* @__PURE__ */ jsxRuntime.jsx(ui.Alert, { variant: "warning", className: "items-start", children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-col gap-y-2", children: [
169
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-wrap items-center gap-x-2 gap-y-1", children: [
170
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: "small", leading: "compact", weight: "plus", children: "Payment reversed, action required" }),
171
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Badge, { size: "2xsmall", color: "orange", children: kindLabel }),
172
+ reversal.statusCode !== null && /* @__PURE__ */ jsxRuntime.jsxs(ui.Badge, { size: "2xsmall", color: "grey", children: [
173
+ "Pay. status ",
174
+ reversal.statusCode
175
+ ] })
176
+ ] }),
177
+ /* @__PURE__ */ jsxRuntime.jsxs(ui.Text, { size: "small", leading: "compact", className: "text-ui-fg-subtle", children: [
178
+ reversal.kind === "chargeback" ? `The customer charged back the Pay. payment${when ? ` on ${when}` : ""}: ${returned} went back to them.` : reversal.kind === "storno" ? `The customer reversed the direct debit${when ? ` on ${when}` : ""}: ${returned} went back to them.` : `The bank did not collect the direct debit${when ? ` (${when})` : ""}: ${returned} was never received.`,
179
+ " ",
180
+ stillOwed ? `This order is not paid, ${outstanding} is still outstanding.${reversal.payable ? ' Use "Copy payment link" in the summary to let the customer pay again, or mark it as paid once the money arrived another way.' : ""}` : "The outstanding amount has since been settled.",
181
+ " ",
182
+ `The "Refunded" payment status is Medusa's wording for the reversal, no refund was sent by the shop.`
183
+ ] })
184
+ ] }) });
185
+ };
186
+ adminSdk.defineWidgetConfig({
187
+ zone: "order.details.before"
188
+ });
11
189
  const PayLogo = ({}) => {
12
190
  return /* @__PURE__ */ jsxRuntime.jsxs("svg", { width: "64", height: "40", viewBox: "0 0 64 40", fill: "none", xmlns: "http://www.w3.org/2000/svg", children: [
13
191
  /* @__PURE__ */ jsxRuntime.jsxs("g", { clipPath: "url(#clip0_596_748)", children: [
@@ -50,15 +228,8 @@ const PayLogo = ({}) => {
50
228
  /* @__PURE__ */ jsxRuntime.jsx("defs", { children: /* @__PURE__ */ jsxRuntime.jsx("clipPath", { id: "clip0_596_748", children: /* @__PURE__ */ jsxRuntime.jsx("rect", { width: "63.0137", height: "40", fill: "white" }) }) })
51
229
  ] });
52
230
  };
53
- const sdk = new Medusa__default.default({
54
- baseUrl: "/",
55
- debug: false,
56
- auth: {
57
- type: "session"
58
- }
59
- });
60
231
  const displayName = "Medusa v2 Pay. Plugin";
61
- const version = "2.2.51";
232
+ const version = "2.2.53";
62
233
  function getSortedPaymentMethods(data) {
63
234
  const { checkoutOptions, checkoutSequence } = data;
64
235
  const { primary } = checkoutSequence.default;
@@ -77,6 +248,7 @@ const formatter = new Intl.NumberFormat([], {
77
248
  minimumFractionDigits: 0,
78
249
  maximumFractionDigits: 2
79
250
  });
251
+ const SETTINGS_QUERY_KEY = ["pay-settings"];
80
252
  const columnHelper = ui.createDataTableColumnHelper();
81
253
  const columns = [
82
254
  columnHelper.accessor("id", {
@@ -97,6 +269,26 @@ const columns = [
97
269
  })
98
270
  ];
99
271
  const PaySettingPage = () => {
272
+ const queryClient = reactQuery.useQueryClient();
273
+ const { data: settings, isLoading: isLoadingSettings } = reactQuery.useQuery({
274
+ queryFn: () => sdk.client.fetch("/admin/pay/settings"),
275
+ queryKey: SETTINGS_QUERY_KEY
276
+ });
277
+ const updateSepaTesting = reactQuery.useMutation({
278
+ mutationFn: (sepaTesting) => sdk.client.fetch("/admin/pay/settings", {
279
+ method: "POST",
280
+ body: { sepaTesting }
281
+ }),
282
+ onSuccess: (updated) => {
283
+ queryClient.setQueryData(SETTINGS_QUERY_KEY, updated);
284
+ ui.toast.success("Success", {
285
+ description: updated.sepaTesting ? "SEPA testing enabled, the simulator is shown on direct debit orders" : "SEPA testing disabled"
286
+ });
287
+ },
288
+ onError: (error) => {
289
+ ui.toast.error("Error", { description: error.message });
290
+ }
291
+ });
100
292
  const { data: config2, refetch: refetchConfig } = reactQuery.useQuery({
101
293
  queryFn: () => sdk.client.fetch("/admin/pay/payment-methods"),
102
294
  queryKey: ["pay-payment-methods"]
@@ -170,6 +362,27 @@ const PaySettingPage = () => {
170
362
  children: "Clear Cache"
171
363
  }
172
364
  ) })
365
+ ] }),
366
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "text-ui-fg-subtle grid grid-cols-2 items-center px-6 py-4", children: [
367
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-col gap-y-1", children: [
368
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: "small", leading: "compact", weight: "plus", children: "SEPA testing" }),
369
+ /* @__PURE__ */ jsxRuntime.jsxs(ui.Text, { size: "small", leading: "compact", children: [
370
+ "Pay. cannot process direct debits in its test environment. When enabled, direct debit orders get a simulator that walks the payment through the exchanges Pay. would send (collected, storno, ...). Meant for staging servers only.",
371
+ settings && !settings.testMode ? " The provider is not in test mode, so the simulator stays hidden." : ""
372
+ ] })
373
+ ] }),
374
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-x-3", children: [
375
+ /* @__PURE__ */ jsxRuntime.jsx(
376
+ ui.Switch,
377
+ {
378
+ id: "pay-sepa-testing",
379
+ checked: (settings == null ? void 0 : settings.sepaTesting) ?? false,
380
+ disabled: isLoadingSettings || updateSepaTesting.isPending,
381
+ onCheckedChange: (checked) => updateSepaTesting.mutate(checked)
382
+ }
383
+ ),
384
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: "small", leading: "compact", children: (settings == null ? void 0 : settings.sepaTesting) ? "Enabled" : "Disabled" })
385
+ ] })
173
386
  ] })
174
387
  ] }),
175
388
  /* @__PURE__ */ jsxRuntime.jsx(ui.Container, { className: "divide-y p-0 overflow-hidden", children: /* @__PURE__ */ jsxRuntime.jsxs(ui.DataTable, { instance: table, children: [
@@ -197,7 +410,16 @@ const PaySettingPage = () => {
197
410
  const config = adminSdk.defineRouteConfig({
198
411
  label: "Pay"
199
412
  });
200
- const widgetModule = { widgets: [] };
413
+ const widgetModule = { widgets: [
414
+ {
415
+ Component: PayDirectDebitSimulatorWidget,
416
+ zone: ["order.details.side.after"]
417
+ },
418
+ {
419
+ Component: PayPaymentReversedWidget,
420
+ zone: ["order.details.before"]
421
+ }
422
+ ] };
201
423
  const routeModule = {
202
424
  routes: [
203
425
  {
@@ -1,10 +1,188 @@
1
1
  import { jsxs, jsx } from "react/jsx-runtime";
2
- import { createDataTableColumnHelper, useDataTable, Container, Heading, Text, Button, DataTable, toast } from "@medusajs/ui";
3
- import { defineRouteConfig } from "@medusajs/admin-sdk";
2
+ import { defineWidgetConfig, defineRouteConfig } from "@medusajs/admin-sdk";
3
+ import { toast, Container, Text, Badge, Button, Alert, createDataTableColumnHelper, useDataTable, Heading, Switch, DataTable } from "@medusajs/ui";
4
+ import { useQueryClient, useQuery, useMutation } from "@tanstack/react-query";
4
5
  import Medusa from "@medusajs/js-sdk";
5
- import { useQuery } from "@tanstack/react-query";
6
6
  import { useMemo } from "react";
7
7
  import "@medusajs/admin-shared";
8
+ const sdk = new Medusa({
9
+ baseUrl: "/",
10
+ debug: false,
11
+ auth: {
12
+ type: "session"
13
+ }
14
+ });
15
+ const PRESET_ORDER = [
16
+ "pending",
17
+ "sent",
18
+ "collected",
19
+ "storno",
20
+ "failed",
21
+ "declined"
22
+ ];
23
+ const statusColor = (status) => {
24
+ switch (status) {
25
+ case "completed":
26
+ case "authorized":
27
+ return "green";
28
+ case "failed":
29
+ case "canceled":
30
+ return "red";
31
+ case "awaiting":
32
+ return "orange";
33
+ default:
34
+ return "grey";
35
+ }
36
+ };
37
+ const queryKey = (orderId) => ["pay-direct-debit-simulation", orderId];
38
+ const PayDirectDebitSimulatorWidget = ({
39
+ data: order
40
+ }) => {
41
+ const queryClient = useQueryClient();
42
+ const { data: state, isLoading } = useQuery({
43
+ queryKey: queryKey(order.id),
44
+ queryFn: () => sdk.client.fetch(`/admin/pay/orders/${order.id}/direct-debit`)
45
+ });
46
+ const simulate = useMutation({
47
+ mutationFn: (action) => sdk.client.fetch(
48
+ `/admin/pay/orders/${order.id}/direct-debit/exchange`,
49
+ { method: "POST", body: { action } }
50
+ ),
51
+ onSuccess: async (_result, action) => {
52
+ var _a;
53
+ await Promise.all([
54
+ queryClient.invalidateQueries({ queryKey: queryKey(order.id) }),
55
+ // The dashboard's order detail, summary and payments
56
+ queryClient.invalidateQueries({ queryKey: ["orders"] }),
57
+ queryClient.invalidateQueries({ queryKey: ["payment_collections"] })
58
+ ]);
59
+ toast.success(`Simulated "${((_a = state == null ? void 0 : state.presets) == null ? void 0 : _a[action].label) ?? action}"`);
60
+ },
61
+ onError: (error) => {
62
+ toast.error(error.message);
63
+ }
64
+ });
65
+ if (isLoading || !(state == null ? void 0 : state.visible)) {
66
+ return null;
67
+ }
68
+ return /* @__PURE__ */ jsxs(Container, { className: "divide-y p-0", children: [
69
+ /* @__PURE__ */ jsxs("div", { className: "flex items-start justify-between gap-x-4 px-6 py-4", children: [
70
+ /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-y-1", children: [
71
+ /* @__PURE__ */ jsx(Text, { size: "small", leading: "compact", weight: "plus", children: "Pay. direct debit simulator" }),
72
+ /* @__PURE__ */ jsx(Text, { size: "small", leading: "compact", className: "text-ui-fg-subtle", children: "Test mode. This mandate only exists on this server, pick the exchange Pay. would send for it." })
73
+ ] }),
74
+ /* @__PURE__ */ jsx(Badge, { size: "2xsmall", color: statusColor(state.paymentCollectionStatus), children: state.paymentCollectionStatus ?? "unknown" })
75
+ ] }),
76
+ /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-y-3 px-6 py-4", children: [
77
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between gap-x-4", children: [
78
+ /* @__PURE__ */ jsx(Text, { size: "small", leading: "compact", className: "text-ui-fg-subtle", children: "Mandate" }),
79
+ /* @__PURE__ */ jsx(Text, { size: "small", leading: "compact", className: "truncate font-mono", children: state.mandateCode })
80
+ ] }),
81
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between gap-x-4", children: [
82
+ /* @__PURE__ */ jsx(Text, { size: "small", leading: "compact", className: "text-ui-fg-subtle", children: "Payment" }),
83
+ /* @__PURE__ */ jsx(Text, { size: "small", leading: "compact", children: state.captured ? "Captured" : "Not captured" })
84
+ ] })
85
+ ] }),
86
+ /* @__PURE__ */ jsx("div", { className: "flex flex-col gap-y-2 px-6 py-4", children: PRESET_ORDER.map((key) => {
87
+ var _a;
88
+ const preset = (_a = state.presets) == null ? void 0 : _a[key];
89
+ if (!preset) {
90
+ return null;
91
+ }
92
+ return /* @__PURE__ */ jsxs(
93
+ "div",
94
+ {
95
+ className: "flex items-center justify-between gap-x-4",
96
+ children: [
97
+ /* @__PURE__ */ jsx(
98
+ Text,
99
+ {
100
+ size: "small",
101
+ leading: "compact",
102
+ className: "text-ui-fg-subtle",
103
+ children: preset.description
104
+ }
105
+ ),
106
+ /* @__PURE__ */ jsx(
107
+ Button,
108
+ {
109
+ size: "small",
110
+ variant: "secondary",
111
+ className: "shrink-0",
112
+ disabled: simulate.isPending,
113
+ isLoading: simulate.isPending && simulate.variables === key,
114
+ onClick: () => simulate.mutate(key),
115
+ children: preset.label
116
+ }
117
+ )
118
+ ]
119
+ },
120
+ key
121
+ );
122
+ }) })
123
+ ] });
124
+ };
125
+ defineWidgetConfig({
126
+ zone: "order.details.side.after"
127
+ });
128
+ const KIND_LABELS = {
129
+ chargeback: "Chargeback",
130
+ storno: "Storno",
131
+ failure: "Failed collection"
132
+ };
133
+ const formatAmount = (amount, currency) => {
134
+ if (!currency) {
135
+ return amount.toFixed(2);
136
+ }
137
+ try {
138
+ return new Intl.NumberFormat(void 0, {
139
+ style: "currency",
140
+ currency: currency.toUpperCase()
141
+ }).format(amount);
142
+ } catch {
143
+ return `${amount.toFixed(2)} ${currency.toUpperCase()}`;
144
+ }
145
+ };
146
+ const formatDate = (iso) => iso ? new Intl.DateTimeFormat(void 0, {
147
+ dateStyle: "medium",
148
+ timeStyle: "short"
149
+ }).format(new Date(iso)) : null;
150
+ const PayPaymentReversedWidget = ({
151
+ data: order
152
+ }) => {
153
+ const { data: reversal } = useQuery({
154
+ queryKey: ["pay-payment-reversal", order.id],
155
+ queryFn: () => sdk.client.fetch(`/admin/pay/orders/${order.id}/reversal`)
156
+ });
157
+ if (!(reversal == null ? void 0 : reversal.reversed)) {
158
+ return null;
159
+ }
160
+ const kindLabel = reversal.kind ? KIND_LABELS[reversal.kind] : "Reversal";
161
+ const when = formatDate(reversal.at);
162
+ const returned = formatAmount(reversal.amount, reversal.currency_code);
163
+ const outstanding = formatAmount(reversal.outstanding, reversal.currency_code);
164
+ const stillOwed = reversal.outstanding > 0;
165
+ return /* @__PURE__ */ jsx(Alert, { variant: "warning", className: "items-start", children: /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-y-2", children: [
166
+ /* @__PURE__ */ jsxs("div", { className: "flex flex-wrap items-center gap-x-2 gap-y-1", children: [
167
+ /* @__PURE__ */ jsx(Text, { size: "small", leading: "compact", weight: "plus", children: "Payment reversed, action required" }),
168
+ /* @__PURE__ */ jsx(Badge, { size: "2xsmall", color: "orange", children: kindLabel }),
169
+ reversal.statusCode !== null && /* @__PURE__ */ jsxs(Badge, { size: "2xsmall", color: "grey", children: [
170
+ "Pay. status ",
171
+ reversal.statusCode
172
+ ] })
173
+ ] }),
174
+ /* @__PURE__ */ jsxs(Text, { size: "small", leading: "compact", className: "text-ui-fg-subtle", children: [
175
+ reversal.kind === "chargeback" ? `The customer charged back the Pay. payment${when ? ` on ${when}` : ""}: ${returned} went back to them.` : reversal.kind === "storno" ? `The customer reversed the direct debit${when ? ` on ${when}` : ""}: ${returned} went back to them.` : `The bank did not collect the direct debit${when ? ` (${when})` : ""}: ${returned} was never received.`,
176
+ " ",
177
+ stillOwed ? `This order is not paid, ${outstanding} is still outstanding.${reversal.payable ? ' Use "Copy payment link" in the summary to let the customer pay again, or mark it as paid once the money arrived another way.' : ""}` : "The outstanding amount has since been settled.",
178
+ " ",
179
+ `The "Refunded" payment status is Medusa's wording for the reversal, no refund was sent by the shop.`
180
+ ] })
181
+ ] }) });
182
+ };
183
+ defineWidgetConfig({
184
+ zone: "order.details.before"
185
+ });
8
186
  const PayLogo = ({}) => {
9
187
  return /* @__PURE__ */ jsxs("svg", { width: "64", height: "40", viewBox: "0 0 64 40", fill: "none", xmlns: "http://www.w3.org/2000/svg", children: [
10
188
  /* @__PURE__ */ jsxs("g", { clipPath: "url(#clip0_596_748)", children: [
@@ -47,15 +225,8 @@ const PayLogo = ({}) => {
47
225
  /* @__PURE__ */ jsx("defs", { children: /* @__PURE__ */ jsx("clipPath", { id: "clip0_596_748", children: /* @__PURE__ */ jsx("rect", { width: "63.0137", height: "40", fill: "white" }) }) })
48
226
  ] });
49
227
  };
50
- const sdk = new Medusa({
51
- baseUrl: "/",
52
- debug: false,
53
- auth: {
54
- type: "session"
55
- }
56
- });
57
228
  const displayName = "Medusa v2 Pay. Plugin";
58
- const version = "2.2.51";
229
+ const version = "2.2.53";
59
230
  function getSortedPaymentMethods(data) {
60
231
  const { checkoutOptions, checkoutSequence } = data;
61
232
  const { primary } = checkoutSequence.default;
@@ -74,6 +245,7 @@ const formatter = new Intl.NumberFormat([], {
74
245
  minimumFractionDigits: 0,
75
246
  maximumFractionDigits: 2
76
247
  });
248
+ const SETTINGS_QUERY_KEY = ["pay-settings"];
77
249
  const columnHelper = createDataTableColumnHelper();
78
250
  const columns = [
79
251
  columnHelper.accessor("id", {
@@ -94,6 +266,26 @@ const columns = [
94
266
  })
95
267
  ];
96
268
  const PaySettingPage = () => {
269
+ const queryClient = useQueryClient();
270
+ const { data: settings, isLoading: isLoadingSettings } = useQuery({
271
+ queryFn: () => sdk.client.fetch("/admin/pay/settings"),
272
+ queryKey: SETTINGS_QUERY_KEY
273
+ });
274
+ const updateSepaTesting = useMutation({
275
+ mutationFn: (sepaTesting) => sdk.client.fetch("/admin/pay/settings", {
276
+ method: "POST",
277
+ body: { sepaTesting }
278
+ }),
279
+ onSuccess: (updated) => {
280
+ queryClient.setQueryData(SETTINGS_QUERY_KEY, updated);
281
+ toast.success("Success", {
282
+ description: updated.sepaTesting ? "SEPA testing enabled, the simulator is shown on direct debit orders" : "SEPA testing disabled"
283
+ });
284
+ },
285
+ onError: (error) => {
286
+ toast.error("Error", { description: error.message });
287
+ }
288
+ });
97
289
  const { data: config2, refetch: refetchConfig } = useQuery({
98
290
  queryFn: () => sdk.client.fetch("/admin/pay/payment-methods"),
99
291
  queryKey: ["pay-payment-methods"]
@@ -167,6 +359,27 @@ const PaySettingPage = () => {
167
359
  children: "Clear Cache"
168
360
  }
169
361
  ) })
362
+ ] }),
363
+ /* @__PURE__ */ jsxs("div", { className: "text-ui-fg-subtle grid grid-cols-2 items-center px-6 py-4", children: [
364
+ /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-y-1", children: [
365
+ /* @__PURE__ */ jsx(Text, { size: "small", leading: "compact", weight: "plus", children: "SEPA testing" }),
366
+ /* @__PURE__ */ jsxs(Text, { size: "small", leading: "compact", children: [
367
+ "Pay. cannot process direct debits in its test environment. When enabled, direct debit orders get a simulator that walks the payment through the exchanges Pay. would send (collected, storno, ...). Meant for staging servers only.",
368
+ settings && !settings.testMode ? " The provider is not in test mode, so the simulator stays hidden." : ""
369
+ ] })
370
+ ] }),
371
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-x-3", children: [
372
+ /* @__PURE__ */ jsx(
373
+ Switch,
374
+ {
375
+ id: "pay-sepa-testing",
376
+ checked: (settings == null ? void 0 : settings.sepaTesting) ?? false,
377
+ disabled: isLoadingSettings || updateSepaTesting.isPending,
378
+ onCheckedChange: (checked) => updateSepaTesting.mutate(checked)
379
+ }
380
+ ),
381
+ /* @__PURE__ */ jsx(Text, { size: "small", leading: "compact", children: (settings == null ? void 0 : settings.sepaTesting) ? "Enabled" : "Disabled" })
382
+ ] })
170
383
  ] })
171
384
  ] }),
172
385
  /* @__PURE__ */ jsx(Container, { className: "divide-y p-0 overflow-hidden", children: /* @__PURE__ */ jsxs(DataTable, { instance: table, children: [
@@ -194,7 +407,16 @@ const PaySettingPage = () => {
194
407
  const config = defineRouteConfig({
195
408
  label: "Pay"
196
409
  });
197
- const widgetModule = { widgets: [] };
410
+ const widgetModule = { widgets: [
411
+ {
412
+ Component: PayDirectDebitSimulatorWidget,
413
+ zone: ["order.details.side.after"]
414
+ },
415
+ {
416
+ Component: PayPaymentReversedWidget,
417
+ zone: ["order.details.before"]
418
+ }
419
+ ] };
198
420
  const routeModule = {
199
421
  routes: [
200
422
  {
@@ -0,0 +1,12 @@
1
+ import { AuthenticatedMedusaRequest, MedusaResponse } from "@medusajs/framework/http";
2
+ /**
3
+ * Simulates the exchange Pay. would send for the order's test mode direct
4
+ * debit. Only allowed while "SEPA testing" is switched on in the Pay. settings
5
+ * (see getDirectDebitSimulationState). The exchange is processed synchronously
6
+ * through the same subscriber that handles real exchanges, so the response
7
+ * carries the resulting state.
8
+ */
9
+ export declare const POST: (req: AuthenticatedMedusaRequest<{
10
+ action?: unknown;
11
+ }>, res: MedusaResponse) => Promise<MedusaResponse>;
12
+ //# sourceMappingURL=route.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"route.d.ts","sourceRoot":"","sources":["../../../../../../../../../../src/api/admin/pay/orders/[id]/direct-debit/exchange/route.ts"],"names":[],"mappings":"AAAA,OAAO,EAAC,0BAA0B,EAAE,cAAc,EAAC,MAAM,0BAA0B,CAAA;AAUnF;;;;;;GAMG;AACH,eAAO,MAAM,IAAI,GACf,KAAK,0BAA0B,CAAC;IAAC,MAAM,CAAC,EAAE,OAAO,CAAA;CAAC,CAAC,EACnD,KAAK,cAAc,4BAoCpB,CAAA"}
@@ -0,0 +1,42 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.POST = void 0;
7
+ const utils_1 = require("@medusajs/framework/utils");
8
+ const directDebitSimulation_1 = require("../../../../../../../utils/directDebitSimulation");
9
+ const directDebitExchange_1 = require("../../../../../../../utils/directDebitExchange");
10
+ const direct_debit_exchange_1 = __importDefault(require("../../../../../../../subscribers/direct-debit-exchange"));
11
+ /**
12
+ * Simulates the exchange Pay. would send for the order's test mode direct
13
+ * debit. Only allowed while "SEPA testing" is switched on in the Pay. settings
14
+ * (see getDirectDebitSimulationState). The exchange is processed synchronously
15
+ * through the same subscriber that handles real exchanges, so the response
16
+ * carries the resulting state.
17
+ */
18
+ const POST = async (req, res) => {
19
+ const action = req.body?.action;
20
+ if (!(0, directDebitSimulation_1.isSimulatedExchangeKey)(action)) {
21
+ throw new utils_1.MedusaError(utils_1.MedusaError.Types.INVALID_DATA, `Unknown exchange "${String(action)}"`);
22
+ }
23
+ const state = await (0, directDebitSimulation_1.getDirectDebitSimulationState)(req.scope, req.params.id);
24
+ if (!state) {
25
+ throw new utils_1.MedusaError(utils_1.MedusaError.Types.NOT_FOUND, "Order not found");
26
+ }
27
+ if (!state.available) {
28
+ throw new utils_1.MedusaError(utils_1.MedusaError.Types.NOT_ALLOWED, `Direct debit exchanges cannot be simulated for this order (${state.reason})`);
29
+ }
30
+ await (0, direct_debit_exchange_1.default)({
31
+ event: {
32
+ name: directDebitExchange_1.PAY_DIRECT_DEBIT_EXCHANGE_EVENT,
33
+ data: { payload: (0, directDebitSimulation_1.buildSimulatedExchangePayload)(action, state) },
34
+ },
35
+ container: req.scope,
36
+ pluginOptions: {},
37
+ });
38
+ const updated = await (0, directDebitSimulation_1.getDirectDebitSimulationState)(req.scope, req.params.id);
39
+ return res.json({ visible: updated?.available ?? false, ...updated });
40
+ };
41
+ exports.POST = POST;
42
+ //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoicm91dGUuanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi8uLi8uLi8uLi8uLi8uLi8uLi8uLi8uLi8uLi9zcmMvYXBpL2FkbWluL3BheS9vcmRlcnMvW2lkXS9kaXJlY3QtZGViaXQvZXhjaGFuZ2Uvcm91dGUudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6Ijs7Ozs7O0FBQ0EscURBQXFEO0FBQ3JELDRGQUl5RDtBQUN6RCx3RkFBOEY7QUFDOUYsbUhBQWtHO0FBRWxHOzs7Ozs7R0FNRztBQUNJLE1BQU0sSUFBSSxHQUFHLEtBQUssRUFDdkIsR0FBbUQsRUFDbkQsR0FBbUIsRUFDbkIsRUFBRTtJQUNGLE1BQU0sTUFBTSxHQUFJLEdBQUcsQ0FBQyxJQUF1QyxFQUFFLE1BQU0sQ0FBQTtJQUVuRSxJQUFJLENBQUMsSUFBQSw4Q0FBc0IsRUFBQyxNQUFNLENBQUMsRUFBRSxDQUFDO1FBQ3BDLE1BQU0sSUFBSSxtQkFBVyxDQUNuQixtQkFBVyxDQUFDLEtBQUssQ0FBQyxZQUFZLEVBQzlCLHFCQUFxQixNQUFNLENBQUMsTUFBTSxDQUFDLEdBQUcsQ0FDdkMsQ0FBQTtJQUNILENBQUM7SUFFRCxNQUFNLEtBQUssR0FBRyxNQUFNLElBQUEscURBQTZCLEVBQUMsR0FBRyxDQUFDLEtBQUssRUFBRSxHQUFHLENBQUMsTUFBTSxDQUFDLEVBQUUsQ0FBQyxDQUFBO0lBRTNFLElBQUksQ0FBQyxLQUFLLEVBQUUsQ0FBQztRQUNYLE1BQU0sSUFBSSxtQkFBVyxDQUFDLG1CQUFXLENBQUMsS0FBSyxDQUFDLFNBQVMsRUFBRSxpQkFBaUIsQ0FBQyxDQUFBO0lBQ3ZFLENBQUM7SUFFRCxJQUFJLENBQUMsS0FBSyxDQUFDLFNBQVMsRUFBRSxDQUFDO1FBQ3JCLE1BQU0sSUFBSSxtQkFBVyxDQUNuQixtQkFBVyxDQUFDLEtBQUssQ0FBQyxXQUFXLEVBQzdCLDhEQUE4RCxLQUFLLENBQUMsTUFBTSxHQUFHLENBQzlFLENBQUE7SUFDSCxDQUFDO0lBRUQsTUFBTSxJQUFBLCtCQUE2QixFQUFDO1FBQ2xDLEtBQUssRUFBRTtZQUNMLElBQUksRUFBRSxxREFBK0I7WUFDckMsSUFBSSxFQUFFLEVBQUMsT0FBTyxFQUFFLElBQUEscURBQTZCLEVBQUMsTUFBTSxFQUFFLEtBQUssQ0FBQyxFQUFDO1NBQzlEO1FBQ0QsU0FBUyxFQUFFLEdBQUcsQ0FBQyxLQUFLO1FBQ3BCLGFBQWEsRUFBRSxFQUFFO0tBQ2xCLENBQUMsQ0FBQTtJQUVGLE1BQU0sT0FBTyxHQUFHLE1BQU0sSUFBQSxxREFBNkIsRUFBQyxHQUFHLENBQUMsS0FBSyxFQUFFLEdBQUcsQ0FBQyxNQUFNLENBQUMsRUFBRSxDQUFDLENBQUE7SUFFN0UsT0FBTyxHQUFHLENBQUMsSUFBSSxDQUFDLEVBQUMsT0FBTyxFQUFFLE9BQU8sRUFBRSxTQUFTLElBQUksS0FBSyxFQUFFLEdBQUcsT0FBTyxFQUFDLENBQUMsQ0FBQTtBQUNyRSxDQUFDLENBQUE7QUF0Q1ksUUFBQSxJQUFJLFFBc0NoQiJ9
@@ -0,0 +1,9 @@
1
+ import { AuthenticatedMedusaRequest, MedusaResponse } from "@medusajs/framework/http";
2
+ /**
3
+ * The direct debit simulation state of an order, for the order detail widget.
4
+ * `visible` is only true while "SEPA testing" is switched on in the Pay.
5
+ * settings, the server runs in test mode and the order holds a simulated
6
+ * mandate, so the widget stays hidden everywhere else.
7
+ */
8
+ export declare const GET: (req: AuthenticatedMedusaRequest, res: MedusaResponse) => Promise<MedusaResponse>;
9
+ //# sourceMappingURL=route.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"route.d.ts","sourceRoot":"","sources":["../../../../../../../../../src/api/admin/pay/orders/[id]/direct-debit/route.ts"],"names":[],"mappings":"AAAA,OAAO,EAAC,0BAA0B,EAAE,cAAc,EAAC,MAAM,0BAA0B,CAAA;AAGnF;;;;;GAKG;AACH,eAAO,MAAM,GAAG,GACd,KAAK,0BAA0B,EAC/B,KAAK,cAAc,4BASpB,CAAA"}