react-shopwave-connect 0.1.1

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 ShopwaveLTD
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,258 @@
1
+ # react-shopwave-connect
2
+
3
+ Shopwave API client split into two clearly separated layers:
4
+
5
+ - **`core`** — framework-agnostic async API functions + all TypeScript types. No React anywhere. Works in any TS/JS project (Node script, Vue, Angular, CLI, …).
6
+ - **`hooks`** — thin React wrappers around `core`, with a consistent `{ data, loading, error, refetch }` shape. React is a **peer dependency** and is never bundled.
7
+
8
+ ```
9
+ src/
10
+ core/ index.ts + one file per domain (category, consumer, employee,
11
+ product, store, report, session, entity, basket) — plus the shared
12
+ request helper (request.ts) and API envelope types (types.ts)
13
+ hooks/ index.ts + one wrapper per domain, all importing logic from core
14
+ ```
15
+
16
+ ## Install
17
+
18
+ ```bash
19
+ npm install react-shopwave-connect
20
+ # React is only needed if you use the hooks layer:
21
+ npm install react react-dom
22
+ ```
23
+
24
+ ## Import paths
25
+
26
+ | Import | What you get |
27
+ | ------------------------------------ | --------------------------------------------- |
28
+ | `react-shopwave-connect/core` | Framework-agnostic functions + types only |
29
+ | `react-shopwave-connect/hooks` | React hooks only |
30
+ | `react-shopwave-connect` | Both (re-exports `core` + `hooks`) |
31
+
32
+ Prefer the subpath imports when you want a hard boundary — e.g. a Node service should import from `/core` so React never enters the dependency graph.
33
+
34
+ ## Configuration (`RequestOptions`)
35
+
36
+ Every `core` function (and every hook) accepts an optional `options` argument:
37
+
38
+ ```ts
39
+ interface RequestOptions {
40
+ baseUrl?: string; // prefix for every path; omit in the browser to use
41
+ // relative URLs like "/api/products"
42
+ token?: string; // forwarded as the `token` request header
43
+ fetch?: typeof fetch; // custom fetch (Node < 18, tests, interceptors)
44
+ signal?: AbortSignal; // cancellation (hooks pass this automatically)
45
+ }
46
+ ```
47
+
48
+ In the browser / Next.js you can usually omit `options` entirely (relative URLs resolve against the current origin). Outside the browser, pass an absolute `baseUrl` and a `token`.
49
+
50
+ ---
51
+
52
+ ## 1. Using `core` standalone (no React)
53
+
54
+ Plain async functions — usable in a Node script, CLI, Vue, Angular, a server route, etc.
55
+
56
+ ```ts
57
+ import {
58
+ fetchProducts,
59
+ fetchStores,
60
+ fetchReport,
61
+ type Product,
62
+ } from "react-shopwave-connect/core";
63
+
64
+ const options = {
65
+ baseUrl: "https://api.shopwave.example",
66
+ token: process.env.SHOPWAVE_TOKEN,
67
+ };
68
+
69
+ async function main() {
70
+ const stores = await fetchStores({}, options);
71
+ console.log(`${stores.length} stores`);
72
+
73
+ const products: Product[] = await fetchProducts(
74
+ { storeId: stores[0].id },
75
+ options
76
+ );
77
+ console.log(products.map((p) => p.name));
78
+
79
+ const reports = await fetchReport(
80
+ { sales: { FROM: "Basket", WHERE: { AND: ["Basket.status = 'completed'"] } } },
81
+ options
82
+ );
83
+ console.log(reports.sales?.data.length, "rows");
84
+ }
85
+
86
+ main().catch(console.error);
87
+ ```
88
+
89
+ On Node < 18 (no global `fetch`), inject one:
90
+
91
+ ```ts
92
+ import { fetchStores } from "react-shopwave-connect/core";
93
+ import fetch from "node-fetch";
94
+
95
+ await fetchStores({}, { baseUrl: "https://api.merchantstack.com", fetch });
96
+ ```
97
+
98
+ Functions **throw** on network failure or API errors (the error message contains the serialized API errors), so wrap calls in `try/catch`.
99
+
100
+ ### Available `core` functions
101
+
102
+ | Domain | Function(s) |
103
+ | --------- | ------------------------------------------------------- |
104
+ | category | `fetchCategories` |
105
+ | consumer | `fetchConsumers` |
106
+ | employee | `fetchEmployees` |
107
+ | product | `fetchProducts`, `fetchProductsMap` (batched, keyed) |
108
+ | store | `fetchStores` |
109
+ | report | `fetchReport` |
110
+ | session | `logout` |
111
+ | entity | `deleteEntity`, `submitEntity` |
112
+ | basket | `buildBasketReportQuery`, `parseBasketReportData`, `combineBasketRows`, `computeBasketSummary`, … (pure transforms) |
113
+
114
+ All types/interfaces (`Product`, `Store`, `Category`, `Consumer`, `Employee`, `ReportQueryMap`, `Basket*`, `apiResponse`, …) are exported from `core` too.
115
+
116
+ ---
117
+
118
+ ## 2. Using `hooks` in React / Next.js
119
+
120
+ Every auto-fetching hook returns the same shape:
121
+
122
+ ```ts
123
+ { data, loading, error, refetch }
124
+ ```
125
+
126
+ ```tsx
127
+ "use client";
128
+ import { useProduct, useStore } from "react-shopwave-connect/hooks";
129
+
130
+ export function ProductList({ storeId }: { storeId: number }) {
131
+ const { data: products, loading, error, refetch } = useProduct({ storeId });
132
+
133
+ if (loading) return <p>Loading…</p>;
134
+ if (error) return <p>Error: {error}</p>;
135
+
136
+ return (
137
+ <>
138
+ <button onClick={refetch}>Reload</button>
139
+ <ul>{products?.map((p) => <li key={p.id}>{p.name}</li>)}</ul>
140
+ </>
141
+ );
142
+ }
143
+ ```
144
+
145
+ In the browser you don't need `baseUrl` — relative `/api/...` URLs work. To target another host or pass a token, give every hook the same optional second argument:
146
+
147
+ ```tsx
148
+ const { data } = useProduct({ storeId }, { baseUrl: "https://api.merchantstack.com", token });
149
+ ```
150
+
151
+ ### Auto-fetch hooks (`useEffect`-based)
152
+
153
+ `useCategory`, `useConsumer`, `useEmployee`, `useProduct`, `useStore`, `useReport`.
154
+ They fetch on mount and re-run when their arguments change. `useConsumer` and `useReport` stay idle until you pass ids / a query (they return `loading: false`, `data: null` until then). `refetch()` replaces the old `reloadFlag` argument.
155
+
156
+ ### Manually-triggered hooks (`useCallback`-based)
157
+
158
+ `useDelete`, `useSubmit`, `useLogout` return `{ mutate, data, loading, error }` — nothing fires until you call `mutate`:
159
+
160
+ ```tsx
161
+ import { useDelete, useSubmit } from "react-shopwave-connect/hooks";
162
+
163
+ const { mutate: remove, loading } = useDelete();
164
+ await remove("products", productId);
165
+
166
+ const { mutate: save } = useSubmit<Product>();
167
+ const saved = await save({
168
+ endpoint: id ? `/api/products/${id}` : "/api/products",
169
+ method: id ? "PUT" : "POST",
170
+ payload,
171
+ });
172
+ ```
173
+
174
+ > The original `useHandleDelete` / `useHandleSubmit` also showed notistack toasts and did validation/payload transforms. Those are app concerns and are intentionally left out of the package — drive them from the returned state or the value `mutate` resolves to.
175
+
176
+ ### Composite hook
177
+
178
+ `useBasketReport(options?)` orchestrates the report → consumers → products pipeline and returns rows, summary, filters, pagination and a `refresh()`. All of its data-shaping logic lives in `core/basket` (pure, testable functions); the hook is just the React glue.
179
+
180
+ ---
181
+
182
+ ## Auth & Next.js note
183
+
184
+ The original `useLogin` / `useLogout` were tightly coupled to Next.js (`next/navigation` router + `'use server'` actions) and **cannot** be framework-agnostic, so they are **not** shipped here. Instead:
185
+
186
+ - `core.logout()` / `useLogout()` end the server session (`DELETE /api/session?action=logout`) — no redirect.
187
+ - The redirect to the auth server's login/logout URL stays in your app, since it depends on your server actions and router:
188
+
189
+ ```tsx
190
+ "use client";
191
+ import { useRouter } from "next/navigation";
192
+ import { useLogout } from "react-shopwave-connect/hooks";
193
+ import { getLogoutUrl } from "@/app/actions";
194
+
195
+ export function LogoutButton() {
196
+ const router = useRouter();
197
+ const { mutate: endSession } = useLogout();
198
+
199
+ const onClick = async () => {
200
+ await endSession();
201
+ router.push(await getLogoutUrl(window.location.origin + "/auth"));
202
+ };
203
+
204
+ return <button onClick={onClick}>Log out</button>;
205
+ }
206
+ ```
207
+
208
+ ---
209
+
210
+ ## Build
211
+
212
+ Built with [`tsdown`](https://tsdown.dev) into dual ESM + CJS with `.d.ts` types, via two separate entries (`core` with no externals, `hooks` with `react`/`react-dom` marked external so they're never bundled).
213
+
214
+ ```bash
215
+ npm run build # emit dist/
216
+ npm run typecheck # tsc --noEmit
217
+ ```
218
+
219
+ ---
220
+
221
+ ## Testing
222
+
223
+ Integration tests run against a live API using [Vitest](https://vitest.dev). Tests are located in `tests/integration/` and cover full CRUD lifecycles for Products, Categories, Consumers, and Employees.
224
+
225
+ ### Environment Variables
226
+
227
+ | Variable | Description | Example |
228
+ |-------------|--------------------------------------|-------------------------------------------|
229
+ | `API_URL` | Base URL for the API | `https://api.staging.merchantstack.com` |
230
+ | `API_TOKEN` | Authentication token | `your-staging-token` |
231
+
232
+ ### Running Tests
233
+
234
+ ```bash
235
+ # Set environment variables
236
+ export API_URL=https://api.staging.merchantstack.com
237
+ export API_TOKEN=your-staging-token
238
+
239
+ # Run all integration tests
240
+ npm run test:integration
241
+
242
+ # Run tests in watch mode
243
+ npm test
244
+ ```
245
+
246
+ ### Test Structure
247
+
248
+ ```
249
+ tests/
250
+ integration/
251
+ setup.ts # Shared config and helpers
252
+ products.integration.test.ts # Products CRUD lifecycle
253
+ categories.integration.test.ts # Categories CRUD lifecycle
254
+ consumers.integration.test.ts # Consumers CRUD lifecycle
255
+ employees.integration.test.ts # Employees CRUD lifecycle
256
+ ```
257
+
258
+ Each test file uses `describe.sequential` to guarantee execution order (create → read → update → delete) with shared state across tests. An `afterAll` cleanup ensures created resources are deleted even if a test fails midway.
@@ -0,0 +1,426 @@
1
+
2
+ //#region src/core/request.ts
3
+ function resolveFetch(options) {
4
+ const f = options.fetch ?? (typeof fetch !== "undefined" ? fetch : void 0);
5
+ if (!f) throw new Error("No fetch implementation available. Pass `options.fetch` (e.g. node-fetch / undici) when running outside the browser.");
6
+ return f;
7
+ }
8
+ function buildExtras(extras, options) {
9
+ const merged = {
10
+ "Content-Type": "application/json",
11
+ ...extras
12
+ };
13
+ if (options.token != null && merged.token == null) merged.token = options.token;
14
+ return merged;
15
+ }
16
+ /**
17
+ * Returns a JSON string of API errors if the response carries any, otherwise
18
+ * `null`. Mirrors the original `!responseJson.api.message.errors` checks.
19
+ */
20
+ function getApiErrors(api) {
21
+ const errors = api?.message?.errors;
22
+ if (errors && Object.keys(errors).length > 0) return JSON.stringify(errors);
23
+ return null;
24
+ }
25
+ /**
26
+ * GET helper. Sends request metadata via the `extras` header exactly like the
27
+ * original hooks did, parses JSON, and returns the typed payload.
28
+ */
29
+ async function apiGet(path, extras, options = {}) {
30
+ const doFetch = resolveFetch(options);
31
+ const url = (options.baseUrl ?? "") + path;
32
+ const headers = { extras: JSON.stringify(buildExtras(extras, options)) };
33
+ console.log("[API GET]", {
34
+ url,
35
+ headers
36
+ });
37
+ const response = await doFetch(url, {
38
+ method: "GET",
39
+ headers,
40
+ signal: options.signal
41
+ });
42
+ if (!response.ok) throw new Error(`Network response was not ok (${response.status})`);
43
+ return await response.json();
44
+ }
45
+ /**
46
+ * JSON body helper for POST/PUT mutations.
47
+ */
48
+ async function apiSend(path, method, payload, options = {}) {
49
+ const doFetch = resolveFetch(options);
50
+ const url = (options.baseUrl ?? "") + path;
51
+ const headers = { "Content-Type": "application/json" };
52
+ if (options.token != null) headers.token = options.token;
53
+ console.log(`[API ${method}]`, {
54
+ url,
55
+ headers,
56
+ payload
57
+ });
58
+ const response = await doFetch(url, {
59
+ method,
60
+ headers,
61
+ body: JSON.stringify(payload),
62
+ signal: options.signal
63
+ });
64
+ if (!response.ok) throw new Error(`Failed to ${method.toLowerCase()}: ${response.statusText}`);
65
+ return await response.json();
66
+ }
67
+ /**
68
+ * DELETE helper.
69
+ */
70
+ async function apiDelete(path, options = {}) {
71
+ const doFetch = resolveFetch(options);
72
+ const url = (options.baseUrl ?? "") + path;
73
+ const headers = {};
74
+ if (options.token != null) headers.token = options.token;
75
+ console.log("[API DELETE]", {
76
+ url,
77
+ headers
78
+ });
79
+ const response = await doFetch(url, {
80
+ method: "DELETE",
81
+ headers,
82
+ signal: options.signal
83
+ });
84
+ if (!response.ok) throw new Error(`Failed to delete: ${response.statusText}`);
85
+ }
86
+
87
+ //#endregion
88
+ //#region src/core/category.ts
89
+ /**
90
+ * Fetches categories. Framework-agnostic: returns the flattened `Category[]`
91
+ * or throws on API errors.
92
+ */
93
+ async function fetchCategories(params = {}, options = {}) {
94
+ const extras = { deleted: params.deleted ?? false };
95
+ if (params.categoryIds) extras.categoryIds = params.categoryIds;
96
+ if (params.token) extras.token = params.token;
97
+ const json = await apiGet("/api/categories", extras, options);
98
+ const error = getApiErrors(json.api);
99
+ if (error) throw new Error(error);
100
+ return Object.values(json.categories ?? {});
101
+ }
102
+
103
+ //#endregion
104
+ //#region src/core/consumer.ts
105
+ /**
106
+ * Fetches consumers by id. Returns an empty array when no ids are supplied
107
+ * (the original hook short-circuited in that case).
108
+ */
109
+ async function fetchConsumers(consumerIds, options = {}) {
110
+ if (!consumerIds || consumerIds.length === 0) return [];
111
+ const json = await apiGet("/api/consumer", { ids: consumerIds.join(",") }, options);
112
+ const error = getApiErrors(json.api);
113
+ if (error) throw new Error(error);
114
+ return Object.values(json.consumers ?? {});
115
+ }
116
+
117
+ //#endregion
118
+ //#region src/core/employee.ts
119
+ const rolesById = {
120
+ "1": "Owner",
121
+ "2": "Manager",
122
+ "3": "Assistant",
123
+ "4": "Guest",
124
+ "5": "Assistant Manager"
125
+ };
126
+ /**
127
+ * Fetches employees. Returns the flattened `Employee[]`.
128
+ */
129
+ async function fetchEmployees(params = {}, options = {}) {
130
+ const extras = { deleted: params.deleted ?? false };
131
+ if (params.employeeIds) extras.employeeIds = params.employeeIds;
132
+ const json = await apiGet("/api/employees", extras, options);
133
+ const error = getApiErrors(json.api);
134
+ if (error) throw new Error(error);
135
+ return Object.values(json.employees ?? {});
136
+ }
137
+
138
+ //#endregion
139
+ //#region src/core/product.ts
140
+ /**
141
+ * Fetches products. Returns the flattened `Product[]`.
142
+ */
143
+ async function fetchProducts(params = {}, options = {}) {
144
+ const extras = { deleted: params.deleted ?? false };
145
+ if (params.productIds) extras.productIds = params.productIds;
146
+ if (params.storeId) extras.storeId = params.storeId;
147
+ if (params.token) extras.token = params.token;
148
+ const json = await apiGet("/api/products", extras, options);
149
+ const error = getApiErrors(json.api);
150
+ if (error) throw new Error(error);
151
+ return Object.values(json.products ?? {});
152
+ }
153
+ /**
154
+ * Fetches products by id and returns them keyed by id, transparently batching
155
+ * the request (default 200 ids per call) to stay within API limits. Extracted
156
+ * from the original `useBasketReport` inline batching loop so it can be reused
157
+ * outside React.
158
+ */
159
+ async function fetchProductsMap(productIds, options = {}, batchSize = 200) {
160
+ const result = {};
161
+ if (!productIds || productIds.length === 0) return result;
162
+ for (let i = 0; i < productIds.length; i += batchSize) {
163
+ const products = await fetchProducts({ productIds: productIds.slice(i, i + batchSize) }, options);
164
+ for (const product of products) result[product.id] = product;
165
+ }
166
+ return result;
167
+ }
168
+
169
+ //#endregion
170
+ //#region src/core/promotion.ts
171
+ /** Promotion rule identifiers as stored by the Shopwave API. */
172
+ let PromotionRuleSet = /* @__PURE__ */ function(PromotionRuleSet$1) {
173
+ PromotionRuleSet$1["Percentage"] = "|%^/";
174
+ PromotionRuleSet$1["Combo"] = "&%</";
175
+ PromotionRuleSet$1["MealDeal"] = "&=^/";
176
+ return PromotionRuleSet$1;
177
+ }({});
178
+ /**
179
+ * Fetches promotions. Returns the flattened `Promotion[]`.
180
+ */
181
+ async function fetchPromotions(params = {}, options = {}) {
182
+ const extras = { deleted: params.deleted ?? false };
183
+ if (params.promotionIds) extras.promotionIds = params.promotionIds;
184
+ if (params.token) extras.token = params.token;
185
+ const json = await apiGet("/api/promotions", extras, options);
186
+ const error = getApiErrors(json.api);
187
+ if (error) throw new Error(error);
188
+ return Object.values(json.promotions ?? {});
189
+ }
190
+
191
+ //#endregion
192
+ //#region src/core/store.ts
193
+ /**
194
+ * Fetches stores. Returns the flattened `Store[]`.
195
+ */
196
+ async function fetchStores(params = {}, options = {}) {
197
+ const json = await apiGet("/api/stores", { deleted: params.deleted ?? false }, options);
198
+ const error = getApiErrors(json.api);
199
+ if (error) throw new Error(error);
200
+ return Object.values(json.stores ?? {});
201
+ }
202
+
203
+ //#endregion
204
+ //#region src/core/report.ts
205
+ /**
206
+ * Runs one or more report queries and returns the `reports` map keyed by query
207
+ * name. The query is JSON-encoded into the `extras.query` header, matching the
208
+ * original endpoint contract.
209
+ */
210
+ async function fetchReport(query, options = {}) {
211
+ const json = await apiGet("/api/report", { query: JSON.stringify(query) }, options);
212
+ const error = getApiErrors(json.api);
213
+ if (error) throw new Error(error);
214
+ return json.reports ?? {};
215
+ }
216
+
217
+ //#endregion
218
+ //#region src/core/session.ts
219
+ /**
220
+ * Ends the current server session.
221
+ *
222
+ * This is the framework-agnostic half of the original `useLogout` hook. The
223
+ * Next.js-specific redirect logic (router.push to the auth server logout URL,
224
+ * built from server actions) is intentionally NOT included here because it is
225
+ * tied to `next/navigation` and `'use server'` actions — see the README.
226
+ */
227
+ async function logout(options = {}) {
228
+ await apiDelete("/api/session?action=logout", options);
229
+ }
230
+
231
+ //#endregion
232
+ //#region src/core/entity.ts
233
+ /**
234
+ * Generic CRUD-style mutations extracted from `useHandleDelete` /
235
+ * `useHandleSubmit`. The original hooks also surfaced notistack snackbars; that
236
+ * UI concern is deliberately left to the hooks/host layer so core stays
237
+ * dependency-free.
238
+ */
239
+ /**
240
+ * Deletes `/api/{resourcePath}/{entityId}`.
241
+ */
242
+ async function deleteEntity(resourcePath, entityId, options = {}) {
243
+ if (entityId == null || entityId === "") throw new Error("entityId is missing");
244
+ await apiDelete(`/api/${resourcePath}/${entityId}`, options);
245
+ }
246
+ /**
247
+ * Sends a create/update mutation and returns the parsed `result` field
248
+ * (matching the original `result.result` access).
249
+ */
250
+ async function submitEntity({ endpoint, method = "POST", payload }, options = {}) {
251
+ return (await apiSend(endpoint, method, payload, options)).result;
252
+ }
253
+
254
+ //#endregion
255
+ //#region src/core/basket.ts
256
+ const DEFAULT_BASKET_FILTERS = {
257
+ search: "",
258
+ dateRange: {
259
+ start: null,
260
+ end: null
261
+ },
262
+ consumerId: null,
263
+ status: "all"
264
+ };
265
+ const DEFAULT_PAGINATION = {
266
+ page: 0,
267
+ pageSize: 25,
268
+ total: 0
269
+ };
270
+ /** Formats a date as `YYYY-MM-DD HH:mm:ss` for the report query. */
271
+ function formatDateForQuery(date, isEndOfDay = false) {
272
+ return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, "0")}-${String(date.getDate()).padStart(2, "0")} ${isEndOfDay ? "23:59:59" : "00:00:00"}`;
273
+ }
274
+ /** Builds the `Basket` report query from a set of filters. */
275
+ function buildBasketReportQuery(filters) {
276
+ const whereConditions = [];
277
+ const today = /* @__PURE__ */ new Date();
278
+ const startDate = filters.dateRange.start ?? today;
279
+ const endDate = filters.dateRange.end ?? today;
280
+ whereConditions.push(`Basket.completeDate > '${formatDateForQuery(startDate, false)}'`);
281
+ whereConditions.push(`Basket.completeDate < '${formatDateForQuery(endDate, true)}'`);
282
+ if (filters.consumerId) whereConditions.push(`Basket.consumerId = ${filters.consumerId}`);
283
+ if (filters.status !== "all") whereConditions.push(`Basket.status = '${filters.status}'`);
284
+ return { baskets: {
285
+ FROM: "Basket",
286
+ WHERE: whereConditions.length > 0 ? { AND: whereConditions } : void 0
287
+ } };
288
+ }
289
+ /** Flattens the raw report rows/keys into structured baskets. */
290
+ function parseBasketReportData(reports) {
291
+ if (!reports?.baskets?.data || !reports?.baskets?.keys) return [];
292
+ const { keys, data } = reports.baskets;
293
+ const keyIndices = {};
294
+ keys.forEach((key, index) => {
295
+ keyIndices[key.name] = index;
296
+ });
297
+ const productKey = keys.find((k) => k.name === "Basket.Product");
298
+ const productSubKeys = {};
299
+ if (productKey?.item) productKey.item.forEach((subKey, index) => {
300
+ productSubKeys[subKey.name] = index;
301
+ });
302
+ return data.map((row) => {
303
+ const products = (row[keyIndices["Basket.Product"]] || []).map((productRow) => ({
304
+ productId: productRow[productSubKeys["Basket.Product.productId"]],
305
+ productName: `Product #${productRow[productSubKeys["Basket.Product.productId"]]}`,
306
+ quantity: productRow[productSubKeys["Basket.Product.quantity"]] || 1,
307
+ price: productRow[productSubKeys["Basket.Product.productPrice"]] || 0,
308
+ tax: productRow[productSubKeys["Basket.Product.productTaxPercentage"]] || 0
309
+ }));
310
+ const total = products.reduce((sum, p) => sum + p.price * p.quantity, 0);
311
+ const taxTotal = products.reduce((sum, p) => sum + p.price * p.quantity * p.tax, 0);
312
+ const completeDateTimestamp = row[keyIndices["Basket.completeDate"]];
313
+ const completeDate = completeDateTimestamp ? new Date(completeDateTimestamp).toISOString() : "";
314
+ return {
315
+ id: row[keyIndices["Basket.id"]],
316
+ name: row[keyIndices["Basket.name"]],
317
+ consumerId: row[keyIndices["Basket.consumerId"]],
318
+ products,
319
+ total,
320
+ taxTotal,
321
+ status: "completed",
322
+ completeDate
323
+ };
324
+ });
325
+ }
326
+ /** Unique non-null consumer ids referenced by a set of baskets. */
327
+ function collectConsumerIds(baskets) {
328
+ if (!baskets.length) return null;
329
+ const ids = baskets.map((b) => b.consumerId).filter((id) => id != null);
330
+ return ids.length > 0 ? [...new Set(ids)] : null;
331
+ }
332
+ /** Unique product ids referenced by a set of baskets. */
333
+ function collectProductIds(baskets) {
334
+ if (!baskets.length) return null;
335
+ const ids = /* @__PURE__ */ new Set();
336
+ baskets.forEach((basket) => {
337
+ basket.products.forEach((product) => {
338
+ if (product.productId) ids.add(product.productId);
339
+ });
340
+ });
341
+ return ids.size > 0 ? [...ids] : null;
342
+ }
343
+ /** Joins baskets with consumer + product lookups, applies search, sorts. */
344
+ function combineBasketRows({ baskets, consumerMap, productMap, search }) {
345
+ if (!baskets.length) return [];
346
+ let combinedRows = baskets.map((basket) => {
347
+ const consumer = basket.consumerId ? consumerMap[basket.consumerId] : null;
348
+ const productsWithNames = basket.products.map((p) => {
349
+ const product = productMap[p.productId];
350
+ return {
351
+ ...p,
352
+ productName: product?.name || `Product #${p.productId}`
353
+ };
354
+ });
355
+ return {
356
+ id: basket.id,
357
+ basketId: basket.id,
358
+ basketName: basket.name || `Basket #${basket.id}`,
359
+ consumerId: basket.consumerId,
360
+ consumerName: consumer ? `${consumer.firstName} ${consumer.lastName}` : "Unknown",
361
+ consumerEmail: consumer?.email || "-",
362
+ products: productsWithNames,
363
+ basketTotal: (basket.total || 0) / 100,
364
+ tax: basket.taxTotal || 0,
365
+ dateTime: basket.completeDate,
366
+ status: basket.status || "pending"
367
+ };
368
+ });
369
+ if (search) {
370
+ const searchLower = search.toLowerCase();
371
+ combinedRows = combinedRows.filter((row) => row.basketId.toString().includes(searchLower) || row.basketName.toLowerCase().includes(searchLower) || row.consumerName.toLowerCase().includes(searchLower) || row.consumerEmail.toLowerCase().includes(searchLower));
372
+ }
373
+ combinedRows.sort((a, b) => {
374
+ const dateA = new Date(a.dateTime).getTime();
375
+ return new Date(b.dateTime).getTime() - dateA;
376
+ });
377
+ return combinedRows;
378
+ }
379
+ /** Aggregate totals for a set of report rows. */
380
+ function computeBasketSummary(rows) {
381
+ const uniqueConsumerIds = new Set(rows.map((row) => row.consumerId));
382
+ return {
383
+ totalBaskets: rows.length,
384
+ totalRevenue: rows.reduce((sum, row) => sum + row.basketTotal, 0),
385
+ totalTax: rows.reduce((sum, row) => sum + row.tax, 0),
386
+ uniqueConsumers: uniqueConsumerIds.size
387
+ };
388
+ }
389
+ /** Builds a consumer id → consumer lookup. */
390
+ function buildConsumerMap(consumers) {
391
+ const map = {};
392
+ if (consumers) consumers.forEach((consumer) => {
393
+ map[consumer.id] = consumer;
394
+ });
395
+ return map;
396
+ }
397
+
398
+ //#endregion
399
+ exports.DEFAULT_BASKET_FILTERS = DEFAULT_BASKET_FILTERS;
400
+ exports.DEFAULT_PAGINATION = DEFAULT_PAGINATION;
401
+ exports.PromotionRuleSet = PromotionRuleSet;
402
+ exports.apiDelete = apiDelete;
403
+ exports.apiGet = apiGet;
404
+ exports.apiSend = apiSend;
405
+ exports.buildBasketReportQuery = buildBasketReportQuery;
406
+ exports.buildConsumerMap = buildConsumerMap;
407
+ exports.collectConsumerIds = collectConsumerIds;
408
+ exports.collectProductIds = collectProductIds;
409
+ exports.combineBasketRows = combineBasketRows;
410
+ exports.computeBasketSummary = computeBasketSummary;
411
+ exports.deleteEntity = deleteEntity;
412
+ exports.fetchCategories = fetchCategories;
413
+ exports.fetchConsumers = fetchConsumers;
414
+ exports.fetchEmployees = fetchEmployees;
415
+ exports.fetchProducts = fetchProducts;
416
+ exports.fetchProductsMap = fetchProductsMap;
417
+ exports.fetchPromotions = fetchPromotions;
418
+ exports.fetchReport = fetchReport;
419
+ exports.fetchStores = fetchStores;
420
+ exports.formatDateForQuery = formatDateForQuery;
421
+ exports.getApiErrors = getApiErrors;
422
+ exports.logout = logout;
423
+ exports.parseBasketReportData = parseBasketReportData;
424
+ exports.rolesById = rolesById;
425
+ exports.submitEntity = submitEntity;
426
+ //# sourceMappingURL=index.cjs.map