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.
@@ -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
  /**
@@ -746,10 +614,12 @@ function getImageUrl(path, base = SHOPWAVE_IMAGE_BASE_URL) {
746
614
  async function uploadImage(file, params, options = {}) {
747
615
  if (!file || typeof file.size !== "number") throw new Error("uploadImage: file must be a File or Blob");
748
616
  if (!isUploadKind(params?.kind)) throw new Error(`uploadImage: unknown kind "${params?.kind}"`);
617
+ if (params.prefix != null && !isUploadPrefix(params.prefix)) throw new Error(`uploadImage: unknown prefix "${params.prefix}"`);
749
618
  const name = params.fileName ?? file.name ?? "upload";
750
619
  const form = new FormData();
751
620
  form.append(SHOPWAVE_UPLOAD.fileField, file, name);
752
621
  form.append(SHOPWAVE_UPLOAD.kindField, params.kind);
622
+ if (params.prefix != null) form.append(SHOPWAVE_UPLOAD.prefixField, params.prefix);
753
623
  const { status, body: raw } = await apiRequest(`/api/${SHOPWAVE_UPLOAD.route}`, {
754
624
  method: "POST",
755
625
  body: form
@@ -837,6 +707,150 @@ async function uploadMerchantImage(file, slot, options = {}) {
837
707
  return uploadImage(fit ? await fitImageToSize(file, def.width, def.height, fitOptions) : file, { kind: def.uploadKind }, requestOptions);
838
708
  }
839
709
 
710
+ //#endregion
711
+ //#region src/core/basket.ts
712
+ const DEFAULT_BASKET_FILTERS = {
713
+ search: "",
714
+ dateRange: {
715
+ start: null,
716
+ end: null
717
+ },
718
+ consumerId: null,
719
+ status: "all"
720
+ };
721
+ const DEFAULT_PAGINATION = {
722
+ page: 0,
723
+ pageSize: 25,
724
+ total: 0
725
+ };
726
+ /** Formats a date as `YYYY-MM-DD HH:mm:ss` for the report query. */
727
+ function formatDateForQuery(date, isEndOfDay = false) {
728
+ return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, "0")}-${String(date.getDate()).padStart(2, "0")} ${isEndOfDay ? "23:59:59" : "00:00:00"}`;
729
+ }
730
+ /** Builds the `Basket` report query from a set of filters. */
731
+ function buildBasketReportQuery(filters) {
732
+ const whereConditions = [];
733
+ const today = /* @__PURE__ */ new Date();
734
+ const startDate = filters.dateRange.start ?? today;
735
+ const endDate = filters.dateRange.end ?? today;
736
+ whereConditions.push(`Basket.completeDate > '${formatDateForQuery(startDate, false)}'`);
737
+ whereConditions.push(`Basket.completeDate < '${formatDateForQuery(endDate, true)}'`);
738
+ if (filters.consumerId) whereConditions.push(`Basket.consumerId = ${filters.consumerId}`);
739
+ if (filters.status !== "all") whereConditions.push(`Basket.status = '${filters.status}'`);
740
+ return { baskets: {
741
+ FROM: "Basket",
742
+ WHERE: whereConditions.length > 0 ? { AND: whereConditions } : void 0
743
+ } };
744
+ }
745
+ /** Flattens the raw report rows/keys into structured baskets. */
746
+ function parseBasketReportData(reports) {
747
+ if (!reports?.baskets?.data || !reports?.baskets?.keys) return [];
748
+ const { keys, data } = reports.baskets;
749
+ const keyIndices = {};
750
+ keys.forEach((key, index) => {
751
+ keyIndices[key.name] = index;
752
+ });
753
+ const productKey = keys.find((k) => k.name === "Basket.Product");
754
+ const productSubKeys = {};
755
+ if (productKey?.item) productKey.item.forEach((subKey, index) => {
756
+ productSubKeys[subKey.name] = index;
757
+ });
758
+ return data.map((row) => {
759
+ const products = (row[keyIndices["Basket.Product"]] || []).map((productRow) => ({
760
+ productId: productRow[productSubKeys["Basket.Product.productId"]],
761
+ productName: `Product #${productRow[productSubKeys["Basket.Product.productId"]]}`,
762
+ quantity: productRow[productSubKeys["Basket.Product.quantity"]] || 1,
763
+ price: productRow[productSubKeys["Basket.Product.productPrice"]] || 0,
764
+ tax: productRow[productSubKeys["Basket.Product.productTaxPercentage"]] || 0
765
+ }));
766
+ const total = products.reduce((sum, p) => sum + p.price * p.quantity, 0);
767
+ const taxTotal = products.reduce((sum, p) => sum + p.price * p.quantity * p.tax, 0);
768
+ const completeDateTimestamp = row[keyIndices["Basket.completeDate"]];
769
+ const completeDate = completeDateTimestamp ? new Date(completeDateTimestamp).toISOString() : "";
770
+ return {
771
+ id: row[keyIndices["Basket.id"]],
772
+ name: row[keyIndices["Basket.name"]],
773
+ consumerId: row[keyIndices["Basket.consumerId"]],
774
+ products,
775
+ total,
776
+ taxTotal,
777
+ status: "completed",
778
+ completeDate
779
+ };
780
+ });
781
+ }
782
+ /** Unique non-null consumer ids referenced by a set of baskets. */
783
+ function collectConsumerIds(baskets) {
784
+ if (!baskets.length) return null;
785
+ const ids = baskets.map((b) => b.consumerId).filter((id) => id != null);
786
+ return ids.length > 0 ? [...new Set(ids)] : null;
787
+ }
788
+ /** Unique product ids referenced by a set of baskets. */
789
+ function collectProductIds(baskets) {
790
+ if (!baskets.length) return null;
791
+ const ids = /* @__PURE__ */ new Set();
792
+ baskets.forEach((basket) => {
793
+ basket.products.forEach((product) => {
794
+ if (product.productId) ids.add(product.productId);
795
+ });
796
+ });
797
+ return ids.size > 0 ? [...ids] : null;
798
+ }
799
+ /** Joins baskets with consumer + product lookups, applies search, sorts. */
800
+ function combineBasketRows({ baskets, consumerMap, productMap, search }) {
801
+ if (!baskets.length) return [];
802
+ let combinedRows = baskets.map((basket) => {
803
+ const consumer = basket.consumerId ? consumerMap[basket.consumerId] : null;
804
+ const productsWithNames = basket.products.map((p) => {
805
+ const product = productMap[p.productId];
806
+ return {
807
+ ...p,
808
+ productName: product?.name || `Product #${p.productId}`
809
+ };
810
+ });
811
+ return {
812
+ id: basket.id,
813
+ basketId: basket.id,
814
+ basketName: basket.name || `Basket #${basket.id}`,
815
+ consumerId: basket.consumerId,
816
+ consumerName: consumer ? `${consumer.firstName} ${consumer.lastName}` : "Unknown",
817
+ consumerEmail: consumer?.email || "-",
818
+ products: productsWithNames,
819
+ basketTotal: (basket.total || 0) / 100,
820
+ tax: basket.taxTotal || 0,
821
+ dateTime: basket.completeDate,
822
+ status: basket.status || "pending"
823
+ };
824
+ });
825
+ if (search) {
826
+ const searchLower = search.toLowerCase();
827
+ combinedRows = combinedRows.filter((row) => row.basketId.toString().includes(searchLower) || row.basketName.toLowerCase().includes(searchLower) || row.consumerName.toLowerCase().includes(searchLower) || row.consumerEmail.toLowerCase().includes(searchLower));
828
+ }
829
+ combinedRows.sort((a, b) => {
830
+ const dateA = new Date(a.dateTime).getTime();
831
+ return new Date(b.dateTime).getTime() - dateA;
832
+ });
833
+ return combinedRows;
834
+ }
835
+ /** Aggregate totals for a set of report rows. */
836
+ function computeBasketSummary(rows) {
837
+ const uniqueConsumerIds = new Set(rows.map((row) => row.consumerId));
838
+ return {
839
+ totalBaskets: rows.length,
840
+ totalRevenue: rows.reduce((sum, row) => sum + row.basketTotal, 0),
841
+ totalTax: rows.reduce((sum, row) => sum + row.tax, 0),
842
+ uniqueConsumers: uniqueConsumerIds.size
843
+ };
844
+ }
845
+ /** Builds a consumer id → consumer lookup. */
846
+ function buildConsumerMap(consumers) {
847
+ const map = {};
848
+ if (consumers) consumers.forEach((consumer) => {
849
+ map[consumer.id] = consumer;
850
+ });
851
+ return map;
852
+ }
853
+
840
854
  //#endregion
841
855
  //#region src/core/merchant.ts
842
856
  const MERCHANT_PATH = `/api/${SHOPWAVE_RESOURCES.merchant.route}`;