@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,36 @@
|
|
|
1
|
+
const fs = require("fs")
|
|
2
|
+
const path = require("path")
|
|
3
|
+
|
|
4
|
+
const serverDir = path.join(__dirname, "..", ".medusa", "server")
|
|
5
|
+
const srcDir = path.join(serverDir, "src")
|
|
6
|
+
|
|
7
|
+
if (!fs.existsSync(serverDir)) {
|
|
8
|
+
process.exit(0)
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
if (!fs.existsSync(srcDir)) {
|
|
12
|
+
fs.mkdirSync(srcDir, { recursive: true })
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const dirsToMove = [
|
|
16
|
+
"api",
|
|
17
|
+
"jobs",
|
|
18
|
+
"lib",
|
|
19
|
+
"links",
|
|
20
|
+
"modules",
|
|
21
|
+
"workflows",
|
|
22
|
+
"__tests__",
|
|
23
|
+
]
|
|
24
|
+
|
|
25
|
+
for (const dir of dirsToMove) {
|
|
26
|
+
const from = path.join(serverDir, dir)
|
|
27
|
+
const to = path.join(srcDir, dir)
|
|
28
|
+
if (fs.existsSync(from)) {
|
|
29
|
+
if (fs.existsSync(to)) {
|
|
30
|
+
fs.rmSync(to, { recursive: true, force: true })
|
|
31
|
+
}
|
|
32
|
+
fs.renameSync(from, to)
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
console.log("postbuild: restructured server output into src/")
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { mapFaireOrderToOrder, faireMoney } from "../workflows/ingest-faire-order-support"
|
|
2
|
+
|
|
3
|
+
describe("faireMoney", () => {
|
|
4
|
+
it("converts integer cents to decimal major units", () => {
|
|
5
|
+
expect(faireMoney(2500)).toBe(25)
|
|
6
|
+
expect(faireMoney(4999)).toBe(49.99)
|
|
7
|
+
})
|
|
8
|
+
it("coerces undefined/NaN to 0", () => {
|
|
9
|
+
expect(faireMoney(undefined)).toBe(0)
|
|
10
|
+
expect(faireMoney(NaN)).toBe(0)
|
|
11
|
+
})
|
|
12
|
+
})
|
|
13
|
+
|
|
14
|
+
describe("mapFaireOrderToOrder", () => {
|
|
15
|
+
it("maps items, address and idempotency token", () => {
|
|
16
|
+
const order = {
|
|
17
|
+
order_token: "o_123",
|
|
18
|
+
currency: "USD",
|
|
19
|
+
total_cents: 4999,
|
|
20
|
+
buyer_name: "Ada Lovelace",
|
|
21
|
+
address: {
|
|
22
|
+
first_name: "Ada",
|
|
23
|
+
last_name: "Lovelace",
|
|
24
|
+
street1: "1 Main St",
|
|
25
|
+
city: "London",
|
|
26
|
+
state: "ENG",
|
|
27
|
+
zip: "EC1",
|
|
28
|
+
country: "GB",
|
|
29
|
+
},
|
|
30
|
+
items: [
|
|
31
|
+
{
|
|
32
|
+
id: "i_1",
|
|
33
|
+
product_token: "p_1",
|
|
34
|
+
sku: "SKU-1",
|
|
35
|
+
quantity: 2,
|
|
36
|
+
wholesale_price_cents: 1250,
|
|
37
|
+
name: "Cushion",
|
|
38
|
+
},
|
|
39
|
+
],
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const mapped = mapFaireOrderToOrder(order)
|
|
43
|
+
|
|
44
|
+
expect(mapped.order_token).toBe("o_123")
|
|
45
|
+
expect(mapped.currency_code).toBe("usd")
|
|
46
|
+
expect(mapped.total).toBe(49.99)
|
|
47
|
+
expect(mapped.buyer_name).toBe("Ada Lovelace")
|
|
48
|
+
expect(mapped.email).toBe("faire+o_123@marketplace.invalid")
|
|
49
|
+
expect(mapped.shipping_address.country_code).toBe("gb")
|
|
50
|
+
expect(mapped.items).toHaveLength(1)
|
|
51
|
+
expect(mapped.items[0]).toMatchObject({
|
|
52
|
+
title: "Cushion",
|
|
53
|
+
quantity: 2,
|
|
54
|
+
unit_price: 12.5,
|
|
55
|
+
})
|
|
56
|
+
expect(mapped.items[0].metadata).toMatchObject({
|
|
57
|
+
faire_order_token: "o_123",
|
|
58
|
+
faire_product_token: "p_1",
|
|
59
|
+
faire_sku: "SKU-1",
|
|
60
|
+
})
|
|
61
|
+
})
|
|
62
|
+
|
|
63
|
+
it("falls back when address fields are missing", () => {
|
|
64
|
+
const mapped = mapFaireOrderToOrder({ order_token: "o_1", items: [] })
|
|
65
|
+
expect(mapped.shipping_address.first_name).toBe("Faire")
|
|
66
|
+
expect(mapped.items).toEqual([])
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
it("uses buyer email when provided", () => {
|
|
70
|
+
const mapped = mapFaireOrderToOrder({
|
|
71
|
+
order_token: "o_2",
|
|
72
|
+
buyer_email: "retailer@example.com",
|
|
73
|
+
items: [{ quantity: 1, name: "x" }],
|
|
74
|
+
})
|
|
75
|
+
expect(mapped.email).toBe("retailer@example.com")
|
|
76
|
+
})
|
|
77
|
+
})
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { verifyFaireWebhook } from "../lib/webhook"
|
|
2
|
+
import crypto from "crypto"
|
|
3
|
+
|
|
4
|
+
const secret = "faire-webhook-secret"
|
|
5
|
+
|
|
6
|
+
const sign = (body: string, enc: "hex" | "base64" = "hex") =>
|
|
7
|
+
crypto.createHmac("sha256", Buffer.from(secret, "utf8")).update(body, "utf8").digest(enc)
|
|
8
|
+
|
|
9
|
+
describe("verifyFaireWebhook", () => {
|
|
10
|
+
it("accepts a valid hex signature", () => {
|
|
11
|
+
const body = '{"event_type":"ORDER_PLACED"}'
|
|
12
|
+
const res = verifyFaireWebhook({
|
|
13
|
+
secret,
|
|
14
|
+
headers: { signature: sign(body, "hex") },
|
|
15
|
+
rawBody: body,
|
|
16
|
+
})
|
|
17
|
+
expect(res.valid).toBe(true)
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
it("accepts a valid base64 signature", () => {
|
|
21
|
+
const body = '{"event_type":"ORDER_PLACED"}'
|
|
22
|
+
const res = verifyFaireWebhook({
|
|
23
|
+
secret,
|
|
24
|
+
headers: { signature: sign(body, "base64") },
|
|
25
|
+
rawBody: body,
|
|
26
|
+
})
|
|
27
|
+
expect(res.valid).toBe(true)
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
it("accepts svix-style v1,<sig> entries", () => {
|
|
31
|
+
const body = '{"event_type":"ORDER_PLACED"}'
|
|
32
|
+
const res = verifyFaireWebhook({
|
|
33
|
+
secret,
|
|
34
|
+
headers: { signature: `v1,${sign(body, "hex")}` },
|
|
35
|
+
rawBody: body,
|
|
36
|
+
})
|
|
37
|
+
expect(res.valid).toBe(true)
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
it("rejects a tampered body", () => {
|
|
41
|
+
const body = '{"event_type":"ORDER_PLACED"}'
|
|
42
|
+
const res = verifyFaireWebhook({
|
|
43
|
+
secret,
|
|
44
|
+
headers: { signature: sign(body, "hex") },
|
|
45
|
+
rawBody: body + "tampered",
|
|
46
|
+
})
|
|
47
|
+
expect(res.valid).toBe(false)
|
|
48
|
+
expect(res.reason).toBe("signature mismatch")
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
it("rejects when signature header is missing", () => {
|
|
52
|
+
const res = verifyFaireWebhook({
|
|
53
|
+
secret,
|
|
54
|
+
headers: {},
|
|
55
|
+
rawBody: "{}",
|
|
56
|
+
})
|
|
57
|
+
expect(res.valid).toBe(false)
|
|
58
|
+
expect(res.reason).toMatch(/missing/)
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
it("rejects when secret is empty", () => {
|
|
62
|
+
const res = verifyFaireWebhook({
|
|
63
|
+
secret: "",
|
|
64
|
+
headers: { signature: "abc" },
|
|
65
|
+
rawBody: "{}",
|
|
66
|
+
})
|
|
67
|
+
expect(res.valid).toBe(false)
|
|
68
|
+
expect(res.reason).toMatch(/secret/)
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
it("enforces timestamp tolerance when a timestamp is present", () => {
|
|
72
|
+
const body = "{}"
|
|
73
|
+
const res = verifyFaireWebhook({
|
|
74
|
+
secret,
|
|
75
|
+
headers: { signature: sign(body, "hex"), timestamp: String(Math.floor(Date.now() / 1000) - 600) },
|
|
76
|
+
rawBody: body,
|
|
77
|
+
})
|
|
78
|
+
expect(res.valid).toBe(false)
|
|
79
|
+
expect(res.reason).toMatch(/timestamp/)
|
|
80
|
+
})
|
|
81
|
+
})
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import Medusa from "@medusajs/js-sdk"
|
|
2
|
+
|
|
3
|
+
export const sdk = new Medusa({
|
|
4
|
+
baseUrl: "/",
|
|
5
|
+
auth: {
|
|
6
|
+
type: "session",
|
|
7
|
+
},
|
|
8
|
+
})
|
|
9
|
+
|
|
10
|
+
export const faireApi = {
|
|
11
|
+
status: () =>
|
|
12
|
+
sdk.client.fetch<{
|
|
13
|
+
connected: boolean
|
|
14
|
+
account: any | null
|
|
15
|
+
settings: any
|
|
16
|
+
readiness: {
|
|
17
|
+
connected: boolean
|
|
18
|
+
brand: boolean
|
|
19
|
+
wholesale_pricing: boolean
|
|
20
|
+
shipping_policy: boolean
|
|
21
|
+
ready_to_publish: boolean
|
|
22
|
+
}
|
|
23
|
+
}>("/admin/faire/status"),
|
|
24
|
+
getSettings: () =>
|
|
25
|
+
sdk.client.fetch<Record<string, any>>("/admin/faire/settings"),
|
|
26
|
+
saveSettings: (settings: Record<string, any>) =>
|
|
27
|
+
sdk.client.fetch("/admin/faire/settings", {
|
|
28
|
+
method: "POST",
|
|
29
|
+
body: settings,
|
|
30
|
+
}),
|
|
31
|
+
authorize: () =>
|
|
32
|
+
sdk.client.fetch<{ authorization_url: string; state: string }>(
|
|
33
|
+
"/admin/faire/auth/authorize"
|
|
34
|
+
),
|
|
35
|
+
callback: (code: string, state: string) =>
|
|
36
|
+
sdk.client.fetch("/admin/faire/auth/callback", {
|
|
37
|
+
method: "POST",
|
|
38
|
+
body: { code, state },
|
|
39
|
+
}),
|
|
40
|
+
disconnect: () =>
|
|
41
|
+
sdk.client.fetch("/admin/faire/auth/disconnect", { method: "POST" }),
|
|
42
|
+
syncProduct: (id: string) =>
|
|
43
|
+
sdk.client.fetch(`/admin/faire/sync/product/${id}`, { method: "POST" }),
|
|
44
|
+
productStatus: (id: string) =>
|
|
45
|
+
sdk.client.fetch<{
|
|
46
|
+
connected: boolean
|
|
47
|
+
synced: boolean
|
|
48
|
+
latest: any | null
|
|
49
|
+
}>(`/admin/faire/status/product/${id}`),
|
|
50
|
+
syncBulk: (product_ids: string[]) =>
|
|
51
|
+
sdk.client.fetch<{ batch_id: string; status: string }>(
|
|
52
|
+
"/admin/faire/sync/bulk",
|
|
53
|
+
{
|
|
54
|
+
method: "POST",
|
|
55
|
+
body: { product_ids },
|
|
56
|
+
}
|
|
57
|
+
),
|
|
58
|
+
bulkStatus: (batchId: string) =>
|
|
59
|
+
sdk.client.fetch<{ batch: any; progress: any }>(
|
|
60
|
+
`/admin/faire/sync/bulk/${batchId}`
|
|
61
|
+
),
|
|
62
|
+
ingestOrders: (limit?: number) =>
|
|
63
|
+
sdk.client.fetch<{ batch_id: string; status: string }>(
|
|
64
|
+
"/admin/faire/ingest/orders",
|
|
65
|
+
{ method: "POST", body: limit != null ? { limit } : {} }
|
|
66
|
+
),
|
|
67
|
+
ingestStatus: (batchId: string) =>
|
|
68
|
+
sdk.client.fetch<{ batch: any; progress: any }>(
|
|
69
|
+
`/admin/faire/ingest/orders/${batchId}`
|
|
70
|
+
),
|
|
71
|
+
listSyncs: (opts: { take?: number; skip?: number; status?: string } = {}) => {
|
|
72
|
+
const query: Record<string, string> = {}
|
|
73
|
+
if (opts.take !== undefined) query.take = String(opts.take)
|
|
74
|
+
if (opts.skip !== undefined) query.skip = String(opts.skip)
|
|
75
|
+
if (opts.status) query.status = opts.status
|
|
76
|
+
return sdk.client.fetch("/admin/faire/syncs", { query })
|
|
77
|
+
},
|
|
78
|
+
getSync: (id: string) => sdk.client.fetch(`/admin/faire/syncs/${id}`),
|
|
79
|
+
retrySync: (id: string) =>
|
|
80
|
+
sdk.client.fetch(`/admin/faire/syncs/${id}`, { method: "POST" }),
|
|
81
|
+
brand: () => sdk.client.fetch("/admin/faire/brand"),
|
|
82
|
+
products: (opts: { limit?: number; page?: number } = {}) => {
|
|
83
|
+
const query: Record<string, string> = {}
|
|
84
|
+
if (opts.limit !== undefined) query.limit = String(opts.limit)
|
|
85
|
+
if (opts.page !== undefined) query.page = String(opts.page)
|
|
86
|
+
return sdk.client.fetch("/admin/faire/products", { query })
|
|
87
|
+
},
|
|
88
|
+
}
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
import {
|
|
2
|
+
Button,
|
|
3
|
+
Container,
|
|
4
|
+
DropdownMenu,
|
|
5
|
+
Heading,
|
|
6
|
+
IconButton,
|
|
7
|
+
Label,
|
|
8
|
+
Skeleton,
|
|
9
|
+
StatusBadge,
|
|
10
|
+
Text,
|
|
11
|
+
Toaster,
|
|
12
|
+
} from "@medusajs/ui"
|
|
13
|
+
import { EllipsisHorizontal, ArrowPath, ArrowUpRightOnBox } from "@medusajs/icons"
|
|
14
|
+
import { useEffect, useState } from "react"
|
|
15
|
+
import { useNavigate, useParams } from "react-router-dom"
|
|
16
|
+
import { faireApi } from "../../../../lib/api"
|
|
17
|
+
import type { FaireSyncRecord } from "../hooks/use-faire-sync-columns"
|
|
18
|
+
|
|
19
|
+
const STATUS_COLORS: Record<string, "green" | "red" | "orange" | "grey"> = {
|
|
20
|
+
success: "green",
|
|
21
|
+
failed: "red",
|
|
22
|
+
draft: "orange",
|
|
23
|
+
pending: "grey",
|
|
24
|
+
syncing: "grey",
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const FaireSyncDetailPage = () => {
|
|
28
|
+
const { id } = useParams<{ id: string }>()
|
|
29
|
+
const navigate = useNavigate()
|
|
30
|
+
const [record, setRecord] = useState<FaireSyncRecord | null>(null)
|
|
31
|
+
const [loading, setLoading] = useState(true)
|
|
32
|
+
const [retrying, setRetrying] = useState(false)
|
|
33
|
+
const [error, setError] = useState<string | null>(null)
|
|
34
|
+
|
|
35
|
+
useEffect(() => {
|
|
36
|
+
if (!id) return
|
|
37
|
+
setLoading(true)
|
|
38
|
+
faireApi
|
|
39
|
+
.getSync(id)
|
|
40
|
+
.then((res: any) => {
|
|
41
|
+
setRecord(res.sync || res)
|
|
42
|
+
})
|
|
43
|
+
.catch((err: any) => setError(err.message || "Failed to load sync record"))
|
|
44
|
+
.finally(() => setLoading(false))
|
|
45
|
+
}, [id])
|
|
46
|
+
|
|
47
|
+
const handleRetry = async () => {
|
|
48
|
+
if (!record) return
|
|
49
|
+
setRetrying(true)
|
|
50
|
+
setError(null)
|
|
51
|
+
try {
|
|
52
|
+
await faireApi.retrySync(record.id)
|
|
53
|
+
const updated = await faireApi.getSync(record.id).catch(() => null)
|
|
54
|
+
if (updated) setRecord((updated as any).sync || updated)
|
|
55
|
+
} catch (err: any) {
|
|
56
|
+
setError(err.message || "Retry failed")
|
|
57
|
+
} finally {
|
|
58
|
+
setRetrying(false)
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
if (loading) {
|
|
63
|
+
return (
|
|
64
|
+
<Container className="p-6">
|
|
65
|
+
<Skeleton className="h-7 w-12 rounded-md" />
|
|
66
|
+
</Container>
|
|
67
|
+
)
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
if (!record) {
|
|
71
|
+
return (
|
|
72
|
+
<Container className="p-6 flex flex-col gap-4">
|
|
73
|
+
<Heading level="h1">Sync record not found</Heading>
|
|
74
|
+
<Button size="small" variant="secondary" onClick={() => navigate("/settings/faire")}>
|
|
75
|
+
Back to Faire settings
|
|
76
|
+
</Button>
|
|
77
|
+
</Container>
|
|
78
|
+
)
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const warnings: string[] = record.error_message
|
|
82
|
+
? record.error_message.split(" | ")
|
|
83
|
+
: []
|
|
84
|
+
|
|
85
|
+
return (
|
|
86
|
+
<div className="flex flex-col gap-4">
|
|
87
|
+
{error && (
|
|
88
|
+
<Container className="p-4">
|
|
89
|
+
<Text className="text-ui-tag-red-text">{error}</Text>
|
|
90
|
+
</Container>
|
|
91
|
+
)}
|
|
92
|
+
|
|
93
|
+
<Container className="divide-y p-0">
|
|
94
|
+
<div className="flex items-center justify-between px-6 py-4">
|
|
95
|
+
<div className="flex flex-col gap-1">
|
|
96
|
+
<Heading level="h1">Sync record</Heading>
|
|
97
|
+
<Text className="text-ui-fg-subtle font-mono text-xs">{record.id}</Text>
|
|
98
|
+
</div>
|
|
99
|
+
<div className="flex items-center gap-3">
|
|
100
|
+
<StatusBadge color={STATUS_COLORS[record.status] || "grey"}>
|
|
101
|
+
{record.status}
|
|
102
|
+
</StatusBadge>
|
|
103
|
+
<DropdownMenu>
|
|
104
|
+
<DropdownMenu.Trigger asChild>
|
|
105
|
+
<IconButton size="small" variant="transparent" disabled={retrying}>
|
|
106
|
+
<EllipsisHorizontal />
|
|
107
|
+
</IconButton>
|
|
108
|
+
</DropdownMenu.Trigger>
|
|
109
|
+
<DropdownMenu.Content align="end">
|
|
110
|
+
<DropdownMenu.Item
|
|
111
|
+
className="gap-x-2"
|
|
112
|
+
onClick={handleRetry}
|
|
113
|
+
disabled={retrying}
|
|
114
|
+
>
|
|
115
|
+
<ArrowPath className="text-ui-fg-subtle" />
|
|
116
|
+
{retrying ? "Re-syncing…" : "Re-sync product"}
|
|
117
|
+
</DropdownMenu.Item>
|
|
118
|
+
{record.product_url && (
|
|
119
|
+
<DropdownMenu.Item
|
|
120
|
+
className="gap-x-2"
|
|
121
|
+
onClick={() => window.open(record.product_url!, "_blank")}
|
|
122
|
+
>
|
|
123
|
+
<ArrowUpRightOnBox className="text-ui-fg-subtle" />
|
|
124
|
+
Open on Faire
|
|
125
|
+
</DropdownMenu.Item>
|
|
126
|
+
)}
|
|
127
|
+
</DropdownMenu.Content>
|
|
128
|
+
</DropdownMenu>
|
|
129
|
+
</div>
|
|
130
|
+
</div>
|
|
131
|
+
|
|
132
|
+
<div className="px-6 py-4 grid grid-cols-2 gap-4">
|
|
133
|
+
<Detail label="Product" value={record.product_id} mono />
|
|
134
|
+
<Detail label="Action" value={record.action} />
|
|
135
|
+
<Detail label="Faire product token" value={record.product_token || "—"} mono />
|
|
136
|
+
<Detail label="State" value={record.product_state || "—"} />
|
|
137
|
+
<Detail label="Published" value={record.published ? "Yes" : "No"} />
|
|
138
|
+
<Detail
|
|
139
|
+
label="Synced at"
|
|
140
|
+
value={record.synced_at ? new Date(record.synced_at).toLocaleString() : "—"}
|
|
141
|
+
/>
|
|
142
|
+
</div>
|
|
143
|
+
</Container>
|
|
144
|
+
|
|
145
|
+
{warnings.length > 0 && (
|
|
146
|
+
<Container className="divide-y p-0">
|
|
147
|
+
<div className="px-6 py-4">
|
|
148
|
+
<Heading level="h2">Issues</Heading>
|
|
149
|
+
</div>
|
|
150
|
+
<div className="px-6 py-4 flex flex-col gap-2">
|
|
151
|
+
{warnings.map((w, i) => (
|
|
152
|
+
<Text key={i} className="text-ui-tag-red-text text-sm">
|
|
153
|
+
{w}
|
|
154
|
+
</Text>
|
|
155
|
+
))}
|
|
156
|
+
</div>
|
|
157
|
+
</Container>
|
|
158
|
+
)}
|
|
159
|
+
|
|
160
|
+
<Toaster />
|
|
161
|
+
</div>
|
|
162
|
+
)
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
const Detail = ({
|
|
166
|
+
label,
|
|
167
|
+
value,
|
|
168
|
+
mono,
|
|
169
|
+
}: {
|
|
170
|
+
label: string
|
|
171
|
+
value: string
|
|
172
|
+
mono?: boolean
|
|
173
|
+
}) => (
|
|
174
|
+
<div className="flex flex-col gap-1">
|
|
175
|
+
<Label>{label}</Label>
|
|
176
|
+
<Text className={mono ? "font-mono text-xs" : ""}>{value}</Text>
|
|
177
|
+
</div>
|
|
178
|
+
)
|
|
179
|
+
|
|
180
|
+
export const handle = {
|
|
181
|
+
breadcrumb: () => "Sync record",
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
export default FaireSyncDetailPage
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { createDataTableColumnHelper, StatusBadge } from "@medusajs/ui"
|
|
2
|
+
|
|
3
|
+
export type FaireSyncRecord = {
|
|
4
|
+
id: string
|
|
5
|
+
product_id: string
|
|
6
|
+
account_id: string
|
|
7
|
+
product_token: string | null
|
|
8
|
+
product_url: string | null
|
|
9
|
+
product_state: string | null
|
|
10
|
+
action: string
|
|
11
|
+
status: string
|
|
12
|
+
published: boolean
|
|
13
|
+
error_message: string | null
|
|
14
|
+
synced_at: string | null
|
|
15
|
+
created_at: string
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const columnHelper = createDataTableColumnHelper<FaireSyncRecord>()
|
|
19
|
+
|
|
20
|
+
export const useFaireSyncColumns = () => {
|
|
21
|
+
return [
|
|
22
|
+
columnHelper.accessor("product_id", {
|
|
23
|
+
header: "Product",
|
|
24
|
+
cell: (info) => (
|
|
25
|
+
<span className="font-mono text-ui-fg-subtle text-xs">
|
|
26
|
+
{info.getValue()}
|
|
27
|
+
</span>
|
|
28
|
+
),
|
|
29
|
+
}),
|
|
30
|
+
columnHelper.accessor("status", {
|
|
31
|
+
header: "Status",
|
|
32
|
+
cell: (info) => {
|
|
33
|
+
const status = info.getValue()
|
|
34
|
+
const color =
|
|
35
|
+
status === "success"
|
|
36
|
+
? "green"
|
|
37
|
+
: status === "failed"
|
|
38
|
+
? "red"
|
|
39
|
+
: status === "draft"
|
|
40
|
+
? "orange"
|
|
41
|
+
: "grey"
|
|
42
|
+
return <StatusBadge color={color as any}>{status}</StatusBadge>
|
|
43
|
+
},
|
|
44
|
+
}),
|
|
45
|
+
columnHelper.accessor("product_state", {
|
|
46
|
+
header: "State",
|
|
47
|
+
cell: (info) => info.getValue() || "—",
|
|
48
|
+
}),
|
|
49
|
+
columnHelper.display({
|
|
50
|
+
id: "product",
|
|
51
|
+
header: "Faire product",
|
|
52
|
+
cell: ({ row }: any) => {
|
|
53
|
+
const r = row.original as FaireSyncRecord
|
|
54
|
+
if (!r.product_url) return "—"
|
|
55
|
+
return (
|
|
56
|
+
<a
|
|
57
|
+
href={r.product_url}
|
|
58
|
+
target="_blank"
|
|
59
|
+
rel="noreferrer"
|
|
60
|
+
onClick={(e) => e.stopPropagation()}
|
|
61
|
+
className="text-ui-fg-interactive hover:text-ui-fg-interactive-hover"
|
|
62
|
+
>
|
|
63
|
+
{r.product_token}
|
|
64
|
+
</a>
|
|
65
|
+
)
|
|
66
|
+
},
|
|
67
|
+
}),
|
|
68
|
+
columnHelper.accessor("synced_at", {
|
|
69
|
+
header: "Synced",
|
|
70
|
+
cell: (info) => {
|
|
71
|
+
const v = info.getValue()
|
|
72
|
+
return v ? new Date(v).toLocaleString() : "—"
|
|
73
|
+
},
|
|
74
|
+
enableSorting: true,
|
|
75
|
+
sortLabel: "Synced",
|
|
76
|
+
sortAscLabel: "Oldest first",
|
|
77
|
+
sortDescLabel: "Newest first",
|
|
78
|
+
}),
|
|
79
|
+
]
|
|
80
|
+
}
|