react-shopwave-connect 0.4.1 → 0.4.3

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.
@@ -340,6 +340,26 @@ declare function fetchProductsMap(productIds: number[] | null | undefined, optio
340
340
  [id: number]: Product;
341
341
  }>;
342
342
  //#endregion
343
+ //#region src/core/productTags.d.ts
344
+ /**
345
+ * Turns a tag list from the API into clean strings: trims each tag, drops
346
+ * blanks and anything that isn't text, and removes exact duplicates (first one
347
+ * wins). The API's own order is kept. A map is accepted too (its values are
348
+ * used), in case the API ever answers keyed by id.
349
+ */
350
+ declare function normalizeTagList(value: unknown): string[];
351
+ /**
352
+ * Every distinct tag used on the merchant's products (`GET /product/tags`),
353
+ * for a tag field's suggestions. Returns `[]` when there are none.
354
+ */
355
+ declare function fetchProductTags(options?: RequestOptions): Promise<string[]>;
356
+ /**
357
+ * Every distinct tag used on the merchant's price instances
358
+ * (`GET /product/instance/tags`): the channel names (Eat In, Takeout, …).
359
+ * Returns `[]` when there are none.
360
+ */
361
+ declare function fetchProductInstanceTags(options?: RequestOptions): Promise<string[]>;
362
+ //#endregion
343
363
  //#region src/core/promotion.d.ts
344
364
  /** Promotion rule identifiers as stored by the Shopwave API. */
345
365
  declare enum PromotionRuleSet {
@@ -537,8 +557,9 @@ declare function submitEntity<R = any>({
537
557
  //#region src/core/entities.d.ts
538
558
  /**
539
559
  * How each Shopwave entity is addressed, shared by the client functions below
540
- * and the server route handlers in `react-shopwave-connect/next`, so the two
541
- * halves of the contract can't drift apart.
560
+ * and the server route handlers in `react-shopwave-connect/server` (wrapped with
561
+ * the session by `react-shopwave-connect/next`), so the two halves of the
562
+ * contract can't drift apart.
542
563
  */
543
564
  interface EntityDefinition {
544
565
  /** App route segment: `/api/<route>` and `/api/<route>/<id>`. */
@@ -556,13 +577,25 @@ interface EntityDefinition {
556
577
  /**
557
578
  * How a delete is done upstream:
558
579
  * - `"delete"`: `DELETE {apiUrl}/<upstream>` with the `idHeader` (205, empty body)
559
- * - `"retire"`: the API has no DELETE; the record is retired by POSTing
560
- * `{ id, [retireField]: <now> }` (employees: `exitDate`, promotions: `endDate`)
580
+ * - `"retire"`: the API has no DELETE; the record is retired by setting
581
+ * `retireField` to now (employees: `exitDate`, promotions: `endDate`)
561
582
  * - `"none"`: can't be deleted
562
583
  */
563
584
  deleteMode: "delete" | "retire" | "none";
564
585
  /** Date field set to "now" when `deleteMode` is `"retire"`. */
565
586
  retireField?: string;
587
+ /**
588
+ * Retire by reading the record and POSTing all of it back with the date(s)
589
+ * changed, instead of a partial `{ id, [retireField]: <now> }`. Needed where
590
+ * the API rejects a partial save (promotions: error 899, live 14 Oct 2026).
591
+ */
592
+ retireWithRecord?: boolean;
593
+ /**
594
+ * Start-date field of a record retired with `retireWithRecord`. If the record
595
+ * hasn't started yet (start after now), the start moves to now as well, so
596
+ * start = end and the record is expired without ever running.
597
+ */
598
+ retireStartField?: string;
566
599
  }
567
600
  type EntityKind = "product" | "category" | "store" | "promotion" | "employee" | "consumer";
568
601
  /** Entities that can be created/updated through the API. */
@@ -574,7 +607,10 @@ type DeletableEntityKind = Exclude<EntityKind, "consumer">;
574
607
  * (https://developer.merchantstack.com/api-reference.html) and checked against
575
608
  * the live API where noted:
576
609
  * - category/product/store: GET filtered by `<x>Ids`, POST upsert, DELETE with `<x>Id` → 205.
577
- * - promotion: no DELETE; "deleting" a promotion ends it by setting `endDate` to now.
610
+ * - promotion: no DELETE; promotions are never deleted, only expired. Expiring
611
+ * reads the promotion and saves the whole record back with `endDate` = now (and
612
+ * `startDate` = now too if it hadn't started); the live API rejects a partial
613
+ * `{ id, endDate }` save with error 899. Dates are UTC `YYYY-MM-DD HH:mm:ss`.
578
614
  * - employee: POST updates `roleId`, `joinedDate`, `exitDate` (names can't be
579
615
  * changed); no DELETE (live API: "Cannot DELETE /employee") → retired via `exitDate`.
580
616
  * - consumer: read-only (`GET` with `ids`, comma-separated); live POST → 404.
@@ -618,7 +654,8 @@ declare function saveEntity<K extends WritableEntityKind>(kind: K, item: EntityI
618
654
  /**
619
655
  * Deletes one entity (a soft delete: it stays readable with `deleted: true`).
620
656
  * Employees and promotions can't be deleted in Shopwave; for them the route
621
- * sets `exitDate` / `endDate` to now instead (see `SHOPWAVE_ENTITIES`).
657
+ * sets `exitDate` / `endDate` to now instead (see `SHOPWAVE_ENTITIES`), and a
658
+ * promotion is saved back whole (see {@link deletePromotion}).
622
659
  *
623
660
  * Shopwave answers 205 with an empty body — also for ids that don't exist — so
624
661
  * a resolved promise means "the API accepted the request", not "a record was
@@ -650,7 +687,12 @@ declare const saveStore: (store: EntityInput<Store>, options?: RequestOptions) =
650
687
  declare const deleteStore: (id: EntityId, options?: RequestOptions) => Promise<void>;
651
688
  declare const fetchStore: (id: EntityId, params?: FetchByIdParams, options?: RequestOptions) => Promise<Store | null>;
652
689
  declare const savePromotion: (promotion: EntityInput<Promotion>, options?: RequestOptions) => Promise<Saved<Promotion>>;
653
- /** Ends a promotion: Shopwave has no promotion DELETE, so this sets `endDate` to now. */
690
+ /**
691
+ * Expires a promotion (Shopwave never deletes promotions). The route reads the
692
+ * promotion and saves it back with `endDate` set to now; a promotion that hasn't
693
+ * started yet gets `startDate` = `endDate` = now. Rejects if the promotion doesn't
694
+ * exist or the save fails.
695
+ */
654
696
  declare const deletePromotion: (id: EntityId, options?: RequestOptions) => Promise<void>;
655
697
  declare const fetchPromotion: (id: EntityId, params?: FetchByIdParams, options?: RequestOptions) => Promise<Promotion | null>;
656
698
  /**
@@ -769,7 +811,7 @@ declare function buildConsumerMap(consumers: Consumer[] | null | undefined): {
769
811
  *
770
812
  * Like `SHOPWAVE_ENTITIES`, this table is shared by the client functions
771
813
  * (`fetchMerchant`, `fetchUser`, `uploadImage`, …) and the server route
772
- * handlers in `react-shopwave-connect/next`, so the two halves of the contract
814
+ * handlers in `react-shopwave-connect/server`, so the two halves of the contract
773
815
  * can't drift apart. These aren't in `SHOPWAVE_ENTITIES` because they aren't
774
816
  * keyed collections: `GET /merchant` answers `{ merchant: {…} }`, not a map by id.
775
817
  */
@@ -816,6 +858,35 @@ declare const SHOPWAVE_UPLOAD: Readonly<{
816
858
  kindField: "kind";
817
859
  }>;
818
860
  declare function isUploadKind(value: unknown): value is UploadKind;
861
+ /**
862
+ * Read-only lists that aren't records: the merchant's distinct tags.
863
+ *
864
+ * Like `SHOPWAVE_RESOURCES`, shared by the client functions
865
+ * (`fetchProductTags`, `fetchProductInstanceTags`) and the server route
866
+ * handlers (`shopwave.lookup(kind)`), so the two halves can't drift apart.
867
+ *
868
+ * - productTags: `GET /product/tags` → `{ tags: string[] }`, every distinct tag
869
+ * on the merchant's products. No parameters. The API reference says
870
+ * "internal SKU tags are excluded", but the live API includes system tags
871
+ * written by imports and syncs (`PID-133`, `VID-137`, `#Hash:…`, `upc-…`;
872
+ * test merchant 5644, 29 Oct 2026). Apps that offer the list as suggestions
873
+ * filter those themselves. Tags of deleted products drop out of the list.
874
+ * - productInstanceTags: `GET /product/instance/tags` → `{ tags: string[] }`,
875
+ * the same for price instances (channels such as "Eat In", "Takeout").
876
+ *
877
+ * The app route for instance tags is `products/instance-tags`, one segment
878
+ * under `products`, so it sits beside `products/[id]` without nesting deeper.
879
+ */
880
+ interface LookupDefinition {
881
+ /** App route: `/api/<route>`. */
882
+ route: string;
883
+ /** Key of the list in the response (`{ tags: [...] }`). */
884
+ key: string;
885
+ /** Shopwave API path. */
886
+ upstream: string;
887
+ }
888
+ type LookupKind = "productTags" | "productInstanceTags";
889
+ declare const SHOPWAVE_LOOKUPS: Readonly<Record<LookupKind, LookupDefinition>>;
819
890
  //#endregion
820
891
  //#region src/core/imageFit.d.ts
821
892
  /**
@@ -1153,5 +1224,5 @@ interface UserResponse {
1153
1224
  /** Reads the logged-in user, or `null` when the API returns none. */
1154
1225
  declare function fetchUser(options?: RequestOptions): Promise<User | null>;
1155
1226
  //#endregion
1156
- export { Basket, BasketFilters, BasketItem, BasketPagination, BasketReportRow, BasketSummary, Category, CategoryResponse, CombineBasketRowsParams, Consumer, ConsumerResponse, CropRect, DEFAULT_BASKET_FILTERS, DEFAULT_PAGINATION, DeletableEntityKind, Employee, EmployeeResponse, EntityDefinition, EntityId, EntityInput, EntityKind, EntityTypes, ErrorMessage, Extras, FetchByIdParams, FetchCategoriesParams, FetchEmployeesParams, FetchProductsParams, FetchPromotionsParams, FetchStoresParams, FitImageOptions, ImageSize, 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, Merchant, MerchantColourKey, MerchantColourSet, MerchantColours, MerchantImageIds, MerchantImageSlot, MerchantImageSlotDefinition, MerchantLinkKey, MerchantLinks, MerchantPatch, MerchantResponse, MessageInfo, ParsedBasket, Product, ProductInstance, ProductInstanceInput, ProductInstances, ProductResponse, ProductSaveInput, Promotion, PromotionResponse, PromotionRuleSet, ReportData, ReportKey, ReportQuery, ReportQueryMap, ReportResponse, RepricedInstance, RequestOptions, ResourceDefinition, ResourceKind, SHOPWAVE_ENTITIES, SHOPWAVE_IMAGE_BASE_URL, SHOPWAVE_RESOURCES, SHOPWAVE_UPLOAD, Saved, SessionStatus, ShopwaveApiError, ShopwaveErrorMap, Store, StoreResponse, SubmitEntityArgs, SubmitEntityResponse, SuccessMessage, UPLOAD_KINDS, UpdateMerchantOptions, UploadImageParams, UploadKind, UploadMerchantImageOptions, UploadedImage, User, UserEmployee, UserResponse, WritableEntityKind, apiDelete, apiGet, apiRequest, apiResponse, 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, 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, parseBasketReportData, readImageSize, rolesById, saveCategory, saveEmployee, saveEntities, saveEntity, saveProduct, savePromotion, saveStore, setMerchantImage, submitEntity, toProductInstancesForSave, toShopwaveApiError, toStoredImageIds, updateMerchant, uploadImage, uploadMerchantImage, validateMerchant, withToken };
1227
+ export { Basket, BasketFilters, BasketItem, BasketPagination, BasketReportRow, BasketSummary, Category, CategoryResponse, CombineBasketRowsParams, Consumer, ConsumerResponse, CropRect, DEFAULT_BASKET_FILTERS, DEFAULT_PAGINATION, DeletableEntityKind, Employee, EmployeeResponse, EntityDefinition, EntityId, EntityInput, EntityKind, EntityTypes, ErrorMessage, Extras, FetchByIdParams, FetchCategoriesParams, FetchEmployeesParams, FetchProductsParams, FetchPromotionsParams, FetchStoresParams, FitImageOptions, ImageSize, LookupDefinition, LookupKind, 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, Merchant, MerchantColourKey, MerchantColourSet, MerchantColours, MerchantImageIds, MerchantImageSlot, MerchantImageSlotDefinition, MerchantLinkKey, MerchantLinks, MerchantPatch, MerchantResponse, MessageInfo, ParsedBasket, Product, ProductInstance, ProductInstanceInput, ProductInstances, ProductResponse, ProductSaveInput, Promotion, PromotionResponse, PromotionRuleSet, ReportData, ReportKey, ReportQuery, ReportQueryMap, ReportResponse, RepricedInstance, RequestOptions, ResourceDefinition, ResourceKind, SHOPWAVE_ENTITIES, SHOPWAVE_IMAGE_BASE_URL, SHOPWAVE_LOOKUPS, SHOPWAVE_RESOURCES, SHOPWAVE_UPLOAD, Saved, SessionStatus, ShopwaveApiError, ShopwaveErrorMap, Store, StoreResponse, SubmitEntityArgs, SubmitEntityResponse, SuccessMessage, UPLOAD_KINDS, UpdateMerchantOptions, UploadImageParams, UploadKind, UploadMerchantImageOptions, UploadedImage, User, UserEmployee, UserResponse, WritableEntityKind, apiDelete, apiGet, apiRequest, apiResponse, 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, validateMerchant, withToken };
1157
1228
  //# sourceMappingURL=index.d.cts.map
@@ -340,6 +340,26 @@ declare function fetchProductsMap(productIds: number[] | null | undefined, optio
340
340
  [id: number]: Product;
341
341
  }>;
342
342
  //#endregion
343
+ //#region src/core/productTags.d.ts
344
+ /**
345
+ * Turns a tag list from the API into clean strings: trims each tag, drops
346
+ * blanks and anything that isn't text, and removes exact duplicates (first one
347
+ * wins). The API's own order is kept. A map is accepted too (its values are
348
+ * used), in case the API ever answers keyed by id.
349
+ */
350
+ declare function normalizeTagList(value: unknown): string[];
351
+ /**
352
+ * Every distinct tag used on the merchant's products (`GET /product/tags`),
353
+ * for a tag field's suggestions. Returns `[]` when there are none.
354
+ */
355
+ declare function fetchProductTags(options?: RequestOptions): Promise<string[]>;
356
+ /**
357
+ * Every distinct tag used on the merchant's price instances
358
+ * (`GET /product/instance/tags`): the channel names (Eat In, Takeout, …).
359
+ * Returns `[]` when there are none.
360
+ */
361
+ declare function fetchProductInstanceTags(options?: RequestOptions): Promise<string[]>;
362
+ //#endregion
343
363
  //#region src/core/promotion.d.ts
344
364
  /** Promotion rule identifiers as stored by the Shopwave API. */
345
365
  declare enum PromotionRuleSet {
@@ -537,8 +557,9 @@ declare function submitEntity<R = any>({
537
557
  //#region src/core/entities.d.ts
538
558
  /**
539
559
  * How each Shopwave entity is addressed, shared by the client functions below
540
- * and the server route handlers in `react-shopwave-connect/next`, so the two
541
- * halves of the contract can't drift apart.
560
+ * and the server route handlers in `react-shopwave-connect/server` (wrapped with
561
+ * the session by `react-shopwave-connect/next`), so the two halves of the
562
+ * contract can't drift apart.
542
563
  */
543
564
  interface EntityDefinition {
544
565
  /** App route segment: `/api/<route>` and `/api/<route>/<id>`. */
@@ -556,13 +577,25 @@ interface EntityDefinition {
556
577
  /**
557
578
  * How a delete is done upstream:
558
579
  * - `"delete"`: `DELETE {apiUrl}/<upstream>` with the `idHeader` (205, empty body)
559
- * - `"retire"`: the API has no DELETE; the record is retired by POSTing
560
- * `{ id, [retireField]: <now> }` (employees: `exitDate`, promotions: `endDate`)
580
+ * - `"retire"`: the API has no DELETE; the record is retired by setting
581
+ * `retireField` to now (employees: `exitDate`, promotions: `endDate`)
561
582
  * - `"none"`: can't be deleted
562
583
  */
563
584
  deleteMode: "delete" | "retire" | "none";
564
585
  /** Date field set to "now" when `deleteMode` is `"retire"`. */
565
586
  retireField?: string;
587
+ /**
588
+ * Retire by reading the record and POSTing all of it back with the date(s)
589
+ * changed, instead of a partial `{ id, [retireField]: <now> }`. Needed where
590
+ * the API rejects a partial save (promotions: error 899, live 14 Oct 2026).
591
+ */
592
+ retireWithRecord?: boolean;
593
+ /**
594
+ * Start-date field of a record retired with `retireWithRecord`. If the record
595
+ * hasn't started yet (start after now), the start moves to now as well, so
596
+ * start = end and the record is expired without ever running.
597
+ */
598
+ retireStartField?: string;
566
599
  }
567
600
  type EntityKind = "product" | "category" | "store" | "promotion" | "employee" | "consumer";
568
601
  /** Entities that can be created/updated through the API. */
@@ -574,7 +607,10 @@ type DeletableEntityKind = Exclude<EntityKind, "consumer">;
574
607
  * (https://developer.merchantstack.com/api-reference.html) and checked against
575
608
  * the live API where noted:
576
609
  * - category/product/store: GET filtered by `<x>Ids`, POST upsert, DELETE with `<x>Id` → 205.
577
- * - promotion: no DELETE; "deleting" a promotion ends it by setting `endDate` to now.
610
+ * - promotion: no DELETE; promotions are never deleted, only expired. Expiring
611
+ * reads the promotion and saves the whole record back with `endDate` = now (and
612
+ * `startDate` = now too if it hadn't started); the live API rejects a partial
613
+ * `{ id, endDate }` save with error 899. Dates are UTC `YYYY-MM-DD HH:mm:ss`.
578
614
  * - employee: POST updates `roleId`, `joinedDate`, `exitDate` (names can't be
579
615
  * changed); no DELETE (live API: "Cannot DELETE /employee") → retired via `exitDate`.
580
616
  * - consumer: read-only (`GET` with `ids`, comma-separated); live POST → 404.
@@ -618,7 +654,8 @@ declare function saveEntity<K extends WritableEntityKind>(kind: K, item: EntityI
618
654
  /**
619
655
  * Deletes one entity (a soft delete: it stays readable with `deleted: true`).
620
656
  * Employees and promotions can't be deleted in Shopwave; for them the route
621
- * sets `exitDate` / `endDate` to now instead (see `SHOPWAVE_ENTITIES`).
657
+ * sets `exitDate` / `endDate` to now instead (see `SHOPWAVE_ENTITIES`), and a
658
+ * promotion is saved back whole (see {@link deletePromotion}).
622
659
  *
623
660
  * Shopwave answers 205 with an empty body — also for ids that don't exist — so
624
661
  * a resolved promise means "the API accepted the request", not "a record was
@@ -650,7 +687,12 @@ declare const saveStore: (store: EntityInput<Store>, options?: RequestOptions) =
650
687
  declare const deleteStore: (id: EntityId, options?: RequestOptions) => Promise<void>;
651
688
  declare const fetchStore: (id: EntityId, params?: FetchByIdParams, options?: RequestOptions) => Promise<Store | null>;
652
689
  declare const savePromotion: (promotion: EntityInput<Promotion>, options?: RequestOptions) => Promise<Saved<Promotion>>;
653
- /** Ends a promotion: Shopwave has no promotion DELETE, so this sets `endDate` to now. */
690
+ /**
691
+ * Expires a promotion (Shopwave never deletes promotions). The route reads the
692
+ * promotion and saves it back with `endDate` set to now; a promotion that hasn't
693
+ * started yet gets `startDate` = `endDate` = now. Rejects if the promotion doesn't
694
+ * exist or the save fails.
695
+ */
654
696
  declare const deletePromotion: (id: EntityId, options?: RequestOptions) => Promise<void>;
655
697
  declare const fetchPromotion: (id: EntityId, params?: FetchByIdParams, options?: RequestOptions) => Promise<Promotion | null>;
656
698
  /**
@@ -769,7 +811,7 @@ declare function buildConsumerMap(consumers: Consumer[] | null | undefined): {
769
811
  *
770
812
  * Like `SHOPWAVE_ENTITIES`, this table is shared by the client functions
771
813
  * (`fetchMerchant`, `fetchUser`, `uploadImage`, …) and the server route
772
- * handlers in `react-shopwave-connect/next`, so the two halves of the contract
814
+ * handlers in `react-shopwave-connect/server`, so the two halves of the contract
773
815
  * can't drift apart. These aren't in `SHOPWAVE_ENTITIES` because they aren't
774
816
  * keyed collections: `GET /merchant` answers `{ merchant: {…} }`, not a map by id.
775
817
  */
@@ -816,6 +858,35 @@ declare const SHOPWAVE_UPLOAD: Readonly<{
816
858
  kindField: "kind";
817
859
  }>;
818
860
  declare function isUploadKind(value: unknown): value is UploadKind;
861
+ /**
862
+ * Read-only lists that aren't records: the merchant's distinct tags.
863
+ *
864
+ * Like `SHOPWAVE_RESOURCES`, shared by the client functions
865
+ * (`fetchProductTags`, `fetchProductInstanceTags`) and the server route
866
+ * handlers (`shopwave.lookup(kind)`), so the two halves can't drift apart.
867
+ *
868
+ * - productTags: `GET /product/tags` → `{ tags: string[] }`, every distinct tag
869
+ * on the merchant's products. No parameters. The API reference says
870
+ * "internal SKU tags are excluded", but the live API includes system tags
871
+ * written by imports and syncs (`PID-133`, `VID-137`, `#Hash:…`, `upc-…`;
872
+ * test merchant 5644, 29 Oct 2026). Apps that offer the list as suggestions
873
+ * filter those themselves. Tags of deleted products drop out of the list.
874
+ * - productInstanceTags: `GET /product/instance/tags` → `{ tags: string[] }`,
875
+ * the same for price instances (channels such as "Eat In", "Takeout").
876
+ *
877
+ * The app route for instance tags is `products/instance-tags`, one segment
878
+ * under `products`, so it sits beside `products/[id]` without nesting deeper.
879
+ */
880
+ interface LookupDefinition {
881
+ /** App route: `/api/<route>`. */
882
+ route: string;
883
+ /** Key of the list in the response (`{ tags: [...] }`). */
884
+ key: string;
885
+ /** Shopwave API path. */
886
+ upstream: string;
887
+ }
888
+ type LookupKind = "productTags" | "productInstanceTags";
889
+ declare const SHOPWAVE_LOOKUPS: Readonly<Record<LookupKind, LookupDefinition>>;
819
890
  //#endregion
820
891
  //#region src/core/imageFit.d.ts
821
892
  /**
@@ -1153,5 +1224,5 @@ interface UserResponse {
1153
1224
  /** Reads the logged-in user, or `null` when the API returns none. */
1154
1225
  declare function fetchUser(options?: RequestOptions): Promise<User | null>;
1155
1226
  //#endregion
1156
- export { Basket, BasketFilters, BasketItem, BasketPagination, BasketReportRow, BasketSummary, Category, CategoryResponse, CombineBasketRowsParams, Consumer, ConsumerResponse, CropRect, DEFAULT_BASKET_FILTERS, DEFAULT_PAGINATION, DeletableEntityKind, Employee, EmployeeResponse, EntityDefinition, EntityId, EntityInput, EntityKind, EntityTypes, ErrorMessage, Extras, FetchByIdParams, FetchCategoriesParams, FetchEmployeesParams, FetchProductsParams, FetchPromotionsParams, FetchStoresParams, FitImageOptions, ImageSize, 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, Merchant, MerchantColourKey, MerchantColourSet, MerchantColours, MerchantImageIds, MerchantImageSlot, MerchantImageSlotDefinition, MerchantLinkKey, MerchantLinks, MerchantPatch, MerchantResponse, MessageInfo, ParsedBasket, Product, ProductInstance, ProductInstanceInput, ProductInstances, ProductResponse, ProductSaveInput, Promotion, PromotionResponse, PromotionRuleSet, ReportData, ReportKey, ReportQuery, ReportQueryMap, ReportResponse, RepricedInstance, RequestOptions, ResourceDefinition, ResourceKind, SHOPWAVE_ENTITIES, SHOPWAVE_IMAGE_BASE_URL, SHOPWAVE_RESOURCES, SHOPWAVE_UPLOAD, Saved, SessionStatus, ShopwaveApiError, ShopwaveErrorMap, Store, StoreResponse, SubmitEntityArgs, SubmitEntityResponse, SuccessMessage, UPLOAD_KINDS, UpdateMerchantOptions, UploadImageParams, UploadKind, UploadMerchantImageOptions, UploadedImage, User, UserEmployee, UserResponse, WritableEntityKind, apiDelete, apiGet, apiRequest, apiResponse, 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, 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, parseBasketReportData, readImageSize, rolesById, saveCategory, saveEmployee, saveEntities, saveEntity, saveProduct, savePromotion, saveStore, setMerchantImage, submitEntity, toProductInstancesForSave, toShopwaveApiError, toStoredImageIds, updateMerchant, uploadImage, uploadMerchantImage, validateMerchant, withToken };
1227
+ export { Basket, BasketFilters, BasketItem, BasketPagination, BasketReportRow, BasketSummary, Category, CategoryResponse, CombineBasketRowsParams, Consumer, ConsumerResponse, CropRect, DEFAULT_BASKET_FILTERS, DEFAULT_PAGINATION, DeletableEntityKind, Employee, EmployeeResponse, EntityDefinition, EntityId, EntityInput, EntityKind, EntityTypes, ErrorMessage, Extras, FetchByIdParams, FetchCategoriesParams, FetchEmployeesParams, FetchProductsParams, FetchPromotionsParams, FetchStoresParams, FitImageOptions, ImageSize, LookupDefinition, LookupKind, 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, Merchant, MerchantColourKey, MerchantColourSet, MerchantColours, MerchantImageIds, MerchantImageSlot, MerchantImageSlotDefinition, MerchantLinkKey, MerchantLinks, MerchantPatch, MerchantResponse, MessageInfo, ParsedBasket, Product, ProductInstance, ProductInstanceInput, ProductInstances, ProductResponse, ProductSaveInput, Promotion, PromotionResponse, PromotionRuleSet, ReportData, ReportKey, ReportQuery, ReportQueryMap, ReportResponse, RepricedInstance, RequestOptions, ResourceDefinition, ResourceKind, SHOPWAVE_ENTITIES, SHOPWAVE_IMAGE_BASE_URL, SHOPWAVE_LOOKUPS, SHOPWAVE_RESOURCES, SHOPWAVE_UPLOAD, Saved, SessionStatus, ShopwaveApiError, ShopwaveErrorMap, Store, StoreResponse, SubmitEntityArgs, SubmitEntityResponse, SuccessMessage, UPLOAD_KINDS, UpdateMerchantOptions, UploadImageParams, UploadKind, UploadMerchantImageOptions, UploadedImage, User, UserEmployee, UserResponse, WritableEntityKind, apiDelete, apiGet, apiRequest, apiResponse, 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, validateMerchant, withToken };
1157
1228
  //# sourceMappingURL=index.d.ts.map
@@ -349,6 +349,106 @@ async function fetchProductsMap(productIds, options = {}, batchSize = 200) {
349
349
  return result;
350
350
  }
351
351
 
352
+ //#endregion
353
+ //#region src/core/resources.ts
354
+ /**
355
+ * - merchant: `GET /merchant`; `POST /merchant` with `{ merchant: { id, … } }`
356
+ * updates it (without an `id` the API would create a second merchant, so the
357
+ * route requires one).
358
+ * - user: `GET /user` (read-only here; the API's `POST /user` invites users and
359
+ * needs the admin scope).
360
+ */
361
+ const SHOPWAVE_RESOURCES = Object.freeze({
362
+ merchant: {
363
+ route: "merchant",
364
+ key: "merchant",
365
+ upstream: "merchant",
366
+ writable: true
367
+ },
368
+ user: {
369
+ route: "user",
370
+ key: "user",
371
+ upstream: "user",
372
+ writable: false
373
+ }
374
+ });
375
+ /** `UploadKind` → the API's `contentType` value. */
376
+ const UPLOAD_KINDS = Object.freeze({
377
+ merchant: "merchant",
378
+ merchantFeature: "merchantFeature",
379
+ product: "product",
380
+ user: "user",
381
+ applicationLogo: "applicaionLogo",
382
+ applicationImages: "applicaionImages"
383
+ });
384
+ /** How uploads are addressed (app route and Shopwave uploader). */
385
+ const SHOPWAVE_UPLOAD = Object.freeze({
386
+ route: "upload",
387
+ upstream: "uploader",
388
+ kindHeader: "contentType",
389
+ fileField: "file",
390
+ kindField: "kind"
391
+ });
392
+ function isUploadKind(value) {
393
+ return typeof value === "string" && Object.prototype.hasOwnProperty.call(UPLOAD_KINDS, value);
394
+ }
395
+ const SHOPWAVE_LOOKUPS = Object.freeze({
396
+ productTags: {
397
+ route: "products/tags",
398
+ key: "tags",
399
+ upstream: "product/tags"
400
+ },
401
+ productInstanceTags: {
402
+ route: "products/instance-tags",
403
+ key: "tags",
404
+ upstream: "product/instance/tags"
405
+ }
406
+ });
407
+
408
+ //#endregion
409
+ //#region src/core/productTags.ts
410
+ /**
411
+ * Turns a tag list from the API into clean strings: trims each tag, drops
412
+ * blanks and anything that isn't text, and removes exact duplicates (first one
413
+ * wins). The API's own order is kept. A map is accepted too (its values are
414
+ * used), in case the API ever answers keyed by id.
415
+ */
416
+ function normalizeTagList(value) {
417
+ const list = Array.isArray(value) ? value : value && typeof value === "object" ? Object.values(value) : [];
418
+ const seen = /* @__PURE__ */ new Set();
419
+ const out = [];
420
+ for (const item of list) {
421
+ if (typeof item !== "string" && typeof item !== "number") continue;
422
+ const tag = String(item).trim();
423
+ if (!tag || seen.has(tag)) continue;
424
+ seen.add(tag);
425
+ out.push(tag);
426
+ }
427
+ return out;
428
+ }
429
+ async function fetchLookup(kind, options) {
430
+ const def = SHOPWAVE_LOOKUPS[kind];
431
+ if (!def) throw new Error(`Unknown Shopwave lookup "${kind}"`);
432
+ const json = await apiGet(`/api/${def.route}`, {}, options);
433
+ assertNoApiErrors(json, 200);
434
+ return normalizeTagList(json?.[def.key]);
435
+ }
436
+ /**
437
+ * Every distinct tag used on the merchant's products (`GET /product/tags`),
438
+ * for a tag field's suggestions. Returns `[]` when there are none.
439
+ */
440
+ function fetchProductTags(options = {}) {
441
+ return fetchLookup("productTags", options);
442
+ }
443
+ /**
444
+ * Every distinct tag used on the merchant's price instances
445
+ * (`GET /product/instance/tags`): the channel names (Eat In, Takeout, …).
446
+ * Returns `[]` when there are none.
447
+ */
448
+ function fetchProductInstanceTags(options = {}) {
449
+ return fetchLookup("productInstanceTags", options);
450
+ }
451
+
352
452
  //#endregion
353
453
  //#region src/core/promotion.ts
354
454
  /** Promotion rule identifiers as stored by the Shopwave API. */
@@ -483,7 +583,10 @@ async function submitEntity({ endpoint, method = "POST", payload }, options = {}
483
583
  * (https://developer.merchantstack.com/api-reference.html) and checked against
484
584
  * the live API where noted:
485
585
  * - category/product/store: GET filtered by `<x>Ids`, POST upsert, DELETE with `<x>Id` → 205.
486
- * - promotion: no DELETE; "deleting" a promotion ends it by setting `endDate` to now.
586
+ * - promotion: no DELETE; promotions are never deleted, only expired. Expiring
587
+ * reads the promotion and saves the whole record back with `endDate` = now (and
588
+ * `startDate` = now too if it hadn't started); the live API rejects a partial
589
+ * `{ id, endDate }` save with error 899. Dates are UTC `YYYY-MM-DD HH:mm:ss`.
487
590
  * - employee: POST updates `roleId`, `joinedDate`, `exitDate` (names can't be
488
591
  * changed); no DELETE (live API: "Cannot DELETE /employee") → retired via `exitDate`.
489
592
  * - consumer: read-only (`GET` with `ids`, comma-separated); live POST → 404.
@@ -524,7 +627,9 @@ const SHOPWAVE_ENTITIES = Object.freeze({
524
627
  idHeader: "promotionId",
525
628
  writable: true,
526
629
  deleteMode: "retire",
527
- retireField: "endDate"
630
+ retireField: "endDate",
631
+ retireWithRecord: true,
632
+ retireStartField: "startDate"
528
633
  },
529
634
  employee: {
530
635
  route: "employees",
@@ -599,7 +704,8 @@ async function saveEntity(kind, item, options = {}) {
599
704
  /**
600
705
  * Deletes one entity (a soft delete: it stays readable with `deleted: true`).
601
706
  * Employees and promotions can't be deleted in Shopwave; for them the route
602
- * sets `exitDate` / `endDate` to now instead (see `SHOPWAVE_ENTITIES`).
707
+ * sets `exitDate` / `endDate` to now instead (see `SHOPWAVE_ENTITIES`), and a
708
+ * promotion is saved back whole (see {@link deletePromotion}).
603
709
  *
604
710
  * Shopwave answers 205 with an empty body — also for ids that don't exist — so
605
711
  * a resolved promise means "the API accepted the request", not "a record was
@@ -640,7 +746,12 @@ const saveStore = (store, options) => saveEntity("store", store, options);
640
746
  const deleteStore = (id, options) => deleteEntityById("store", id, options);
641
747
  const fetchStore = (id, params, options) => fetchEntityById("store", id, params, options);
642
748
  const savePromotion = (promotion, options) => saveEntity("promotion", promotion, options);
643
- /** Ends a promotion: Shopwave has no promotion DELETE, so this sets `endDate` to now. */
749
+ /**
750
+ * Expires a promotion (Shopwave never deletes promotions). The route reads the
751
+ * promotion and saves it back with `endDate` set to now; a promotion that hasn't
752
+ * started yet gets `startDate` = `endDate` = now. Rejects if the promotion doesn't
753
+ * exist or the save fails.
754
+ */
644
755
  const deletePromotion = (id, options) => deleteEntityById("promotion", id, options);
645
756
  const fetchPromotion = (id, params, options) => fetchEntityById("promotion", id, params, options);
646
757
  /**
@@ -799,50 +910,6 @@ function buildConsumerMap(consumers) {
799
910
  return map;
800
911
  }
801
912
 
802
- //#endregion
803
- //#region src/core/resources.ts
804
- /**
805
- * - merchant: `GET /merchant`; `POST /merchant` with `{ merchant: { id, … } }`
806
- * updates it (without an `id` the API would create a second merchant, so the
807
- * route requires one).
808
- * - user: `GET /user` (read-only here; the API's `POST /user` invites users and
809
- * needs the admin scope).
810
- */
811
- const SHOPWAVE_RESOURCES = Object.freeze({
812
- merchant: {
813
- route: "merchant",
814
- key: "merchant",
815
- upstream: "merchant",
816
- writable: true
817
- },
818
- user: {
819
- route: "user",
820
- key: "user",
821
- upstream: "user",
822
- writable: false
823
- }
824
- });
825
- /** `UploadKind` → the API's `contentType` value. */
826
- const UPLOAD_KINDS = Object.freeze({
827
- merchant: "merchant",
828
- merchantFeature: "merchantFeature",
829
- product: "product",
830
- user: "user",
831
- applicationLogo: "applicaionLogo",
832
- applicationImages: "applicaionImages"
833
- });
834
- /** How uploads are addressed (app route and Shopwave uploader). */
835
- const SHOPWAVE_UPLOAD = Object.freeze({
836
- route: "upload",
837
- upstream: "uploader",
838
- kindHeader: "contentType",
839
- fileField: "file",
840
- kindField: "kind"
841
- });
842
- function isUploadKind(value) {
843
- return typeof value === "string" && Object.prototype.hasOwnProperty.call(UPLOAD_KINDS, value);
844
- }
845
-
846
913
  //#endregion
847
914
  //#region src/core/imageFit.ts
848
915
  /**
@@ -1280,5 +1347,5 @@ async function fetchUser(options = {}) {
1280
1347
  }
1281
1348
 
1282
1349
  //#endregion
1283
- 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_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, 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, parseBasketReportData, readImageSize, rolesById, saveCategory, saveEmployee, saveEntities, saveEntity, saveProduct, savePromotion, saveStore, setMerchantImage, submitEntity, toProductInstancesForSave, toShopwaveApiError, toStoredImageIds, updateMerchant, uploadImage, uploadMerchantImage, validateMerchant, withToken };
1350
+ 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, validateMerchant, withToken };
1284
1351
  //# sourceMappingURL=index.js.map