@jytextiles/medusa-plugin-faire-store-sync 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.medusa/server/src/__tests__/ingest-faire-order-support.spec.js +74 -0
- package/.medusa/server/src/__tests__/webhook.spec.js +77 -0
- package/.medusa/server/src/admin/index.js +960 -0
- package/.medusa/server/src/admin/index.mjs +959 -0
- package/.medusa/server/src/api/admin/faire/auth/authorize/route.js +12 -0
- package/.medusa/server/src/api/admin/faire/auth/callback/route.js +25 -0
- package/.medusa/server/src/api/admin/faire/auth/disconnect/route.js +12 -0
- package/.medusa/server/src/api/admin/faire/brand/route.js +14 -0
- package/.medusa/server/src/api/admin/faire/ingest/orders/[batchId]/route.js +27 -0
- package/.medusa/server/src/api/admin/faire/ingest/orders/route.js +20 -0
- package/.medusa/server/src/api/admin/faire/products/route.js +16 -0
- package/.medusa/server/src/api/admin/faire/settings/route.js +19 -0
- package/.medusa/server/src/api/admin/faire/status/product/[id]/route.js +30 -0
- package/.medusa/server/src/api/admin/faire/status/route.js +46 -0
- package/.medusa/server/src/api/admin/faire/sync/bulk/[batchId]/route.js +29 -0
- package/.medusa/server/src/api/admin/faire/sync/bulk/route.js +20 -0
- package/.medusa/server/src/api/admin/faire/sync/product/[id]/route.js +14 -0
- package/.medusa/server/src/api/admin/faire/syncs/[id]/route.js +32 -0
- package/.medusa/server/src/api/admin/faire/syncs/route.js +18 -0
- package/.medusa/server/src/api/webhooks/faire/route.js +125 -0
- package/.medusa/server/src/jobs/pull-faire-orders.js +32 -0
- package/.medusa/server/src/jobs/refresh-faire-token.js +25 -0
- package/.medusa/server/src/lib/faire-client.js +321 -0
- package/.medusa/server/src/lib/types.js +10 -0
- package/.medusa/server/src/lib/webhook.js +58 -0
- package/.medusa/server/src/links/product-faire-sync-link.js +57 -0
- package/.medusa/server/src/modules/faire-sync/index.js +15 -0
- package/.medusa/server/src/modules/faire-sync/migrations/Migration20260707211346.js +34 -0
- package/.medusa/server/src/modules/faire-sync/models/faire-order.js +21 -0
- package/.medusa/server/src/modules/faire-sync/models/faire-sync-account.js +17 -0
- package/.medusa/server/src/modules/faire-sync/models/faire-sync-batch.js +18 -0
- package/.medusa/server/src/modules/faire-sync/models/faire-sync-record.js +22 -0
- package/.medusa/server/src/modules/faire-sync/models/faire-sync-settings.js +26 -0
- package/.medusa/server/src/modules/faire-sync/models/faire-webhook-event.js +21 -0
- package/.medusa/server/src/modules/faire-sync/service.js +222 -0
- package/.medusa/server/src/subscribers/faire-inventory-sync.js +87 -0
- package/.medusa/server/src/subscribers/faire-listing-events.js +46 -0
- package/.medusa/server/src/subscribers/faire-order-ingest.js +54 -0
- package/.medusa/server/src/subscribers/faire-order-lifecycle.js +71 -0
- package/.medusa/server/src/subscribers/faire-product-status-sync.js +71 -0
- package/.medusa/server/src/workflows/index.js +21 -0
- package/.medusa/server/src/workflows/ingest-faire-order-support.js +76 -0
- package/.medusa/server/src/workflows/ingest-faire-order.js +148 -0
- package/.medusa/server/src/workflows/ingest-faire-orders-bulk.js +124 -0
- package/.medusa/server/src/workflows/refresh-faire-token.js +20 -0
- package/.medusa/server/src/workflows/sync-product-to-faire.js +262 -0
- package/.medusa/server/src/workflows/sync-products-to-faire.js +90 -0
- package/LICENSE +21 -0
- package/README.md +180 -0
- package/package.json +84 -0
- package/scripts/postbuild.js +36 -0
- package/src/__tests__/ingest-faire-order-support.spec.ts +77 -0
- package/src/__tests__/webhook.spec.ts +81 -0
- package/src/admin/lib/api.ts +88 -0
- package/src/admin/routes/settings/faire/[id]/page.tsx +184 -0
- package/src/admin/routes/settings/faire/hooks/use-faire-sync-columns.tsx +80 -0
- package/src/admin/routes/settings/faire/page.tsx +445 -0
- package/src/admin/routes/settings/faire/settings/page.tsx +286 -0
- package/src/admin/routes/settings/oauth/faire/callback/page.tsx +67 -0
- package/src/admin/widgets/faire-product-widget.tsx +180 -0
- package/src/api/admin/faire/auth/authorize/route.ts +10 -0
- package/src/api/admin/faire/auth/callback/route.ts +29 -0
- package/src/api/admin/faire/auth/disconnect/route.ts +10 -0
- package/src/api/admin/faire/brand/route.ts +12 -0
- package/src/api/admin/faire/ingest/orders/[batchId]/route.ts +31 -0
- package/src/api/admin/faire/ingest/orders/route.ts +29 -0
- package/src/api/admin/faire/products/route.ts +14 -0
- package/src/api/admin/faire/settings/route.ts +17 -0
- package/src/api/admin/faire/status/product/[id]/route.ts +30 -0
- package/src/api/admin/faire/status/route.ts +47 -0
- package/src/api/admin/faire/sync/bulk/[batchId]/route.ts +33 -0
- package/src/api/admin/faire/sync/bulk/route.ts +27 -0
- package/src/api/admin/faire/sync/product/[id]/route.ts +13 -0
- package/src/api/admin/faire/syncs/[id]/route.ts +32 -0
- package/src/api/admin/faire/syncs/route.ts +17 -0
- package/src/api/webhooks/faire/route.ts +133 -0
- package/src/jobs/pull-faire-orders.ts +30 -0
- package/src/jobs/refresh-faire-token.ts +23 -0
- package/src/lib/faire-client.ts +436 -0
- package/src/lib/types.ts +117 -0
- package/src/lib/webhook.ts +85 -0
- package/src/links/product-faire-sync-link.ts +56 -0
- package/src/modules/faire-sync/index.ts +11 -0
- package/src/modules/faire-sync/migrations/.snapshot-medusa-faire-sync.json +1486 -0
- package/src/modules/faire-sync/migrations/Migration20260707211346.ts +43 -0
- package/src/modules/faire-sync/models/faire-order.ts +20 -0
- package/src/modules/faire-sync/models/faire-sync-account.ts +16 -0
- package/src/modules/faire-sync/models/faire-sync-batch.ts +17 -0
- package/src/modules/faire-sync/models/faire-sync-record.ts +21 -0
- package/src/modules/faire-sync/models/faire-sync-settings.ts +25 -0
- package/src/modules/faire-sync/models/faire-webhook-event.ts +20 -0
- package/src/modules/faire-sync/service.ts +252 -0
- package/src/subscribers/faire-inventory-sync.ts +97 -0
- package/src/subscribers/faire-listing-events.ts +48 -0
- package/src/subscribers/faire-order-ingest.ts +61 -0
- package/src/subscribers/faire-order-lifecycle.ts +81 -0
- package/src/subscribers/faire-product-status-sync.ts +76 -0
- package/src/workflows/index.ts +4 -0
- package/src/workflows/ingest-faire-order-support.ts +96 -0
- package/src/workflows/ingest-faire-order.ts +212 -0
- package/src/workflows/ingest-faire-orders-bulk.ts +168 -0
- package/src/workflows/refresh-faire-token.ts +28 -0
- package/src/workflows/sync-product-to-faire.ts +375 -0
- package/src/workflows/sync-products-to-faire.ts +133 -0
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.syncProductToFaireWorkflow = exports.SYNC_PRODUCT_TO_FAIRE = void 0;
|
|
4
|
+
const workflows_sdk_1 = require("@medusajs/framework/workflows-sdk");
|
|
5
|
+
const utils_1 = require("@medusajs/framework/utils");
|
|
6
|
+
const faire_sync_1 = require("../modules/faire-sync");
|
|
7
|
+
exports.SYNC_PRODUCT_TO_FAIRE = "faire-sync-product";
|
|
8
|
+
/**
|
|
9
|
+
* Faire doesn't expose a public storefront URL for every product the way Etsy
|
|
10
|
+
* does; fall back to the brand portal link if no url is present.
|
|
11
|
+
*/
|
|
12
|
+
const productDisplayUrl = (product) => {
|
|
13
|
+
if (product.url)
|
|
14
|
+
return product.url;
|
|
15
|
+
if (!product.product_token)
|
|
16
|
+
return null;
|
|
17
|
+
return `https://www.faire.com/brand/products/${product.product_token}`;
|
|
18
|
+
};
|
|
19
|
+
// ── Step 1: resolve account (fresh token) + settings ─────────────────────
|
|
20
|
+
const resolveFaireConfigStep = (0, workflows_sdk_1.createStep)("faire-resolve-config-step", async (_, { container }) => {
|
|
21
|
+
const service = container.resolve(faire_sync_1.FAIRE_SYNC_MODULE);
|
|
22
|
+
const account = await service.ensureFreshToken();
|
|
23
|
+
const settings = await service.getSettings();
|
|
24
|
+
return new workflows_sdk_1.StepResponse({
|
|
25
|
+
account_id: account.id,
|
|
26
|
+
brand_id: account.brand_id,
|
|
27
|
+
access_token: account.access_token,
|
|
28
|
+
currency: account.currency ?? null,
|
|
29
|
+
settings,
|
|
30
|
+
});
|
|
31
|
+
});
|
|
32
|
+
// ── Step 2: fetch + map product ──────────────────────────────────────────
|
|
33
|
+
const prepareProductStep = (0, workflows_sdk_1.createStep)("faire-prepare-product-step", async (input, { container }) => {
|
|
34
|
+
const query = container.resolve(utils_1.ContainerRegistrationKeys.QUERY);
|
|
35
|
+
const remoteLink = container.resolve(utils_1.ContainerRegistrationKeys.LINK);
|
|
36
|
+
const service = container.resolve(faire_sync_1.FAIRE_SYNC_MODULE);
|
|
37
|
+
const { data: products } = await query.graph({
|
|
38
|
+
entity: "product",
|
|
39
|
+
fields: [
|
|
40
|
+
"id",
|
|
41
|
+
"title",
|
|
42
|
+
"description",
|
|
43
|
+
"status",
|
|
44
|
+
"metadata",
|
|
45
|
+
"tags.*",
|
|
46
|
+
"variants.*",
|
|
47
|
+
"variants.prices.*",
|
|
48
|
+
"variants.inventory_items.*",
|
|
49
|
+
"images.*",
|
|
50
|
+
"variants.sku",
|
|
51
|
+
],
|
|
52
|
+
filters: { id: input.product_id },
|
|
53
|
+
});
|
|
54
|
+
const product = products?.[0];
|
|
55
|
+
if (!product) {
|
|
56
|
+
throw new Error(`Product ${input.product_id} not found`);
|
|
57
|
+
}
|
|
58
|
+
// Resolve existing Faire product (from link)
|
|
59
|
+
let existing_product_token = null;
|
|
60
|
+
const account = await service.getActiveAccount();
|
|
61
|
+
if (account) {
|
|
62
|
+
try {
|
|
63
|
+
const linkRows = await remoteLink.list({
|
|
64
|
+
[utils_1.Modules.PRODUCT]: { product_id: input.product_id },
|
|
65
|
+
[faire_sync_1.FAIRE_SYNC_MODULE]: { faire_sync_account_id: account.id },
|
|
66
|
+
});
|
|
67
|
+
const row = linkRows?.[0];
|
|
68
|
+
existing_product_token = row?.faire_product_token ?? null;
|
|
69
|
+
}
|
|
70
|
+
catch {
|
|
71
|
+
// no link yet
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
const settings = input.settings;
|
|
75
|
+
const metadata = product.metadata || {};
|
|
76
|
+
const variants = product.variants || [];
|
|
77
|
+
// Medusa v2 stores money as a whole decimal in the price's currency (NOT
|
|
78
|
+
// minor units). Faire wants integer cents, so round to cents.
|
|
79
|
+
const toCents = (amount) => Math.round(Number(amount || 0) * 100);
|
|
80
|
+
const markupPercent = Number(metadata.faire_wholesale_markup_percent ?? settings.default_wholesale_markup_percent) ||
|
|
81
|
+
0;
|
|
82
|
+
const buildVariant = (v) => {
|
|
83
|
+
const prices = v.prices || [];
|
|
84
|
+
const retail = prices[0] ?? prices.find(Boolean);
|
|
85
|
+
const retailCents = retail ? toCents(retail.amount) : 0;
|
|
86
|
+
const wholesaleCents = markupPercent > 0
|
|
87
|
+
? Math.round((retailCents * (100 - markupPercent)) / 100)
|
|
88
|
+
: retailCents;
|
|
89
|
+
return {
|
|
90
|
+
sku: v.sku || v.id,
|
|
91
|
+
name: v.title || undefined,
|
|
92
|
+
retail_price_cents: retailCents || undefined,
|
|
93
|
+
wholesale_price_cents: wholesaleCents || undefined,
|
|
94
|
+
inventory_count: Number(v.inventory_quantity) || 0,
|
|
95
|
+
};
|
|
96
|
+
};
|
|
97
|
+
// Aggregate retail/wholesale from min-variant for the product-level price.
|
|
98
|
+
const allPrices = variants.flatMap((v) => v.prices || []);
|
|
99
|
+
const minRetail = allPrices
|
|
100
|
+
.map((p) => toCents(p.amount))
|
|
101
|
+
.filter((a) => a > 0)
|
|
102
|
+
.sort((a, b) => a - b)[0] ?? 0;
|
|
103
|
+
const minWholesale = markupPercent > 0
|
|
104
|
+
? Math.round((minRetail * (100 - markupPercent)) / 100)
|
|
105
|
+
: minRetail;
|
|
106
|
+
const tags = (product.tags || [])
|
|
107
|
+
.map((t) => t.value)
|
|
108
|
+
.filter(Boolean);
|
|
109
|
+
const images = (product.images || [])
|
|
110
|
+
.map((img) => ({ url: img.url }))
|
|
111
|
+
.filter((i) => i.url);
|
|
112
|
+
const product_input = {
|
|
113
|
+
brand_id: input.brand_id,
|
|
114
|
+
name: (product.title || "Untitled Product").slice(0, 140),
|
|
115
|
+
description: product.description || "",
|
|
116
|
+
wholesale_price_cents: minWholesale || undefined,
|
|
117
|
+
retail_price_cents: minRetail || undefined,
|
|
118
|
+
images,
|
|
119
|
+
variants: variants.length ? variants.map(buildVariant) : undefined,
|
|
120
|
+
tags,
|
|
121
|
+
short_description: typeof metadata.faire_short_description === "string"
|
|
122
|
+
? metadata.faire_short_description
|
|
123
|
+
: undefined,
|
|
124
|
+
};
|
|
125
|
+
return new workflows_sdk_1.StepResponse({
|
|
126
|
+
product_id: input.product_id,
|
|
127
|
+
product_title: product.title,
|
|
128
|
+
product_status: product.status,
|
|
129
|
+
product_input,
|
|
130
|
+
existing_product_token,
|
|
131
|
+
});
|
|
132
|
+
});
|
|
133
|
+
// ── Step 3: create/update product + push inventory ───────────────────────
|
|
134
|
+
const syncProductStep = (0, workflows_sdk_1.createStep)("faire-sync-product-step", async (input, { container }) => {
|
|
135
|
+
const service = container.resolve(faire_sync_1.FAIRE_SYNC_MODULE);
|
|
136
|
+
const client = service.getClient();
|
|
137
|
+
const { access_token, settings } = input.config;
|
|
138
|
+
const { product_input, existing_product_token, product_status } = input.prepared;
|
|
139
|
+
const warnings = [];
|
|
140
|
+
let product;
|
|
141
|
+
try {
|
|
142
|
+
if (existing_product_token) {
|
|
143
|
+
product = await client.updateProduct(access_token, existing_product_token, {
|
|
144
|
+
name: product_input.name,
|
|
145
|
+
description: product_input.description,
|
|
146
|
+
images: product_input.images,
|
|
147
|
+
variants: product_input.variants,
|
|
148
|
+
tags: product_input.tags,
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
else {
|
|
152
|
+
product = await client.createProduct(access_token, product_input);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
catch (err) {
|
|
156
|
+
// Image fetch/upload failures shouldn't abort the whole sync.
|
|
157
|
+
warnings.push(`Product create/update failed: ${err?.message || err}`);
|
|
158
|
+
throw err;
|
|
159
|
+
}
|
|
160
|
+
// Push inventory levels for each variant.
|
|
161
|
+
if (product_input.variants?.length) {
|
|
162
|
+
try {
|
|
163
|
+
const levels = product_input.variants.map((v) => ({
|
|
164
|
+
sku: v.sku,
|
|
165
|
+
current_count: v.inventory_count ?? 0,
|
|
166
|
+
}));
|
|
167
|
+
await client.updateInventory(access_token, levels);
|
|
168
|
+
}
|
|
169
|
+
catch (err) {
|
|
170
|
+
warnings.push(`Inventory push failed: ${err?.message || err}`);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
// Publish decision: follow product status.
|
|
174
|
+
const shouldPublish = settings.follow_product_status !== false && product_status === "published";
|
|
175
|
+
const published = shouldPublish && product.state === "active";
|
|
176
|
+
return new workflows_sdk_1.StepResponse({
|
|
177
|
+
product,
|
|
178
|
+
published,
|
|
179
|
+
warnings,
|
|
180
|
+
});
|
|
181
|
+
}, async () => { });
|
|
182
|
+
// ── Step 4: persist sync record + link ───────────────────────────────────
|
|
183
|
+
const persistSyncResultStep = (0, workflows_sdk_1.createStep)("faire-persist-sync-result-step", async (input, { container }) => {
|
|
184
|
+
const service = container.resolve(faire_sync_1.FAIRE_SYNC_MODULE);
|
|
185
|
+
const remoteLink = container.resolve(utils_1.ContainerRegistrationKeys.LINK);
|
|
186
|
+
const { config, prepared, result } = input;
|
|
187
|
+
const status = result.published
|
|
188
|
+
? "success"
|
|
189
|
+
: result.product.state === "draft"
|
|
190
|
+
? "draft"
|
|
191
|
+
: "success";
|
|
192
|
+
await service.createSyncRecord({
|
|
193
|
+
product_id: prepared.product_id,
|
|
194
|
+
account_id: config.account_id,
|
|
195
|
+
product_token: result.product.product_token,
|
|
196
|
+
product_url: productDisplayUrl(result.product),
|
|
197
|
+
product_state: result.product.state,
|
|
198
|
+
action: prepared.existing_product_token ? "update" : "create",
|
|
199
|
+
status,
|
|
200
|
+
published: result.published,
|
|
201
|
+
error_message: result.warnings.length ? result.warnings.join(" | ") : null,
|
|
202
|
+
warnings: result.warnings,
|
|
203
|
+
metadata: { title: prepared.product_title },
|
|
204
|
+
synced_at: new Date(),
|
|
205
|
+
});
|
|
206
|
+
try {
|
|
207
|
+
await remoteLink.dismiss([
|
|
208
|
+
{
|
|
209
|
+
[utils_1.Modules.PRODUCT]: { product_id: prepared.product_id },
|
|
210
|
+
[faire_sync_1.FAIRE_SYNC_MODULE]: { faire_sync_account_id: config.account_id },
|
|
211
|
+
},
|
|
212
|
+
]);
|
|
213
|
+
}
|
|
214
|
+
catch {
|
|
215
|
+
// ignore dismiss errors
|
|
216
|
+
}
|
|
217
|
+
await remoteLink.create([
|
|
218
|
+
{
|
|
219
|
+
[utils_1.Modules.PRODUCT]: { product_id: prepared.product_id },
|
|
220
|
+
[faire_sync_1.FAIRE_SYNC_MODULE]: { faire_sync_account_id: config.account_id },
|
|
221
|
+
data: {
|
|
222
|
+
faire_product_token: result.product.product_token,
|
|
223
|
+
faire_url: productDisplayUrl(result.product),
|
|
224
|
+
sync_status: "synced",
|
|
225
|
+
last_synced_at: new Date(),
|
|
226
|
+
sync_error: result.warnings.length ? result.warnings.join(" | ") : null,
|
|
227
|
+
metadata: {
|
|
228
|
+
published: result.published,
|
|
229
|
+
state: result.product.state,
|
|
230
|
+
product_status: prepared.product_status,
|
|
231
|
+
},
|
|
232
|
+
},
|
|
233
|
+
},
|
|
234
|
+
]);
|
|
235
|
+
return new workflows_sdk_1.StepResponse();
|
|
236
|
+
});
|
|
237
|
+
// ── Workflow ─────────────────────────────────────────────────────────────
|
|
238
|
+
exports.syncProductToFaireWorkflow = (0, workflows_sdk_1.createWorkflow)(exports.SYNC_PRODUCT_TO_FAIRE, (input) => {
|
|
239
|
+
const config = resolveFaireConfigStep();
|
|
240
|
+
const prepared = prepareProductStep((0, workflows_sdk_1.transform)({ input, config }, (data) => ({
|
|
241
|
+
product_id: data.input.product_id,
|
|
242
|
+
settings: data.config.settings,
|
|
243
|
+
brand_id: data.config.brand_id,
|
|
244
|
+
})));
|
|
245
|
+
const result = syncProductStep((0, workflows_sdk_1.transform)({ config, prepared }, (data) => ({
|
|
246
|
+
config: data.config,
|
|
247
|
+
prepared: data.prepared,
|
|
248
|
+
})));
|
|
249
|
+
persistSyncResultStep((0, workflows_sdk_1.transform)({ config, prepared, result }, (data) => ({
|
|
250
|
+
config: data.config,
|
|
251
|
+
prepared: data.prepared,
|
|
252
|
+
result: data.result,
|
|
253
|
+
})));
|
|
254
|
+
return new workflows_sdk_1.WorkflowResponse({
|
|
255
|
+
product_token: result.product.product_token,
|
|
256
|
+
product_url: productDisplayUrl(result.product),
|
|
257
|
+
published: result.published,
|
|
258
|
+
state: result.product.state,
|
|
259
|
+
warnings: result.warnings,
|
|
260
|
+
});
|
|
261
|
+
});
|
|
262
|
+
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoic3luYy1wcm9kdWN0LXRvLWZhaXJlLmpzIiwic291cmNlUm9vdCI6IiIsInNvdXJjZXMiOlsiLi4vLi4vLi4vLi4vc3JjL3dvcmtmbG93cy9zeW5jLXByb2R1Y3QtdG8tZmFpcmUudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6Ijs7O0FBQUEscUVBTzBDO0FBQzFDLHFEQUE4RTtBQUc5RSxzREFBeUQ7QUFPNUMsUUFBQSxxQkFBcUIsR0FBRyxvQkFBb0IsQ0FBQTtBQTRCekQ7OztHQUdHO0FBQ0gsTUFBTSxpQkFBaUIsR0FBRyxDQUFDLE9BQXdCLEVBQWlCLEVBQUU7SUFDcEUsSUFBSSxPQUFPLENBQUMsR0FBRztRQUFFLE9BQU8sT0FBTyxDQUFDLEdBQUcsQ0FBQTtJQUNuQyxJQUFJLENBQUMsT0FBTyxDQUFDLGFBQWE7UUFBRSxPQUFPLElBQUksQ0FBQTtJQUN2QyxPQUFPLHdDQUF3QyxPQUFPLENBQUMsYUFBYSxFQUFFLENBQUE7QUFDeEUsQ0FBQyxDQUFBO0FBRUQsNEVBQTRFO0FBRTVFLE1BQU0sc0JBQXNCLEdBQUcsSUFBQSwwQkFBVSxFQUN2QywyQkFBMkIsRUFDM0IsS0FBSyxFQUFFLENBQUMsRUFBRSxFQUFFLFNBQVMsRUFBRSxFQUF5QyxFQUFFO0lBQ2hFLE1BQU0sT0FBTyxHQUFxQixTQUFTLENBQUMsT0FBTyxDQUFDLDhCQUFpQixDQUFDLENBQUE7SUFDdEUsTUFBTSxPQUFPLEdBQUcsTUFBTSxPQUFPLENBQUMsZ0JBQWdCLEVBQUUsQ0FBQTtJQUNoRCxNQUFNLFFBQVEsR0FBRyxNQUFNLE9BQU8sQ0FBQyxXQUFXLEVBQUUsQ0FBQTtJQUM1QyxPQUFPLElBQUksNEJBQVksQ0FBQztRQUN0QixVQUFVLEVBQUUsT0FBTyxDQUFDLEVBQUU7UUFDdEIsUUFBUSxFQUFFLE9BQU8sQ0FBQyxRQUFRO1FBQzFCLFlBQVksRUFBRSxPQUFPLENBQUMsWUFBWTtRQUNsQyxRQUFRLEVBQUUsT0FBTyxDQUFDLFFBQVEsSUFBSSxJQUFJO1FBQ2xDLFFBQVE7S0FD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
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.syncProductsToFaireWorkflow = exports.SYNC_PRODUCTS_TO_FAIRE = void 0;
|
|
4
|
+
const workflows_sdk_1 = require("@medusajs/framework/workflows-sdk");
|
|
5
|
+
const faire_sync_1 = require("../modules/faire-sync");
|
|
6
|
+
const sync_product_to_faire_1 = require("./sync-product-to-faire");
|
|
7
|
+
exports.SYNC_PRODUCTS_TO_FAIRE = "faire-sync-products-bulk";
|
|
8
|
+
// Faire enforces a modest per-second request budget and a daily call ceiling.
|
|
9
|
+
// A single product sync fans out to several calls (create + inventory push),
|
|
10
|
+
// so we pace *products* conservatively and let the client's 429 retry-after
|
|
11
|
+
// backoff absorb bursts over the per-second limit.
|
|
12
|
+
const BASE_DELAY_MS = 600;
|
|
13
|
+
const MAX_DELAY_MS = 10_000;
|
|
14
|
+
const PROGRESS_EVERY = 5;
|
|
15
|
+
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
16
|
+
// ── Step 1 (inline): open the batch so the HTTP caller gets an id to poll ──
|
|
17
|
+
const openBatchStep = (0, workflows_sdk_1.createStep)("faire-open-batch-step", async (input, { container, context }) => {
|
|
18
|
+
const service = container.resolve(faire_sync_1.FAIRE_SYNC_MODULE);
|
|
19
|
+
const account = await service.getActiveAccount();
|
|
20
|
+
if (!account) {
|
|
21
|
+
throw new Error("Faire account is not connected");
|
|
22
|
+
}
|
|
23
|
+
const batch = await service.createFaireSyncBatches({
|
|
24
|
+
transaction_id: context.transactionId,
|
|
25
|
+
status: "processing",
|
|
26
|
+
total_products: input.product_ids.length,
|
|
27
|
+
synced_count: 0,
|
|
28
|
+
failed_count: 0,
|
|
29
|
+
error_log: {},
|
|
30
|
+
started_at: new Date(),
|
|
31
|
+
});
|
|
32
|
+
return new workflows_sdk_1.StepResponse({ batch_id: batch.id });
|
|
33
|
+
});
|
|
34
|
+
// ── Step 2 (async, background): process every product with backoff ─────────
|
|
35
|
+
const processBatchStep = (0, workflows_sdk_1.createStep)("faire-process-batch-step", async (input, { container }) => {
|
|
36
|
+
const service = container.resolve(faire_sync_1.FAIRE_SYNC_MODULE);
|
|
37
|
+
let synced = 0;
|
|
38
|
+
let failed = 0;
|
|
39
|
+
let delay = BASE_DELAY_MS;
|
|
40
|
+
const errors = {};
|
|
41
|
+
const persistProgress = async () => {
|
|
42
|
+
await service
|
|
43
|
+
.updateFaireSyncBatches({
|
|
44
|
+
id: input.batch_id,
|
|
45
|
+
synced_count: synced,
|
|
46
|
+
failed_count: failed,
|
|
47
|
+
error_log: errors,
|
|
48
|
+
})
|
|
49
|
+
.catch(() => { });
|
|
50
|
+
};
|
|
51
|
+
for (let i = 0; i < input.product_ids.length; i++) {
|
|
52
|
+
const product_id = input.product_ids[i];
|
|
53
|
+
try {
|
|
54
|
+
await (0, sync_product_to_faire_1.syncProductToFaireWorkflow)(container).run({ input: { product_id } });
|
|
55
|
+
synced++;
|
|
56
|
+
delay = Math.max(BASE_DELAY_MS, Math.floor(delay / 2));
|
|
57
|
+
}
|
|
58
|
+
catch (err) {
|
|
59
|
+
failed++;
|
|
60
|
+
errors[product_id] = err?.message || "Unknown error";
|
|
61
|
+
if (/429|rate limit/i.test(errors[product_id])) {
|
|
62
|
+
delay = Math.min(delay * 2, MAX_DELAY_MS);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
if ((i + 1) % PROGRESS_EVERY === 0) {
|
|
66
|
+
await persistProgress();
|
|
67
|
+
}
|
|
68
|
+
if (i < input.product_ids.length - 1) {
|
|
69
|
+
await sleep(delay);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
await service.updateFaireSyncBatches({
|
|
73
|
+
id: input.batch_id,
|
|
74
|
+
status: failed === input.product_ids.length ? "failed" : "completed",
|
|
75
|
+
synced_count: synced,
|
|
76
|
+
failed_count: failed,
|
|
77
|
+
error_log: errors,
|
|
78
|
+
completed_at: new Date(),
|
|
79
|
+
});
|
|
80
|
+
return new workflows_sdk_1.StepResponse({ synced, failed });
|
|
81
|
+
});
|
|
82
|
+
exports.syncProductsToFaireWorkflow = (0, workflows_sdk_1.createWorkflow)(exports.SYNC_PRODUCTS_TO_FAIRE, (input) => {
|
|
83
|
+
const opened = openBatchStep(input);
|
|
84
|
+
processBatchStep((0, workflows_sdk_1.transform)({ input, opened }, (data) => ({
|
|
85
|
+
batch_id: data.opened.batch_id,
|
|
86
|
+
product_ids: data.input.product_ids,
|
|
87
|
+
}))).config({ async: true, backgroundExecution: true });
|
|
88
|
+
return new workflows_sdk_1.WorkflowResponse({ batch_id: opened.batch_id });
|
|
89
|
+
});
|
|
90
|
+
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoic3luYy1wcm9kdWN0cy10by1mYWlyZS5qcyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbIi4uLy4uLy4uLy4uL3NyYy93b3JrZmxvd3Mvc3luYy1wcm9kdWN0cy10by1mYWlyZS50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiOzs7QUFBQSxxRUFPMEM7QUFDMUMsc0RBQXlEO0FBRXpELG1FQUFvRTtBQUV2RCxRQUFBLHNCQUFzQixHQUFHLDBCQUEwQixDQUFBO0FBTWhFLDhFQUE4RTtBQUM5RSw2RUFBNkU7QUFDN0UsNEVBQTRFO0FBQzVFLG1EQUFtRDtBQUNuRCxNQUFNLGFBQWEsR0FBRyxHQUFHLENBQUE7QUFDekIsTUFBTSxZQUFZLEdBQUcsTUFBTSxDQUFBO0FBQzNCLE1BQU0sY0FBYyxHQUFHLENBQUMsQ0FBQTtBQUV4QixNQUFNLEtBQUssR0FBRyxDQUFDLEVBQVUsRUFBRSxFQUFFLENBQUMsSUFBSSxPQUFPLENBQUMsQ0FBQyxDQUFDLEVBQUUsRUFBRSxDQUFDLFVBQVUsQ0FBQyxDQUFDLEVBQUUsRUFBRSxDQUFDLENBQUMsQ0FBQTtBQUVuRSw4RUFBOEU7QUFFOUUsTUFBTSxhQUFhLEdBQUcsSUFBQSwwQkFBVSxFQUM5Qix1QkFBdUIsRUFDdkIsS0FBSyxFQUNILEtBQWdDLEVBQ2hDLEVBQUUsU0FBUyxFQUFFLE9BQU8sRUFBRSxFQUN1QixFQUFFO0lBQy9DLE1BQU0sT0FBTyxHQUFxQixTQUFTLENBQUMsT0FBTyxDQUFDLDhCQUFpQixDQUFDLENBQUE7SUFFdEUsTUFBTSxPQUFPLEdBQUcsTUFBTSxPQUFPLENBQUMsZ0JBQWdCLEVBQUUsQ0FBQTtJQUNoRCxJQUFJLENBQUMsT0FBTyxFQUFFLENBQUM7UUFDYixNQUFNLElBQUksS0FBSyxDQUFDLGdDQUFnQyxDQUFDLENBQUE7SUFDbkQsQ0FBQztJQUVELE1BQU0sS0FBSyxHQUFHLE1BQU0sT0FBTyxDQUFDLHNCQUFzQixDQUFDO1FBQ2pELGNBQWMsRUFBRSxPQUFPLENBQUMsYUFBYTtRQUNyQyxNQUFNLEVBQUUsWUFBWTtRQUNwQixjQUFjLEVBQUUsS0FBSyxDQUFDLFdBQVcsQ0FBQyxNQUFNO1FBQ3hDLFlBQVksRUFBRSxDQUFDO1FBQ2YsWUFBWSxFQUFFLENBQUM7UUFDZixTQUFTLEVBQUUsRUFBRTtRQUNiLFVBQVUsRUFBRSxJQUFJLElBQUksRUFBRTtLQUNoQixDQUFDLENBQUE7SUFFVCxPQUFPLElBQUksNEJBQVksQ0FBQyxFQUFFLFFBQVEsRUFBRyxLQUFhLENBQUMsRUFBRSxFQUFFLENBQUMsQ0FBQTtBQUMxRCxDQUFDLENBQ0YsQ0FBQTtBQUVELDhFQUE4RTtBQUU5RSxNQUFNLGdCQUFnQixHQUFHLElBQUEsMEJBQVUsRUFDakMsMEJBQTBCLEVBQzFCLEtBQUssRUFDSCxLQUFrRCxFQUNsRCxFQUFFLFNBQVMsRUFBRSxFQUM4QyxFQUFFO0lBQzdELE1BQU0sT0FBTyxHQUFxQixTQUFTLENBQUMsT0FBTyxDQUFDLDhCQUFpQixDQUFDLENBQUE7SUFFdEUsSUFBSSxNQUFNLEdBQUcsQ0FBQyxDQUFBO0lBQ2QsSUFBSSxNQUFNLEdBQUcsQ0FBQyxDQUFBO0lBQ2QsSUFBSSxLQUFLLEdBQUcsYUFBYSxDQUFBO0lBQ3pCLE1BQU0sTUFBTSxHQUEyQixFQUFFLENBQUE7SUFFekMsTUFBTSxlQUFlLEdBQUcsS0FBSyxJQUFJLEVBQUU7UUFDakMsTUFBTSxPQUFPO2FBQ1Ysc0JBQXNCLENBQUM7WUFDdEIsRUFBRSxFQUFFLEtBQUssQ0FBQyxRQUFRO1lBQ2xCLFlBQVksRUFBRSxNQUFNO1lBQ3BCLFlBQVksRUFBRSxNQUFNO1lBQ3BCLFNBQVMsRUFBRSxNQUFNO1NBQ1gsQ0FBQzthQUNSLEtBQUssQ0FBQyxHQUFHLEVBQUUsR0FBRSxDQUFDLENBQUMsQ0FBQTtJQUNwQixDQUFDLENBQUE7SUFFRCxLQUFLLElBQUksQ0FBQyxHQUFHLENBQUMsRUFBRSxDQUFDLEdBQUcsS0FBSyxDQUFDLFdBQVcsQ0FBQyxNQUFNLEVBQUUsQ0FBQyxFQUFFLEVBQUUsQ0FBQztRQUNsRCxNQUFNLFVBQVUsR0FBRyxLQUFLLENBQUMsV0FBVyxDQUFDLENBQUMsQ0FBQyxDQUFBO1FBQ3ZDLElBQUksQ0FBQztZQUNILE1BQU0sSUFBQSxrREFBMEIsRUFBQyxTQUFTLENBQUMsQ0FBQyxHQUFHLENBQUMsRUFBRSxLQUFLLEVBQUUsRUFBRSxVQUFVLEVBQUUsRUFBRSxDQUFDLENBQUE7WUFDMUUsTUFBTSxFQUFFLENBQUE7WUFDUixLQUFLLEdBQUcsSUFBSSxDQUFDLEdBQUcsQ0FBQyxhQUFhLEVBQUUsSUFBSSxDQUFDLEtBQUssQ0FBQyxLQUFLLEdBQUcsQ0FBQyxDQUFDLENBQUMsQ0FBQTtRQUN4RCxDQUFDO1FBQUMsT0FBTyxHQUFRLEVBQUUsQ0FBQztZQUNsQixNQUFNLEVBQUUsQ0FBQTtZQUNSLE1BQU0sQ0FBQyxVQUFVLENBQUMsR0FBRyxHQUFHLEVBQUUsT0FBTyxJQUFJLGVBQWUsQ0FBQTtZQUNwRCxJQUFJLGlCQUFpQixDQUFDLElBQUksQ0FBQyxNQUFNLENBQUMsVUFBVSxDQUFDLENBQUMsRUFBRSxDQUFDO2dCQUMvQyxLQUFLLEdBQUcsSUFBSSxDQUFDLEdBQUcsQ0FBQyxLQUFLLEdBQUcsQ0FBQyxFQUFFLFlBQVksQ0FBQyxDQUFBO1lBQzNDLENBQUM7UUFDSCxDQUFDO1FBRUQsSUFBSSxDQUFDLENBQUMsR0FBRyxDQUFDLENBQUMsR0FBRyxjQUFjLEtBQUssQ0FBQyxFQUFFLENBQUM7WUFDbkMsTUFBTSxlQUFlLEVBQUUsQ0FBQTtRQUN6QixDQUFDO1FBQ0QsSUFBSSxDQUFDLEdBQUcsS0FBSyxDQUFDLFdBQVcsQ0FBQyxNQUFNLEdBQUcsQ0FBQyxFQUFFLENBQUM7WUFDckMsTUFBTSxLQUFLLENBQUMsS0FBSyxDQUFDLENBQUE7UUFDcEIsQ0FBQztJQUNILENBQUM7SUFFRCxNQUFNLE9BQU8sQ0FBQyxzQkFBc0IsQ0FBQztRQUNuQyxFQUFFLEVBQUUsS0FBSyxDQUFDLFFBQVE7UUFDbEIsTUFBTSxFQUFFLE1BQU0sS0FBSyxLQUFLLENBQUMsV0FBVyxDQUFDLE1BQU0sQ0FBQyxDQUFDLENBQUMsUUFBUSxDQUFDLENBQUMsQ0FBQyxXQUFXO1FBQ3BFLFlBQVksRUFBRSxNQUFNO1FBQ3BCLFlBQVksRUFBRSxNQUFNO1FBQ3BCLFNBQVMsRUFBRSxNQUFNO1FBQ2pCLFlBQVksRUFBRSxJQUFJLElBQUksRUFBRTtLQUNsQixDQUFDLENBQUE7SUFFVCxPQUFPLElBQUksNEJBQVksQ0FBQyxFQUFFLE1BQU0sRUFBRSxNQUFNLEVBQUUsQ0FBQyxDQUFBO0FBQzdDLENBQUMsQ0FDRixDQUFBO0FBRVksUUFBQSwyQkFBMkIsR0FBRyxJQUFBLDhCQUFjLEVBQ3ZELDhCQUFzQixFQUN0QixDQUFDLEtBQTZDLEVBQUUsRUFBRTtJQUNoRCxNQUFNLE1BQU0sR0FBRyxhQUFhLENBQUMsS0FBSyxDQUFDLENBQUE7SUFFbkMsZ0JBQWdCLENBQ2QsSUFBQSx5QkFBUyxFQUFDLEVBQUUsS0FBSyxFQUFFLE1BQU0sRUFBRSxFQUFFLENBQUMsSUFBSSxFQUFFLEVBQUUsQ0FBQyxDQUFDO1FBQ3RDLFFBQVEsRUFBRSxJQUFJLENBQUMsTUFBTSxDQUFDLFFBQVE7UUFDOUIsV0FBVyxFQUFFLElBQUksQ0FBQyxLQUFLLENBQUMsV0FBVztLQUNwQyxDQUFDLENBQUMsQ0FDSixDQUFDLE1BQU0sQ0FBQyxFQUFFLEtBQUssRUFBRSxJQUFJLEVBQUUsbUJBQW1CLEVBQUUsSUFBSSxFQUFFLENBQUMsQ0FBQTtJQUVwRCxPQUFPLElBQUksZ0NBQWdCLENBQUMsRUFBRSxRQUFRLEVBQUUsTUFBTSxDQUFDLFFBQVEsRUFBRSxDQUFDLENBQUE7QUFDNUQsQ0FBQyxDQUNGLENBQUEifQ==
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Jaal Yantra Textiles
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
# @jytextiles/medusa-plugin-faire-store-sync
|
|
2
|
+
|
|
3
|
+
A [Medusa v2](https://medusajs.com) plugin that **bidirectionally** syncs your
|
|
4
|
+
Medusa products, inventory and orders with a [Faire](https://www.faire.com)
|
|
5
|
+
brand account via the [Faire External Platform API](https://developer.faire.com)
|
|
6
|
+
(OAuth 2.0). It handles the OAuth connect flow, product push (create/update +
|
|
7
|
+
inventory), bulk product sync, bulk order ingestion, and inbound Faire webhooks
|
|
8
|
+
for order/inventory/product events.
|
|
9
|
+
|
|
10
|
+
> ⚠️ **Note on API specifics.** Faire's developer portal is access-gated, so
|
|
11
|
+
> this plugin's endpoint paths, payload shapes and webhook signature scheme
|
|
12
|
+
> reflect the publicly documented v2 surface and standard OAuth2 conventions.
|
|
13
|
+
> If your Faire app uses a different base URL, token header, or signing scheme,
|
|
14
|
+
> override via the options/env vars below — the client is intentionally
|
|
15
|
+
> permissive. Verify against your Faire app's documentation before going live.
|
|
16
|
+
|
|
17
|
+
## Features
|
|
18
|
+
|
|
19
|
+
- **OAuth 2.0 redirect connect flow** — connect/disconnect a Faire brand from
|
|
20
|
+
**Settings → Faire** in the Admin; tokens are stored and auto-refreshed when
|
|
21
|
+
applicable.
|
|
22
|
+
- **Product → Faire sync** — create/update Faire products, push variant
|
|
23
|
+
inventory, upload images via URL (single or bulk).
|
|
24
|
+
- **Faire → Medusa order ingestion** — bulk-pull Faire orders and create
|
|
25
|
+
matching Medusa orders (idempotent, payment marked captured).
|
|
26
|
+
- **Readiness checks** — surfaces what's missing (brand, wholesale-pricing
|
|
27
|
+
strategy, shipping policy) before publishing.
|
|
28
|
+
- **Sync records** — every sync is tracked per product with status and the
|
|
29
|
+
remote Faire product token.
|
|
30
|
+
- **Long-running bulk workflows** — both bulk product push and bulk order pull
|
|
31
|
+
run as background workflows (`backgroundExecution`) with a pollable batch id.
|
|
32
|
+
- **Event-driven** — inbound Faire webhooks (order placed/canceled/shipped,
|
|
33
|
+
inventory updated, product updated) are verified, recorded idempotently and
|
|
34
|
+
re-emitted as Medusa events; `product.updated` re-syncs linked products when
|
|
35
|
+
their status actually changes.
|
|
36
|
+
|
|
37
|
+
## Requirements
|
|
38
|
+
|
|
39
|
+
- Medusa **2.17.x**
|
|
40
|
+
- Node.js **>= 22**
|
|
41
|
+
- A Faire app (client id + client secret) from the Faire developer portal, with
|
|
42
|
+
the OAuth redirect URL registered.
|
|
43
|
+
|
|
44
|
+
## Install
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
npm install @jytextiles/medusa-plugin-faire-store-sync
|
|
48
|
+
# or
|
|
49
|
+
yarn add @jytextiles/medusa-plugin-faire-store-sync
|
|
50
|
+
# or
|
|
51
|
+
pnpm add @jytextiles/medusa-plugin-faire-store-sync
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
## Configure
|
|
55
|
+
|
|
56
|
+
Register the plugin in `medusa-config.ts`:
|
|
57
|
+
|
|
58
|
+
```ts
|
|
59
|
+
module.exports = defineConfig({
|
|
60
|
+
// ...
|
|
61
|
+
plugins: [
|
|
62
|
+
{
|
|
63
|
+
resolve: "@jytextiles/medusa-plugin-faire-store-sync",
|
|
64
|
+
options: {
|
|
65
|
+
clientId: process.env.FAIRE_CLIENT_ID,
|
|
66
|
+
clientSecret: process.env.FAIRE_CLIENT_SECRET,
|
|
67
|
+
redirectUri:
|
|
68
|
+
process.env.FAIRE_REDIRECT_URI ??
|
|
69
|
+
"http://localhost:9000/app/settings/oauth/faire/callback",
|
|
70
|
+
webhookSecret: process.env.FAIRE_WEBHOOK_SECRET,
|
|
71
|
+
// Optional overrides (defaults shown):
|
|
72
|
+
// apiBase: "https://www.faire.com/api/v2",
|
|
73
|
+
// authUrl: "https://www.faire.com/oauth/authorize",
|
|
74
|
+
// tokenUrl: "https://www.faire.com/oauth/token",
|
|
75
|
+
},
|
|
76
|
+
},
|
|
77
|
+
],
|
|
78
|
+
})
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
Options can be supplied via the `options` object above **or** the matching
|
|
82
|
+
environment variables (env takes precedence):
|
|
83
|
+
|
|
84
|
+
| Option | Env var | Required | Description |
|
|
85
|
+
| --------------- | --------------------- | -------- | ------------------------------------------------------------------ |
|
|
86
|
+
| `clientId` | `FAIRE_CLIENT_ID` | yes | Your Faire app's client id. |
|
|
87
|
+
| `clientSecret` | `FAIRE_CLIENT_SECRET` | yes | Your Faire app's client secret. |
|
|
88
|
+
| `redirectUri` | `FAIRE_REDIRECT_URI` | no | OAuth callback URL. Must match your Faire app's registered redirect. |
|
|
89
|
+
| `webhookSecret` | `FAIRE_WEBHOOK_SECRET`| no | Signing secret for verifying inbound Faire webhooks. |
|
|
90
|
+
| `apiBase` | `FAIRE_API_BASE` | no | Override the Faire API base URL (e.g. for sandbox). |
|
|
91
|
+
| `authUrl` | `FAIRE_AUTH_URL` | no | Override the OAuth authorize URL. |
|
|
92
|
+
| `tokenUrl` | `FAIRE_TOKEN_URL` | no | Override the OAuth token URL. |
|
|
93
|
+
|
|
94
|
+
After installing, run your project's migrations so the plugin's tables are
|
|
95
|
+
created:
|
|
96
|
+
|
|
97
|
+
```bash
|
|
98
|
+
npx medusa db:migrate
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
### Webhook raw body
|
|
102
|
+
|
|
103
|
+
The host app's `src/api/middlewares.ts` must preserve the raw request body for
|
|
104
|
+
the Faire webhook path so the signature can be verified:
|
|
105
|
+
|
|
106
|
+
```ts
|
|
107
|
+
import { defineMiddlewares } from "@medusajs/framework/http"
|
|
108
|
+
|
|
109
|
+
export default defineMiddlewares({
|
|
110
|
+
routes: [
|
|
111
|
+
{
|
|
112
|
+
matcher: "/webhooks/faire",
|
|
113
|
+
method: "POST",
|
|
114
|
+
middlewares: [/* preserveRawBody — see your Medusa version's helper */],
|
|
115
|
+
},
|
|
116
|
+
],
|
|
117
|
+
})
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
Register the webhook URL (`https://your-host/webhooks/faire`) and copy the
|
|
121
|
+
signing secret into `FAIRE_WEBHOOK_SECRET` when you create the webhook via the
|
|
122
|
+
Faire API.
|
|
123
|
+
|
|
124
|
+
### Optional behavior flags
|
|
125
|
+
|
|
126
|
+
| Env var | Default | Effect |
|
|
127
|
+
| ----------------------------- | ------- | ----------------------------------------------------------- |
|
|
128
|
+
| `FAIRE_INGEST_ORDERS` | `true` | Set `false` to disable creating Medusa orders from Faire. |
|
|
129
|
+
| `FAIRE_DECREMENT_INVENTORY` | `false` | Set `true` to decrement Medusa stock on Faire order ingest. |
|
|
130
|
+
| `FAIRE_AUTO_INGEST_ORDERS` | `true` | Set `false` to disable the scheduled order-pull job. |
|
|
131
|
+
|
|
132
|
+
## Usage
|
|
133
|
+
|
|
134
|
+
1. In the Admin, go to **Settings → Faire** and click **Connect** to authorize
|
|
135
|
+
your Faire brand.
|
|
136
|
+
2. Configure sync settings (brand, wholesale markup %, shipping policy).
|
|
137
|
+
3. From a product page, use the **Faire Sync** widget to sync the product, or
|
|
138
|
+
sync in bulk from the Faire settings page (Bulk operations card).
|
|
139
|
+
4. Pull Faire orders into Medusa with the **Start order pull** button (also runs
|
|
140
|
+
on a 30-minute schedule).
|
|
141
|
+
|
|
142
|
+
### Admin API routes
|
|
143
|
+
|
|
144
|
+
| Method | Route | Purpose |
|
|
145
|
+
| ------ | ---------------------------------- | -------------------------------------------------- |
|
|
146
|
+
| GET | `/admin/faire/status` | Connection + readiness status |
|
|
147
|
+
| GET/POST | `/admin/faire/settings` | Read / save sync settings |
|
|
148
|
+
| GET | `/admin/faire/auth/authorize` | Start the OAuth2 flow |
|
|
149
|
+
| POST | `/admin/faire/auth/callback` | OAuth2 callback (called by the SPA) |
|
|
150
|
+
| POST | `/admin/faire/auth/disconnect` | Disconnect the brand |
|
|
151
|
+
| POST | `/admin/faire/sync/product/:id` | Sync a single product |
|
|
152
|
+
| POST | `/admin/faire/sync/bulk` | Sync multiple products (background workflow, 202) |
|
|
153
|
+
| GET | `/admin/faire/sync/bulk/:batchId` | Poll bulk product-sync progress |
|
|
154
|
+
| POST | `/admin/faire/ingest/orders` | Pull Faire orders (background workflow, 202) |
|
|
155
|
+
| GET | `/admin/faire/ingest/orders/:batchId` | Poll bulk order-ingest progress |
|
|
156
|
+
| GET | `/admin/faire/syncs` | List sync records |
|
|
157
|
+
| GET/POST | `/admin/faire/syncs/:id` | Fetch / retry a sync record |
|
|
158
|
+
| GET | `/admin/faire/brand` | The connected Faire brand |
|
|
159
|
+
| GET | `/admin/faire/products` | Products already on Faire |
|
|
160
|
+
| POST | `/webhooks/faire` | Inbound Faire webhook receiver |
|
|
161
|
+
|
|
162
|
+
## Development
|
|
163
|
+
|
|
164
|
+
```bash
|
|
165
|
+
# Build the plugin (compiles to .medusa/server, then restructures via postbuild)
|
|
166
|
+
npm run build
|
|
167
|
+
|
|
168
|
+
# Watch mode while developing against a local Medusa app
|
|
169
|
+
npm run dev
|
|
170
|
+
|
|
171
|
+
# Generate a migration after changing a model
|
|
172
|
+
npm run db:generate
|
|
173
|
+
|
|
174
|
+
# Run tests
|
|
175
|
+
npm run test
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
## License
|
|
179
|
+
|
|
180
|
+
[MIT](./LICENSE)
|
package/package.json
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@jytextiles/medusa-plugin-faire-store-sync",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"publishConfig": {
|
|
5
|
+
"access": "public",
|
|
6
|
+
"registry": "https://registry.npmjs.org/"
|
|
7
|
+
},
|
|
8
|
+
"description": "Medusa v2 plugin to bidirectionally sync Medusa products, inventory and orders with a Faire brand account via the Faire External Platform API (OAuth 2.0).",
|
|
9
|
+
"keywords": [
|
|
10
|
+
"medusa-v2",
|
|
11
|
+
"medusa-plugin-integration",
|
|
12
|
+
"medusa-plugin-other",
|
|
13
|
+
"faire",
|
|
14
|
+
"medusa",
|
|
15
|
+
"plugin"
|
|
16
|
+
],
|
|
17
|
+
"license": "MIT",
|
|
18
|
+
"author": "Jaal Yantra Textiles",
|
|
19
|
+
"homepage": "https://github.com/Jaal-Yantra-Textiles/v2/tree/main/packages/medusa-plugin-faire-store-sync#readme",
|
|
20
|
+
"repository": {
|
|
21
|
+
"type": "git",
|
|
22
|
+
"url": "git+https://github.com/Jaal-Yantra-Textiles/v2.git",
|
|
23
|
+
"directory": "packages/medusa-plugin-faire-store-sync"
|
|
24
|
+
},
|
|
25
|
+
"bugs": {
|
|
26
|
+
"url": "https://github.com/Jaal-Yantra-Textiles/v2/issues"
|
|
27
|
+
},
|
|
28
|
+
"scripts": {
|
|
29
|
+
"build": "medusa plugin:build",
|
|
30
|
+
"dev": "medusa plugin:develop",
|
|
31
|
+
"db:generate": "medusa plugin:db:generate",
|
|
32
|
+
"test": "jest"
|
|
33
|
+
},
|
|
34
|
+
"exports": {
|
|
35
|
+
"./package.json": "./package.json",
|
|
36
|
+
"./workflows": "./.medusa/server/src/workflows/index.js",
|
|
37
|
+
"./.medusa/server/src/modules/*": "./.medusa/server/src/modules/*/index.js",
|
|
38
|
+
"./providers/*": "./.medusa/server/src/providers/*/index.js",
|
|
39
|
+
"./admin": {
|
|
40
|
+
"import": "./.medusa/server/src/admin/index.mjs",
|
|
41
|
+
"require": "./.medusa/server/src/admin/index.js",
|
|
42
|
+
"default": "./.medusa/server/src/admin/index.js"
|
|
43
|
+
},
|
|
44
|
+
"./*": "./.medusa/server/src/*.js"
|
|
45
|
+
},
|
|
46
|
+
"files": [
|
|
47
|
+
"src",
|
|
48
|
+
".medusa",
|
|
49
|
+
"scripts"
|
|
50
|
+
],
|
|
51
|
+
"devDependencies": {
|
|
52
|
+
"@medusajs/admin-sdk": "2.17.2",
|
|
53
|
+
"@medusajs/cli": "2.17.2",
|
|
54
|
+
"@medusajs/dashboard": "2.17.2",
|
|
55
|
+
"@medusajs/framework": "2.17.2",
|
|
56
|
+
"@medusajs/medusa": "2.17.2",
|
|
57
|
+
"@medusajs/test-utils": "2.17.2",
|
|
58
|
+
"@medusajs/ui": "4.1.19",
|
|
59
|
+
"@medusajs/icons": "2.17.2",
|
|
60
|
+
"@medusajs/types": "2.17.2",
|
|
61
|
+
"@swc/core": "1.5.7",
|
|
62
|
+
"@swc/jest": "0.2.37",
|
|
63
|
+
"@types/jest": "29.5.14",
|
|
64
|
+
"@types/node": "^20.0.0",
|
|
65
|
+
"@types/react": "^18.3.2",
|
|
66
|
+
"@types/react-dom": "^18.2.25",
|
|
67
|
+
"jest": "29.7.0",
|
|
68
|
+
"typescript": "5.6.3"
|
|
69
|
+
},
|
|
70
|
+
"peerDependencies": {
|
|
71
|
+
"@medusajs/admin-sdk": "2.17.2",
|
|
72
|
+
"@medusajs/cli": "2.17.2",
|
|
73
|
+
"@medusajs/dashboard": "2.17.2",
|
|
74
|
+
"@medusajs/framework": "2.17.2",
|
|
75
|
+
"@medusajs/medusa": "2.17.2",
|
|
76
|
+
"@medusajs/test-utils": "2.17.2",
|
|
77
|
+
"@medusajs/ui": "4.1.19",
|
|
78
|
+
"@medusajs/icons": "2.17.2",
|
|
79
|
+
"@medusajs/types": "2.17.2",
|
|
80
|
+
"react": "^18.3.1",
|
|
81
|
+
"react-dom": "^18.3.1",
|
|
82
|
+
"react-router-dom": "6.30.4"
|
|
83
|
+
}
|
|
84
|
+
}
|