@jytextiles/medusa-plugin-faire-store-sync 0.2.4 → 0.2.6

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 (26) hide show
  1. package/.medusa/server/src/__tests__/faire-client.spec.js +27 -7
  2. package/.medusa/server/src/__tests__/ingest-faire-order-support.spec.js +61 -1
  3. package/.medusa/server/src/admin/index.js +182 -132
  4. package/.medusa/server/src/admin/index.mjs +184 -134
  5. package/.medusa/server/src/lib/faire-client.js +100 -18
  6. package/.medusa/server/src/lib/types.js +6 -2
  7. package/.medusa/server/src/modules/faire-sync/migrations/Migration20260709161201.js +14 -0
  8. package/.medusa/server/src/modules/faire-sync/models/faire-sync-account.js +5 -1
  9. package/.medusa/server/src/modules/faire-sync/service.js +33 -14
  10. package/.medusa/server/src/workflows/ingest-faire-order-support.js +81 -28
  11. package/.medusa/server/src/workflows/ingest-faire-orders-bulk.js +2 -2
  12. package/.medusa/server/src/workflows/sync-product-to-faire.js +109 -37
  13. package/package.json +1 -1
  14. package/src/__tests__/faire-client.spec.ts +28 -6
  15. package/src/__tests__/ingest-faire-order-support.spec.ts +63 -0
  16. package/src/admin/routes/settings/faire/[id]/page.tsx +56 -61
  17. package/src/admin/routes/settings/faire/page.tsx +78 -8
  18. package/src/lib/faire-client.ts +112 -16
  19. package/src/lib/types.ts +57 -18
  20. package/src/modules/faire-sync/migrations/.snapshot-medusa-faire-sync.json +19 -0
  21. package/src/modules/faire-sync/migrations/Migration20260709161201.ts +13 -0
  22. package/src/modules/faire-sync/models/faire-sync-account.ts +4 -0
  23. package/src/modules/faire-sync/service.ts +42 -16
  24. package/src/workflows/ingest-faire-order-support.ts +96 -29
  25. package/src/workflows/ingest-faire-orders-bulk.ts +1 -1
  26. package/src/workflows/sync-product-to-faire.ts +143 -41
@@ -1,9 +1,9 @@
1
1
  import { jsxs, jsx, Fragment } from "react/jsx-runtime";
2
2
  import { defineWidgetConfig, defineRouteConfig } from "@medusajs/admin-sdk";
3
- import { Container, Heading, Text, StatusBadge, Alert, Tooltip, Button, createDataTableColumnHelper, toast, createDataTableFilterHelper, useDataTable, Skeleton, DataTable, Toaster, Label, DropdownMenu, IconButton, FocusModal, Input, Badge, Drawer, Switch, clx } from "@medusajs/ui";
3
+ import { Container, Heading, Text, StatusBadge, Alert, Tooltip, Button, createDataTableColumnHelper, toast, createDataTableFilterHelper, useDataTable, Skeleton, DropdownMenu, DataTable, Drawer, Label, Input, Toaster, FocusModal, Badge, IconButton, Switch, clx } from "@medusajs/ui";
4
4
  import { useQueryClient, useQuery, useMutation, keepPreviousData } from "@tanstack/react-query";
5
5
  import Medusa from "@medusajs/js-sdk";
6
- import { BuildingStorefront, EllipsisHorizontal, ArrowPath, ArrowUpRightOnBox } from "@medusajs/icons";
6
+ import { BuildingStorefront, ChevronDownMini, EllipsisHorizontal, ArrowPath, ArrowUpRightOnBox } from "@medusajs/icons";
7
7
  import { useState, useEffect, useMemo } from "react";
8
8
  import { useNavigate, useParams, useSearchParams } from "react-router-dom";
9
9
  const sdk = new Medusa({
@@ -236,6 +236,8 @@ const FaireSettingsPage = () => {
236
236
  pageSize: PAGE_SIZE$1
237
237
  });
238
238
  const [filtering, setFiltering] = useState({});
239
+ const [apiKeyOpen, setApiKeyOpen] = useState(false);
240
+ const [apiKey, setApiKey] = useState("");
239
241
  const statusQuery = useQuery({
240
242
  queryKey: STATUS_KEY$1,
241
243
  queryFn: () => faireApi.status()
@@ -268,6 +270,18 @@ const FaireSettingsPage = () => {
268
270
  description: err.message
269
271
  })
270
272
  });
273
+ const apiKeyMutation = useMutation({
274
+ mutationFn: () => faireApi.connectApiKey(apiKey.trim()),
275
+ onSuccess: () => {
276
+ queryClient.invalidateQueries({ queryKey: STATUS_KEY$1 });
277
+ setApiKeyOpen(false);
278
+ setApiKey("");
279
+ toast.success("Connected to Faire with API key");
280
+ },
281
+ onError: (err) => toast.error("Failed to connect with API key", {
282
+ description: err.message
283
+ })
284
+ });
271
285
  const disconnectMutation = useMutation({
272
286
  mutationFn: () => faireApi.disconnect(),
273
287
  onSuccess: () => {
@@ -363,15 +377,16 @@ const FaireSettingsPage = () => {
363
377
  children: "Disconnect"
364
378
  }
365
379
  )
366
- ] }) : /* @__PURE__ */ jsx(
367
- Button,
368
- {
369
- size: "small",
370
- onClick: () => connectMutation.mutate(),
371
- isLoading: connectMutation.isPending,
372
- children: "Connect Faire"
373
- }
374
- )
380
+ ] }) : /* @__PURE__ */ jsxs(DropdownMenu, { children: [
381
+ /* @__PURE__ */ jsx(DropdownMenu.Trigger, { asChild: true, children: /* @__PURE__ */ jsxs(Button, { size: "small", isLoading: connectMutation.isPending, children: [
382
+ "Connect Faire",
383
+ /* @__PURE__ */ jsx(ChevronDownMini, {})
384
+ ] }) }),
385
+ /* @__PURE__ */ jsxs(DropdownMenu.Content, { children: [
386
+ /* @__PURE__ */ jsx(DropdownMenu.Item, { onClick: () => connectMutation.mutate(), children: "Connect with OAuth" }),
387
+ /* @__PURE__ */ jsx(DropdownMenu.Item, { onClick: () => setApiKeyOpen(true), children: "Connect with API key (private)" })
388
+ ] })
389
+ ] })
375
390
  ] }),
376
391
  connected && (status == null ? void 0 : status.account) && /* @__PURE__ */ jsxs("div", { className: "px-6 py-4 grid grid-cols-4 gap-4", children: [
377
392
  /* @__PURE__ */ jsx(InfoField, { label: "Brand", value: status.account.brand_name }),
@@ -409,6 +424,46 @@ const FaireSettingsPage = () => {
409
424
  /* @__PURE__ */ jsx(DataTable.Table, {}),
410
425
  /* @__PURE__ */ jsx(DataTable.Pagination, {})
411
426
  ] }) }),
427
+ /* @__PURE__ */ jsx(Drawer, { open: apiKeyOpen, onOpenChange: setApiKeyOpen, children: /* @__PURE__ */ jsxs(Drawer.Content, { children: [
428
+ /* @__PURE__ */ jsx(Drawer.Header, { children: /* @__PURE__ */ jsx(Drawer.Title, { children: "Connect with API key" }) }),
429
+ /* @__PURE__ */ jsxs(Drawer.Body, { className: "flex flex-col gap-y-3", children: [
430
+ /* @__PURE__ */ jsx(Text, { className: "text-ui-fg-subtle", size: "small", children: "For private / unpublished Faire integrations, paste the API key your Faire brand issued for this app. This connects without OAuth." }),
431
+ /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-y-1", children: [
432
+ /* @__PURE__ */ jsx(Label, { size: "small", children: "Faire API key" }),
433
+ /* @__PURE__ */ jsx(
434
+ Input,
435
+ {
436
+ type: "password",
437
+ placeholder: "Paste the Faire API key…",
438
+ value: apiKey,
439
+ onChange: (e) => setApiKey(e.target.value),
440
+ autoComplete: "off"
441
+ }
442
+ )
443
+ ] })
444
+ ] }),
445
+ /* @__PURE__ */ jsxs(Drawer.Footer, { children: [
446
+ /* @__PURE__ */ jsx(
447
+ Button,
448
+ {
449
+ variant: "secondary",
450
+ size: "small",
451
+ onClick: () => setApiKeyOpen(false),
452
+ children: "Cancel"
453
+ }
454
+ ),
455
+ /* @__PURE__ */ jsx(
456
+ Button,
457
+ {
458
+ size: "small",
459
+ onClick: () => apiKeyMutation.mutate(),
460
+ isLoading: apiKeyMutation.isPending,
461
+ disabled: !apiKey.trim(),
462
+ children: "Connect"
463
+ }
464
+ )
465
+ ] })
466
+ ] }) }),
412
467
  /* @__PURE__ */ jsx(Toaster, {})
413
468
  ] });
414
469
  };
@@ -423,124 +478,6 @@ const config = defineRouteConfig({
423
478
  const handle$3 = {
424
479
  breadcrumb: () => "Faire Sync"
425
480
  };
426
- const STATUS_COLORS = {
427
- success: "green",
428
- failed: "red",
429
- draft: "orange",
430
- pending: "grey",
431
- syncing: "grey"
432
- };
433
- const FaireSyncDetailPage = () => {
434
- const { id } = useParams();
435
- const navigate = useNavigate();
436
- const [record, setRecord] = useState(null);
437
- const [loading, setLoading] = useState(true);
438
- const [retrying, setRetrying] = useState(false);
439
- const [error, setError] = useState(null);
440
- useEffect(() => {
441
- if (!id) return;
442
- setLoading(true);
443
- faireApi.getSync(id).then((res) => {
444
- setRecord(res.sync || res);
445
- }).catch((err) => setError(err.message || "Failed to load sync record")).finally(() => setLoading(false));
446
- }, [id]);
447
- const handleRetry = async () => {
448
- if (!record) return;
449
- setRetrying(true);
450
- setError(null);
451
- try {
452
- await faireApi.retrySync(record.id);
453
- const updated = await faireApi.getSync(record.id).catch(() => null);
454
- if (updated) setRecord(updated.sync || updated);
455
- } catch (err) {
456
- setError(err.message || "Retry failed");
457
- } finally {
458
- setRetrying(false);
459
- }
460
- };
461
- if (loading) {
462
- return /* @__PURE__ */ jsx(Container, { className: "p-6", children: /* @__PURE__ */ jsx(Skeleton, { className: "h-7 w-12 rounded-md" }) });
463
- }
464
- if (!record) {
465
- return /* @__PURE__ */ jsxs(Container, { className: "p-6 flex flex-col gap-4", children: [
466
- /* @__PURE__ */ jsx(Heading, { level: "h1", children: "Sync record not found" }),
467
- /* @__PURE__ */ jsx(Button, { size: "small", variant: "secondary", onClick: () => navigate("/settings/faire"), children: "Back to Faire settings" })
468
- ] });
469
- }
470
- const warnings = record.error_message ? record.error_message.split(" | ") : [];
471
- return /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-4", children: [
472
- error && /* @__PURE__ */ jsx(Container, { className: "p-4", children: /* @__PURE__ */ jsx(Text, { className: "text-ui-tag-red-text", children: error }) }),
473
- /* @__PURE__ */ jsxs(Container, { className: "divide-y p-0", children: [
474
- /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between px-6 py-4", children: [
475
- /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-1", children: [
476
- /* @__PURE__ */ jsx(Heading, { level: "h1", children: "Sync record" }),
477
- /* @__PURE__ */ jsx(Text, { className: "text-ui-fg-subtle font-mono text-xs", children: record.id })
478
- ] }),
479
- /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-3", children: [
480
- /* @__PURE__ */ jsx(StatusBadge, { color: STATUS_COLORS[record.status] || "grey", children: record.status }),
481
- /* @__PURE__ */ jsxs(DropdownMenu, { children: [
482
- /* @__PURE__ */ jsx(DropdownMenu.Trigger, { asChild: true, children: /* @__PURE__ */ jsx(IconButton, { size: "small", variant: "transparent", disabled: retrying, children: /* @__PURE__ */ jsx(EllipsisHorizontal, {}) }) }),
483
- /* @__PURE__ */ jsxs(DropdownMenu.Content, { align: "end", children: [
484
- /* @__PURE__ */ jsxs(
485
- DropdownMenu.Item,
486
- {
487
- className: "gap-x-2",
488
- onClick: handleRetry,
489
- disabled: retrying,
490
- children: [
491
- /* @__PURE__ */ jsx(ArrowPath, { className: "text-ui-fg-subtle" }),
492
- retrying ? "Re-syncing…" : "Re-sync product"
493
- ]
494
- }
495
- ),
496
- record.product_url && /* @__PURE__ */ jsxs(
497
- DropdownMenu.Item,
498
- {
499
- className: "gap-x-2",
500
- onClick: () => window.open(record.product_url, "_blank"),
501
- children: [
502
- /* @__PURE__ */ jsx(ArrowUpRightOnBox, { className: "text-ui-fg-subtle" }),
503
- "Open on Faire"
504
- ]
505
- }
506
- )
507
- ] })
508
- ] })
509
- ] })
510
- ] }),
511
- /* @__PURE__ */ jsxs("div", { className: "px-6 py-4 grid grid-cols-2 gap-4", children: [
512
- /* @__PURE__ */ jsx(Detail, { label: "Product", value: record.product_id, mono: true }),
513
- /* @__PURE__ */ jsx(Detail, { label: "Action", value: record.action }),
514
- /* @__PURE__ */ jsx(Detail, { label: "Faire product token", value: record.product_token || "—", mono: true }),
515
- /* @__PURE__ */ jsx(Detail, { label: "State", value: record.product_state || "—" }),
516
- /* @__PURE__ */ jsx(Detail, { label: "Published", value: record.published ? "Yes" : "No" }),
517
- /* @__PURE__ */ jsx(
518
- Detail,
519
- {
520
- label: "Synced at",
521
- value: record.synced_at ? new Date(record.synced_at).toLocaleString() : "—"
522
- }
523
- )
524
- ] })
525
- ] }),
526
- warnings.length > 0 && /* @__PURE__ */ jsxs(Container, { className: "divide-y p-0", children: [
527
- /* @__PURE__ */ jsx("div", { className: "px-6 py-4", children: /* @__PURE__ */ jsx(Heading, { level: "h2", children: "Issues" }) }),
528
- /* @__PURE__ */ jsx("div", { className: "px-6 py-4 flex flex-col gap-2", children: warnings.map((w, i) => /* @__PURE__ */ jsx(Text, { className: "text-ui-tag-red-text text-sm", children: w }, i)) })
529
- ] }),
530
- /* @__PURE__ */ jsx(Toaster, {})
531
- ] });
532
- };
533
- const Detail = ({
534
- label,
535
- value,
536
- mono
537
- }) => /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-1", children: [
538
- /* @__PURE__ */ jsx(Label, { children: label }),
539
- /* @__PURE__ */ jsx(Text, { className: mono ? "font-mono text-xs" : "", children: value })
540
- ] });
541
- const handle$2 = {
542
- breadcrumb: () => "Sync record"
543
- };
544
481
  const Root = ({ prev = "..", children }) => {
545
482
  const navigate = useNavigate();
546
483
  const [open, setOpen] = useState(false);
@@ -837,9 +774,122 @@ const FaireBulkPage = () => {
837
774
  /* @__PURE__ */ jsx(Toaster, {})
838
775
  ] });
839
776
  };
840
- const handle$1 = {
777
+ const handle$2 = {
841
778
  breadcrumb: () => "Bulk sync"
842
779
  };
780
+ const PARENT$1 = "/settings/faire";
781
+ const STATUS_COLORS = {
782
+ success: "green",
783
+ failed: "red",
784
+ draft: "orange",
785
+ pending: "grey",
786
+ syncing: "grey"
787
+ };
788
+ const FaireSyncDetailPage = () => {
789
+ const { id } = useParams();
790
+ const navigate = useNavigate();
791
+ const [record, setRecord] = useState(null);
792
+ const [loading, setLoading] = useState(true);
793
+ const [retrying, setRetrying] = useState(false);
794
+ const [error, setError] = useState(null);
795
+ useEffect(() => {
796
+ if (!id) return;
797
+ setLoading(true);
798
+ faireApi.getSync(id).then((res) => {
799
+ setRecord(res.sync || res);
800
+ }).catch((err) => setError(err.message || "Failed to load sync record")).finally(() => setLoading(false));
801
+ }, [id]);
802
+ const handleRetry = async () => {
803
+ if (!record) return;
804
+ setRetrying(true);
805
+ setError(null);
806
+ try {
807
+ await faireApi.retrySync(record.id);
808
+ const updated = await faireApi.getSync(record.id).catch(() => null);
809
+ if (updated) setRecord(updated.sync || updated);
810
+ } catch (err) {
811
+ setError(err.message || "Retry failed");
812
+ } finally {
813
+ setRetrying(false);
814
+ }
815
+ };
816
+ const warnings = (record == null ? void 0 : record.error_message) ? record.error_message.split(" | ") : [];
817
+ return /* @__PURE__ */ jsxs(RouteFocusModal, { prev: PARENT$1, children: [
818
+ /* @__PURE__ */ jsxs(RouteFocusModal.Header, { children: [
819
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-3", children: [
820
+ /* @__PURE__ */ jsx(Heading, { level: "h1", children: "Sync record" }),
821
+ record && /* @__PURE__ */ jsx(StatusBadge, { color: STATUS_COLORS[record.status] || "grey", children: record.status })
822
+ ] }),
823
+ record && /* @__PURE__ */ jsx("div", { className: "flex items-center gap-2", children: /* @__PURE__ */ jsxs(DropdownMenu, { children: [
824
+ /* @__PURE__ */ jsx(DropdownMenu.Trigger, { asChild: true, children: /* @__PURE__ */ jsx(IconButton, { size: "small", variant: "transparent", disabled: retrying, children: /* @__PURE__ */ jsx(EllipsisHorizontal, {}) }) }),
825
+ /* @__PURE__ */ jsxs(DropdownMenu.Content, { align: "end", children: [
826
+ /* @__PURE__ */ jsxs(
827
+ DropdownMenu.Item,
828
+ {
829
+ className: "gap-x-2",
830
+ onClick: handleRetry,
831
+ disabled: retrying,
832
+ children: [
833
+ /* @__PURE__ */ jsx(ArrowPath, { className: "text-ui-fg-subtle" }),
834
+ retrying ? "Re-syncing…" : "Re-sync product"
835
+ ]
836
+ }
837
+ ),
838
+ record.product_url && /* @__PURE__ */ jsxs(
839
+ DropdownMenu.Item,
840
+ {
841
+ className: "gap-x-2",
842
+ onClick: () => window.open(record.product_url, "_blank"),
843
+ children: [
844
+ /* @__PURE__ */ jsx(ArrowUpRightOnBox, { className: "text-ui-fg-subtle" }),
845
+ "Open on Faire"
846
+ ]
847
+ }
848
+ )
849
+ ] })
850
+ ] }) })
851
+ ] }),
852
+ /* @__PURE__ */ jsxs(RouteFocusModal.Body, { className: "flex flex-col gap-4 overflow-y-auto p-6", children: [
853
+ error && /* @__PURE__ */ jsx(Text, { className: "text-ui-tag-red-text", children: error }),
854
+ loading ? /* @__PURE__ */ jsx("div", { className: "flex flex-col gap-3", children: Array.from({ length: 4 }).map((_, i) => /* @__PURE__ */ jsx(Skeleton, { className: "h-10 w-full" }, i)) }) : !record ? /* @__PURE__ */ jsxs("div", { className: "flex flex-col items-start gap-4", children: [
855
+ /* @__PURE__ */ jsx(Heading, { level: "h2", children: "Sync record not found" }),
856
+ /* @__PURE__ */ jsx(Button, { size: "small", variant: "secondary", onClick: () => navigate(PARENT$1), children: "Back to Faire settings" })
857
+ ] }) : /* @__PURE__ */ jsxs(Fragment, { children: [
858
+ /* @__PURE__ */ jsxs("div", { className: "border-ui-border-base grid grid-cols-2 gap-4 rounded-lg border p-4", children: [
859
+ /* @__PURE__ */ jsx(Detail, { label: "Record id", value: record.id, mono: true }),
860
+ /* @__PURE__ */ jsx(Detail, { label: "Product", value: record.product_id, mono: true }),
861
+ /* @__PURE__ */ jsx(Detail, { label: "Action", value: record.action }),
862
+ /* @__PURE__ */ jsx(Detail, { label: "Faire product token", value: record.product_token || "—", mono: true }),
863
+ /* @__PURE__ */ jsx(Detail, { label: "State", value: record.product_state || "—" }),
864
+ /* @__PURE__ */ jsx(Detail, { label: "Published", value: record.published ? "Yes" : "No" }),
865
+ /* @__PURE__ */ jsx(
866
+ Detail,
867
+ {
868
+ label: "Synced at",
869
+ value: record.synced_at ? new Date(record.synced_at).toLocaleString() : "—"
870
+ }
871
+ )
872
+ ] }),
873
+ warnings.length > 0 && /* @__PURE__ */ jsxs("div", { className: "border-ui-border-base flex flex-col gap-2 rounded-lg border p-4", children: [
874
+ /* @__PURE__ */ jsx(Heading, { level: "h2", children: "Issues" }),
875
+ warnings.map((w, i) => /* @__PURE__ */ jsx(Text, { className: "text-ui-tag-red-text text-sm", children: w }, i))
876
+ ] })
877
+ ] })
878
+ ] }),
879
+ /* @__PURE__ */ jsx(Toaster, {})
880
+ ] });
881
+ };
882
+ const Detail = ({
883
+ label,
884
+ value,
885
+ mono
886
+ }) => /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-1", children: [
887
+ /* @__PURE__ */ jsx(Label, { children: label }),
888
+ /* @__PURE__ */ jsx(Text, { className: mono ? "font-mono text-xs" : "", children: value })
889
+ ] });
890
+ const handle$1 = {
891
+ breadcrumb: () => "Sync record"
892
+ };
843
893
  const STATUS_KEY = ["faire", "status"];
844
894
  const PARENT = "/settings/faire";
845
895
  const NONE = "__none__";
@@ -1107,13 +1157,13 @@ const routeModule = {
1107
1157
  handle: handle$3
1108
1158
  },
1109
1159
  {
1110
- Component: FaireSyncDetailPage,
1111
- path: "/settings/faire/:id",
1160
+ Component: FaireBulkPage,
1161
+ path: "/settings/faire/bulk",
1112
1162
  handle: handle$2
1113
1163
  },
1114
1164
  {
1115
- Component: FaireBulkPage,
1116
- path: "/settings/faire/bulk",
1165
+ Component: FaireSyncDetailPage,
1166
+ path: "/settings/faire/:id",
1117
1167
  handle: handle$1
1118
1168
  },
1119
1169
  {