react-shopwave-connect 0.4.3 → 0.4.4

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/dist/index.js CHANGED
@@ -383,17 +383,29 @@ const UPLOAD_KINDS = Object.freeze({
383
383
  applicationLogo: "applicaionLogo",
384
384
  applicationImages: "applicaionImages"
385
385
  });
386
+ /** Every `UploadPrefix`, longest first so a name check never takes `product` for `product_thumbnail`. */
387
+ const UPLOAD_PREFIXES = Object.freeze([
388
+ "product_thumbnail",
389
+ "variant_thumbnail",
390
+ "product",
391
+ "variant"
392
+ ]);
386
393
  /** How uploads are addressed (app route and Shopwave uploader). */
387
394
  const SHOPWAVE_UPLOAD = Object.freeze({
388
395
  route: "upload",
389
396
  upstream: "uploader",
390
397
  kindHeader: "contentType",
398
+ prefixHeader: "prefix",
391
399
  fileField: "file",
392
- kindField: "kind"
400
+ kindField: "kind",
401
+ prefixField: "prefix"
393
402
  });
394
403
  function isUploadKind(value) {
395
404
  return typeof value === "string" && Object.prototype.hasOwnProperty.call(UPLOAD_KINDS, value);
396
405
  }
406
+ function isUploadPrefix(value) {
407
+ return typeof value === "string" && UPLOAD_PREFIXES.includes(value);
408
+ }
397
409
  const SHOPWAVE_LOOKUPS = Object.freeze({
398
410
  productTags: {
399
411
  route: "products/tags",
@@ -579,443 +591,109 @@ async function submitEntity({ endpoint, method = "POST", payload }, options = {}
579
591
  }
580
592
 
581
593
  //#endregion
582
- //#region src/core/entities.ts
594
+ //#region src/core/imageFit.ts
583
595
  /**
584
- * Addressing per entity, following the Shopwave API reference
585
- * (https://developer.merchantstack.com/api-reference.html) and checked against
586
- * the live API where noted:
587
- * - category/product/store: GET filtered by `<x>Ids`, POST upsert, DELETE with `<x>Id` → 205.
588
- * - promotion: no DELETE; promotions are never deleted, only expired. Expiring
589
- * reads the promotion and saves the whole record back with `endDate` = now (and
590
- * `startDate` = now too if it hadn't started); the live API rejects a partial
591
- * `{ id, endDate }` save with error 899. Dates are UTC `YYYY-MM-DD HH:mm:ss`.
592
- * - employee: POST updates `roleId`, `joinedDate`, `exitDate` (names can't be
593
- * changed); no DELETE (live API: "Cannot DELETE /employee") → retired via `exitDate`.
594
- * - consumer: read-only (`GET` with `ids`, comma-separated); live POST → 404.
596
+ * The centred part of a `srcWidth × srcHeight` image with the aspect ratio of
597
+ * `dstWidth × dstHeight` (like CSS `object-fit: cover`).
595
598
  */
596
- const SHOPWAVE_ENTITIES = Object.freeze({
597
- product: {
598
- route: "products",
599
- collection: "products",
600
- upstream: "product",
601
- idsHeader: "productIds",
602
- idHeader: "productId",
603
- writable: true,
604
- deleteMode: "delete"
605
- },
606
- category: {
607
- route: "categories",
608
- collection: "categories",
609
- upstream: "category",
610
- idsHeader: "categoryIds",
611
- idHeader: "categoryId",
612
- writable: true,
613
- deleteMode: "delete"
614
- },
615
- store: {
616
- route: "stores",
617
- collection: "stores",
618
- upstream: "store",
619
- idsHeader: "storeIds",
620
- idHeader: "storeId",
621
- writable: true,
622
- deleteMode: "delete"
623
- },
624
- promotion: {
625
- route: "promotions",
626
- collection: "promotions",
627
- upstream: "promotion",
628
- idsHeader: "promotionIds",
629
- idHeader: "promotionId",
630
- writable: true,
631
- deleteMode: "retire",
632
- retireField: "endDate",
633
- retireWithRecord: true,
634
- retireStartField: "startDate"
635
- },
636
- employee: {
637
- route: "employees",
638
- collection: "employees",
639
- upstream: "employee",
640
- idsHeader: "employeeIds",
641
- idHeader: "employeeId",
642
- writable: true,
643
- deleteMode: "retire",
644
- retireField: "exitDate"
645
- },
646
- consumer: {
647
- route: "consumer",
648
- collection: "consumers",
649
- upstream: "consumer",
650
- idsHeader: "ids",
651
- idHeader: "consumerId",
652
- writable: false,
653
- deleteMode: "none"
599
+ function coverCrop(srcWidth, srcHeight, dstWidth, dstHeight) {
600
+ if (![
601
+ srcWidth,
602
+ srcHeight,
603
+ dstWidth,
604
+ dstHeight
605
+ ].every((n) => Number.isFinite(n) && n > 0)) throw new Error("coverCrop: sizes must be positive numbers");
606
+ const srcRatio = srcWidth / srcHeight;
607
+ const dstRatio = dstWidth / dstHeight;
608
+ if (srcRatio > dstRatio) {
609
+ const sw = Math.round(srcHeight * dstRatio);
610
+ return {
611
+ sx: Math.round((srcWidth - sw) / 2),
612
+ sy: 0,
613
+ sw,
614
+ sh: srcHeight
615
+ };
654
616
  }
655
- });
656
- function definition(kind) {
657
- const def = SHOPWAVE_ENTITIES[kind];
658
- if (!def) throw new Error(`Unknown Shopwave entity "${kind}"`);
659
- return def;
617
+ const sh = Math.round(srcWidth / dstRatio);
618
+ return {
619
+ sx: 0,
620
+ sy: Math.round((srcHeight - sh) / 2),
621
+ sw: srcWidth,
622
+ sh
623
+ };
660
624
  }
661
- function assertId(kind, id) {
662
- const s = id == null ? "" : String(id).trim();
663
- if (!s) throw new Error(`${kind} id is missing`);
664
- return encodeURIComponent(s);
625
+ function requireBrowserImages(fn) {
626
+ if (typeof createImageBitmap !== "function") throw new Error(`${fn} needs a browser (createImageBitmap is not available)`);
665
627
  }
666
- /**
667
- * Creates or updates several entities of one kind in a single request and
668
- * returns them as saved (with their ids), in the order given.
669
- *
670
- * The request is `{ <collection>: { "0": item0, "1": item1, … } }`. Shopwave
671
- * answers 201 and echoes every entity under the ref it was sent with, so each
672
- * result is matched back by ref. Throws {@link ShopwaveApiError} when the
673
- * response carries `api.message.errors` or any ref is missing from it.
674
- *
675
- * The API's partial-success semantics for multi-entity saves haven't been
676
- * confirmed yet; single saves (`saveProduct` etc.) are the tested path.
677
- */
678
- async function saveEntities(kind, items, options = {}) {
679
- const def = definition(kind);
680
- if (items.length === 0) return [];
681
- const refs = items.map((_, i) => String(i));
682
- const payload = { [def.collection]: Object.fromEntries(refs.map((ref, i) => [ref, items[i]])) };
683
- const { status, body: raw } = await apiRequest(`/api/${def.route}`, {
684
- method: "POST",
685
- headers: { "Content-Type": "application/json" },
686
- body: JSON.stringify(payload)
687
- }, options);
688
- const body = raw;
689
- assertNoApiErrors(body, status);
690
- const echoed = body?.[def.collection] ?? {};
691
- return refs.map((ref, i) => {
692
- const saved = echoed[ref];
693
- if (!saved || saved.id == null) throw new ShopwaveApiError(status, `Shopwave API error (${status}): the saved ${kind} (ref "${ref}") was not returned`, { body });
628
+ /** Reads an image file's pixel size (browser only). */
629
+ async function readImageSize(file) {
630
+ requireBrowserImages("readImageSize");
631
+ const bitmap = await createImageBitmap(file);
632
+ try {
694
633
  return {
695
- ...items[i],
696
- ...saved,
697
- id: Number(saved.id)
634
+ width: bitmap.width,
635
+ height: bitmap.height
698
636
  };
699
- });
700
- }
701
- /** Creates (no `id`) or updates (with `id`) one entity and returns it with its id. */
702
- async function saveEntity(kind, item, options = {}) {
703
- const [saved] = await saveEntities(kind, [item], options);
704
- return saved;
637
+ } finally {
638
+ bitmap.close?.();
639
+ }
705
640
  }
706
641
  /**
707
- * Deletes one entity (a soft delete: it stays readable with `deleted: true`).
708
- * Employees and promotions can't be deleted in Shopwave; for them the route
709
- * sets `exitDate` / `endDate` to now instead (see `SHOPWAVE_ENTITIES`), and a
710
- * promotion is saved back whole (see {@link deletePromotion}).
711
- *
712
- * Shopwave answers 205 with an empty body — also for ids that don't exist — so
713
- * a resolved promise means "the API accepted the request", not "a record was
714
- * deleted".
642
+ * Returns the image at exactly `width × height`: scaled and centre-cropped
643
+ * (never stretched). Browser only. The result is a `File` named like the
644
+ * input, with the output type's extension.
715
645
  */
716
- async function deleteEntityById(kind, id, options = {}) {
717
- await apiDelete(`/api/${definition(kind).route}/${assertId(kind, id)}`, options);
718
- }
719
- /** Reads one entity by id, or `null` when there's no such (non-deleted) record. */
720
- async function fetchEntityById(kind, id, params = {}, options = {}) {
721
- const def = definition(kind);
722
- const body = await apiGet(`/api/${def.route}/${assertId(kind, id)}`, { deleted: params.deleted ?? false }, options);
723
- assertNoApiErrors(body, 200);
724
- const map = body?.[def.collection] ?? {};
725
- return map[String(id)] ?? Object.values(map).find((e) => String(e?.id) === String(id)) ?? null;
646
+ async function fitImageToSize(file, width, height, options = {}) {
647
+ requireBrowserImages("fitImageToSize");
648
+ const { type = "image/png", quality, keepIfExact = true } = options;
649
+ const name = file.name || "image";
650
+ const ext = type === "image/jpeg" ? "jpg" : type.split("/")[1];
651
+ const outName = `${name.replace(/\.[^.]+$/, "")}.${ext}`;
652
+ const bitmap = await createImageBitmap(file);
653
+ try {
654
+ if (keepIfExact && bitmap.width === width && bitmap.height === height && file.type === type) return file instanceof File ? file : new File([file], outName, { type });
655
+ const { sx, sy, sw, sh } = coverCrop(bitmap.width, bitmap.height, width, height);
656
+ let blob;
657
+ if (typeof OffscreenCanvas === "function") {
658
+ const canvas = new OffscreenCanvas(width, height);
659
+ const ctx = canvas.getContext("2d");
660
+ if (!ctx) throw new Error("fitImageToSize: no 2D canvas context");
661
+ ctx.imageSmoothingQuality = "high";
662
+ ctx.drawImage(bitmap, sx, sy, sw, sh, 0, 0, width, height);
663
+ blob = await canvas.convertToBlob({
664
+ type,
665
+ quality
666
+ });
667
+ } else {
668
+ const canvas = document.createElement("canvas");
669
+ canvas.width = width;
670
+ canvas.height = height;
671
+ const ctx = canvas.getContext("2d");
672
+ if (!ctx) throw new Error("fitImageToSize: no 2D canvas context");
673
+ ctx.imageSmoothingQuality = "high";
674
+ ctx.drawImage(bitmap, sx, sy, sw, sh, 0, 0, width, height);
675
+ blob = await new Promise((resolve, reject) => canvas.toBlob((b) => b ? resolve(b) : reject(/* @__PURE__ */ new Error("fitImageToSize: encoding failed")), type, quality));
676
+ }
677
+ return new File([blob], outName, { type });
678
+ } finally {
679
+ bitmap.close?.();
680
+ }
726
681
  }
682
+
683
+ //#endregion
684
+ //#region src/core/upload.ts
727
685
  /**
728
- * Creates (no `id`) or updates (with `id`) a product.
729
- *
730
- * Shopwave instances (prices) can't be updated, only replaced. Build
731
- * `instances` with {@link toProductInstancesForSave} (passing the product as last
732
- * read) so an edited price is sent as a new instance. As a safeguard, a new
733
- * product (no `id`, e.g. a duplicate) never sends instance ids: an id copied from
734
- * another product would otherwise point the save at that product's instances.
735
- */
736
- const saveProduct = (product, options) => {
737
- return saveEntity("product", (product.id == null || String(product.id).trim() === "") && product.instances != null ? {
738
- ...product,
739
- instances: toProductInstancesForSave(product.instances)
740
- } : product, options);
741
- };
742
- const deleteProduct = (id, options) => deleteEntityById("product", id, options);
743
- const fetchProduct = (id, params, options) => fetchEntityById("product", id, params, options);
744
- const saveCategory = (category, options) => saveEntity("category", category, options);
745
- const deleteCategory = (id, options) => deleteEntityById("category", id, options);
746
- const fetchCategory = (id, params, options) => fetchEntityById("category", id, params, options);
747
- const saveStore = (store, options) => saveEntity("store", store, options);
748
- const deleteStore = (id, options) => deleteEntityById("store", id, options);
749
- const fetchStore = (id, params, options) => fetchEntityById("store", id, params, options);
750
- const savePromotion = (promotion, options) => saveEntity("promotion", promotion, options);
751
- /**
752
- * Expires a promotion (Shopwave never deletes promotions). The route reads the
753
- * promotion and saves it back with `endDate` set to now; a promotion that hasn't
754
- * started yet gets `startDate` = `endDate` = now. Rejects if the promotion doesn't
755
- * exist or the save fails.
686
+ * HTTPS host that serves the Shopwave CDN's images. The API returns
687
+ * `http://static.merchantstack.com/images/…`, which has no HTTPS endpoint
688
+ * (checked Sep 2026), so pages served over HTTPS can't show it.
756
689
  */
757
- const deletePromotion = (id, options) => deleteEntityById("promotion", id, options);
758
- const fetchPromotion = (id, params, options) => fetchEntityById("promotion", id, params, options);
690
+ const SHOPWAVE_IMAGE_BASE_URL = "https://s3-eu-west-1.amazonaws.com/static.merchantstack.com/images";
691
+ const STATIC_HOST = /^https?:\/\/static\.merchantstack\.com\/images\//i;
759
692
  /**
760
- * Creates an employee, or updates one (with `id`). On update Shopwave only
761
- * applies `roleId`, `joinedDate` and `exitDate` (and store roles); names and
762
- * email can't be changed, and the echo only carries the updated fields — re-read
763
- * with `fetchEmployee` if you need the stored record.
764
- */
765
- const saveEmployee = (employee, options) => saveEntity("employee", employee, options);
766
- /** Retires an employee: Shopwave has no employee DELETE, so this sets `exitDate` to now. */
767
- const deleteEmployee = (id, options) => deleteEntityById("employee", id, options);
768
- const fetchEmployee = (id, params, options) => fetchEntityById("employee", id, params, options);
769
- const fetchConsumer = (id, params, options) => fetchEntityById("consumer", id, params, options);
770
-
771
- //#endregion
772
- //#region src/core/basket.ts
773
- const DEFAULT_BASKET_FILTERS = {
774
- search: "",
775
- dateRange: {
776
- start: null,
777
- end: null
778
- },
779
- consumerId: null,
780
- status: "all"
781
- };
782
- const DEFAULT_PAGINATION = {
783
- page: 0,
784
- pageSize: 25,
785
- total: 0
786
- };
787
- /** Formats a date as `YYYY-MM-DD HH:mm:ss` for the report query. */
788
- function formatDateForQuery(date, isEndOfDay = false) {
789
- return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, "0")}-${String(date.getDate()).padStart(2, "0")} ${isEndOfDay ? "23:59:59" : "00:00:00"}`;
790
- }
791
- /** Builds the `Basket` report query from a set of filters. */
792
- function buildBasketReportQuery(filters) {
793
- const whereConditions = [];
794
- const today = /* @__PURE__ */ new Date();
795
- const startDate = filters.dateRange.start ?? today;
796
- const endDate = filters.dateRange.end ?? today;
797
- whereConditions.push(`Basket.completeDate > '${formatDateForQuery(startDate, false)}'`);
798
- whereConditions.push(`Basket.completeDate < '${formatDateForQuery(endDate, true)}'`);
799
- if (filters.consumerId) whereConditions.push(`Basket.consumerId = ${filters.consumerId}`);
800
- if (filters.status !== "all") whereConditions.push(`Basket.status = '${filters.status}'`);
801
- return { baskets: {
802
- FROM: "Basket",
803
- WHERE: whereConditions.length > 0 ? { AND: whereConditions } : void 0
804
- } };
805
- }
806
- /** Flattens the raw report rows/keys into structured baskets. */
807
- function parseBasketReportData(reports) {
808
- if (!reports?.baskets?.data || !reports?.baskets?.keys) return [];
809
- const { keys, data } = reports.baskets;
810
- const keyIndices = {};
811
- keys.forEach((key, index) => {
812
- keyIndices[key.name] = index;
813
- });
814
- const productKey = keys.find((k) => k.name === "Basket.Product");
815
- const productSubKeys = {};
816
- if (productKey?.item) productKey.item.forEach((subKey, index) => {
817
- productSubKeys[subKey.name] = index;
818
- });
819
- return data.map((row) => {
820
- const products = (row[keyIndices["Basket.Product"]] || []).map((productRow) => ({
821
- productId: productRow[productSubKeys["Basket.Product.productId"]],
822
- productName: `Product #${productRow[productSubKeys["Basket.Product.productId"]]}`,
823
- quantity: productRow[productSubKeys["Basket.Product.quantity"]] || 1,
824
- price: productRow[productSubKeys["Basket.Product.productPrice"]] || 0,
825
- tax: productRow[productSubKeys["Basket.Product.productTaxPercentage"]] || 0
826
- }));
827
- const total = products.reduce((sum, p) => sum + p.price * p.quantity, 0);
828
- const taxTotal = products.reduce((sum, p) => sum + p.price * p.quantity * p.tax, 0);
829
- const completeDateTimestamp = row[keyIndices["Basket.completeDate"]];
830
- const completeDate = completeDateTimestamp ? new Date(completeDateTimestamp).toISOString() : "";
831
- return {
832
- id: row[keyIndices["Basket.id"]],
833
- name: row[keyIndices["Basket.name"]],
834
- consumerId: row[keyIndices["Basket.consumerId"]],
835
- products,
836
- total,
837
- taxTotal,
838
- status: "completed",
839
- completeDate
840
- };
841
- });
842
- }
843
- /** Unique non-null consumer ids referenced by a set of baskets. */
844
- function collectConsumerIds(baskets) {
845
- if (!baskets.length) return null;
846
- const ids = baskets.map((b) => b.consumerId).filter((id) => id != null);
847
- return ids.length > 0 ? [...new Set(ids)] : null;
848
- }
849
- /** Unique product ids referenced by a set of baskets. */
850
- function collectProductIds(baskets) {
851
- if (!baskets.length) return null;
852
- const ids = /* @__PURE__ */ new Set();
853
- baskets.forEach((basket) => {
854
- basket.products.forEach((product) => {
855
- if (product.productId) ids.add(product.productId);
856
- });
857
- });
858
- return ids.size > 0 ? [...ids] : null;
859
- }
860
- /** Joins baskets with consumer + product lookups, applies search, sorts. */
861
- function combineBasketRows({ baskets, consumerMap, productMap, search }) {
862
- if (!baskets.length) return [];
863
- let combinedRows = baskets.map((basket) => {
864
- const consumer = basket.consumerId ? consumerMap[basket.consumerId] : null;
865
- const productsWithNames = basket.products.map((p) => {
866
- const product = productMap[p.productId];
867
- return {
868
- ...p,
869
- productName: product?.name || `Product #${p.productId}`
870
- };
871
- });
872
- return {
873
- id: basket.id,
874
- basketId: basket.id,
875
- basketName: basket.name || `Basket #${basket.id}`,
876
- consumerId: basket.consumerId,
877
- consumerName: consumer ? `${consumer.firstName} ${consumer.lastName}` : "Unknown",
878
- consumerEmail: consumer?.email || "-",
879
- products: productsWithNames,
880
- basketTotal: (basket.total || 0) / 100,
881
- tax: basket.taxTotal || 0,
882
- dateTime: basket.completeDate,
883
- status: basket.status || "pending"
884
- };
885
- });
886
- if (search) {
887
- const searchLower = search.toLowerCase();
888
- combinedRows = combinedRows.filter((row) => row.basketId.toString().includes(searchLower) || row.basketName.toLowerCase().includes(searchLower) || row.consumerName.toLowerCase().includes(searchLower) || row.consumerEmail.toLowerCase().includes(searchLower));
889
- }
890
- combinedRows.sort((a, b) => {
891
- const dateA = new Date(a.dateTime).getTime();
892
- return new Date(b.dateTime).getTime() - dateA;
893
- });
894
- return combinedRows;
895
- }
896
- /** Aggregate totals for a set of report rows. */
897
- function computeBasketSummary(rows) {
898
- const uniqueConsumerIds = new Set(rows.map((row) => row.consumerId));
899
- return {
900
- totalBaskets: rows.length,
901
- totalRevenue: rows.reduce((sum, row) => sum + row.basketTotal, 0),
902
- totalTax: rows.reduce((sum, row) => sum + row.tax, 0),
903
- uniqueConsumers: uniqueConsumerIds.size
904
- };
905
- }
906
- /** Builds a consumer id → consumer lookup. */
907
- function buildConsumerMap(consumers) {
908
- const map = {};
909
- if (consumers) consumers.forEach((consumer) => {
910
- map[consumer.id] = consumer;
911
- });
912
- return map;
913
- }
914
-
915
- //#endregion
916
- //#region src/core/imageFit.ts
917
- /**
918
- * The centred part of a `srcWidth × srcHeight` image with the aspect ratio of
919
- * `dstWidth × dstHeight` (like CSS `object-fit: cover`).
920
- */
921
- function coverCrop(srcWidth, srcHeight, dstWidth, dstHeight) {
922
- if (![
923
- srcWidth,
924
- srcHeight,
925
- dstWidth,
926
- dstHeight
927
- ].every((n) => Number.isFinite(n) && n > 0)) throw new Error("coverCrop: sizes must be positive numbers");
928
- const srcRatio = srcWidth / srcHeight;
929
- const dstRatio = dstWidth / dstHeight;
930
- if (srcRatio > dstRatio) {
931
- const sw = Math.round(srcHeight * dstRatio);
932
- return {
933
- sx: Math.round((srcWidth - sw) / 2),
934
- sy: 0,
935
- sw,
936
- sh: srcHeight
937
- };
938
- }
939
- const sh = Math.round(srcWidth / dstRatio);
940
- return {
941
- sx: 0,
942
- sy: Math.round((srcHeight - sh) / 2),
943
- sw: srcWidth,
944
- sh
945
- };
946
- }
947
- function requireBrowserImages(fn) {
948
- if (typeof createImageBitmap !== "function") throw new Error(`${fn} needs a browser (createImageBitmap is not available)`);
949
- }
950
- /** Reads an image file's pixel size (browser only). */
951
- async function readImageSize(file) {
952
- requireBrowserImages("readImageSize");
953
- const bitmap = await createImageBitmap(file);
954
- try {
955
- return {
956
- width: bitmap.width,
957
- height: bitmap.height
958
- };
959
- } finally {
960
- bitmap.close?.();
961
- }
962
- }
963
- /**
964
- * Returns the image at exactly `width × height`: scaled and centre-cropped
965
- * (never stretched). Browser only. The result is a `File` named like the
966
- * input, with the output type's extension.
967
- */
968
- async function fitImageToSize(file, width, height, options = {}) {
969
- requireBrowserImages("fitImageToSize");
970
- const { type = "image/png", quality, keepIfExact = true } = options;
971
- const name = file.name || "image";
972
- const ext = type === "image/jpeg" ? "jpg" : type.split("/")[1];
973
- const outName = `${name.replace(/\.[^.]+$/, "")}.${ext}`;
974
- const bitmap = await createImageBitmap(file);
975
- try {
976
- if (keepIfExact && bitmap.width === width && bitmap.height === height && file.type === type) return file instanceof File ? file : new File([file], outName, { type });
977
- const { sx, sy, sw, sh } = coverCrop(bitmap.width, bitmap.height, width, height);
978
- let blob;
979
- if (typeof OffscreenCanvas === "function") {
980
- const canvas = new OffscreenCanvas(width, height);
981
- const ctx = canvas.getContext("2d");
982
- if (!ctx) throw new Error("fitImageToSize: no 2D canvas context");
983
- ctx.imageSmoothingQuality = "high";
984
- ctx.drawImage(bitmap, sx, sy, sw, sh, 0, 0, width, height);
985
- blob = await canvas.convertToBlob({
986
- type,
987
- quality
988
- });
989
- } else {
990
- const canvas = document.createElement("canvas");
991
- canvas.width = width;
992
- canvas.height = height;
993
- const ctx = canvas.getContext("2d");
994
- if (!ctx) throw new Error("fitImageToSize: no 2D canvas context");
995
- ctx.imageSmoothingQuality = "high";
996
- ctx.drawImage(bitmap, sx, sy, sw, sh, 0, 0, width, height);
997
- blob = await new Promise((resolve, reject) => canvas.toBlob((b) => b ? resolve(b) : reject(/* @__PURE__ */ new Error("fitImageToSize: encoding failed")), type, quality));
998
- }
999
- return new File([blob], outName, { type });
1000
- } finally {
1001
- bitmap.close?.();
1002
- }
1003
- }
1004
-
1005
- //#endregion
1006
- //#region src/core/upload.ts
1007
- /**
1008
- * HTTPS host that serves the Shopwave CDN's images. The API returns
1009
- * `http://static.merchantstack.com/images/…`, which has no HTTPS endpoint
1010
- * (checked Sep 2026), so pages served over HTTPS can't show it.
1011
- */
1012
- const SHOPWAVE_IMAGE_BASE_URL = "https://s3-eu-west-1.amazonaws.com/static.merchantstack.com/images";
1013
- const STATIC_HOST = /^https?:\/\/static\.merchantstack\.com\/images\//i;
1014
- /**
1015
- * Turns an image path from the API into a URL a page can load over HTTPS:
1016
- * `http://static.merchantstack.com/images/x.png` → `<base>/x.png`, and a
1017
- * relative path (`merchant/5644/logo/x.png`) → `<base>/merchant/5644/logo/x.png`.
1018
- * Other absolute URLs are returned unchanged; empty input gives `""`.
693
+ * Turns an image path from the API into a URL a page can load over HTTPS:
694
+ * `http://static.merchantstack.com/images/x.png` → `<base>/x.png`, and a
695
+ * relative path (`merchant/5644/logo/x.png`) → `<base>/merchant/5644/logo/x.png`.
696
+ * Other absolute URLs are returned unchanged; empty input gives `""`.
1019
697
  */
1020
698
  function getImageUrl(path, base = SHOPWAVE_IMAGE_BASE_URL) {
1021
699
  if (!path) return "";
@@ -1035,10 +713,12 @@ function getImageUrl(path, base = SHOPWAVE_IMAGE_BASE_URL) {
1035
713
  async function uploadImage(file, params, options = {}) {
1036
714
  if (!file || typeof file.size !== "number") throw new Error("uploadImage: file must be a File or Blob");
1037
715
  if (!isUploadKind(params?.kind)) throw new Error(`uploadImage: unknown kind "${params?.kind}"`);
716
+ if (params.prefix != null && !isUploadPrefix(params.prefix)) throw new Error(`uploadImage: unknown prefix "${params.prefix}"`);
1038
717
  const name = params.fileName ?? file.name ?? "upload";
1039
718
  const form = new FormData();
1040
719
  form.append(SHOPWAVE_UPLOAD.fileField, file, name);
1041
720
  form.append(SHOPWAVE_UPLOAD.kindField, params.kind);
721
+ if (params.prefix != null) form.append(SHOPWAVE_UPLOAD.prefixField, params.prefix);
1042
722
  const { status, body: raw } = await apiRequest(`/api/${SHOPWAVE_UPLOAD.route}`, {
1043
723
  method: "POST",
1044
724
  body: form
@@ -1173,6 +853,382 @@ async function uploadMerchantImage(file, slot, options = {}) {
1173
853
  return uploadImage(fit ? await fitImageToSize(file, def.width, def.height, fitOptions) : file, { kind: def.uploadKind }, requestOptions);
1174
854
  }
1175
855
 
856
+ //#endregion
857
+ //#region src/core/productImages.ts
858
+ /**
859
+ * Product images, as the live API stores them (merchant 5644, 4 Nov 2026):
860
+ * - A product's `images` is a list. **Reads return full URLs**
861
+ * (`http://static.merchantstack.com/images/merchant/<id>/product/<file>`);
862
+ * **writes take the uploader's file names** ({@link toStoredProductImages};
863
+ * `saveProduct` does this for you).
864
+ * - The uploader takes one file per request. With a `prefix` (see
865
+ * `UploadPrefix`) it names the file `<prefix>_<hash>.<ext>`
866
+ * (`product_thumbnail_1bdd…41090b.png`); without one, `<hash>.<ext>`. So an
867
+ * image's role can be read back from its name ({@link productImageRole}).
868
+ */
869
+ /**
870
+ * The role in an image's file name or URL, from the uploader's `prefix`:
871
+ * `"…/product_thumbnail_1bdd.png"` → `"product_thumbnail"`. `null` for a name
872
+ * without one (images uploaded without a prefix, e.g. before prefixes existed).
873
+ */
874
+ function productImageRole(value) {
875
+ const name = imageFileName(value);
876
+ if (!name) return null;
877
+ return UPLOAD_PREFIXES.find((prefix) => name.startsWith(`${prefix}_`)) ?? null;
878
+ }
879
+ /**
880
+ * A product's `images` as the API takes them on a save: file names, not the
881
+ * URLs a read returns. Non-strings, blanks and duplicates are dropped; the
882
+ * order is kept.
883
+ */
884
+ function toStoredProductImages(images) {
885
+ if (!Array.isArray(images)) return [];
886
+ const names = images.map(imageFileName).filter((name) => Boolean(name));
887
+ return Array.from(new Set(names));
888
+ }
889
+
890
+ //#endregion
891
+ //#region src/core/entities.ts
892
+ /**
893
+ * Addressing per entity, following the Shopwave API reference
894
+ * (https://developer.merchantstack.com/api-reference.html) and checked against
895
+ * the live API where noted:
896
+ * - category/product/store: GET filtered by `<x>Ids`, POST upsert, DELETE with `<x>Id` → 205.
897
+ * - promotion: no DELETE; promotions are never deleted, only expired. Expiring
898
+ * reads the promotion and saves the whole record back with `endDate` = now (and
899
+ * `startDate` = now too if it hadn't started); the live API rejects a partial
900
+ * `{ id, endDate }` save with error 899. Dates are UTC `YYYY-MM-DD HH:mm:ss`.
901
+ * - employee: POST updates `roleId`, `joinedDate`, `exitDate` (names can't be
902
+ * changed); no DELETE (live API: "Cannot DELETE /employee") → retired via `exitDate`.
903
+ * - consumer: read-only (`GET` with `ids`, comma-separated); live POST → 404.
904
+ */
905
+ const SHOPWAVE_ENTITIES = Object.freeze({
906
+ product: {
907
+ route: "products",
908
+ collection: "products",
909
+ upstream: "product",
910
+ idsHeader: "productIds",
911
+ idHeader: "productId",
912
+ writable: true,
913
+ deleteMode: "delete"
914
+ },
915
+ category: {
916
+ route: "categories",
917
+ collection: "categories",
918
+ upstream: "category",
919
+ idsHeader: "categoryIds",
920
+ idHeader: "categoryId",
921
+ writable: true,
922
+ deleteMode: "delete"
923
+ },
924
+ store: {
925
+ route: "stores",
926
+ collection: "stores",
927
+ upstream: "store",
928
+ idsHeader: "storeIds",
929
+ idHeader: "storeId",
930
+ writable: true,
931
+ deleteMode: "delete"
932
+ },
933
+ promotion: {
934
+ route: "promotions",
935
+ collection: "promotions",
936
+ upstream: "promotion",
937
+ idsHeader: "promotionIds",
938
+ idHeader: "promotionId",
939
+ writable: true,
940
+ deleteMode: "retire",
941
+ retireField: "endDate",
942
+ retireWithRecord: true,
943
+ retireStartField: "startDate"
944
+ },
945
+ employee: {
946
+ route: "employees",
947
+ collection: "employees",
948
+ upstream: "employee",
949
+ idsHeader: "employeeIds",
950
+ idHeader: "employeeId",
951
+ writable: true,
952
+ deleteMode: "retire",
953
+ retireField: "exitDate"
954
+ },
955
+ consumer: {
956
+ route: "consumer",
957
+ collection: "consumers",
958
+ upstream: "consumer",
959
+ idsHeader: "ids",
960
+ idHeader: "consumerId",
961
+ writable: false,
962
+ deleteMode: "none"
963
+ }
964
+ });
965
+ function definition(kind) {
966
+ const def = SHOPWAVE_ENTITIES[kind];
967
+ if (!def) throw new Error(`Unknown Shopwave entity "${kind}"`);
968
+ return def;
969
+ }
970
+ function assertId(kind, id) {
971
+ const s = id == null ? "" : String(id).trim();
972
+ if (!s) throw new Error(`${kind} id is missing`);
973
+ return encodeURIComponent(s);
974
+ }
975
+ /**
976
+ * Creates or updates several entities of one kind in a single request and
977
+ * returns them as saved (with their ids), in the order given.
978
+ *
979
+ * The request is `{ <collection>: { "0": item0, "1": item1, … } }`. Shopwave
980
+ * answers 201 and echoes every entity under the ref it was sent with, so each
981
+ * result is matched back by ref. Throws {@link ShopwaveApiError} when the
982
+ * response carries `api.message.errors` or any ref is missing from it.
983
+ *
984
+ * The API's partial-success semantics for multi-entity saves haven't been
985
+ * confirmed yet; single saves (`saveProduct` etc.) are the tested path.
986
+ */
987
+ async function saveEntities(kind, items, options = {}) {
988
+ const def = definition(kind);
989
+ if (items.length === 0) return [];
990
+ const refs = items.map((_, i) => String(i));
991
+ const payload = { [def.collection]: Object.fromEntries(refs.map((ref, i) => [ref, items[i]])) };
992
+ const { status, body: raw } = await apiRequest(`/api/${def.route}`, {
993
+ method: "POST",
994
+ headers: { "Content-Type": "application/json" },
995
+ body: JSON.stringify(payload)
996
+ }, options);
997
+ const body = raw;
998
+ assertNoApiErrors(body, status);
999
+ const echoed = body?.[def.collection] ?? {};
1000
+ return refs.map((ref, i) => {
1001
+ const saved = echoed[ref];
1002
+ if (!saved || saved.id == null) throw new ShopwaveApiError(status, `Shopwave API error (${status}): the saved ${kind} (ref "${ref}") was not returned`, { body });
1003
+ return {
1004
+ ...items[i],
1005
+ ...saved,
1006
+ id: Number(saved.id)
1007
+ };
1008
+ });
1009
+ }
1010
+ /** Creates (no `id`) or updates (with `id`) one entity and returns it with its id. */
1011
+ async function saveEntity(kind, item, options = {}) {
1012
+ const [saved] = await saveEntities(kind, [item], options);
1013
+ return saved;
1014
+ }
1015
+ /**
1016
+ * Deletes one entity (a soft delete: it stays readable with `deleted: true`).
1017
+ * Employees and promotions can't be deleted in Shopwave; for them the route
1018
+ * sets `exitDate` / `endDate` to now instead (see `SHOPWAVE_ENTITIES`), and a
1019
+ * promotion is saved back whole (see {@link deletePromotion}).
1020
+ *
1021
+ * Shopwave answers 205 with an empty body — also for ids that don't exist — so
1022
+ * a resolved promise means "the API accepted the request", not "a record was
1023
+ * deleted".
1024
+ */
1025
+ async function deleteEntityById(kind, id, options = {}) {
1026
+ await apiDelete(`/api/${definition(kind).route}/${assertId(kind, id)}`, options);
1027
+ }
1028
+ /** Reads one entity by id, or `null` when there's no such (non-deleted) record. */
1029
+ async function fetchEntityById(kind, id, params = {}, options = {}) {
1030
+ const def = definition(kind);
1031
+ const body = await apiGet(`/api/${def.route}/${assertId(kind, id)}`, { deleted: params.deleted ?? false }, options);
1032
+ assertNoApiErrors(body, 200);
1033
+ const map = body?.[def.collection] ?? {};
1034
+ return map[String(id)] ?? Object.values(map).find((e) => String(e?.id) === String(id)) ?? null;
1035
+ }
1036
+ /**
1037
+ * Creates (no `id`) or updates (with `id`) a product.
1038
+ *
1039
+ * Shopwave instances (prices) can't be updated, only replaced. Build
1040
+ * `instances` with {@link toProductInstancesForSave} (passing the product as last
1041
+ * read) so an edited price is sent as a new instance. As a safeguard, a new
1042
+ * product (no `id`, e.g. a duplicate) never sends instance ids: an id copied from
1043
+ * another product would otherwise point the save at that product's instances.
1044
+ *
1045
+ * `images` is sent as file names ({@link toStoredProductImages}), so the URLs a
1046
+ * read returns can be passed straight back; leaving `images` out keeps them.
1047
+ */
1048
+ const saveProduct = (product, options) => {
1049
+ let toSave = (product.id == null || String(product.id).trim() === "") && product.instances != null ? {
1050
+ ...product,
1051
+ instances: toProductInstancesForSave(product.instances)
1052
+ } : product;
1053
+ if (Array.isArray(toSave.images)) toSave = {
1054
+ ...toSave,
1055
+ images: toStoredProductImages(toSave.images)
1056
+ };
1057
+ return saveEntity("product", toSave, options);
1058
+ };
1059
+ const deleteProduct = (id, options) => deleteEntityById("product", id, options);
1060
+ const fetchProduct = (id, params, options) => fetchEntityById("product", id, params, options);
1061
+ const saveCategory = (category, options) => saveEntity("category", category, options);
1062
+ const deleteCategory = (id, options) => deleteEntityById("category", id, options);
1063
+ const fetchCategory = (id, params, options) => fetchEntityById("category", id, params, options);
1064
+ const saveStore = (store, options) => saveEntity("store", store, options);
1065
+ const deleteStore = (id, options) => deleteEntityById("store", id, options);
1066
+ const fetchStore = (id, params, options) => fetchEntityById("store", id, params, options);
1067
+ const savePromotion = (promotion, options) => saveEntity("promotion", promotion, options);
1068
+ /**
1069
+ * Expires a promotion (Shopwave never deletes promotions). The route reads the
1070
+ * promotion and saves it back with `endDate` set to now; a promotion that hasn't
1071
+ * started yet gets `startDate` = `endDate` = now. Rejects if the promotion doesn't
1072
+ * exist or the save fails.
1073
+ */
1074
+ const deletePromotion = (id, options) => deleteEntityById("promotion", id, options);
1075
+ const fetchPromotion = (id, params, options) => fetchEntityById("promotion", id, params, options);
1076
+ /**
1077
+ * Creates an employee, or updates one (with `id`). On update Shopwave only
1078
+ * applies `roleId`, `joinedDate` and `exitDate` (and store roles); names and
1079
+ * email can't be changed, and the echo only carries the updated fields — re-read
1080
+ * with `fetchEmployee` if you need the stored record.
1081
+ */
1082
+ const saveEmployee = (employee, options) => saveEntity("employee", employee, options);
1083
+ /** Retires an employee: Shopwave has no employee DELETE, so this sets `exitDate` to now. */
1084
+ const deleteEmployee = (id, options) => deleteEntityById("employee", id, options);
1085
+ const fetchEmployee = (id, params, options) => fetchEntityById("employee", id, params, options);
1086
+ const fetchConsumer = (id, params, options) => fetchEntityById("consumer", id, params, options);
1087
+
1088
+ //#endregion
1089
+ //#region src/core/basket.ts
1090
+ const DEFAULT_BASKET_FILTERS = {
1091
+ search: "",
1092
+ dateRange: {
1093
+ start: null,
1094
+ end: null
1095
+ },
1096
+ consumerId: null,
1097
+ status: "all"
1098
+ };
1099
+ const DEFAULT_PAGINATION = {
1100
+ page: 0,
1101
+ pageSize: 25,
1102
+ total: 0
1103
+ };
1104
+ /** Formats a date as `YYYY-MM-DD HH:mm:ss` for the report query. */
1105
+ function formatDateForQuery(date, isEndOfDay = false) {
1106
+ return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, "0")}-${String(date.getDate()).padStart(2, "0")} ${isEndOfDay ? "23:59:59" : "00:00:00"}`;
1107
+ }
1108
+ /** Builds the `Basket` report query from a set of filters. */
1109
+ function buildBasketReportQuery(filters) {
1110
+ const whereConditions = [];
1111
+ const today = /* @__PURE__ */ new Date();
1112
+ const startDate = filters.dateRange.start ?? today;
1113
+ const endDate = filters.dateRange.end ?? today;
1114
+ whereConditions.push(`Basket.completeDate > '${formatDateForQuery(startDate, false)}'`);
1115
+ whereConditions.push(`Basket.completeDate < '${formatDateForQuery(endDate, true)}'`);
1116
+ if (filters.consumerId) whereConditions.push(`Basket.consumerId = ${filters.consumerId}`);
1117
+ if (filters.status !== "all") whereConditions.push(`Basket.status = '${filters.status}'`);
1118
+ return { baskets: {
1119
+ FROM: "Basket",
1120
+ WHERE: whereConditions.length > 0 ? { AND: whereConditions } : void 0
1121
+ } };
1122
+ }
1123
+ /** Flattens the raw report rows/keys into structured baskets. */
1124
+ function parseBasketReportData(reports) {
1125
+ if (!reports?.baskets?.data || !reports?.baskets?.keys) return [];
1126
+ const { keys, data } = reports.baskets;
1127
+ const keyIndices = {};
1128
+ keys.forEach((key, index) => {
1129
+ keyIndices[key.name] = index;
1130
+ });
1131
+ const productKey = keys.find((k) => k.name === "Basket.Product");
1132
+ const productSubKeys = {};
1133
+ if (productKey?.item) productKey.item.forEach((subKey, index) => {
1134
+ productSubKeys[subKey.name] = index;
1135
+ });
1136
+ return data.map((row) => {
1137
+ const products = (row[keyIndices["Basket.Product"]] || []).map((productRow) => ({
1138
+ productId: productRow[productSubKeys["Basket.Product.productId"]],
1139
+ productName: `Product #${productRow[productSubKeys["Basket.Product.productId"]]}`,
1140
+ quantity: productRow[productSubKeys["Basket.Product.quantity"]] || 1,
1141
+ price: productRow[productSubKeys["Basket.Product.productPrice"]] || 0,
1142
+ tax: productRow[productSubKeys["Basket.Product.productTaxPercentage"]] || 0
1143
+ }));
1144
+ const total = products.reduce((sum, p) => sum + p.price * p.quantity, 0);
1145
+ const taxTotal = products.reduce((sum, p) => sum + p.price * p.quantity * p.tax, 0);
1146
+ const completeDateTimestamp = row[keyIndices["Basket.completeDate"]];
1147
+ const completeDate = completeDateTimestamp ? new Date(completeDateTimestamp).toISOString() : "";
1148
+ return {
1149
+ id: row[keyIndices["Basket.id"]],
1150
+ name: row[keyIndices["Basket.name"]],
1151
+ consumerId: row[keyIndices["Basket.consumerId"]],
1152
+ products,
1153
+ total,
1154
+ taxTotal,
1155
+ status: "completed",
1156
+ completeDate
1157
+ };
1158
+ });
1159
+ }
1160
+ /** Unique non-null consumer ids referenced by a set of baskets. */
1161
+ function collectConsumerIds(baskets) {
1162
+ if (!baskets.length) return null;
1163
+ const ids = baskets.map((b) => b.consumerId).filter((id) => id != null);
1164
+ return ids.length > 0 ? [...new Set(ids)] : null;
1165
+ }
1166
+ /** Unique product ids referenced by a set of baskets. */
1167
+ function collectProductIds(baskets) {
1168
+ if (!baskets.length) return null;
1169
+ const ids = /* @__PURE__ */ new Set();
1170
+ baskets.forEach((basket) => {
1171
+ basket.products.forEach((product) => {
1172
+ if (product.productId) ids.add(product.productId);
1173
+ });
1174
+ });
1175
+ return ids.size > 0 ? [...ids] : null;
1176
+ }
1177
+ /** Joins baskets with consumer + product lookups, applies search, sorts. */
1178
+ function combineBasketRows({ baskets, consumerMap, productMap, search }) {
1179
+ if (!baskets.length) return [];
1180
+ let combinedRows = baskets.map((basket) => {
1181
+ const consumer = basket.consumerId ? consumerMap[basket.consumerId] : null;
1182
+ const productsWithNames = basket.products.map((p) => {
1183
+ const product = productMap[p.productId];
1184
+ return {
1185
+ ...p,
1186
+ productName: product?.name || `Product #${p.productId}`
1187
+ };
1188
+ });
1189
+ return {
1190
+ id: basket.id,
1191
+ basketId: basket.id,
1192
+ basketName: basket.name || `Basket #${basket.id}`,
1193
+ consumerId: basket.consumerId,
1194
+ consumerName: consumer ? `${consumer.firstName} ${consumer.lastName}` : "Unknown",
1195
+ consumerEmail: consumer?.email || "-",
1196
+ products: productsWithNames,
1197
+ basketTotal: (basket.total || 0) / 100,
1198
+ tax: basket.taxTotal || 0,
1199
+ dateTime: basket.completeDate,
1200
+ status: basket.status || "pending"
1201
+ };
1202
+ });
1203
+ if (search) {
1204
+ const searchLower = search.toLowerCase();
1205
+ combinedRows = combinedRows.filter((row) => row.basketId.toString().includes(searchLower) || row.basketName.toLowerCase().includes(searchLower) || row.consumerName.toLowerCase().includes(searchLower) || row.consumerEmail.toLowerCase().includes(searchLower));
1206
+ }
1207
+ combinedRows.sort((a, b) => {
1208
+ const dateA = new Date(a.dateTime).getTime();
1209
+ return new Date(b.dateTime).getTime() - dateA;
1210
+ });
1211
+ return combinedRows;
1212
+ }
1213
+ /** Aggregate totals for a set of report rows. */
1214
+ function computeBasketSummary(rows) {
1215
+ const uniqueConsumerIds = new Set(rows.map((row) => row.consumerId));
1216
+ return {
1217
+ totalBaskets: rows.length,
1218
+ totalRevenue: rows.reduce((sum, row) => sum + row.basketTotal, 0),
1219
+ totalTax: rows.reduce((sum, row) => sum + row.tax, 0),
1220
+ uniqueConsumers: uniqueConsumerIds.size
1221
+ };
1222
+ }
1223
+ /** Builds a consumer id → consumer lookup. */
1224
+ function buildConsumerMap(consumers) {
1225
+ const map = {};
1226
+ if (consumers) consumers.forEach((consumer) => {
1227
+ map[consumer.id] = consumer;
1228
+ });
1229
+ return map;
1230
+ }
1231
+
1176
1232
  //#endregion
1177
1233
  //#region src/core/merchant.ts
1178
1234
  const MERCHANT_PATH = `/api/${SHOPWAVE_RESOURCES.merchant.route}`;
@@ -1915,5 +1971,5 @@ function useBasketReport(options) {
1915
1971
  }
1916
1972
 
1917
1973
  //#endregion
1918
- export { DEFAULT_BASKET_FILTERS, DEFAULT_PAGINATION, MERCHANT_COLOUR_KEYS, MERCHANT_FIELD_MAX_LENGTH, MERCHANT_IMAGE_SLOTS, MERCHANT_IMAGE_SLOT_KEYS, MERCHANT_LINK_KEYS, MERCHANT_LINK_PATHS, MERCHANT_OBJECT_FIELDS, MERCHANT_READ_ONLY_FIELDS, PromotionRuleSet, SHOPWAVE_ENTITIES, SHOPWAVE_IMAGE_BASE_URL, SHOPWAVE_LOOKUPS, SHOPWAVE_RESOURCES, SHOPWAVE_UPLOAD, ShopwaveApiError, UPLOAD_KINDS, apiDelete, apiGet, apiRequest, apiSend, assertNoApiErrors, authorizationFor, buildBasketReportQuery, buildConsumerMap, collectConsumerIds, collectProductIds, combineBasketRows, computeBasketSummary, coverCrop, deleteCategory, deleteEmployee, deleteEntity, deleteEntityById, deleteProduct, deletePromotion, deleteStore, fetchCategories, fetchCategory, fetchConsumer, fetchConsumers, fetchEmployee, fetchEmployees, fetchEntityById, fetchMerchant, fetchProduct, fetchProductInstanceTags, fetchProductTags, fetchProducts, fetchProductsMap, fetchPromotion, fetchPromotions, fetchReport, fetchSession, fetchStore, fetchStores, fetchUser, findRepricedInstances, fitImageToSize, formatDateForQuery, getApiErrorMap, getApiErrors, getImageUrl, getMerchantImage, getMerchantImageUrl, imageFileName, isSameInstancePrice, isUploadKind, loginPath, logout, logoutPath, merchantLinksFromForm, merchantLinksToForm, normalizeMerchantLinks, normalizeTagList, parseBasketReportData, readImageSize, rolesById, saveCategory, saveEmployee, saveEntities, saveEntity, saveProduct, savePromotion, saveStore, setMerchantImage, submitEntity, toProductInstancesForSave, toShopwaveApiError, toStoredImageIds, updateMerchant, uploadImage, uploadMerchantImage, useBasketReport, useCategory, useConsumer, useDelete, useEmployee, useLogout, useMerchant, useProduct, useProductInstanceTags, useProductTags, usePromotion, useReport, useSession, useStore, useSubmit, useUpdateMerchant, useUploadImage, useUploadMerchantImage, useUser, validateMerchant, withToken };
1974
+ export { DEFAULT_BASKET_FILTERS, DEFAULT_PAGINATION, MERCHANT_COLOUR_KEYS, MERCHANT_FIELD_MAX_LENGTH, MERCHANT_IMAGE_SLOTS, MERCHANT_IMAGE_SLOT_KEYS, MERCHANT_LINK_KEYS, MERCHANT_LINK_PATHS, MERCHANT_OBJECT_FIELDS, MERCHANT_READ_ONLY_FIELDS, PromotionRuleSet, SHOPWAVE_ENTITIES, SHOPWAVE_IMAGE_BASE_URL, SHOPWAVE_LOOKUPS, SHOPWAVE_RESOURCES, SHOPWAVE_UPLOAD, ShopwaveApiError, UPLOAD_KINDS, UPLOAD_PREFIXES, apiDelete, apiGet, apiRequest, apiSend, assertNoApiErrors, authorizationFor, buildBasketReportQuery, buildConsumerMap, collectConsumerIds, collectProductIds, combineBasketRows, computeBasketSummary, coverCrop, deleteCategory, deleteEmployee, deleteEntity, deleteEntityById, deleteProduct, deletePromotion, deleteStore, fetchCategories, fetchCategory, fetchConsumer, fetchConsumers, fetchEmployee, fetchEmployees, fetchEntityById, fetchMerchant, fetchProduct, fetchProductInstanceTags, fetchProductTags, fetchProducts, fetchProductsMap, fetchPromotion, fetchPromotions, fetchReport, fetchSession, fetchStore, fetchStores, fetchUser, findRepricedInstances, fitImageToSize, formatDateForQuery, getApiErrorMap, getApiErrors, getImageUrl, getMerchantImage, getMerchantImageUrl, imageFileName, isSameInstancePrice, isUploadKind, isUploadPrefix, loginPath, logout, logoutPath, merchantLinksFromForm, merchantLinksToForm, normalizeMerchantLinks, normalizeTagList, parseBasketReportData, productImageRole, readImageSize, rolesById, saveCategory, saveEmployee, saveEntities, saveEntity, saveProduct, savePromotion, saveStore, setMerchantImage, submitEntity, toProductInstancesForSave, toShopwaveApiError, toStoredImageIds, toStoredProductImages, updateMerchant, uploadImage, uploadMerchantImage, useBasketReport, useCategory, useConsumer, useDelete, useEmployee, useLogout, useMerchant, useProduct, useProductInstanceTags, useProductTags, usePromotion, useReport, useSession, useStore, useSubmit, useUpdateMerchant, useUploadImage, useUploadMerchantImage, useUser, validateMerchant, withToken };
1919
1975
  //# sourceMappingURL=index.js.map