react-shopwave-connect 0.4.6 → 0.4.7

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.
@@ -179,12 +179,15 @@ async function apiSend(path, method, payload, options = {}) {
179
179
  /**
180
180
  * DELETE helper. Shopwave answers deletes with 205 and an empty body; if a body
181
181
  * does come back and carries API errors, they are thrown. Returns the parsed
182
- * body (`null` when empty).
182
+ * body (`null` when empty). `extras`, when given, travel in the `extras` header
183
+ * (the server forwards only keys its delete route allows).
183
184
  */
184
- async function apiDelete(path, options = {}) {
185
+ async function apiDelete(path, options = {}, extras) {
186
+ const headers = baseHeaders(options);
187
+ if (extras) headers.extras = JSON.stringify(buildExtras(extras));
185
188
  const { status, body } = await send(path, {
186
189
  method: "DELETE",
187
- headers: baseHeaders(options)
190
+ headers
188
191
  }, options);
189
192
  if (getApiErrorMap(body)) throw toShopwaveApiError(status, body);
190
193
  return body;
@@ -371,115 +374,6 @@ function fetchProductInstanceTags(options = {}) {
371
374
  return fetchLookup("productInstanceTags", options);
372
375
  }
373
376
 
374
- //#endregion
375
- //#region src/core/promotion.ts
376
- /**
377
- * Fetches promotions. Returns the flattened `Promotion[]`.
378
- */
379
- async function fetchPromotions(params = {}, options = {}) {
380
- const extras = { deleted: params.deleted ?? false };
381
- if (params.promotionIds) extras.promotionIds = params.promotionIds;
382
- if (params.imminent !== void 0) extras.imminent = params.imminent;
383
- if (params.active !== void 0) extras.active = params.active;
384
- if (params.expired !== void 0) extras.expired = params.expired;
385
- const json = await apiGet("/api/promotions", extras, withToken(options, params.token));
386
- assertNoApiErrors(json, 200);
387
- return Object.values(json?.promotions ?? {});
388
- }
389
-
390
- //#endregion
391
- //#region src/core/store.ts
392
- /**
393
- * Fetches stores. Returns the flattened `Store[]`.
394
- */
395
- async function fetchStores(params = {}, options = {}) {
396
- const extras = { deleted: params.deleted ?? false };
397
- if (params.storeIds) extras.storeIds = params.storeIds;
398
- const json = await apiGet("/api/stores", extras, withToken(options, params.token));
399
- assertNoApiErrors(json, 200);
400
- return Object.values(json?.stores ?? {});
401
- }
402
-
403
- //#endregion
404
- //#region src/core/report.ts
405
- /**
406
- * Runs one or more report queries and returns the `reports` map keyed by query
407
- * name. The query is JSON-encoded into the `extras.query` header, matching the
408
- * original endpoint contract.
409
- */
410
- async function fetchReport(query, options = {}) {
411
- const json = await apiGet("/api/report", { query: JSON.stringify(query) }, options);
412
- assertNoApiErrors(json, 200);
413
- return json?.reports ?? {};
414
- }
415
-
416
- //#endregion
417
- //#region src/core/session.ts
418
- /**
419
- * Reads the current login status from `GET /api/session`.
420
- *
421
- * Works with the new `react-shopwave-connect/next` session route
422
- * (`{ loggedIn }`) and with older routes that returned the raw session
423
- * (`{ token: {...} }`) — in that case only a boolean is surfaced.
424
- */
425
- async function fetchSession(options = {}) {
426
- const doFetch = options.fetch ?? (typeof fetch !== "undefined" ? fetch : void 0);
427
- if (!doFetch) throw new Error("No fetch implementation available. Pass `options.fetch`.");
428
- const response = await doFetch(`${options.baseUrl ?? ""}/api/session`, {
429
- method: "GET",
430
- headers: { Accept: "application/json" },
431
- cache: "no-store",
432
- credentials: "same-origin",
433
- signal: options.signal
434
- });
435
- if (response.status === 401) return { loggedIn: false };
436
- if (!response.ok) throw new Error(`Session request failed (${response.status})`);
437
- const json = await response.json();
438
- if (json && typeof json.loggedIn === "boolean") return {
439
- loggedIn: json.loggedIn,
440
- expiresAt: typeof json.expiresAt === "number" ? json.expiresAt : void 0
441
- };
442
- return { loggedIn: Boolean(json && json.token) };
443
- }
444
- /**
445
- * Ends the current server session (no redirect).
446
- *
447
- * Prefer navigating to {@link logoutPath} when using
448
- * `react-shopwave-connect/next`: it also signs the user out of the auth server.
449
- */
450
- async function logout(options = {}) {
451
- await apiDelete("/api/session?action=logout", options);
452
- }
453
-
454
- //#endregion
455
- //#region src/core/entity.ts
456
- /**
457
- * Low-level, untyped mutations: the caller supplies the route and payload.
458
- *
459
- * Prefer the typed functions in `./entities` (`saveProduct`, `deleteCategory`,
460
- * …): they know each route, build the request envelope, check the response and
461
- * return the saved entity with its id. Keep these for routes that have no typed
462
- * function yet (e.g. `/api/merchant`).
463
- */
464
- /**
465
- * Deletes `/api/{resourcePath}/{entityId}`.
466
- */
467
- async function deleteEntity(resourcePath, entityId, options = {}) {
468
- if (entityId == null || entityId === "") throw new Error("entityId is missing");
469
- await apiDelete(`/api/${resourcePath}/${entityId}`, options);
470
- }
471
- /**
472
- * Sends a create/update mutation. Returns the response's `result` field when it
473
- * has one (older app routes wrapped their answer that way), otherwise the whole
474
- * parsed response body. Throws `ShopwaveApiError` on HTTP or API errors.
475
- */
476
- async function submitEntity({ endpoint, method = "POST", payload }, options = {}) {
477
- const json = await apiSend(endpoint, method, payload, options);
478
- assertNoApiErrors(json, 200);
479
- if (json && typeof json === "object" && "result" in json) return json.result;
480
- return json ?? void 0;
481
- }
482
-
483
377
  //#endregion
484
378
  //#region src/core/imageFit.ts
485
379
  /**
@@ -689,6 +583,245 @@ async function uploadMerchantImage(file, slot, options = {}) {
689
583
  return uploadImage(fit ? await fitImageToSize(file, def.width, def.height, fitOptions) : file, { kind: def.uploadKind }, requestOptions);
690
584
  }
691
585
 
586
+ //#endregion
587
+ //#region src/core/entities.ts
588
+ /**
589
+ * Addressing per entity, following the Shopwave API reference
590
+ * (https://developer.merchantstack.com/api-reference.html) and checked against
591
+ * the live API where noted:
592
+ * - category/product/store: GET filtered by `<x>Ids`, POST upsert, DELETE with `<x>Id` → 205.
593
+ * - promotion: no DELETE; promotions are never deleted, only expired. Expiring
594
+ * reads the promotion and saves the whole record back with `endDate` = now (and
595
+ * `startDate` = now too if it hadn't started); the live API rejects a partial
596
+ * `{ id, endDate }` save with error 899. Dates are UTC `YYYY-MM-DD HH:mm:ss`.
597
+ * - employee: POST updates `roleId`, `joinedDate`, `exitDate` (names can't be
598
+ * changed); no DELETE (live API: "Cannot DELETE /employee") → retired via `exitDate`.
599
+ * - consumer: read-only (`GET` with `ids`, comma-separated); live POST → 404.
600
+ * - productComponent: modifier groups, `/product/component` (live, staging, 11 Nov 2026). The
601
+ * API reference lists no read filters; these are what the live API does:
602
+ * - Reads filter by `componentIds` (group ids) or `productIds` (the parent product; option
603
+ * products don't match). `productComponentIds` is ignored on reads: every group comes back.
604
+ * `deleted: false` gives live groups only, `true` deleted groups only, no header both.
605
+ * No match → 204 with an empty body. No order, sort, priority or position field anywhere.
606
+ * - Groups are keyed by id; `products` (option products) by product id; `productInstances`
607
+ * (option rows) by row id. Reads reformat values: price `"100"` → `"100.0"`,
608
+ * vatPercentage `"0.2000"` → `"0.200"`. `tags` and `name` are stored as sent (no trimming;
609
+ * a `tags` array is joined with commas; `tags` null or left out → null).
610
+ * - POST upserts (201, echoed under the ref). For each option product in `products`, its rows
611
+ * are replaced: every row sent is created afresh with a new id (a row `id` is ignored, also
612
+ * on a price change) and rows left out are removed. Option products left out of the save
613
+ * are untouched, and an empty `productInstances` removes nothing. A save without `products`
614
+ * updates only the group's own fields. The echo isn't proof of what was stored: renames of
615
+ * groups created moments earlier by a test run echoed the new name but never stuck (re-read
616
+ * for 90 s), while older groups, and groups created in Shopwave's own admin, renamed at
617
+ * once. Cause not yet known.
618
+ * - DELETE with `productComponentIds` (plural, comma list) → 205, soft delete. Adding
619
+ * `productComponentInstanceIds` deletes just those rows (205); they stay in live reads with
620
+ * `deleteDate` set. The singular `productComponentId` hangs (504 after 60 s).
621
+ */
622
+ const SHOPWAVE_ENTITIES = Object.freeze({
623
+ product: {
624
+ route: "products",
625
+ collection: "products",
626
+ upstream: "product",
627
+ idsHeader: "productIds",
628
+ idHeader: "productId",
629
+ writable: true,
630
+ deleteMode: "delete"
631
+ },
632
+ category: {
633
+ route: "categories",
634
+ collection: "categories",
635
+ upstream: "category",
636
+ idsHeader: "categoryIds",
637
+ idHeader: "categoryId",
638
+ writable: true,
639
+ deleteMode: "delete"
640
+ },
641
+ store: {
642
+ route: "stores",
643
+ collection: "stores",
644
+ upstream: "store",
645
+ idsHeader: "storeIds",
646
+ idHeader: "storeId",
647
+ writable: true,
648
+ deleteMode: "delete"
649
+ },
650
+ promotion: {
651
+ route: "promotions",
652
+ collection: "promotions",
653
+ upstream: "promotion",
654
+ idsHeader: "promotionIds",
655
+ idHeader: "promotionId",
656
+ writable: true,
657
+ deleteMode: "retire",
658
+ retireField: "endDate",
659
+ retireWithRecord: true,
660
+ retireStartField: "startDate"
661
+ },
662
+ employee: {
663
+ route: "employees",
664
+ collection: "employees",
665
+ upstream: "employee",
666
+ idsHeader: "employeeIds",
667
+ idHeader: "employeeId",
668
+ writable: true,
669
+ deleteMode: "retire",
670
+ retireField: "exitDate"
671
+ },
672
+ consumer: {
673
+ route: "consumer",
674
+ collection: "consumers",
675
+ upstream: "consumer",
676
+ idsHeader: "ids",
677
+ idHeader: "consumerId",
678
+ writable: false,
679
+ deleteMode: "none"
680
+ },
681
+ productComponent: {
682
+ route: "product-components",
683
+ collection: "productComponents",
684
+ upstream: "product/component",
685
+ idsHeader: "componentIds",
686
+ idHeader: "productComponentIds",
687
+ writable: true,
688
+ deleteMode: "delete",
689
+ childIdsHeader: "productComponentInstanceIds"
690
+ }
691
+ });
692
+
693
+ //#endregion
694
+ //#region src/core/productComponent.ts
695
+ const isObject = (v) => !!v && typeof v === "object" && !Array.isArray(v);
696
+ /**
697
+ * Reads product components, optionally filtered by parent product and/or group id.
698
+ * With no filter it returns every group on the merchant. Returns `[]` when nothing matches
699
+ * (the API answers 204), and when a filter list is empty (no request is made).
700
+ *
701
+ * Rows deleted with {@link deleteProductComponentInstances} are still returned, with
702
+ * `deleteDate` set; skip them when listing options.
703
+ */
704
+ async function fetchProductComponents(params = {}, options = {}) {
705
+ const def = SHOPWAVE_ENTITIES.productComponent;
706
+ if (params.productIds?.length === 0 || params.productComponentIds?.length === 0) return [];
707
+ const extras = { deleted: params.deleted ?? false };
708
+ if (params.productIds) extras.productIds = params.productIds;
709
+ if (params.productComponentIds) extras[def.idsHeader] = params.productComponentIds;
710
+ const json = await apiGet(`/api/${def.route}`, extras, options);
711
+ assertNoApiErrors(json, 200);
712
+ const map = json?.[def.collection];
713
+ return isObject(map) ? Object.values(map) : [];
714
+ }
715
+
716
+ //#endregion
717
+ //#region src/core/promotion.ts
718
+ /**
719
+ * Fetches promotions. Returns the flattened `Promotion[]`.
720
+ */
721
+ async function fetchPromotions(params = {}, options = {}) {
722
+ const extras = { deleted: params.deleted ?? false };
723
+ if (params.promotionIds) extras.promotionIds = params.promotionIds;
724
+ if (params.imminent !== void 0) extras.imminent = params.imminent;
725
+ if (params.active !== void 0) extras.active = params.active;
726
+ if (params.expired !== void 0) extras.expired = params.expired;
727
+ const json = await apiGet("/api/promotions", extras, withToken(options, params.token));
728
+ assertNoApiErrors(json, 200);
729
+ return Object.values(json?.promotions ?? {});
730
+ }
731
+
732
+ //#endregion
733
+ //#region src/core/store.ts
734
+ /**
735
+ * Fetches stores. Returns the flattened `Store[]`.
736
+ */
737
+ async function fetchStores(params = {}, options = {}) {
738
+ const extras = { deleted: params.deleted ?? false };
739
+ if (params.storeIds) extras.storeIds = params.storeIds;
740
+ const json = await apiGet("/api/stores", extras, withToken(options, params.token));
741
+ assertNoApiErrors(json, 200);
742
+ return Object.values(json?.stores ?? {});
743
+ }
744
+
745
+ //#endregion
746
+ //#region src/core/report.ts
747
+ /**
748
+ * Runs one or more report queries and returns the `reports` map keyed by query
749
+ * name. The query is JSON-encoded into the `extras.query` header, matching the
750
+ * original endpoint contract.
751
+ */
752
+ async function fetchReport(query, options = {}) {
753
+ const json = await apiGet("/api/report", { query: JSON.stringify(query) }, options);
754
+ assertNoApiErrors(json, 200);
755
+ return json?.reports ?? {};
756
+ }
757
+
758
+ //#endregion
759
+ //#region src/core/session.ts
760
+ /**
761
+ * Reads the current login status from `GET /api/session`.
762
+ *
763
+ * Works with the new `react-shopwave-connect/next` session route
764
+ * (`{ loggedIn }`) and with older routes that returned the raw session
765
+ * (`{ token: {...} }`) — in that case only a boolean is surfaced.
766
+ */
767
+ async function fetchSession(options = {}) {
768
+ const doFetch = options.fetch ?? (typeof fetch !== "undefined" ? fetch : void 0);
769
+ if (!doFetch) throw new Error("No fetch implementation available. Pass `options.fetch`.");
770
+ const response = await doFetch(`${options.baseUrl ?? ""}/api/session`, {
771
+ method: "GET",
772
+ headers: { Accept: "application/json" },
773
+ cache: "no-store",
774
+ credentials: "same-origin",
775
+ signal: options.signal
776
+ });
777
+ if (response.status === 401) return { loggedIn: false };
778
+ if (!response.ok) throw new Error(`Session request failed (${response.status})`);
779
+ const json = await response.json();
780
+ if (json && typeof json.loggedIn === "boolean") return {
781
+ loggedIn: json.loggedIn,
782
+ expiresAt: typeof json.expiresAt === "number" ? json.expiresAt : void 0
783
+ };
784
+ return { loggedIn: Boolean(json && json.token) };
785
+ }
786
+ /**
787
+ * Ends the current server session (no redirect).
788
+ *
789
+ * Prefer navigating to {@link logoutPath} when using
790
+ * `react-shopwave-connect/next`: it also signs the user out of the auth server.
791
+ */
792
+ async function logout(options = {}) {
793
+ await apiDelete("/api/session?action=logout", options);
794
+ }
795
+
796
+ //#endregion
797
+ //#region src/core/entity.ts
798
+ /**
799
+ * Low-level, untyped mutations: the caller supplies the route and payload.
800
+ *
801
+ * Prefer the typed functions in `./entities` (`saveProduct`, `deleteCategory`,
802
+ * …): they know each route, build the request envelope, check the response and
803
+ * return the saved entity with its id. Keep these for routes that have no typed
804
+ * function yet (e.g. `/api/merchant`).
805
+ */
806
+ /**
807
+ * Deletes `/api/{resourcePath}/{entityId}`.
808
+ */
809
+ async function deleteEntity(resourcePath, entityId, options = {}) {
810
+ if (entityId == null || entityId === "") throw new Error("entityId is missing");
811
+ await apiDelete(`/api/${resourcePath}/${entityId}`, options);
812
+ }
813
+ /**
814
+ * Sends a create/update mutation. Returns the response's `result` field when it
815
+ * has one (older app routes wrapped their answer that way), otherwise the whole
816
+ * parsed response body. Throws `ShopwaveApiError` on HTTP or API errors.
817
+ */
818
+ async function submitEntity({ endpoint, method = "POST", payload }, options = {}) {
819
+ const json = await apiSend(endpoint, method, payload, options);
820
+ assertNoApiErrors(json, 200);
821
+ if (json && typeof json === "object" && "result" in json) return json.result;
822
+ return json ?? void 0;
823
+ }
824
+
692
825
  //#endregion
693
826
  //#region src/core/basket.ts
694
827
  const DEFAULT_BASKET_FILTERS = {
@@ -1203,6 +1336,25 @@ function useProductInstanceTags(options) {
1203
1336
  }), [options?.baseUrl, options?.token], options?.enabled ?? true);
1204
1337
  }
1205
1338
 
1339
+ //#endregion
1340
+ //#region src/hooks/useProductComponent.ts
1341
+ /**
1342
+ * Auto-fetches product components (modifier groups) on mount and whenever the params change:
1343
+ * a product's groups with `{ productIds: [id] }`, one group with `{ productComponentIds: [id] }`,
1344
+ * or every group with no filter. Option rows with a `deleteDate` are included; skip them when
1345
+ * listing options.
1346
+ */
1347
+ function useProductComponent(params = {}, options) {
1348
+ return useQuery((signal) => fetchProductComponents(params, {
1349
+ ...options,
1350
+ signal
1351
+ }), [
1352
+ JSON.stringify(params),
1353
+ options?.baseUrl,
1354
+ options?.token
1355
+ ]);
1356
+ }
1357
+
1206
1358
  //#endregion
1207
1359
  //#region src/hooks/usePromotion.ts
1208
1360
  /**
@@ -1513,5 +1665,5 @@ function useBasketReport(options) {
1513
1665
  }
1514
1666
 
1515
1667
  //#endregion
1516
- export { useBasketReport, useCategory, useConsumer, useDelete, useEmployee, useLogout, useMerchant, useProduct, useProductInstanceTags, useProductTags, usePromotion, useReport, useSession, useStore, useSubmit, useUpdateMerchant, useUploadImage, useUploadMerchantImage, useUser };
1668
+ export { useBasketReport, useCategory, useConsumer, useDelete, useEmployee, useLogout, useMerchant, useProduct, useProductComponent, useProductInstanceTags, useProductTags, usePromotion, useReport, useSession, useStore, useSubmit, useUpdateMerchant, useUploadImage, useUploadMerchantImage, useUser };
1517
1669
  //# sourceMappingURL=index.js.map