react-shopwave-connect 0.4.0 → 0.4.2

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.
@@ -272,6 +272,64 @@ interface FetchProductsParams {
272
272
  * Fetches products. Returns the flattened `Product[]`.
273
273
  */
274
274
  declare function fetchProducts(params?: FetchProductsParams, options?: RequestOptions): Promise<Product[]>;
275
+ /** An instance as a form or caller holds it: `id` is absent for new rows. */
276
+ type ProductInstanceInput = {
277
+ id?: number | string | null;
278
+ price?: string | number | null;
279
+ taxPercentage?: string | number | null;
280
+ name?: string | null;
281
+ };
282
+ /** Instances as the API returns them (keyed by id) or as a plain list. */
283
+ type ProductInstances<T = ProductInstanceInput> = Readonly<Record<string, T>> | Readonly<Record<number, T>> | ReadonlyArray<T>;
284
+ /**
285
+ * What `saveProduct` accepts: any product fields, with `instances` either as the
286
+ * API returns them or as built by {@link toProductInstancesForSave}. Include `id`
287
+ * to update; leave it out to create.
288
+ */
289
+ type ProductSaveInput = Partial<Omit<Product, "instances">> & {
290
+ instances?: ProductInstances;
291
+ [field: string]: unknown;
292
+ };
293
+ /** True when two instances have the same price and tax (the fields Shopwave won't update). */
294
+ declare function isSameInstancePrice(a: ProductInstanceInput, b: ProductInstanceInput): boolean;
295
+ /** An instance whose price or tax was edited, so saving it will give it a new id. */
296
+ interface RepricedInstance {
297
+ /** Position of the instance in the list that was passed in. */
298
+ index: number;
299
+ /** The id it has now, which Shopwave will replace. */
300
+ id: number | string;
301
+ name: string;
302
+ before: {
303
+ price: string;
304
+ taxPercentage: string;
305
+ };
306
+ after: {
307
+ price: string;
308
+ taxPercentage: string;
309
+ };
310
+ }
311
+ /**
312
+ * Lists the instances whose price or tax differs from the `original` instance
313
+ * with the same id: saving them creates new instances with new ids, which can
314
+ * affect anything that stores instance ids (integrations, reports, modifiers).
315
+ * Use it to warn before saving. New rows (no id) aren't listed.
316
+ */
317
+ declare function findRepricedInstances(instances: ProductInstances | null | undefined, original: ProductInstances | null | undefined): RepricedInstance[];
318
+ /**
319
+ * Builds the `instances` map for a product save:
320
+ * - keyed by position (`"0"`, `"1"`, …), like every other ref in a save;
321
+ * - an instance keeps its `id` only when `original` has an instance with that id
322
+ * and the same price and tax; otherwise the id is removed, so Shopwave creates
323
+ * a new instance instead of re-pointing or rewriting someone else's;
324
+ * - server fields (`timestamp`) are removed.
325
+ *
326
+ * Pass the product as last read from the API as `original`. Leave it out for a
327
+ * new product (including a duplicate): every id is then removed.
328
+ * Instances left out of the list are left out of the save (removed).
329
+ */
330
+ declare function toProductInstancesForSave<T extends ProductInstanceInput>(instances: ProductInstances<T> | null | undefined, original?: ProductInstances | null): Record<string, Omit<T, "id"> & {
331
+ id?: number | string;
332
+ }>;
275
333
  /**
276
334
  * Fetches products by id and returns them keyed by id, transparently batching
277
335
  * the request (default 200 ids per call) to stay within API limits. Extracted
@@ -498,13 +556,25 @@ interface EntityDefinition {
498
556
  /**
499
557
  * How a delete is done upstream:
500
558
  * - `"delete"`: `DELETE {apiUrl}/<upstream>` with the `idHeader` (205, empty body)
501
- * - `"retire"`: the API has no DELETE; the record is retired by POSTing
502
- * `{ id, [retireField]: <now> }` (employees: `exitDate`, promotions: `endDate`)
559
+ * - `"retire"`: the API has no DELETE; the record is retired by setting
560
+ * `retireField` to now (employees: `exitDate`, promotions: `endDate`)
503
561
  * - `"none"`: can't be deleted
504
562
  */
505
563
  deleteMode: "delete" | "retire" | "none";
506
564
  /** Date field set to "now" when `deleteMode` is `"retire"`. */
507
565
  retireField?: string;
566
+ /**
567
+ * Retire by reading the record and POSTing all of it back with the date(s)
568
+ * changed, instead of a partial `{ id, [retireField]: <now> }`. Needed where
569
+ * the API rejects a partial save (promotions: error 899, live 14 Oct 2026).
570
+ */
571
+ retireWithRecord?: boolean;
572
+ /**
573
+ * Start-date field of a record retired with `retireWithRecord`. If the record
574
+ * hasn't started yet (start after now), the start moves to now as well, so
575
+ * start = end and the record is expired without ever running.
576
+ */
577
+ retireStartField?: string;
508
578
  }
509
579
  type EntityKind = "product" | "category" | "store" | "promotion" | "employee" | "consumer";
510
580
  /** Entities that can be created/updated through the API. */
@@ -516,7 +586,10 @@ type DeletableEntityKind = Exclude<EntityKind, "consumer">;
516
586
  * (https://developer.merchantstack.com/api-reference.html) and checked against
517
587
  * the live API where noted:
518
588
  * - category/product/store: GET filtered by `<x>Ids`, POST upsert, DELETE with `<x>Id` → 205.
519
- * - promotion: no DELETE; "deleting" a promotion ends it by setting `endDate` to now.
589
+ * - promotion: no DELETE; promotions are never deleted, only expired. Expiring
590
+ * reads the promotion and saves the whole record back with `endDate` = now (and
591
+ * `startDate` = now too if it hadn't started); the live API rejects a partial
592
+ * `{ id, endDate }` save with error 899. Dates are UTC `YYYY-MM-DD HH:mm:ss`.
520
593
  * - employee: POST updates `roleId`, `joinedDate`, `exitDate` (names can't be
521
594
  * changed); no DELETE (live API: "Cannot DELETE /employee") → retired via `exitDate`.
522
595
  * - consumer: read-only (`GET` with `ids`, comma-separated); live POST → 404.
@@ -560,7 +633,8 @@ declare function saveEntity<K extends WritableEntityKind>(kind: K, item: EntityI
560
633
  /**
561
634
  * Deletes one entity (a soft delete: it stays readable with `deleted: true`).
562
635
  * Employees and promotions can't be deleted in Shopwave; for them the route
563
- * sets `exitDate` / `endDate` to now instead (see `SHOPWAVE_ENTITIES`).
636
+ * sets `exitDate` / `endDate` to now instead (see `SHOPWAVE_ENTITIES`), and a
637
+ * promotion is saved back whole (see {@link deletePromotion}).
564
638
  *
565
639
  * Shopwave answers 205 with an empty body — also for ids that don't exist — so
566
640
  * a resolved promise means "the API accepted the request", not "a record was
@@ -573,7 +647,16 @@ interface FetchByIdParams {
573
647
  }
574
648
  /** Reads one entity by id, or `null` when there's no such (non-deleted) record. */
575
649
  declare function fetchEntityById<K extends EntityKind>(kind: K, id: EntityId, params?: FetchByIdParams, options?: RequestOptions): Promise<EntityTypes[K] | null>;
576
- declare const saveProduct: (product: EntityInput<Product>, options?: RequestOptions) => Promise<Saved<Product>>;
650
+ /**
651
+ * Creates (no `id`) or updates (with `id`) a product.
652
+ *
653
+ * Shopwave instances (prices) can't be updated, only replaced. Build
654
+ * `instances` with {@link toProductInstancesForSave} (passing the product as last
655
+ * read) so an edited price is sent as a new instance. As a safeguard, a new
656
+ * product (no `id`, e.g. a duplicate) never sends instance ids: an id copied from
657
+ * another product would otherwise point the save at that product's instances.
658
+ */
659
+ declare const saveProduct: (product: ProductSaveInput, options?: RequestOptions) => Promise<Saved<Product>>;
577
660
  declare const deleteProduct: (id: EntityId, options?: RequestOptions) => Promise<void>;
578
661
  declare const fetchProduct: (id: EntityId, params?: FetchByIdParams, options?: RequestOptions) => Promise<Product | null>;
579
662
  declare const saveCategory: (category: EntityInput<Category>, options?: RequestOptions) => Promise<Saved<Category>>;
@@ -583,7 +666,12 @@ declare const saveStore: (store: EntityInput<Store>, options?: RequestOptions) =
583
666
  declare const deleteStore: (id: EntityId, options?: RequestOptions) => Promise<void>;
584
667
  declare const fetchStore: (id: EntityId, params?: FetchByIdParams, options?: RequestOptions) => Promise<Store | null>;
585
668
  declare const savePromotion: (promotion: EntityInput<Promotion>, options?: RequestOptions) => Promise<Saved<Promotion>>;
586
- /** Ends a promotion: Shopwave has no promotion DELETE, so this sets `endDate` to now. */
669
+ /**
670
+ * Expires a promotion (Shopwave never deletes promotions). The route reads the
671
+ * promotion and saves it back with `endDate` set to now; a promotion that hasn't
672
+ * started yet gets `startDate` = `endDate` = now. Rejects if the promotion doesn't
673
+ * exist or the save fails.
674
+ */
587
675
  declare const deletePromotion: (id: EntityId, options?: RequestOptions) => Promise<void>;
588
676
  declare const fetchPromotion: (id: EntityId, params?: FetchByIdParams, options?: RequestOptions) => Promise<Promotion | null>;
589
677
  /**
@@ -1086,5 +1174,5 @@ interface UserResponse {
1086
1174
  /** Reads the logged-in user, or `null` when the API returns none. */
1087
1175
  declare function fetchUser(options?: RequestOptions): Promise<User | null>;
1088
1176
  //#endregion
1089
- 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, ProductResponse, Promotion, PromotionResponse, PromotionRuleSet, ReportData, ReportKey, ReportQuery, ReportQueryMap, ReportResponse, 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, fitImageToSize, formatDateForQuery, getApiErrorMap, getApiErrors, getImageUrl, getMerchantImage, getMerchantImageUrl, imageFileName, isUploadKind, loginPath, logout, logoutPath, merchantLinksFromForm, merchantLinksToForm, normalizeMerchantLinks, parseBasketReportData, readImageSize, rolesById, saveCategory, saveEmployee, saveEntities, saveEntity, saveProduct, savePromotion, saveStore, setMerchantImage, submitEntity, toShopwaveApiError, toStoredImageIds, updateMerchant, uploadImage, uploadMerchantImage, validateMerchant, withToken };
1177
+ 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 };
1090
1178
  //# sourceMappingURL=index.d.cts.map
@@ -272,6 +272,64 @@ interface FetchProductsParams {
272
272
  * Fetches products. Returns the flattened `Product[]`.
273
273
  */
274
274
  declare function fetchProducts(params?: FetchProductsParams, options?: RequestOptions): Promise<Product[]>;
275
+ /** An instance as a form or caller holds it: `id` is absent for new rows. */
276
+ type ProductInstanceInput = {
277
+ id?: number | string | null;
278
+ price?: string | number | null;
279
+ taxPercentage?: string | number | null;
280
+ name?: string | null;
281
+ };
282
+ /** Instances as the API returns them (keyed by id) or as a plain list. */
283
+ type ProductInstances<T = ProductInstanceInput> = Readonly<Record<string, T>> | Readonly<Record<number, T>> | ReadonlyArray<T>;
284
+ /**
285
+ * What `saveProduct` accepts: any product fields, with `instances` either as the
286
+ * API returns them or as built by {@link toProductInstancesForSave}. Include `id`
287
+ * to update; leave it out to create.
288
+ */
289
+ type ProductSaveInput = Partial<Omit<Product, "instances">> & {
290
+ instances?: ProductInstances;
291
+ [field: string]: unknown;
292
+ };
293
+ /** True when two instances have the same price and tax (the fields Shopwave won't update). */
294
+ declare function isSameInstancePrice(a: ProductInstanceInput, b: ProductInstanceInput): boolean;
295
+ /** An instance whose price or tax was edited, so saving it will give it a new id. */
296
+ interface RepricedInstance {
297
+ /** Position of the instance in the list that was passed in. */
298
+ index: number;
299
+ /** The id it has now, which Shopwave will replace. */
300
+ id: number | string;
301
+ name: string;
302
+ before: {
303
+ price: string;
304
+ taxPercentage: string;
305
+ };
306
+ after: {
307
+ price: string;
308
+ taxPercentage: string;
309
+ };
310
+ }
311
+ /**
312
+ * Lists the instances whose price or tax differs from the `original` instance
313
+ * with the same id: saving them creates new instances with new ids, which can
314
+ * affect anything that stores instance ids (integrations, reports, modifiers).
315
+ * Use it to warn before saving. New rows (no id) aren't listed.
316
+ */
317
+ declare function findRepricedInstances(instances: ProductInstances | null | undefined, original: ProductInstances | null | undefined): RepricedInstance[];
318
+ /**
319
+ * Builds the `instances` map for a product save:
320
+ * - keyed by position (`"0"`, `"1"`, …), like every other ref in a save;
321
+ * - an instance keeps its `id` only when `original` has an instance with that id
322
+ * and the same price and tax; otherwise the id is removed, so Shopwave creates
323
+ * a new instance instead of re-pointing or rewriting someone else's;
324
+ * - server fields (`timestamp`) are removed.
325
+ *
326
+ * Pass the product as last read from the API as `original`. Leave it out for a
327
+ * new product (including a duplicate): every id is then removed.
328
+ * Instances left out of the list are left out of the save (removed).
329
+ */
330
+ declare function toProductInstancesForSave<T extends ProductInstanceInput>(instances: ProductInstances<T> | null | undefined, original?: ProductInstances | null): Record<string, Omit<T, "id"> & {
331
+ id?: number | string;
332
+ }>;
275
333
  /**
276
334
  * Fetches products by id and returns them keyed by id, transparently batching
277
335
  * the request (default 200 ids per call) to stay within API limits. Extracted
@@ -498,13 +556,25 @@ interface EntityDefinition {
498
556
  /**
499
557
  * How a delete is done upstream:
500
558
  * - `"delete"`: `DELETE {apiUrl}/<upstream>` with the `idHeader` (205, empty body)
501
- * - `"retire"`: the API has no DELETE; the record is retired by POSTing
502
- * `{ id, [retireField]: <now> }` (employees: `exitDate`, promotions: `endDate`)
559
+ * - `"retire"`: the API has no DELETE; the record is retired by setting
560
+ * `retireField` to now (employees: `exitDate`, promotions: `endDate`)
503
561
  * - `"none"`: can't be deleted
504
562
  */
505
563
  deleteMode: "delete" | "retire" | "none";
506
564
  /** Date field set to "now" when `deleteMode` is `"retire"`. */
507
565
  retireField?: string;
566
+ /**
567
+ * Retire by reading the record and POSTing all of it back with the date(s)
568
+ * changed, instead of a partial `{ id, [retireField]: <now> }`. Needed where
569
+ * the API rejects a partial save (promotions: error 899, live 14 Oct 2026).
570
+ */
571
+ retireWithRecord?: boolean;
572
+ /**
573
+ * Start-date field of a record retired with `retireWithRecord`. If the record
574
+ * hasn't started yet (start after now), the start moves to now as well, so
575
+ * start = end and the record is expired without ever running.
576
+ */
577
+ retireStartField?: string;
508
578
  }
509
579
  type EntityKind = "product" | "category" | "store" | "promotion" | "employee" | "consumer";
510
580
  /** Entities that can be created/updated through the API. */
@@ -516,7 +586,10 @@ type DeletableEntityKind = Exclude<EntityKind, "consumer">;
516
586
  * (https://developer.merchantstack.com/api-reference.html) and checked against
517
587
  * the live API where noted:
518
588
  * - category/product/store: GET filtered by `<x>Ids`, POST upsert, DELETE with `<x>Id` → 205.
519
- * - promotion: no DELETE; "deleting" a promotion ends it by setting `endDate` to now.
589
+ * - promotion: no DELETE; promotions are never deleted, only expired. Expiring
590
+ * reads the promotion and saves the whole record back with `endDate` = now (and
591
+ * `startDate` = now too if it hadn't started); the live API rejects a partial
592
+ * `{ id, endDate }` save with error 899. Dates are UTC `YYYY-MM-DD HH:mm:ss`.
520
593
  * - employee: POST updates `roleId`, `joinedDate`, `exitDate` (names can't be
521
594
  * changed); no DELETE (live API: "Cannot DELETE /employee") → retired via `exitDate`.
522
595
  * - consumer: read-only (`GET` with `ids`, comma-separated); live POST → 404.
@@ -560,7 +633,8 @@ declare function saveEntity<K extends WritableEntityKind>(kind: K, item: EntityI
560
633
  /**
561
634
  * Deletes one entity (a soft delete: it stays readable with `deleted: true`).
562
635
  * Employees and promotions can't be deleted in Shopwave; for them the route
563
- * sets `exitDate` / `endDate` to now instead (see `SHOPWAVE_ENTITIES`).
636
+ * sets `exitDate` / `endDate` to now instead (see `SHOPWAVE_ENTITIES`), and a
637
+ * promotion is saved back whole (see {@link deletePromotion}).
564
638
  *
565
639
  * Shopwave answers 205 with an empty body — also for ids that don't exist — so
566
640
  * a resolved promise means "the API accepted the request", not "a record was
@@ -573,7 +647,16 @@ interface FetchByIdParams {
573
647
  }
574
648
  /** Reads one entity by id, or `null` when there's no such (non-deleted) record. */
575
649
  declare function fetchEntityById<K extends EntityKind>(kind: K, id: EntityId, params?: FetchByIdParams, options?: RequestOptions): Promise<EntityTypes[K] | null>;
576
- declare const saveProduct: (product: EntityInput<Product>, options?: RequestOptions) => Promise<Saved<Product>>;
650
+ /**
651
+ * Creates (no `id`) or updates (with `id`) a product.
652
+ *
653
+ * Shopwave instances (prices) can't be updated, only replaced. Build
654
+ * `instances` with {@link toProductInstancesForSave} (passing the product as last
655
+ * read) so an edited price is sent as a new instance. As a safeguard, a new
656
+ * product (no `id`, e.g. a duplicate) never sends instance ids: an id copied from
657
+ * another product would otherwise point the save at that product's instances.
658
+ */
659
+ declare const saveProduct: (product: ProductSaveInput, options?: RequestOptions) => Promise<Saved<Product>>;
577
660
  declare const deleteProduct: (id: EntityId, options?: RequestOptions) => Promise<void>;
578
661
  declare const fetchProduct: (id: EntityId, params?: FetchByIdParams, options?: RequestOptions) => Promise<Product | null>;
579
662
  declare const saveCategory: (category: EntityInput<Category>, options?: RequestOptions) => Promise<Saved<Category>>;
@@ -583,7 +666,12 @@ declare const saveStore: (store: EntityInput<Store>, options?: RequestOptions) =
583
666
  declare const deleteStore: (id: EntityId, options?: RequestOptions) => Promise<void>;
584
667
  declare const fetchStore: (id: EntityId, params?: FetchByIdParams, options?: RequestOptions) => Promise<Store | null>;
585
668
  declare const savePromotion: (promotion: EntityInput<Promotion>, options?: RequestOptions) => Promise<Saved<Promotion>>;
586
- /** Ends a promotion: Shopwave has no promotion DELETE, so this sets `endDate` to now. */
669
+ /**
670
+ * Expires a promotion (Shopwave never deletes promotions). The route reads the
671
+ * promotion and saves it back with `endDate` set to now; a promotion that hasn't
672
+ * started yet gets `startDate` = `endDate` = now. Rejects if the promotion doesn't
673
+ * exist or the save fails.
674
+ */
587
675
  declare const deletePromotion: (id: EntityId, options?: RequestOptions) => Promise<void>;
588
676
  declare const fetchPromotion: (id: EntityId, params?: FetchByIdParams, options?: RequestOptions) => Promise<Promotion | null>;
589
677
  /**
@@ -1086,5 +1174,5 @@ interface UserResponse {
1086
1174
  /** Reads the logged-in user, or `null` when the API returns none. */
1087
1175
  declare function fetchUser(options?: RequestOptions): Promise<User | null>;
1088
1176
  //#endregion
1089
- 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, ProductResponse, Promotion, PromotionResponse, PromotionRuleSet, ReportData, ReportKey, ReportQuery, ReportQueryMap, ReportResponse, 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, fitImageToSize, formatDateForQuery, getApiErrorMap, getApiErrors, getImageUrl, getMerchantImage, getMerchantImageUrl, imageFileName, isUploadKind, loginPath, logout, logoutPath, merchantLinksFromForm, merchantLinksToForm, normalizeMerchantLinks, parseBasketReportData, readImageSize, rolesById, saveCategory, saveEmployee, saveEntities, saveEntity, saveProduct, savePromotion, saveStore, setMerchantImage, submitEntity, toShopwaveApiError, toStoredImageIds, updateMerchant, uploadImage, uploadMerchantImage, validateMerchant, withToken };
1177
+ 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 };
1090
1178
  //# sourceMappingURL=index.d.ts.map
@@ -40,8 +40,8 @@ function getApiErrorMap(body) {
40
40
  }
41
41
  function describeErrors(errors) {
42
42
  return Object.entries(errors).map(([code, e]) => {
43
- const text = [e?.title, e?.details].filter(Boolean).join(": ");
44
- return text ? `${code} ${text}` : code;
43
+ const text$1 = [e?.title, e?.details].filter(Boolean).join(": ");
44
+ return text$1 ? `${code} ${text$1}` : code;
45
45
  }).join("; ");
46
46
  }
47
47
  /**
@@ -103,12 +103,12 @@ function buildExtras(extras) {
103
103
  }
104
104
  /** Parses a response body as JSON; `null` for an empty body, `{ raw }` for non-JSON. */
105
105
  async function readBody(response) {
106
- const text = await response.text();
107
- if (!text) return null;
106
+ const text$1 = await response.text();
107
+ if (!text$1) return null;
108
108
  try {
109
- return JSON.parse(text);
109
+ return JSON.parse(text$1);
110
110
  } catch {
111
- return { raw: text };
111
+ return { raw: text$1 };
112
112
  }
113
113
  }
114
114
  /**
@@ -256,6 +256,83 @@ async function fetchProducts(params = {}, options = {}) {
256
256
  assertNoApiErrors(json, 200);
257
257
  return Object.values(json?.products ?? {});
258
258
  }
259
+ /** Fields the server sets on an instance; never sent back. */
260
+ const SERVER_INSTANCE_FIELDS = ["timestamp"];
261
+ function instanceList(instances) {
262
+ if (!instances) return [];
263
+ return (Array.isArray(instances) ? [...instances] : Object.values(instances)).filter((inst) => inst != null && typeof inst === "object");
264
+ }
265
+ function hasId(id) {
266
+ return id != null && String(id).trim() !== "";
267
+ }
268
+ /** Same number, so `"422.0"`, `"422"` and `422` are equal; blanks equal each other. */
269
+ function sameAmount(a, b) {
270
+ const blank = (v) => v == null || String(v).trim() === "";
271
+ if (blank(a) || blank(b)) return blank(a) && blank(b);
272
+ const x = Number(a);
273
+ const y = Number(b);
274
+ return Number.isNaN(x) || Number.isNaN(y) ? String(a).trim() === String(b).trim() : x === y;
275
+ }
276
+ /** True when two instances have the same price and tax (the fields Shopwave won't update). */
277
+ function isSameInstancePrice(a, b) {
278
+ return sameAmount(a.price, b.price) && sameAmount(a.taxPercentage, b.taxPercentage);
279
+ }
280
+ const text = (v) => v == null ? "" : String(v);
281
+ /**
282
+ * Lists the instances whose price or tax differs from the `original` instance
283
+ * with the same id: saving them creates new instances with new ids, which can
284
+ * affect anything that stores instance ids (integrations, reports, modifiers).
285
+ * Use it to warn before saving. New rows (no id) aren't listed.
286
+ */
287
+ function findRepricedInstances(instances, original) {
288
+ const byId = new Map(instanceList(original).filter((o) => hasId(o.id)).map((o) => [String(o.id), o]));
289
+ const result = [];
290
+ instanceList(instances).forEach((inst, index) => {
291
+ if (!hasId(inst.id)) return;
292
+ const before = byId.get(String(inst.id));
293
+ if (!before || isSameInstancePrice(inst, before)) return;
294
+ result.push({
295
+ index,
296
+ id: inst.id,
297
+ name: text(inst.name),
298
+ before: {
299
+ price: text(before.price),
300
+ taxPercentage: text(before.taxPercentage)
301
+ },
302
+ after: {
303
+ price: text(inst.price),
304
+ taxPercentage: text(inst.taxPercentage)
305
+ }
306
+ });
307
+ });
308
+ return result;
309
+ }
310
+ /**
311
+ * Builds the `instances` map for a product save:
312
+ * - keyed by position (`"0"`, `"1"`, …), like every other ref in a save;
313
+ * - an instance keeps its `id` only when `original` has an instance with that id
314
+ * and the same price and tax; otherwise the id is removed, so Shopwave creates
315
+ * a new instance instead of re-pointing or rewriting someone else's;
316
+ * - server fields (`timestamp`) are removed.
317
+ *
318
+ * Pass the product as last read from the API as `original`. Leave it out for a
319
+ * new product (including a duplicate): every id is then removed.
320
+ * Instances left out of the list are left out of the save (removed).
321
+ */
322
+ function toProductInstancesForSave(instances, original) {
323
+ const keep = new Map(instanceList(original).filter((o) => hasId(o.id)).map((o) => [String(o.id), o]));
324
+ const out = {};
325
+ instanceList(instances).forEach((inst, index) => {
326
+ const { id,...rest } = inst;
327
+ for (const field of SERVER_INSTANCE_FIELDS) delete rest[field];
328
+ const before = hasId(id) ? keep.get(String(id)) : void 0;
329
+ out[String(index)] = before && isSameInstancePrice(inst, before) ? {
330
+ id,
331
+ ...rest
332
+ } : rest;
333
+ });
334
+ return out;
335
+ }
259
336
  /**
260
337
  * Fetches products by id and returns them keyed by id, transparently batching
261
338
  * the request (default 200 ids per call) to stay within API limits. Extracted
@@ -406,7 +483,10 @@ async function submitEntity({ endpoint, method = "POST", payload }, options = {}
406
483
  * (https://developer.merchantstack.com/api-reference.html) and checked against
407
484
  * the live API where noted:
408
485
  * - category/product/store: GET filtered by `<x>Ids`, POST upsert, DELETE with `<x>Id` → 205.
409
- * - promotion: no DELETE; "deleting" a promotion ends it by setting `endDate` to now.
486
+ * - promotion: no DELETE; promotions are never deleted, only expired. Expiring
487
+ * reads the promotion and saves the whole record back with `endDate` = now (and
488
+ * `startDate` = now too if it hadn't started); the live API rejects a partial
489
+ * `{ id, endDate }` save with error 899. Dates are UTC `YYYY-MM-DD HH:mm:ss`.
410
490
  * - employee: POST updates `roleId`, `joinedDate`, `exitDate` (names can't be
411
491
  * changed); no DELETE (live API: "Cannot DELETE /employee") → retired via `exitDate`.
412
492
  * - consumer: read-only (`GET` with `ids`, comma-separated); live POST → 404.
@@ -447,7 +527,9 @@ const SHOPWAVE_ENTITIES = Object.freeze({
447
527
  idHeader: "promotionId",
448
528
  writable: true,
449
529
  deleteMode: "retire",
450
- retireField: "endDate"
530
+ retireField: "endDate",
531
+ retireWithRecord: true,
532
+ retireStartField: "startDate"
451
533
  },
452
534
  employee: {
453
535
  route: "employees",
@@ -522,7 +604,8 @@ async function saveEntity(kind, item, options = {}) {
522
604
  /**
523
605
  * Deletes one entity (a soft delete: it stays readable with `deleted: true`).
524
606
  * Employees and promotions can't be deleted in Shopwave; for them the route
525
- * sets `exitDate` / `endDate` to now instead (see `SHOPWAVE_ENTITIES`).
607
+ * sets `exitDate` / `endDate` to now instead (see `SHOPWAVE_ENTITIES`), and a
608
+ * promotion is saved back whole (see {@link deletePromotion}).
526
609
  *
527
610
  * Shopwave answers 205 with an empty body — also for ids that don't exist — so
528
611
  * a resolved promise means "the API accepted the request", not "a record was
@@ -539,7 +622,21 @@ async function fetchEntityById(kind, id, params = {}, options = {}) {
539
622
  const map = body?.[def.collection] ?? {};
540
623
  return map[String(id)] ?? Object.values(map).find((e) => String(e?.id) === String(id)) ?? null;
541
624
  }
542
- const saveProduct = (product, options) => saveEntity("product", product, options);
625
+ /**
626
+ * Creates (no `id`) or updates (with `id`) a product.
627
+ *
628
+ * Shopwave instances (prices) can't be updated, only replaced. Build
629
+ * `instances` with {@link toProductInstancesForSave} (passing the product as last
630
+ * read) so an edited price is sent as a new instance. As a safeguard, a new
631
+ * product (no `id`, e.g. a duplicate) never sends instance ids: an id copied from
632
+ * another product would otherwise point the save at that product's instances.
633
+ */
634
+ const saveProduct = (product, options) => {
635
+ return saveEntity("product", (product.id == null || String(product.id).trim() === "") && product.instances != null ? {
636
+ ...product,
637
+ instances: toProductInstancesForSave(product.instances)
638
+ } : product, options);
639
+ };
543
640
  const deleteProduct = (id, options) => deleteEntityById("product", id, options);
544
641
  const fetchProduct = (id, params, options) => fetchEntityById("product", id, params, options);
545
642
  const saveCategory = (category, options) => saveEntity("category", category, options);
@@ -549,7 +646,12 @@ const saveStore = (store, options) => saveEntity("store", store, options);
549
646
  const deleteStore = (id, options) => deleteEntityById("store", id, options);
550
647
  const fetchStore = (id, params, options) => fetchEntityById("store", id, params, options);
551
648
  const savePromotion = (promotion, options) => saveEntity("promotion", promotion, options);
552
- /** Ends a promotion: Shopwave has no promotion DELETE, so this sets `endDate` to now. */
649
+ /**
650
+ * Expires a promotion (Shopwave never deletes promotions). The route reads the
651
+ * promotion and saves it back with `endDate` set to now; a promotion that hasn't
652
+ * started yet gets `startDate` = `endDate` = now. Rejects if the promotion doesn't
653
+ * exist or the save fails.
654
+ */
553
655
  const deletePromotion = (id, options) => deleteEntityById("promotion", id, options);
554
656
  const fetchPromotion = (id, params, options) => fetchEntityById("promotion", id, params, options);
555
657
  /**
@@ -1189,5 +1291,5 @@ async function fetchUser(options = {}) {
1189
1291
  }
1190
1292
 
1191
1293
  //#endregion
1192
- 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, fitImageToSize, formatDateForQuery, getApiErrorMap, getApiErrors, getImageUrl, getMerchantImage, getMerchantImageUrl, imageFileName, isUploadKind, loginPath, logout, logoutPath, merchantLinksFromForm, merchantLinksToForm, normalizeMerchantLinks, parseBasketReportData, readImageSize, rolesById, saveCategory, saveEmployee, saveEntities, saveEntity, saveProduct, savePromotion, saveStore, setMerchantImage, submitEntity, toShopwaveApiError, toStoredImageIds, updateMerchant, uploadImage, uploadMerchantImage, validateMerchant, withToken };
1294
+ 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 };
1193
1295
  //# sourceMappingURL=index.js.map