react-shopwave-connect 0.4.3 → 0.4.5

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.
@@ -315,17 +315,29 @@ const UPLOAD_KINDS = Object.freeze({
315
315
  applicationLogo: "applicaionLogo",
316
316
  applicationImages: "applicaionImages"
317
317
  });
318
+ /** Every `UploadPrefix`, longest first so a name check never takes `product` for `product_thumbnail`. */
319
+ const UPLOAD_PREFIXES = Object.freeze([
320
+ "product_thumbnail",
321
+ "variant_thumbnail",
322
+ "product",
323
+ "variant"
324
+ ]);
318
325
  /** How uploads are addressed (app route and Shopwave uploader). */
319
326
  const SHOPWAVE_UPLOAD = Object.freeze({
320
327
  route: "upload",
321
328
  upstream: "uploader",
322
329
  kindHeader: "contentType",
330
+ prefixHeader: "prefix",
323
331
  fileField: "file",
324
- kindField: "kind"
332
+ kindField: "kind",
333
+ prefixField: "prefix"
325
334
  });
326
335
  function isUploadKind(value) {
327
336
  return typeof value === "string" && Object.prototype.hasOwnProperty.call(UPLOAD_KINDS, value);
328
337
  }
338
+ function isUploadPrefix(value) {
339
+ return typeof value === "string" && UPLOAD_PREFIXES.includes(value);
340
+ }
329
341
  const SHOPWAVE_LOOKUPS = Object.freeze({
330
342
  productTags: {
331
343
  route: "products/tags",
@@ -492,150 +504,6 @@ async function submitEntity({ endpoint, method = "POST", payload }, options = {}
492
504
  return json ?? void 0;
493
505
  }
494
506
 
495
- //#endregion
496
- //#region src/core/basket.ts
497
- const DEFAULT_BASKET_FILTERS = {
498
- search: "",
499
- dateRange: {
500
- start: null,
501
- end: null
502
- },
503
- consumerId: null,
504
- status: "all"
505
- };
506
- const DEFAULT_PAGINATION = {
507
- page: 0,
508
- pageSize: 25,
509
- total: 0
510
- };
511
- /** Formats a date as `YYYY-MM-DD HH:mm:ss` for the report query. */
512
- function formatDateForQuery(date, isEndOfDay = false) {
513
- return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, "0")}-${String(date.getDate()).padStart(2, "0")} ${isEndOfDay ? "23:59:59" : "00:00:00"}`;
514
- }
515
- /** Builds the `Basket` report query from a set of filters. */
516
- function buildBasketReportQuery(filters) {
517
- const whereConditions = [];
518
- const today = /* @__PURE__ */ new Date();
519
- const startDate = filters.dateRange.start ?? today;
520
- const endDate = filters.dateRange.end ?? today;
521
- whereConditions.push(`Basket.completeDate > '${formatDateForQuery(startDate, false)}'`);
522
- whereConditions.push(`Basket.completeDate < '${formatDateForQuery(endDate, true)}'`);
523
- if (filters.consumerId) whereConditions.push(`Basket.consumerId = ${filters.consumerId}`);
524
- if (filters.status !== "all") whereConditions.push(`Basket.status = '${filters.status}'`);
525
- return { baskets: {
526
- FROM: "Basket",
527
- WHERE: whereConditions.length > 0 ? { AND: whereConditions } : void 0
528
- } };
529
- }
530
- /** Flattens the raw report rows/keys into structured baskets. */
531
- function parseBasketReportData(reports) {
532
- if (!reports?.baskets?.data || !reports?.baskets?.keys) return [];
533
- const { keys, data } = reports.baskets;
534
- const keyIndices = {};
535
- keys.forEach((key, index) => {
536
- keyIndices[key.name] = index;
537
- });
538
- const productKey = keys.find((k) => k.name === "Basket.Product");
539
- const productSubKeys = {};
540
- if (productKey?.item) productKey.item.forEach((subKey, index) => {
541
- productSubKeys[subKey.name] = index;
542
- });
543
- return data.map((row) => {
544
- const products = (row[keyIndices["Basket.Product"]] || []).map((productRow) => ({
545
- productId: productRow[productSubKeys["Basket.Product.productId"]],
546
- productName: `Product #${productRow[productSubKeys["Basket.Product.productId"]]}`,
547
- quantity: productRow[productSubKeys["Basket.Product.quantity"]] || 1,
548
- price: productRow[productSubKeys["Basket.Product.productPrice"]] || 0,
549
- tax: productRow[productSubKeys["Basket.Product.productTaxPercentage"]] || 0
550
- }));
551
- const total = products.reduce((sum, p) => sum + p.price * p.quantity, 0);
552
- const taxTotal = products.reduce((sum, p) => sum + p.price * p.quantity * p.tax, 0);
553
- const completeDateTimestamp = row[keyIndices["Basket.completeDate"]];
554
- const completeDate = completeDateTimestamp ? new Date(completeDateTimestamp).toISOString() : "";
555
- return {
556
- id: row[keyIndices["Basket.id"]],
557
- name: row[keyIndices["Basket.name"]],
558
- consumerId: row[keyIndices["Basket.consumerId"]],
559
- products,
560
- total,
561
- taxTotal,
562
- status: "completed",
563
- completeDate
564
- };
565
- });
566
- }
567
- /** Unique non-null consumer ids referenced by a set of baskets. */
568
- function collectConsumerIds(baskets) {
569
- if (!baskets.length) return null;
570
- const ids = baskets.map((b) => b.consumerId).filter((id) => id != null);
571
- return ids.length > 0 ? [...new Set(ids)] : null;
572
- }
573
- /** Unique product ids referenced by a set of baskets. */
574
- function collectProductIds(baskets) {
575
- if (!baskets.length) return null;
576
- const ids = /* @__PURE__ */ new Set();
577
- baskets.forEach((basket) => {
578
- basket.products.forEach((product) => {
579
- if (product.productId) ids.add(product.productId);
580
- });
581
- });
582
- return ids.size > 0 ? [...ids] : null;
583
- }
584
- /** Joins baskets with consumer + product lookups, applies search, sorts. */
585
- function combineBasketRows({ baskets, consumerMap, productMap, search }) {
586
- if (!baskets.length) return [];
587
- let combinedRows = baskets.map((basket) => {
588
- const consumer = basket.consumerId ? consumerMap[basket.consumerId] : null;
589
- const productsWithNames = basket.products.map((p) => {
590
- const product = productMap[p.productId];
591
- return {
592
- ...p,
593
- productName: product?.name || `Product #${p.productId}`
594
- };
595
- });
596
- return {
597
- id: basket.id,
598
- basketId: basket.id,
599
- basketName: basket.name || `Basket #${basket.id}`,
600
- consumerId: basket.consumerId,
601
- consumerName: consumer ? `${consumer.firstName} ${consumer.lastName}` : "Unknown",
602
- consumerEmail: consumer?.email || "-",
603
- products: productsWithNames,
604
- basketTotal: (basket.total || 0) / 100,
605
- tax: basket.taxTotal || 0,
606
- dateTime: basket.completeDate,
607
- status: basket.status || "pending"
608
- };
609
- });
610
- if (search) {
611
- const searchLower = search.toLowerCase();
612
- combinedRows = combinedRows.filter((row) => row.basketId.toString().includes(searchLower) || row.basketName.toLowerCase().includes(searchLower) || row.consumerName.toLowerCase().includes(searchLower) || row.consumerEmail.toLowerCase().includes(searchLower));
613
- }
614
- combinedRows.sort((a, b) => {
615
- const dateA = new Date(a.dateTime).getTime();
616
- return new Date(b.dateTime).getTime() - dateA;
617
- });
618
- return combinedRows;
619
- }
620
- /** Aggregate totals for a set of report rows. */
621
- function computeBasketSummary(rows) {
622
- const uniqueConsumerIds = new Set(rows.map((row) => row.consumerId));
623
- return {
624
- totalBaskets: rows.length,
625
- totalRevenue: rows.reduce((sum, row) => sum + row.basketTotal, 0),
626
- totalTax: rows.reduce((sum, row) => sum + row.tax, 0),
627
- uniqueConsumers: uniqueConsumerIds.size
628
- };
629
- }
630
- /** Builds a consumer id → consumer lookup. */
631
- function buildConsumerMap(consumers) {
632
- const map = {};
633
- if (consumers) consumers.forEach((consumer) => {
634
- map[consumer.id] = consumer;
635
- });
636
- return map;
637
- }
638
-
639
507
  //#endregion
640
508
  //#region src/core/imageFit.ts
641
509
  /**
@@ -671,6 +539,35 @@ function coverCrop(srcWidth, srcHeight, dstWidth, dstHeight) {
671
539
  function requireBrowserImages(fn) {
672
540
  if (typeof createImageBitmap !== "function") throw new Error(`${fn} needs a browser (createImageBitmap is not available)`);
673
541
  }
542
+ /** Draws `crop` of the bitmap at `width × height` and encodes it (one resample). */
543
+ async function drawCrop(bitmap, crop, width, height, type, quality, fn) {
544
+ const { sx, sy, sw, sh } = crop;
545
+ if (typeof OffscreenCanvas === "function") {
546
+ const canvas$1 = new OffscreenCanvas(width, height);
547
+ const ctx$1 = canvas$1.getContext("2d");
548
+ if (!ctx$1) throw new Error(`${fn}: no 2D canvas context`);
549
+ ctx$1.imageSmoothingQuality = "high";
550
+ ctx$1.drawImage(bitmap, sx, sy, sw, sh, 0, 0, width, height);
551
+ return canvas$1.convertToBlob({
552
+ type,
553
+ quality
554
+ });
555
+ }
556
+ const canvas = document.createElement("canvas");
557
+ canvas.width = width;
558
+ canvas.height = height;
559
+ const ctx = canvas.getContext("2d");
560
+ if (!ctx) throw new Error(`${fn}: no 2D canvas context`);
561
+ ctx.imageSmoothingQuality = "high";
562
+ ctx.drawImage(bitmap, sx, sy, sw, sh, 0, 0, width, height);
563
+ return new Promise((resolve, reject) => canvas.toBlob((b) => b ? resolve(b) : reject(/* @__PURE__ */ new Error(`${fn}: encoding failed`)), type, quality));
564
+ }
565
+ /** The input's name with the output type's extension. */
566
+ function outputName(file, type) {
567
+ const name = file.name || "image";
568
+ const ext = type === "image/jpeg" ? "jpg" : type.split("/")[1];
569
+ return `${name.replace(/\.[^.]+$/, "")}.${ext}`;
570
+ }
674
571
  /**
675
572
  * Returns the image at exactly `width × height`: scaled and centre-cropped
676
573
  * (never stretched). Browser only. The result is a `File` named like the
@@ -679,34 +576,11 @@ function requireBrowserImages(fn) {
679
576
  async function fitImageToSize(file, width, height, options = {}) {
680
577
  requireBrowserImages("fitImageToSize");
681
578
  const { type = "image/png", quality, keepIfExact = true } = options;
682
- const name = file.name || "image";
683
- const ext = type === "image/jpeg" ? "jpg" : type.split("/")[1];
684
- const outName = `${name.replace(/\.[^.]+$/, "")}.${ext}`;
579
+ const outName = outputName(file, type);
685
580
  const bitmap = await createImageBitmap(file);
686
581
  try {
687
582
  if (keepIfExact && bitmap.width === width && bitmap.height === height && file.type === type) return file instanceof File ? file : new File([file], outName, { type });
688
- const { sx, sy, sw, sh } = coverCrop(bitmap.width, bitmap.height, width, height);
689
- let blob;
690
- if (typeof OffscreenCanvas === "function") {
691
- const canvas = new OffscreenCanvas(width, height);
692
- const ctx = canvas.getContext("2d");
693
- if (!ctx) throw new Error("fitImageToSize: no 2D canvas context");
694
- ctx.imageSmoothingQuality = "high";
695
- ctx.drawImage(bitmap, sx, sy, sw, sh, 0, 0, width, height);
696
- blob = await canvas.convertToBlob({
697
- type,
698
- quality
699
- });
700
- } else {
701
- const canvas = document.createElement("canvas");
702
- canvas.width = width;
703
- canvas.height = height;
704
- const ctx = canvas.getContext("2d");
705
- if (!ctx) throw new Error("fitImageToSize: no 2D canvas context");
706
- ctx.imageSmoothingQuality = "high";
707
- ctx.drawImage(bitmap, sx, sy, sw, sh, 0, 0, width, height);
708
- blob = await new Promise((resolve, reject) => canvas.toBlob((b) => b ? resolve(b) : reject(/* @__PURE__ */ new Error("fitImageToSize: encoding failed")), type, quality));
709
- }
583
+ const blob = await drawCrop(bitmap, coverCrop(bitmap.width, bitmap.height, width, height), width, height, type, quality, "fitImageToSize");
710
584
  return new File([blob], outName, { type });
711
585
  } finally {
712
586
  bitmap.close?.();
@@ -746,10 +620,12 @@ function getImageUrl(path, base = SHOPWAVE_IMAGE_BASE_URL) {
746
620
  async function uploadImage(file, params, options = {}) {
747
621
  if (!file || typeof file.size !== "number") throw new Error("uploadImage: file must be a File or Blob");
748
622
  if (!isUploadKind(params?.kind)) throw new Error(`uploadImage: unknown kind "${params?.kind}"`);
623
+ if (params.prefix != null && !isUploadPrefix(params.prefix)) throw new Error(`uploadImage: unknown prefix "${params.prefix}"`);
749
624
  const name = params.fileName ?? file.name ?? "upload";
750
625
  const form = new FormData();
751
626
  form.append(SHOPWAVE_UPLOAD.fileField, file, name);
752
627
  form.append(SHOPWAVE_UPLOAD.kindField, params.kind);
628
+ if (params.prefix != null) form.append(SHOPWAVE_UPLOAD.prefixField, params.prefix);
753
629
  const { status, body: raw } = await apiRequest(`/api/${SHOPWAVE_UPLOAD.route}`, {
754
630
  method: "POST",
755
631
  body: form
@@ -837,6 +713,150 @@ async function uploadMerchantImage(file, slot, options = {}) {
837
713
  return uploadImage(fit ? await fitImageToSize(file, def.width, def.height, fitOptions) : file, { kind: def.uploadKind }, requestOptions);
838
714
  }
839
715
 
716
+ //#endregion
717
+ //#region src/core/basket.ts
718
+ const DEFAULT_BASKET_FILTERS = {
719
+ search: "",
720
+ dateRange: {
721
+ start: null,
722
+ end: null
723
+ },
724
+ consumerId: null,
725
+ status: "all"
726
+ };
727
+ const DEFAULT_PAGINATION = {
728
+ page: 0,
729
+ pageSize: 25,
730
+ total: 0
731
+ };
732
+ /** Formats a date as `YYYY-MM-DD HH:mm:ss` for the report query. */
733
+ function formatDateForQuery(date, isEndOfDay = false) {
734
+ return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, "0")}-${String(date.getDate()).padStart(2, "0")} ${isEndOfDay ? "23:59:59" : "00:00:00"}`;
735
+ }
736
+ /** Builds the `Basket` report query from a set of filters. */
737
+ function buildBasketReportQuery(filters) {
738
+ const whereConditions = [];
739
+ const today = /* @__PURE__ */ new Date();
740
+ const startDate = filters.dateRange.start ?? today;
741
+ const endDate = filters.dateRange.end ?? today;
742
+ whereConditions.push(`Basket.completeDate > '${formatDateForQuery(startDate, false)}'`);
743
+ whereConditions.push(`Basket.completeDate < '${formatDateForQuery(endDate, true)}'`);
744
+ if (filters.consumerId) whereConditions.push(`Basket.consumerId = ${filters.consumerId}`);
745
+ if (filters.status !== "all") whereConditions.push(`Basket.status = '${filters.status}'`);
746
+ return { baskets: {
747
+ FROM: "Basket",
748
+ WHERE: whereConditions.length > 0 ? { AND: whereConditions } : void 0
749
+ } };
750
+ }
751
+ /** Flattens the raw report rows/keys into structured baskets. */
752
+ function parseBasketReportData(reports) {
753
+ if (!reports?.baskets?.data || !reports?.baskets?.keys) return [];
754
+ const { keys, data } = reports.baskets;
755
+ const keyIndices = {};
756
+ keys.forEach((key, index) => {
757
+ keyIndices[key.name] = index;
758
+ });
759
+ const productKey = keys.find((k) => k.name === "Basket.Product");
760
+ const productSubKeys = {};
761
+ if (productKey?.item) productKey.item.forEach((subKey, index) => {
762
+ productSubKeys[subKey.name] = index;
763
+ });
764
+ return data.map((row) => {
765
+ const products = (row[keyIndices["Basket.Product"]] || []).map((productRow) => ({
766
+ productId: productRow[productSubKeys["Basket.Product.productId"]],
767
+ productName: `Product #${productRow[productSubKeys["Basket.Product.productId"]]}`,
768
+ quantity: productRow[productSubKeys["Basket.Product.quantity"]] || 1,
769
+ price: productRow[productSubKeys["Basket.Product.productPrice"]] || 0,
770
+ tax: productRow[productSubKeys["Basket.Product.productTaxPercentage"]] || 0
771
+ }));
772
+ const total = products.reduce((sum, p) => sum + p.price * p.quantity, 0);
773
+ const taxTotal = products.reduce((sum, p) => sum + p.price * p.quantity * p.tax, 0);
774
+ const completeDateTimestamp = row[keyIndices["Basket.completeDate"]];
775
+ const completeDate = completeDateTimestamp ? new Date(completeDateTimestamp).toISOString() : "";
776
+ return {
777
+ id: row[keyIndices["Basket.id"]],
778
+ name: row[keyIndices["Basket.name"]],
779
+ consumerId: row[keyIndices["Basket.consumerId"]],
780
+ products,
781
+ total,
782
+ taxTotal,
783
+ status: "completed",
784
+ completeDate
785
+ };
786
+ });
787
+ }
788
+ /** Unique non-null consumer ids referenced by a set of baskets. */
789
+ function collectConsumerIds(baskets) {
790
+ if (!baskets.length) return null;
791
+ const ids = baskets.map((b) => b.consumerId).filter((id) => id != null);
792
+ return ids.length > 0 ? [...new Set(ids)] : null;
793
+ }
794
+ /** Unique product ids referenced by a set of baskets. */
795
+ function collectProductIds(baskets) {
796
+ if (!baskets.length) return null;
797
+ const ids = /* @__PURE__ */ new Set();
798
+ baskets.forEach((basket) => {
799
+ basket.products.forEach((product) => {
800
+ if (product.productId) ids.add(product.productId);
801
+ });
802
+ });
803
+ return ids.size > 0 ? [...ids] : null;
804
+ }
805
+ /** Joins baskets with consumer + product lookups, applies search, sorts. */
806
+ function combineBasketRows({ baskets, consumerMap, productMap, search }) {
807
+ if (!baskets.length) return [];
808
+ let combinedRows = baskets.map((basket) => {
809
+ const consumer = basket.consumerId ? consumerMap[basket.consumerId] : null;
810
+ const productsWithNames = basket.products.map((p) => {
811
+ const product = productMap[p.productId];
812
+ return {
813
+ ...p,
814
+ productName: product?.name || `Product #${p.productId}`
815
+ };
816
+ });
817
+ return {
818
+ id: basket.id,
819
+ basketId: basket.id,
820
+ basketName: basket.name || `Basket #${basket.id}`,
821
+ consumerId: basket.consumerId,
822
+ consumerName: consumer ? `${consumer.firstName} ${consumer.lastName}` : "Unknown",
823
+ consumerEmail: consumer?.email || "-",
824
+ products: productsWithNames,
825
+ basketTotal: (basket.total || 0) / 100,
826
+ tax: basket.taxTotal || 0,
827
+ dateTime: basket.completeDate,
828
+ status: basket.status || "pending"
829
+ };
830
+ });
831
+ if (search) {
832
+ const searchLower = search.toLowerCase();
833
+ combinedRows = combinedRows.filter((row) => row.basketId.toString().includes(searchLower) || row.basketName.toLowerCase().includes(searchLower) || row.consumerName.toLowerCase().includes(searchLower) || row.consumerEmail.toLowerCase().includes(searchLower));
834
+ }
835
+ combinedRows.sort((a, b) => {
836
+ const dateA = new Date(a.dateTime).getTime();
837
+ return new Date(b.dateTime).getTime() - dateA;
838
+ });
839
+ return combinedRows;
840
+ }
841
+ /** Aggregate totals for a set of report rows. */
842
+ function computeBasketSummary(rows) {
843
+ const uniqueConsumerIds = new Set(rows.map((row) => row.consumerId));
844
+ return {
845
+ totalBaskets: rows.length,
846
+ totalRevenue: rows.reduce((sum, row) => sum + row.basketTotal, 0),
847
+ totalTax: rows.reduce((sum, row) => sum + row.tax, 0),
848
+ uniqueConsumers: uniqueConsumerIds.size
849
+ };
850
+ }
851
+ /** Builds a consumer id → consumer lookup. */
852
+ function buildConsumerMap(consumers) {
853
+ const map = {};
854
+ if (consumers) consumers.forEach((consumer) => {
855
+ map[consumer.id] = consumer;
856
+ });
857
+ return map;
858
+ }
859
+
840
860
  //#endregion
841
861
  //#region src/core/merchant.ts
842
862
  const MERCHANT_PATH = `/api/${SHOPWAVE_RESOURCES.merchant.route}`;