react-shopwave-connect 0.1.1 → 0.2.0

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 CHANGED
@@ -28,6 +28,8 @@ npm install react react-dom
28
28
  | `react-shopwave-connect/core` | Framework-agnostic functions + types only |
29
29
  | `react-shopwave-connect/hooks` | React hooks only |
30
30
  | `react-shopwave-connect` | Both (re-exports `core` + `hooks`) |
31
+ | `react-shopwave-connect/server` | **Server-only.** Framework-agnostic Shopwave OAuth client + token helpers |
32
+ | `react-shopwave-connect/next` | **Server-only.** Next.js login/callback/logout/session route handlers, token refresh, proxy guard |
31
33
 
32
34
  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
35
 
@@ -107,7 +109,7 @@ Functions **throw** on network failure or API errors (the error message contains
107
109
  | product | `fetchProducts`, `fetchProductsMap` (batched, keyed) |
108
110
  | store | `fetchStores` |
109
111
  | report | `fetchReport` |
110
- | session | `logout` |
112
+ | session | `fetchSession`, `loginPath`, `logoutPath`, `logout` |
111
113
  | entity | `deleteEntity`, `submitEntity` |
112
114
  | basket | `buildBasketReportQuery`, `parseBasketReportData`, `combineBasketRows`, `computeBasketSummary`, … (pure transforms) |
113
115
 
@@ -150,7 +152,7 @@ const { data } = useProduct({ storeId }, { baseUrl: "https://api.merchantstack.c
150
152
 
151
153
  ### Auto-fetch hooks (`useEffect`-based)
152
154
 
153
- `useCategory`, `useConsumer`, `useEmployee`, `useProduct`, `useStore`, `useReport`.
155
+ `useCategory`, `useConsumer`, `useEmployee`, `useProduct`, `usePromotion`, `useStore`, `useReport`, `useSession`.
154
156
  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
157
 
156
158
  ### Manually-triggered hooks (`useCallback`-based)
@@ -179,37 +181,144 @@ const saved = await save({
179
181
 
180
182
  ---
181
183
 
182
- ## Auth & Next.js note
184
+ ## 3. Authentication (Shopwave OAuth)
183
185
 
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:
186
+ Login is the same for every Shopwave app; only the **config** differs (client id/secret, redirect URL, session secret). The Shopwave auth server uses the authorization-code flow **with a client secret** (no PKCE), so the exchange must happen on a server — the browser only follows redirects and never sees a token.
185
187
 
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
+ browser ──/auth?returnTo=/products──▶ your app ──302──▶ {authServer}/login?…&state=…
190
+ ◀──────────── user signs in on Shopwave ─────────────┘
191
+ browser ──/auth?code=…&state=…──▶ your app ──POST /oauth/token (secret)──▶ auth server
192
+ ◀──302 /products + encrypted httpOnly cookie (tokens stay server-side)
193
+ ```
188
194
 
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";
195
+ What the SDK handles for you:
196
+
197
+ - `state` check (login-CSRF protection) and a safe `returnTo` (same-origin paths only).
198
+ - One callback URL per app: `returnTo` rides along in the session, so tools/sub-sections don't need their own redirect URIs.
199
+ - Tokens stored as `{ accessToken, refreshToken, tokenType, expiresAt }` in an **iron-session** cookie (httpOnly, SameSite=Lax, Secure in production).
200
+ - Refresh when the access token has expired (Shopwave issues a new one only after expiry; the refresh token is not rotated), de-duplicated across concurrent requests. A rejected refresh token logs the user out.
201
+ - `GET /api/session` returns `{ loggedIn, expiresAt }` — **never tokens**.
202
+ - Sessions written by older apps (raw `{ access_token, refresh_token, … }` in `session.token`) keep working.
203
+
204
+ ### Next.js (App Router) setup
205
+
206
+ Install the peer dependencies once: `npm install react-shopwave-connect iron-session`.
207
+
208
+ **1. One config file per app**
209
+
210
+ ```ts
211
+ // lib/auth.ts
212
+ import { createShopwaveAuth } from "react-shopwave-connect/next";
213
+
214
+ export const auth = createShopwaveAuth({
215
+ authServerUrl: process.env.SHOPWAVE_AUTH_SERVER_URL!, // e.g. https://secure.merchantstack.com
216
+ clientId: process.env.SHOPWAVE_CLIENT_ID!,
217
+ clientSecret: process.env.SHOPWAVE_CLIENT_SECRET!,
218
+ redirectUri: process.env.SHOPWAVE_REDIRECT_URL!, // e.g. https://admin.example.com/auth (registered on the auth server)
219
+ session: {
220
+ password: process.env.SESSION_SECRET!, // ≥ 32 random chars
221
+ cookieName: "shopwave_session_cookie", // keep your existing name to keep users logged in
222
+ },
223
+ // authPath: "/auth", logoutPath: "/auth/logout", sessionPath: "/api/session",
224
+ // defaultReturnTo: "/", requireState: true, refreshSkewSeconds: 0,
225
+ });
226
+ ```
227
+
228
+ Config is validated on first use, so a missing env var doesn't break `next build`.
229
+
230
+ **2. Three route files**
194
231
 
195
- export function LogoutButton() {
196
- const router = useRouter();
197
- const { mutate: endSession } = useLogout();
232
+ ```ts
233
+ // app/auth/route.ts — the redirect-URI path: starts login AND receives the callback
234
+ import { auth } from "@/lib/auth";
235
+ export const GET = auth.handlers.auth;
236
+
237
+ // app/auth/logout/route.ts — clears the session, then logs out of the auth server
238
+ import { auth } from "@/lib/auth";
239
+ export const GET = auth.handlers.logout;
198
240
 
199
- const onClick = async () => {
200
- await endSession();
201
- router.push(await getLogoutUrl(window.location.origin + "/auth"));
202
- };
241
+ // app/api/session/route.ts — { loggedIn, expiresAt } for the browser; DELETE ends the session
242
+ import { auth } from "@/lib/auth";
243
+ export const { GET, DELETE } = auth.handlers.session;
244
+ ```
203
245
 
204
- return <button onClick={onClick}>Log out</button>;
246
+ **3. Protect pages and APIs** (`proxy.ts` in Next 16, `middleware.ts` before that)
247
+
248
+ ```ts
249
+ import { NextResponse, type NextRequest } from "next/server";
250
+ import { auth } from "@/lib/auth";
251
+
252
+ export async function proxy(request: NextRequest) {
253
+ return (await auth.protect(request, { publicPaths: ["/tools/tag-joiner"] })) ?? NextResponse.next();
205
254
  }
255
+
256
+ export const config = { matcher: ["/((?!_next/static|_next/image|favicon.ico).*)"] };
206
257
  ```
207
258
 
259
+ Logged-out page requests are redirected to `/auth?returnTo=<page>`; paths under `/api` get a `401` JSON instead. The auth, logout and session paths are always public. `protect` only reads the cookie — it never calls the auth server.
260
+
261
+ **4. Use the token in route handlers**
262
+
263
+ ```ts
264
+ import { auth } from "@/lib/auth";
265
+ import { isExpiredTokenResponse } from "react-shopwave-connect/next";
266
+
267
+ export const GET = auth.withAuth(async (request, context, { authorization }) => {
268
+ const call = (header: string) =>
269
+ fetch(`${process.env.SHOPWAVE_API_SERVER_URL}/product`, {
270
+ headers: { Authorization: header, "x-accept-version": "2.0" },
271
+ });
272
+
273
+ let res = await call(authorization);
274
+ let body = await res.json();
275
+
276
+ // Clock said valid but the API says expired (HTTP 401 / API error 908): refresh once and retry.
277
+ if (isExpiredTokenResponse(res.status, body)) {
278
+ const fresh = await auth.getAuthorizationHeader({ forceRefresh: true });
279
+ if (!fresh) return Response.json({ error: "unauthorized" }, { status: 401 });
280
+ res = await call(fresh);
281
+ body = await res.json();
282
+ }
283
+ return Response.json(body, { status: res.status });
284
+ });
285
+ ```
286
+
287
+ Also available: `auth.getAccessToken()`, `auth.getAuthorizationHeader()`, `auth.getToken()`, `auth.getStatus()`, `auth.loginPath(returnTo)`, `auth.logoutPath`, `auth.isAuthenticated(request)`. Refreshed tokens are saved automatically from route handlers, server actions and proxy (Server Components can read tokens but can't write cookies).
288
+
289
+ **5. In the browser**
290
+
291
+ ```tsx
292
+ "use client";
293
+ import { useSession, loginPath, logoutPath } from "react-shopwave-connect";
294
+
295
+ export function AccountButton() {
296
+ const { data: session, loading } = useSession();
297
+ if (loading) return null;
298
+ return session?.loggedIn
299
+ ? <a href={logoutPath()}>Log out</a>
300
+ : <a href={loginPath(window.location.pathname)}>Log in</a>;
301
+ }
302
+ ```
303
+
304
+ Use plain links / `window.location` for login and logout — they are full-page redirects to the auth server, not client-side navigations.
305
+
306
+ ### Other frameworks
307
+
308
+ `react-shopwave-connect/server` has the same building blocks without Next.js: `createShopwaveOAuth(config)` → `buildLoginUrl({ state })`, `exchangeCode(code)`, `refreshToken(token)`, `buildLogoutUrl()`, plus `createState`, `sanitizeReturnTo`, `isTokenExpired`, `isExpiredTokenResponse` and `authorizationHeader`. Store the token in your framework's session (Express `express-session`, iron-session's `getIronSession(req, res)`, …).
309
+
310
+ ### Moving an existing app over
311
+
312
+ - Keep `redirectUri` equal to the URL already registered on the auth server (AdminUI: `…/auth`) and mount `handlers.auth` at that path.
313
+ - Keep `cookieName` the same and use the same `SESSION_SECRET` value as the old iron-session password — current users stay logged in; their old token shape is read and upgraded on the next refresh.
314
+ - Per-tool auth routes can go: link to `auth.loginPath("/tools/where-to-next")` instead.
315
+ - `core.logout()` / `useLogout()` still work (`DELETE /api/session`), but prefer linking to `logoutPath()` so the auth-server session ends too.
316
+
208
317
  ---
209
318
 
210
319
  ## Build
211
320
 
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).
321
+ Built with [`tsdown`](https://tsdown.dev) into dual ESM + CJS with `.d.ts` types, via separate entries: `core` (no externals), `hooks` (`react`/`react-dom` external), `server` (no externals) and `next` (`next`/`iron-session` external — optional peer dependencies). `server` and `next` are never re-exported from the root entry, so they can't end up in client bundles.
213
322
 
214
323
  ```bash
215
324
  npm run build # emit dist/
@@ -220,6 +329,12 @@ npm run typecheck # tsc --noEmit
220
329
 
221
330
  ## Testing
222
331
 
332
+ Unit tests (offline, no credentials) cover the OAuth client, token handling, the Next.js handlers (with an in-memory cookie store) and the session client:
333
+
334
+ ```bash
335
+ npm test # = npm run test:unit
336
+ ```
337
+
223
338
  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
339
 
225
340
  ### Environment Variables
@@ -239,7 +354,7 @@ export API_TOKEN=your-staging-token
239
354
  # Run all integration tests
240
355
  npm run test:integration
241
356
 
242
- # Run tests in watch mode
357
+ # Run unit tests
243
358
  npm test
244
359
  ```
245
360
 
@@ -247,6 +362,10 @@ npm test
247
362
 
248
363
  ```
249
364
  tests/
365
+ unit/
366
+ server.test.ts # OAuth client, tokens, returnTo/state
367
+ next.test.ts # Next.js handlers, refresh, proxy guard
368
+ session-client.test.ts # fetchSession / loginPath / logoutPath
250
369
  integration/
251
370
  setup.ts # Shared config and helpers
252
371
  products.integration.test.ts # Products CRUD lifecycle
@@ -181,6 +181,9 @@ let PromotionRuleSet = /* @__PURE__ */ function(PromotionRuleSet$1) {
181
181
  async function fetchPromotions(params = {}, options = {}) {
182
182
  const extras = { deleted: params.deleted ?? false };
183
183
  if (params.promotionIds) extras.promotionIds = params.promotionIds;
184
+ if (params.imminent !== void 0) extras.imminent = params.imminent;
185
+ if (params.active !== void 0) extras.active = params.active;
186
+ if (params.expired !== void 0) extras.expired = params.expired;
184
187
  if (params.token) extras.token = params.token;
185
188
  const json = await apiGet("/api/promotions", extras, options);
186
189
  const error = getApiErrors(json.api);
@@ -217,12 +220,47 @@ async function fetchReport(query, options = {}) {
217
220
  //#endregion
218
221
  //#region src/core/session.ts
219
222
  /**
220
- * Ends the current server session.
223
+ * Reads the current login status from `GET /api/session`.
221
224
  *
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.
225
+ * Works with the new `react-shopwave-connect/next` session route
226
+ * (`{ loggedIn }`) and with older routes that returned the raw session
227
+ * (`{ token: {...} }`) — in that case only a boolean is surfaced.
228
+ */
229
+ async function fetchSession(options = {}) {
230
+ const doFetch = options.fetch ?? (typeof fetch !== "undefined" ? fetch : void 0);
231
+ if (!doFetch) throw new Error("No fetch implementation available. Pass `options.fetch`.");
232
+ const response = await doFetch(`${options.baseUrl ?? ""}/api/session`, {
233
+ method: "GET",
234
+ headers: { Accept: "application/json" },
235
+ cache: "no-store",
236
+ credentials: "same-origin",
237
+ signal: options.signal
238
+ });
239
+ if (response.status === 401) return { loggedIn: false };
240
+ if (!response.ok) throw new Error(`Session request failed (${response.status})`);
241
+ const json = await response.json();
242
+ if (json && typeof json.loggedIn === "boolean") return {
243
+ loggedIn: json.loggedIn,
244
+ expiresAt: typeof json.expiresAt === "number" ? json.expiresAt : void 0
245
+ };
246
+ return { loggedIn: Boolean(json && json.token) };
247
+ }
248
+ /**
249
+ * Path that starts a login and returns to `returnTo` afterwards.
250
+ * Navigate the browser to it (`window.location.href = loginPath(...)`).
251
+ */
252
+ function loginPath(returnTo, authPath = "/auth") {
253
+ return returnTo ? `${authPath}?returnTo=${encodeURIComponent(returnTo)}` : authPath;
254
+ }
255
+ /** Path that logs out of the app and the Shopwave auth server. */
256
+ function logoutPath(authPath = "/auth") {
257
+ return `${authPath}/logout`;
258
+ }
259
+ /**
260
+ * Ends the current server session (no redirect).
261
+ *
262
+ * Prefer navigating to {@link logoutPath} when using
263
+ * `react-shopwave-connect/next`: it also signs the user out of the auth server.
226
264
  */
227
265
  async function logout(options = {}) {
228
266
  await apiDelete("/api/session?action=logout", options);
@@ -416,10 +454,13 @@ exports.fetchProducts = fetchProducts;
416
454
  exports.fetchProductsMap = fetchProductsMap;
417
455
  exports.fetchPromotions = fetchPromotions;
418
456
  exports.fetchReport = fetchReport;
457
+ exports.fetchSession = fetchSession;
419
458
  exports.fetchStores = fetchStores;
420
459
  exports.formatDateForQuery = formatDateForQuery;
421
460
  exports.getApiErrors = getApiErrors;
461
+ exports.loginPath = loginPath;
422
462
  exports.logout = logout;
463
+ exports.logoutPath = logoutPath;
423
464
  exports.parseBasketReportData = parseBasketReportData;
424
465
  exports.rolesById = rolesById;
425
466
  exports.submitEntity = submitEntity;
@@ -1 +1 @@
1
- {"version":3,"file":"index.cjs","names":["merged: Extras","headers: Record<string, string>","extras: Record<string, unknown>","extras: Record<string, unknown>","extras: Record<string, unknown>","result: { [id: number]: Product }","extras: Record<string, unknown>","DEFAULT_BASKET_FILTERS: BasketFilters","DEFAULT_PAGINATION: BasketPagination","whereConditions: string[]","keyIndices: { [key: string]: number }","productSubKeys: { [key: string]: number }","products: BasketItem[]","combinedRows: BasketReportRow[]","productsWithNames: BasketItem[]","map: { [id: number]: Consumer }"],"sources":["../../src/core/request.ts","../../src/core/category.ts","../../src/core/consumer.ts","../../src/core/employee.ts","../../src/core/product.ts","../../src/core/promotion.ts","../../src/core/store.ts","../../src/core/report.ts","../../src/core/session.ts","../../src/core/entity.ts","../../src/core/basket.ts"],"sourcesContent":["import type { apiResponse } from \"./types\";\n\n/**\n * Per-call configuration shared by every core API function.\n *\n * Nothing here is React-specific. By accepting these options per call, the\n * core layer works in the browser/Next (relative `/api/...` URLs, the default)\n * AND in Node / CLI / Vue / Angular by passing an absolute `baseUrl`, an auth\n * `token`, and/or a custom `fetch` implementation.\n */\nexport interface RequestOptions {\n /**\n * Prefixed in front of every request path. Leave undefined in the browser to\n * use relative URLs (e.g. `/api/products`). In Node/CLI pass something like\n * `https://api.shopwave.com`.\n */\n baseUrl?: string;\n\n /** OAuth/session token forwarded in the `extras.token` header. */\n token?: string;\n\n /**\n * Custom fetch implementation. Defaults to the global `fetch`. Useful for\n * Node < 18, testing, or injecting interceptors.\n */\n fetch?: typeof fetch;\n\n /** Optional abort signal for cancellation (used by the hooks layer). */\n signal?: AbortSignal;\n}\n\n/** The Shopwave API ships request metadata inside a single `extras` header. */\nexport type Extras = Record<string, unknown>;\n\nfunction resolveFetch(options: RequestOptions): typeof fetch {\n const f = options.fetch ?? (typeof fetch !== \"undefined\" ? fetch : undefined);\n if (!f) {\n throw new Error(\n \"No fetch implementation available. Pass `options.fetch` (e.g. node-fetch / undici) when running outside the browser.\"\n );\n }\n return f;\n}\n\nfunction buildExtras(extras: Extras, options: RequestOptions): Extras {\n const merged: Extras = { \"Content-Type\": \"application/json\", ...extras };\n if (options.token != null && merged.token == null) {\n merged.token = options.token;\n }\n return merged;\n}\n\n/**\n * Returns a JSON string of API errors if the response carries any, otherwise\n * `null`. Mirrors the original `!responseJson.api.message.errors` checks.\n */\nexport function getApiErrors(api?: apiResponse): string | null {\n const errors = api?.message?.errors;\n if (errors && Object.keys(errors).length > 0) {\n return JSON.stringify(errors);\n }\n return null;\n}\n\n/**\n * GET helper. Sends request metadata via the `extras` header exactly like the\n * original hooks did, parses JSON, and returns the typed payload.\n */\nexport async function apiGet<T>(\n path: string,\n extras: Extras,\n options: RequestOptions = {}\n): Promise<T> {\n const doFetch = resolveFetch(options);\n const url = (options.baseUrl ?? \"\") + path;\n\n const headers = { extras: JSON.stringify(buildExtras(extras, options)) };\n console.log('[API GET]', { url, headers });\n\n const response = await doFetch(url, {\n method: \"GET\",\n headers,\n signal: options.signal,\n });\n\n if (!response.ok) {\n throw new Error(`Network response was not ok (${response.status})`);\n }\n\n return (await response.json()) as T;\n}\n\n/**\n * JSON body helper for POST/PUT mutations.\n */\nexport async function apiSend<T>(\n path: string,\n method: \"POST\" | \"PUT\",\n payload: unknown,\n options: RequestOptions = {}\n): Promise<T> {\n const doFetch = resolveFetch(options);\n const url = (options.baseUrl ?? \"\") + path;\n\n const headers: Record<string, string> = { \"Content-Type\": \"application/json\" };\n if (options.token != null) {\n headers.token = options.token;\n }\n\n console.log(`[API ${method}]`, { url, headers, payload });\n\n const response = await doFetch(url, {\n method,\n headers,\n body: JSON.stringify(payload),\n signal: options.signal,\n });\n\n if (!response.ok) {\n throw new Error(`Failed to ${method.toLowerCase()}: ${response.statusText}`);\n }\n\n return (await response.json()) as T;\n}\n\n/**\n * DELETE helper.\n */\nexport async function apiDelete(\n path: string,\n options: RequestOptions = {}\n): Promise<void> {\n const doFetch = resolveFetch(options);\n const url = (options.baseUrl ?? \"\") + path;\n\n const headers: Record<string, string> = {};\n if (options.token != null) {\n headers.token = options.token;\n }\n\n console.log('[API DELETE]', { url, headers });\n\n const response = await doFetch(url, {\n method: \"DELETE\",\n headers,\n signal: options.signal,\n });\n\n if (!response.ok) {\n throw new Error(`Failed to delete: ${response.statusText}`);\n }\n}\n","import { apiGet, getApiErrors, type RequestOptions } from \"./request\";\nimport type { apiResponse } from \"./types\";\n\nexport interface Category {\n id: number;\n title: string;\n parentId: number | null;\n deleteDate: string;\n activeDate: string;\n type: number;\n}\n\nexport interface CategoryResponse {\n categories: {\n [key: number]: Category;\n };\n api: apiResponse;\n}\n\nexport interface FetchCategoriesParams {\n categoryIds?: Array<number>;\n /** Whether to include soft-deleted records. Defaults to `false`. */\n deleted?: boolean;\n /** Token override (also accepted via `options.token`). */\n token?: string;\n}\n\n/**\n * Fetches categories. Framework-agnostic: returns the flattened `Category[]`\n * or throws on API errors.\n */\nexport async function fetchCategories(\n params: FetchCategoriesParams = {},\n options: RequestOptions = {}\n): Promise<Category[]> {\n const extras: Record<string, unknown> = { deleted: params.deleted ?? false };\n\n if (params.categoryIds) {\n extras.categoryIds = params.categoryIds;\n }\n if (params.token) {\n extras.token = params.token;\n }\n\n const json = await apiGet<CategoryResponse>(\"/api/categories\", extras, options);\n\n const error = getApiErrors(json.api);\n if (error) {\n throw new Error(error);\n }\n\n return Object.values(json.categories ?? {});\n}\n","import { apiGet, getApiErrors, type RequestOptions } from \"./request\";\nimport type { apiResponse } from \"./types\";\n\nexport interface Consumer {\n id: number;\n activeCount: number;\n firstName: string;\n lastName: string;\n email: string;\n}\n\nexport interface ConsumerResponse {\n consumers: {\n [key: number]: Consumer;\n };\n api: apiResponse;\n}\n\n/**\n * Fetches consumers by id. Returns an empty array when no ids are supplied\n * (the original hook short-circuited in that case).\n */\nexport async function fetchConsumers(\n consumerIds: number[] | null | undefined,\n options: RequestOptions = {}\n): Promise<Consumer[]> {\n if (!consumerIds || consumerIds.length === 0) {\n return [];\n }\n\n const extras: Record<string, unknown> = { ids: consumerIds.join(\",\") };\n\n const json = await apiGet<ConsumerResponse>(\"/api/consumer\", extras, options);\n\n const error = getApiErrors(json.api);\n if (error) {\n throw new Error(error);\n }\n\n return Object.values(json.consumers ?? {});\n}\n","import { apiGet, getApiErrors, type RequestOptions } from \"./request\";\nimport type { apiResponse } from \"./types\";\n\nexport const rolesById = {\n \"1\": \"Owner\",\n \"2\": \"Manager\",\n \"3\": \"Assistant\",\n \"4\": \"Guest\",\n \"5\": \"Assistant Manager\",\n} as const;\n\nexport interface Employee {\n id: number;\n firstName: string;\n lastName: string;\n email: string;\n roleId: number;\n joinedDate: string;\n exitDate: string;\n stores: {\n [key: number]: {\n id: number;\n roleId: number;\n };\n };\n}\n\nexport interface EmployeeResponse {\n employees: {\n [key: number]: Employee;\n };\n api: apiResponse;\n}\n\nexport interface FetchEmployeesParams {\n /** Optional id filter. Forwarded when provided. */\n employeeIds?: Array<number>;\n /** Whether to include soft-deleted records. Defaults to `false`. */\n deleted?: boolean;\n}\n\n/**\n * Fetches employees. Returns the flattened `Employee[]`.\n */\nexport async function fetchEmployees(\n params: FetchEmployeesParams = {},\n options: RequestOptions = {}\n): Promise<Employee[]> {\n const extras: Record<string, unknown> = { deleted: params.deleted ?? false };\n\n if (params.employeeIds) {\n extras.employeeIds = params.employeeIds;\n }\n\n const json = await apiGet<EmployeeResponse>(\"/api/employees\", extras, options);\n\n const error = getApiErrors(json.api);\n if (error) {\n throw new Error(error);\n }\n\n return Object.values(json.employees ?? {});\n}\n","import { apiGet, getApiErrors, type RequestOptions } from \"./request\";\nimport type { apiResponse } from \"./types\";\n\nexport interface ProductInstance {\n id: number;\n price: string;\n taxPercentage: string;\n timestamp: string;\n size: string;\n name: string;\n activeDate: string;\n tags: string;\n}\n\nexport interface Product {\n id: number;\n barcode: string;\n name: string;\n details: string;\n tags: string;\n unit: number;\n timestamp: string;\n activeDate: string;\n deleteDate: string;\n images: Array<string>;\n categories: Array<number>;\n instances: {\n [key: number]: ProductInstance;\n };\n}\n\nexport interface ProductResponse {\n products: {\n [key: number]: Product;\n };\n api: apiResponse;\n}\n\nexport interface FetchProductsParams {\n productIds?: Array<number>;\n storeId?: number;\n /** Whether to include soft-deleted records. Defaults to `false`. */\n deleted?: boolean;\n /** Token override (also accepted via `options.token`). */\n token?: string;\n}\n\n/**\n * Fetches products. Returns the flattened `Product[]`.\n */\nexport async function fetchProducts(\n params: FetchProductsParams = {},\n options: RequestOptions = {}\n): Promise<Product[]> {\n const extras: Record<string, unknown> = { deleted: params.deleted ?? false };\n\n if (params.productIds) {\n extras.productIds = params.productIds;\n }\n if (params.storeId) {\n extras.storeId = params.storeId;\n }\n if (params.token) {\n extras.token = params.token;\n }\n\n const json = await apiGet<ProductResponse>(\"/api/products\", extras, options);\n\n const error = getApiErrors(json.api);\n if (error) {\n throw new Error(error);\n }\n\n return Object.values(json.products ?? {});\n}\n\n/**\n * Fetches products by id and returns them keyed by id, transparently batching\n * the request (default 200 ids per call) to stay within API limits. Extracted\n * from the original `useBasketReport` inline batching loop so it can be reused\n * outside React.\n */\nexport async function fetchProductsMap(\n productIds: number[] | null | undefined,\n options: RequestOptions = {},\n batchSize = 200\n): Promise<{ [id: number]: Product }> {\n const result: { [id: number]: Product } = {};\n if (!productIds || productIds.length === 0) {\n return result;\n }\n\n for (let i = 0; i < productIds.length; i += batchSize) {\n const batch = productIds.slice(i, i + batchSize);\n const products = await fetchProducts({ productIds: batch }, options);\n for (const product of products) {\n result[product.id] = product;\n }\n }\n\n return result;\n}\n","import { apiGet, getApiErrors, type RequestOptions } from \"./request\";\nimport type { apiResponse } from \"./types\";\n\n/** Promotion rule identifiers as stored by the Shopwave API. */\nexport enum PromotionRuleSet {\n Percentage = \"|%^/\",\n Combo = \"&%</\",\n MealDeal = \"&=^/\",\n}\n\nexport interface Promotion {\n id: number;\n merchantId: number;\n storeIds: number[];\n categoryIds: { [key: string]: number };\n productIds: { [key: string]: any };\n title: string;\n details: string | null;\n terms: string | null;\n startDate: string;\n endDate: string;\n daysAvailable: number[];\n automatic: number;\n rule: PromotionRuleSet;\n x: number;\n y: number | null;\n}\n\nexport interface PromotionResponse {\n promotions: {\n [key: number]: Promotion;\n };\n api: apiResponse;\n}\n\nexport interface FetchPromotionsParams {\n /** Optional id filter. Forwarded when provided. */\n promotionIds?: Array<number>;\n /** Whether to include soft-deleted records. Defaults to `false`. */\n deleted?: boolean;\n /** Token override (also accepted via `options.token`). */\n token?: string;\n}\n\n/**\n * Fetches promotions. Returns the flattened `Promotion[]`.\n */\nexport async function fetchPromotions(\n params: FetchPromotionsParams = {},\n options: RequestOptions = {}\n): Promise<Promotion[]> {\n const extras: Record<string, unknown> = { deleted: params.deleted ?? false };\n\n if (params.promotionIds) {\n extras.promotionIds = params.promotionIds;\n }\n if (params.token) {\n extras.token = params.token;\n }\n\n const json = await apiGet<PromotionResponse>(\"/api/promotions\", extras, options);\n\n const error = getApiErrors(json.api);\n if (error) {\n throw new Error(error);\n }\n\n return Object.values(json.promotions ?? {});\n}\n","import { apiGet, getApiErrors, type RequestOptions } from \"./request\";\nimport type { apiResponse } from \"./types\";\n\nexport interface Store {\n id: number;\n lat: number;\n lng: number;\n addressLine1: string;\n addressLine2: string | null;\n addressLine3: string;\n phoneNumber: string;\n city: string;\n postcode: string;\n countryId: number | null;\n timezoneId: number | null;\n storeDeleteDate: string | null;\n email: string | null;\n sandbox: boolean | null;\n}\n\nexport interface StoreResponse {\n stores: {\n [key: number]: Store;\n };\n api: apiResponse;\n}\n\nexport interface FetchStoresParams {\n /** Whether to include soft-deleted records. Defaults to `false`. */\n deleted?: boolean;\n}\n\n/**\n * Fetches stores. Returns the flattened `Store[]`.\n */\nexport async function fetchStores(\n params: FetchStoresParams = {},\n options: RequestOptions = {}\n): Promise<Store[]> {\n const extras: Record<string, unknown> = { deleted: params.deleted ?? false };\n\n const json = await apiGet<StoreResponse>(\"/api/stores\", extras, options);\n\n const error = getApiErrors(json.api);\n if (error) {\n throw new Error(error);\n }\n\n return Object.values(json.stores ?? {});\n}\n","import { apiGet, getApiErrors, type RequestOptions } from \"./request\";\nimport type { apiResponse } from \"./types\";\n\nexport interface ReportKey {\n name: string;\n type: string;\n item?: ReportKey[];\n}\n\nexport interface ReportData {\n keys: ReportKey[];\n data: any[][];\n}\n\nexport interface ReportResponse {\n reports: {\n [key: string]: ReportData;\n };\n api: apiResponse;\n}\n\nexport interface ReportQuery {\n FROM: string;\n WHERE?: {\n AND?: string[];\n OR?: string[];\n };\n SELECT?: string[];\n LIMIT?: number;\n OFFSET?: number;\n}\n\nexport interface ReportQueryMap {\n [key: string]: ReportQuery;\n}\n\n/**\n * Runs one or more report queries and returns the `reports` map keyed by query\n * name. The query is JSON-encoded into the `extras.query` header, matching the\n * original endpoint contract.\n */\nexport async function fetchReport(\n query: ReportQueryMap,\n options: RequestOptions = {}\n): Promise<ReportResponse[\"reports\"]> {\n const extras: Record<string, unknown> = { query: JSON.stringify(query) };\n\n const json = await apiGet<ReportResponse>(\"/api/report\", extras, options);\n\n const error = getApiErrors(json.api);\n if (error) {\n throw new Error(error);\n }\n\n return json.reports ?? {};\n}\n","import { apiDelete, type RequestOptions } from \"./request\";\n\n/**\n * Ends the current server session.\n *\n * This is the framework-agnostic half of the original `useLogout` hook. The\n * Next.js-specific redirect logic (router.push to the auth server logout URL,\n * built from server actions) is intentionally NOT included here because it is\n * tied to `next/navigation` and `'use server'` actions — see the README.\n */\nexport async function logout(options: RequestOptions = {}): Promise<void> {\n await apiDelete(\"/api/session?action=logout\", options);\n}\n","import { apiDelete, apiSend, type RequestOptions } from \"./request\";\n\n/**\n * Generic CRUD-style mutations extracted from `useHandleDelete` /\n * `useHandleSubmit`. The original hooks also surfaced notistack snackbars; that\n * UI concern is deliberately left to the hooks/host layer so core stays\n * dependency-free.\n */\n\n/**\n * Deletes `/api/{resourcePath}/{entityId}`.\n */\nexport async function deleteEntity(\n resourcePath: string,\n entityId: number | string,\n options: RequestOptions = {}\n): Promise<void> {\n if (entityId == null || entityId === \"\") {\n throw new Error(\"entityId is missing\");\n }\n await apiDelete(`/api/${resourcePath}/${entityId}`, options);\n}\n\nexport interface SubmitEntityArgs {\n /** Fully-resolved endpoint, e.g. `/api/products` or `/api/products/42`. */\n endpoint: string;\n /** Defaults to PUT when an `entityId` exists, otherwise POST. */\n method?: \"POST\" | \"PUT\";\n /** The request payload (already transformed by the caller). */\n payload: unknown;\n}\n\nexport interface SubmitEntityResponse<R = any> {\n result?: R;\n [key: string]: unknown;\n}\n\n/**\n * Sends a create/update mutation and returns the parsed `result` field\n * (matching the original `result.result` access).\n */\nexport async function submitEntity<R = any>(\n { endpoint, method = \"POST\", payload }: SubmitEntityArgs,\n options: RequestOptions = {}\n): Promise<R | undefined> {\n const json = await apiSend<SubmitEntityResponse<R>>(\n endpoint,\n method,\n payload,\n options\n );\n return json.result;\n}\n","import type { Consumer } from \"./consumer\";\nimport type { Product } from \"./product\";\nimport type { ReportQueryMap, ReportResponse } from \"./report\";\n\n// ── Basket Report Types (preserved from the original `types/basket.ts`) ──────\n\nexport interface BasketItem {\n productId: number;\n productName: string;\n quantity: number;\n price: number;\n tax: number;\n}\n\nexport interface Basket {\n id: number;\n name: string;\n consumerId: number;\n products: BasketItem[];\n total: number;\n taxTotal: number;\n status: \"pending\" | \"completed\" | \"cancelled\";\n completeDate: string;\n}\n\nexport interface BasketReportRow {\n id: number;\n basketId: number;\n basketName: string;\n consumerId: number;\n consumerName: string;\n consumerEmail: string;\n products: BasketItem[];\n basketTotal: number;\n tax: number;\n dateTime: string;\n status: string;\n}\n\nexport interface BasketSummary {\n totalBaskets: number;\n totalRevenue: number;\n totalTax: number;\n uniqueConsumers: number;\n}\n\nexport interface BasketFilters {\n search: string;\n dateRange: {\n start: Date | null;\n end: Date | null;\n };\n consumerId: number | null;\n status: \"all\" | \"pending\" | \"completed\" | \"cancelled\";\n}\n\nexport interface BasketPagination {\n page: number;\n pageSize: number;\n total: number;\n}\n\nexport const DEFAULT_BASKET_FILTERS: BasketFilters = {\n search: \"\",\n dateRange: {\n start: null,\n end: null,\n },\n consumerId: null,\n status: \"all\",\n};\n\nexport const DEFAULT_PAGINATION: BasketPagination = {\n page: 0,\n pageSize: 25,\n total: 0,\n};\n\n/** Intermediate shape produced by {@link parseBasketReportData}. */\nexport interface ParsedBasket {\n id: number;\n name: string;\n consumerId: number;\n products: BasketItem[];\n total: number;\n taxTotal: number;\n status: \"completed\";\n completeDate: string;\n}\n\n// ── Pure transform helpers (extracted from the original `useBasketReport`) ────\n\n/** Formats a date as `YYYY-MM-DD HH:mm:ss` for the report query. */\nexport function formatDateForQuery(date: Date, isEndOfDay = false): string {\n const year = date.getFullYear();\n const month = String(date.getMonth() + 1).padStart(2, \"0\");\n const day = String(date.getDate()).padStart(2, \"0\");\n const time = isEndOfDay ? \"23:59:59\" : \"00:00:00\";\n return `${year}-${month}-${day} ${time}`;\n}\n\n/** Builds the `Basket` report query from a set of filters. */\nexport function buildBasketReportQuery(filters: BasketFilters): ReportQueryMap {\n const whereConditions: string[] = [];\n\n const today = new Date();\n const startDate = filters.dateRange.start ?? today;\n const endDate = filters.dateRange.end ?? today;\n\n whereConditions.push(\n `Basket.completeDate > '${formatDateForQuery(startDate, false)}'`\n );\n whereConditions.push(\n `Basket.completeDate < '${formatDateForQuery(endDate, true)}'`\n );\n\n if (filters.consumerId) {\n whereConditions.push(`Basket.consumerId = ${filters.consumerId}`);\n }\n\n if (filters.status !== \"all\") {\n whereConditions.push(`Basket.status = '${filters.status}'`);\n }\n\n return {\n baskets: {\n FROM: \"Basket\",\n WHERE: whereConditions.length > 0 ? { AND: whereConditions } : undefined,\n },\n };\n}\n\n/** Flattens the raw report rows/keys into structured baskets. */\nexport function parseBasketReportData(\n reports: ReportResponse[\"reports\"] | null | undefined\n): ParsedBasket[] {\n if (!reports?.baskets?.data || !reports?.baskets?.keys) {\n return [];\n }\n\n const { keys, data } = reports.baskets;\n\n const keyIndices: { [key: string]: number } = {};\n keys.forEach((key, index) => {\n keyIndices[key.name] = index;\n });\n\n const productKey = keys.find((k) => k.name === \"Basket.Product\");\n const productSubKeys: { [key: string]: number } = {};\n if (productKey?.item) {\n productKey.item.forEach((subKey, index) => {\n productSubKeys[subKey.name] = index;\n });\n }\n\n return data.map((row) => {\n const productsArray = (row[keyIndices[\"Basket.Product\"]] as any[][]) || [];\n const products: BasketItem[] = productsArray.map((productRow) => ({\n productId: productRow[productSubKeys[\"Basket.Product.productId\"]] as number,\n productName: `Product #${productRow[productSubKeys[\"Basket.Product.productId\"]]}`,\n quantity:\n (productRow[productSubKeys[\"Basket.Product.quantity\"]] as number) || 1,\n price:\n (productRow[productSubKeys[\"Basket.Product.productPrice\"]] as number) ||\n 0,\n tax:\n (productRow[\n productSubKeys[\"Basket.Product.productTaxPercentage\"]\n ] as number) || 0,\n }));\n\n const total = products.reduce((sum, p) => sum + p.price * p.quantity, 0);\n const taxTotal = products.reduce(\n (sum, p) => sum + p.price * p.quantity * p.tax,\n 0\n );\n\n const completeDateTimestamp = row[keyIndices[\"Basket.completeDate\"]] as number;\n const completeDate = completeDateTimestamp\n ? new Date(completeDateTimestamp).toISOString()\n : \"\";\n\n return {\n id: row[keyIndices[\"Basket.id\"]] as number,\n name: row[keyIndices[\"Basket.name\"]] as string,\n consumerId: row[keyIndices[\"Basket.consumerId\"]] as number,\n products,\n total,\n taxTotal,\n status: \"completed\" as const,\n completeDate,\n };\n });\n}\n\n/** Unique non-null consumer ids referenced by a set of baskets. */\nexport function collectConsumerIds(baskets: ParsedBasket[]): number[] | null {\n if (!baskets.length) return null;\n const ids = baskets\n .map((b) => b.consumerId)\n .filter((id): id is number => id != null);\n return ids.length > 0 ? [...new Set(ids)] : null;\n}\n\n/** Unique product ids referenced by a set of baskets. */\nexport function collectProductIds(baskets: ParsedBasket[]): number[] | null {\n if (!baskets.length) return null;\n const ids = new Set<number>();\n baskets.forEach((basket) => {\n basket.products.forEach((product) => {\n if (product.productId) {\n ids.add(product.productId);\n }\n });\n });\n return ids.size > 0 ? [...ids] : null;\n}\n\nexport interface CombineBasketRowsParams {\n baskets: ParsedBasket[];\n consumerMap: { [id: number]: Consumer };\n productMap: { [id: number]: Product };\n search?: string;\n}\n\n/** Joins baskets with consumer + product lookups, applies search, sorts. */\nexport function combineBasketRows({\n baskets,\n consumerMap,\n productMap,\n search,\n}: CombineBasketRowsParams): BasketReportRow[] {\n if (!baskets.length) return [];\n\n let combinedRows: BasketReportRow[] = baskets.map((basket) => {\n const consumer = basket.consumerId ? consumerMap[basket.consumerId] : null;\n\n const productsWithNames: BasketItem[] = basket.products.map((p) => {\n const product = productMap[p.productId];\n return {\n ...p,\n productName: product?.name || `Product #${p.productId}`,\n };\n });\n\n return {\n id: basket.id,\n basketId: basket.id,\n basketName: basket.name || `Basket #${basket.id}`,\n consumerId: basket.consumerId,\n consumerName: consumer\n ? `${consumer.firstName} ${consumer.lastName}`\n : \"Unknown\",\n consumerEmail: consumer?.email || \"-\",\n products: productsWithNames,\n basketTotal: (basket.total || 0) / 100,\n tax: basket.taxTotal || 0,\n dateTime: basket.completeDate,\n status: basket.status || \"pending\",\n };\n });\n\n if (search) {\n const searchLower = search.toLowerCase();\n combinedRows = combinedRows.filter(\n (row) =>\n row.basketId.toString().includes(searchLower) ||\n row.basketName.toLowerCase().includes(searchLower) ||\n row.consumerName.toLowerCase().includes(searchLower) ||\n row.consumerEmail.toLowerCase().includes(searchLower)\n );\n }\n\n combinedRows.sort((a, b) => {\n const dateA = new Date(a.dateTime).getTime();\n const dateB = new Date(b.dateTime).getTime();\n return dateB - dateA;\n });\n\n return combinedRows;\n}\n\n/** Aggregate totals for a set of report rows. */\nexport function computeBasketSummary(rows: BasketReportRow[]): BasketSummary {\n const uniqueConsumerIds = new Set(rows.map((row) => row.consumerId));\n return {\n totalBaskets: rows.length,\n totalRevenue: rows.reduce((sum, row) => sum + row.basketTotal, 0),\n totalTax: rows.reduce((sum, row) => sum + row.tax, 0),\n uniqueConsumers: uniqueConsumerIds.size,\n };\n}\n\n/** Builds a consumer id → consumer lookup. */\nexport function buildConsumerMap(consumers: Consumer[] | null | undefined): {\n [id: number]: Consumer;\n} {\n const map: { [id: number]: Consumer } = {};\n if (consumers) {\n consumers.forEach((consumer) => {\n map[consumer.id] = consumer;\n });\n }\n return map;\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"index.cjs","names":["merged: Extras","headers: Record<string, string>","extras: Record<string, unknown>","extras: Record<string, unknown>","extras: Record<string, unknown>","result: { [id: number]: Product }","extras: Record<string, unknown>","DEFAULT_BASKET_FILTERS: BasketFilters","DEFAULT_PAGINATION: BasketPagination","whereConditions: string[]","keyIndices: { [key: string]: number }","productSubKeys: { [key: string]: number }","products: BasketItem[]","combinedRows: BasketReportRow[]","productsWithNames: BasketItem[]","map: { [id: number]: Consumer }"],"sources":["../../src/core/request.ts","../../src/core/category.ts","../../src/core/consumer.ts","../../src/core/employee.ts","../../src/core/product.ts","../../src/core/promotion.ts","../../src/core/store.ts","../../src/core/report.ts","../../src/core/session.ts","../../src/core/entity.ts","../../src/core/basket.ts"],"sourcesContent":["import type { apiResponse } from \"./types\";\n\n/**\n * Per-call configuration shared by every core API function.\n *\n * Nothing here is React-specific. By accepting these options per call, the\n * core layer works in the browser/Next (relative `/api/...` URLs, the default)\n * AND in Node / CLI / Vue / Angular by passing an absolute `baseUrl`, an auth\n * `token`, and/or a custom `fetch` implementation.\n */\nexport interface RequestOptions {\n /**\n * Prefixed in front of every request path. Leave undefined in the browser to\n * use relative URLs (e.g. `/api/products`). In Node/CLI pass something like\n * `https://api.shopwave.com`.\n */\n baseUrl?: string;\n\n /** OAuth/session token forwarded in the `extras.token` header. */\n token?: string;\n\n /**\n * Custom fetch implementation. Defaults to the global `fetch`. Useful for\n * Node < 18, testing, or injecting interceptors.\n */\n fetch?: typeof fetch;\n\n /** Optional abort signal for cancellation (used by the hooks layer). */\n signal?: AbortSignal;\n}\n\n/** The Shopwave API ships request metadata inside a single `extras` header. */\nexport type Extras = Record<string, unknown>;\n\nfunction resolveFetch(options: RequestOptions): typeof fetch {\n const f = options.fetch ?? (typeof fetch !== \"undefined\" ? fetch : undefined);\n if (!f) {\n throw new Error(\n \"No fetch implementation available. Pass `options.fetch` (e.g. node-fetch / undici) when running outside the browser.\"\n );\n }\n return f;\n}\n\nfunction buildExtras(extras: Extras, options: RequestOptions): Extras {\n const merged: Extras = { \"Content-Type\": \"application/json\", ...extras };\n if (options.token != null && merged.token == null) {\n merged.token = options.token;\n }\n return merged;\n}\n\n/**\n * Returns a JSON string of API errors if the response carries any, otherwise\n * `null`. Mirrors the original `!responseJson.api.message.errors` checks.\n */\nexport function getApiErrors(api?: apiResponse): string | null {\n const errors = api?.message?.errors;\n if (errors && Object.keys(errors).length > 0) {\n return JSON.stringify(errors);\n }\n return null;\n}\n\n/**\n * GET helper. Sends request metadata via the `extras` header exactly like the\n * original hooks did, parses JSON, and returns the typed payload.\n */\nexport async function apiGet<T>(\n path: string,\n extras: Extras,\n options: RequestOptions = {}\n): Promise<T> {\n const doFetch = resolveFetch(options);\n const url = (options.baseUrl ?? \"\") + path;\n\n const headers = { extras: JSON.stringify(buildExtras(extras, options)) };\n console.log('[API GET]', { url, headers });\n\n const response = await doFetch(url, {\n method: \"GET\",\n headers,\n signal: options.signal,\n });\n\n if (!response.ok) {\n throw new Error(`Network response was not ok (${response.status})`);\n }\n\n return (await response.json()) as T;\n}\n\n/**\n * JSON body helper for POST/PUT mutations.\n */\nexport async function apiSend<T>(\n path: string,\n method: \"POST\" | \"PUT\",\n payload: unknown,\n options: RequestOptions = {}\n): Promise<T> {\n const doFetch = resolveFetch(options);\n const url = (options.baseUrl ?? \"\") + path;\n\n const headers: Record<string, string> = { \"Content-Type\": \"application/json\" };\n if (options.token != null) {\n headers.token = options.token;\n }\n\n console.log(`[API ${method}]`, { url, headers, payload });\n\n const response = await doFetch(url, {\n method,\n headers,\n body: JSON.stringify(payload),\n signal: options.signal,\n });\n\n if (!response.ok) {\n throw new Error(`Failed to ${method.toLowerCase()}: ${response.statusText}`);\n }\n\n return (await response.json()) as T;\n}\n\n/**\n * DELETE helper.\n */\nexport async function apiDelete(\n path: string,\n options: RequestOptions = {}\n): Promise<void> {\n const doFetch = resolveFetch(options);\n const url = (options.baseUrl ?? \"\") + path;\n\n const headers: Record<string, string> = {};\n if (options.token != null) {\n headers.token = options.token;\n }\n\n console.log('[API DELETE]', { url, headers });\n\n const response = await doFetch(url, {\n method: \"DELETE\",\n headers,\n signal: options.signal,\n });\n\n if (!response.ok) {\n throw new Error(`Failed to delete: ${response.statusText}`);\n }\n}\n","import { apiGet, getApiErrors, type RequestOptions } from \"./request\";\nimport type { apiResponse } from \"./types\";\n\nexport interface Category {\n id: number;\n title: string;\n parentId: number | null;\n deleteDate: string;\n activeDate: string;\n type: number;\n}\n\nexport interface CategoryResponse {\n categories: {\n [key: number]: Category;\n };\n api: apiResponse;\n}\n\nexport interface FetchCategoriesParams {\n categoryIds?: Array<number>;\n /** Whether to include soft-deleted records. Defaults to `false`. */\n deleted?: boolean;\n /** Token override (also accepted via `options.token`). */\n token?: string;\n}\n\n/**\n * Fetches categories. Framework-agnostic: returns the flattened `Category[]`\n * or throws on API errors.\n */\nexport async function fetchCategories(\n params: FetchCategoriesParams = {},\n options: RequestOptions = {}\n): Promise<Category[]> {\n const extras: Record<string, unknown> = { deleted: params.deleted ?? false };\n\n if (params.categoryIds) {\n extras.categoryIds = params.categoryIds;\n }\n if (params.token) {\n extras.token = params.token;\n }\n\n const json = await apiGet<CategoryResponse>(\"/api/categories\", extras, options);\n\n const error = getApiErrors(json.api);\n if (error) {\n throw new Error(error);\n }\n\n return Object.values(json.categories ?? {});\n}\n","import { apiGet, getApiErrors, type RequestOptions } from \"./request\";\nimport type { apiResponse } from \"./types\";\n\nexport interface Consumer {\n id: number;\n activeCount: number;\n firstName: string;\n lastName: string;\n email: string;\n}\n\nexport interface ConsumerResponse {\n consumers: {\n [key: number]: Consumer;\n };\n api: apiResponse;\n}\n\n/**\n * Fetches consumers by id. Returns an empty array when no ids are supplied\n * (the original hook short-circuited in that case).\n */\nexport async function fetchConsumers(\n consumerIds: number[] | null | undefined,\n options: RequestOptions = {}\n): Promise<Consumer[]> {\n if (!consumerIds || consumerIds.length === 0) {\n return [];\n }\n\n const extras: Record<string, unknown> = { ids: consumerIds.join(\",\") };\n\n const json = await apiGet<ConsumerResponse>(\"/api/consumer\", extras, options);\n\n const error = getApiErrors(json.api);\n if (error) {\n throw new Error(error);\n }\n\n return Object.values(json.consumers ?? {});\n}\n","import { apiGet, getApiErrors, type RequestOptions } from \"./request\";\nimport type { apiResponse } from \"./types\";\n\nexport const rolesById = {\n \"1\": \"Owner\",\n \"2\": \"Manager\",\n \"3\": \"Assistant\",\n \"4\": \"Guest\",\n \"5\": \"Assistant Manager\",\n} as const;\n\nexport interface Employee {\n id: number;\n firstName: string;\n lastName: string;\n email: string;\n roleId: number;\n joinedDate: string;\n exitDate: string;\n stores: {\n [key: number]: {\n id: number;\n roleId: number;\n };\n };\n}\n\nexport interface EmployeeResponse {\n employees: {\n [key: number]: Employee;\n };\n api: apiResponse;\n}\n\nexport interface FetchEmployeesParams {\n /** Optional id filter. Forwarded when provided. */\n employeeIds?: Array<number>;\n /** Whether to include soft-deleted records. Defaults to `false`. */\n deleted?: boolean;\n}\n\n/**\n * Fetches employees. Returns the flattened `Employee[]`.\n */\nexport async function fetchEmployees(\n params: FetchEmployeesParams = {},\n options: RequestOptions = {}\n): Promise<Employee[]> {\n const extras: Record<string, unknown> = { deleted: params.deleted ?? false };\n\n if (params.employeeIds) {\n extras.employeeIds = params.employeeIds;\n }\n\n const json = await apiGet<EmployeeResponse>(\"/api/employees\", extras, options);\n\n const error = getApiErrors(json.api);\n if (error) {\n throw new Error(error);\n }\n\n return Object.values(json.employees ?? {});\n}\n","import { apiGet, getApiErrors, type RequestOptions } from \"./request\";\nimport type { apiResponse } from \"./types\";\n\nexport interface ProductInstance {\n id: number;\n price: string;\n taxPercentage: string;\n timestamp: string;\n size: string;\n name: string;\n activeDate: string;\n tags: string;\n}\n\nexport interface Product {\n id: number;\n barcode: string;\n name: string;\n details: string;\n tags: string;\n unit: number;\n timestamp: string;\n activeDate: string;\n deleteDate: string;\n images: Array<string>;\n categories: Array<number>;\n instances: {\n [key: number]: ProductInstance;\n };\n}\n\nexport interface ProductResponse {\n products: {\n [key: number]: Product;\n };\n api: apiResponse;\n}\n\nexport interface FetchProductsParams {\n productIds?: Array<number>;\n storeId?: number;\n /** Whether to include soft-deleted records. Defaults to `false`. */\n deleted?: boolean;\n /** Token override (also accepted via `options.token`). */\n token?: string;\n}\n\n/**\n * Fetches products. Returns the flattened `Product[]`.\n */\nexport async function fetchProducts(\n params: FetchProductsParams = {},\n options: RequestOptions = {}\n): Promise<Product[]> {\n const extras: Record<string, unknown> = { deleted: params.deleted ?? false };\n\n if (params.productIds) {\n extras.productIds = params.productIds;\n }\n if (params.storeId) {\n extras.storeId = params.storeId;\n }\n if (params.token) {\n extras.token = params.token;\n }\n\n const json = await apiGet<ProductResponse>(\"/api/products\", extras, options);\n\n const error = getApiErrors(json.api);\n if (error) {\n throw new Error(error);\n }\n\n return Object.values(json.products ?? {});\n}\n\n/**\n * Fetches products by id and returns them keyed by id, transparently batching\n * the request (default 200 ids per call) to stay within API limits. Extracted\n * from the original `useBasketReport` inline batching loop so it can be reused\n * outside React.\n */\nexport async function fetchProductsMap(\n productIds: number[] | null | undefined,\n options: RequestOptions = {},\n batchSize = 200\n): Promise<{ [id: number]: Product }> {\n const result: { [id: number]: Product } = {};\n if (!productIds || productIds.length === 0) {\n return result;\n }\n\n for (let i = 0; i < productIds.length; i += batchSize) {\n const batch = productIds.slice(i, i + batchSize);\n const products = await fetchProducts({ productIds: batch }, options);\n for (const product of products) {\n result[product.id] = product;\n }\n }\n\n return result;\n}\n","import { apiGet, getApiErrors, type RequestOptions } from \"./request\";\nimport type { apiResponse } from \"./types\";\n\n/** Promotion rule identifiers as stored by the Shopwave API. */\nexport enum PromotionRuleSet {\n Percentage = \"|%^/\",\n Combo = \"&%</\",\n MealDeal = \"&=^/\",\n}\n\nexport interface Promotion {\n id: number;\n merchantId: number;\n storeIds: number[];\n categoryIds: { [key: string]: number };\n productIds: { [key: string]: any };\n title: string;\n details: string | null;\n terms: string | null;\n startDate: string;\n endDate: string;\n daysAvailable: number[];\n automatic: number;\n rule: PromotionRuleSet;\n x: number;\n y: number | null;\n}\n\nexport interface PromotionResponse {\n promotions: {\n [key: number]: Promotion;\n };\n api: apiResponse;\n}\n\nexport interface FetchPromotionsParams {\n /** Optional id filter. Forwarded when provided. */\n promotionIds?: Array<number>;\n /** Whether to include soft-deleted records. Defaults to `false`. */\n deleted?: boolean;\n /** Filter for imminent promotions. */\n imminent?: boolean;\n /** Filter for active promotions. */\n active?: boolean;\n /** Filter for expired promotions. */\n expired?: boolean;\n /** Token override (also accepted via `options.token`). */\n token?: string;\n}\n\n/**\n * Fetches promotions. Returns the flattened `Promotion[]`.\n */\nexport async function fetchPromotions(\n params: FetchPromotionsParams = {},\n options: RequestOptions = {}\n): Promise<Promotion[]> {\n const extras: Record<string, unknown> = { deleted: params.deleted ?? false };\n\n if (params.promotionIds) {\n extras.promotionIds = params.promotionIds;\n }\n if (params.imminent !== undefined) {\n extras.imminent = params.imminent;\n }\n if (params.active !== undefined) {\n extras.active = params.active;\n }\n if (params.expired !== undefined) {\n extras.expired = params.expired;\n }\n if (params.token) {\n extras.token = params.token;\n }\n\n const json = await apiGet<PromotionResponse>(\"/api/promotions\", extras, options);\n\n const error = getApiErrors(json.api);\n if (error) {\n throw new Error(error);\n }\n\n return Object.values(json.promotions ?? {});\n}\n","import { apiGet, getApiErrors, type RequestOptions } from \"./request\";\nimport type { apiResponse } from \"./types\";\n\nexport interface Store {\n id: number;\n lat: number;\n lng: number;\n addressLine1: string;\n addressLine2: string | null;\n addressLine3: string;\n phoneNumber: string;\n city: string;\n postcode: string;\n countryId: number | null;\n timezoneId: number | null;\n storeDeleteDate: string | null;\n email: string | null;\n sandbox: boolean | null;\n}\n\nexport interface StoreResponse {\n stores: {\n [key: number]: Store;\n };\n api: apiResponse;\n}\n\nexport interface FetchStoresParams {\n /** Whether to include soft-deleted records. Defaults to `false`. */\n deleted?: boolean;\n}\n\n/**\n * Fetches stores. Returns the flattened `Store[]`.\n */\nexport async function fetchStores(\n params: FetchStoresParams = {},\n options: RequestOptions = {}\n): Promise<Store[]> {\n const extras: Record<string, unknown> = { deleted: params.deleted ?? false };\n\n const json = await apiGet<StoreResponse>(\"/api/stores\", extras, options);\n\n const error = getApiErrors(json.api);\n if (error) {\n throw new Error(error);\n }\n\n return Object.values(json.stores ?? {});\n}\n","import { apiGet, getApiErrors, type RequestOptions } from \"./request\";\nimport type { apiResponse } from \"./types\";\n\nexport interface ReportKey {\n name: string;\n type: string;\n item?: ReportKey[];\n}\n\nexport interface ReportData {\n keys: ReportKey[];\n data: any[][];\n}\n\nexport interface ReportResponse {\n reports: {\n [key: string]: ReportData;\n };\n api: apiResponse;\n}\n\nexport interface ReportQuery {\n FROM: string;\n WHERE?: {\n AND?: string[];\n OR?: string[];\n };\n SELECT?: string[];\n LIMIT?: number;\n OFFSET?: number;\n}\n\nexport interface ReportQueryMap {\n [key: string]: ReportQuery;\n}\n\n/**\n * Runs one or more report queries and returns the `reports` map keyed by query\n * name. The query is JSON-encoded into the `extras.query` header, matching the\n * original endpoint contract.\n */\nexport async function fetchReport(\n query: ReportQueryMap,\n options: RequestOptions = {}\n): Promise<ReportResponse[\"reports\"]> {\n const extras: Record<string, unknown> = { query: JSON.stringify(query) };\n\n const json = await apiGet<ReportResponse>(\"/api/report\", extras, options);\n\n const error = getApiErrors(json.api);\n if (error) {\n throw new Error(error);\n }\n\n return json.reports ?? {};\n}\n","import { apiDelete, type RequestOptions } from \"./request\";\n\n/**\n * Login status returned by the app's `/api/session` route. It deliberately\n * never contains tokens — those stay in the encrypted httpOnly cookie.\n */\nexport interface SessionStatus {\n loggedIn: boolean;\n /** Access-token expiry (epoch ms), when known. */\n expiresAt?: number;\n}\n\n/**\n * Reads the current login status from `GET /api/session`.\n *\n * Works with the new `react-shopwave-connect/next` session route\n * (`{ loggedIn }`) and with older routes that returned the raw session\n * (`{ token: {...} }`) — in that case only a boolean is surfaced.\n */\nexport async function fetchSession(options: RequestOptions = {}): Promise<SessionStatus> {\n const doFetch = options.fetch ?? (typeof fetch !== \"undefined\" ? fetch : undefined);\n if (!doFetch) {\n throw new Error(\"No fetch implementation available. Pass `options.fetch`.\");\n }\n\n const response = await doFetch(`${options.baseUrl ?? \"\"}/api/session`, {\n method: \"GET\",\n headers: { Accept: \"application/json\" },\n cache: \"no-store\",\n credentials: \"same-origin\",\n signal: options.signal,\n } as RequestInit);\n\n if (response.status === 401) return { loggedIn: false };\n if (!response.ok) {\n throw new Error(`Session request failed (${response.status})`);\n }\n\n const json = (await response.json()) as Record<string, unknown> | null;\n if (json && typeof json.loggedIn === \"boolean\") {\n return {\n loggedIn: json.loggedIn,\n expiresAt: typeof json.expiresAt === \"number\" ? json.expiresAt : undefined,\n };\n }\n // Legacy route that returns the whole session: never pass tokens through.\n return { loggedIn: Boolean(json && (json as { token?: unknown }).token) };\n}\n\n/**\n * Path that starts a login and returns to `returnTo` afterwards.\n * Navigate the browser to it (`window.location.href = loginPath(...)`).\n */\nexport function loginPath(returnTo?: string, authPath = \"/auth\"): string {\n return returnTo ? `${authPath}?returnTo=${encodeURIComponent(returnTo)}` : authPath;\n}\n\n/** Path that logs out of the app and the Shopwave auth server. */\nexport function logoutPath(authPath = \"/auth\"): string {\n return `${authPath}/logout`;\n}\n\n/**\n * Ends the current server session (no redirect).\n *\n * Prefer navigating to {@link logoutPath} when using\n * `react-shopwave-connect/next`: it also signs the user out of the auth server.\n */\nexport async function logout(options: RequestOptions = {}): Promise<void> {\n await apiDelete(\"/api/session?action=logout\", options);\n}\n","import { apiDelete, apiSend, type RequestOptions } from \"./request\";\n\n/**\n * Generic CRUD-style mutations extracted from `useHandleDelete` /\n * `useHandleSubmit`. The original hooks also surfaced notistack snackbars; that\n * UI concern is deliberately left to the hooks/host layer so core stays\n * dependency-free.\n */\n\n/**\n * Deletes `/api/{resourcePath}/{entityId}`.\n */\nexport async function deleteEntity(\n resourcePath: string,\n entityId: number | string,\n options: RequestOptions = {}\n): Promise<void> {\n if (entityId == null || entityId === \"\") {\n throw new Error(\"entityId is missing\");\n }\n await apiDelete(`/api/${resourcePath}/${entityId}`, options);\n}\n\nexport interface SubmitEntityArgs {\n /** Fully-resolved endpoint, e.g. `/api/products` or `/api/products/42`. */\n endpoint: string;\n /** Defaults to PUT when an `entityId` exists, otherwise POST. */\n method?: \"POST\" | \"PUT\";\n /** The request payload (already transformed by the caller). */\n payload: unknown;\n}\n\nexport interface SubmitEntityResponse<R = any> {\n result?: R;\n [key: string]: unknown;\n}\n\n/**\n * Sends a create/update mutation and returns the parsed `result` field\n * (matching the original `result.result` access).\n */\nexport async function submitEntity<R = any>(\n { endpoint, method = \"POST\", payload }: SubmitEntityArgs,\n options: RequestOptions = {}\n): Promise<R | undefined> {\n const json = await apiSend<SubmitEntityResponse<R>>(\n endpoint,\n method,\n payload,\n options\n );\n return json.result;\n}\n","import type { Consumer } from \"./consumer\";\nimport type { Product } from \"./product\";\nimport type { ReportQueryMap, ReportResponse } from \"./report\";\n\n// ── Basket Report Types (preserved from the original `types/basket.ts`) ──────\n\nexport interface BasketItem {\n productId: number;\n productName: string;\n quantity: number;\n price: number;\n tax: number;\n}\n\nexport interface Basket {\n id: number;\n name: string;\n consumerId: number;\n products: BasketItem[];\n total: number;\n taxTotal: number;\n status: \"pending\" | \"completed\" | \"cancelled\";\n completeDate: string;\n}\n\nexport interface BasketReportRow {\n id: number;\n basketId: number;\n basketName: string;\n consumerId: number;\n consumerName: string;\n consumerEmail: string;\n products: BasketItem[];\n basketTotal: number;\n tax: number;\n dateTime: string;\n status: string;\n}\n\nexport interface BasketSummary {\n totalBaskets: number;\n totalRevenue: number;\n totalTax: number;\n uniqueConsumers: number;\n}\n\nexport interface BasketFilters {\n search: string;\n dateRange: {\n start: Date | null;\n end: Date | null;\n };\n consumerId: number | null;\n status: \"all\" | \"pending\" | \"completed\" | \"cancelled\";\n}\n\nexport interface BasketPagination {\n page: number;\n pageSize: number;\n total: number;\n}\n\nexport const DEFAULT_BASKET_FILTERS: BasketFilters = {\n search: \"\",\n dateRange: {\n start: null,\n end: null,\n },\n consumerId: null,\n status: \"all\",\n};\n\nexport const DEFAULT_PAGINATION: BasketPagination = {\n page: 0,\n pageSize: 25,\n total: 0,\n};\n\n/** Intermediate shape produced by {@link parseBasketReportData}. */\nexport interface ParsedBasket {\n id: number;\n name: string;\n consumerId: number;\n products: BasketItem[];\n total: number;\n taxTotal: number;\n status: \"completed\";\n completeDate: string;\n}\n\n// ── Pure transform helpers (extracted from the original `useBasketReport`) ────\n\n/** Formats a date as `YYYY-MM-DD HH:mm:ss` for the report query. */\nexport function formatDateForQuery(date: Date, isEndOfDay = false): string {\n const year = date.getFullYear();\n const month = String(date.getMonth() + 1).padStart(2, \"0\");\n const day = String(date.getDate()).padStart(2, \"0\");\n const time = isEndOfDay ? \"23:59:59\" : \"00:00:00\";\n return `${year}-${month}-${day} ${time}`;\n}\n\n/** Builds the `Basket` report query from a set of filters. */\nexport function buildBasketReportQuery(filters: BasketFilters): ReportQueryMap {\n const whereConditions: string[] = [];\n\n const today = new Date();\n const startDate = filters.dateRange.start ?? today;\n const endDate = filters.dateRange.end ?? today;\n\n whereConditions.push(\n `Basket.completeDate > '${formatDateForQuery(startDate, false)}'`\n );\n whereConditions.push(\n `Basket.completeDate < '${formatDateForQuery(endDate, true)}'`\n );\n\n if (filters.consumerId) {\n whereConditions.push(`Basket.consumerId = ${filters.consumerId}`);\n }\n\n if (filters.status !== \"all\") {\n whereConditions.push(`Basket.status = '${filters.status}'`);\n }\n\n return {\n baskets: {\n FROM: \"Basket\",\n WHERE: whereConditions.length > 0 ? { AND: whereConditions } : undefined,\n },\n };\n}\n\n/** Flattens the raw report rows/keys into structured baskets. */\nexport function parseBasketReportData(\n reports: ReportResponse[\"reports\"] | null | undefined\n): ParsedBasket[] {\n if (!reports?.baskets?.data || !reports?.baskets?.keys) {\n return [];\n }\n\n const { keys, data } = reports.baskets;\n\n const keyIndices: { [key: string]: number } = {};\n keys.forEach((key, index) => {\n keyIndices[key.name] = index;\n });\n\n const productKey = keys.find((k) => k.name === \"Basket.Product\");\n const productSubKeys: { [key: string]: number } = {};\n if (productKey?.item) {\n productKey.item.forEach((subKey, index) => {\n productSubKeys[subKey.name] = index;\n });\n }\n\n return data.map((row) => {\n const productsArray = (row[keyIndices[\"Basket.Product\"]] as any[][]) || [];\n const products: BasketItem[] = productsArray.map((productRow) => ({\n productId: productRow[productSubKeys[\"Basket.Product.productId\"]] as number,\n productName: `Product #${productRow[productSubKeys[\"Basket.Product.productId\"]]}`,\n quantity:\n (productRow[productSubKeys[\"Basket.Product.quantity\"]] as number) || 1,\n price:\n (productRow[productSubKeys[\"Basket.Product.productPrice\"]] as number) ||\n 0,\n tax:\n (productRow[\n productSubKeys[\"Basket.Product.productTaxPercentage\"]\n ] as number) || 0,\n }));\n\n const total = products.reduce((sum, p) => sum + p.price * p.quantity, 0);\n const taxTotal = products.reduce(\n (sum, p) => sum + p.price * p.quantity * p.tax,\n 0\n );\n\n const completeDateTimestamp = row[keyIndices[\"Basket.completeDate\"]] as number;\n const completeDate = completeDateTimestamp\n ? new Date(completeDateTimestamp).toISOString()\n : \"\";\n\n return {\n id: row[keyIndices[\"Basket.id\"]] as number,\n name: row[keyIndices[\"Basket.name\"]] as string,\n consumerId: row[keyIndices[\"Basket.consumerId\"]] as number,\n products,\n total,\n taxTotal,\n status: \"completed\" as const,\n completeDate,\n };\n });\n}\n\n/** Unique non-null consumer ids referenced by a set of baskets. */\nexport function collectConsumerIds(baskets: ParsedBasket[]): number[] | null {\n if (!baskets.length) return null;\n const ids = baskets\n .map((b) => b.consumerId)\n .filter((id): id is number => id != null);\n return ids.length > 0 ? [...new Set(ids)] : null;\n}\n\n/** Unique product ids referenced by a set of baskets. */\nexport function collectProductIds(baskets: ParsedBasket[]): number[] | null {\n if (!baskets.length) return null;\n const ids = new Set<number>();\n baskets.forEach((basket) => {\n basket.products.forEach((product) => {\n if (product.productId) {\n ids.add(product.productId);\n }\n });\n });\n return ids.size > 0 ? [...ids] : null;\n}\n\nexport interface CombineBasketRowsParams {\n baskets: ParsedBasket[];\n consumerMap: { [id: number]: Consumer };\n productMap: { [id: number]: Product };\n search?: string;\n}\n\n/** Joins baskets with consumer + product lookups, applies search, sorts. */\nexport function combineBasketRows({\n baskets,\n consumerMap,\n productMap,\n search,\n}: CombineBasketRowsParams): BasketReportRow[] {\n if (!baskets.length) return [];\n\n let combinedRows: BasketReportRow[] = baskets.map((basket) => {\n const consumer = basket.consumerId ? consumerMap[basket.consumerId] : null;\n\n const productsWithNames: BasketItem[] = basket.products.map((p) => {\n const product = productMap[p.productId];\n return {\n ...p,\n productName: product?.name || `Product #${p.productId}`,\n };\n });\n\n return {\n id: basket.id,\n basketId: basket.id,\n basketName: basket.name || `Basket #${basket.id}`,\n consumerId: basket.consumerId,\n consumerName: consumer\n ? `${consumer.firstName} ${consumer.lastName}`\n : \"Unknown\",\n consumerEmail: consumer?.email || \"-\",\n products: productsWithNames,\n basketTotal: (basket.total || 0) / 100,\n tax: basket.taxTotal || 0,\n dateTime: basket.completeDate,\n status: basket.status || \"pending\",\n };\n });\n\n if (search) {\n const searchLower = search.toLowerCase();\n combinedRows = combinedRows.filter(\n (row) =>\n row.basketId.toString().includes(searchLower) ||\n row.basketName.toLowerCase().includes(searchLower) ||\n row.consumerName.toLowerCase().includes(searchLower) ||\n row.consumerEmail.toLowerCase().includes(searchLower)\n );\n }\n\n combinedRows.sort((a, b) => {\n const dateA = new Date(a.dateTime).getTime();\n const dateB = new Date(b.dateTime).getTime();\n return dateB - dateA;\n });\n\n return combinedRows;\n}\n\n/** Aggregate totals for a set of report rows. */\nexport function computeBasketSummary(rows: BasketReportRow[]): BasketSummary {\n const uniqueConsumerIds = new Set(rows.map((row) => row.consumerId));\n return {\n totalBaskets: rows.length,\n totalRevenue: rows.reduce((sum, row) => sum + row.basketTotal, 0),\n totalTax: rows.reduce((sum, row) => sum + row.tax, 0),\n uniqueConsumers: uniqueConsumerIds.size,\n };\n}\n\n/** Builds a consumer id → consumer lookup. */\nexport function buildConsumerMap(consumers: Consumer[] | null | undefined): {\n [id: number]: Consumer;\n} {\n const map: { [id: number]: Consumer } = {};\n if (consumers) {\n consumers.forEach((consumer) => {\n map[consumer.id] = consumer;\n });\n }\n return map;\n}\n"],"mappings":"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"}
@@ -259,6 +259,12 @@ interface FetchPromotionsParams {
259
259
  promotionIds?: Array<number>;
260
260
  /** Whether to include soft-deleted records. Defaults to `false`. */
261
261
  deleted?: boolean;
262
+ /** Filter for imminent promotions. */
263
+ imminent?: boolean;
264
+ /** Filter for active promotions. */
265
+ active?: boolean;
266
+ /** Filter for expired promotions. */
267
+ expired?: boolean;
262
268
  /** Token override (also accepted via `options.token`). */
263
269
  token?: string;
264
270
  }
@@ -337,12 +343,34 @@ declare function fetchReport(query: ReportQueryMap, options?: RequestOptions): P
337
343
  //#endregion
338
344
  //#region src/core/session.d.ts
339
345
  /**
340
- * Ends the current server session.
346
+ * Login status returned by the app's `/api/session` route. It deliberately
347
+ * never contains tokens — those stay in the encrypted httpOnly cookie.
348
+ */
349
+ interface SessionStatus {
350
+ loggedIn: boolean;
351
+ /** Access-token expiry (epoch ms), when known. */
352
+ expiresAt?: number;
353
+ }
354
+ /**
355
+ * Reads the current login status from `GET /api/session`.
356
+ *
357
+ * Works with the new `react-shopwave-connect/next` session route
358
+ * (`{ loggedIn }`) and with older routes that returned the raw session
359
+ * (`{ token: {...} }`) — in that case only a boolean is surfaced.
360
+ */
361
+ declare function fetchSession(options?: RequestOptions): Promise<SessionStatus>;
362
+ /**
363
+ * Path that starts a login and returns to `returnTo` afterwards.
364
+ * Navigate the browser to it (`window.location.href = loginPath(...)`).
365
+ */
366
+ declare function loginPath(returnTo?: string, authPath?: string): string;
367
+ /** Path that logs out of the app and the Shopwave auth server. */
368
+ declare function logoutPath(authPath?: string): string;
369
+ /**
370
+ * Ends the current server session (no redirect).
341
371
  *
342
- * This is the framework-agnostic half of the original `useLogout` hook. The
343
- * Next.js-specific redirect logic (router.push to the auth server logout URL,
344
- * built from server actions) is intentionally NOT included here because it is
345
- * tied to `next/navigation` and `'use server'` actions — see the README.
372
+ * Prefer navigating to {@link logoutPath} when using
373
+ * `react-shopwave-connect/next`: it also signs the user out of the auth server.
346
374
  */
347
375
  declare function logout(options?: RequestOptions): Promise<void>;
348
376
  //#endregion
@@ -477,5 +505,5 @@ declare function buildConsumerMap(consumers: Consumer[] | null | undefined): {
477
505
  [id: number]: Consumer;
478
506
  };
479
507
  //#endregion
480
- export { Basket, BasketFilters, BasketItem, BasketPagination, BasketReportRow, BasketSummary, Category, CategoryResponse, CombineBasketRowsParams, Consumer, ConsumerResponse, DEFAULT_BASKET_FILTERS, DEFAULT_PAGINATION, Employee, EmployeeResponse, ErrorMessage, Extras, FetchCategoriesParams, FetchEmployeesParams, FetchProductsParams, FetchPromotionsParams, FetchStoresParams, MessageInfo, ParsedBasket, Product, ProductInstance, ProductResponse, Promotion, PromotionResponse, PromotionRuleSet, ReportData, ReportKey, ReportQuery, ReportQueryMap, ReportResponse, RequestOptions, Store, StoreResponse, SubmitEntityArgs, SubmitEntityResponse, SuccessMessage, apiDelete, apiGet, apiResponse, apiSend, buildBasketReportQuery, buildConsumerMap, collectConsumerIds, collectProductIds, combineBasketRows, computeBasketSummary, deleteEntity, fetchCategories, fetchConsumers, fetchEmployees, fetchProducts, fetchProductsMap, fetchPromotions, fetchReport, fetchStores, formatDateForQuery, getApiErrors, logout, parseBasketReportData, rolesById, submitEntity };
508
+ export { Basket, BasketFilters, BasketItem, BasketPagination, BasketReportRow, BasketSummary, Category, CategoryResponse, CombineBasketRowsParams, Consumer, ConsumerResponse, DEFAULT_BASKET_FILTERS, DEFAULT_PAGINATION, Employee, EmployeeResponse, ErrorMessage, Extras, FetchCategoriesParams, FetchEmployeesParams, FetchProductsParams, FetchPromotionsParams, FetchStoresParams, MessageInfo, ParsedBasket, Product, ProductInstance, ProductResponse, Promotion, PromotionResponse, PromotionRuleSet, ReportData, ReportKey, ReportQuery, ReportQueryMap, ReportResponse, RequestOptions, SessionStatus, Store, StoreResponse, SubmitEntityArgs, SubmitEntityResponse, SuccessMessage, apiDelete, apiGet, apiResponse, apiSend, buildBasketReportQuery, buildConsumerMap, collectConsumerIds, collectProductIds, combineBasketRows, computeBasketSummary, deleteEntity, fetchCategories, fetchConsumers, fetchEmployees, fetchProducts, fetchProductsMap, fetchPromotions, fetchReport, fetchSession, fetchStores, formatDateForQuery, getApiErrors, loginPath, logout, logoutPath, parseBasketReportData, rolesById, submitEntity };
481
509
  //# sourceMappingURL=index.d.cts.map
@@ -259,6 +259,12 @@ interface FetchPromotionsParams {
259
259
  promotionIds?: Array<number>;
260
260
  /** Whether to include soft-deleted records. Defaults to `false`. */
261
261
  deleted?: boolean;
262
+ /** Filter for imminent promotions. */
263
+ imminent?: boolean;
264
+ /** Filter for active promotions. */
265
+ active?: boolean;
266
+ /** Filter for expired promotions. */
267
+ expired?: boolean;
262
268
  /** Token override (also accepted via `options.token`). */
263
269
  token?: string;
264
270
  }
@@ -337,12 +343,34 @@ declare function fetchReport(query: ReportQueryMap, options?: RequestOptions): P
337
343
  //#endregion
338
344
  //#region src/core/session.d.ts
339
345
  /**
340
- * Ends the current server session.
346
+ * Login status returned by the app's `/api/session` route. It deliberately
347
+ * never contains tokens — those stay in the encrypted httpOnly cookie.
348
+ */
349
+ interface SessionStatus {
350
+ loggedIn: boolean;
351
+ /** Access-token expiry (epoch ms), when known. */
352
+ expiresAt?: number;
353
+ }
354
+ /**
355
+ * Reads the current login status from `GET /api/session`.
356
+ *
357
+ * Works with the new `react-shopwave-connect/next` session route
358
+ * (`{ loggedIn }`) and with older routes that returned the raw session
359
+ * (`{ token: {...} }`) — in that case only a boolean is surfaced.
360
+ */
361
+ declare function fetchSession(options?: RequestOptions): Promise<SessionStatus>;
362
+ /**
363
+ * Path that starts a login and returns to `returnTo` afterwards.
364
+ * Navigate the browser to it (`window.location.href = loginPath(...)`).
365
+ */
366
+ declare function loginPath(returnTo?: string, authPath?: string): string;
367
+ /** Path that logs out of the app and the Shopwave auth server. */
368
+ declare function logoutPath(authPath?: string): string;
369
+ /**
370
+ * Ends the current server session (no redirect).
341
371
  *
342
- * This is the framework-agnostic half of the original `useLogout` hook. The
343
- * Next.js-specific redirect logic (router.push to the auth server logout URL,
344
- * built from server actions) is intentionally NOT included here because it is
345
- * tied to `next/navigation` and `'use server'` actions — see the README.
372
+ * Prefer navigating to {@link logoutPath} when using
373
+ * `react-shopwave-connect/next`: it also signs the user out of the auth server.
346
374
  */
347
375
  declare function logout(options?: RequestOptions): Promise<void>;
348
376
  //#endregion
@@ -477,5 +505,5 @@ declare function buildConsumerMap(consumers: Consumer[] | null | undefined): {
477
505
  [id: number]: Consumer;
478
506
  };
479
507
  //#endregion
480
- export { Basket, BasketFilters, BasketItem, BasketPagination, BasketReportRow, BasketSummary, Category, CategoryResponse, CombineBasketRowsParams, Consumer, ConsumerResponse, DEFAULT_BASKET_FILTERS, DEFAULT_PAGINATION, Employee, EmployeeResponse, ErrorMessage, Extras, FetchCategoriesParams, FetchEmployeesParams, FetchProductsParams, FetchPromotionsParams, FetchStoresParams, MessageInfo, ParsedBasket, Product, ProductInstance, ProductResponse, Promotion, PromotionResponse, PromotionRuleSet, ReportData, ReportKey, ReportQuery, ReportQueryMap, ReportResponse, RequestOptions, Store, StoreResponse, SubmitEntityArgs, SubmitEntityResponse, SuccessMessage, apiDelete, apiGet, apiResponse, apiSend, buildBasketReportQuery, buildConsumerMap, collectConsumerIds, collectProductIds, combineBasketRows, computeBasketSummary, deleteEntity, fetchCategories, fetchConsumers, fetchEmployees, fetchProducts, fetchProductsMap, fetchPromotions, fetchReport, fetchStores, formatDateForQuery, getApiErrors, logout, parseBasketReportData, rolesById, submitEntity };
508
+ export { Basket, BasketFilters, BasketItem, BasketPagination, BasketReportRow, BasketSummary, Category, CategoryResponse, CombineBasketRowsParams, Consumer, ConsumerResponse, DEFAULT_BASKET_FILTERS, DEFAULT_PAGINATION, Employee, EmployeeResponse, ErrorMessage, Extras, FetchCategoriesParams, FetchEmployeesParams, FetchProductsParams, FetchPromotionsParams, FetchStoresParams, MessageInfo, ParsedBasket, Product, ProductInstance, ProductResponse, Promotion, PromotionResponse, PromotionRuleSet, ReportData, ReportKey, ReportQuery, ReportQueryMap, ReportResponse, RequestOptions, SessionStatus, Store, StoreResponse, SubmitEntityArgs, SubmitEntityResponse, SuccessMessage, apiDelete, apiGet, apiResponse, apiSend, buildBasketReportQuery, buildConsumerMap, collectConsumerIds, collectProductIds, combineBasketRows, computeBasketSummary, deleteEntity, fetchCategories, fetchConsumers, fetchEmployees, fetchProducts, fetchProductsMap, fetchPromotions, fetchReport, fetchSession, fetchStores, formatDateForQuery, getApiErrors, loginPath, logout, logoutPath, parseBasketReportData, rolesById, submitEntity };
481
509
  //# sourceMappingURL=index.d.ts.map