@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,17 @@
|
|
|
1
|
+
import { MedusaRequest, MedusaResponse } from "@medusajs/framework/http"
|
|
2
|
+
import { FAIRE_SYNC_MODULE } from "../../../../modules/faire-sync"
|
|
3
|
+
import FaireSyncService from "../../../../modules/faire-sync/service"
|
|
4
|
+
|
|
5
|
+
// GET /admin/faire/settings
|
|
6
|
+
export const GET = async (req: MedusaRequest, res: MedusaResponse) => {
|
|
7
|
+
const service: FaireSyncService = req.scope.resolve(FAIRE_SYNC_MODULE)
|
|
8
|
+
const settings = await service.getSettings()
|
|
9
|
+
res.json({ settings })
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
// POST /admin/faire/settings
|
|
13
|
+
export const POST = async (req: MedusaRequest, res: MedusaResponse) => {
|
|
14
|
+
const service: FaireSyncService = req.scope.resolve(FAIRE_SYNC_MODULE)
|
|
15
|
+
const settings = await service.updateSettings(req.body || {})
|
|
16
|
+
res.json({ settings })
|
|
17
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { MedusaRequest, MedusaResponse } from "@medusajs/framework/http"
|
|
2
|
+
import { FAIRE_SYNC_MODULE } from "../../../../../../modules/faire-sync"
|
|
3
|
+
import FaireSyncService from "../../../../../../modules/faire-sync/service"
|
|
4
|
+
|
|
5
|
+
// GET /admin/faire/status/product/:id — latest sync state for a single product.
|
|
6
|
+
export const GET = async (req: MedusaRequest, res: MedusaResponse) => {
|
|
7
|
+
const { id } = req.params
|
|
8
|
+
const service: FaireSyncService = req.scope.resolve(FAIRE_SYNC_MODULE)
|
|
9
|
+
|
|
10
|
+
const connected = !!(await service.getActiveAccount())
|
|
11
|
+
const [records] = await service.listSyncRecords({ product_id: id }, 1, 0)
|
|
12
|
+
const latest: any = records?.[0] ?? null
|
|
13
|
+
|
|
14
|
+
res.json({
|
|
15
|
+
connected,
|
|
16
|
+
synced: !!latest,
|
|
17
|
+
latest: latest
|
|
18
|
+
? {
|
|
19
|
+
id: latest.id,
|
|
20
|
+
status: latest.status,
|
|
21
|
+
product_token: latest.product_token,
|
|
22
|
+
product_url: latest.product_url,
|
|
23
|
+
product_state: latest.product_state,
|
|
24
|
+
published: latest.published,
|
|
25
|
+
error_message: latest.error_message,
|
|
26
|
+
synced_at: latest.synced_at,
|
|
27
|
+
}
|
|
28
|
+
: null,
|
|
29
|
+
})
|
|
30
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { MedusaRequest, MedusaResponse } from "@medusajs/framework/http"
|
|
2
|
+
import { FAIRE_SYNC_MODULE } from "../../../../modules/faire-sync"
|
|
3
|
+
import FaireSyncService from "../../../../modules/faire-sync/service"
|
|
4
|
+
|
|
5
|
+
// GET /admin/faire/status — connection status + readiness checklist
|
|
6
|
+
export const GET = async (req: MedusaRequest, res: MedusaResponse) => {
|
|
7
|
+
const service: FaireSyncService = req.scope.resolve(FAIRE_SYNC_MODULE)
|
|
8
|
+
const account = await service.getActiveAccount()
|
|
9
|
+
const settings = await service.getSettings()
|
|
10
|
+
|
|
11
|
+
const connected = !!account && account.is_active
|
|
12
|
+
const hasBrand = !!settings.default_brand_id
|
|
13
|
+
const hasMarkup =
|
|
14
|
+
settings.default_wholesale_markup_percent != null &&
|
|
15
|
+
settings.default_wholesale_markup_percent > 0
|
|
16
|
+
const hasShipping = !!settings.default_shipping_policy_id
|
|
17
|
+
|
|
18
|
+
res.json({
|
|
19
|
+
connected,
|
|
20
|
+
account: account
|
|
21
|
+
? {
|
|
22
|
+
id: account.id,
|
|
23
|
+
brand_id: account.brand_id,
|
|
24
|
+
brand_name: account.brand_name,
|
|
25
|
+
currency: account.currency,
|
|
26
|
+
token_expires_at: account.token_expires_at,
|
|
27
|
+
}
|
|
28
|
+
: null,
|
|
29
|
+
settings: {
|
|
30
|
+
default_brand_id: settings.default_brand_id,
|
|
31
|
+
default_wholesale_markup_percent: settings.default_wholesale_markup_percent,
|
|
32
|
+
default_min_order_quantity: settings.default_min_order_quantity,
|
|
33
|
+
default_lead_time_days: settings.default_lead_time_days,
|
|
34
|
+
default_shipping_policy_id: settings.default_shipping_policy_id,
|
|
35
|
+
default_category: settings.default_category,
|
|
36
|
+
auto_publish: settings.auto_publish,
|
|
37
|
+
follow_product_status: settings.follow_product_status,
|
|
38
|
+
},
|
|
39
|
+
readiness: {
|
|
40
|
+
connected,
|
|
41
|
+
brand: hasBrand,
|
|
42
|
+
wholesale_pricing: hasMarkup,
|
|
43
|
+
shipping_policy: hasShipping,
|
|
44
|
+
ready_to_publish: connected && hasBrand,
|
|
45
|
+
},
|
|
46
|
+
})
|
|
47
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { MedusaRequest, MedusaResponse } from "@medusajs/framework/http"
|
|
2
|
+
import { MedusaError } from "@medusajs/framework/utils"
|
|
3
|
+
import { FAIRE_SYNC_MODULE } from "../../../../../../modules/faire-sync"
|
|
4
|
+
import FaireSyncService from "../../../../../../modules/faire-sync/service"
|
|
5
|
+
|
|
6
|
+
// GET /admin/faire/sync/bulk/:batchId — poll the progress of a background batch
|
|
7
|
+
export const GET = async (req: MedusaRequest, res: MedusaResponse) => {
|
|
8
|
+
const { batchId } = req.params
|
|
9
|
+
const service: FaireSyncService = req.scope.resolve(FAIRE_SYNC_MODULE)
|
|
10
|
+
|
|
11
|
+
const batch = await service
|
|
12
|
+
.retrieveFaireSyncBatch(batchId)
|
|
13
|
+
.catch(() => null)
|
|
14
|
+
|
|
15
|
+
if (!batch) {
|
|
16
|
+
throw new MedusaError(MedusaError.Types.NOT_FOUND, "Batch not found")
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const total = (batch as any).total_products || 0
|
|
20
|
+
const done =
|
|
21
|
+
((batch as any).synced_count || 0) + ((batch as any).failed_count || 0)
|
|
22
|
+
|
|
23
|
+
res.json({
|
|
24
|
+
batch,
|
|
25
|
+
progress: {
|
|
26
|
+
total,
|
|
27
|
+
done,
|
|
28
|
+
pct: total ? Math.round((done / total) * 100) : 0,
|
|
29
|
+
finished:
|
|
30
|
+
(batch as any).status === "completed" || (batch as any).status === "failed",
|
|
31
|
+
},
|
|
32
|
+
})
|
|
33
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { MedusaRequest, MedusaResponse } from "@medusajs/framework/http"
|
|
2
|
+
import { MedusaError } from "@medusajs/framework/utils"
|
|
3
|
+
import { syncProductsToFaireWorkflow } from "../../../../../workflows/sync-products-to-faire"
|
|
4
|
+
|
|
5
|
+
type BulkSyncBody = {
|
|
6
|
+
product_ids?: string[]
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
// POST /admin/faire/sync/bulk — sync many products (long-running background workflow)
|
|
10
|
+
export const POST = async (req: MedusaRequest<BulkSyncBody>, res: MedusaResponse) => {
|
|
11
|
+
const { product_ids } = req.body || {}
|
|
12
|
+
|
|
13
|
+
if (!product_ids?.length) {
|
|
14
|
+
throw new MedusaError(
|
|
15
|
+
MedusaError.Types.INVALID_DATA,
|
|
16
|
+
"product_ids array is required"
|
|
17
|
+
)
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const { result } = await syncProductsToFaireWorkflow(req.scope).run({
|
|
21
|
+
input: { product_ids },
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
// The workflow runs the per-product sync in the background; we return the
|
|
25
|
+
// batch id immediately so the client can poll GET /admin/faire/sync/bulk/:id.
|
|
26
|
+
res.status(202).json({ batch_id: (result as any).batch_id, status: "processing" })
|
|
27
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { MedusaRequest, MedusaResponse } from "@medusajs/framework/http"
|
|
2
|
+
import { syncProductToFaireWorkflow } from "../../../../../../workflows/sync-product-to-faire"
|
|
3
|
+
|
|
4
|
+
// POST /admin/faire/sync/product/:id — sync a single product to Faire
|
|
5
|
+
export const POST = async (req: MedusaRequest, res: MedusaResponse) => {
|
|
6
|
+
const { id } = req.params
|
|
7
|
+
|
|
8
|
+
const { result } = await syncProductToFaireWorkflow(req.scope).run({
|
|
9
|
+
input: { product_id: id },
|
|
10
|
+
})
|
|
11
|
+
|
|
12
|
+
res.json({ result })
|
|
13
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { MedusaRequest, MedusaResponse } from "@medusajs/framework/http"
|
|
2
|
+
import { MedusaError } from "@medusajs/framework/utils"
|
|
3
|
+
import { FAIRE_SYNC_MODULE } from "../../../../../modules/faire-sync"
|
|
4
|
+
import FaireSyncService from "../../../../../modules/faire-sync/service"
|
|
5
|
+
import { syncProductToFaireWorkflow } from "../../../../../workflows/sync-product-to-faire"
|
|
6
|
+
|
|
7
|
+
// GET /admin/faire/syncs/:id — fetch a single sync record
|
|
8
|
+
export const GET = async (req: MedusaRequest, res: MedusaResponse) => {
|
|
9
|
+
const service: FaireSyncService = req.scope.resolve(FAIRE_SYNC_MODULE)
|
|
10
|
+
const { id } = req.params
|
|
11
|
+
const [record] = await service.listFaireSyncRecords({ id } as any, { take: 1 } as any)
|
|
12
|
+
if (!record) {
|
|
13
|
+
throw new MedusaError(MedusaError.Types.NOT_FOUND, "Sync record not found")
|
|
14
|
+
}
|
|
15
|
+
res.json({ sync: record })
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// POST /admin/faire/syncs/:id/retry — re-sync the product referenced by this record
|
|
19
|
+
export const POST = async (req: MedusaRequest, res: MedusaResponse) => {
|
|
20
|
+
const service: FaireSyncService = req.scope.resolve(FAIRE_SYNC_MODULE)
|
|
21
|
+
const { id } = req.params
|
|
22
|
+
const [record] = await service.listFaireSyncRecords({ id } as any, { take: 1 } as any)
|
|
23
|
+
if (!record) {
|
|
24
|
+
throw new MedusaError(MedusaError.Types.NOT_FOUND, "Sync record not found")
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const { result } = await syncProductToFaireWorkflow(req.scope).run({
|
|
28
|
+
input: { product_id: (record as any).product_id },
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
res.json({ result })
|
|
32
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { MedusaRequest, MedusaResponse } from "@medusajs/framework/http"
|
|
2
|
+
import { FAIRE_SYNC_MODULE } from "../../../../modules/faire-sync"
|
|
3
|
+
import FaireSyncService from "../../../../modules/faire-sync/service"
|
|
4
|
+
|
|
5
|
+
// GET /admin/faire/syncs — list recent sync records (paginated)
|
|
6
|
+
export const GET = async (req: MedusaRequest, res: MedusaResponse) => {
|
|
7
|
+
const service: FaireSyncService = req.scope.resolve(FAIRE_SYNC_MODULE)
|
|
8
|
+
const take = Number(req.query.take) || 20
|
|
9
|
+
const skip = Number(req.query.skip) || 0
|
|
10
|
+
const status = req.query.status as string | undefined
|
|
11
|
+
|
|
12
|
+
const filters: any = {}
|
|
13
|
+
if (status) filters.status = status
|
|
14
|
+
|
|
15
|
+
const [records, count] = await service.listSyncRecords(filters, take, skip)
|
|
16
|
+
res.json({ syncs: records, count, take, skip })
|
|
17
|
+
}
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import crypto from "crypto"
|
|
2
|
+
import { MedusaRequest, MedusaResponse } from "@medusajs/framework/http"
|
|
3
|
+
import { ContainerRegistrationKeys, Modules } from "@medusajs/framework/utils"
|
|
4
|
+
import { FAIRE_SYNC_MODULE } from "../../../modules/faire-sync"
|
|
5
|
+
import FaireSyncService from "../../../modules/faire-sync/service"
|
|
6
|
+
import { verifyFaireWebhook } from "../../../lib/webhook"
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* POST /webhooks/faire — inbound Faire webhook receiver.
|
|
10
|
+
*
|
|
11
|
+
* Faire signs each delivery with an HMAC-SHA256 of the raw body using the
|
|
12
|
+
* webhook secret shared at registration time, sent in the
|
|
13
|
+
* `X-Faire-Webhook-Signature` header. This route:
|
|
14
|
+
* 1. verifies the signature over the raw body,
|
|
15
|
+
* 2. records the delivery idempotently (unique delivery id),
|
|
16
|
+
* 3. best-effort fetches the resource referenced and re-emits a Medusa event
|
|
17
|
+
* `faire.<event_type>` so subscribers / visual flows can act on it,
|
|
18
|
+
* 4. always returns 200 once verified+recorded (Faire retries on non-2xx).
|
|
19
|
+
*
|
|
20
|
+
* Raw body is available because the host's middlewares.ts sets preserveRawBody
|
|
21
|
+
* for this path. Register the endpoint URL + copy the signing secret
|
|
22
|
+
* (FAIRE_WEBHOOK_SECRET) when you create the webhook via the Faire API.
|
|
23
|
+
*
|
|
24
|
+
* Faire emits events such as ORDER_PLACED, ORDER_CANCELED, ORDER_SHIPPED,
|
|
25
|
+
* INVENTORY_UPDATED, PRODUCT_UPDATED (verify against your Faire app's
|
|
26
|
+
* configuration). We normalize the event_type to lowercase for the emitted
|
|
27
|
+
* Medusa event name.
|
|
28
|
+
*/
|
|
29
|
+
export const POST = async (req: MedusaRequest, res: MedusaResponse) => {
|
|
30
|
+
const logger = req.scope.resolve(ContainerRegistrationKeys.LOGGER) as any
|
|
31
|
+
const service: FaireSyncService = req.scope.resolve(FAIRE_SYNC_MODULE)
|
|
32
|
+
const secret = (service.getOptions() as any).webhookSecret as string
|
|
33
|
+
|
|
34
|
+
const rawBody = (req as any).rawBody as Buffer | undefined
|
|
35
|
+
const rawStr = rawBody?.length ? rawBody.toString("utf8") : JSON.stringify(req.body ?? {})
|
|
36
|
+
|
|
37
|
+
const headers = {
|
|
38
|
+
signature: req.headers["x-faire-webhook-signature"] as string | undefined,
|
|
39
|
+
timestamp: req.headers["x-faire-webhook-timestamp"] as string | undefined,
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const { valid, reason } = verifyFaireWebhook({ secret, headers, rawBody: rawStr })
|
|
43
|
+
if (!valid) {
|
|
44
|
+
logger?.warn?.(`[faire-webhook] rejected: ${reason}`)
|
|
45
|
+
return res.status(401).json({ error: "invalid signature" })
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const payload: any = req.body ?? safeParse(rawStr)
|
|
49
|
+
const eventType: string = String(
|
|
50
|
+
payload?.event_type || payload?.type || "unknown"
|
|
51
|
+
).toLowerCase()
|
|
52
|
+
|
|
53
|
+
// Delivery id — Faire sends a unique id per delivery; fall back to a hash of
|
|
54
|
+
// the body + type so we still dedupe if absent.
|
|
55
|
+
const webhookId =
|
|
56
|
+
(req.headers["x-faire-webhook-id"] as string | undefined) ||
|
|
57
|
+
(payload?.webhook_id as string | undefined) ||
|
|
58
|
+
`${eventType}:${crypto.createHash("sha256").update(rawStr).digest("hex")}`
|
|
59
|
+
|
|
60
|
+
// Idempotency — Faire retries with backoff; a verified duplicate is a no-op.
|
|
61
|
+
const existing = await service
|
|
62
|
+
.listFaireWebhookEvents({ webhook_id: webhookId } as any)
|
|
63
|
+
.catch(() => [])
|
|
64
|
+
if ((existing as any[])?.length) {
|
|
65
|
+
return res.status(200).json({ ok: true, duplicate: true })
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
let record: any
|
|
69
|
+
try {
|
|
70
|
+
record = await service.createFaireWebhookEvents({
|
|
71
|
+
webhook_id: webhookId,
|
|
72
|
+
event_type: eventType,
|
|
73
|
+
brand_id: payload?.brand_id != null ? String(payload.brand_id) : null,
|
|
74
|
+
resource_url: payload?.resource_url ?? null,
|
|
75
|
+
payload,
|
|
76
|
+
received_at: new Date(),
|
|
77
|
+
} as any)
|
|
78
|
+
} catch {
|
|
79
|
+
// Unique-constraint race → treat as duplicate.
|
|
80
|
+
return res.status(200).json({ ok: true, duplicate: true })
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Best-effort processing — never fail the webhook once it's verified+recorded.
|
|
84
|
+
try {
|
|
85
|
+
let resource: any = null
|
|
86
|
+
if (payload?.resource_url) {
|
|
87
|
+
const account = await service.getActiveAccount()
|
|
88
|
+
if (account) {
|
|
89
|
+
resource = await service
|
|
90
|
+
.getClient()
|
|
91
|
+
.fetchResource(account.access_token, payload.resource_url)
|
|
92
|
+
.catch(() => null)
|
|
93
|
+
}
|
|
94
|
+
} else if (payload?.data) {
|
|
95
|
+
resource = payload.data
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const eventBus: any = req.scope.resolve(Modules.EVENT_BUS)
|
|
99
|
+
await eventBus.emit({
|
|
100
|
+
name: `faire.${eventType}`,
|
|
101
|
+
data: {
|
|
102
|
+
webhook_id: webhookId,
|
|
103
|
+
event_type: eventType,
|
|
104
|
+
brand_id: payload?.brand_id ?? null,
|
|
105
|
+
resource_url: payload?.resource_url ?? null,
|
|
106
|
+
resource,
|
|
107
|
+
},
|
|
108
|
+
})
|
|
109
|
+
|
|
110
|
+
await service.updateFaireWebhookEvents({
|
|
111
|
+
id: record.id,
|
|
112
|
+
resource,
|
|
113
|
+
processed: true,
|
|
114
|
+
} as any)
|
|
115
|
+
} catch (err: any) {
|
|
116
|
+
logger?.warn?.(
|
|
117
|
+
`[faire-webhook] processing failed for ${webhookId}: ${err?.message || err}`
|
|
118
|
+
)
|
|
119
|
+
await service
|
|
120
|
+
.updateFaireWebhookEvents({ id: record.id, error: String(err?.message || err) } as any)
|
|
121
|
+
.catch(() => {})
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
return res.status(200).json({ ok: true })
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function safeParse(s: string): any {
|
|
128
|
+
try {
|
|
129
|
+
return JSON.parse(s)
|
|
130
|
+
} catch {
|
|
131
|
+
return {}
|
|
132
|
+
}
|
|
133
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { MedusaContainer } from "@medusajs/framework/types"
|
|
2
|
+
import { FAIRE_SYNC_MODULE } from "../modules/faire-sync"
|
|
3
|
+
import FaireSyncService from "../modules/faire-sync/service"
|
|
4
|
+
import { ingestFaireOrdersBulkWorkflow } from "../workflows/ingest-faire-orders-bulk"
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Scheduled pull of new Faire orders. Runs the bulk order-ingestion workflow
|
|
8
|
+
* (a long-running background workflow) so new wholesale orders placed on Faire
|
|
9
|
+
* are mirrored into Medusa without a seller having to trigger it manually.
|
|
10
|
+
*
|
|
11
|
+
* Disable by setting FAIRE_AUTO_INGEST_ORDERS=false.
|
|
12
|
+
*/
|
|
13
|
+
export default async function pullFaireOrdersJob(container: MedusaContainer) {
|
|
14
|
+
if (process.env.FAIRE_AUTO_INGEST_ORDERS === "false") return
|
|
15
|
+
|
|
16
|
+
const service: FaireSyncService = container.resolve(FAIRE_SYNC_MODULE)
|
|
17
|
+
const account = await service.getActiveAccount()
|
|
18
|
+
if (!account) return
|
|
19
|
+
|
|
20
|
+
try {
|
|
21
|
+
await ingestFaireOrdersBulkWorkflow(container).run({ input: {} })
|
|
22
|
+
} catch (err: any) {
|
|
23
|
+
console.error("[faire-sync] scheduled order pull failed:", err.message)
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export const config = {
|
|
28
|
+
name: "faire-pull-orders",
|
|
29
|
+
schedule: "*/30 * * * *", // every 30 minutes
|
|
30
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { MedusaContainer } from "@medusajs/framework/types"
|
|
2
|
+
import { FAIRE_SYNC_MODULE } from "../modules/faire-sync"
|
|
3
|
+
import FaireSyncService from "../modules/faire-sync/service"
|
|
4
|
+
|
|
5
|
+
// Refreshes the Faire access token if it is set to expire (Faire tokens
|
|
6
|
+
// historically do not expire, but this keeps the OAuth2 path correct if yours do).
|
|
7
|
+
export default async function refreshFaireTokenJob(container: MedusaContainer) {
|
|
8
|
+
const service: FaireSyncService = container.resolve(FAIRE_SYNC_MODULE)
|
|
9
|
+
const account = await service.getActiveAccount()
|
|
10
|
+
if (!account) {
|
|
11
|
+
return
|
|
12
|
+
}
|
|
13
|
+
try {
|
|
14
|
+
await service.ensureFreshToken(account)
|
|
15
|
+
} catch (err: any) {
|
|
16
|
+
console.error("[faire-sync] token refresh failed:", err.message)
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export const config = {
|
|
21
|
+
name: "faire-refresh-token",
|
|
22
|
+
schedule: "*/10 * * * *",
|
|
23
|
+
}
|