@legenki/print2medusa 0.1.0 → 0.2.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 +138 -1
- package/.medusa/server/src/admin/index.mjs +139 -2
- package/.medusa/server/src/api/admin/printful/webhook/route.js +78 -0
- package/.medusa/server/src/api/hooks/printful/[token]/route.js +80 -0
- package/.medusa/server/src/api/middlewares.js +60 -0
- package/.medusa/server/src/jobs/retry-webhook-events.js +65 -0
- package/.medusa/server/src/modules/printful/migrations/Migration20260731000000.js +62 -0
- package/.medusa/server/src/modules/printful/models/printful-webhook-event.js +18 -0
- package/.medusa/server/src/modules/printful/service.js +123 -3
- package/.medusa/server/src/providers/printful-fulfillment/service.js +1 -1
- package/.medusa/server/src/subscribers/payment-captured.js +1 -1
- package/.medusa/server/src/utils/mappers.js +67 -20
- package/.medusa/server/src/utils/order-state.js +58 -0
- package/.medusa/server/src/utils/printful-client.js +23 -1
- package/.medusa/server/src/utils/webhook-events.js +117 -0
- package/.medusa/server/src/utils/webhook-path.js +44 -0
- package/.medusa/server/src/workflows/apply-order-status.js +244 -0
- package/.medusa/server/src/workflows/create-printful-order.js +3 -2
- package/.medusa/server/src/workflows/index.js +5 -2
- package/.medusa/server/src/workflows/sync-products.js +1 -1
- package/README.md +124 -18
- package/package.json +6 -1
|
@@ -3,10 +3,65 @@ const jsxRuntime = require("react/jsx-runtime");
|
|
|
3
3
|
const adminSdk = require("@medusajs/admin-sdk");
|
|
4
4
|
const ui = require("@medusajs/ui");
|
|
5
5
|
const react = require("react");
|
|
6
|
+
const ATTENTION_STATES = /* @__PURE__ */ new Set(["failed", "canceled", "onhold"]);
|
|
7
|
+
const PrintfulOrderWidget = ({ data }) => {
|
|
8
|
+
const metadata = data.metadata ?? {};
|
|
9
|
+
const printfulOrderId = metadata.printful_order_id;
|
|
10
|
+
if (!printfulOrderId) {
|
|
11
|
+
return null;
|
|
12
|
+
}
|
|
13
|
+
const status = metadata.printful_status;
|
|
14
|
+
const statusUpdatedAt = metadata.printful_status_updated_at;
|
|
15
|
+
const shipments = metadata.printful_shipments ?? [];
|
|
16
|
+
const isAttention = status ? ATTENTION_STATES.has(status) : false;
|
|
17
|
+
return /* @__PURE__ */ jsxRuntime.jsxs(ui.Container, { className: "divide-y p-0", children: [
|
|
18
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center justify-between px-6 py-4", children: [
|
|
19
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Heading, { level: "h2", children: "Printful" }),
|
|
20
|
+
status ? /* @__PURE__ */ jsxRuntime.jsx(ui.Badge, { color: isAttention ? "red" : "green", children: status }) : null
|
|
21
|
+
] }),
|
|
22
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "px-6 py-4 flex flex-col gap-2", children: [
|
|
23
|
+
/* @__PURE__ */ jsxRuntime.jsxs(ui.Text, { size: "small", children: [
|
|
24
|
+
"Printful order: ",
|
|
25
|
+
/* @__PURE__ */ jsxRuntime.jsx("strong", { children: printfulOrderId })
|
|
26
|
+
] }),
|
|
27
|
+
shipments.length > 0 ? /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex flex-col gap-3 pt-1", children: shipments.map((shipment) => /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-col gap-0.5", children: [
|
|
28
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-2", children: [
|
|
29
|
+
/* @__PURE__ */ jsxRuntime.jsxs(ui.Text, { size: "small", children: [
|
|
30
|
+
shipment.carrier ?? "Unknown carrier",
|
|
31
|
+
shipment.service ? ` · ${shipment.service}` : ""
|
|
32
|
+
] }),
|
|
33
|
+
shipment.reshipment ? /* @__PURE__ */ jsxRuntime.jsx(ui.Badge, { color: "purple", size: "2xsmall", children: "Reshipment" }) : null
|
|
34
|
+
] }),
|
|
35
|
+
shipment.reshipment ? /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: "small", className: "text-ui-fg-subtle", children: "Replacement parcel — no fulfillment is recorded for it." }) : null,
|
|
36
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: "small", className: "text-ui-fg-subtle", children: shipment.ship_date ? `Shipped ${new Date(shipment.ship_date).toLocaleDateString()}` : "Ship date pending" }),
|
|
37
|
+
shipment.tracking_number ? shipment.tracking_url ? /* @__PURE__ */ jsxRuntime.jsx(
|
|
38
|
+
"a",
|
|
39
|
+
{
|
|
40
|
+
href: shipment.tracking_url,
|
|
41
|
+
target: "_blank",
|
|
42
|
+
rel: "noreferrer noopener",
|
|
43
|
+
className: "text-ui-fg-interactive text-sm hover:underline",
|
|
44
|
+
children: shipment.tracking_number
|
|
45
|
+
}
|
|
46
|
+
) : /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: "small", children: shipment.tracking_number }) : null
|
|
47
|
+
] }, shipment.id)) }) : /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: "small", className: "text-ui-fg-subtle", children: "No shipments yet." }),
|
|
48
|
+
statusUpdatedAt ? /* @__PURE__ */ jsxRuntime.jsxs(ui.Text, { size: "small", className: "text-ui-fg-subtle pt-1", children: [
|
|
49
|
+
"Last synced: ",
|
|
50
|
+
new Date(statusUpdatedAt).toLocaleString()
|
|
51
|
+
] }) : null
|
|
52
|
+
] })
|
|
53
|
+
] });
|
|
54
|
+
};
|
|
55
|
+
adminSdk.defineWidgetConfig({
|
|
56
|
+
zone: "order.details.side.before"
|
|
57
|
+
});
|
|
6
58
|
const PrintfulSyncWidget = () => {
|
|
7
59
|
const [status, setStatus] = react.useState(null);
|
|
8
60
|
const [loading, setLoading] = react.useState(false);
|
|
9
61
|
const [syncing, setSyncing] = react.useState(false);
|
|
62
|
+
const [webhook, setWebhook] = react.useState(null);
|
|
63
|
+
const [webhookLoading, setWebhookLoading] = react.useState(false);
|
|
64
|
+
const [registering, setRegistering] = react.useState(false);
|
|
10
65
|
const loadStatus = async () => {
|
|
11
66
|
setLoading(true);
|
|
12
67
|
try {
|
|
@@ -24,9 +79,52 @@ const PrintfulSyncWidget = () => {
|
|
|
24
79
|
setLoading(false);
|
|
25
80
|
}
|
|
26
81
|
};
|
|
82
|
+
const loadWebhook = async () => {
|
|
83
|
+
setWebhookLoading(true);
|
|
84
|
+
try {
|
|
85
|
+
const res = await fetch("/admin/printful/webhook", {
|
|
86
|
+
credentials: "include"
|
|
87
|
+
});
|
|
88
|
+
if (!res.ok) {
|
|
89
|
+
throw new Error(`Status ${res.status}`);
|
|
90
|
+
}
|
|
91
|
+
const data = await res.json();
|
|
92
|
+
setWebhook(data);
|
|
93
|
+
} catch (e) {
|
|
94
|
+
console.error(e);
|
|
95
|
+
} finally {
|
|
96
|
+
setWebhookLoading(false);
|
|
97
|
+
}
|
|
98
|
+
};
|
|
27
99
|
react.useEffect(() => {
|
|
28
100
|
void loadStatus();
|
|
101
|
+
void loadWebhook();
|
|
29
102
|
}, []);
|
|
103
|
+
const onRegisterWebhook = async () => {
|
|
104
|
+
setRegistering(true);
|
|
105
|
+
try {
|
|
106
|
+
const res = await fetch("/admin/printful/webhook", {
|
|
107
|
+
method: "POST",
|
|
108
|
+
credentials: "include",
|
|
109
|
+
headers: { "Content-Type": "application/json" },
|
|
110
|
+
body: JSON.stringify({ base_url: window.location.origin })
|
|
111
|
+
});
|
|
112
|
+
if (!res.ok) {
|
|
113
|
+
const text = await res.text();
|
|
114
|
+
throw new Error(text || `Registration failed (${res.status})`);
|
|
115
|
+
}
|
|
116
|
+
ui.toast.success("Printful webhook registered", {
|
|
117
|
+
description: "The new configuration replaced any previous Printful webhook."
|
|
118
|
+
});
|
|
119
|
+
await loadWebhook();
|
|
120
|
+
} catch (e) {
|
|
121
|
+
ui.toast.error("Failed to register webhook", {
|
|
122
|
+
description: e instanceof Error ? e.message : String(e)
|
|
123
|
+
});
|
|
124
|
+
} finally {
|
|
125
|
+
setRegistering(false);
|
|
126
|
+
}
|
|
127
|
+
};
|
|
30
128
|
const onSync = async () => {
|
|
31
129
|
var _a, _b, _c;
|
|
32
130
|
setSyncing(true);
|
|
@@ -93,7 +191,41 @@ const PrintfulSyncWidget = () => {
|
|
|
93
191
|
latest.products_failed ?? 0
|
|
94
192
|
] }),
|
|
95
193
|
latest.error_message ? /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: "small", className: "text-ui-fg-error", children: latest.error_message.slice(0, 300) }) : null
|
|
96
|
-
] }) : /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: "small", className: "text-ui-fg-subtle", children: "No sync runs yet." }) })
|
|
194
|
+
] }) : /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: "small", className: "text-ui-fg-subtle", children: "No sync runs yet." }) }),
|
|
195
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "px-6 py-4 flex flex-col gap-2", children: [
|
|
196
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center justify-between", children: [
|
|
197
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Heading, { level: "h3", children: "Webhook" }),
|
|
198
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
199
|
+
ui.Button,
|
|
200
|
+
{
|
|
201
|
+
size: "small",
|
|
202
|
+
variant: "secondary",
|
|
203
|
+
onClick: () => void onRegisterWebhook(),
|
|
204
|
+
isLoading: registering,
|
|
205
|
+
disabled: webhookLoading || registering || !(webhook == null ? void 0 : webhook.secret_set),
|
|
206
|
+
children: "Register webhook"
|
|
207
|
+
}
|
|
208
|
+
)
|
|
209
|
+
] }),
|
|
210
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: "small", className: "text-ui-fg-subtle", children: "Printful allows only one webhook configuration per store. Registering replaces any existing Printful webhook URL and event list — including one set up by another integration or a previous install of this plugin." }),
|
|
211
|
+
webhookLoading && !webhook ? /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: "small", children: "Loading webhook configuration…" }) : webhook ? /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
|
|
212
|
+
/* @__PURE__ */ jsxRuntime.jsxs(ui.Text, { size: "small", children: [
|
|
213
|
+
"Current URL:",
|
|
214
|
+
" ",
|
|
215
|
+
/* @__PURE__ */ jsxRuntime.jsx("strong", { children: webhook.current.url ?? "Not configured" })
|
|
216
|
+
] }),
|
|
217
|
+
/* @__PURE__ */ jsxRuntime.jsxs(ui.Text, { size: "small", className: "text-ui-fg-subtle", children: [
|
|
218
|
+
"Current events:",
|
|
219
|
+
" ",
|
|
220
|
+
webhook.current.types.length > 0 ? webhook.current.types.join(", ") : "None"
|
|
221
|
+
] }),
|
|
222
|
+
/* @__PURE__ */ jsxRuntime.jsxs(ui.Text, { size: "small", className: "text-ui-fg-subtle", children: [
|
|
223
|
+
"Will register: ",
|
|
224
|
+
webhook.configured_types.join(", ")
|
|
225
|
+
] }),
|
|
226
|
+
!webhook.secret_set ? /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: "small", className: "text-ui-fg-error", children: "Set the webhookSecret plugin option before registering a webhook." }) : null
|
|
227
|
+
] }) : /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: "small", className: "text-ui-fg-subtle", children: "Unable to load webhook configuration." })
|
|
228
|
+
] })
|
|
97
229
|
] });
|
|
98
230
|
};
|
|
99
231
|
adminSdk.defineWidgetConfig({
|
|
@@ -101,6 +233,11 @@ adminSdk.defineWidgetConfig({
|
|
|
101
233
|
});
|
|
102
234
|
const i18nTranslations0 = {};
|
|
103
235
|
const widgetModule = { widgets: [
|
|
236
|
+
{
|
|
237
|
+
Component: PrintfulOrderWidget,
|
|
238
|
+
zone: ["order.details.side.before"],
|
|
239
|
+
widgetId: "Widget-537b"
|
|
240
|
+
},
|
|
104
241
|
{
|
|
105
242
|
Component: PrintfulSyncWidget,
|
|
106
243
|
zone: ["product.list.before"],
|
|
@@ -1,11 +1,66 @@
|
|
|
1
1
|
import { jsxs, jsx, Fragment } from "react/jsx-runtime";
|
|
2
2
|
import { defineWidgetConfig } from "@medusajs/admin-sdk";
|
|
3
|
-
import { Container, Heading, Text, Button, toast } from "@medusajs/ui";
|
|
3
|
+
import { Container, Heading, Badge, Text, Button, toast } from "@medusajs/ui";
|
|
4
4
|
import { useState, useEffect } from "react";
|
|
5
|
+
const ATTENTION_STATES = /* @__PURE__ */ new Set(["failed", "canceled", "onhold"]);
|
|
6
|
+
const PrintfulOrderWidget = ({ data }) => {
|
|
7
|
+
const metadata = data.metadata ?? {};
|
|
8
|
+
const printfulOrderId = metadata.printful_order_id;
|
|
9
|
+
if (!printfulOrderId) {
|
|
10
|
+
return null;
|
|
11
|
+
}
|
|
12
|
+
const status = metadata.printful_status;
|
|
13
|
+
const statusUpdatedAt = metadata.printful_status_updated_at;
|
|
14
|
+
const shipments = metadata.printful_shipments ?? [];
|
|
15
|
+
const isAttention = status ? ATTENTION_STATES.has(status) : false;
|
|
16
|
+
return /* @__PURE__ */ jsxs(Container, { className: "divide-y p-0", children: [
|
|
17
|
+
/* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between px-6 py-4", children: [
|
|
18
|
+
/* @__PURE__ */ jsx(Heading, { level: "h2", children: "Printful" }),
|
|
19
|
+
status ? /* @__PURE__ */ jsx(Badge, { color: isAttention ? "red" : "green", children: status }) : null
|
|
20
|
+
] }),
|
|
21
|
+
/* @__PURE__ */ jsxs("div", { className: "px-6 py-4 flex flex-col gap-2", children: [
|
|
22
|
+
/* @__PURE__ */ jsxs(Text, { size: "small", children: [
|
|
23
|
+
"Printful order: ",
|
|
24
|
+
/* @__PURE__ */ jsx("strong", { children: printfulOrderId })
|
|
25
|
+
] }),
|
|
26
|
+
shipments.length > 0 ? /* @__PURE__ */ jsx("div", { className: "flex flex-col gap-3 pt-1", children: shipments.map((shipment) => /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-0.5", children: [
|
|
27
|
+
/* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
|
|
28
|
+
/* @__PURE__ */ jsxs(Text, { size: "small", children: [
|
|
29
|
+
shipment.carrier ?? "Unknown carrier",
|
|
30
|
+
shipment.service ? ` · ${shipment.service}` : ""
|
|
31
|
+
] }),
|
|
32
|
+
shipment.reshipment ? /* @__PURE__ */ jsx(Badge, { color: "purple", size: "2xsmall", children: "Reshipment" }) : null
|
|
33
|
+
] }),
|
|
34
|
+
shipment.reshipment ? /* @__PURE__ */ jsx(Text, { size: "small", className: "text-ui-fg-subtle", children: "Replacement parcel — no fulfillment is recorded for it." }) : null,
|
|
35
|
+
/* @__PURE__ */ jsx(Text, { size: "small", className: "text-ui-fg-subtle", children: shipment.ship_date ? `Shipped ${new Date(shipment.ship_date).toLocaleDateString()}` : "Ship date pending" }),
|
|
36
|
+
shipment.tracking_number ? shipment.tracking_url ? /* @__PURE__ */ jsx(
|
|
37
|
+
"a",
|
|
38
|
+
{
|
|
39
|
+
href: shipment.tracking_url,
|
|
40
|
+
target: "_blank",
|
|
41
|
+
rel: "noreferrer noopener",
|
|
42
|
+
className: "text-ui-fg-interactive text-sm hover:underline",
|
|
43
|
+
children: shipment.tracking_number
|
|
44
|
+
}
|
|
45
|
+
) : /* @__PURE__ */ jsx(Text, { size: "small", children: shipment.tracking_number }) : null
|
|
46
|
+
] }, shipment.id)) }) : /* @__PURE__ */ jsx(Text, { size: "small", className: "text-ui-fg-subtle", children: "No shipments yet." }),
|
|
47
|
+
statusUpdatedAt ? /* @__PURE__ */ jsxs(Text, { size: "small", className: "text-ui-fg-subtle pt-1", children: [
|
|
48
|
+
"Last synced: ",
|
|
49
|
+
new Date(statusUpdatedAt).toLocaleString()
|
|
50
|
+
] }) : null
|
|
51
|
+
] })
|
|
52
|
+
] });
|
|
53
|
+
};
|
|
54
|
+
defineWidgetConfig({
|
|
55
|
+
zone: "order.details.side.before"
|
|
56
|
+
});
|
|
5
57
|
const PrintfulSyncWidget = () => {
|
|
6
58
|
const [status, setStatus] = useState(null);
|
|
7
59
|
const [loading, setLoading] = useState(false);
|
|
8
60
|
const [syncing, setSyncing] = useState(false);
|
|
61
|
+
const [webhook, setWebhook] = useState(null);
|
|
62
|
+
const [webhookLoading, setWebhookLoading] = useState(false);
|
|
63
|
+
const [registering, setRegistering] = useState(false);
|
|
9
64
|
const loadStatus = async () => {
|
|
10
65
|
setLoading(true);
|
|
11
66
|
try {
|
|
@@ -23,9 +78,52 @@ const PrintfulSyncWidget = () => {
|
|
|
23
78
|
setLoading(false);
|
|
24
79
|
}
|
|
25
80
|
};
|
|
81
|
+
const loadWebhook = async () => {
|
|
82
|
+
setWebhookLoading(true);
|
|
83
|
+
try {
|
|
84
|
+
const res = await fetch("/admin/printful/webhook", {
|
|
85
|
+
credentials: "include"
|
|
86
|
+
});
|
|
87
|
+
if (!res.ok) {
|
|
88
|
+
throw new Error(`Status ${res.status}`);
|
|
89
|
+
}
|
|
90
|
+
const data = await res.json();
|
|
91
|
+
setWebhook(data);
|
|
92
|
+
} catch (e) {
|
|
93
|
+
console.error(e);
|
|
94
|
+
} finally {
|
|
95
|
+
setWebhookLoading(false);
|
|
96
|
+
}
|
|
97
|
+
};
|
|
26
98
|
useEffect(() => {
|
|
27
99
|
void loadStatus();
|
|
100
|
+
void loadWebhook();
|
|
28
101
|
}, []);
|
|
102
|
+
const onRegisterWebhook = async () => {
|
|
103
|
+
setRegistering(true);
|
|
104
|
+
try {
|
|
105
|
+
const res = await fetch("/admin/printful/webhook", {
|
|
106
|
+
method: "POST",
|
|
107
|
+
credentials: "include",
|
|
108
|
+
headers: { "Content-Type": "application/json" },
|
|
109
|
+
body: JSON.stringify({ base_url: window.location.origin })
|
|
110
|
+
});
|
|
111
|
+
if (!res.ok) {
|
|
112
|
+
const text = await res.text();
|
|
113
|
+
throw new Error(text || `Registration failed (${res.status})`);
|
|
114
|
+
}
|
|
115
|
+
toast.success("Printful webhook registered", {
|
|
116
|
+
description: "The new configuration replaced any previous Printful webhook."
|
|
117
|
+
});
|
|
118
|
+
await loadWebhook();
|
|
119
|
+
} catch (e) {
|
|
120
|
+
toast.error("Failed to register webhook", {
|
|
121
|
+
description: e instanceof Error ? e.message : String(e)
|
|
122
|
+
});
|
|
123
|
+
} finally {
|
|
124
|
+
setRegistering(false);
|
|
125
|
+
}
|
|
126
|
+
};
|
|
29
127
|
const onSync = async () => {
|
|
30
128
|
var _a, _b, _c;
|
|
31
129
|
setSyncing(true);
|
|
@@ -92,7 +190,41 @@ const PrintfulSyncWidget = () => {
|
|
|
92
190
|
latest.products_failed ?? 0
|
|
93
191
|
] }),
|
|
94
192
|
latest.error_message ? /* @__PURE__ */ jsx(Text, { size: "small", className: "text-ui-fg-error", children: latest.error_message.slice(0, 300) }) : null
|
|
95
|
-
] }) : /* @__PURE__ */ jsx(Text, { size: "small", className: "text-ui-fg-subtle", children: "No sync runs yet." }) })
|
|
193
|
+
] }) : /* @__PURE__ */ jsx(Text, { size: "small", className: "text-ui-fg-subtle", children: "No sync runs yet." }) }),
|
|
194
|
+
/* @__PURE__ */ jsxs("div", { className: "px-6 py-4 flex flex-col gap-2", children: [
|
|
195
|
+
/* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between", children: [
|
|
196
|
+
/* @__PURE__ */ jsx(Heading, { level: "h3", children: "Webhook" }),
|
|
197
|
+
/* @__PURE__ */ jsx(
|
|
198
|
+
Button,
|
|
199
|
+
{
|
|
200
|
+
size: "small",
|
|
201
|
+
variant: "secondary",
|
|
202
|
+
onClick: () => void onRegisterWebhook(),
|
|
203
|
+
isLoading: registering,
|
|
204
|
+
disabled: webhookLoading || registering || !(webhook == null ? void 0 : webhook.secret_set),
|
|
205
|
+
children: "Register webhook"
|
|
206
|
+
}
|
|
207
|
+
)
|
|
208
|
+
] }),
|
|
209
|
+
/* @__PURE__ */ jsx(Text, { size: "small", className: "text-ui-fg-subtle", children: "Printful allows only one webhook configuration per store. Registering replaces any existing Printful webhook URL and event list — including one set up by another integration or a previous install of this plugin." }),
|
|
210
|
+
webhookLoading && !webhook ? /* @__PURE__ */ jsx(Text, { size: "small", children: "Loading webhook configuration…" }) : webhook ? /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
211
|
+
/* @__PURE__ */ jsxs(Text, { size: "small", children: [
|
|
212
|
+
"Current URL:",
|
|
213
|
+
" ",
|
|
214
|
+
/* @__PURE__ */ jsx("strong", { children: webhook.current.url ?? "Not configured" })
|
|
215
|
+
] }),
|
|
216
|
+
/* @__PURE__ */ jsxs(Text, { size: "small", className: "text-ui-fg-subtle", children: [
|
|
217
|
+
"Current events:",
|
|
218
|
+
" ",
|
|
219
|
+
webhook.current.types.length > 0 ? webhook.current.types.join(", ") : "None"
|
|
220
|
+
] }),
|
|
221
|
+
/* @__PURE__ */ jsxs(Text, { size: "small", className: "text-ui-fg-subtle", children: [
|
|
222
|
+
"Will register: ",
|
|
223
|
+
webhook.configured_types.join(", ")
|
|
224
|
+
] }),
|
|
225
|
+
!webhook.secret_set ? /* @__PURE__ */ jsx(Text, { size: "small", className: "text-ui-fg-error", children: "Set the webhookSecret plugin option before registering a webhook." }) : null
|
|
226
|
+
] }) : /* @__PURE__ */ jsx(Text, { size: "small", className: "text-ui-fg-subtle", children: "Unable to load webhook configuration." })
|
|
227
|
+
] })
|
|
96
228
|
] });
|
|
97
229
|
};
|
|
98
230
|
defineWidgetConfig({
|
|
@@ -100,6 +232,11 @@ defineWidgetConfig({
|
|
|
100
232
|
});
|
|
101
233
|
const i18nTranslations0 = {};
|
|
102
234
|
const widgetModule = { widgets: [
|
|
235
|
+
{
|
|
236
|
+
Component: PrintfulOrderWidget,
|
|
237
|
+
zone: ["order.details.side.before"],
|
|
238
|
+
widgetId: "Widget-537b"
|
|
239
|
+
},
|
|
103
240
|
{
|
|
104
241
|
Component: PrintfulSyncWidget,
|
|
105
242
|
zone: ["product.list.before"],
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.GET = GET;
|
|
4
|
+
exports.POST = POST;
|
|
5
|
+
const printful_1 = require("../../../../modules/printful");
|
|
6
|
+
const webhook_events_1 = require("../../../../utils/webhook-events");
|
|
7
|
+
/**
|
|
8
|
+
* Printful returns the webhook URL we registered, which embeds our secret
|
|
9
|
+
* token as its last path segment (see POST below). The GET here is
|
|
10
|
+
* admin-authenticated, so returning the URL at all is defensible — the whole
|
|
11
|
+
* point of this endpoint is to show the current config before an admin
|
|
12
|
+
* overwrites it. But the secret is a bearer credential (anyone who has it can
|
|
13
|
+
* forge webhook deliveries prior to the timing-safe check even being
|
|
14
|
+
* relevant, since it's compared as-is), and this endpoint is polled on every
|
|
15
|
+
* widget mount, so there is no reason to put it on the wire repeatedly. Mask
|
|
16
|
+
* everything after the last path segment so the UI can still show host+path
|
|
17
|
+
* shape and confirm "yes, a Printful webhook is registered here" without
|
|
18
|
+
* re-exposing the live token on every page load.
|
|
19
|
+
*/
|
|
20
|
+
function maskSecretInUrl(url) {
|
|
21
|
+
if (!url) {
|
|
22
|
+
return url;
|
|
23
|
+
}
|
|
24
|
+
try {
|
|
25
|
+
const parsed = new URL(url);
|
|
26
|
+
const segments = parsed.pathname.split("/").filter(Boolean);
|
|
27
|
+
if (segments.length > 0) {
|
|
28
|
+
segments[segments.length - 1] = "••••••••";
|
|
29
|
+
parsed.pathname = `/${segments.join("/")}`;
|
|
30
|
+
}
|
|
31
|
+
return parsed.toString();
|
|
32
|
+
}
|
|
33
|
+
catch {
|
|
34
|
+
return url;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
async function GET(req, res) {
|
|
38
|
+
const printful = req.scope.resolve(printful_1.PRINTFUL_MODULE);
|
|
39
|
+
const client = await printful.getClient();
|
|
40
|
+
const options = await printful.getOptions();
|
|
41
|
+
const current = await client.getWebhookConfig();
|
|
42
|
+
res.status(200).json({
|
|
43
|
+
current: {
|
|
44
|
+
...current,
|
|
45
|
+
url: maskSecretInUrl(current.url),
|
|
46
|
+
},
|
|
47
|
+
configured_types: webhook_events_1.PRINTFUL_WEBHOOK_TYPES,
|
|
48
|
+
secret_set: Boolean(options.webhookSecret),
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
async function POST(req, res) {
|
|
52
|
+
const printful = req.scope.resolve(printful_1.PRINTFUL_MODULE);
|
|
53
|
+
const client = await printful.getClient();
|
|
54
|
+
const options = await printful.getOptions();
|
|
55
|
+
if (!options.webhookSecret) {
|
|
56
|
+
res.status(400).json({
|
|
57
|
+
message: "Set the webhookSecret plugin option before registering a webhook",
|
|
58
|
+
});
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
const body = (req.body ?? {});
|
|
62
|
+
if (!body.base_url) {
|
|
63
|
+
res.status(400).json({ message: "base_url is required" });
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
const url = `${body.base_url.replace(/\/$/, "")}/hooks/printful/${options.webhookSecret}`;
|
|
67
|
+
// Printful keeps one config per store, so this replaces the whole allowlist.
|
|
68
|
+
const updated = await client.setWebhookConfig(url, [
|
|
69
|
+
...webhook_events_1.PRINTFUL_WEBHOOK_TYPES,
|
|
70
|
+
]);
|
|
71
|
+
res.status(200).json({
|
|
72
|
+
current: {
|
|
73
|
+
...updated,
|
|
74
|
+
url: maskSecretInUrl(updated.url),
|
|
75
|
+
},
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoicm91dGUuanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi8uLi8uLi8uLi8uLi8uLi8uLi9zcmMvYXBpL2FkbWluL3ByaW50ZnVsL3dlYmhvb2svcm91dGUudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6Ijs7QUFtQ0Esa0JBZUM7QUFFRCxvQkFnQ0M7QUFuRkQsMkRBQThEO0FBRTlELHFFQUF5RTtBQUV6RTs7Ozs7Ozs7Ozs7O0dBWUc7QUFDSCxTQUFTLGVBQWUsQ0FBQyxHQUFrQjtJQUN6QyxJQUFJLENBQUMsR0FBRyxFQUFFLENBQUM7UUFDVCxPQUFPLEdBQUcsQ0FBQTtJQUNaLENBQUM7SUFDRCxJQUFJLENBQUM7UUFDSCxNQUFNLE1BQU0sR0FBRyxJQUFJLEdBQUcsQ0FBQyxHQUFHLENBQUMsQ0FBQTtRQUMzQixNQUFNLFFBQVEsR0FBRyxNQUFNLENBQUMsUUFBUSxDQUFDLEtBQUssQ0FBQyxHQUFHLENBQUMsQ0FBQyxNQUFNLENBQUMsT0FBTyxDQUFDLENBQUE7UUFDM0QsSUFBSSxRQUFRLENBQUMsTUFBTSxHQUFHLENBQUMsRUFBRSxDQUFDO1lBQ3hCLFFBQVEsQ0FBQyxRQUFRLENBQUMsTUFBTSxHQUFHLENBQUMsQ0FBQyxHQUFHLFVBQVUsQ0FBQTtZQUMxQyxNQUFNLENBQUMsUUFBUSxHQUFHLElBQUksUUFBUSxDQUFDLElBQUksQ0FBQyxHQUFHLENBQUMsRUFBRSxDQUFBO1FBQzVDLENBQUM7UUFDRCxPQUFPLE1BQU0sQ0FBQyxRQUFRLEVBQUUsQ0FBQTtJQUMxQixDQUFDO0lBQUMsTUFBTSxDQUFDO1FBQ1AsT0FBTyxHQUFHLENBQUE7SUFDWixDQUFDO0FBQ0gsQ0FBQztBQUVNLEtBQUssVUFBVSxHQUFHLENBQUMsR0FBa0IsRUFBRSxHQUFtQjtJQUMvRCxNQUFNLFFBQVEsR0FBMEIsR0FBRyxDQUFDLEtBQUssQ0FBQyxPQUFPLENBQUMsMEJBQWUsQ0FBQyxDQUFBO0lBQzFFLE1BQU0sTUFBTSxHQUFHLE1BQU0sUUFBUSxDQUFDLFNBQVMsRUFBRSxDQUFBO0lBQ3pDLE1BQU0sT0FBTyxHQUFHLE1BQU0sUUFBUSxDQUFDLFVBQVUsRUFBRSxDQUFBO0lBRTNDLE1BQU0sT0FBTyxHQUFHLE1BQU0sTUFBTSxDQUFDLGdCQUFnQixFQUFFLENBQUE7SUFFL0MsR0FBRyxDQUFDLE1BQU0sQ0FBQyxHQUFHLENBQUMsQ0FBQyxJQUFJLENBQUM7UUFDbkIsT0FBTyxFQUFFO1lBQ1AsR0FBRyxPQUFPO1lBQ1YsR0FBRyxFQUFFLGVBQWUsQ0FBQyxPQUFPLENBQUMsR0FBRyxDQUFDO1NBQ2xDO1FBQ0QsZ0JBQWdCLEVBQUUsdUNBQXNCO1FBQ3hDLFVBQVUsRUFBRSxPQUFPLENBQUMsT0FBTyxDQUFDLGFBQWEsQ0FBQztLQUMzQyxDQUFDLENBQUE7QUFDSixDQUFDO0FBRU0sS0FBSyxVQUFVLElBQUksQ0FBQyxHQUFrQixFQUFFLEdBQW1CO0lBQ2hFLE1BQU0sUUFBUSxHQUEwQixHQUFHLENBQUMsS0FBSyxDQUFDLE9BQU8sQ0FBQywwQkFBZSxDQUFDLENBQUE7SUFDMUUsTUFBTSxNQUFNLEdBQUcsTUFBTSxRQUFRLENBQUMsU0FBUyxFQUFFLENBQUE7SUFDekMsTUFBTSxPQUFPLEdBQUcsTUFBTSxRQUFRLENBQUMsVUFBVSxFQUFFLENBQUE7SUFFM0MsSUFBSSxDQUFDLE9BQU8sQ0FBQyxhQUFhLEVBQUUsQ0FBQztRQUMzQixHQUFHLENBQUMsTUFBTSxDQUFDLEdBQUcsQ0FBQyxDQUFDLElBQUksQ0FBQztZQUNuQixPQUFPLEVBQ0wsa0VBQWtFO1NBQ3JFLENBQUMsQ0FBQTtRQUNGLE9BQU07SUFDUixDQUFDO0lBRUQsTUFBTSxJQUFJLEdBQUcsQ0FBQyxHQUFHLENBQUMsSUFBSSxJQUFJLEVBQUUsQ0FBMEIsQ0FBQTtJQUN0RCxJQUFJLENBQUMsSUFBSSxDQUFDLFFBQVEsRUFBRSxDQUFDO1FBQ25CLEdBQUcsQ0FBQyxNQUFNLENBQUMsR0FBRyxDQUFDLENBQUMsSUFBSSxDQUFDLEVBQUUsT0FBTyxFQUFFLHNCQUFzQixFQUFFLENBQUMsQ0FBQTtRQUN6RCxPQUFNO0lBQ1IsQ0FBQztJQUVELE1BQU0sR0FBRyxHQUFHLEdBQUcsSUFBSSxDQUFDLFFBQVEsQ0FBQyxPQUFPLENBQUMsS0FBSyxFQUFFLEVBQUUsQ0FBQyxtQkFBbUIsT0FBTyxDQUFDLGFBQWEsRUFBRSxDQUFBO0lBRXpGLDZFQUE2RTtJQUM3RSxNQUFNLE9BQU8sR0FBRyxNQUFNLE1BQU0sQ0FBQyxnQkFBZ0IsQ0FBQyxHQUFHLEVBQUU7UUFDakQsR0FBRyx1Q0FBc0I7S0FDMUIsQ0FBQyxDQUFBO0lBRUYsR0FBRyxDQUFDLE1BQU0sQ0FBQyxHQUFHLENBQUMsQ0FBQyxJQUFJLENBQUM7UUFDbkIsT0FBTyxFQUFFO1lBQ1AsR0FBRyxPQUFPO1lBQ1YsR0FBRyxFQUFFLGVBQWUsQ0FBQyxPQUFPLENBQUMsR0FBRyxDQUFDO1NBQ2xDO0tBQ0YsQ0FBQyxDQUFBO0FBQ0osQ0FBQyJ9
|
|
@@ -0,0 +1,80 @@
|
|
|
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 = POST;
|
|
7
|
+
const utils_1 = require("@medusajs/framework/utils");
|
|
8
|
+
const printful_1 = require("../../../../modules/printful");
|
|
9
|
+
const webhook_events_1 = require("../../../../utils/webhook-events");
|
|
10
|
+
const apply_order_status_1 = __importDefault(require("../../../../workflows/apply-order-status"));
|
|
11
|
+
/**
|
|
12
|
+
* Public Printful webhook endpoint.
|
|
13
|
+
*
|
|
14
|
+
* The payload is a trigger, not a source of truth: we persist it, answer 200,
|
|
15
|
+
* and let the workflow re-read GET /orders/{id} for the real state. Never log
|
|
16
|
+
* the request URL or token.
|
|
17
|
+
*/
|
|
18
|
+
async function POST(req, res) {
|
|
19
|
+
const printful = req.scope.resolve(printful_1.PRINTFUL_MODULE);
|
|
20
|
+
const logger = req.scope.resolve(utils_1.ContainerRegistrationKeys.LOGGER);
|
|
21
|
+
const options = await printful.getOptions();
|
|
22
|
+
const provided = req.params.token;
|
|
23
|
+
if (!(0, webhook_events_1.verifyWebhookToken)(options.webhookSecret, provided)) {
|
|
24
|
+
// 404, not 401: do not confirm that this path exists.
|
|
25
|
+
res.status(404).json({ message: "Not found" });
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
const payload = (req.body ?? {});
|
|
29
|
+
const type = payload.type ?? "unknown";
|
|
30
|
+
const printfulOrderId = (0, webhook_events_1.extractOrderId)(payload);
|
|
31
|
+
if (!printfulOrderId) {
|
|
32
|
+
res.status(200).json({ received: true, ignored: "no_order_id" });
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
let eventId;
|
|
36
|
+
try {
|
|
37
|
+
eventId = (0, webhook_events_1.deriveEventId)(payload);
|
|
38
|
+
}
|
|
39
|
+
catch {
|
|
40
|
+
// Malformed or hostile payload — refuse it without a 5xx, which would make
|
|
41
|
+
// Printful retry something that can never succeed.
|
|
42
|
+
res.status(400).json({ message: "Malformed payload" });
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
const handled = webhook_events_1.PRINTFUL_WEBHOOK_TYPES.includes(type);
|
|
46
|
+
let event;
|
|
47
|
+
try {
|
|
48
|
+
event = await printful.recordWebhookEvent({
|
|
49
|
+
event_id: eventId,
|
|
50
|
+
type,
|
|
51
|
+
printful_order_id: printfulOrderId,
|
|
52
|
+
printful_shipment_id: (0, webhook_events_1.extractShipmentId)(payload),
|
|
53
|
+
payload: payload,
|
|
54
|
+
status: handled ? "received" : "ignored",
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
catch (err) {
|
|
58
|
+
// Only storage failures reach here. 500 is correct: we want Printful to retry.
|
|
59
|
+
logger.error(`Printful: failed to store webhook event ${eventId}: ${err instanceof Error ? err.message : String(err)}`);
|
|
60
|
+
res.status(500).json({ message: "Failed to store event" });
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
// Redelivery of an event we already hold: absorb it.
|
|
64
|
+
if (!event) {
|
|
65
|
+
res.status(200).json({ received: true, event_id: eventId, duplicate: true });
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
res.status(200).json({ received: true, event_id: eventId });
|
|
69
|
+
if (handled) {
|
|
70
|
+
// Fire-and-forget: the durable row plus the retry job (once it lands) are
|
|
71
|
+
// the intended safety net, so losing this in-process attempt costs at most
|
|
72
|
+
// one retry interval.
|
|
73
|
+
void (0, apply_order_status_1.default)(req.scope)
|
|
74
|
+
.run({ input: { event_row_id: event.id } })
|
|
75
|
+
.catch((err) => {
|
|
76
|
+
logger.error(`Printful: apply failed for event ${eventId}: ${err instanceof Error ? err.message : String(err)}`);
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoicm91dGUuanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi8uLi8uLi8uLi8uLi8uLi8uLi9zcmMvYXBpL2hvb2tzL3ByaW50ZnVsL1t0b2tlbl0vcm91dGUudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6Ijs7Ozs7QUFxQkEsb0JBOEVDO0FBbEdELHFEQUFxRTtBQUNyRSwyREFBOEQ7QUFFOUQscUVBT3lDO0FBQ3pDLGtHQUErRTtBQUUvRTs7Ozs7O0dBTUc7QUFDSSxLQUFLLFVBQVUsSUFBSSxDQUFDLEdBQWtCLEVBQUUsR0FBbUI7SUFDaEUsTUFBTSxRQUFRLEdBQTBCLEdBQUcsQ0FBQyxLQUFLLENBQUMsT0FBTyxDQUFDLDBCQUFlLENBQUMsQ0FBQTtJQUMxRSxNQUFNLE1BQU0sR0FBcUMsR0FBRyxDQUFDLEtBQUssQ0FBQyxPQUFPLENBQ2hFLGlDQUF5QixDQUFDLE1BQU0sQ0FDakMsQ0FBQTtJQUNELE1BQU0sT0FBTyxHQUFHLE1BQU0sUUFBUSxDQUFDLFVBQVUsRUFBRSxDQUFBO0lBRTNDLE1BQU0sUUFBUSxHQUFHLEdBQUcsQ0FBQyxNQUFNLENBQUMsS0FBSyxDQUFBO0lBQ2pDLElBQUksQ0FBQyxJQUFBLG1DQUFrQixFQUFDLE9BQU8sQ0FBQyxhQUFhLEVBQUUsUUFBUSxDQUFDLEVBQUUsQ0FBQztRQUN6RCxzREFBc0Q7UUFDdEQsR0FBRyxDQUFDLE1BQU0sQ0FBQyxHQUFHLENBQUMsQ0FBQyxJQUFJLENBQUMsRUFBRSxPQUFPLEVBQUUsV0FBVyxFQUFFLENBQUMsQ0FBQTtRQUM5QyxPQUFNO0lBQ1IsQ0FBQztJQUVELE1BQU0sT0FBTyxHQUFHLENBQUMsR0FBRyxDQUFDLElBQUksSUFBSSxFQUFFLENBQTJCLENBQUE7SUFDMUQsTUFBTSxJQUFJLEdBQUcsT0FBTyxDQUFDLElBQUksSUFBSSxTQUFTLENBQUE7SUFDdEMsTUFBTSxlQUFlLEdBQUcsSUFBQSwrQkFBYyxFQUFDLE9BQU8sQ0FBQyxDQUFBO0lBRS9DLElBQUksQ0FBQyxlQUFl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
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const http_1 = require("@medusajs/framework/http");
|
|
4
|
+
const webhook_path_1 = require("../utils/webhook-path");
|
|
5
|
+
exports.default = (0, http_1.defineMiddlewares)({
|
|
6
|
+
routes: [
|
|
7
|
+
{
|
|
8
|
+
matcher: "/hooks/printful/*",
|
|
9
|
+
/**
|
|
10
|
+
* Sized deliberately, for two reasons.
|
|
11
|
+
*
|
|
12
|
+
* Correctness: Express defaults to 100KB, and a real `package_shipped`
|
|
13
|
+
* body for a ~25-line-item order already exceeds that (~132KB; ~262KB at
|
|
14
|
+
* 50 items). Under the default, Printful deliveries for large orders are
|
|
15
|
+
* rejected outright.
|
|
16
|
+
*
|
|
17
|
+
* Secrecy: that rejection is a 413 raised by the GLOBAL body-parser,
|
|
18
|
+
* which Medusa registers at `app.use("/", ...)` before any route-scoped
|
|
19
|
+
* middleware (@medusajs/framework/dist/http/router.js). Such an error
|
|
20
|
+
* goes straight to the error handler without ever running the redaction
|
|
21
|
+
* below, so the live secret in the path would be logged in cleartext.
|
|
22
|
+
* Raising the limit keeps legitimate traffic from ever reaching that
|
|
23
|
+
* path. This config IS honoured despite the global registration: the
|
|
24
|
+
* parser resolves its config per request via a RoutesFinder lookup on
|
|
25
|
+
* `req.path` (dist/http/middlewares/bodyparser.js).
|
|
26
|
+
*/
|
|
27
|
+
bodyParser: { sizeLimit: webhook_path_1.WEBHOOK_BODY_SIZE_LIMIT },
|
|
28
|
+
middlewares: [
|
|
29
|
+
(req, _res, next) => {
|
|
30
|
+
// Medusa's error handler logs `Error ${statusCode} at ${req.path}`
|
|
31
|
+
// (@medusajs/framework/dist/http/middlewares/error-handler.js), and
|
|
32
|
+
// our path carries the webhook secret as a segment. Redact it before
|
|
33
|
+
// any downstream handler or the error middleware can write it out.
|
|
34
|
+
//
|
|
35
|
+
// Medusa registers plugin middleware with app.use(matcher, handler),
|
|
36
|
+
// so inside THIS function Express has already stripped the matched
|
|
37
|
+
// prefix: req.path is "/" and the secret sits in req.baseUrl. The
|
|
38
|
+
// full path is only reassembled downstream, which is why we redact
|
|
39
|
+
// req.originalUrl and pin req.path to the redacted full path — the
|
|
40
|
+
// value the error handler ends up reading.
|
|
41
|
+
//
|
|
42
|
+
// Verified: this does not affect req.params.token (param extraction
|
|
43
|
+
// happens in the route layer from the untouched URL), so the route's
|
|
44
|
+
// auth check is unaffected.
|
|
45
|
+
const redacted = (0, webhook_path_1.redactWebhookPath)(req.originalUrl);
|
|
46
|
+
Object.defineProperty(req, "path", {
|
|
47
|
+
value: redacted,
|
|
48
|
+
configurable: true,
|
|
49
|
+
});
|
|
50
|
+
Object.defineProperty(req, "originalUrl", {
|
|
51
|
+
value: redacted,
|
|
52
|
+
configurable: true,
|
|
53
|
+
});
|
|
54
|
+
next();
|
|
55
|
+
},
|
|
56
|
+
],
|
|
57
|
+
},
|
|
58
|
+
],
|
|
59
|
+
});
|
|
60
|
+
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoibWlkZGxld2FyZXMuanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi8uLi8uLi8uLi9zcmMvYXBpL21pZGRsZXdhcmVzLnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiI7O0FBQUEsbURBQTREO0FBQzVELHdEQUc4QjtBQUU5QixrQkFBZSxJQUFBLHdCQUFpQixFQUFDO0lBQy9CLE1BQU0sRUFBRTtRQUNOO1lBQ0UsT0FBTyxFQUFFLG1CQUFtQjtZQUM1Qjs7Ozs7Ozs7Ozs7Ozs7Ozs7ZUFpQkc7WUFDSCxVQUFVLEVBQUUsRUFBRSxTQUFTLEVBQUUsc0NBQXVCLEVBQUU7WUFDbEQsV0FBVyxFQUFFO2dCQUNYLENBQUMsR0FBRyxFQUFFLElBQUksRUFBRSxJQUFJLEVBQUUsRUFBRTtvQkFDbEIsbUVBQW1FO29CQUNuRSxvRUFBb0U7b0JBQ3BFLHFFQUFxRTtvQkFDckUsbUVBQW1FO29CQUNuRSxFQUFFO29CQUNGLHFFQUFxRTtvQkFDckUsbUVBQW1FO29CQUNuRSxrRUFBa0U7b0JBQ2xFLG1FQUFtRTtvQkFDbkUsbUVBQW1FO29CQUNuRSwyQ0FBMkM7b0JBQzNDLEVBQUU7b0JBQ0Ysb0VBQW9FO29CQUNwRSxxRUFBcUU7b0JBQ3JFLDRCQUE0QjtvQkFDNUIsTUFBTSxRQUFRLEdBQUcsSUFBQSxnQ0FBaUIsRUFBQyxHQUFHLENBQUMsV0FBVyxDQUFDLENBQUE7b0JBQ25ELE1BQU0sQ0FBQyxjQUFjLENBQUMsR0FBRyxFQUFFLE1BQU0sRUFBRTt3QkFDakMsS0FBSyxFQUFFLFFBQVE7d0JBQ2YsWUFBWSxFQUFFLElBQUk7cUJBQ25CLENBQUMsQ0FBQTtvQkFDRixNQUFNLENBQUMsY0FBYyxDQUFDLEdBQUcsRUFBRSxhQUFhLEVBQUU7d0JBQ3hDLEtBQUssRUFBRSxRQUFRO3dCQUNmLFlBQVksRUFBRSxJQUFJO3FCQUNuQixDQUFDLENBQUE7b0JBQ0YsSUFBSSxFQUFFLENBQUE7Z0JBQ1IsQ0FBQzthQUNGO1NBQ0Y7S0FDRjtDQUNGLENBQUMsQ0FBQSJ9
|
|
@@ -0,0 +1,65 @@
|
|
|
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.config = void 0;
|
|
7
|
+
exports.default = retryPrintfulWebhookEvents;
|
|
8
|
+
const utils_1 = require("@medusajs/framework/utils");
|
|
9
|
+
const printful_1 = require("../modules/printful");
|
|
10
|
+
const apply_order_status_1 = __importDefault(require("../workflows/apply-order-status"));
|
|
11
|
+
/**
|
|
12
|
+
* Drains webhook events that were stored but not applied — most often because
|
|
13
|
+
* the order link had not been written yet when the webhook landed, or because
|
|
14
|
+
* the process died between storing the event and applying it.
|
|
15
|
+
*
|
|
16
|
+
* This is the safety net the webhook route's fire-and-forget dispatch relies on:
|
|
17
|
+
* the route's in-process attempt is only an optimization to avoid waiting a
|
|
18
|
+
* retry interval in the common case.
|
|
19
|
+
*/
|
|
20
|
+
/** Events drained per sweep. */
|
|
21
|
+
const BATCH_SIZE = 50;
|
|
22
|
+
/**
|
|
23
|
+
* Consecutive failures before the sweep gives up for this run. A database or
|
|
24
|
+
* Printful outage fails every event identically, and grinding through the whole
|
|
25
|
+
* batch each tick buys nothing — the remaining events keep their backoff and
|
|
26
|
+
* are picked up by the next sweep.
|
|
27
|
+
*/
|
|
28
|
+
const CONSECUTIVE_FAILURE_LIMIT = 5;
|
|
29
|
+
async function retryPrintfulWebhookEvents(container) {
|
|
30
|
+
const printful = container.resolve(printful_1.PRINTFUL_MODULE);
|
|
31
|
+
const logger = container.resolve(utils_1.ContainerRegistrationKeys.LOGGER);
|
|
32
|
+
const due = await printful.listDueWebhookEvents(BATCH_SIZE);
|
|
33
|
+
if (!due.length) {
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
logger.info(`Printful: retrying ${due.length} webhook event(s)`);
|
|
37
|
+
if (due.length === BATCH_SIZE) {
|
|
38
|
+
// A full batch means the backlog may exceed what one sweep can drain.
|
|
39
|
+
logger.info(`Printful: retry batch is full (${BATCH_SIZE}); backlog may be growing`);
|
|
40
|
+
}
|
|
41
|
+
let consecutiveFailures = 0;
|
|
42
|
+
for (const event of due) {
|
|
43
|
+
try {
|
|
44
|
+
await (0, apply_order_status_1.default)(container).run({
|
|
45
|
+
input: { event_row_id: event.id },
|
|
46
|
+
});
|
|
47
|
+
consecutiveFailures = 0;
|
|
48
|
+
}
|
|
49
|
+
catch (err) {
|
|
50
|
+
// The workflow records its own failure state; a throw here means the
|
|
51
|
+
// sweep should continue rather than abandon the remaining events.
|
|
52
|
+
consecutiveFailures += 1;
|
|
53
|
+
logger.error(`Printful: retry failed for event ${event.event_id}: ${err instanceof Error ? err.message : String(err)}`);
|
|
54
|
+
if (consecutiveFailures >= CONSECUTIVE_FAILURE_LIMIT) {
|
|
55
|
+
logger.error(`Printful: aborting retry sweep after ${consecutiveFailures} consecutive failures`);
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
exports.config = {
|
|
62
|
+
name: "printful-retry-webhook-events",
|
|
63
|
+
schedule: "*/5 * * * *",
|
|
64
|
+
};
|
|
65
|
+
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoicmV0cnktd2ViaG9vay1ldmVudHMuanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi8uLi8uLi8uLi9zcmMvam9icy9yZXRyeS13ZWJob29rLWV2ZW50cy50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiOzs7Ozs7QUEwQkEsNkNBK0NDO0FBeEVELHFEQUFxRTtBQUNyRSxrREFBcUQ7QUFFckQseUZBQXNFO0FBRXRFOzs7Ozs7OztHQVFHO0FBQ0gsZ0NBQWdDO0FBQ2hDLE1BQU0sVUFBVSxHQUFHLEVBQUUsQ0FBQTtBQUVyQjs7Ozs7R0FLRztBQUNILE1BQU0seUJBQXlCLEdBQUcsQ0FBQyxDQUFBO0FBRXBCLEtBQUssVUFBVSwwQkFBMEIsQ0FDdEQsU0FBMEI7SUFFMUIsTUFBTSxRQUFRLEdBQTBCLFNBQVMsQ0FBQyxPQUFPLENBQUMsMEJBQWUsQ0FBQyxDQUFBO0lBQzFFLE1BQU0sTUFBTSxHQUNWLFNBQVMsQ0FBQyxPQUFPLENBQUMsaUNBQXlCLENBQUMsTUFBTSxDQUFDLENBQUE7SUFFckQsTUFBTSxHQUFHLEdBQUcsTUFBTSxRQUFRLENBQUMsb0JBQW9CLENBQUMsVUFBVSxDQUFDLENBQUE7SUFDM0QsSUFBSSxDQUFDLEdBQUcsQ0FBQyxNQUFNLEVBQUUsQ0FBQztRQUNoQixPQUFNO0lBQ1IsQ0FBQztJQUVELE1BQU0sQ0FBQyxJQUFJLENBQUMsc0JBQXNCLEdBQUcsQ0FBQyxNQUFNLG1CQUFtQixDQUFDLENBQUE7SUFFaEUsSUFBSSxHQUFHLENBQUMsTUFBTSxLQUFLLFVBQVUsRUFBRSxDQUFDO1FBQzlCLHNFQUFzRTtRQUN0RSxNQUFNLENBQUMsSUFBSSxDQUNULGtDQUFrQyxVQUFVLDJCQUEyQixDQUN4RSxDQUFBO0lBQ0gsQ0FBQztJQUVELElBQUksbUJBQW1CLEdBQUcsQ0FBQyxDQUFBO0lBRTNCLEtBQUssTUFBTSxLQUFLLElBQUksR0FBRyxFQUFFLENBQUM7UUFDeEIsSUFBSSxDQUFDO1lBQ0gsTUFBTSxJQUFBLDRCQUF3QixFQUFDLFNBQVMsQ0FBQyxDQUFDLEdBQUcsQ0FBQztnQkFDNUMsS0FBSyxFQUFFLEVBQUUsWUFBWSxFQUFFLEtBQUssQ0FBQyxFQUFFLEVBQUU7YUFDbEMsQ0FBQyxDQUFBO1lBQ0YsbUJBQW1CLEdBQUcsQ0FBQyxDQUFBO1FBQ3pCLENBQUM7UUFBQyxPQUFPLEdBQUcsRUFBRSxDQUFDO1lBQ2IscUVBQXFFO1lBQ3JFLGtFQUFrRTtZQUNsRSxtQkFBbUIsSUFBSSxDQUFDLENBQUE7WUFDeEIsTUFBTSxDQUFDLEtBQUssQ0FDVixvQ0FBb0MsS0FBSyxDQUFDLFFBQVEsS0FDaEQsR0FBRyxZQUFZLEtBQUssQ0FBQyxDQUFDLENBQUMsR0FBRyxDQUFDLE9BQU8sQ0FBQyxDQUFDLENBQUMsTUFBTSxDQUFDLEdBQUcsQ0FDakQsRUFBRSxDQUNILENBQUE7WUFFRCxJQUFJLG1CQUFtQixJQUFJLHlCQUF5QixFQUFFLENBQUM7Z0JBQ3JELE1BQU0sQ0FBQyxLQUFLLENBQ1Ysd0NBQXdDLG1CQUFtQix1QkFBdUIsQ0FDbkYsQ0FBQTtnQkFDRCxPQUFNO1lBQ1IsQ0FBQztRQUNILENBQUM7SUFDSCxDQUFDO0FBQ0gsQ0FBQztBQUVZLFFBQUEsTUFBTSxHQUFHO0lBQ3BCLElBQUksRUFBRSwrQkFBK0I7SUFDckMsUUFBUSxFQUFFLGFBQWE7Q0FDeEIsQ0FBQSJ9
|