@legenki/print2medusa 0.3.0 → 0.5.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 +66 -9
- package/.medusa/server/src/admin/index.mjs +66 -9
- package/.medusa/server/src/api/admin/printful/sync/route.js +36 -6
- package/.medusa/server/src/jobs/sync-products.js +17 -3
- package/.medusa/server/src/modules/printful/migrations/Migration20260802000000.js +40 -0
- package/.medusa/server/src/modules/printful/models/printful-sync-log.js +4 -1
- package/.medusa/server/src/modules/printful/service.js +67 -1
- package/.medusa/server/src/utils/costs.js +109 -0
- package/.medusa/server/src/utils/mappers.js +8 -2
- package/.medusa/server/src/utils/order-state.js +5 -1
- package/.medusa/server/src/utils/orphans.js +47 -0
- package/.medusa/server/src/utils/stock.js +85 -0
- package/.medusa/server/src/workflows/create-printful-order.js +23 -1
- package/.medusa/server/src/workflows/sync-products.js +61 -22
- package/README.md +70 -19
- package/package.json +2 -2
|
@@ -4,6 +4,13 @@ const adminSdk = require("@medusajs/admin-sdk");
|
|
|
4
4
|
const ui = require("@medusajs/ui");
|
|
5
5
|
const react = require("react");
|
|
6
6
|
const ATTENTION_STATES = /* @__PURE__ */ new Set(["failed", "canceled", "onhold"]);
|
|
7
|
+
const formatMinor = (amount, currency) => {
|
|
8
|
+
if (typeof amount !== "number" || !Number.isFinite(amount)) {
|
|
9
|
+
return null;
|
|
10
|
+
}
|
|
11
|
+
const code = typeof currency === "string" ? currency.toUpperCase() : "";
|
|
12
|
+
return `${(amount / 100).toFixed(2)}${code ? ` ${code}` : ""}`;
|
|
13
|
+
};
|
|
7
14
|
const PrintfulOrderWidget = ({ data }) => {
|
|
8
15
|
const metadata = data.metadata ?? {};
|
|
9
16
|
const printfulOrderId = metadata.printful_order_id;
|
|
@@ -14,6 +21,19 @@ const PrintfulOrderWidget = ({ data }) => {
|
|
|
14
21
|
const statusUpdatedAt = metadata.printful_status_updated_at;
|
|
15
22
|
const shipments = metadata.printful_shipments ?? [];
|
|
16
23
|
const isAttention = status ? ATTENTION_STATES.has(status) : false;
|
|
24
|
+
const costTotal = formatMinor(
|
|
25
|
+
metadata.printful_cost_total,
|
|
26
|
+
metadata.printful_cost_currency
|
|
27
|
+
);
|
|
28
|
+
const retailTotal = formatMinor(
|
|
29
|
+
metadata.printful_retail_total,
|
|
30
|
+
metadata.printful_retail_currency
|
|
31
|
+
);
|
|
32
|
+
const margin = formatMinor(
|
|
33
|
+
metadata.printful_margin,
|
|
34
|
+
metadata.printful_cost_currency
|
|
35
|
+
);
|
|
36
|
+
const currencyMismatch = costTotal !== null && retailTotal !== null && margin === null;
|
|
17
37
|
return /* @__PURE__ */ jsxRuntime.jsxs(ui.Container, { className: "divide-y p-0", children: [
|
|
18
38
|
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center justify-between px-6 py-4", children: [
|
|
19
39
|
/* @__PURE__ */ jsxRuntime.jsx(ui.Heading, { level: "h2", children: "Printful" }),
|
|
@@ -49,7 +69,23 @@ const PrintfulOrderWidget = ({ data }) => {
|
|
|
49
69
|
"Last synced: ",
|
|
50
70
|
new Date(statusUpdatedAt).toLocaleString()
|
|
51
71
|
] }) : null
|
|
52
|
-
] })
|
|
72
|
+
] }),
|
|
73
|
+
costTotal ? /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "px-6 py-4 flex flex-col gap-2", children: [
|
|
74
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: "small", weight: "plus", children: "Economics" }),
|
|
75
|
+
/* @__PURE__ */ jsxRuntime.jsxs(ui.Text, { size: "small", className: "text-ui-fg-subtle", children: [
|
|
76
|
+
"Printful cost: ",
|
|
77
|
+
costTotal
|
|
78
|
+
] }),
|
|
79
|
+
retailTotal ? /* @__PURE__ */ jsxRuntime.jsxs(ui.Text, { size: "small", className: "text-ui-fg-subtle", children: [
|
|
80
|
+
"Retail: ",
|
|
81
|
+
retailTotal
|
|
82
|
+
] }) : null,
|
|
83
|
+
margin ? /* @__PURE__ */ jsxRuntime.jsxs(ui.Text, { size: "small", children: [
|
|
84
|
+
"Margin: ",
|
|
85
|
+
margin
|
|
86
|
+
] }) : null,
|
|
87
|
+
currencyMismatch ? /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: "small", className: "text-ui-fg-subtle", children: "Margin unavailable — Printful billed in a different currency than the order." }) : null
|
|
88
|
+
] }) : null
|
|
53
89
|
] });
|
|
54
90
|
};
|
|
55
91
|
adminSdk.defineWidgetConfig({
|
|
@@ -100,6 +136,13 @@ const PrintfulSyncWidget = () => {
|
|
|
100
136
|
void loadStatus();
|
|
101
137
|
void loadWebhook();
|
|
102
138
|
}, []);
|
|
139
|
+
react.useEffect(() => {
|
|
140
|
+
if (!(status == null ? void 0 : status.running)) {
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
const t = setInterval(() => void loadStatus(), 3e3);
|
|
144
|
+
return () => clearInterval(t);
|
|
145
|
+
}, [status == null ? void 0 : status.running]);
|
|
103
146
|
const onRegisterWebhook = async () => {
|
|
104
147
|
setRegistering(true);
|
|
105
148
|
try {
|
|
@@ -126,7 +169,6 @@ const PrintfulSyncWidget = () => {
|
|
|
126
169
|
}
|
|
127
170
|
};
|
|
128
171
|
const onSync = async () => {
|
|
129
|
-
var _a, _b, _c;
|
|
130
172
|
setSyncing(true);
|
|
131
173
|
try {
|
|
132
174
|
const res = await fetch("/admin/printful/sync", {
|
|
@@ -135,13 +177,20 @@ const PrintfulSyncWidget = () => {
|
|
|
135
177
|
headers: { "Content-Type": "application/json" },
|
|
136
178
|
body: JSON.stringify({})
|
|
137
179
|
});
|
|
180
|
+
if (res.status === 409) {
|
|
181
|
+
const body = await res.json();
|
|
182
|
+
ui.toast.info("A Printful sync is already running", {
|
|
183
|
+
description: body.started_at ? `Started ${new Date(body.started_at).toLocaleString()}` : void 0
|
|
184
|
+
});
|
|
185
|
+
await loadStatus();
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
138
188
|
if (!res.ok) {
|
|
139
189
|
const text = await res.text();
|
|
140
190
|
throw new Error(text || `Sync failed (${res.status})`);
|
|
141
191
|
}
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
description: `Created ${((_a = data.counters) == null ? void 0 : _a.created) ?? 0}, updated ${((_b = data.counters) == null ? void 0 : _b.updated) ?? 0}, failed ${((_c = data.counters) == null ? void 0 : _c.failed) ?? 0}`
|
|
192
|
+
ui.toast.success("Printful sync started", {
|
|
193
|
+
description: "Progress will appear below as products are processed."
|
|
145
194
|
});
|
|
146
195
|
await loadStatus();
|
|
147
196
|
} catch (e) {
|
|
@@ -153,6 +202,7 @@ const PrintfulSyncWidget = () => {
|
|
|
153
202
|
}
|
|
154
203
|
};
|
|
155
204
|
const latest = status == null ? void 0 : status.latest_sync;
|
|
205
|
+
const running = (status == null ? void 0 : status.running) === true;
|
|
156
206
|
return /* @__PURE__ */ jsxRuntime.jsxs(ui.Container, { className: "divide-y p-0", children: [
|
|
157
207
|
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center justify-between px-6 py-4", children: [
|
|
158
208
|
/* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
|
|
@@ -164,9 +214,9 @@ const PrintfulSyncWidget = () => {
|
|
|
164
214
|
{
|
|
165
215
|
size: "small",
|
|
166
216
|
onClick: () => void onSync(),
|
|
167
|
-
isLoading: syncing,
|
|
168
|
-
disabled: loading || syncing,
|
|
169
|
-
children: "Sync Now"
|
|
217
|
+
isLoading: syncing || running,
|
|
218
|
+
disabled: loading || syncing || running,
|
|
219
|
+
children: running ? "Syncing…" : "Sync Now"
|
|
170
220
|
}
|
|
171
221
|
)
|
|
172
222
|
] }),
|
|
@@ -180,7 +230,14 @@ const PrintfulSyncWidget = () => {
|
|
|
180
230
|
new Date(latest.started_at).toLocaleString(),
|
|
181
231
|
latest.finished_at ? ` · Finished: ${new Date(latest.finished_at).toLocaleString()}` : ""
|
|
182
232
|
] }),
|
|
183
|
-
/* @__PURE__ */ jsxRuntime.jsxs(ui.Text, { size: "small", className: "text-ui-fg-subtle", children: [
|
|
233
|
+
running ? /* @__PURE__ */ jsxRuntime.jsxs(ui.Text, { size: "small", className: "text-ui-fg-subtle", children: [
|
|
234
|
+
"Processed ",
|
|
235
|
+
latest.products_processed ?? 0,
|
|
236
|
+
" of",
|
|
237
|
+
" ",
|
|
238
|
+
latest.products_total ?? 0,
|
|
239
|
+
" products"
|
|
240
|
+
] }) : /* @__PURE__ */ jsxRuntime.jsxs(ui.Text, { size: "small", className: "text-ui-fg-subtle", children: [
|
|
184
241
|
"Created ",
|
|
185
242
|
latest.products_created ?? 0,
|
|
186
243
|
" · Updated",
|
|
@@ -3,6 +3,13 @@ import { defineWidgetConfig } from "@medusajs/admin-sdk";
|
|
|
3
3
|
import { Container, Heading, Badge, Text, Button, toast } from "@medusajs/ui";
|
|
4
4
|
import { useState, useEffect } from "react";
|
|
5
5
|
const ATTENTION_STATES = /* @__PURE__ */ new Set(["failed", "canceled", "onhold"]);
|
|
6
|
+
const formatMinor = (amount, currency) => {
|
|
7
|
+
if (typeof amount !== "number" || !Number.isFinite(amount)) {
|
|
8
|
+
return null;
|
|
9
|
+
}
|
|
10
|
+
const code = typeof currency === "string" ? currency.toUpperCase() : "";
|
|
11
|
+
return `${(amount / 100).toFixed(2)}${code ? ` ${code}` : ""}`;
|
|
12
|
+
};
|
|
6
13
|
const PrintfulOrderWidget = ({ data }) => {
|
|
7
14
|
const metadata = data.metadata ?? {};
|
|
8
15
|
const printfulOrderId = metadata.printful_order_id;
|
|
@@ -13,6 +20,19 @@ const PrintfulOrderWidget = ({ data }) => {
|
|
|
13
20
|
const statusUpdatedAt = metadata.printful_status_updated_at;
|
|
14
21
|
const shipments = metadata.printful_shipments ?? [];
|
|
15
22
|
const isAttention = status ? ATTENTION_STATES.has(status) : false;
|
|
23
|
+
const costTotal = formatMinor(
|
|
24
|
+
metadata.printful_cost_total,
|
|
25
|
+
metadata.printful_cost_currency
|
|
26
|
+
);
|
|
27
|
+
const retailTotal = formatMinor(
|
|
28
|
+
metadata.printful_retail_total,
|
|
29
|
+
metadata.printful_retail_currency
|
|
30
|
+
);
|
|
31
|
+
const margin = formatMinor(
|
|
32
|
+
metadata.printful_margin,
|
|
33
|
+
metadata.printful_cost_currency
|
|
34
|
+
);
|
|
35
|
+
const currencyMismatch = costTotal !== null && retailTotal !== null && margin === null;
|
|
16
36
|
return /* @__PURE__ */ jsxs(Container, { className: "divide-y p-0", children: [
|
|
17
37
|
/* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between px-6 py-4", children: [
|
|
18
38
|
/* @__PURE__ */ jsx(Heading, { level: "h2", children: "Printful" }),
|
|
@@ -48,7 +68,23 @@ const PrintfulOrderWidget = ({ data }) => {
|
|
|
48
68
|
"Last synced: ",
|
|
49
69
|
new Date(statusUpdatedAt).toLocaleString()
|
|
50
70
|
] }) : null
|
|
51
|
-
] })
|
|
71
|
+
] }),
|
|
72
|
+
costTotal ? /* @__PURE__ */ jsxs("div", { className: "px-6 py-4 flex flex-col gap-2", children: [
|
|
73
|
+
/* @__PURE__ */ jsx(Text, { size: "small", weight: "plus", children: "Economics" }),
|
|
74
|
+
/* @__PURE__ */ jsxs(Text, { size: "small", className: "text-ui-fg-subtle", children: [
|
|
75
|
+
"Printful cost: ",
|
|
76
|
+
costTotal
|
|
77
|
+
] }),
|
|
78
|
+
retailTotal ? /* @__PURE__ */ jsxs(Text, { size: "small", className: "text-ui-fg-subtle", children: [
|
|
79
|
+
"Retail: ",
|
|
80
|
+
retailTotal
|
|
81
|
+
] }) : null,
|
|
82
|
+
margin ? /* @__PURE__ */ jsxs(Text, { size: "small", children: [
|
|
83
|
+
"Margin: ",
|
|
84
|
+
margin
|
|
85
|
+
] }) : null,
|
|
86
|
+
currencyMismatch ? /* @__PURE__ */ jsx(Text, { size: "small", className: "text-ui-fg-subtle", children: "Margin unavailable — Printful billed in a different currency than the order." }) : null
|
|
87
|
+
] }) : null
|
|
52
88
|
] });
|
|
53
89
|
};
|
|
54
90
|
defineWidgetConfig({
|
|
@@ -99,6 +135,13 @@ const PrintfulSyncWidget = () => {
|
|
|
99
135
|
void loadStatus();
|
|
100
136
|
void loadWebhook();
|
|
101
137
|
}, []);
|
|
138
|
+
useEffect(() => {
|
|
139
|
+
if (!(status == null ? void 0 : status.running)) {
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
const t = setInterval(() => void loadStatus(), 3e3);
|
|
143
|
+
return () => clearInterval(t);
|
|
144
|
+
}, [status == null ? void 0 : status.running]);
|
|
102
145
|
const onRegisterWebhook = async () => {
|
|
103
146
|
setRegistering(true);
|
|
104
147
|
try {
|
|
@@ -125,7 +168,6 @@ const PrintfulSyncWidget = () => {
|
|
|
125
168
|
}
|
|
126
169
|
};
|
|
127
170
|
const onSync = async () => {
|
|
128
|
-
var _a, _b, _c;
|
|
129
171
|
setSyncing(true);
|
|
130
172
|
try {
|
|
131
173
|
const res = await fetch("/admin/printful/sync", {
|
|
@@ -134,13 +176,20 @@ const PrintfulSyncWidget = () => {
|
|
|
134
176
|
headers: { "Content-Type": "application/json" },
|
|
135
177
|
body: JSON.stringify({})
|
|
136
178
|
});
|
|
179
|
+
if (res.status === 409) {
|
|
180
|
+
const body = await res.json();
|
|
181
|
+
toast.info("A Printful sync is already running", {
|
|
182
|
+
description: body.started_at ? `Started ${new Date(body.started_at).toLocaleString()}` : void 0
|
|
183
|
+
});
|
|
184
|
+
await loadStatus();
|
|
185
|
+
return;
|
|
186
|
+
}
|
|
137
187
|
if (!res.ok) {
|
|
138
188
|
const text = await res.text();
|
|
139
189
|
throw new Error(text || `Sync failed (${res.status})`);
|
|
140
190
|
}
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
description: `Created ${((_a = data.counters) == null ? void 0 : _a.created) ?? 0}, updated ${((_b = data.counters) == null ? void 0 : _b.updated) ?? 0}, failed ${((_c = data.counters) == null ? void 0 : _c.failed) ?? 0}`
|
|
191
|
+
toast.success("Printful sync started", {
|
|
192
|
+
description: "Progress will appear below as products are processed."
|
|
144
193
|
});
|
|
145
194
|
await loadStatus();
|
|
146
195
|
} catch (e) {
|
|
@@ -152,6 +201,7 @@ const PrintfulSyncWidget = () => {
|
|
|
152
201
|
}
|
|
153
202
|
};
|
|
154
203
|
const latest = status == null ? void 0 : status.latest_sync;
|
|
204
|
+
const running = (status == null ? void 0 : status.running) === true;
|
|
155
205
|
return /* @__PURE__ */ jsxs(Container, { className: "divide-y p-0", children: [
|
|
156
206
|
/* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between px-6 py-4", children: [
|
|
157
207
|
/* @__PURE__ */ jsxs("div", { children: [
|
|
@@ -163,9 +213,9 @@ const PrintfulSyncWidget = () => {
|
|
|
163
213
|
{
|
|
164
214
|
size: "small",
|
|
165
215
|
onClick: () => void onSync(),
|
|
166
|
-
isLoading: syncing,
|
|
167
|
-
disabled: loading || syncing,
|
|
168
|
-
children: "Sync Now"
|
|
216
|
+
isLoading: syncing || running,
|
|
217
|
+
disabled: loading || syncing || running,
|
|
218
|
+
children: running ? "Syncing…" : "Sync Now"
|
|
169
219
|
}
|
|
170
220
|
)
|
|
171
221
|
] }),
|
|
@@ -179,7 +229,14 @@ const PrintfulSyncWidget = () => {
|
|
|
179
229
|
new Date(latest.started_at).toLocaleString(),
|
|
180
230
|
latest.finished_at ? ` · Finished: ${new Date(latest.finished_at).toLocaleString()}` : ""
|
|
181
231
|
] }),
|
|
182
|
-
/* @__PURE__ */ jsxs(Text, { size: "small", className: "text-ui-fg-subtle", children: [
|
|
232
|
+
running ? /* @__PURE__ */ jsxs(Text, { size: "small", className: "text-ui-fg-subtle", children: [
|
|
233
|
+
"Processed ",
|
|
234
|
+
latest.products_processed ?? 0,
|
|
235
|
+
" of",
|
|
236
|
+
" ",
|
|
237
|
+
latest.products_total ?? 0,
|
|
238
|
+
" products"
|
|
239
|
+
] }) : /* @__PURE__ */ jsxs(Text, { size: "small", className: "text-ui-fg-subtle", children: [
|
|
183
240
|
"Created ",
|
|
184
241
|
latest.products_created ?? 0,
|
|
185
242
|
" · Updated",
|
|
@@ -4,22 +4,52 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
4
4
|
};
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
6
|
exports.POST = POST;
|
|
7
|
+
const utils_1 = require("@medusajs/framework/utils");
|
|
8
|
+
const printful_1 = require("../../../../modules/printful");
|
|
7
9
|
const sync_products_1 = __importDefault(require("../../../../workflows/sync-products"));
|
|
10
|
+
/**
|
|
11
|
+
* Start a catalog sync.
|
|
12
|
+
*
|
|
13
|
+
* The claim completes before this responds. Doing it inside the background
|
|
14
|
+
* step would let two requests both answer 202 before either reached the
|
|
15
|
+
* insert, which is exactly the race the unique index exists to prevent.
|
|
16
|
+
*/
|
|
8
17
|
async function POST(req, res) {
|
|
18
|
+
const printful = req.scope.resolve(printful_1.PRINTFUL_MODULE);
|
|
19
|
+
const logger = req.scope.resolve(utils_1.ContainerRegistrationKeys.LOGGER);
|
|
20
|
+
const options = await printful.getOptions();
|
|
9
21
|
const limit = typeof req.body === "object" &&
|
|
10
22
|
req.body &&
|
|
11
23
|
"limit" in req.body &&
|
|
12
24
|
req.body.limit != null
|
|
13
25
|
? Number(req.body.limit)
|
|
14
26
|
: undefined;
|
|
15
|
-
const
|
|
27
|
+
const claim = await printful.claimSyncLog(options.syncStaleMinutes ?? 60);
|
|
28
|
+
if (!claim) {
|
|
29
|
+
const running = await printful.getRunningSyncLog();
|
|
30
|
+
res.status(409).json({
|
|
31
|
+
message: "A Printful sync is already running",
|
|
32
|
+
running_sync_id: running?.id,
|
|
33
|
+
started_at: running?.started_at,
|
|
34
|
+
heartbeat_at: running?.heartbeat_at,
|
|
35
|
+
});
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
// Deliberately not awaited: the step is backgroundExecution, and the point
|
|
39
|
+
// of this release is that the admin gets an answer immediately rather than
|
|
40
|
+
// holding the request open for a 500-product catalog. The scheduled job in
|
|
41
|
+
// src/jobs/sync-products.ts DOES await the same workflow — it serves no HTTP
|
|
42
|
+
// response and wants the counters to log.
|
|
43
|
+
void (0, sync_products_1.default)(req.scope)
|
|
44
|
+
.run({
|
|
16
45
|
input: {
|
|
46
|
+
sync_log_id: claim.id,
|
|
17
47
|
limit: Number.isFinite(limit) ? limit : undefined,
|
|
18
48
|
},
|
|
49
|
+
})
|
|
50
|
+
.catch((err) => {
|
|
51
|
+
logger.error(`Printful sync ${claim.id} failed to start: ${err instanceof Error ? err.message : String(err)}`);
|
|
19
52
|
});
|
|
20
|
-
res.status(
|
|
21
|
-
sync_log: result.sync_log,
|
|
22
|
-
counters: result.counters,
|
|
23
|
-
});
|
|
53
|
+
res.status(202).json({ sync_id: claim.id });
|
|
24
54
|
}
|
|
25
|
-
//# sourceMappingURL=data:application/json;base64,
|
|
55
|
+
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoicm91dGUuanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi8uLi8uLi8uLi8uLi8uLi8uLi9zcmMvYXBpL2FkbWluL3ByaW50ZnVsL3N5bmMvcm91dGUudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6Ijs7Ozs7QUFhQSxvQkFpREM7QUE3REQscURBQXFFO0FBQ3JFLDJEQUE4RDtBQUU5RCx3RkFBc0U7QUFFdEU7Ozs7OztHQU1HO0FBQ0ksS0FBSyxVQUFVLElBQUksQ0FBQyxHQUFrQixFQUFFLEdBQW1CO0lBQ2hFLE1BQU0sUUFBUSxHQUEwQixHQUFHLENBQUMsS0FBSyxDQUFDLE9BQU8sQ0FBQywwQkFBZSxDQUFDLENBQUE7SUFDMUUsTUFBTSxNQUFNLEdBQXFDLEdBQUcsQ0FBQyxLQUFLLENBQUMsT0FBTyxDQUNoRSxpQ0FBeUIsQ0FBQyxNQUFNLENBQ2pDLENBQUE7SUFDRCxNQUFNLE9BQU8sR0FBRyxNQUFNLFFBQVEsQ0FBQyxVQUFVLEVBQUUsQ0FBQTtJQUUzQyxNQUFNLEtBQUssR0FDVCxPQUFPLEdBQUcsQ0FBQyxJQUFJLEtBQUssUUFBUTtRQUM1QixHQUFHLENBQUMsSUFBSTtRQUNSLE9BQU8sSUFBSSxHQUFHLENBQUMsSUFBSTtRQUNuQixHQUFHLENBQUMsSUFBSSxDQUFDLEtBQUssSUFBSSxJQUFJO1FBQ3BCLENBQUMsQ0FBQyxNQUFNLENBQUUsR0FBRyxDQUFDLElBQTJCLENBQUMsS0FBSyxDQUFDO1FBQ2hELENBQUMsQ0FBQyxTQUFTLENBQUE7SUFFZixNQUFNLEtBQUssR0FBRyxNQUFNLFFBQVEsQ0FBQyxZQUFZLENBQUMsT0FBTyxDQUFDLGdCQUFnQixJQUFJLEVBQUUsQ0FBQyxDQUFBO0lBRXpFLElBQUksQ0FBQyxLQUFLLEVBQUUsQ0FBQztRQUNYLE1BQU0sT0FBTyxHQUFHLE1BQU0sUUFBUSxDQUFDLGlCQUFpQixFQUFFLENBQUE7UUFDbEQsR0FBRyxDQUFDLE1BQU0sQ0FBQyxHQUFHLENBQUMsQ0FBQyxJQUFJLENBQUM7WUFDbkIsT0FBTyxFQUFFLG9DQUFvQztZQUM3QyxlQUFlLEVBQUUsT0FBTyxFQUFFLEVBQUU7WUFDNUIsVUFBVSxFQUFFLE9BQU8sRUFBRSxVQUFVO1lBQy9CLFlBQVksRUFBRSxPQUFPLEVBQUUsWUFBWTtTQUNwQyxDQUFDLENBQUE7UUFDRixPQUFNO0lBQ1IsQ0FBQztJQUVELDJFQUEyRTtJQUMzRSwyRUFBMkU7SUFDM0UsMkVBQTJFO0lBQzNFLDZFQUE2RTtJQUM3RSwwQ0FBMEM7SUFDMUMsS0FBSyxJQUFBLHVCQUFvQixFQUFDLEdBQUcsQ0FBQyxLQUFLLENBQUM7U0FDakMsR0FBRyxDQUFDO1FBQ0gsS0FBSyxFQUFFO1lBQ0wsV0FBVyxFQUFFLEtBQUssQ0FBQyxFQUFFO1lBQ3JCLEtBQUssRUFBRSxNQUFNLENBQUMsUUFBUSxDQUFDLEtBQUssQ0FBQyxDQUFDLENBQUMsQ0FBQyxLQUFLLENBQUMsQ0FBQyxDQUFDLFNBQVM7U0FDbEQ7S0FDRixDQUFDO1NBQ0QsS0FBSyxDQUFDLENBQUMsR0FBRyxFQUFFLEVBQUU7UUFDYixNQUFNLENBQUMsS0FBSyxDQUNWLGlCQUFpQixLQUFLLENBQUMsRUFBRSxxQkFDdkIsR0FBRyxZQUFZLEtBQUssQ0FBQyxDQUFDLENBQUMsR0FBRyxDQUFDLE9BQU8sQ0FBQyxDQUFDLENBQUMsTUFBTSxDQUFDLEdBQUcsQ0FDakQsRUFBRSxDQUNILENBQUE7SUFDSCxDQUFDLENBQUMsQ0FBQTtJQUVKLEdBQUcsQ0FBQyxNQUFNLENBQUMsR0FBRyxDQUFDLENBQUMsSUFBSSxDQUFDLEVBQUUsT0FBTyxFQUFFLEtBQUssQ0FBQyxFQUFFLEVBQUUsQ0FBQyxDQUFBO0FBQzdDLENBQUMifQ==
|
|
@@ -5,18 +5,32 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
6
|
exports.config = void 0;
|
|
7
7
|
exports.default = syncPrintfulProductsJob;
|
|
8
|
+
const utils_1 = require("@medusajs/framework/utils");
|
|
8
9
|
const sync_products_1 = __importDefault(require("../workflows/sync-products"));
|
|
10
|
+
const printful_1 = require("../modules/printful");
|
|
9
11
|
/**
|
|
10
12
|
* Optional scheduled full sync.
|
|
11
13
|
* Enable by ensuring jobs are loaded (default in plugins).
|
|
12
14
|
* Schedule: daily at 03:00 UTC — adjust as needed.
|
|
13
15
|
*/
|
|
14
16
|
async function syncPrintfulProductsJob(container) {
|
|
15
|
-
const logger = container.resolve(
|
|
17
|
+
const logger = container.resolve(utils_1.ContainerRegistrationKeys.LOGGER);
|
|
18
|
+
const printful = container.resolve(printful_1.PRINTFUL_MODULE);
|
|
19
|
+
const options = await printful.getOptions();
|
|
20
|
+
const claim = await printful.claimSyncLog(options.syncStaleMinutes ?? 60);
|
|
21
|
+
if (!claim) {
|
|
22
|
+
// A manual sync is already covering the catalog. Not a problem.
|
|
23
|
+
logger.info("Printful scheduled sync skipped: another sync is running");
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
16
26
|
logger.info("Printful scheduled sync starting");
|
|
17
27
|
try {
|
|
28
|
+
// Awaited on purpose, unlike the admin route in
|
|
29
|
+
// src/api/admin/printful/sync/route.ts. That route must answer 202 without
|
|
30
|
+
// waiting for a 500-product catalog; this job serves no HTTP response, and
|
|
31
|
+
// awaiting is what gives it counters worth logging.
|
|
18
32
|
const { result } = await (0, sync_products_1.default)(container).run({
|
|
19
|
-
input: {},
|
|
33
|
+
input: { sync_log_id: claim.id },
|
|
20
34
|
});
|
|
21
35
|
logger.info(`Printful scheduled sync done: created=${result.counters.created} updated=${result.counters.updated} failed=${result.counters.failed}`);
|
|
22
36
|
}
|
|
@@ -28,4 +42,4 @@ exports.config = {
|
|
|
28
42
|
name: "printful-sync-products",
|
|
29
43
|
schedule: "0 3 * * *",
|
|
30
44
|
};
|
|
31
|
-
//# sourceMappingURL=data:application/json;base64,
|
|
45
|
+
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoic3luYy1wcm9kdWN0cy5qcyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbIi4uLy4uLy4uLy4uL3NyYy9qb2JzL3N5bmMtcHJvZHVjdHMudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6Ijs7Ozs7O0FBV0EsMENBcUNDO0FBL0NELHFEQUFxRTtBQUNyRSwrRUFBNkQ7QUFDN0Qsa0RBQXFEO0FBR3JEOzs7O0dBSUc7QUFDWSxLQUFLLFVBQVUsdUJBQXVCLENBQ25ELFNBQTBCO0lBRTFCLE1BQU0sTUFBTSxHQUdSLFNBQVMsQ0FBQyxPQUFPLENBQUMsaUNBQXlCLENBQUMsTUFBTSxDQUFDLENBQUE7SUFFdkQsTUFBTSxRQUFRLEdBQTBCLFNBQVMsQ0FBQyxPQUFPLENBQUMsMEJBQWUsQ0FBQyxDQUFBO0lBQzFFLE1BQU0sT0FBTyxHQUFHLE1BQU0sUUFBUSxDQUFDLFVBQVUsRUFBRSxDQUFBO0lBRTNDLE1BQU0sS0FBSyxHQUFHLE1BQU0sUUFBUSxDQUFDLFlBQVksQ0FBQyxPQUFPLENBQUMsZ0JBQWdCLElBQUksRUFBRSxDQUFDLENBQUE7SUFDekUsSUFBSSxDQUFDLEtBQUssRUFBRSxDQUFDO1FBQ1gsZ0VBQWdFO1FBQ2hFLE1BQU0sQ0FBQyxJQUFJLENBQUMsMERBQTBELENBQUMsQ0FBQTtRQUN2RSxPQUFNO0lBQ1IsQ0FBQztJQUVELE1BQU0sQ0FBQyxJQUFJLENBQUMsa0NBQWtDLENBQUMsQ0FBQTtJQUMvQyxJQUFJLENBQUM7UUFDSCxnREFBZ0Q7UUFDaEQsMkVBQTJFO1FBQzNFLDJFQUEyRTtRQUMzRSxvREFBb0Q7UUFDcEQsTUFBTSxFQUFFLE1BQU0sRUFBRSxHQUFHLE1BQU0sSUFBQSx1QkFBb0IsRUFBQyxTQUFTLENBQUMsQ0FBQyxHQUFHLENBQUM7WUFDM0QsS0FBSyxFQUFFLEVBQUUsV0FBVyxFQUFFLEtBQUssQ0FBQyxFQUFFLEVBQUU7U0FDakMsQ0FBQyxDQUFBO1FBQ0YsTUFBTSxDQUFDLElBQUksQ0FDVCx5Q0FBeUMsTUFBTSxDQUFDLFFBQVEsQ0FBQyxPQUFPLFlBQVksTUFBTSxDQUFDLFFBQVEsQ0FBQyxPQUFPLFdBQVcsTUFBTSxDQUFDLFFBQVEsQ0FBQyxNQUFNLEVBQUUsQ0FDdkksQ0FBQTtJQUNILENBQUM7SUFBQyxPQUFPLEdBQUcsRUFBRSxDQUFDO1FBQ2IsTUFBTSxDQUFDLEtBQUssQ0FDVixtQ0FDRSxHQUFHLFlBQVksS0FBSyxDQUFDLENBQUMsQ0FBQyxHQUFHLENBQUMsT0FBTyxDQUFDLENBQUMsQ0FBQyxNQUFNLENBQUMsR0FBRyxDQUNqRCxFQUFFLENBQ0gsQ0FBQTtJQUNILENBQUM7QUFDSCxDQUFDO0FBRVksUUFBQSxNQUFNLEdBQUc7SUFDcEIsSUFBSSxFQUFFLHdCQUF3QjtJQUM5QixRQUFRLEVBQUUsV0FBVztDQUN0QixDQUFBIn0=
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.Migration20260802000000 = void 0;
|
|
4
|
+
const migrations_1 = require("@medusajs/framework/mikro-orm/migrations");
|
|
5
|
+
class Migration20260802000000 extends migrations_1.Migration {
|
|
6
|
+
async up() {
|
|
7
|
+
this.addSql(`
|
|
8
|
+
alter table "printful_sync_log"
|
|
9
|
+
add column if not exists "heartbeat_at" timestamptz null,
|
|
10
|
+
add column if not exists "products_processed" integer not null default 0,
|
|
11
|
+
add column if not exists "products_total" integer not null default 0;
|
|
12
|
+
`);
|
|
13
|
+
// At most one running sync, whatever the state of finished ones. Indexing
|
|
14
|
+
// the constant `true` under a status predicate is what makes the claim
|
|
15
|
+
// atomic: a second concurrent claim collides here rather than racing
|
|
16
|
+
// between a read and a write.
|
|
17
|
+
this.addSql(`
|
|
18
|
+
create unique index if not exists "IDX_printful_sync_log_one_running"
|
|
19
|
+
on "printful_sync_log" ((true))
|
|
20
|
+
where status = 'running' and deleted_at is null;
|
|
21
|
+
`);
|
|
22
|
+
this.addSql(`
|
|
23
|
+
create index if not exists "IDX_printful_sync_log_heartbeat"
|
|
24
|
+
on "printful_sync_log" ("heartbeat_at")
|
|
25
|
+
where status = 'running' and deleted_at is null;
|
|
26
|
+
`);
|
|
27
|
+
}
|
|
28
|
+
async down() {
|
|
29
|
+
this.addSql(`drop index if exists "IDX_printful_sync_log_heartbeat";`);
|
|
30
|
+
this.addSql(`drop index if exists "IDX_printful_sync_log_one_running";`);
|
|
31
|
+
this.addSql(`
|
|
32
|
+
alter table "printful_sync_log"
|
|
33
|
+
drop column if exists "products_total",
|
|
34
|
+
drop column if exists "products_processed",
|
|
35
|
+
drop column if exists "heartbeat_at";
|
|
36
|
+
`);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
exports.Migration20260802000000 = Migration20260802000000;
|
|
40
|
+
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiTWlncmF0aW9uMjAyNjA4MDIwMDAwMDAuanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi8uLi8uLi8uLi8uLi8uLi9zcmMvbW9kdWxlcy9wcmludGZ1bC9taWdyYXRpb25zL01pZ3JhdGlvbjIwMjYwODAyMDAwMDAwLnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiI7OztBQUFBLHlFQUFvRTtBQUVwRSxNQUFhLHVCQUF3QixTQUFRLHNCQUFTO0lBQzNDLEtBQUssQ0FBQyxFQUFFO1FBQ2YsSUFBSSxDQUFDLE1BQU0sQ0FBQzs7Ozs7S0FLWCxDQUFDLENBQUE7UUFFRiwwRUFBMEU7UUFDMUUsdUVBQXVFO1FBQ3ZFLHFFQUFxRTtRQUNyRSw4QkFBOEI7UUFDOUIsSUFBSSxDQUFDLE1BQU0sQ0FBQzs7OztLQUlYLENBQUMsQ0FBQTtRQUVGLElBQUksQ0FBQyxNQUFNLENBQUM7Ozs7S0FJWCxDQUFDLENBQUE7SUFDSixDQUFDO0lBRVEsS0FBSyxDQUFDLElBQUk7UUFDakIsSUFBSSxDQUFDLE1BQU0sQ0FBQyx5REFBeUQsQ0FBQyxDQUFBO1FBQ3RFLElBQUksQ0FBQyxNQUFNLENBQUMsMkRBQTJELENBQUMsQ0FBQTtRQUN4RSxJQUFJLENBQUMsTUFBTSxDQUFDOzs7OztLQUtYLENBQUMsQ0FBQTtJQUNKLENBQUM7Q0FDRjtBQXBDRCwwREFvQ0MifQ==
|
|
@@ -10,6 +10,9 @@ const PrintfulSyncLog = utils_1.model.define("printful_sync_log", {
|
|
|
10
10
|
products_updated: utils_1.model.number().default(0),
|
|
11
11
|
products_failed: utils_1.model.number().default(0),
|
|
12
12
|
error_message: utils_1.model.text().nullable(),
|
|
13
|
+
heartbeat_at: utils_1.model.dateTime().nullable(),
|
|
14
|
+
products_processed: utils_1.model.number().default(0),
|
|
15
|
+
products_total: utils_1.model.number().default(0),
|
|
13
16
|
});
|
|
14
17
|
exports.default = PrintfulSyncLog;
|
|
15
|
-
//# sourceMappingURL=data:application/json;base64,
|
|
18
|
+
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoicHJpbnRmdWwtc3luYy1sb2cuanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi8uLi8uLi8uLi8uLi8uLi9zcmMvbW9kdWxlcy9wcmludGZ1bC9tb2RlbHMvcHJpbnRmdWwtc3luYy1sb2cudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6Ijs7QUFBQSxxREFBaUQ7QUFFakQsTUFBTSxlQUFlLEdBQUcsYUFBSyxDQUFDLE1BQU0sQ0FBQyxtQkFBbUIsRUFBRTtJQUN4RCxFQUFFLEVBQUUsYUFBSyxDQUFDLEVBQUUsRUFBRSxDQUFDLFVBQVUsRUFBRTtJQUMzQixNQUFNLEVBQUUsYUFBSyxDQUFDLElBQUksRUFBRSxDQUFDLE9BQU8sQ0FBQyxTQUFTLENBQUM7SUFDdkMsVUFBVSxFQUFFLGFBQUssQ0FBQyxRQUFRLEVBQUU7SUFDNUIsV0FBVyxFQUFFLGFBQUssQ0FBQyxRQUFRLEVBQUUsQ0FBQyxRQUFRLEVBQUU7SUFDeEMsZ0JBQWdCLEVBQUUsYUFBSyxDQUFDLE1BQU0sRUFBRSxDQUFDLE9BQU8sQ0FBQyxDQUFDLENBQUM7SUFDM0MsZ0JBQWdCLEVBQUUsYUFBSyxDQUFDLE1BQU0sRUFBRSxDQUFDLE9BQU8sQ0FBQyxDQUFDLENBQUM7SUFDM0MsZUFBZSxFQUFFLGFBQUssQ0FBQyxNQUFNLEVBQUUsQ0FBQyxPQUFPLENBQUMsQ0FBQyxDQUFDO0lBQzFDLGFBQWEsRUFBRSxhQUFLLENBQUMsSUFBSSxFQUFFLENBQUMsUUFBUSxFQUFFO0lBQ3RDLFlBQVksRUFBRSxhQUFLLENBQUMsUUFBUSxFQUFFLENBQUMsUUFBUSxFQUFFO0lBQ3pDLGtCQUFrQixFQUFFLGFBQUssQ0FBQyxNQUFNLEVBQUUsQ0FBQyxPQUFPLENBQUMsQ0FBQyxDQUFDO0lBQzdDLGNBQWMsRUFBRSxhQUFLLENBQUMsTUFBTSxFQUFFLENBQUMsT0FBTyxDQUFDLENBQUMsQ0FBQztDQUMxQyxDQUFDLENBQUE7QUFFRixrQkFBZSxlQUFlLENBQUEifQ==
|
|
@@ -241,6 +241,72 @@ class PrintfulModuleService extends (0, utils_1.MedusaService)({
|
|
|
241
241
|
const logs = await this.listPrintfulSyncLogs({}, { take: 1, order: { started_at: "DESC" } });
|
|
242
242
|
return logs[0] ?? null;
|
|
243
243
|
}
|
|
244
|
+
/**
|
|
245
|
+
* Mark abandoned running syncs as failed.
|
|
246
|
+
*
|
|
247
|
+
* One statement whose predicate does the selecting, so there is no
|
|
248
|
+
* read-then-decide window. A row is abandoned when its heartbeat is older
|
|
249
|
+
* than the stale window, or when it never got one.
|
|
250
|
+
*/
|
|
251
|
+
async reapStaleSyncLogs(staleMinutes) {
|
|
252
|
+
const cutoff = new Date(Date.now() - staleMinutes * 60 * 1000);
|
|
253
|
+
const stale = await this.listPrintfulSyncLogs({
|
|
254
|
+
status: "running",
|
|
255
|
+
$or: [{ heartbeat_at: { $lt: cutoff } }, { heartbeat_at: null }],
|
|
256
|
+
});
|
|
257
|
+
for (const log of stale) {
|
|
258
|
+
await this.updatePrintfulSyncLogs({
|
|
259
|
+
id: log.id,
|
|
260
|
+
status: "failed",
|
|
261
|
+
error_message: "stale_running",
|
|
262
|
+
finished_at: new Date(),
|
|
263
|
+
});
|
|
264
|
+
}
|
|
265
|
+
return stale.length;
|
|
266
|
+
}
|
|
267
|
+
/**
|
|
268
|
+
* Claim the right to run a sync.
|
|
269
|
+
*
|
|
270
|
+
* Returns the new log on success, or null when another sync already holds
|
|
271
|
+
* the claim. The unique index on `status = 'running'` is what makes this
|
|
272
|
+
* atomic — a second concurrent claim collides in the database rather than
|
|
273
|
+
* racing between a read and a write.
|
|
274
|
+
*/
|
|
275
|
+
async claimSyncLog(staleMinutes) {
|
|
276
|
+
await this.reapStaleSyncLogs(staleMinutes);
|
|
277
|
+
const now = new Date();
|
|
278
|
+
try {
|
|
279
|
+
return await this.createPrintfulSyncLogs({
|
|
280
|
+
status: "running",
|
|
281
|
+
started_at: now,
|
|
282
|
+
heartbeat_at: now,
|
|
283
|
+
products_created: 0,
|
|
284
|
+
products_updated: 0,
|
|
285
|
+
products_failed: 0,
|
|
286
|
+
products_processed: 0,
|
|
287
|
+
products_total: 0,
|
|
288
|
+
});
|
|
289
|
+
}
|
|
290
|
+
catch (err) {
|
|
291
|
+
if (isUniqueViolation(err)) {
|
|
292
|
+
return null;
|
|
293
|
+
}
|
|
294
|
+
throw err;
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
/** The sync currently holding the claim, for a 409 body. */
|
|
298
|
+
async getRunningSyncLog() {
|
|
299
|
+
const [log] = await this.listPrintfulSyncLogs({ status: "running" });
|
|
300
|
+
return log ?? null;
|
|
301
|
+
}
|
|
302
|
+
/** Refresh the claim so it is not reaped, and report progress. */
|
|
303
|
+
async heartbeatSyncLog(id, progress = {}) {
|
|
304
|
+
return this.updatePrintfulSyncLogs({
|
|
305
|
+
id,
|
|
306
|
+
heartbeat_at: new Date(),
|
|
307
|
+
...progress,
|
|
308
|
+
});
|
|
309
|
+
}
|
|
244
310
|
}
|
|
245
311
|
exports.default = PrintfulModuleService;
|
|
246
|
-
//# sourceMappingURL=data:application/json;base64,
|
|
312
|
+
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoic2VydmljZS5qcyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbIi4uLy4uLy4uLy4uLy4uL3NyYy9tb2R1bGVzL3ByaW50ZnVsL3NlcnZpY2UudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6Ijs7Ozs7O0FBZ0RBLDRDQUlDO0FBYUQsZ0NBR0M7QUFpQkQsOENBMkJDO0FBaEhELHFEQUF5RDtBQUN6RCxtQ0FBbUM7QUFDbkMsaUVBQTREO0FBRTVELHVGQUE0RDtBQUM1RCwyRkFBZ0U7QUFDaEUsbUZBQXdEO0FBQ3hELDJGQUFnRTtBQUNoRSw2RkFBa0U7QUEyQmxFOzs7Ozs7R0FNRztBQUNVLFFBQUEseUJBQXlCLEdBQUcsU0FBUyxDQUFBO0FBRWxELGdFQUFnRTtBQUNuRCxRQUFBLG9CQUFvQixHQUFHLEVBQUUsQ0FBQTtBQUV0QyxrRUFBa0U7QUFDbEUsU0FBZ0IsZ0JBQWdCLENBQUMsUUFBZ0I7SUFDL0MsTUFBTSxZQUFZLEdBQUcsQ0FBQyxHQUFHLEVBQUUsR0FBRyxJQUFJLENBQUE7SUFDbEMsTUFBTSxTQUFTLEdBQUcsQ0FBQyxHQUFHLEVBQUUsR0FBRyxFQUFFLEdBQUcsSUFBSSxDQUFBO0lBQ3BDLE9BQU8sSUFBSSxDQUFDLEdBQUcsQ0FBQyxZQUFZLEdBQUcsSUFBSSxDQUFDLEdBQUcsQ0FBQyxDQUFDLEVBQUUsUUFBUSxDQUFDLEVBQUUsU0FBUyxDQUFDLENBQUE7QUFDbEUsQ0FBQztBQUVEOzs7Ozs7Ozs7O0dBVUc7QUFDSCxTQUFnQixVQUFVLENBQUMsZUFBdUI7SUFDaEQsTUFBTSxNQUFNLEdBQUcsSUFBQSxtQkFBVSxFQUFDLFFBQVEsQ0FBQyxDQUFDLE1BQU0sQ0FBQyxNQUFNLENBQUMsZUFBZSxDQUFDLENBQUMsQ0FBQyxNQUFNLEVBQUUsQ0FBQTtJQUM1RSxPQUFPLE1BQU0sQ0FBQyxXQUFXLENBQUMsQ0FBQyxDQUFDLENBQUE7QUFDOUIsQ0FBQztBQUVEOzs7Ozs7Ozs7Ozs7OztHQWNHO0FBQ0gsU0FBZ0IsaUJBQWlCLENBQUMsR0FB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
|