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.
- package/README.md +5 -4
- package/dist/core/index.cjs +116 -11
- package/dist/core/index.cjs.map +1 -1
- package/dist/core/index.d.cts +95 -7
- package/dist/core/index.d.ts +95 -7
- package/dist/core/index.js +114 -12
- package/dist/core/index.js.map +1 -1
- package/dist/hooks/index.cjs.map +1 -1
- package/dist/hooks/index.js.map +1 -1
- package/dist/index.cjs +116 -11
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +95 -7
- package/dist/index.d.ts +95 -7
- package/dist/index.js +114 -12
- package/dist/index.js.map +1 -1
- package/dist/next/index.cjs +59 -2
- package/dist/next/index.cjs.map +1 -1
- package/dist/next/index.d.cts +18 -3
- package/dist/next/index.d.ts +18 -3
- package/dist/next/index.js +59 -2
- package/dist/next/index.js.map +1 -1
- package/dist/server/index.cjs +59 -2
- package/dist/server/index.cjs.map +1 -1
- package/dist/server/index.d.cts +18 -3
- package/dist/server/index.d.ts +18 -3
- package/dist/server/index.js +59 -2
- package/dist/server/index.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -149,7 +149,7 @@ await deleteCategory(created.id);
|
|
|
149
149
|
| product | `saveProduct`, `deleteProduct`, `fetchProduct` | `/api/products[/:id]` |
|
|
150
150
|
| category | `saveCategory`, `deleteCategory`, `fetchCategory` | `/api/categories[/:id]` |
|
|
151
151
|
| store | `saveStore`, `deleteStore`, `fetchStore` | `/api/stores[/:id]` |
|
|
152
|
-
| promotion | `savePromotion`, `deletePromotion` (
|
|
152
|
+
| promotion | `savePromotion`, `deletePromotion` (expires it), `fetchPromotion` | `/api/promotions[/:id]` |
|
|
153
153
|
| employee | `saveEmployee`, `deleteEmployee` (retires), `fetchEmployee` | `/api/employees[/:id]` |
|
|
154
154
|
| consumer | `fetchConsumer` only — read-only in the API | `/api/consumer[/:id]` (GET) |
|
|
155
155
|
|
|
@@ -158,9 +158,10 @@ How they behave (matches what the Shopwave API does):
|
|
|
158
158
|
- **Save** sends `{ <collection>: { "0": entity } }`. Shopwave upserts (an `id` means update), answers **201** and echoes the entity under the same ref with its `id` and server fields. The function checks `api.message.errors` and that the ref came back, and returns the saved entity (fields the echo leaves out are kept from your input). `saveEntities(kind, [a, b])` saves several at once and matches results back by ref.
|
|
159
159
|
- **Delete** is a soft delete (read it back with `deleted: true`). Shopwave answers **205 with an empty body — also for ids that don't exist**, so a resolved promise means "accepted", not "a record was deleted".
|
|
160
160
|
- **Read one** calls `GET /api/<route>/:id` and returns the record or `null`. List reads return `[]` when nothing matches (Shopwave answers with an empty body).
|
|
161
|
-
- **Promotions:**
|
|
161
|
+
- **Promotions:** promotions are never deleted, only expired. `deletePromotion` (and `DELETE /api/promotions/:id`) reads the promotion and saves the whole record back with `endDate` set to now; one that hasn't started yet gets `startDate` = `endDate` = now. (The API rejects a partial `{ id, endDate }` save with error 899.) Dates go as UTC `YYYY-MM-DD HH:mm:ss`.
|
|
162
162
|
- **Employees:** Shopwave has no employee DELETE, so `deleteEmployee` (and `DELETE /api/employees/:id`) retires the employee by setting `exitDate`. Updating an existing employee only changes `roleId`, `joinedDate` and `exitDate` — names and email are fixed — and the echo carries only those fields, so re-read with `fetchEmployee` if you need the stored record.
|
|
163
163
|
- **Consumers** are read-only (`GET /consumer` by `ids`); their write/delete routes answer 405.
|
|
164
|
+
- **Product prices (instances) can't be updated** — the API keeps them for history; a price change means a new instance with a new id. Build `instances` with `toProductInstancesForSave(formInstances, productAsLastRead)`: it keys them `"0"…"n"`, keeps an instance's `id` only when the original has that id with the same price and tax, and drops it otherwise (and drops server `timestamp`). `findRepricedInstances(formInstances, productAsLastRead)` lists the instances that will get new ids, so you can warn first. `saveProduct` without an `id` (a new product or a duplicate) always strips instance ids, so a copy can't point at the original's prices.
|
|
164
165
|
- Errors are read from `api.message.errors` and also `api.message.error` (the name used in the [API reference](https://developer.merchantstack.com/api-reference.html)).
|
|
165
166
|
|
|
166
167
|
The generic forms are `saveEntity(kind, item)`, `deleteEntityById(kind, id)` and `fetchEntityById(kind, id)`; `SHOPWAVE_ENTITIES` lists each entity's route, collection key, Shopwave path and id headers.
|
|
@@ -225,7 +226,7 @@ The shapes below are the ones adminV1 saves and adminV2 reads, so all three admi
|
|
|
225
226
|
| category | `fetchCategories`, `fetchCategory`, `saveCategory`, `deleteCategory` |
|
|
226
227
|
| consumer | `fetchConsumers`, `fetchConsumer` (read-only) |
|
|
227
228
|
| employee | `fetchEmployees`, `fetchEmployee`, `saveEmployee`, `deleteEmployee` |
|
|
228
|
-
| product | `fetchProducts`, `fetchProductsMap` (batched, keyed), `fetchProduct`, `saveProduct`, `deleteProduct` |
|
|
229
|
+
| product | `fetchProducts`, `fetchProductsMap` (batched, keyed), `fetchProduct`, `saveProduct`, `deleteProduct`; prices `toProductInstancesForSave`, `findRepricedInstances`, `isSameInstancePrice` |
|
|
229
230
|
| promotion | `fetchPromotions`, `fetchPromotion`, `savePromotion`, `deletePromotion` |
|
|
230
231
|
| store | `fetchStores` (now with `storeIds`), `fetchStore`, `saveStore`, `deleteStore` |
|
|
231
232
|
| report | `fetchReport` |
|
|
@@ -471,7 +472,7 @@ export const { POST } = shopwave.upload(); // or upload({ maxBytes,
|
|
|
471
472
|
- `resource("merchant")`: `PUT`/`POST` take `{ merchant: {…} }` or the bare object and send every field on as `postBody`. The `id` is required (400 without it), because a POST without one would create a second merchant. `resource("user")` answers 405 to writes.
|
|
472
473
|
- `upload()`: takes multipart `file` + `kind`, checks the size (default 10 MB → 413) and type (default `image/*` → 415), and `PUT`s only the file to `/uploader` with the `contentType` header. `extras` are ignored on uploads. Shopwave's reply (201 `{ fileName, path }`) is passed through.
|
|
473
474
|
|
|
474
|
-
Entities: `product`, `category`, `store`, `promotion`, `employee` (DELETE retires via `exitDate`), `promotion` (DELETE
|
|
475
|
+
Entities: `product`, `category`, `store`, `promotion`, `employee` (DELETE retires via `exitDate`), `promotion` (DELETE expires it: the whole record saved back with `endDate`, and a not-yet-started `startDate`, set to now), `consumer` (mounted at `/api/consumer`; GET only, writes answer 405). For anything else, `shopwave.forward(request, { method, path, headers, postBody })` does the same token/refresh/encoding work for a custom handler.
|
|
475
476
|
|
|
476
477
|
Every handler:
|
|
477
478
|
|
package/dist/core/index.cjs
CHANGED
|
@@ -41,8 +41,8 @@ function getApiErrorMap(body) {
|
|
|
41
41
|
}
|
|
42
42
|
function describeErrors(errors) {
|
|
43
43
|
return Object.entries(errors).map(([code, e]) => {
|
|
44
|
-
const text = [e?.title, e?.details].filter(Boolean).join(": ");
|
|
45
|
-
return text ? `${code} ${text}` : code;
|
|
44
|
+
const text$1 = [e?.title, e?.details].filter(Boolean).join(": ");
|
|
45
|
+
return text$1 ? `${code} ${text$1}` : code;
|
|
46
46
|
}).join("; ");
|
|
47
47
|
}
|
|
48
48
|
/**
|
|
@@ -104,12 +104,12 @@ function buildExtras(extras) {
|
|
|
104
104
|
}
|
|
105
105
|
/** Parses a response body as JSON; `null` for an empty body, `{ raw }` for non-JSON. */
|
|
106
106
|
async function readBody(response) {
|
|
107
|
-
const text = await response.text();
|
|
108
|
-
if (!text) return null;
|
|
107
|
+
const text$1 = await response.text();
|
|
108
|
+
if (!text$1) return null;
|
|
109
109
|
try {
|
|
110
|
-
return JSON.parse(text);
|
|
110
|
+
return JSON.parse(text$1);
|
|
111
111
|
} catch {
|
|
112
|
-
return { raw: text };
|
|
112
|
+
return { raw: text$1 };
|
|
113
113
|
}
|
|
114
114
|
}
|
|
115
115
|
/**
|
|
@@ -257,6 +257,83 @@ async function fetchProducts(params = {}, options = {}) {
|
|
|
257
257
|
assertNoApiErrors(json, 200);
|
|
258
258
|
return Object.values(json?.products ?? {});
|
|
259
259
|
}
|
|
260
|
+
/** Fields the server sets on an instance; never sent back. */
|
|
261
|
+
const SERVER_INSTANCE_FIELDS = ["timestamp"];
|
|
262
|
+
function instanceList(instances) {
|
|
263
|
+
if (!instances) return [];
|
|
264
|
+
return (Array.isArray(instances) ? [...instances] : Object.values(instances)).filter((inst) => inst != null && typeof inst === "object");
|
|
265
|
+
}
|
|
266
|
+
function hasId(id) {
|
|
267
|
+
return id != null && String(id).trim() !== "";
|
|
268
|
+
}
|
|
269
|
+
/** Same number, so `"422.0"`, `"422"` and `422` are equal; blanks equal each other. */
|
|
270
|
+
function sameAmount(a, b) {
|
|
271
|
+
const blank = (v) => v == null || String(v).trim() === "";
|
|
272
|
+
if (blank(a) || blank(b)) return blank(a) && blank(b);
|
|
273
|
+
const x = Number(a);
|
|
274
|
+
const y = Number(b);
|
|
275
|
+
return Number.isNaN(x) || Number.isNaN(y) ? String(a).trim() === String(b).trim() : x === y;
|
|
276
|
+
}
|
|
277
|
+
/** True when two instances have the same price and tax (the fields Shopwave won't update). */
|
|
278
|
+
function isSameInstancePrice(a, b) {
|
|
279
|
+
return sameAmount(a.price, b.price) && sameAmount(a.taxPercentage, b.taxPercentage);
|
|
280
|
+
}
|
|
281
|
+
const text = (v) => v == null ? "" : String(v);
|
|
282
|
+
/**
|
|
283
|
+
* Lists the instances whose price or tax differs from the `original` instance
|
|
284
|
+
* with the same id: saving them creates new instances with new ids, which can
|
|
285
|
+
* affect anything that stores instance ids (integrations, reports, modifiers).
|
|
286
|
+
* Use it to warn before saving. New rows (no id) aren't listed.
|
|
287
|
+
*/
|
|
288
|
+
function findRepricedInstances(instances, original) {
|
|
289
|
+
const byId = new Map(instanceList(original).filter((o) => hasId(o.id)).map((o) => [String(o.id), o]));
|
|
290
|
+
const result = [];
|
|
291
|
+
instanceList(instances).forEach((inst, index) => {
|
|
292
|
+
if (!hasId(inst.id)) return;
|
|
293
|
+
const before = byId.get(String(inst.id));
|
|
294
|
+
if (!before || isSameInstancePrice(inst, before)) return;
|
|
295
|
+
result.push({
|
|
296
|
+
index,
|
|
297
|
+
id: inst.id,
|
|
298
|
+
name: text(inst.name),
|
|
299
|
+
before: {
|
|
300
|
+
price: text(before.price),
|
|
301
|
+
taxPercentage: text(before.taxPercentage)
|
|
302
|
+
},
|
|
303
|
+
after: {
|
|
304
|
+
price: text(inst.price),
|
|
305
|
+
taxPercentage: text(inst.taxPercentage)
|
|
306
|
+
}
|
|
307
|
+
});
|
|
308
|
+
});
|
|
309
|
+
return result;
|
|
310
|
+
}
|
|
311
|
+
/**
|
|
312
|
+
* Builds the `instances` map for a product save:
|
|
313
|
+
* - keyed by position (`"0"`, `"1"`, …), like every other ref in a save;
|
|
314
|
+
* - an instance keeps its `id` only when `original` has an instance with that id
|
|
315
|
+
* and the same price and tax; otherwise the id is removed, so Shopwave creates
|
|
316
|
+
* a new instance instead of re-pointing or rewriting someone else's;
|
|
317
|
+
* - server fields (`timestamp`) are removed.
|
|
318
|
+
*
|
|
319
|
+
* Pass the product as last read from the API as `original`. Leave it out for a
|
|
320
|
+
* new product (including a duplicate): every id is then removed.
|
|
321
|
+
* Instances left out of the list are left out of the save (removed).
|
|
322
|
+
*/
|
|
323
|
+
function toProductInstancesForSave(instances, original) {
|
|
324
|
+
const keep = new Map(instanceList(original).filter((o) => hasId(o.id)).map((o) => [String(o.id), o]));
|
|
325
|
+
const out = {};
|
|
326
|
+
instanceList(instances).forEach((inst, index) => {
|
|
327
|
+
const { id,...rest } = inst;
|
|
328
|
+
for (const field of SERVER_INSTANCE_FIELDS) delete rest[field];
|
|
329
|
+
const before = hasId(id) ? keep.get(String(id)) : void 0;
|
|
330
|
+
out[String(index)] = before && isSameInstancePrice(inst, before) ? {
|
|
331
|
+
id,
|
|
332
|
+
...rest
|
|
333
|
+
} : rest;
|
|
334
|
+
});
|
|
335
|
+
return out;
|
|
336
|
+
}
|
|
260
337
|
/**
|
|
261
338
|
* Fetches products by id and returns them keyed by id, transparently batching
|
|
262
339
|
* the request (default 200 ids per call) to stay within API limits. Extracted
|
|
@@ -407,7 +484,10 @@ async function submitEntity({ endpoint, method = "POST", payload }, options = {}
|
|
|
407
484
|
* (https://developer.merchantstack.com/api-reference.html) and checked against
|
|
408
485
|
* the live API where noted:
|
|
409
486
|
* - category/product/store: GET filtered by `<x>Ids`, POST upsert, DELETE with `<x>Id` → 205.
|
|
410
|
-
* - promotion: no DELETE;
|
|
487
|
+
* - promotion: no DELETE; promotions are never deleted, only expired. Expiring
|
|
488
|
+
* reads the promotion and saves the whole record back with `endDate` = now (and
|
|
489
|
+
* `startDate` = now too if it hadn't started); the live API rejects a partial
|
|
490
|
+
* `{ id, endDate }` save with error 899. Dates are UTC `YYYY-MM-DD HH:mm:ss`.
|
|
411
491
|
* - employee: POST updates `roleId`, `joinedDate`, `exitDate` (names can't be
|
|
412
492
|
* changed); no DELETE (live API: "Cannot DELETE /employee") → retired via `exitDate`.
|
|
413
493
|
* - consumer: read-only (`GET` with `ids`, comma-separated); live POST → 404.
|
|
@@ -448,7 +528,9 @@ const SHOPWAVE_ENTITIES = Object.freeze({
|
|
|
448
528
|
idHeader: "promotionId",
|
|
449
529
|
writable: true,
|
|
450
530
|
deleteMode: "retire",
|
|
451
|
-
retireField: "endDate"
|
|
531
|
+
retireField: "endDate",
|
|
532
|
+
retireWithRecord: true,
|
|
533
|
+
retireStartField: "startDate"
|
|
452
534
|
},
|
|
453
535
|
employee: {
|
|
454
536
|
route: "employees",
|
|
@@ -523,7 +605,8 @@ async function saveEntity(kind, item, options = {}) {
|
|
|
523
605
|
/**
|
|
524
606
|
* Deletes one entity (a soft delete: it stays readable with `deleted: true`).
|
|
525
607
|
* Employees and promotions can't be deleted in Shopwave; for them the route
|
|
526
|
-
* sets `exitDate` / `endDate` to now instead (see `SHOPWAVE_ENTITIES`)
|
|
608
|
+
* sets `exitDate` / `endDate` to now instead (see `SHOPWAVE_ENTITIES`), and a
|
|
609
|
+
* promotion is saved back whole (see {@link deletePromotion}).
|
|
527
610
|
*
|
|
528
611
|
* Shopwave answers 205 with an empty body — also for ids that don't exist — so
|
|
529
612
|
* a resolved promise means "the API accepted the request", not "a record was
|
|
@@ -540,7 +623,21 @@ async function fetchEntityById(kind, id, params = {}, options = {}) {
|
|
|
540
623
|
const map = body?.[def.collection] ?? {};
|
|
541
624
|
return map[String(id)] ?? Object.values(map).find((e) => String(e?.id) === String(id)) ?? null;
|
|
542
625
|
}
|
|
543
|
-
|
|
626
|
+
/**
|
|
627
|
+
* Creates (no `id`) or updates (with `id`) a product.
|
|
628
|
+
*
|
|
629
|
+
* Shopwave instances (prices) can't be updated, only replaced. Build
|
|
630
|
+
* `instances` with {@link toProductInstancesForSave} (passing the product as last
|
|
631
|
+
* read) so an edited price is sent as a new instance. As a safeguard, a new
|
|
632
|
+
* product (no `id`, e.g. a duplicate) never sends instance ids: an id copied from
|
|
633
|
+
* another product would otherwise point the save at that product's instances.
|
|
634
|
+
*/
|
|
635
|
+
const saveProduct = (product, options) => {
|
|
636
|
+
return saveEntity("product", (product.id == null || String(product.id).trim() === "") && product.instances != null ? {
|
|
637
|
+
...product,
|
|
638
|
+
instances: toProductInstancesForSave(product.instances)
|
|
639
|
+
} : product, options);
|
|
640
|
+
};
|
|
544
641
|
const deleteProduct = (id, options) => deleteEntityById("product", id, options);
|
|
545
642
|
const fetchProduct = (id, params, options) => fetchEntityById("product", id, params, options);
|
|
546
643
|
const saveCategory = (category, options) => saveEntity("category", category, options);
|
|
@@ -550,7 +647,12 @@ const saveStore = (store, options) => saveEntity("store", store, options);
|
|
|
550
647
|
const deleteStore = (id, options) => deleteEntityById("store", id, options);
|
|
551
648
|
const fetchStore = (id, params, options) => fetchEntityById("store", id, params, options);
|
|
552
649
|
const savePromotion = (promotion, options) => saveEntity("promotion", promotion, options);
|
|
553
|
-
/**
|
|
650
|
+
/**
|
|
651
|
+
* Expires a promotion (Shopwave never deletes promotions). The route reads the
|
|
652
|
+
* promotion and saves it back with `endDate` set to now; a promotion that hasn't
|
|
653
|
+
* started yet gets `startDate` = `endDate` = now. Rejects if the promotion doesn't
|
|
654
|
+
* exist or the save fails.
|
|
655
|
+
*/
|
|
554
656
|
const deletePromotion = (id, options) => deleteEntityById("promotion", id, options);
|
|
555
657
|
const fetchPromotion = (id, params, options) => fetchEntityById("promotion", id, params, options);
|
|
556
658
|
/**
|
|
@@ -1245,6 +1347,7 @@ exports.fetchSession = fetchSession;
|
|
|
1245
1347
|
exports.fetchStore = fetchStore;
|
|
1246
1348
|
exports.fetchStores = fetchStores;
|
|
1247
1349
|
exports.fetchUser = fetchUser;
|
|
1350
|
+
exports.findRepricedInstances = findRepricedInstances;
|
|
1248
1351
|
exports.fitImageToSize = fitImageToSize;
|
|
1249
1352
|
exports.formatDateForQuery = formatDateForQuery;
|
|
1250
1353
|
exports.getApiErrorMap = getApiErrorMap;
|
|
@@ -1253,6 +1356,7 @@ exports.getImageUrl = getImageUrl;
|
|
|
1253
1356
|
exports.getMerchantImage = getMerchantImage;
|
|
1254
1357
|
exports.getMerchantImageUrl = getMerchantImageUrl;
|
|
1255
1358
|
exports.imageFileName = imageFileName;
|
|
1359
|
+
exports.isSameInstancePrice = isSameInstancePrice;
|
|
1256
1360
|
exports.isUploadKind = isUploadKind;
|
|
1257
1361
|
exports.loginPath = loginPath;
|
|
1258
1362
|
exports.logout = logout;
|
|
@@ -1272,6 +1376,7 @@ exports.savePromotion = savePromotion;
|
|
|
1272
1376
|
exports.saveStore = saveStore;
|
|
1273
1377
|
exports.setMerchantImage = setMerchantImage;
|
|
1274
1378
|
exports.submitEntity = submitEntity;
|
|
1379
|
+
exports.toProductInstancesForSave = toProductInstancesForSave;
|
|
1275
1380
|
exports.toShopwaveApiError = toShopwaveApiError;
|
|
1276
1381
|
exports.toStoredImageIds = toStoredImageIds;
|
|
1277
1382
|
exports.updateMerchant = updateMerchant;
|