@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.
Files changed (104) hide show
  1. package/.medusa/server/src/__tests__/ingest-faire-order-support.spec.js +74 -0
  2. package/.medusa/server/src/__tests__/webhook.spec.js +77 -0
  3. package/.medusa/server/src/admin/index.js +960 -0
  4. package/.medusa/server/src/admin/index.mjs +959 -0
  5. package/.medusa/server/src/api/admin/faire/auth/authorize/route.js +12 -0
  6. package/.medusa/server/src/api/admin/faire/auth/callback/route.js +25 -0
  7. package/.medusa/server/src/api/admin/faire/auth/disconnect/route.js +12 -0
  8. package/.medusa/server/src/api/admin/faire/brand/route.js +14 -0
  9. package/.medusa/server/src/api/admin/faire/ingest/orders/[batchId]/route.js +27 -0
  10. package/.medusa/server/src/api/admin/faire/ingest/orders/route.js +20 -0
  11. package/.medusa/server/src/api/admin/faire/products/route.js +16 -0
  12. package/.medusa/server/src/api/admin/faire/settings/route.js +19 -0
  13. package/.medusa/server/src/api/admin/faire/status/product/[id]/route.js +30 -0
  14. package/.medusa/server/src/api/admin/faire/status/route.js +46 -0
  15. package/.medusa/server/src/api/admin/faire/sync/bulk/[batchId]/route.js +29 -0
  16. package/.medusa/server/src/api/admin/faire/sync/bulk/route.js +20 -0
  17. package/.medusa/server/src/api/admin/faire/sync/product/[id]/route.js +14 -0
  18. package/.medusa/server/src/api/admin/faire/syncs/[id]/route.js +32 -0
  19. package/.medusa/server/src/api/admin/faire/syncs/route.js +18 -0
  20. package/.medusa/server/src/api/webhooks/faire/route.js +125 -0
  21. package/.medusa/server/src/jobs/pull-faire-orders.js +32 -0
  22. package/.medusa/server/src/jobs/refresh-faire-token.js +25 -0
  23. package/.medusa/server/src/lib/faire-client.js +321 -0
  24. package/.medusa/server/src/lib/types.js +10 -0
  25. package/.medusa/server/src/lib/webhook.js +58 -0
  26. package/.medusa/server/src/links/product-faire-sync-link.js +57 -0
  27. package/.medusa/server/src/modules/faire-sync/index.js +15 -0
  28. package/.medusa/server/src/modules/faire-sync/migrations/Migration20260707211346.js +34 -0
  29. package/.medusa/server/src/modules/faire-sync/models/faire-order.js +21 -0
  30. package/.medusa/server/src/modules/faire-sync/models/faire-sync-account.js +17 -0
  31. package/.medusa/server/src/modules/faire-sync/models/faire-sync-batch.js +18 -0
  32. package/.medusa/server/src/modules/faire-sync/models/faire-sync-record.js +22 -0
  33. package/.medusa/server/src/modules/faire-sync/models/faire-sync-settings.js +26 -0
  34. package/.medusa/server/src/modules/faire-sync/models/faire-webhook-event.js +21 -0
  35. package/.medusa/server/src/modules/faire-sync/service.js +222 -0
  36. package/.medusa/server/src/subscribers/faire-inventory-sync.js +87 -0
  37. package/.medusa/server/src/subscribers/faire-listing-events.js +46 -0
  38. package/.medusa/server/src/subscribers/faire-order-ingest.js +54 -0
  39. package/.medusa/server/src/subscribers/faire-order-lifecycle.js +71 -0
  40. package/.medusa/server/src/subscribers/faire-product-status-sync.js +71 -0
  41. package/.medusa/server/src/workflows/index.js +21 -0
  42. package/.medusa/server/src/workflows/ingest-faire-order-support.js +76 -0
  43. package/.medusa/server/src/workflows/ingest-faire-order.js +148 -0
  44. package/.medusa/server/src/workflows/ingest-faire-orders-bulk.js +124 -0
  45. package/.medusa/server/src/workflows/refresh-faire-token.js +20 -0
  46. package/.medusa/server/src/workflows/sync-product-to-faire.js +262 -0
  47. package/.medusa/server/src/workflows/sync-products-to-faire.js +90 -0
  48. package/LICENSE +21 -0
  49. package/README.md +180 -0
  50. package/package.json +84 -0
  51. package/scripts/postbuild.js +36 -0
  52. package/src/__tests__/ingest-faire-order-support.spec.ts +77 -0
  53. package/src/__tests__/webhook.spec.ts +81 -0
  54. package/src/admin/lib/api.ts +88 -0
  55. package/src/admin/routes/settings/faire/[id]/page.tsx +184 -0
  56. package/src/admin/routes/settings/faire/hooks/use-faire-sync-columns.tsx +80 -0
  57. package/src/admin/routes/settings/faire/page.tsx +445 -0
  58. package/src/admin/routes/settings/faire/settings/page.tsx +286 -0
  59. package/src/admin/routes/settings/oauth/faire/callback/page.tsx +67 -0
  60. package/src/admin/widgets/faire-product-widget.tsx +180 -0
  61. package/src/api/admin/faire/auth/authorize/route.ts +10 -0
  62. package/src/api/admin/faire/auth/callback/route.ts +29 -0
  63. package/src/api/admin/faire/auth/disconnect/route.ts +10 -0
  64. package/src/api/admin/faire/brand/route.ts +12 -0
  65. package/src/api/admin/faire/ingest/orders/[batchId]/route.ts +31 -0
  66. package/src/api/admin/faire/ingest/orders/route.ts +29 -0
  67. package/src/api/admin/faire/products/route.ts +14 -0
  68. package/src/api/admin/faire/settings/route.ts +17 -0
  69. package/src/api/admin/faire/status/product/[id]/route.ts +30 -0
  70. package/src/api/admin/faire/status/route.ts +47 -0
  71. package/src/api/admin/faire/sync/bulk/[batchId]/route.ts +33 -0
  72. package/src/api/admin/faire/sync/bulk/route.ts +27 -0
  73. package/src/api/admin/faire/sync/product/[id]/route.ts +13 -0
  74. package/src/api/admin/faire/syncs/[id]/route.ts +32 -0
  75. package/src/api/admin/faire/syncs/route.ts +17 -0
  76. package/src/api/webhooks/faire/route.ts +133 -0
  77. package/src/jobs/pull-faire-orders.ts +30 -0
  78. package/src/jobs/refresh-faire-token.ts +23 -0
  79. package/src/lib/faire-client.ts +436 -0
  80. package/src/lib/types.ts +117 -0
  81. package/src/lib/webhook.ts +85 -0
  82. package/src/links/product-faire-sync-link.ts +56 -0
  83. package/src/modules/faire-sync/index.ts +11 -0
  84. package/src/modules/faire-sync/migrations/.snapshot-medusa-faire-sync.json +1486 -0
  85. package/src/modules/faire-sync/migrations/Migration20260707211346.ts +43 -0
  86. package/src/modules/faire-sync/models/faire-order.ts +20 -0
  87. package/src/modules/faire-sync/models/faire-sync-account.ts +16 -0
  88. package/src/modules/faire-sync/models/faire-sync-batch.ts +17 -0
  89. package/src/modules/faire-sync/models/faire-sync-record.ts +21 -0
  90. package/src/modules/faire-sync/models/faire-sync-settings.ts +25 -0
  91. package/src/modules/faire-sync/models/faire-webhook-event.ts +20 -0
  92. package/src/modules/faire-sync/service.ts +252 -0
  93. package/src/subscribers/faire-inventory-sync.ts +97 -0
  94. package/src/subscribers/faire-listing-events.ts +48 -0
  95. package/src/subscribers/faire-order-ingest.ts +61 -0
  96. package/src/subscribers/faire-order-lifecycle.ts +81 -0
  97. package/src/subscribers/faire-product-status-sync.ts +76 -0
  98. package/src/workflows/index.ts +4 -0
  99. package/src/workflows/ingest-faire-order-support.ts +96 -0
  100. package/src/workflows/ingest-faire-order.ts +212 -0
  101. package/src/workflows/ingest-faire-orders-bulk.ts +168 -0
  102. package/src/workflows/refresh-faire-token.ts +28 -0
  103. package/src/workflows/sync-product-to-faire.ts +375 -0
  104. 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,eyJ2ZXJzaW9uIjozLCJmaWxlIjoic3luYy1wcm9kdWN0cy10by1mYWlyZS5qcyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbIi4uLy4uLy4uLy4uL3NyYy93b3JrZmxvd3Mvc3luYy1wcm9kdWN0cy10by1mYWlyZS50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiOzs7QUFBQSxxRUFPMEM7QUFDMUMsc0RBQXlEO0FBRXpELG1FQUFvRTtBQUV2RCxRQUFBLHNCQUFzQixHQUFHLDBCQUEwQixDQUFBO0FBTWhFLDhFQUE4RTtBQUM5RSw2RUFBNkU7QUFDN0UsNEVBQTRFO0FBQzVFLG1EQUFtRDtBQUNuRCxNQUFNLGFBQWEsR0FBRyxHQUFHLENBQUE7QUFDekIsTUFBTSxZQUFZLEdBQUcsTUFBTSxDQUFBO0FBQzNCLE1BQU0sY0FBYyxHQUFHLENBQUMsQ0FBQTtBQUV4QixNQUFNLEtBQUssR0FBRyxDQUFDLEVBQVUsRUFBRSxFQUFFLENBQUMsSUFBSSxPQUFPLENBQUMsQ0FBQyxDQUFDLEVBQUUsRUFBRSxDQUFDLFVBQVUsQ0FBQyxDQUFDLEVBQUUsRUFBRSxDQUFDLENBQUMsQ0FBQTtBQUVuRSw4RUFBOEU7QUFFOUUsTUFBTSxhQUFhLEdBQUcsSUFBQSwwQkFBVSxFQUM5Qix1QkFBdUIsRUFDdkIsS0FBSyxFQUNILEtBQWdDLEVBQ2hDLEVBQUUsU0FBUyxFQUFFLE9BQU8sRUFBRSxFQUN1QixFQUFFO0lBQy9DLE1BQU0sT0FBTyxHQUFxQixTQUFTLENBQUMsT0FBTyxDQUFDLDhCQUFpQixDQUFDLENBQUE7SUFFdEUsTUFBTSxPQUFPLEdBQUcsTUFBTSxPQUFPLENBQUMsZ0JBQWdCLEVBQUUsQ0FBQTtJQUNoRCxJQUFJLENBQUMsT0FBTyxFQUFFLENBQUM7UUFDYixNQUFNLElBQUksS0FBSyxDQUFDLGdDQUFnQyxDQUFDLENBQUE7SUFDbkQsQ0FBQztJQUVELE1BQU0sS0FBSyxHQUFHLE1BQU0sT0FBTyxDQUFDLHNCQUFzQixDQUFDO1FBQ2pELGNBQWMsRUFBRSxPQUFPLENBQUMsYUFBYTtRQUNy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
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
+ }