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.
@@ -354,6 +354,19 @@ interface BasketPagination {
354
354
  * the correctly spelt names here and the server maps them.
355
355
  */
356
356
  type UploadKind = "merchant" | "merchantFeature" | "product" | "user" | "applicationLogo" | "applicationImages";
357
+ /**
358
+ * The role an image plays, sent to Shopwave as the uploader's `prefix` header.
359
+ * The uploader names the file `<prefix>_<hash>.<ext>`, so the role travels with
360
+ * the file wherever a record stores it (a product's `images`). Live, merchant
361
+ * 5644, 4 Nov 2026: all four values answered 201.
362
+ * - `product`: a product gallery image;
363
+ * - `product_thumbnail`: the product's lead image, shown in lists;
364
+ * - `variant` / `variant_thumbnail`: a variant's own image and its lead image
365
+ * (used by the Connect integrations).
366
+ *
367
+ * Values are the ones Connect's `uploadFileToShopwave` sends.
368
+ */
369
+ type UploadPrefix = "product" | "product_thumbnail" | "variant" | "variant_thumbnail";
357
370
  //#endregion
358
371
  //#region src/core/imageFit.d.ts
359
372
  interface FitImageOptions {
@@ -369,6 +382,12 @@ interface FitImageOptions {
369
382
  interface UploadImageParams {
370
383
  /** What the image is for (sent to Shopwave as the uploader's `contentType`). */
371
384
  kind: UploadKind;
385
+ /**
386
+ * The image's role (sent to Shopwave as the uploader's `prefix` header), e.g.
387
+ * `product_thumbnail` for a product's lead image. Leave it out for images
388
+ * without a role (merchant logos).
389
+ */
390
+ prefix?: UploadPrefix;
372
391
  /** File name to send when `file` is a Blob without one. Defaults to `File.name`, else `"upload"`. */
373
392
  fileName?: string;
374
393
  }
@@ -354,6 +354,19 @@ interface BasketPagination {
354
354
  * the correctly spelt names here and the server maps them.
355
355
  */
356
356
  type UploadKind = "merchant" | "merchantFeature" | "product" | "user" | "applicationLogo" | "applicationImages";
357
+ /**
358
+ * The role an image plays, sent to Shopwave as the uploader's `prefix` header.
359
+ * The uploader names the file `<prefix>_<hash>.<ext>`, so the role travels with
360
+ * the file wherever a record stores it (a product's `images`). Live, merchant
361
+ * 5644, 4 Nov 2026: all four values answered 201.
362
+ * - `product`: a product gallery image;
363
+ * - `product_thumbnail`: the product's lead image, shown in lists;
364
+ * - `variant` / `variant_thumbnail`: a variant's own image and its lead image
365
+ * (used by the Connect integrations).
366
+ *
367
+ * Values are the ones Connect's `uploadFileToShopwave` sends.
368
+ */
369
+ type UploadPrefix = "product" | "product_thumbnail" | "variant" | "variant_thumbnail";
357
370
  //#endregion
358
371
  //#region src/core/imageFit.d.ts
359
372
  interface FitImageOptions {
@@ -369,6 +382,12 @@ interface FitImageOptions {
369
382
  interface UploadImageParams {
370
383
  /** What the image is for (sent to Shopwave as the uploader's `contentType`). */
371
384
  kind: UploadKind;
385
+ /**
386
+ * The image's role (sent to Shopwave as the uploader's `prefix` header), e.g.
387
+ * `product_thumbnail` for a product's lead image. Leave it out for images
388
+ * without a role (merchant logos).
389
+ */
390
+ prefix?: UploadPrefix;
372
391
  /** File name to send when `file` is a Blob without one. Defaults to `File.name`, else `"upload"`. */
373
392
  fileName?: string;
374
393
  }
@@ -291,17 +291,29 @@ const UPLOAD_KINDS = Object.freeze({
291
291
  applicationLogo: "applicaionLogo",
292
292
  applicationImages: "applicaionImages"
293
293
  });
294
+ /** Every `UploadPrefix`, longest first so a name check never takes `product` for `product_thumbnail`. */
295
+ const UPLOAD_PREFIXES = Object.freeze([
296
+ "product_thumbnail",
297
+ "variant_thumbnail",
298
+ "product",
299
+ "variant"
300
+ ]);
294
301
  /** How uploads are addressed (app route and Shopwave uploader). */
295
302
  const SHOPWAVE_UPLOAD = Object.freeze({
296
303
  route: "upload",
297
304
  upstream: "uploader",
298
305
  kindHeader: "contentType",
306
+ prefixHeader: "prefix",
299
307
  fileField: "file",
300
- kindField: "kind"
308
+ kindField: "kind",
309
+ prefixField: "prefix"
301
310
  });
302
311
  function isUploadKind(value) {
303
312
  return typeof value === "string" && Object.prototype.hasOwnProperty.call(UPLOAD_KINDS, value);
304
313
  }
314
+ function isUploadPrefix(value) {
315
+ return typeof value === "string" && UPLOAD_PREFIXES.includes(value);
316
+ }
305
317
  const SHOPWAVE_LOOKUPS = Object.freeze({
306
318
  productTags: {
307
319
  route: "products/tags",
@@ -468,150 +480,6 @@ async function submitEntity({ endpoint, method = "POST", payload }, options = {}
468
480
  return json ?? void 0;
469
481
  }
470
482
 
471
- //#endregion
472
- //#region src/core/basket.ts
473
- const DEFAULT_BASKET_FILTERS = {
474
- search: "",
475
- dateRange: {
476
- start: null,
477
- end: null
478
- },
479
- consumerId: null,
480
- status: "all"
481
- };
482
- const DEFAULT_PAGINATION = {
483
- page: 0,
484
- pageSize: 25,
485
- total: 0
486
- };
487
- /** Formats a date as `YYYY-MM-DD HH:mm:ss` for the report query. */
488
- function formatDateForQuery(date, isEndOfDay = false) {
489
- return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, "0")}-${String(date.getDate()).padStart(2, "0")} ${isEndOfDay ? "23:59:59" : "00:00:00"}`;
490
- }
491
- /** Builds the `Basket` report query from a set of filters. */
492
- function buildBasketReportQuery(filters) {
493
- const whereConditions = [];
494
- const today = /* @__PURE__ */ new Date();
495
- const startDate = filters.dateRange.start ?? today;
496
- const endDate = filters.dateRange.end ?? today;
497
- whereConditions.push(`Basket.completeDate > '${formatDateForQuery(startDate, false)}'`);
498
- whereConditions.push(`Basket.completeDate < '${formatDateForQuery(endDate, true)}'`);
499
- if (filters.consumerId) whereConditions.push(`Basket.consumerId = ${filters.consumerId}`);
500
- if (filters.status !== "all") whereConditions.push(`Basket.status = '${filters.status}'`);
501
- return { baskets: {
502
- FROM: "Basket",
503
- WHERE: whereConditions.length > 0 ? { AND: whereConditions } : void 0
504
- } };
505
- }
506
- /** Flattens the raw report rows/keys into structured baskets. */
507
- function parseBasketReportData(reports) {
508
- if (!reports?.baskets?.data || !reports?.baskets?.keys) return [];
509
- const { keys, data } = reports.baskets;
510
- const keyIndices = {};
511
- keys.forEach((key, index) => {
512
- keyIndices[key.name] = index;
513
- });
514
- const productKey = keys.find((k) => k.name === "Basket.Product");
515
- const productSubKeys = {};
516
- if (productKey?.item) productKey.item.forEach((subKey, index) => {
517
- productSubKeys[subKey.name] = index;
518
- });
519
- return data.map((row) => {
520
- const products = (row[keyIndices["Basket.Product"]] || []).map((productRow) => ({
521
- productId: productRow[productSubKeys["Basket.Product.productId"]],
522
- productName: `Product #${productRow[productSubKeys["Basket.Product.productId"]]}`,
523
- quantity: productRow[productSubKeys["Basket.Product.quantity"]] || 1,
524
- price: productRow[productSubKeys["Basket.Product.productPrice"]] || 0,
525
- tax: productRow[productSubKeys["Basket.Product.productTaxPercentage"]] || 0
526
- }));
527
- const total = products.reduce((sum, p) => sum + p.price * p.quantity, 0);
528
- const taxTotal = products.reduce((sum, p) => sum + p.price * p.quantity * p.tax, 0);
529
- const completeDateTimestamp = row[keyIndices["Basket.completeDate"]];
530
- const completeDate = completeDateTimestamp ? new Date(completeDateTimestamp).toISOString() : "";
531
- return {
532
- id: row[keyIndices["Basket.id"]],
533
- name: row[keyIndices["Basket.name"]],
534
- consumerId: row[keyIndices["Basket.consumerId"]],
535
- products,
536
- total,
537
- taxTotal,
538
- status: "completed",
539
- completeDate
540
- };
541
- });
542
- }
543
- /** Unique non-null consumer ids referenced by a set of baskets. */
544
- function collectConsumerIds(baskets) {
545
- if (!baskets.length) return null;
546
- const ids = baskets.map((b) => b.consumerId).filter((id) => id != null);
547
- return ids.length > 0 ? [...new Set(ids)] : null;
548
- }
549
- /** Unique product ids referenced by a set of baskets. */
550
- function collectProductIds(baskets) {
551
- if (!baskets.length) return null;
552
- const ids = /* @__PURE__ */ new Set();
553
- baskets.forEach((basket) => {
554
- basket.products.forEach((product) => {
555
- if (product.productId) ids.add(product.productId);
556
- });
557
- });
558
- return ids.size > 0 ? [...ids] : null;
559
- }
560
- /** Joins baskets with consumer + product lookups, applies search, sorts. */
561
- function combineBasketRows({ baskets, consumerMap, productMap, search }) {
562
- if (!baskets.length) return [];
563
- let combinedRows = baskets.map((basket) => {
564
- const consumer = basket.consumerId ? consumerMap[basket.consumerId] : null;
565
- const productsWithNames = basket.products.map((p) => {
566
- const product = productMap[p.productId];
567
- return {
568
- ...p,
569
- productName: product?.name || `Product #${p.productId}`
570
- };
571
- });
572
- return {
573
- id: basket.id,
574
- basketId: basket.id,
575
- basketName: basket.name || `Basket #${basket.id}`,
576
- consumerId: basket.consumerId,
577
- consumerName: consumer ? `${consumer.firstName} ${consumer.lastName}` : "Unknown",
578
- consumerEmail: consumer?.email || "-",
579
- products: productsWithNames,
580
- basketTotal: (basket.total || 0) / 100,
581
- tax: basket.taxTotal || 0,
582
- dateTime: basket.completeDate,
583
- status: basket.status || "pending"
584
- };
585
- });
586
- if (search) {
587
- const searchLower = search.toLowerCase();
588
- combinedRows = combinedRows.filter((row) => row.basketId.toString().includes(searchLower) || row.basketName.toLowerCase().includes(searchLower) || row.consumerName.toLowerCase().includes(searchLower) || row.consumerEmail.toLowerCase().includes(searchLower));
589
- }
590
- combinedRows.sort((a, b) => {
591
- const dateA = new Date(a.dateTime).getTime();
592
- return new Date(b.dateTime).getTime() - dateA;
593
- });
594
- return combinedRows;
595
- }
596
- /** Aggregate totals for a set of report rows. */
597
- function computeBasketSummary(rows) {
598
- const uniqueConsumerIds = new Set(rows.map((row) => row.consumerId));
599
- return {
600
- totalBaskets: rows.length,
601
- totalRevenue: rows.reduce((sum, row) => sum + row.basketTotal, 0),
602
- totalTax: rows.reduce((sum, row) => sum + row.tax, 0),
603
- uniqueConsumers: uniqueConsumerIds.size
604
- };
605
- }
606
- /** Builds a consumer id → consumer lookup. */
607
- function buildConsumerMap(consumers) {
608
- const map = {};
609
- if (consumers) consumers.forEach((consumer) => {
610
- map[consumer.id] = consumer;
611
- });
612
- return map;
613
- }
614
-
615
483
  //#endregion
616
484
  //#region src/core/imageFit.ts
617
485
  /**
@@ -722,10 +590,12 @@ function getImageUrl(path, base = SHOPWAVE_IMAGE_BASE_URL) {
722
590
  async function uploadImage(file, params, options = {}) {
723
591
  if (!file || typeof file.size !== "number") throw new Error("uploadImage: file must be a File or Blob");
724
592
  if (!isUploadKind(params?.kind)) throw new Error(`uploadImage: unknown kind "${params?.kind}"`);
593
+ if (params.prefix != null && !isUploadPrefix(params.prefix)) throw new Error(`uploadImage: unknown prefix "${params.prefix}"`);
725
594
  const name = params.fileName ?? file.name ?? "upload";
726
595
  const form = new FormData();
727
596
  form.append(SHOPWAVE_UPLOAD.fileField, file, name);
728
597
  form.append(SHOPWAVE_UPLOAD.kindField, params.kind);
598
+ if (params.prefix != null) form.append(SHOPWAVE_UPLOAD.prefixField, params.prefix);
729
599
  const { status, body: raw } = await apiRequest(`/api/${SHOPWAVE_UPLOAD.route}`, {
730
600
  method: "POST",
731
601
  body: form
@@ -813,6 +683,150 @@ async function uploadMerchantImage(file, slot, options = {}) {
813
683
  return uploadImage(fit ? await fitImageToSize(file, def.width, def.height, fitOptions) : file, { kind: def.uploadKind }, requestOptions);
814
684
  }
815
685
 
686
+ //#endregion
687
+ //#region src/core/basket.ts
688
+ const DEFAULT_BASKET_FILTERS = {
689
+ search: "",
690
+ dateRange: {
691
+ start: null,
692
+ end: null
693
+ },
694
+ consumerId: null,
695
+ status: "all"
696
+ };
697
+ const DEFAULT_PAGINATION = {
698
+ page: 0,
699
+ pageSize: 25,
700
+ total: 0
701
+ };
702
+ /** Formats a date as `YYYY-MM-DD HH:mm:ss` for the report query. */
703
+ function formatDateForQuery(date, isEndOfDay = false) {
704
+ return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, "0")}-${String(date.getDate()).padStart(2, "0")} ${isEndOfDay ? "23:59:59" : "00:00:00"}`;
705
+ }
706
+ /** Builds the `Basket` report query from a set of filters. */
707
+ function buildBasketReportQuery(filters) {
708
+ const whereConditions = [];
709
+ const today = /* @__PURE__ */ new Date();
710
+ const startDate = filters.dateRange.start ?? today;
711
+ const endDate = filters.dateRange.end ?? today;
712
+ whereConditions.push(`Basket.completeDate > '${formatDateForQuery(startDate, false)}'`);
713
+ whereConditions.push(`Basket.completeDate < '${formatDateForQuery(endDate, true)}'`);
714
+ if (filters.consumerId) whereConditions.push(`Basket.consumerId = ${filters.consumerId}`);
715
+ if (filters.status !== "all") whereConditions.push(`Basket.status = '${filters.status}'`);
716
+ return { baskets: {
717
+ FROM: "Basket",
718
+ WHERE: whereConditions.length > 0 ? { AND: whereConditions } : void 0
719
+ } };
720
+ }
721
+ /** Flattens the raw report rows/keys into structured baskets. */
722
+ function parseBasketReportData(reports) {
723
+ if (!reports?.baskets?.data || !reports?.baskets?.keys) return [];
724
+ const { keys, data } = reports.baskets;
725
+ const keyIndices = {};
726
+ keys.forEach((key, index) => {
727
+ keyIndices[key.name] = index;
728
+ });
729
+ const productKey = keys.find((k) => k.name === "Basket.Product");
730
+ const productSubKeys = {};
731
+ if (productKey?.item) productKey.item.forEach((subKey, index) => {
732
+ productSubKeys[subKey.name] = index;
733
+ });
734
+ return data.map((row) => {
735
+ const products = (row[keyIndices["Basket.Product"]] || []).map((productRow) => ({
736
+ productId: productRow[productSubKeys["Basket.Product.productId"]],
737
+ productName: `Product #${productRow[productSubKeys["Basket.Product.productId"]]}`,
738
+ quantity: productRow[productSubKeys["Basket.Product.quantity"]] || 1,
739
+ price: productRow[productSubKeys["Basket.Product.productPrice"]] || 0,
740
+ tax: productRow[productSubKeys["Basket.Product.productTaxPercentage"]] || 0
741
+ }));
742
+ const total = products.reduce((sum, p) => sum + p.price * p.quantity, 0);
743
+ const taxTotal = products.reduce((sum, p) => sum + p.price * p.quantity * p.tax, 0);
744
+ const completeDateTimestamp = row[keyIndices["Basket.completeDate"]];
745
+ const completeDate = completeDateTimestamp ? new Date(completeDateTimestamp).toISOString() : "";
746
+ return {
747
+ id: row[keyIndices["Basket.id"]],
748
+ name: row[keyIndices["Basket.name"]],
749
+ consumerId: row[keyIndices["Basket.consumerId"]],
750
+ products,
751
+ total,
752
+ taxTotal,
753
+ status: "completed",
754
+ completeDate
755
+ };
756
+ });
757
+ }
758
+ /** Unique non-null consumer ids referenced by a set of baskets. */
759
+ function collectConsumerIds(baskets) {
760
+ if (!baskets.length) return null;
761
+ const ids = baskets.map((b) => b.consumerId).filter((id) => id != null);
762
+ return ids.length > 0 ? [...new Set(ids)] : null;
763
+ }
764
+ /** Unique product ids referenced by a set of baskets. */
765
+ function collectProductIds(baskets) {
766
+ if (!baskets.length) return null;
767
+ const ids = /* @__PURE__ */ new Set();
768
+ baskets.forEach((basket) => {
769
+ basket.products.forEach((product) => {
770
+ if (product.productId) ids.add(product.productId);
771
+ });
772
+ });
773
+ return ids.size > 0 ? [...ids] : null;
774
+ }
775
+ /** Joins baskets with consumer + product lookups, applies search, sorts. */
776
+ function combineBasketRows({ baskets, consumerMap, productMap, search }) {
777
+ if (!baskets.length) return [];
778
+ let combinedRows = baskets.map((basket) => {
779
+ const consumer = basket.consumerId ? consumerMap[basket.consumerId] : null;
780
+ const productsWithNames = basket.products.map((p) => {
781
+ const product = productMap[p.productId];
782
+ return {
783
+ ...p,
784
+ productName: product?.name || `Product #${p.productId}`
785
+ };
786
+ });
787
+ return {
788
+ id: basket.id,
789
+ basketId: basket.id,
790
+ basketName: basket.name || `Basket #${basket.id}`,
791
+ consumerId: basket.consumerId,
792
+ consumerName: consumer ? `${consumer.firstName} ${consumer.lastName}` : "Unknown",
793
+ consumerEmail: consumer?.email || "-",
794
+ products: productsWithNames,
795
+ basketTotal: (basket.total || 0) / 100,
796
+ tax: basket.taxTotal || 0,
797
+ dateTime: basket.completeDate,
798
+ status: basket.status || "pending"
799
+ };
800
+ });
801
+ if (search) {
802
+ const searchLower = search.toLowerCase();
803
+ combinedRows = combinedRows.filter((row) => row.basketId.toString().includes(searchLower) || row.basketName.toLowerCase().includes(searchLower) || row.consumerName.toLowerCase().includes(searchLower) || row.consumerEmail.toLowerCase().includes(searchLower));
804
+ }
805
+ combinedRows.sort((a, b) => {
806
+ const dateA = new Date(a.dateTime).getTime();
807
+ return new Date(b.dateTime).getTime() - dateA;
808
+ });
809
+ return combinedRows;
810
+ }
811
+ /** Aggregate totals for a set of report rows. */
812
+ function computeBasketSummary(rows) {
813
+ const uniqueConsumerIds = new Set(rows.map((row) => row.consumerId));
814
+ return {
815
+ totalBaskets: rows.length,
816
+ totalRevenue: rows.reduce((sum, row) => sum + row.basketTotal, 0),
817
+ totalTax: rows.reduce((sum, row) => sum + row.tax, 0),
818
+ uniqueConsumers: uniqueConsumerIds.size
819
+ };
820
+ }
821
+ /** Builds a consumer id → consumer lookup. */
822
+ function buildConsumerMap(consumers) {
823
+ const map = {};
824
+ if (consumers) consumers.forEach((consumer) => {
825
+ map[consumer.id] = consumer;
826
+ });
827
+ return map;
828
+ }
829
+
816
830
  //#endregion
817
831
  //#region src/core/merchant.ts
818
832
  const MERCHANT_PATH = `/api/${SHOPWAVE_RESOURCES.merchant.route}`;