react-shopwave-connect 0.4.3 → 0.4.5

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.
@@ -1 +1 @@
1
- {"version":3,"file":"index.cjs","names":["headers: Record<string, string>","merged: Extras","response: Response","extras: Record<string, unknown>","extras: Record<string, unknown>","extras: Record<string, unknown>","result: { [id: number]: Product }","SHOPWAVE_RESOURCES: Readonly<Record<ResourceKind, ResourceDefinition>>","UPLOAD_KINDS: Readonly<Record<UploadKind, string>>","SHOPWAVE_LOOKUPS: Readonly<Record<LookupKind, LookupDefinition>>","list: unknown[]","out: string[]","extras: Record<string, unknown>","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 }","blob: Blob","MERCHANT_IMAGE_SLOTS: Readonly<Record<MerchantImageSlot, MerchantImageSlotDefinition>>","out: Record<string, unknown>","MERCHANT_LINK_PATHS: Readonly<Record<MerchantLinkKey, readonly [string, string]>>","MERCHANT_OBJECT_FIELDS: ReadonlyArray<string>","MERCHANT_READ_ONLY_FIELDS: ReadonlyArray<string>","MERCHANT_FIELD_MAX_LENGTH: Readonly<Record<string, number>>","problems: string[]","merchant: Record<string, unknown>","useReport","useConsumer","rows: BasketReportRow[]","summary: BasketSummary"],"sources":["../../src/core/errors.ts","../../src/core/request.ts","../../src/core/category.ts","../../src/core/consumer.ts","../../src/core/employee.ts","../../src/core/product.ts","../../src/core/resources.ts","../../src/core/productTags.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","../../src/core/imageFit.ts","../../src/core/upload.ts","../../src/core/merchantImages.ts","../../src/core/merchant.ts","../../src/core/user.ts","../../src/hooks/shared.ts","../../src/hooks/useCategory.ts","../../src/hooks/useConsumer.ts","../../src/hooks/useEmployee.ts","../../src/hooks/useProduct.ts","../../src/hooks/useProductTags.ts","../../src/hooks/usePromotion.ts","../../src/hooks/useStore.ts","../../src/hooks/useReport.ts","../../src/hooks/useDelete.ts","../../src/hooks/useSubmit.ts","../../src/hooks/useLogout.ts","../../src/hooks/useSession.ts","../../src/hooks/useMerchant.ts","../../src/hooks/useUser.ts","../../src/hooks/useUploadImage.ts","../../src/hooks/useBasketReport.ts"],"sourcesContent":["import type { ErrorMessage } from \"./types\";\n\n/** Map of Shopwave API errors as found in `api.message.errors`, keyed by error code. */\nexport type ShopwaveErrorMap = Record<string, Partial<ErrorMessage>>;\n\n/**\n * Thrown by every `core` function when a request fails, either at the HTTP\n * level (`status` is the HTTP status) or because the Shopwave response carried\n * `api.message.errors` (then `status` is the HTTP status of that response,\n * usually 200/201, and `errors` holds the API errors).\n *\n * Branch on `status` (e.g. `isUnauthorized` → send the user to log in) rather\n * than parsing `message`.\n */\nexport class ShopwaveApiError extends Error {\n /** HTTP status of the response (0 when the request never got a response). */\n readonly status: number;\n /** Shopwave API errors, when the response carried any. */\n readonly errors: ShopwaveErrorMap | null;\n /** Parsed response body, when there was one. */\n readonly body?: unknown;\n\n constructor(\n status: number,\n message: string,\n details: { errors?: ShopwaveErrorMap | null; body?: unknown; cause?: unknown } = {}\n ) {\n super(message, details.cause !== undefined ? { cause: details.cause } : undefined);\n this.name = \"ShopwaveApiError\";\n this.status = status;\n this.errors = details.errors ?? null;\n this.body = details.body;\n }\n\n /** 401, or the Shopwave \"token expired / invalid\" error 908. */\n get isUnauthorized(): boolean {\n if (this.status === 401) return true;\n return !!this.errors && Object.entries(this.errors).some(([k, v]) => k === \"908\" || Number(v?.id) === 908);\n }\n\n get isNotFound(): boolean {\n return this.status === 404;\n }\n}\n\n/** The `api.message.errors` (or `api.message.error`) map of a Shopwave response, or `null` when there are none. */\nexport function getApiErrorMap(body: unknown): ShopwaveErrorMap | null {\n // The API reference names the map `error`; responses seen so far use `errors`.\n const message = (body as { api?: { message?: { errors?: unknown; error?: unknown } } } | null)?.api?.message;\n const errors = message?.errors ?? message?.error;\n if (errors && typeof errors === \"object\" && Object.keys(errors).length > 0) {\n return errors as ShopwaveErrorMap;\n }\n return null;\n}\n\nfunction describeErrors(errors: ShopwaveErrorMap): string {\n return Object.entries(errors)\n .map(([code, e]) => {\n const text = [e?.title, e?.details].filter(Boolean).join(\": \");\n return text ? `${code} ${text}` : code;\n })\n .join(\"; \");\n}\n\n/**\n * Builds a {@link ShopwaveApiError} for a response. Understands the Shopwave\n * envelope (`api.message.errors`) and the `{ error, message }` bodies returned\n * by the `react-shopwave-connect/next` routes.\n */\nexport function toShopwaveApiError(status: number, body: unknown, fallback?: string): ShopwaveApiError {\n const errors = getApiErrorMap(body);\n let detail = errors ? describeErrors(errors) : \"\";\n if (!detail && body && typeof body === \"object\") {\n const b = body as { message?: unknown; error?: unknown };\n if (typeof b.message === \"string\") detail = b.message;\n else if (typeof b.error === \"string\") detail = b.error;\n }\n if (!detail) detail = fallback ?? (status >= 400 ? \"request failed\" : \"API returned errors\");\n return new ShopwaveApiError(status, `Shopwave API error (${status}): ${detail}`, { errors, body });\n}\n\n/** Throws a {@link ShopwaveApiError} when the response body carries API errors. */\nexport function assertNoApiErrors(body: unknown, status: number): void {\n if (getApiErrorMap(body)) throw toShopwaveApiError(status, body);\n}\n","import type { apiResponse } from \"./types\";\nimport { ShopwaveApiError, getApiErrorMap, toShopwaveApiError } from \"./errors\";\n\n/**\n * Per-call configuration shared by every core API function.\n *\n * Nothing here is React-specific. In the browser/Next leave `baseUrl` empty to\n * call the app's own `/api/...` routes (which hold the session). From Node, a\n * CLI or tests, pass an absolute `baseUrl` pointing at such an app plus a\n * `token`.\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`). From Node pass the app origin,\n * e.g. `http://localhost:3000`.\n */\n baseUrl?: string;\n\n /**\n * Shopwave OAuth access token. Sent as `Authorization: OAuth <token>` on every\n * request (GET, POST, PUT and DELETE alike). A value that already starts with\n * a scheme (`OAuth …` / `Bearer …`) is sent as-is. Leave unset in the browser:\n * the app's routes use the session cookie.\n */\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\n/** Options with `token` filled in from a legacy per-call `params.token`, unless `options.token` is set. */\nexport function withToken(options: RequestOptions, token: string | undefined): RequestOptions {\n return token && !options.token ? { ...options, token } : options;\n}\n\n/** `Authorization` header value for a token (`OAuth <token>` unless it already has a scheme). */\nexport function authorizationFor(token: string): string {\n const t = token.trim();\n return /^(oauth|bearer)\\s+\\S/i.test(t) ? t : `OAuth ${t}`;\n}\n\n/** Headers shared by every request: Accept + the token, when one is given. */\nfunction baseHeaders(options: RequestOptions): Record<string, string> {\n const headers: Record<string, string> = { Accept: \"application/json\" };\n if (options.token) headers.Authorization = authorizationFor(options.token);\n return headers;\n}\n\nfunction buildExtras(extras: Extras): Extras {\n const merged: Extras = { \"Content-Type\": \"application/json\", ...extras };\n // Tokens never travel inside extras any more (see RequestOptions.token).\n delete merged.token;\n return merged;\n}\n\n/** Parses a response body as JSON; `null` for an empty body, `{ raw }` for non-JSON. */\nasync function readBody(response: Response): Promise<unknown> {\n const text = await response.text();\n if (!text) return null;\n try {\n return JSON.parse(text);\n } catch {\n return { raw: text };\n }\n}\n\n/**\n * Low-level request: resolves `{ status, body }` for 2xx responses and throws\n * {@link ShopwaveApiError} otherwise. `init.headers` are merged over the\n * standard ones (Accept + Authorization).\n */\nexport async function apiRequest(\n path: string,\n init: RequestInit,\n options: RequestOptions = {}\n): Promise<{ status: number; body: unknown }> {\n return send(path, { ...init, headers: { ...baseHeaders(options), ...(init.headers as Record<string, string>) } }, options);\n}\n\nasync function send(path: string, init: RequestInit, options: RequestOptions): Promise<{ status: number; body: unknown }> {\n const doFetch = resolveFetch(options);\n const url = (options.baseUrl ?? \"\") + path;\n\n let response: Response;\n try {\n response = await doFetch(url, { ...init, signal: options.signal });\n } catch (error) {\n if ((error as { name?: string })?.name === \"AbortError\") throw error;\n throw new ShopwaveApiError(0, `Shopwave API request failed: ${(error as Error)?.message ?? error}`, { cause: error });\n }\n\n const body = await readBody(response);\n if (!response.ok) {\n throw toShopwaveApiError(response.status, body, response.statusText || undefined);\n }\n return { status: response.status, body };\n}\n\n/**\n * Returns a JSON string of API errors if the response carries any, otherwise\n * `null`. Kept for backwards compatibility; prefer `ShopwaveApiError`.\n */\nexport function getApiErrors(api?: apiResponse): string | null {\n const errors = getApiErrorMap({ api });\n return errors ? JSON.stringify(errors) : null;\n}\n\n/**\n * GET helper. Sends request metadata via the `extras` header, parses JSON and\n * returns the payload. Throws {@link ShopwaveApiError} for non-2xx responses.\n */\nexport async function apiGet<T>(path: string, extras: Extras, options: RequestOptions = {}): Promise<T> {\n const { body } = await send(\n path,\n {\n method: \"GET\",\n headers: { ...baseHeaders(options), extras: JSON.stringify(buildExtras(extras)) },\n },\n options\n );\n return body as T;\n}\n\n/**\n * JSON body helper for POST/PUT mutations. Returns the parsed body (`null` for\n * an empty one). Throws {@link ShopwaveApiError} for non-2xx responses.\n */\nexport async function apiSend<T>(\n path: string,\n method: \"POST\" | \"PUT\",\n payload: unknown,\n options: RequestOptions = {}\n): Promise<T> {\n const { body } = await send(\n path,\n {\n method,\n headers: { ...baseHeaders(options), \"Content-Type\": \"application/json\" },\n body: JSON.stringify(payload),\n },\n options\n );\n return body as T;\n}\n\n/**\n * DELETE helper. Shopwave answers deletes with 205 and an empty body; if a body\n * does come back and carries API errors, they are thrown. Returns the parsed\n * body (`null` when empty).\n */\nexport async function apiDelete(path: string, options: RequestOptions = {}): Promise<unknown> {\n const { status, body } = await send(path, { method: \"DELETE\", headers: baseHeaders(options) }, options);\n if (getApiErrorMap(body)) throw toShopwaveApiError(status, body);\n return body;\n}\n","import { apiGet, withToken, type RequestOptions } from \"./request\";\nimport { assertNoApiErrors } from \"./errors\";\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 /** @deprecated Pass `options.token` instead. Sent the same way (Authorization header). */\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\n const json = await apiGet<CategoryResponse | null>(\"/api/categories\", extras, withToken(options, params.token));\n\n assertNoApiErrors(json, 200);\n\n // Shopwave answers an empty body when nothing matches.\n return Object.values(json?.categories ?? {});\n}\n","import { apiGet, type RequestOptions } from \"./request\";\nimport { assertNoApiErrors } from \"./errors\";\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 | null>(\"/api/consumer\", extras, options);\n\n assertNoApiErrors(json, 200);\n\n // Shopwave answers an empty body when nothing matches.\n return Object.values(json?.consumers ?? {});\n}\n","import { apiGet, withToken, type RequestOptions } from \"./request\";\nimport { assertNoApiErrors } from \"./errors\";\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 /** @deprecated Pass `options.token` instead. Sent the same way (Authorization header). */\n token?: string;\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 | null>(\"/api/employees\", extras, withToken(options, params.token));\n\n assertNoApiErrors(json, 200);\n\n // Shopwave answers an empty body when nothing matches.\n return Object.values(json?.employees ?? {});\n}\n","import { apiGet, withToken, type RequestOptions } from \"./request\";\nimport { assertNoApiErrors } from \"./errors\";\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 /** @deprecated Pass `options.token` instead. Sent the same way (Authorization header). */\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\n const json = await apiGet<ProductResponse | null>(\"/api/products\", extras, withToken(options, params.token));\n\n assertNoApiErrors(json, 200);\n\n // Shopwave answers an empty body when nothing matches.\n return Object.values(json?.products ?? {});\n}\n\n// ---------------------------------------------------------------------------\n// Instances (prices) on a product save\n// ---------------------------------------------------------------------------\n//\n// Shopwave API reference, POST /product: \"Product instances cannot be updated —\n// they are stored for historical reference. To change pricing, remove the old\n// instance and create a new one.\" An instance is therefore only sent with its\n// `id` when it is unchanged; a new or re-priced instance is sent without one, and\n// Shopwave gives it a new id. (adminV2 does the same and warns before saving.)\n\n/** An instance as a form or caller holds it: `id` is absent for new rows. */\nexport type ProductInstanceInput = {\n id?: number | string | null;\n price?: string | number | null;\n taxPercentage?: string | number | null;\n name?: string | null;\n};\n\n/** Instances as the API returns them (keyed by id) or as a plain list. */\nexport type ProductInstances<T = ProductInstanceInput> =\n | Readonly<Record<string, T>>\n | Readonly<Record<number, T>>\n | ReadonlyArray<T>;\n\n/**\n * What `saveProduct` accepts: any product fields, with `instances` either as the\n * API returns them or as built by {@link toProductInstancesForSave}. Include `id`\n * to update; leave it out to create.\n */\nexport type ProductSaveInput = Partial<Omit<Product, \"instances\">> & {\n instances?: ProductInstances;\n [field: string]: unknown;\n};\n\n/** Fields the server sets on an instance; never sent back. */\nconst SERVER_INSTANCE_FIELDS = [\"timestamp\"] as const;\n\nfunction instanceList<T>(instances: ProductInstances<T> | null | undefined): T[] {\n if (!instances) return [];\n const list: unknown[] = Array.isArray(instances) ? [...instances] : Object.values(instances);\n return list.filter(\n (inst): inst is T => inst != null && typeof inst === \"object\"\n );\n}\n\nfunction hasId(id: unknown): id is number | string {\n return id != null && String(id).trim() !== \"\";\n}\n\n/** Same number, so `\"422.0\"`, `\"422\"` and `422` are equal; blanks equal each other. */\nfunction sameAmount(a: unknown, b: unknown): boolean {\n const blank = (v: unknown) => v == null || String(v).trim() === \"\";\n if (blank(a) || blank(b)) return blank(a) && blank(b);\n const x = Number(a);\n const y = Number(b);\n return Number.isNaN(x) || Number.isNaN(y) ? String(a).trim() === String(b).trim() : x === y;\n}\n\n/** True when two instances have the same price and tax (the fields Shopwave won't update). */\nexport function isSameInstancePrice(a: ProductInstanceInput, b: ProductInstanceInput): boolean {\n return sameAmount(a.price, b.price) && sameAmount(a.taxPercentage, b.taxPercentage);\n}\n\n/** An instance whose price or tax was edited, so saving it will give it a new id. */\nexport interface RepricedInstance {\n /** Position of the instance in the list that was passed in. */\n index: number;\n /** The id it has now, which Shopwave will replace. */\n id: number | string;\n name: string;\n before: { price: string; taxPercentage: string };\n after: { price: string; taxPercentage: string };\n}\n\nconst text = (v: unknown) => (v == null ? \"\" : String(v));\n\n/**\n * Lists the instances whose price or tax differs from the `original` instance\n * with the same id: saving them creates new instances with new ids, which can\n * affect anything that stores instance ids (integrations, reports, modifiers).\n * Use it to warn before saving. New rows (no id) aren't listed.\n */\nexport function findRepricedInstances(\n instances: ProductInstances | null | undefined,\n original: ProductInstances | null | undefined\n): RepricedInstance[] {\n const byId = new Map(instanceList(original).filter((o) => hasId(o.id)).map((o) => [String(o.id), o]));\n const result: RepricedInstance[] = [];\n instanceList(instances).forEach((inst, index) => {\n if (!hasId(inst.id)) return;\n const before = byId.get(String(inst.id));\n if (!before || isSameInstancePrice(inst, before)) return;\n result.push({\n index,\n id: inst.id,\n name: text(inst.name),\n before: { price: text(before.price), taxPercentage: text(before.taxPercentage) },\n after: { price: text(inst.price), taxPercentage: text(inst.taxPercentage) },\n });\n });\n return result;\n}\n\n/**\n * Builds the `instances` map for a product save:\n * - keyed by position (`\"0\"`, `\"1\"`, …), like every other ref in a save;\n * - an instance keeps its `id` only when `original` has an instance with that id\n * and the same price and tax; otherwise the id is removed, so Shopwave creates\n * a new instance instead of re-pointing or rewriting someone else's;\n * - server fields (`timestamp`) are removed.\n *\n * Pass the product as last read from the API as `original`. Leave it out for a\n * new product (including a duplicate): every id is then removed.\n * Instances left out of the list are left out of the save (removed).\n */\nexport function toProductInstancesForSave<T extends ProductInstanceInput>(\n instances: ProductInstances<T> | null | undefined,\n original?: ProductInstances | null\n): Record<string, Omit<T, \"id\"> & { id?: number | string }> {\n const keep = new Map(instanceList(original).filter((o) => hasId(o.id)).map((o) => [String(o.id), o]));\n const out: Record<string, Omit<T, \"id\"> & { id?: number | string }> = {};\n instanceList(instances).forEach((inst, index) => {\n const { id, ...rest } = inst;\n for (const field of SERVER_INSTANCE_FIELDS) delete (rest as Record<string, unknown>)[field];\n const before = hasId(id) ? keep.get(String(id)) : undefined;\n out[String(index)] = (before && isSameInstancePrice(inst, before) ? { id, ...rest } : rest) as Omit<T, \"id\"> & {\n id?: number | string;\n };\n });\n return out;\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","/**\n * Singleton resources (one per logged-in user) and the uploader.\n *\n * Like `SHOPWAVE_ENTITIES`, this table is shared by the client functions\n * (`fetchMerchant`, `fetchUser`, `uploadImage`, …) and the server route\n * handlers in `react-shopwave-connect/server`, so the two halves of the contract\n * can't drift apart. These aren't in `SHOPWAVE_ENTITIES` because they aren't\n * keyed collections: `GET /merchant` answers `{ merchant: {…} }`, not a map by id.\n */\n\nexport interface ResourceDefinition {\n /** App route: `/api/<route>`. */\n route: string;\n /** Key of the object in requests and responses (`{ merchant: {…} }`). */\n key: string;\n /** Shopwave API path. */\n upstream: string;\n /** False for read-only resources. */\n writable: boolean;\n}\n\nexport type ResourceKind = \"merchant\" | \"user\";\n\n/**\n * - merchant: `GET /merchant`; `POST /merchant` with `{ merchant: { id, … } }`\n * updates it (without an `id` the API would create a second merchant, so the\n * route requires one).\n * - user: `GET /user` (read-only here; the API's `POST /user` invites users and\n * needs the admin scope).\n */\nexport const SHOPWAVE_RESOURCES: Readonly<Record<ResourceKind, ResourceDefinition>> = Object.freeze({\n merchant: { route: \"merchant\", key: \"merchant\", upstream: \"merchant\", writable: true },\n user: { route: \"user\", key: \"user\", upstream: \"user\", writable: false },\n});\n\n/**\n * What an upload is for. Sent to Shopwave as the `contentType` header of\n * `PUT /uploader`. `merchant` is for the receipt and square logos (stored\n * under `…/merchant/<id>/logo/`), `merchantFeature` for the featured image.\n * The API spells two of its values `applicaionLogo` / `applicaionImages`; use\n * the correctly spelt names here and the server maps them.\n */\nexport type UploadKind = \"merchant\" | \"merchantFeature\" | \"product\" | \"user\" | \"applicationLogo\" | \"applicationImages\";\n\n/** `UploadKind` → the API's `contentType` value. */\nexport const UPLOAD_KINDS: Readonly<Record<UploadKind, string>> = Object.freeze({\n merchant: \"merchant\",\n /** The merchant's featured image (adminV1; not in the API reference). */\n merchantFeature: \"merchantFeature\",\n product: \"product\",\n user: \"user\",\n applicationLogo: \"applicaionLogo\",\n applicationImages: \"applicaionImages\",\n});\n\n/** How uploads are addressed (app route and Shopwave uploader). */\nexport const SHOPWAVE_UPLOAD = Object.freeze({\n /** App route: `POST /api/<route>` (multipart `file` + `kind`). */\n route: \"upload\",\n /** Shopwave path: `PUT {apiUrl}/uploader` (multipart `file`). */\n upstream: \"uploader\",\n /** Header that carries the upload kind upstream. */\n kindHeader: \"contentType\",\n /** Multipart field name for the file, on both sides. */\n fileField: \"file\",\n /** Multipart field name for the kind in the app request. */\n kindField: \"kind\",\n});\n\nexport function isUploadKind(value: unknown): value is UploadKind {\n return typeof value === \"string\" && Object.prototype.hasOwnProperty.call(UPLOAD_KINDS, value);\n}\n\n/**\n * Read-only lists that aren't records: the merchant's distinct tags.\n *\n * Like `SHOPWAVE_RESOURCES`, shared by the client functions\n * (`fetchProductTags`, `fetchProductInstanceTags`) and the server route\n * handlers (`shopwave.lookup(kind)`), so the two halves can't drift apart.\n *\n * - productTags: `GET /product/tags` → `{ tags: string[] }`, every distinct tag\n * on the merchant's products. No parameters. The API reference says\n * \"internal SKU tags are excluded\", but the live API includes system tags\n * written by imports and syncs (`PID-133`, `VID-137`, `#Hash:…`, `upc-…`;\n * test merchant 5644, 29 Oct 2026). Apps that offer the list as suggestions\n * filter those themselves. Tags of deleted products drop out of the list.\n * - productInstanceTags: `GET /product/instance/tags` → `{ tags: string[] }`,\n * the same for price instances (channels such as \"Eat In\", \"Takeout\").\n *\n * The app route for instance tags is `products/instance-tags`, one segment\n * under `products`, so it sits beside `products/[id]` without nesting deeper.\n */\nexport interface LookupDefinition {\n /** App route: `/api/<route>`. */\n route: string;\n /** Key of the list in the response (`{ tags: [...] }`). */\n key: string;\n /** Shopwave API path. */\n upstream: string;\n}\n\nexport type LookupKind = \"productTags\" | \"productInstanceTags\";\n\nexport const SHOPWAVE_LOOKUPS: Readonly<Record<LookupKind, LookupDefinition>> = Object.freeze({\n productTags: { route: \"products/tags\", key: \"tags\", upstream: \"product/tags\" },\n productInstanceTags: { route: \"products/instance-tags\", key: \"tags\", upstream: \"product/instance/tags\" },\n});\n","import { apiGet, type RequestOptions } from \"./request\";\nimport { assertNoApiErrors } from \"./errors\";\nimport { SHOPWAVE_LOOKUPS, type LookupKind } from \"./resources\";\n\n/**\n * Turns a tag list from the API into clean strings: trims each tag, drops\n * blanks and anything that isn't text, and removes exact duplicates (first one\n * wins). The API's own order is kept. A map is accepted too (its values are\n * used), in case the API ever answers keyed by id.\n */\nexport function normalizeTagList(value: unknown): string[] {\n const list: unknown[] = Array.isArray(value)\n ? value\n : value && typeof value === \"object\"\n ? Object.values(value as Record<string, unknown>)\n : [];\n const seen = new Set<string>();\n const out: string[] = [];\n for (const item of list) {\n if (typeof item !== \"string\" && typeof item !== \"number\") continue;\n const tag = String(item).trim();\n if (!tag || seen.has(tag)) continue;\n seen.add(tag);\n out.push(tag);\n }\n return out;\n}\n\nasync function fetchLookup(kind: LookupKind, options: RequestOptions): Promise<string[]> {\n const def = SHOPWAVE_LOOKUPS[kind];\n if (!def) throw new Error(`Unknown Shopwave lookup \"${kind}\"`);\n const json = await apiGet<Record<string, unknown> | null>(`/api/${def.route}`, {}, options);\n assertNoApiErrors(json, 200);\n // Shopwave answers an empty body when there's nothing to return.\n return normalizeTagList(json?.[def.key]);\n}\n\n/**\n * Every distinct tag used on the merchant's products (`GET /product/tags`),\n * for a tag field's suggestions. Returns `[]` when there are none.\n */\nexport function fetchProductTags(options: RequestOptions = {}): Promise<string[]> {\n return fetchLookup(\"productTags\", options);\n}\n\n/**\n * Every distinct tag used on the merchant's price instances\n * (`GET /product/instance/tags`): the channel names (Eat In, Takeout, …).\n * Returns `[]` when there are none.\n */\nexport function fetchProductInstanceTags(options: RequestOptions = {}): Promise<string[]> {\n return fetchLookup(\"productInstanceTags\", options);\n}\n","import { apiGet, withToken, type RequestOptions } from \"./request\";\nimport { assertNoApiErrors } from \"./errors\";\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 /** @deprecated Pass `options.token` instead. Sent the same way (Authorization header). */\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\n const json = await apiGet<PromotionResponse | null>(\"/api/promotions\", extras, withToken(options, params.token));\n\n assertNoApiErrors(json, 200);\n\n // Shopwave answers an empty body when nothing matches.\n return Object.values(json?.promotions ?? {});\n}\n","import { apiGet, withToken, type RequestOptions } from \"./request\";\nimport { assertNoApiErrors } from \"./errors\";\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 /** Optional id filter. */\n storeIds?: Array<number>;\n /** Whether to include soft-deleted records. Defaults to `false`. */\n deleted?: boolean;\n /** @deprecated Pass `options.token` instead. Sent the same way (Authorization header). */\n token?: string;\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 if (params.storeIds) {\n extras.storeIds = params.storeIds;\n }\n\n const json = await apiGet<StoreResponse | null>(\"/api/stores\", extras, withToken(options, params.token));\n\n assertNoApiErrors(json, 200);\n\n // Shopwave answers an empty body when nothing matches.\n return Object.values(json?.stores ?? {});\n}\n","import { apiGet, type RequestOptions } from \"./request\";\nimport { assertNoApiErrors } from \"./errors\";\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 | null>(\"/api/report\", extras, options);\n\n assertNoApiErrors(json, 200);\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\";\nimport { assertNoApiErrors } from \"./errors\";\n\n/**\n * Low-level, untyped mutations: the caller supplies the route and payload.\n *\n * Prefer the typed functions in `./entities` (`saveProduct`, `deleteCategory`,\n * …): they know each route, build the request envelope, check the response and\n * return the saved entity with its id. Keep these for routes that have no typed\n * function yet (e.g. `/api/merchant`).\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. Returns the response's `result` field when it\n * has one (older app routes wrapped their answer that way), otherwise the whole\n * parsed response body. Throws `ShopwaveApiError` on HTTP or API errors.\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> | null>(\n endpoint,\n method,\n payload,\n options\n );\n assertNoApiErrors(json, 200);\n if (json && typeof json === \"object\" && \"result\" in json) return json.result;\n return (json ?? undefined) as R | undefined;\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","/**\n * Browser helpers for getting an image to an exact pixel size before upload.\n *\n * Shopwave doesn't resize uploads, and the merchant image slots have fixed\n * sizes (adminV1 refuses any other size), so the SDK scales and centre-crops\n * the image in the browser instead.\n */\n\n/** Source rectangle to draw so the image covers `dstWidth × dstHeight` (centre crop, no stretching). */\nexport interface CropRect {\n sx: number;\n sy: number;\n sw: number;\n sh: number;\n}\n\n/**\n * The centred part of a `srcWidth × srcHeight` image with the aspect ratio of\n * `dstWidth × dstHeight` (like CSS `object-fit: cover`).\n */\nexport function coverCrop(srcWidth: number, srcHeight: number, dstWidth: number, dstHeight: number): CropRect {\n if (![srcWidth, srcHeight, dstWidth, dstHeight].every((n) => Number.isFinite(n) && n > 0)) {\n throw new Error(\"coverCrop: sizes must be positive numbers\");\n }\n const srcRatio = srcWidth / srcHeight;\n const dstRatio = dstWidth / dstHeight;\n if (srcRatio > dstRatio) {\n // Too wide: keep the full height, trim the sides.\n const sw = Math.round(srcHeight * dstRatio);\n return { sx: Math.round((srcWidth - sw) / 2), sy: 0, sw, sh: srcHeight };\n }\n // Too tall (or exact): keep the full width, trim top and bottom.\n const sh = Math.round(srcWidth / dstRatio);\n return { sx: 0, sy: Math.round((srcHeight - sh) / 2), sw: srcWidth, sh };\n}\n\nexport interface ImageSize {\n width: number;\n height: number;\n}\n\nfunction requireBrowserImages(fn: string): void {\n if (typeof createImageBitmap !== \"function\") {\n throw new Error(`${fn} needs a browser (createImageBitmap is not available)`);\n }\n}\n\n/** Reads an image file's pixel size (browser only). */\nexport async function readImageSize(file: Blob): Promise<ImageSize> {\n requireBrowserImages(\"readImageSize\");\n const bitmap = await createImageBitmap(file);\n try {\n return { width: bitmap.width, height: bitmap.height };\n } finally {\n bitmap.close?.();\n }\n}\n\nexport interface FitImageOptions {\n /** Output type. Default `\"image/png\"`. */\n type?: \"image/png\" | \"image/jpeg\" | \"image/webp\";\n /** 0–1, for JPEG/WebP. */\n quality?: number;\n /** Return the file untouched when it's already the right size and type. Default true. */\n keepIfExact?: boolean;\n}\n\n/**\n * Returns the image at exactly `width × height`: scaled and centre-cropped\n * (never stretched). Browser only. The result is a `File` named like the\n * input, with the output type's extension.\n */\nexport async function fitImageToSize(file: Blob, width: number, height: number, options: FitImageOptions = {}): Promise<File> {\n requireBrowserImages(\"fitImageToSize\");\n const { type = \"image/png\", quality, keepIfExact = true } = options;\n const name = (file as File).name || \"image\";\n const ext = type === \"image/jpeg\" ? \"jpg\" : type.split(\"/\")[1];\n const outName = `${name.replace(/\\.[^.]+$/, \"\")}.${ext}`;\n\n const bitmap = await createImageBitmap(file);\n try {\n if (keepIfExact && bitmap.width === width && bitmap.height === height && file.type === type) {\n return file instanceof File ? file : new File([file], outName, { type });\n }\n const { sx, sy, sw, sh } = coverCrop(bitmap.width, bitmap.height, width, height);\n\n let blob: Blob;\n if (typeof OffscreenCanvas === \"function\") {\n const canvas = new OffscreenCanvas(width, height);\n const ctx = canvas.getContext(\"2d\");\n if (!ctx) throw new Error(\"fitImageToSize: no 2D canvas context\");\n ctx.imageSmoothingQuality = \"high\";\n ctx.drawImage(bitmap, sx, sy, sw, sh, 0, 0, width, height);\n blob = await canvas.convertToBlob({ type, quality });\n } else {\n const canvas = document.createElement(\"canvas\");\n canvas.width = width;\n canvas.height = height;\n const ctx = canvas.getContext(\"2d\");\n if (!ctx) throw new Error(\"fitImageToSize: no 2D canvas context\");\n ctx.imageSmoothingQuality = \"high\";\n ctx.drawImage(bitmap, sx, sy, sw, sh, 0, 0, width, height);\n blob = await new Promise<Blob>((resolve, reject) =>\n canvas.toBlob((b) => (b ? resolve(b) : reject(new Error(\"fitImageToSize: encoding failed\"))), type, quality)\n );\n }\n return new File([blob], outName, { type });\n } finally {\n bitmap.close?.();\n }\n}\n","import { apiRequest, type RequestOptions } from \"./request\";\nimport { ShopwaveApiError, assertNoApiErrors } from \"./errors\";\nimport { SHOPWAVE_UPLOAD, isUploadKind, type UploadKind } from \"./resources\";\n\nexport interface UploadImageParams {\n /** What the image is for (sent to Shopwave as the uploader's `contentType`). */\n kind: UploadKind;\n /** File name to send when `file` is a Blob without one. Defaults to `File.name`, else `\"upload\"`. */\n fileName?: string;\n}\n\nexport interface UploadedImage {\n /** The generated file name (products store it in `images`). */\n id: string;\n /** Public HTTPS URL of the file (see {@link getImageUrl}). */\n url: string;\n /** The URL exactly as the API returned it (`http://static.merchantstack.com/images/…`). */\n path: string;\n}\n\n/**\n * HTTPS host that serves the Shopwave CDN's images. The API returns\n * `http://static.merchantstack.com/images/…`, which has no HTTPS endpoint\n * (checked Sep 2026), so pages served over HTTPS can't show it.\n */\nexport const SHOPWAVE_IMAGE_BASE_URL = \"https://s3-eu-west-1.amazonaws.com/static.merchantstack.com/images\";\n\nconst STATIC_HOST = /^https?:\\/\\/static\\.merchantstack\\.com\\/images\\//i;\n\n/**\n * Turns an image path from the API into a URL a page can load over HTTPS:\n * `http://static.merchantstack.com/images/x.png` → `<base>/x.png`, and a\n * relative path (`merchant/5644/logo/x.png`) → `<base>/merchant/5644/logo/x.png`.\n * Other absolute URLs are returned unchanged; empty input gives `\"\"`.\n */\nexport function getImageUrl(path: string | null | undefined, base: string = SHOPWAVE_IMAGE_BASE_URL): string {\n if (!path) return \"\";\n const root = base.replace(/\\/+$/, \"\");\n if (STATIC_HOST.test(path)) return `${root}/${path.replace(STATIC_HOST, \"\")}`;\n if (/^[a-z][a-z0-9+.-]*:/i.test(path) || path.startsWith(\"//\")) return path;\n return `${root}/${path.replace(/^\\/+/, \"\")}`;\n}\n\n/**\n * Uploads one image to the Shopwave CDN through the app's `/api/upload` route\n * (`createShopwaveApi(...).upload()`), and returns its id and URL.\n *\n * The file only lands on the CDN (under `…/images/<kind>/<merchantId>/…`); a\n * record has to point at it to use it. Live API (Sep 2026): the upload answers\n * 201 `{ fileName, path }`.\n */\nexport async function uploadImage(\n file: Blob,\n params: UploadImageParams,\n options: RequestOptions = {}\n): Promise<UploadedImage> {\n if (!file || typeof (file as Blob).size !== \"number\") throw new Error(\"uploadImage: file must be a File or Blob\");\n if (!isUploadKind(params?.kind)) throw new Error(`uploadImage: unknown kind \"${params?.kind}\"`);\n\n const name = params.fileName ?? (file as File).name ?? \"upload\";\n const form = new FormData();\n form.append(SHOPWAVE_UPLOAD.fileField, file, name);\n form.append(SHOPWAVE_UPLOAD.kindField, params.kind);\n\n // No Content-Type: fetch sets the multipart boundary itself.\n const { status, body: raw } = await apiRequest(`/api/${SHOPWAVE_UPLOAD.route}`, { method: \"POST\", body: form }, options);\n const body = raw as { fileName?: unknown; path?: unknown } | null;\n assertNoApiErrors(body, status);\n\n const id = typeof body?.fileName === \"string\" ? body.fileName : \"\";\n const path = typeof body?.path === \"string\" ? body.path : \"\";\n if (!id) {\n throw new ShopwaveApiError(status, `Shopwave API error (${status}): the upload response has no fileName`, { body });\n }\n return { id, url: getImageUrl(path), path };\n}\n","import type { RequestOptions } from \"./request\";\nimport type { UploadKind } from \"./resources\";\nimport { fitImageToSize, type FitImageOptions } from \"./imageFit\";\nimport { getImageUrl, uploadImage, type UploadedImage } from \"./upload\";\n\n/**\n * Merchant images, as adminV1 saves them and adminV2 reads them:\n *\n * ```\n * { logo: { receipt: \"<file>\", square: \"<file>\" }, feature: [\"<file>\"] }\n * ```\n *\n * - **Writes** take the uploader's `fileName` (`<hash>.png`), not a URL.\n * - **Reads** return full URLs (`http://static.merchantstack.com/images/…`),\n * so a value read back must be turned into a file name before it's saved\n * again ({@link toStoredImageIds}; `updateMerchant` does this for you).\n * - Sending `imageIds` replaces the whole stored object (`{}` clears it);\n * leaving it out keeps it.\n * - The API builds each URL from the key, not from where the file is:\n * `logo.*` → `…/merchant/<id>/logo/<file>`, `feature` → `…/feature/<file>`.\n * So a featured image must be uploaded with kind `merchantFeature` (which\n * stores it under `feature/`); a `merchant` upload lands under `logo/`.\n *\n * Confirmed on the live API, merchant 5644, 30 Sep 2026.\n */\nexport interface MerchantImageIds {\n logo?: {\n receipt?: string | null;\n square?: string | null;\n [key: string]: string | null | undefined;\n } | null;\n feature?: string[] | null;\n [key: string]: unknown;\n}\n\n/** The three image slots on the merchant settings page. */\nexport const MERCHANT_IMAGE_SLOT_KEYS = [\"receipt\", \"square\", \"featured\"] as const;\nexport type MerchantImageSlot = (typeof MERCHANT_IMAGE_SLOT_KEYS)[number];\n\nexport interface MerchantImageSlotDefinition {\n /** Where it's stored: `logo.receipt`, `logo.square` or `feature[0]`. */\n field: \"logo.receipt\" | \"logo.square\" | \"feature[0]\";\n /** Uploader `contentType` (the featured image has its own). */\n uploadKind: UploadKind;\n /** Exact size in pixels. adminV1 refuses any other size; the API doesn't resize. */\n width: number;\n height: number;\n}\n\n/**\n * Where each slot lives, its upload kind and its exact size (from adminV1's\n * merchant settings page).\n */\nexport const MERCHANT_IMAGE_SLOTS: Readonly<Record<MerchantImageSlot, MerchantImageSlotDefinition>> = Object.freeze({\n receipt: { field: \"logo.receipt\", uploadKind: \"merchant\", width: 576, height: 325 },\n square: { field: \"logo.square\", uploadKind: \"merchant\", width: 1000, height: 1000 },\n featured: { field: \"feature[0]\", uploadKind: \"merchantFeature\", width: 1200, height: 600 },\n});\n\n/**\n * The file name in an image value: `\"http://…/logo/abc.png\"` → `\"abc.png\"`,\n * `\"abc.png\"` → `\"abc.png\"`. Query strings and fragments are ignored.\n * Returns `null` for empty or non-string values.\n */\nexport function imageFileName(value: unknown): string | null {\n if (typeof value !== \"string\") return null;\n const clean = value.trim().replace(/[?#].*$/, \"\");\n const last = clean.split(\"/\").pop() ?? \"\";\n return last ? decodeURIComponent(last) : null;\n}\n\n/** The stored value for a slot (a URL when read from the API; a file name right after an upload). */\nexport function getMerchantImage(imageIds: MerchantImageIds | null | undefined, slot: MerchantImageSlot): string | null {\n if (!imageIds || typeof imageIds !== \"object\") return null;\n let value: unknown;\n if (slot === \"featured\") {\n value = Array.isArray(imageIds.feature) ? imageIds.feature[0] : undefined;\n } else {\n const logo = imageIds.logo;\n value = logo && typeof logo === \"object\" ? logo[slot] : undefined;\n }\n return typeof value === \"string\" && value.trim() ? value.trim() : null;\n}\n\n/**\n * An HTTPS URL for a slot's stored image, or `\"\"` when the slot is empty or\n * holds only a file name (a fresh upload not yet saved; use the upload's `url`).\n */\nexport function getMerchantImageUrl(\n imageIds: MerchantImageIds | null | undefined,\n slot: MerchantImageSlot,\n base?: string\n): string {\n const value = getMerchantImage(imageIds, slot);\n if (!value || !value.includes(\"/\")) return \"\";\n return getImageUrl(value, base);\n}\n\n/**\n * Returns a copy of `imageIds` with one slot set to `fileName` (or cleared\n * with `null`). Other slots and unknown keys are kept; an empty `logo` or\n * `feature` is removed.\n */\nexport function setMerchantImage(\n imageIds: MerchantImageIds | null | undefined,\n slot: MerchantImageSlot,\n fileName: string | null\n): MerchantImageIds {\n const next: MerchantImageIds = { ...(imageIds && typeof imageIds === \"object\" ? imageIds : {}) };\n if (slot === \"featured\") {\n const rest = Array.isArray(next.feature) ? next.feature.slice(1) : [];\n const feature = fileName ? [fileName, ...rest] : rest;\n if (feature.length > 0) next.feature = feature;\n else delete next.feature;\n } else {\n const logo = { ...(next.logo && typeof next.logo === \"object\" ? next.logo : {}) };\n if (fileName) logo[slot] = fileName;\n else delete logo[slot];\n if (Object.keys(logo).length > 0) next.logo = logo;\n else delete next.logo;\n }\n return next;\n}\n\nfunction toStored(value: unknown): unknown {\n if (typeof value === \"string\") return imageFileName(value) ?? undefined;\n if (Array.isArray(value)) {\n const items = value.map(toStored).filter((v) => v !== undefined && v !== null && v !== \"\");\n return items.length > 0 ? items : undefined;\n }\n if (value && typeof value === \"object\") {\n const out: Record<string, unknown> = {};\n for (const [k, v] of Object.entries(value as Record<string, unknown>)) {\n const stored = toStored(v);\n if (stored !== undefined && stored !== null && stored !== \"\") out[k] = stored;\n }\n return Object.keys(out).length > 0 ? out : undefined;\n }\n return undefined;\n}\n\n/**\n * The `imageIds` to send on a save: every URL is turned back into its file\n * name (what adminV1 does with images you didn't change) and empty values\n * are dropped. No images gives `{}`.\n */\nexport function toStoredImageIds(imageIds: unknown): MerchantImageIds {\n return (toStored(imageIds) as MerchantImageIds | undefined) ?? {};\n}\n\nexport interface UploadMerchantImageOptions extends RequestOptions {\n /**\n * Scale and centre-crop the image to the slot's exact size before upload\n * (browser only). Default true. With `false` the file is sent as-is.\n */\n fit?: boolean;\n /** Output type and quality when fitting. Default PNG. */\n fitOptions?: FitImageOptions;\n}\n\n/**\n * Uploads an image for a merchant slot: fits it to the slot's size, uploads\n * it with the slot's kind, and returns `{ id, url, path }`. Put `id` into\n * `imageIds` with {@link setMerchantImage} and save with `updateMerchant`.\n */\nexport async function uploadMerchantImage(\n file: Blob,\n slot: MerchantImageSlot,\n options: UploadMerchantImageOptions = {}\n): Promise<UploadedImage> {\n const def = MERCHANT_IMAGE_SLOTS[slot];\n if (!def) throw new Error(`uploadMerchantImage: unknown slot \"${slot}\"`);\n const { fit = true, fitOptions, ...requestOptions } = options;\n const body = fit ? await fitImageToSize(file, def.width, def.height, fitOptions) : file;\n return uploadImage(body, { kind: def.uploadKind }, requestOptions);\n}\n","import { apiGet, apiRequest, type RequestOptions } from \"./request\";\nimport { ShopwaveApiError, assertNoApiErrors } from \"./errors\";\nimport type { apiResponse } from \"./types\";\nimport { SHOPWAVE_RESOURCES } from \"./resources\";\nimport { toStoredImageIds, type MerchantImageIds } from \"./merchantImages\";\n\nconst MERCHANT_PATH = `/api/${SHOPWAVE_RESOURCES.merchant.route}`;\n\n/** A colour set: hex strings (`\"#1a2b3c\"`). */\nexport interface MerchantColourSet {\n main?: string | null;\n highlight?: string | null;\n contrast?: string | null;\n [key: string]: string | null | undefined;\n}\n\n/**\n * Brand colours, as adminV1 saves them and adminV2 reads them\n * (`colours.primary.main` is the logo background): `{ primary: { main,\n * highlight, contrast } }`. `{}` when unset. Confirmed live, 30 Sep 2026.\n */\nexport interface MerchantColours {\n primary?: MerchantColourSet | null;\n [key: string]: MerchantColourSet | null | undefined;\n}\n\n/** Colour keys inside `colours.primary`, in display order. */\nexport const MERCHANT_COLOUR_KEYS = [\"main\", \"highlight\", \"contrast\"] as const;\nexport type MerchantColourKey = (typeof MERCHANT_COLOUR_KEYS)[number];\n\n/**\n * Web and social links, as adminV1 saves them:\n * `{ website: { home }, social: { twitter, facebook, instagram } }`.\n * `{}` when empty (confirmed live, 30 Sep 2026). Use {@link normalizeMerchantLinks} to read older shapes and\n * {@link merchantLinksToForm} / {@link merchantLinksFromForm} for flat forms.\n */\nexport interface MerchantLinks {\n website?: { home?: string | null; [key: string]: string | null | undefined } | null;\n social?: {\n twitter?: string | null;\n facebook?: string | null;\n instagram?: string | null;\n [key: string]: string | null | undefined;\n } | null;\n [key: string]: unknown;\n}\n\n/** Flat link fields a form edits, in display order. */\nexport const MERCHANT_LINK_KEYS = [\"website\", \"twitter\", \"facebook\", \"instagram\"] as const;\nexport type MerchantLinkKey = (typeof MERCHANT_LINK_KEYS)[number];\n\n/** Where each flat link field lives in `merchant.links`. */\nexport const MERCHANT_LINK_PATHS: Readonly<Record<MerchantLinkKey, readonly [string, string]>> = Object.freeze({\n website: [\"website\", \"home\"],\n twitter: [\"social\", \"twitter\"],\n facebook: [\"social\", \"facebook\"],\n instagram: [\"social\", \"instagram\"],\n});\n\nfunction trimmed(value: unknown): string | undefined {\n return typeof value === \"string\" && value.trim() ? value.trim() : undefined;\n}\n\n/**\n * Reads `merchant.links` as flat fields (`{ website, twitter, facebook,\n * instagram }`), whatever shape it was saved in:\n * - the nested shape adminV1 saves (`{ website: { home }, social: { … } }`);\n * - flat keys (`{ website: \"…\" }`, what SDK 0.4.0 pre-releases sent);\n * - an array in {@link MERCHANT_LINK_KEYS} order (older AdminUI versions).\n * Empty values are dropped.\n */\nexport function merchantLinksToForm(links: unknown): Partial<Record<MerchantLinkKey, string>> {\n const out: Partial<Record<MerchantLinkKey, string>> = {};\n if (Array.isArray(links)) {\n links.forEach((value, i) => {\n const key = MERCHANT_LINK_KEYS[i];\n const v = trimmed(value);\n if (key && v) out[key] = v;\n });\n return out;\n }\n if (!links || typeof links !== \"object\") return out;\n const obj = links as Record<string, unknown>;\n for (const key of MERCHANT_LINK_KEYS) {\n const [group, field] = MERCHANT_LINK_PATHS[key];\n const nested = obj[group];\n const v =\n nested && typeof nested === \"object\" && !Array.isArray(nested)\n ? trimmed((nested as Record<string, unknown>)[field])\n : trimmed(obj[key]);\n if (v) out[key] = v;\n }\n return out;\n}\n\n/**\n * Builds the stored `links` object from flat fields. Empty values are left\n * out, and so is a group with nothing in it; no links gives `{}`.\n */\nexport function merchantLinksFromForm(form: Partial<Record<MerchantLinkKey, string | null | undefined>>): MerchantLinks {\n const out: Record<string, Record<string, string>> = {};\n for (const key of MERCHANT_LINK_KEYS) {\n const v = trimmed(form[key]);\n if (!v) continue;\n const [group, field] = MERCHANT_LINK_PATHS[key];\n (out[group] ??= {})[field] = v;\n }\n return out as MerchantLinks;\n}\n\n/** Reads any stored or older `links` value into the nested shape adminV1 saves. */\nexport function normalizeMerchantLinks(links: unknown): MerchantLinks {\n return merchantLinksFromForm(merchantLinksToForm(links));\n}\n\nexport interface Merchant {\n id: number;\n name: string;\n description?: string | null;\n /** Company registration number. */\n companyNumber?: string | null;\n vatNumber?: string | null;\n categoryId?: number | null;\n estAnnualRevenue?: number | string | null;\n note?: string | null;\n links?: MerchantLinks | null;\n colours?: MerchantColours | null;\n imageIds?: MerchantImageIds | null;\n createdDate?: string;\n modifiedDate?: string;\n /** Other fields the API returns are kept as-is. */\n [field: string]: unknown;\n}\n\nexport interface MerchantResponse {\n merchant: Merchant;\n api: apiResponse;\n}\n\n/** What `updateMerchant` accepts: any subset of the merchant's fields. */\nexport type MerchantPatch = Partial<Merchant> & { [field: string]: unknown };\n\n/**\n * Object fields of the merchant. The API keeps one you leave out, and\n * replaces it as a whole when you send it.\n */\nexport const MERCHANT_OBJECT_FIELDS: ReadonlyArray<string> = [\"colours\", \"links\", \"imageIds\"];\n\n/** Fields the API sets itself; `updateMerchant` never sends them back. */\nexport const MERCHANT_READ_ONLY_FIELDS: ReadonlyArray<string> = [\"createdDate\", \"modifiedDate\"];\n\n/**\n * Longest values the API stores. Longer values are cut off silently\n * (live API, Sep 2026), so `updateMerchant` rejects them instead.\n * `vatNumber` fits a UK VAT number without the `GB` prefix.\n */\nexport const MERCHANT_FIELD_MAX_LENGTH: Readonly<Record<string, number>> = Object.freeze({\n companyNumber: 11,\n vatNumber: 9,\n});\n\n/** Returns `\"<field> is longer than <n> characters\"` messages for over-long fields. */\nexport function validateMerchant(merchant: Record<string, unknown>): string[] {\n const problems: string[] = [];\n for (const [field, max] of Object.entries(MERCHANT_FIELD_MAX_LENGTH)) {\n const value = merchant[field];\n if (typeof value === \"string\" && value.length > max) problems.push(`${field} is longer than ${max} characters`);\n }\n return problems;\n}\n\n/**\n * Reads the logged-in user's merchant (a user has exactly one), or `null`\n * when the API returns none.\n */\nexport async function fetchMerchant(options: RequestOptions = {}): Promise<Merchant | null> {\n const json = await apiGet<MerchantResponse | null>(MERCHANT_PATH, {}, options);\n assertNoApiErrors(json, 200);\n return json?.merchant ?? null;\n}\n\nexport interface UpdateMerchantOptions extends RequestOptions {\n /**\n * Read the merchant first and send the patch merged over it (the default).\n * Scalar fields are merged over the stored ones. Object fields\n * (`colours`, `links`, `imageIds`) are sent only when the patch has them,\n * and then replace the stored one as a whole.\n *\n * Set to `false` to send only the patch (it must then include `id`). Only\n * do that if you know the API keeps the fields you leave out.\n */\n merge?: boolean;\n}\n\n/**\n * Updates the merchant and returns it as stored.\n *\n * Shopwave's `POST /merchant` replaces the whole record: a scalar field you\n * leave out (`companyNumber`, `vatNumber`, `note`, …) is set to null, while\n * an object field you leave out (`colours`, `links`, `imageIds`) is kept and\n * one you send replaces the stored one; sending `{}` clears it (live API,\n * Sep 2026). So by default the\n * current merchant is read first and the patch is merged over it.\n *\n * `imageIds` is always sent as file names ({@link toStoredImageIds}), so you\n * can pass back what you read. See `MerchantImageIds` for the shape.\n *\n * The API answers 205 with an empty body, so the merchant is read again after\n * the save and that is what's returned (it shows exactly what was kept).\n * Throws before sending when a value is longer than the API stores\n * ({@link MERCHANT_FIELD_MAX_LENGTH}).\n */\nexport async function updateMerchant(\n patch: MerchantPatch,\n options: UpdateMerchantOptions = {}\n): Promise<Merchant> {\n const { merge = true, ...requestOptions } = options;\n\n let merchant: Record<string, unknown> = { ...patch };\n if (merge) {\n const current = await fetchMerchant(requestOptions);\n if (!current) throw new ShopwaveApiError(404, \"Shopwave API error (404): no merchant to update\");\n if (patch.id != null && String(patch.id) !== String(current.id)) {\n throw new Error(`Merchant id ${patch.id} is not the logged-in merchant (${current.id})`);\n }\n merchant = { ...current, ...patch, id: current.id };\n // Object fields the patch doesn't touch are left out: the API keeps an\n // object that isn't sent, and sending back what was read would turn\n // image file names into URLs.\n for (const field of MERCHANT_OBJECT_FIELDS) {\n if (!(field in patch)) delete merchant[field];\n }\n }\n // Reads return image URLs; the API stores file names.\n if (merchant.imageIds && typeof merchant.imageIds === \"object\") {\n merchant.imageIds = toStoredImageIds(merchant.imageIds);\n }\n if (merchant.id == null || merchant.id === \"\") throw new Error(\"merchant id is missing\");\n for (const field of MERCHANT_READ_ONLY_FIELDS) delete merchant[field];\n const problems = validateMerchant(merchant);\n if (problems.length > 0) throw new Error(`Invalid merchant: ${problems.join(\"; \")}`);\n\n const { status, body: raw } = await apiRequest(\n MERCHANT_PATH,\n { method: \"PUT\", headers: { \"Content-Type\": \"application/json\" }, body: JSON.stringify({ merchant }) },\n requestOptions\n );\n const body = raw as { merchant?: Record<string, unknown> } | null;\n assertNoApiErrors(body, status);\n\n // An echo, if the API ever sends one, is used as-is (merged over what was sent).\n const echoed = body?.merchant && typeof body.merchant === \"object\" ? body.merchant : null;\n if (echoed) return { ...merchant, ...echoed, id: Number(echoed.id ?? merchant.id) } as Merchant;\n\n // Usual case: 205 + empty body. Read back what was actually stored.\n const stored = await fetchMerchant(requestOptions);\n return stored ?? ({ ...merchant, id: Number(merchant.id) } as Merchant);\n}\n","import { apiGet, type RequestOptions } from \"./request\";\nimport { assertNoApiErrors } from \"./errors\";\nimport type { apiResponse } from \"./types\";\nimport type { Merchant } from \"./merchant\";\nimport { SHOPWAVE_RESOURCES } from \"./resources\";\n\n/** The logged-in user's employee record (`GET /user` → `user.employee`). */\nexport interface UserEmployee {\n merchantId?: number;\n /** See `rolesById`. */\n roleId?: number;\n /** Store roles, keyed by store id. */\n stores?: Record<string, unknown>;\n [field: string]: unknown;\n}\n\n/** The logged-in user (`GET /user`). */\nexport interface User {\n id: number;\n firstName: string;\n lastName: string;\n email: string;\n employee?: UserEmployee | null;\n /** Documented in the API reference but not returned by the live API (Sep 2026). */\n createdDate?: string;\n /** Not returned by the live API (Sep 2026); use `fetchMerchant()`. */\n merchant?: Merchant | null;\n /** Other fields the API returns are kept as-is. */\n [field: string]: unknown;\n}\n\nexport interface UserResponse {\n user: User;\n api: apiResponse;\n}\n\n/** Reads the logged-in user, or `null` when the API returns none. */\nexport async function fetchUser(options: RequestOptions = {}): Promise<User | null> {\n const json = await apiGet<UserResponse | null>(`/api/${SHOPWAVE_RESOURCES.user.route}`, {}, options);\n assertNoApiErrors(json, 200);\n return json?.user ?? null;\n}\n","import { useCallback, useEffect, useReducer, useState } from \"react\";\nimport { ShopwaveApiError } from \"../core/errors\";\n\n/**\n * Consistent return shape for every auto-fetching (\"query\") hook.\n */\nexport interface QueryState<T> {\n /**\n * The latest data for the current params. Kept while a `refetch()` is in\n * flight (and after a failed refetch), so lists don't blank out and flash a\n * skeleton after every save. Reset to `null` when the params change.\n */\n data: T | null;\n /** True while there's no data to show yet (first load, or params changed). */\n loading: boolean;\n /** True whenever a request is in flight, including background refetches. */\n fetching: boolean;\n error: string | null;\n /** HTTP status of the last error (0 = no response), or `null`. */\n errorStatus: number | null;\n /** Re-runs the underlying fetch, keeping the current data visible meanwhile. */\n refetch: () => void;\n}\n\n/**\n * Consistent return shape for manually-triggered (\"mutation\") hooks.\n */\nexport interface MutationState<TData, TArgs extends any[]> {\n /** Triggers the request. Resolves to the result, or `null` on error. */\n mutate: (...args: TArgs) => Promise<TData | null>;\n data: TData | null;\n loading: boolean;\n error: string | null;\n /** HTTP status of the last error (0 = no response), or `null`. */\n errorStatus: number | null;\n}\n\nfunction toMessage(e: unknown): string {\n if (e instanceof Error) return e.message;\n return String(e);\n}\n\nfunction toStatus(e: unknown): number | null {\n return e instanceof ShopwaveApiError ? e.status : null;\n}\n\n// ---------------------------------------------------------------------------\n// Query state machine (pure, unit-tested)\n// ---------------------------------------------------------------------------\n\nexport interface QueryInternalState<T> {\n data: T | null;\n /** Params key the `data` belongs to. */\n key: string | null;\n fetching: boolean;\n error: string | null;\n errorStatus: number | null;\n}\n\nexport type QueryAction<T> =\n | { type: \"start\"; key: string }\n | { type: \"success\"; key: string; data: T }\n | { type: \"failure\"; key: string; error: string; errorStatus: number | null }\n | { type: \"idle\" };\n\nexport function queryReducer<T>(state: QueryInternalState<T>, action: QueryAction<T>): QueryInternalState<T> {\n switch (action.type) {\n case \"start\":\n // New params: the old data describes something else, so drop it.\n if (action.key !== state.key) {\n return { data: null, key: action.key, fetching: true, error: null, errorStatus: null };\n }\n // Same params (refetch): keep showing what we have.\n return { ...state, fetching: true, error: null, errorStatus: null };\n case \"success\":\n if (action.key !== state.key) return state;\n return { data: action.data, key: action.key, fetching: false, error: null, errorStatus: null };\n case \"failure\":\n if (action.key !== state.key) return state;\n return { ...state, fetching: false, error: action.error, errorStatus: action.errorStatus };\n case \"idle\":\n return state.fetching ? { ...state, fetching: false } : state;\n default:\n return state;\n }\n}\n\n/** Public view of the internal state for the params currently being rendered. */\nexport function selectQueryView<T>(\n state: QueryInternalState<T>,\n key: string,\n enabled: boolean\n): Pick<QueryState<T>, \"data\" | \"loading\" | \"fetching\" | \"error\" | \"errorStatus\"> {\n // Params changed but the effect hasn't run yet: don't show the old data.\n if (state.key !== key) {\n return { data: null, loading: enabled, fetching: enabled, error: null, errorStatus: null };\n }\n return {\n data: state.data,\n loading: state.fetching && state.data === null,\n fetching: state.fetching,\n error: state.error,\n errorStatus: state.errorStatus,\n };\n}\n\nfunction depsKey(deps: ReadonlyArray<unknown>): string {\n try {\n return JSON.stringify(deps);\n } catch {\n return String(deps);\n }\n}\n\n/**\n * Internal primitive powering the auto-fetch hooks. Runs `fetcher` on mount and\n * whenever `deps` change, tracks loading/error/data, cancels in-flight requests\n * on unmount or dep change, and exposes `refetch` (which keeps the current data\n * visible until the new data arrives).\n *\n * When `enabled` is false the fetch is skipped (used for hooks that need\n * arguments before they can run, e.g. consumers-by-id).\n */\nexport function useQuery<T>(\n fetcher: (signal: AbortSignal) => Promise<T>,\n deps: ReadonlyArray<unknown>,\n enabled = true\n): QueryState<T> {\n const key = depsKey(deps);\n const [state, dispatch] = useReducer(queryReducer<T>, {\n data: null,\n key: enabled ? key : null,\n fetching: enabled,\n error: null,\n errorStatus: null,\n } as QueryInternalState<T>);\n const [tick, setTick] = useState(0);\n\n const refetch = useCallback(() => setTick((t) => t + 1), []);\n\n useEffect(() => {\n if (!enabled) {\n dispatch({ type: \"idle\" });\n return;\n }\n\n const controller = new AbortController();\n dispatch({ type: \"start\", key });\n\n fetcher(controller.signal)\n .then((result) => {\n if (!controller.signal.aborted) dispatch({ type: \"success\", key, data: result });\n })\n .catch((e) => {\n if (!controller.signal.aborted && (e as { name?: string })?.name !== \"AbortError\") {\n dispatch({ type: \"failure\", key, error: toMessage(e), errorStatus: toStatus(e) });\n }\n });\n\n return () => {\n controller.abort();\n };\n // `fetcher` is intentionally excluded; callers pass stable `deps`.\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [key, tick, enabled]);\n\n return { ...selectQueryView(state, key, enabled), refetch };\n}\n\n/**\n * Internal primitive powering the manually-triggered hooks.\n */\nexport function useMutation<TData, TArgs extends any[]>(\n action: (...args: TArgs) => Promise<TData>,\n deps: ReadonlyArray<unknown>\n): MutationState<TData, TArgs> {\n const [data, setData] = useState<TData | null>(null);\n const [loading, setLoading] = useState(false);\n const [error, setError] = useState<string | null>(null);\n const [errorStatus, setErrorStatus] = useState<number | null>(null);\n\n const mutate = useCallback(\n async (...args: TArgs): Promise<TData | null> => {\n setLoading(true);\n setError(null);\n setErrorStatus(null);\n try {\n const result = await action(...args);\n setData(result);\n return result;\n } catch (e) {\n setError(toMessage(e));\n setErrorStatus(toStatus(e));\n return null;\n } finally {\n setLoading(false);\n }\n },\n // eslint-disable-next-line react-hooks/exhaustive-deps\n deps\n );\n\n return { mutate, data, loading, error, errorStatus };\n}\n","import {\n fetchCategories,\n type Category,\n type FetchCategoriesParams,\n type RequestOptions,\n} from \"../core\";\nimport { useQuery, type QueryState } from \"./shared\";\n\n/**\n * Auto-fetches categories on mount and whenever the params change.\n * Manual reloads are available via `refetch` (replaces the old `reloadFlag`).\n */\nexport function useCategory(\n params: FetchCategoriesParams = {},\n options?: RequestOptions\n): QueryState<Category[]> {\n return useQuery<Category[]>(\n (signal) => fetchCategories(params, { ...options, signal }),\n [JSON.stringify(params), options?.baseUrl, options?.token]\n );\n}\n\nexport default useCategory;\n","import {\n fetchConsumers,\n type Consumer,\n type RequestOptions,\n} from \"../core\";\nimport { useQuery, type QueryState } from \"./shared\";\n\n/**\n * Auto-fetches consumers by id. Disabled (no request) until at least one id is\n * supplied, mirroring the original early-return behaviour.\n */\nexport function useConsumer(\n consumerIds: number[] | null,\n options?: RequestOptions\n): QueryState<Consumer[]> {\n const enabled = !!consumerIds && consumerIds.length > 0;\n return useQuery<Consumer[]>(\n (signal) => fetchConsumers(consumerIds, { ...options, signal }),\n [JSON.stringify(consumerIds), options?.baseUrl, options?.token],\n enabled\n );\n}\n\nexport default useConsumer;\n","import {\n fetchEmployees,\n type Employee,\n type FetchEmployeesParams,\n type RequestOptions,\n} from \"../core\";\nimport { useQuery, type QueryState } from \"./shared\";\n\n/**\n * Auto-fetches employees on mount and whenever the params change.\n */\nexport function useEmployee(\n params: FetchEmployeesParams = {},\n options?: RequestOptions\n): QueryState<Employee[]> {\n return useQuery<Employee[]>(\n (signal) => fetchEmployees(params, { ...options, signal }),\n [JSON.stringify(params), options?.baseUrl, options?.token]\n );\n}\n\nexport default useEmployee;\n","import {\n fetchProducts,\n type Product,\n type FetchProductsParams,\n type RequestOptions,\n} from \"../core\";\nimport { useQuery, type QueryState } from \"./shared\";\n\n/**\n * Auto-fetches products on mount and whenever the params change.\n */\nexport function useProduct(\n params: FetchProductsParams = {},\n options?: RequestOptions\n): QueryState<Product[]> {\n return useQuery<Product[]>(\n (signal) => fetchProducts(params, { ...options, signal }),\n [JSON.stringify(params), options?.baseUrl, options?.token]\n );\n}\n\nexport default useProduct;\n","import { fetchProductInstanceTags, fetchProductTags, type RequestOptions } from \"../core\";\nimport { useQuery, type QueryState } from \"./shared\";\n\ntype LookupOptions = RequestOptions & {\n /** Fetch only when true (e.g. once a tag field is first opened). Defaults to true. */\n enabled?: boolean;\n};\n\n/**\n * The merchant's distinct product tags, for a tag field's suggestions.\n * `data` is `string[]` once loaded.\n *\n * @example\n * const [opened, setOpened] = useState(false);\n * const { data: tags, loading } = useProductTags({ enabled: opened });\n */\nexport function useProductTags(options?: LookupOptions): QueryState<string[]> {\n return useQuery<string[]>(\n (signal) => fetchProductTags({ ...options, signal }),\n [options?.baseUrl, options?.token],\n options?.enabled ?? true\n );\n}\n\n/**\n * The merchant's distinct price-instance (channel) tags: Eat In, Takeout, …\n * `data` is `string[]` once loaded.\n */\nexport function useProductInstanceTags(options?: LookupOptions): QueryState<string[]> {\n return useQuery<string[]>(\n (signal) => fetchProductInstanceTags({ ...options, signal }),\n [options?.baseUrl, options?.token],\n options?.enabled ?? true\n );\n}\n\nexport default useProductTags;\n","import {\n fetchPromotions,\n type Promotion,\n type FetchPromotionsParams,\n type RequestOptions,\n} from \"../core\";\nimport { useQuery, type QueryState } from \"./shared\";\n\n/**\n * Auto-fetches promotions on mount and whenever the params change.\n * Manual reloads are available via `refetch`.\n */\nexport function usePromotion(\n params: FetchPromotionsParams = {},\n options?: RequestOptions\n): QueryState<Promotion[]> {\n return useQuery<Promotion[]>(\n (signal) => fetchPromotions(params, { ...options, signal }),\n [JSON.stringify(params), options?.baseUrl, options?.token]\n );\n}\n\nexport default usePromotion;\n","import {\n fetchStores,\n type Store,\n type FetchStoresParams,\n type RequestOptions,\n} from \"../core\";\nimport { useQuery, type QueryState } from \"./shared\";\n\n/**\n * Auto-fetches stores on mount. Manual reloads via `refetch`\n * (replaces the old `reloadFlag` argument).\n */\nexport function useStore(\n params: FetchStoresParams = {},\n options?: RequestOptions\n): QueryState<Store[]> {\n return useQuery<Store[]>(\n (signal) => fetchStores(params, { ...options, signal }),\n [JSON.stringify(params), options?.baseUrl, options?.token]\n );\n}\n\nexport default useStore;\n","import {\n fetchReport,\n type ReportQueryMap,\n type ReportResponse,\n type RequestOptions,\n} from \"../core\";\nimport { useQuery, type QueryState } from \"./shared\";\n\n/**\n * Auto-fetches one or more reports. Disabled (no request) until a non-null\n * query is supplied, mirroring the original early-return behaviour.\n */\nexport function useReport(\n query: ReportQueryMap | null,\n options?: RequestOptions\n): QueryState<ReportResponse[\"reports\"]> {\n const enabled = !!query;\n return useQuery<ReportResponse[\"reports\"]>(\n (signal) => fetchReport(query as ReportQueryMap, { ...options, signal }),\n [JSON.stringify(query), options?.baseUrl, options?.token],\n enabled\n );\n}\n\nexport default useReport;\n","import { deleteEntity, type RequestOptions } from \"../core\";\nimport { useMutation, type MutationState } from \"./shared\";\n\n/**\n * Manually-triggered delete. Returns `{ mutate, loading, error, data }`.\n *\n * Note: the original `useHandleDelete` surfaced notistack toasts. That UI\n * concern is left to the host app — handle success/error from the returned\n * state or the promise resolved by `mutate`.\n *\n * @example\n * const { mutate, loading } = useDelete();\n * await mutate(\"products\", 42);\n */\nexport function useDelete(\n options?: RequestOptions\n): MutationState<void, [resourcePath: string, entityId: number | string]> {\n return useMutation<void, [string, number | string]>(\n (resourcePath, entityId) => deleteEntity(resourcePath, entityId, options),\n [options?.baseUrl, options?.token]\n );\n}\n\nexport default useDelete;\n","import {\n submitEntity,\n type SubmitEntityArgs,\n type RequestOptions,\n} from \"../core\";\nimport { useMutation, type MutationState } from \"./shared\";\n\n/**\n * Manually-triggered create/update. Returns `{ mutate, loading, error, data }`,\n * where `data` is the API `result` payload.\n *\n * The original `useHandleSubmit` bundled validation, payload transformation and\n * notistack toasts. Those are app concerns: resolve the endpoint/method/payload\n * in your component and pass them in, then react to the returned state.\n *\n * @example\n * const { mutate, loading } = useSubmit<Product>();\n * const saved = await mutate({\n * endpoint: id ? `/api/products/${id}` : \"/api/products\",\n * method: id ? \"PUT\" : \"POST\",\n * payload,\n * });\n */\nexport function useSubmit<R = any>(\n options?: RequestOptions\n): MutationState<R | undefined, [args: SubmitEntityArgs]> {\n return useMutation<R | undefined, [SubmitEntityArgs]>(\n (args) => submitEntity<R>(args, options),\n [options?.baseUrl, options?.token]\n );\n}\n\nexport default useSubmit;\n","import { logout, type RequestOptions } from \"../core\";\nimport { useMutation, type MutationState } from \"./shared\";\n\n/**\n * Manually-triggered logout (ends the server session).\n *\n * This is framework-agnostic and does NOT redirect. The original `useLogout`\n * also pushed the browser to the auth-server logout URL via `next/navigation`\n * and `'use server'` actions; that part is Next-specific and stays in your app.\n *\n * @example\n * const { mutate: doLogout } = useLogout();\n * await doLogout();\n * router.push(logoutUrl); // app-specific redirect\n */\nexport function useLogout(\n options?: RequestOptions\n): MutationState<void, []> {\n return useMutation<void, []>(\n () => logout(options),\n [options?.baseUrl, options?.token]\n );\n}\n\nexport default useLogout;\n","import { fetchSession, type RequestOptions, type SessionStatus } from \"../core\";\nimport { useQuery, type QueryState } from \"./shared\";\n\n/**\n * Current login status (`{ loggedIn, expiresAt }`) from `GET /api/session`.\n * Tokens are never exposed to the browser.\n *\n * @example\n * const { data: session, loading } = useSession();\n * if (!loading && !session?.loggedIn) window.location.href = loginPath(location.pathname);\n */\nexport function useSession(options?: RequestOptions): QueryState<SessionStatus> {\n return useQuery<SessionStatus>(\n (signal) => fetchSession({ ...options, signal }),\n [options?.baseUrl]\n );\n}\n\nexport default useSession;\n","import {\n fetchMerchant,\n updateMerchant,\n type Merchant,\n type MerchantPatch,\n type RequestOptions,\n type UpdateMerchantOptions,\n} from \"../core\";\nimport { useMutation, useQuery, type MutationState, type QueryState } from \"./shared\";\n\n/**\n * The logged-in user's merchant, fetched on mount. `data` is `null` while\n * loading or when the API has no merchant.\n *\n * @example\n * const { data: merchant, loading, refetch } = useMerchant();\n */\nexport function useMerchant(options?: RequestOptions): QueryState<Merchant | null> {\n return useQuery<Merchant | null>(\n (signal) => fetchMerchant({ ...options, signal }),\n [options?.baseUrl, options?.token]\n );\n}\n\n/**\n * Manually-triggered merchant update (see `updateMerchant`: the patch is merged\n * over the current merchant by default). Resolves to the saved merchant, or\n * `null` on error (then `error` / `errorStatus` are set).\n *\n * @example\n * const { mutate: saveMerchant, loading: saving } = useUpdateMerchant();\n * const saved = await saveMerchant({ vatNumber: \"GB123\" });\n */\nexport function useUpdateMerchant(options?: UpdateMerchantOptions): MutationState<Merchant, [MerchantPatch]> {\n return useMutation<Merchant, [MerchantPatch]>(\n (patch) => updateMerchant(patch, options),\n [options?.baseUrl, options?.token, options?.merge]\n );\n}\n\nexport default useMerchant;\n","import { fetchUser, type RequestOptions, type User } from \"../core\";\nimport { useQuery, type QueryState } from \"./shared\";\n\n/**\n * The logged-in user, fetched on mount. `errorStatus` is 401 when logged out.\n *\n * @example\n * const { data: user } = useUser();\n */\nexport function useUser(options?: RequestOptions & { enabled?: boolean }): QueryState<User | null> {\n const enabled = options?.enabled ?? true;\n return useQuery<User | null>(\n (signal) => fetchUser({ ...options, signal }),\n [options?.baseUrl, options?.token],\n enabled\n );\n}\n\nexport default useUser;\n","import {\n uploadImage,\n uploadMerchantImage,\n type MerchantImageSlot,\n type RequestOptions,\n type UploadImageParams,\n type UploadMerchantImageOptions,\n type UploadedImage,\n} from \"../core\";\nimport { useMutation, type MutationState } from \"./shared\";\n\n/**\n * Manually-triggered image upload. Resolves to `{ id, url }`, or `null` on\n * error (then `error` / `errorStatus` are set).\n *\n * @example\n * const { mutate: upload, loading: uploading } = useUploadImage();\n * const image = await upload(file, { kind: \"merchant\" });\n */\nexport function useUploadImage(options?: RequestOptions): MutationState<UploadedImage, [Blob, UploadImageParams]> {\n return useMutation<UploadedImage, [Blob, UploadImageParams]>(\n (file, params) => uploadImage(file, params, options),\n [options?.baseUrl, options?.token]\n );\n}\n\n/**\n * Manually-triggered upload for a merchant image slot (`receipt`, `square`,\n * `featured`): fits the image to the slot's exact size, uploads it with the\n * slot's kind, and resolves to `{ id, url }`. Put `id` into `imageIds` with\n * `setMerchantImage`.\n *\n * @example\n * const { mutate: upload } = useUploadMerchantImage();\n * const image = await upload(file, \"square\");\n */\nexport function useUploadMerchantImage(\n options?: UploadMerchantImageOptions\n): MutationState<UploadedImage, [Blob, MerchantImageSlot]> {\n return useMutation<UploadedImage, [Blob, MerchantImageSlot]>(\n (file, slot) => uploadMerchantImage(file, slot, options),\n [options?.baseUrl, options?.token, options?.fit]\n );\n}\n\nexport default useUploadImage;\n","import { useCallback, useEffect, useMemo, useState } from \"react\";\nimport {\n buildBasketReportQuery,\n buildConsumerMap,\n collectConsumerIds,\n collectProductIds,\n combineBasketRows,\n computeBasketSummary,\n fetchProductsMap,\n parseBasketReportData,\n DEFAULT_BASKET_FILTERS,\n DEFAULT_PAGINATION,\n type BasketFilters,\n type BasketPagination,\n type BasketReportRow,\n type BasketSummary,\n type Product,\n type RequestOptions,\n} from \"../core\";\nimport useReport from \"./useReport\";\nimport useConsumer from \"./useConsumer\";\n\nexport interface UseBasketReportReturn {\n rows: BasketReportRow[];\n summary: BasketSummary;\n loading: boolean;\n error: string | null;\n filters: BasketFilters;\n setFilters: (filters: Partial<BasketFilters>) => void;\n pagination: BasketPagination;\n setPagination: (page: number, pageSize: number) => void;\n refresh: () => void;\n}\n\n/**\n * Composite hook that orchestrates the report → consumers → products pipeline\n * for the basket report screen. All the data-shaping logic now lives in the\n * framework-agnostic `core/basket` helpers; this hook is the thin React glue\n * (state, memoisation, effects) on top of them.\n */\nexport function useBasketReport(\n options?: RequestOptions\n): UseBasketReportReturn {\n const [filters, setFiltersState] = useState<BasketFilters>(\n DEFAULT_BASKET_FILTERS\n );\n const [pagination, setPaginationState] = useState<BasketPagination>(\n DEFAULT_PAGINATION\n );\n const [refreshFlag, setRefreshFlag] = useState(0);\n\n // Build the report query from filters (core helper).\n const reportQuery = useMemo(\n () => buildBasketReportQuery(filters),\n // refreshFlag forces a new object identity to re-trigger the fetch.\n // eslint-disable-next-line react-hooks/exhaustive-deps\n [filters, refreshFlag]\n );\n\n const {\n data: reports,\n loading: basketLoading,\n error: basketError,\n } = useReport(reportQuery, options);\n\n // Flatten raw report rows into structured baskets (core helper).\n const basketData = useMemo(() => parseBasketReportData(reports), [reports]);\n\n // Derive id sets for the dependent fetches (core helpers).\n const consumerIds = useMemo(\n () => collectConsumerIds(basketData),\n [basketData]\n );\n const productIds = useMemo(\n () => collectProductIds(basketData),\n [basketData]\n );\n\n const {\n data: consumers,\n loading: consumerLoading,\n error: consumerError,\n } = useConsumer(consumerIds, options);\n\n // Batched product fetch (core helper handles the 200-per-request batching).\n const [productMap, setProductMap] = useState<{ [id: number]: Product }>({});\n const [productLoading, setProductLoading] = useState(false);\n const [productError, setProductError] = useState<string | null>(null);\n\n useEffect(() => {\n if (!productIds || productIds.length === 0) {\n setProductMap({});\n return;\n }\n\n let active = true;\n setProductLoading(true);\n setProductError(null);\n\n fetchProductsMap(productIds, options)\n .then((map) => {\n if (active) setProductMap(map);\n })\n .catch((e) => {\n if (active) {\n setProductError(\n e instanceof Error ? e.message : \"Failed to fetch products\"\n );\n }\n })\n .finally(() => {\n if (active) setProductLoading(false);\n });\n\n return () => {\n active = false;\n };\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [JSON.stringify(productIds), options?.baseUrl, options?.token]);\n\n // Consumer lookup (core helper).\n const consumerMap = useMemo(() => buildConsumerMap(consumers), [consumers]);\n\n // Combine into final rows (core helper: join + search + sort).\n const rows: BasketReportRow[] = useMemo(\n () =>\n combineBasketRows({\n baskets: basketData,\n consumerMap,\n productMap,\n search: filters.search,\n }),\n [basketData, consumerMap, productMap, filters.search]\n );\n\n // Summary (core helper).\n const summary: BasketSummary = useMemo(\n () => computeBasketSummary(rows),\n [rows]\n );\n\n // Keep pagination total in sync with fetched data.\n useEffect(() => {\n if (rows.length > 0 && rows.length === pagination.pageSize) {\n setPaginationState((prev) => ({\n ...prev,\n total: (pagination.page + 2) * pagination.pageSize,\n }));\n } else if (rows.length > 0) {\n setPaginationState((prev) => ({\n ...prev,\n total: pagination.page * pagination.pageSize + rows.length,\n }));\n }\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [rows.length, pagination.page, pagination.pageSize]);\n\n const setFilters = useCallback((newFilters: Partial<BasketFilters>) => {\n setFiltersState((prev) => ({ ...prev, ...newFilters }));\n setPaginationState((prev) => ({ ...prev, page: 0 }));\n }, []);\n\n const setPagination = useCallback((page: number, pageSize: number) => {\n setPaginationState((prev) => ({ ...prev, page, pageSize }));\n }, []);\n\n const refresh = useCallback(() => {\n setRefreshFlag((prev) => prev + 1);\n }, []);\n\n const loading = basketLoading || consumerLoading || productLoading;\n const error = basketError || consumerError || productError;\n\n return {\n rows,\n summary,\n loading,\n error,\n filters,\n setFilters,\n pagination,\n setPagination,\n refresh,\n };\n}\n\nexport default useBasketReport;\n"],"mappings":"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"}
1
+ {"version":3,"file":"index.cjs","names":["headers: Record<string, string>","merged: Extras","response: Response","extras: Record<string, unknown>","extras: Record<string, unknown>","extras: Record<string, unknown>","result: { [id: number]: Product }","SHOPWAVE_RESOURCES: Readonly<Record<ResourceKind, ResourceDefinition>>","UPLOAD_KINDS: Readonly<Record<UploadKind, string>>","UPLOAD_PREFIXES: ReadonlyArray<UploadPrefix>","SHOPWAVE_LOOKUPS: Readonly<Record<LookupKind, LookupDefinition>>","list: unknown[]","out: string[]","extras: Record<string, unknown>","extras: Record<string, unknown>","canvas","ctx","MERCHANT_IMAGE_SLOTS: Readonly<Record<MerchantImageSlot, MerchantImageSlotDefinition>>","out: 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 }","MERCHANT_LINK_PATHS: Readonly<Record<MerchantLinkKey, readonly [string, string]>>","MERCHANT_OBJECT_FIELDS: ReadonlyArray<string>","MERCHANT_READ_ONLY_FIELDS: ReadonlyArray<string>","MERCHANT_FIELD_MAX_LENGTH: Readonly<Record<string, number>>","problems: string[]","merchant: Record<string, unknown>","useReport","useConsumer","rows: BasketReportRow[]","summary: BasketSummary"],"sources":["../../src/core/errors.ts","../../src/core/request.ts","../../src/core/category.ts","../../src/core/consumer.ts","../../src/core/employee.ts","../../src/core/product.ts","../../src/core/resources.ts","../../src/core/productTags.ts","../../src/core/promotion.ts","../../src/core/store.ts","../../src/core/report.ts","../../src/core/session.ts","../../src/core/entity.ts","../../src/core/imageFit.ts","../../src/core/upload.ts","../../src/core/merchantImages.ts","../../src/core/basket.ts","../../src/core/merchant.ts","../../src/core/user.ts","../../src/hooks/shared.ts","../../src/hooks/useCategory.ts","../../src/hooks/useConsumer.ts","../../src/hooks/useEmployee.ts","../../src/hooks/useProduct.ts","../../src/hooks/useProductTags.ts","../../src/hooks/usePromotion.ts","../../src/hooks/useStore.ts","../../src/hooks/useReport.ts","../../src/hooks/useDelete.ts","../../src/hooks/useSubmit.ts","../../src/hooks/useLogout.ts","../../src/hooks/useSession.ts","../../src/hooks/useMerchant.ts","../../src/hooks/useUser.ts","../../src/hooks/useUploadImage.ts","../../src/hooks/useBasketReport.ts"],"sourcesContent":["import type { ErrorMessage } from \"./types\";\n\n/** Map of Shopwave API errors as found in `api.message.errors`, keyed by error code. */\nexport type ShopwaveErrorMap = Record<string, Partial<ErrorMessage>>;\n\n/**\n * Thrown by every `core` function when a request fails, either at the HTTP\n * level (`status` is the HTTP status) or because the Shopwave response carried\n * `api.message.errors` (then `status` is the HTTP status of that response,\n * usually 200/201, and `errors` holds the API errors).\n *\n * Branch on `status` (e.g. `isUnauthorized` → send the user to log in) rather\n * than parsing `message`.\n */\nexport class ShopwaveApiError extends Error {\n /** HTTP status of the response (0 when the request never got a response). */\n readonly status: number;\n /** Shopwave API errors, when the response carried any. */\n readonly errors: ShopwaveErrorMap | null;\n /** Parsed response body, when there was one. */\n readonly body?: unknown;\n\n constructor(\n status: number,\n message: string,\n details: { errors?: ShopwaveErrorMap | null; body?: unknown; cause?: unknown } = {}\n ) {\n super(message, details.cause !== undefined ? { cause: details.cause } : undefined);\n this.name = \"ShopwaveApiError\";\n this.status = status;\n this.errors = details.errors ?? null;\n this.body = details.body;\n }\n\n /** 401, or the Shopwave \"token expired / invalid\" error 908. */\n get isUnauthorized(): boolean {\n if (this.status === 401) return true;\n return !!this.errors && Object.entries(this.errors).some(([k, v]) => k === \"908\" || Number(v?.id) === 908);\n }\n\n get isNotFound(): boolean {\n return this.status === 404;\n }\n}\n\n/** The `api.message.errors` (or `api.message.error`) map of a Shopwave response, or `null` when there are none. */\nexport function getApiErrorMap(body: unknown): ShopwaveErrorMap | null {\n // The API reference names the map `error`; responses seen so far use `errors`.\n const message = (body as { api?: { message?: { errors?: unknown; error?: unknown } } } | null)?.api?.message;\n const errors = message?.errors ?? message?.error;\n if (errors && typeof errors === \"object\" && Object.keys(errors).length > 0) {\n return errors as ShopwaveErrorMap;\n }\n return null;\n}\n\nfunction describeErrors(errors: ShopwaveErrorMap): string {\n return Object.entries(errors)\n .map(([code, e]) => {\n const text = [e?.title, e?.details].filter(Boolean).join(\": \");\n return text ? `${code} ${text}` : code;\n })\n .join(\"; \");\n}\n\n/**\n * Builds a {@link ShopwaveApiError} for a response. Understands the Shopwave\n * envelope (`api.message.errors`) and the `{ error, message }` bodies returned\n * by the `react-shopwave-connect/next` routes.\n */\nexport function toShopwaveApiError(status: number, body: unknown, fallback?: string): ShopwaveApiError {\n const errors = getApiErrorMap(body);\n let detail = errors ? describeErrors(errors) : \"\";\n if (!detail && body && typeof body === \"object\") {\n const b = body as { message?: unknown; error?: unknown };\n if (typeof b.message === \"string\") detail = b.message;\n else if (typeof b.error === \"string\") detail = b.error;\n }\n if (!detail) detail = fallback ?? (status >= 400 ? \"request failed\" : \"API returned errors\");\n return new ShopwaveApiError(status, `Shopwave API error (${status}): ${detail}`, { errors, body });\n}\n\n/** Throws a {@link ShopwaveApiError} when the response body carries API errors. */\nexport function assertNoApiErrors(body: unknown, status: number): void {\n if (getApiErrorMap(body)) throw toShopwaveApiError(status, body);\n}\n","import type { apiResponse } from \"./types\";\nimport { ShopwaveApiError, getApiErrorMap, toShopwaveApiError } from \"./errors\";\n\n/**\n * Per-call configuration shared by every core API function.\n *\n * Nothing here is React-specific. In the browser/Next leave `baseUrl` empty to\n * call the app's own `/api/...` routes (which hold the session). From Node, a\n * CLI or tests, pass an absolute `baseUrl` pointing at such an app plus a\n * `token`.\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`). From Node pass the app origin,\n * e.g. `http://localhost:3000`.\n */\n baseUrl?: string;\n\n /**\n * Shopwave OAuth access token. Sent as `Authorization: OAuth <token>` on every\n * request (GET, POST, PUT and DELETE alike). A value that already starts with\n * a scheme (`OAuth …` / `Bearer …`) is sent as-is. Leave unset in the browser:\n * the app's routes use the session cookie.\n */\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\n/** Options with `token` filled in from a legacy per-call `params.token`, unless `options.token` is set. */\nexport function withToken(options: RequestOptions, token: string | undefined): RequestOptions {\n return token && !options.token ? { ...options, token } : options;\n}\n\n/** `Authorization` header value for a token (`OAuth <token>` unless it already has a scheme). */\nexport function authorizationFor(token: string): string {\n const t = token.trim();\n return /^(oauth|bearer)\\s+\\S/i.test(t) ? t : `OAuth ${t}`;\n}\n\n/** Headers shared by every request: Accept + the token, when one is given. */\nfunction baseHeaders(options: RequestOptions): Record<string, string> {\n const headers: Record<string, string> = { Accept: \"application/json\" };\n if (options.token) headers.Authorization = authorizationFor(options.token);\n return headers;\n}\n\nfunction buildExtras(extras: Extras): Extras {\n const merged: Extras = { \"Content-Type\": \"application/json\", ...extras };\n // Tokens never travel inside extras any more (see RequestOptions.token).\n delete merged.token;\n return merged;\n}\n\n/** Parses a response body as JSON; `null` for an empty body, `{ raw }` for non-JSON. */\nasync function readBody(response: Response): Promise<unknown> {\n const text = await response.text();\n if (!text) return null;\n try {\n return JSON.parse(text);\n } catch {\n return { raw: text };\n }\n}\n\n/**\n * Low-level request: resolves `{ status, body }` for 2xx responses and throws\n * {@link ShopwaveApiError} otherwise. `init.headers` are merged over the\n * standard ones (Accept + Authorization).\n */\nexport async function apiRequest(\n path: string,\n init: RequestInit,\n options: RequestOptions = {}\n): Promise<{ status: number; body: unknown }> {\n return send(path, { ...init, headers: { ...baseHeaders(options), ...(init.headers as Record<string, string>) } }, options);\n}\n\nasync function send(path: string, init: RequestInit, options: RequestOptions): Promise<{ status: number; body: unknown }> {\n const doFetch = resolveFetch(options);\n const url = (options.baseUrl ?? \"\") + path;\n\n let response: Response;\n try {\n response = await doFetch(url, { ...init, signal: options.signal });\n } catch (error) {\n if ((error as { name?: string })?.name === \"AbortError\") throw error;\n throw new ShopwaveApiError(0, `Shopwave API request failed: ${(error as Error)?.message ?? error}`, { cause: error });\n }\n\n const body = await readBody(response);\n if (!response.ok) {\n throw toShopwaveApiError(response.status, body, response.statusText || undefined);\n }\n return { status: response.status, body };\n}\n\n/**\n * Returns a JSON string of API errors if the response carries any, otherwise\n * `null`. Kept for backwards compatibility; prefer `ShopwaveApiError`.\n */\nexport function getApiErrors(api?: apiResponse): string | null {\n const errors = getApiErrorMap({ api });\n return errors ? JSON.stringify(errors) : null;\n}\n\n/**\n * GET helper. Sends request metadata via the `extras` header, parses JSON and\n * returns the payload. Throws {@link ShopwaveApiError} for non-2xx responses.\n */\nexport async function apiGet<T>(path: string, extras: Extras, options: RequestOptions = {}): Promise<T> {\n const { body } = await send(\n path,\n {\n method: \"GET\",\n headers: { ...baseHeaders(options), extras: JSON.stringify(buildExtras(extras)) },\n },\n options\n );\n return body as T;\n}\n\n/**\n * JSON body helper for POST/PUT mutations. Returns the parsed body (`null` for\n * an empty one). Throws {@link ShopwaveApiError} for non-2xx responses.\n */\nexport async function apiSend<T>(\n path: string,\n method: \"POST\" | \"PUT\",\n payload: unknown,\n options: RequestOptions = {}\n): Promise<T> {\n const { body } = await send(\n path,\n {\n method,\n headers: { ...baseHeaders(options), \"Content-Type\": \"application/json\" },\n body: JSON.stringify(payload),\n },\n options\n );\n return body as T;\n}\n\n/**\n * DELETE helper. Shopwave answers deletes with 205 and an empty body; if a body\n * does come back and carries API errors, they are thrown. Returns the parsed\n * body (`null` when empty).\n */\nexport async function apiDelete(path: string, options: RequestOptions = {}): Promise<unknown> {\n const { status, body } = await send(path, { method: \"DELETE\", headers: baseHeaders(options) }, options);\n if (getApiErrorMap(body)) throw toShopwaveApiError(status, body);\n return body;\n}\n","import { apiGet, withToken, type RequestOptions } from \"./request\";\nimport { assertNoApiErrors } from \"./errors\";\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 /** @deprecated Pass `options.token` instead. Sent the same way (Authorization header). */\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\n const json = await apiGet<CategoryResponse | null>(\"/api/categories\", extras, withToken(options, params.token));\n\n assertNoApiErrors(json, 200);\n\n // Shopwave answers an empty body when nothing matches.\n return Object.values(json?.categories ?? {});\n}\n","import { apiGet, type RequestOptions } from \"./request\";\nimport { assertNoApiErrors } from \"./errors\";\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 | null>(\"/api/consumer\", extras, options);\n\n assertNoApiErrors(json, 200);\n\n // Shopwave answers an empty body when nothing matches.\n return Object.values(json?.consumers ?? {});\n}\n","import { apiGet, withToken, type RequestOptions } from \"./request\";\nimport { assertNoApiErrors } from \"./errors\";\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 /** @deprecated Pass `options.token` instead. Sent the same way (Authorization header). */\n token?: string;\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 | null>(\"/api/employees\", extras, withToken(options, params.token));\n\n assertNoApiErrors(json, 200);\n\n // Shopwave answers an empty body when nothing matches.\n return Object.values(json?.employees ?? {});\n}\n","import { apiGet, withToken, type RequestOptions } from \"./request\";\nimport { assertNoApiErrors } from \"./errors\";\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 /** @deprecated Pass `options.token` instead. Sent the same way (Authorization header). */\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\n const json = await apiGet<ProductResponse | null>(\"/api/products\", extras, withToken(options, params.token));\n\n assertNoApiErrors(json, 200);\n\n // Shopwave answers an empty body when nothing matches.\n return Object.values(json?.products ?? {});\n}\n\n// ---------------------------------------------------------------------------\n// Instances (prices) on a product save\n// ---------------------------------------------------------------------------\n//\n// Shopwave API reference, POST /product: \"Product instances cannot be updated —\n// they are stored for historical reference. To change pricing, remove the old\n// instance and create a new one.\" An instance is therefore only sent with its\n// `id` when it is unchanged; a new or re-priced instance is sent without one, and\n// Shopwave gives it a new id. (adminV2 does the same and warns before saving.)\n\n/** An instance as a form or caller holds it: `id` is absent for new rows. */\nexport type ProductInstanceInput = {\n id?: number | string | null;\n price?: string | number | null;\n taxPercentage?: string | number | null;\n name?: string | null;\n};\n\n/** Instances as the API returns them (keyed by id) or as a plain list. */\nexport type ProductInstances<T = ProductInstanceInput> =\n | Readonly<Record<string, T>>\n | Readonly<Record<number, T>>\n | ReadonlyArray<T>;\n\n/**\n * What `saveProduct` accepts: any product fields, with `instances` either as the\n * API returns them or as built by {@link toProductInstancesForSave}. Include `id`\n * to update; leave it out to create.\n */\nexport type ProductSaveInput = Partial<Omit<Product, \"instances\">> & {\n instances?: ProductInstances;\n [field: string]: unknown;\n};\n\n/** Fields the server sets on an instance; never sent back. */\nconst SERVER_INSTANCE_FIELDS = [\"timestamp\"] as const;\n\nfunction instanceList<T>(instances: ProductInstances<T> | null | undefined): T[] {\n if (!instances) return [];\n const list: unknown[] = Array.isArray(instances) ? [...instances] : Object.values(instances);\n return list.filter(\n (inst): inst is T => inst != null && typeof inst === \"object\"\n );\n}\n\nfunction hasId(id: unknown): id is number | string {\n return id != null && String(id).trim() !== \"\";\n}\n\n/** Same number, so `\"422.0\"`, `\"422\"` and `422` are equal; blanks equal each other. */\nfunction sameAmount(a: unknown, b: unknown): boolean {\n const blank = (v: unknown) => v == null || String(v).trim() === \"\";\n if (blank(a) || blank(b)) return blank(a) && blank(b);\n const x = Number(a);\n const y = Number(b);\n return Number.isNaN(x) || Number.isNaN(y) ? String(a).trim() === String(b).trim() : x === y;\n}\n\n/** True when two instances have the same price and tax (the fields Shopwave won't update). */\nexport function isSameInstancePrice(a: ProductInstanceInput, b: ProductInstanceInput): boolean {\n return sameAmount(a.price, b.price) && sameAmount(a.taxPercentage, b.taxPercentage);\n}\n\n/** An instance whose price or tax was edited, so saving it will give it a new id. */\nexport interface RepricedInstance {\n /** Position of the instance in the list that was passed in. */\n index: number;\n /** The id it has now, which Shopwave will replace. */\n id: number | string;\n name: string;\n before: { price: string; taxPercentage: string };\n after: { price: string; taxPercentage: string };\n}\n\nconst text = (v: unknown) => (v == null ? \"\" : String(v));\n\n/**\n * Lists the instances whose price or tax differs from the `original` instance\n * with the same id: saving them creates new instances with new ids, which can\n * affect anything that stores instance ids (integrations, reports, modifiers).\n * Use it to warn before saving. New rows (no id) aren't listed.\n */\nexport function findRepricedInstances(\n instances: ProductInstances | null | undefined,\n original: ProductInstances | null | undefined\n): RepricedInstance[] {\n const byId = new Map(instanceList(original).filter((o) => hasId(o.id)).map((o) => [String(o.id), o]));\n const result: RepricedInstance[] = [];\n instanceList(instances).forEach((inst, index) => {\n if (!hasId(inst.id)) return;\n const before = byId.get(String(inst.id));\n if (!before || isSameInstancePrice(inst, before)) return;\n result.push({\n index,\n id: inst.id,\n name: text(inst.name),\n before: { price: text(before.price), taxPercentage: text(before.taxPercentage) },\n after: { price: text(inst.price), taxPercentage: text(inst.taxPercentage) },\n });\n });\n return result;\n}\n\n/**\n * Builds the `instances` map for a product save:\n * - keyed by position (`\"0\"`, `\"1\"`, …), like every other ref in a save;\n * - an instance keeps its `id` only when `original` has an instance with that id\n * and the same price and tax; otherwise the id is removed, so Shopwave creates\n * a new instance instead of re-pointing or rewriting someone else's;\n * - server fields (`timestamp`) are removed.\n *\n * Pass the product as last read from the API as `original`. Leave it out for a\n * new product (including a duplicate): every id is then removed.\n * Instances left out of the list are left out of the save (removed).\n */\nexport function toProductInstancesForSave<T extends ProductInstanceInput>(\n instances: ProductInstances<T> | null | undefined,\n original?: ProductInstances | null\n): Record<string, Omit<T, \"id\"> & { id?: number | string }> {\n const keep = new Map(instanceList(original).filter((o) => hasId(o.id)).map((o) => [String(o.id), o]));\n const out: Record<string, Omit<T, \"id\"> & { id?: number | string }> = {};\n instanceList(instances).forEach((inst, index) => {\n const { id, ...rest } = inst;\n for (const field of SERVER_INSTANCE_FIELDS) delete (rest as Record<string, unknown>)[field];\n const before = hasId(id) ? keep.get(String(id)) : undefined;\n out[String(index)] = (before && isSameInstancePrice(inst, before) ? { id, ...rest } : rest) as Omit<T, \"id\"> & {\n id?: number | string;\n };\n });\n return out;\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","/**\n * Singleton resources (one per logged-in user) and the uploader.\n *\n * Like `SHOPWAVE_ENTITIES`, this table is shared by the client functions\n * (`fetchMerchant`, `fetchUser`, `uploadImage`, …) and the server route\n * handlers in `react-shopwave-connect/server`, so the two halves of the contract\n * can't drift apart. These aren't in `SHOPWAVE_ENTITIES` because they aren't\n * keyed collections: `GET /merchant` answers `{ merchant: {…} }`, not a map by id.\n */\n\nexport interface ResourceDefinition {\n /** App route: `/api/<route>`. */\n route: string;\n /** Key of the object in requests and responses (`{ merchant: {…} }`). */\n key: string;\n /** Shopwave API path. */\n upstream: string;\n /** False for read-only resources. */\n writable: boolean;\n}\n\nexport type ResourceKind = \"merchant\" | \"user\";\n\n/**\n * - merchant: `GET /merchant`; `POST /merchant` with `{ merchant: { id, … } }`\n * updates it (without an `id` the API would create a second merchant, so the\n * route requires one).\n * - user: `GET /user` (read-only here; the API's `POST /user` invites users and\n * needs the admin scope).\n */\nexport const SHOPWAVE_RESOURCES: Readonly<Record<ResourceKind, ResourceDefinition>> = Object.freeze({\n merchant: { route: \"merchant\", key: \"merchant\", upstream: \"merchant\", writable: true },\n user: { route: \"user\", key: \"user\", upstream: \"user\", writable: false },\n});\n\n/**\n * What an upload is for. Sent to Shopwave as the `contentType` header of\n * `PUT /uploader`. `merchant` is for the receipt and square logos (stored\n * under `…/merchant/<id>/logo/`), `merchantFeature` for the featured image.\n * The API spells two of its values `applicaionLogo` / `applicaionImages`; use\n * the correctly spelt names here and the server maps them.\n */\nexport type UploadKind = \"merchant\" | \"merchantFeature\" | \"product\" | \"user\" | \"applicationLogo\" | \"applicationImages\";\n\n/** `UploadKind` → the API's `contentType` value. */\nexport const UPLOAD_KINDS: Readonly<Record<UploadKind, string>> = Object.freeze({\n merchant: \"merchant\",\n /** The merchant's featured image (adminV1; not in the API reference). */\n merchantFeature: \"merchantFeature\",\n product: \"product\",\n user: \"user\",\n applicationLogo: \"applicaionLogo\",\n applicationImages: \"applicaionImages\",\n});\n\n/**\n * The role an image plays, sent to Shopwave as the uploader's `prefix` header.\n * The uploader names the file `<prefix>_<hash>.<ext>`, so the role travels with\n * the file wherever a record stores it (a product's `images`). Live, merchant\n * 5644, 4 Nov 2026: all four values answered 201.\n * - `product`: a product gallery image;\n * - `product_thumbnail`: the product's lead image, shown in lists;\n * - `variant` / `variant_thumbnail`: a variant's own image and its lead image\n * (used by the Connect integrations).\n *\n * Values are the ones Connect's `uploadFileToShopwave` sends.\n */\nexport type UploadPrefix = \"product\" | \"product_thumbnail\" | \"variant\" | \"variant_thumbnail\";\n\n/** Every `UploadPrefix`, longest first so a name check never takes `product` for `product_thumbnail`. */\nexport const UPLOAD_PREFIXES: ReadonlyArray<UploadPrefix> = Object.freeze([\n \"product_thumbnail\",\n \"variant_thumbnail\",\n \"product\",\n \"variant\",\n] as const);\n\n/** How uploads are addressed (app route and Shopwave uploader). */\nexport const SHOPWAVE_UPLOAD = Object.freeze({\n /** App route: `POST /api/<route>` (multipart `file` + `kind`, optional `prefix`). */\n route: \"upload\",\n /** Shopwave path: `PUT {apiUrl}/uploader` (multipart `file`). */\n upstream: \"uploader\",\n /** Header that carries the upload kind upstream. */\n kindHeader: \"contentType\",\n /** Header that carries the image role upstream (see `UploadPrefix`). */\n prefixHeader: \"prefix\",\n /** Multipart field name for the file, on both sides. */\n fileField: \"file\",\n /** Multipart field name for the kind in the app request. */\n kindField: \"kind\",\n /** Multipart field name for the image role in the app request. */\n prefixField: \"prefix\",\n});\n\nexport function isUploadKind(value: unknown): value is UploadKind {\n return typeof value === \"string\" && Object.prototype.hasOwnProperty.call(UPLOAD_KINDS, value);\n}\n\nexport function isUploadPrefix(value: unknown): value is UploadPrefix {\n return typeof value === \"string\" && (UPLOAD_PREFIXES as ReadonlyArray<string>).includes(value);\n}\n\n/**\n * Read-only lists that aren't records: the merchant's distinct tags.\n *\n * Like `SHOPWAVE_RESOURCES`, shared by the client functions\n * (`fetchProductTags`, `fetchProductInstanceTags`) and the server route\n * handlers (`shopwave.lookup(kind)`), so the two halves can't drift apart.\n *\n * - productTags: `GET /product/tags` → `{ tags: string[] }`, every distinct tag\n * on the merchant's products. No parameters. The API reference says\n * \"internal SKU tags are excluded\", but the live API includes system tags\n * written by imports and syncs (`PID-133`, `VID-137`, `#Hash:…`, `upc-…`;\n * test merchant 5644, 29 Oct 2026). Apps that offer the list as suggestions\n * filter those themselves. Tags of deleted products drop out of the list.\n * - productInstanceTags: `GET /product/instance/tags` → `{ tags: string[] }`,\n * the same for price instances (channels such as \"Eat In\", \"Takeout\").\n *\n * The app route for instance tags is `products/instance-tags`, one segment\n * under `products`, so it sits beside `products/[id]` without nesting deeper.\n */\nexport interface LookupDefinition {\n /** App route: `/api/<route>`. */\n route: string;\n /** Key of the list in the response (`{ tags: [...] }`). */\n key: string;\n /** Shopwave API path. */\n upstream: string;\n}\n\nexport type LookupKind = \"productTags\" | \"productInstanceTags\";\n\nexport const SHOPWAVE_LOOKUPS: Readonly<Record<LookupKind, LookupDefinition>> = Object.freeze({\n productTags: { route: \"products/tags\", key: \"tags\", upstream: \"product/tags\" },\n productInstanceTags: { route: \"products/instance-tags\", key: \"tags\", upstream: \"product/instance/tags\" },\n});\n","import { apiGet, type RequestOptions } from \"./request\";\nimport { assertNoApiErrors } from \"./errors\";\nimport { SHOPWAVE_LOOKUPS, type LookupKind } from \"./resources\";\n\n/**\n * Turns a tag list from the API into clean strings: trims each tag, drops\n * blanks and anything that isn't text, and removes exact duplicates (first one\n * wins). The API's own order is kept. A map is accepted too (its values are\n * used), in case the API ever answers keyed by id.\n */\nexport function normalizeTagList(value: unknown): string[] {\n const list: unknown[] = Array.isArray(value)\n ? value\n : value && typeof value === \"object\"\n ? Object.values(value as Record<string, unknown>)\n : [];\n const seen = new Set<string>();\n const out: string[] = [];\n for (const item of list) {\n if (typeof item !== \"string\" && typeof item !== \"number\") continue;\n const tag = String(item).trim();\n if (!tag || seen.has(tag)) continue;\n seen.add(tag);\n out.push(tag);\n }\n return out;\n}\n\nasync function fetchLookup(kind: LookupKind, options: RequestOptions): Promise<string[]> {\n const def = SHOPWAVE_LOOKUPS[kind];\n if (!def) throw new Error(`Unknown Shopwave lookup \"${kind}\"`);\n const json = await apiGet<Record<string, unknown> | null>(`/api/${def.route}`, {}, options);\n assertNoApiErrors(json, 200);\n // Shopwave answers an empty body when there's nothing to return.\n return normalizeTagList(json?.[def.key]);\n}\n\n/**\n * Every distinct tag used on the merchant's products (`GET /product/tags`),\n * for a tag field's suggestions. Returns `[]` when there are none.\n */\nexport function fetchProductTags(options: RequestOptions = {}): Promise<string[]> {\n return fetchLookup(\"productTags\", options);\n}\n\n/**\n * Every distinct tag used on the merchant's price instances\n * (`GET /product/instance/tags`): the channel names (Eat In, Takeout, …).\n * Returns `[]` when there are none.\n */\nexport function fetchProductInstanceTags(options: RequestOptions = {}): Promise<string[]> {\n return fetchLookup(\"productInstanceTags\", options);\n}\n","import { apiGet, withToken, type RequestOptions } from \"./request\";\nimport { assertNoApiErrors } from \"./errors\";\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 /** @deprecated Pass `options.token` instead. Sent the same way (Authorization header). */\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\n const json = await apiGet<PromotionResponse | null>(\"/api/promotions\", extras, withToken(options, params.token));\n\n assertNoApiErrors(json, 200);\n\n // Shopwave answers an empty body when nothing matches.\n return Object.values(json?.promotions ?? {});\n}\n","import { apiGet, withToken, type RequestOptions } from \"./request\";\nimport { assertNoApiErrors } from \"./errors\";\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 /** Optional id filter. */\n storeIds?: Array<number>;\n /** Whether to include soft-deleted records. Defaults to `false`. */\n deleted?: boolean;\n /** @deprecated Pass `options.token` instead. Sent the same way (Authorization header). */\n token?: string;\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 if (params.storeIds) {\n extras.storeIds = params.storeIds;\n }\n\n const json = await apiGet<StoreResponse | null>(\"/api/stores\", extras, withToken(options, params.token));\n\n assertNoApiErrors(json, 200);\n\n // Shopwave answers an empty body when nothing matches.\n return Object.values(json?.stores ?? {});\n}\n","import { apiGet, type RequestOptions } from \"./request\";\nimport { assertNoApiErrors } from \"./errors\";\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 | null>(\"/api/report\", extras, options);\n\n assertNoApiErrors(json, 200);\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\";\nimport { assertNoApiErrors } from \"./errors\";\n\n/**\n * Low-level, untyped mutations: the caller supplies the route and payload.\n *\n * Prefer the typed functions in `./entities` (`saveProduct`, `deleteCategory`,\n * …): they know each route, build the request envelope, check the response and\n * return the saved entity with its id. Keep these for routes that have no typed\n * function yet (e.g. `/api/merchant`).\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. Returns the response's `result` field when it\n * has one (older app routes wrapped their answer that way), otherwise the whole\n * parsed response body. Throws `ShopwaveApiError` on HTTP or API errors.\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> | null>(\n endpoint,\n method,\n payload,\n options\n );\n assertNoApiErrors(json, 200);\n if (json && typeof json === \"object\" && \"result\" in json) return json.result;\n return (json ?? undefined) as R | undefined;\n}\n","/**\n * Browser helpers for getting an image to the right size before upload.\n *\n * Shopwave doesn't resize uploads, so images are scaled and centre-cropped in\n * the browser: to an exact size for the merchant image slots\n * ({@link fitImageToSize}), or to a rule (ratio, minimum, maximum) with\n * {@link prepareImage}, which apps use to give each kind of image its own rule.\n */\n\n/** Source rectangle to draw so the image covers `dstWidth × dstHeight` (centre crop, no stretching). */\nexport interface CropRect {\n sx: number;\n sy: number;\n sw: number;\n sh: number;\n}\n\n/**\n * The centred part of a `srcWidth × srcHeight` image with the aspect ratio of\n * `dstWidth × dstHeight` (like CSS `object-fit: cover`).\n */\nexport function coverCrop(srcWidth: number, srcHeight: number, dstWidth: number, dstHeight: number): CropRect {\n if (![srcWidth, srcHeight, dstWidth, dstHeight].every((n) => Number.isFinite(n) && n > 0)) {\n throw new Error(\"coverCrop: sizes must be positive numbers\");\n }\n const srcRatio = srcWidth / srcHeight;\n const dstRatio = dstWidth / dstHeight;\n if (srcRatio > dstRatio) {\n // Too wide: keep the full height, trim the sides.\n const sw = Math.round(srcHeight * dstRatio);\n return { sx: Math.round((srcWidth - sw) / 2), sy: 0, sw, sh: srcHeight };\n }\n // Too tall (or exact): keep the full width, trim top and bottom.\n const sh = Math.round(srcWidth / dstRatio);\n return { sx: 0, sy: Math.round((srcHeight - sh) / 2), sw: srcWidth, sh };\n}\n\nexport interface ImageSize {\n width: number;\n height: number;\n}\n\nfunction requireBrowserImages(fn: string): void {\n if (typeof createImageBitmap !== \"function\") {\n throw new Error(`${fn} needs a browser (createImageBitmap is not available)`);\n }\n}\n\n/** Reads an image file's pixel size (browser only). */\nexport async function readImageSize(file: Blob): Promise<ImageSize> {\n requireBrowserImages(\"readImageSize\");\n const bitmap = await createImageBitmap(file);\n try {\n return { width: bitmap.width, height: bitmap.height };\n } finally {\n bitmap.close?.();\n }\n}\n\nexport interface FitImageOptions {\n /** Output type. Default `\"image/png\"`. */\n type?: \"image/png\" | \"image/jpeg\" | \"image/webp\";\n /** 0–1, for JPEG/WebP. */\n quality?: number;\n /** Return the file untouched when it's already the right size and type. Default true. */\n keepIfExact?: boolean;\n}\n\n/** Draws `crop` of the bitmap at `width × height` and encodes it (one resample). */\nasync function drawCrop(\n bitmap: ImageBitmap,\n crop: CropRect,\n width: number,\n height: number,\n type: string,\n quality: number | undefined,\n fn: string\n): Promise<Blob> {\n const { sx, sy, sw, sh } = crop;\n if (typeof OffscreenCanvas === \"function\") {\n const canvas = new OffscreenCanvas(width, height);\n const ctx = canvas.getContext(\"2d\");\n if (!ctx) throw new Error(`${fn}: no 2D canvas context`);\n ctx.imageSmoothingQuality = \"high\";\n ctx.drawImage(bitmap, sx, sy, sw, sh, 0, 0, width, height);\n return canvas.convertToBlob({ type, quality });\n }\n const canvas = document.createElement(\"canvas\");\n canvas.width = width;\n canvas.height = height;\n const ctx = canvas.getContext(\"2d\");\n if (!ctx) throw new Error(`${fn}: no 2D canvas context`);\n ctx.imageSmoothingQuality = \"high\";\n ctx.drawImage(bitmap, sx, sy, sw, sh, 0, 0, width, height);\n return new Promise<Blob>((resolve, reject) =>\n canvas.toBlob((b) => (b ? resolve(b) : reject(new Error(`${fn}: encoding failed`))), type, quality)\n );\n}\n\n/** The input's name with the output type's extension. */\nfunction outputName(file: Blob, type: string): string {\n const name = (file as File).name || \"image\";\n const ext = type === \"image/jpeg\" ? \"jpg\" : type.split(\"/\")[1];\n return `${name.replace(/\\.[^.]+$/, \"\")}.${ext}`;\n}\n\n/**\n * Returns the image at exactly `width × height`: scaled and centre-cropped\n * (never stretched). Browser only. The result is a `File` named like the\n * input, with the output type's extension.\n */\nexport async function fitImageToSize(file: Blob, width: number, height: number, options: FitImageOptions = {}): Promise<File> {\n requireBrowserImages(\"fitImageToSize\");\n const { type = \"image/png\", quality, keepIfExact = true } = options;\n const outName = outputName(file, type);\n\n const bitmap = await createImageBitmap(file);\n try {\n if (keepIfExact && bitmap.width === width && bitmap.height === height && file.type === type) {\n return file instanceof File ? file : new File([file], outName, { type });\n }\n const blob = await drawCrop(bitmap, coverCrop(bitmap.width, bitmap.height, width, height), width, height, type, quality, \"fitImageToSize\");\n return new File([blob], outName, { type });\n } finally {\n bitmap.close?.();\n }\n}\n\n// ---------------------------------------------------------------------------\n// Image rules\n// ---------------------------------------------------------------------------\n\n/**\n * How an image must end up before upload. Every field is optional; give an\n * exact size (`width` + `height`) or an `aspectRatio`, not both.\n *\n * Images are always scaled down first and then centre-cropped (never\n * stretched): the kept area is the largest one with the right shape.\n *\n * @example\n * // \"Cropped to 100:77, at least 600 × 462\", at most 2000 px on the long side\n * { aspectRatio: \"100:77\", minWidth: 600, minHeight: 462, maxLongSide: 2000 }\n * // Exactly 600 × 462\n * { width: 600, height: 462 }\n * // Any shape, scaled down to fit 2000 px\n * { maxLongSide: 2000 }\n */\nexport interface ImageRule {\n /** Exact output width (needs `height`). */\n width?: number;\n /** Exact output height (needs `width`). */\n height?: number;\n /** Shape to crop to: `1.3`, `\"100:77\"`, `\"100/77\"` or `[100, 77]`. */\n aspectRatio?: number | string | readonly [number, number];\n /** Smallest acceptable output; a smaller image is refused (see `allowUpscale`). */\n minWidth?: number;\n minHeight?: number;\n /** Largest output; a bigger image is scaled down to fit. */\n maxWidth?: number;\n maxHeight?: number;\n /** Largest output on the longer side, whichever it is. */\n maxLongSide?: number;\n /** Enlarge a too-small image instead of refusing it. Default false. */\n allowUpscale?: boolean;\n}\n\n/** What {@link prepareImage} will do to an image, worked out without touching it. */\nexport interface ImageFitPlan {\n /** The area of the source that's kept. */\n crop: CropRect;\n /** Output size. */\n width: number;\n height: number;\n /** True when the source already fits the rule, so it can be uploaded as it is. */\n unchanged: boolean;\n}\n\n/** A rule that can't be met by any image (a mistake in the rule, not the image). */\nexport class ImageRuleError extends Error {\n constructor(message: string) {\n super(message);\n this.name = \"ImageRuleError\";\n }\n}\n\n/** The image is smaller than the rule's minimum (and `allowUpscale` is off). */\nexport class ImageTooSmallError extends Error {\n /** The smallest size the rule accepts. */\n readonly required: ImageSize;\n /** The largest area of the right shape the image could give. */\n readonly actual: ImageSize;\n constructor(required: ImageSize, actual: ImageSize) {\n super(`The image is too small: it needs to be at least ${required.width} × ${required.height}, and it's ${actual.width} × ${actual.height}`);\n this.name = \"ImageTooSmallError\";\n this.required = required;\n this.actual = actual;\n }\n}\n\nconst isPositive = (n: unknown): n is number => typeof n === \"number\" && Number.isFinite(n) && n > 0;\n\n/**\n * An aspect ratio as width ÷ height: `\"100:77\"`, `\"100/77\"`, `[100, 77]` →\n * 1.2987…; `1.3` → 1.3. Throws {@link ImageRuleError} for anything else.\n */\nexport function parseAspectRatio(value: number | string | readonly [number, number]): number {\n let parts: [unknown, unknown] | null = null;\n if (typeof value === \"number\") return isPositive(value) ? value : badRatio(value);\n if (Array.isArray(value)) parts = [value[0], value[1]];\n else if (typeof value === \"string\") {\n const text = value.trim();\n const match = /^(\\d+(?:\\.\\d+)?)\\s*[:/x×]\\s*(\\d+(?:\\.\\d+)?)$/i.exec(text);\n if (match) parts = [Number(match[1]), Number(match[2])];\n else if (/^\\d+(?:\\.\\d+)?$/.test(text)) return isPositive(Number(text)) ? Number(text) : badRatio(value);\n }\n if (parts && isPositive(parts[0]) && isPositive(parts[1])) return parts[0] / parts[1];\n return badRatio(value);\n}\n\nfunction badRatio(value: unknown): never {\n throw new ImageRuleError(`Not an aspect ratio: ${JSON.stringify(value)} (use e.g. \"100:77\", [100, 77] or 1.3)`);\n}\n\n/** Throws {@link ImageRuleError} when the rule contradicts itself. */\nexport function checkImageRule(rule: ImageRule): void {\n if (!rule || typeof rule !== \"object\") throw new ImageRuleError(\"An image rule must be an object\");\n const sizes = [\"width\", \"height\", \"minWidth\", \"minHeight\", \"maxWidth\", \"maxHeight\", \"maxLongSide\"] as const;\n for (const key of sizes) {\n if (rule[key] != null && !isPositive(rule[key])) throw new ImageRuleError(`${key} must be a positive number`);\n }\n const exact = rule.width != null || rule.height != null;\n if (exact) {\n if (rule.width == null || rule.height == null) throw new ImageRuleError(\"An exact size needs both width and height\");\n if (rule.aspectRatio != null) throw new ImageRuleError(\"Give an exact size or an aspectRatio, not both (the size sets the ratio)\");\n const limits = [\"minWidth\", \"minHeight\", \"maxWidth\", \"maxHeight\", \"maxLongSide\"] as const;\n const extra = limits.filter((key) => rule[key] != null);\n if (extra.length > 0) throw new ImageRuleError(`An exact size is already the minimum and the maximum; remove ${extra.join(\", \")}`);\n return;\n }\n if (rule.aspectRatio != null) parseAspectRatio(rule.aspectRatio);\n if (rule.minWidth != null && rule.maxWidth != null && rule.minWidth > rule.maxWidth) {\n throw new ImageRuleError(\"minWidth is larger than maxWidth\");\n }\n if (rule.minHeight != null && rule.maxHeight != null && rule.minHeight > rule.maxHeight) {\n throw new ImageRuleError(\"minHeight is larger than maxHeight\");\n }\n if (rule.maxLongSide != null && Math.max(rule.minWidth ?? 0, rule.minHeight ?? 0) > rule.maxLongSide) {\n throw new ImageRuleError(\"The minimum size is larger than maxLongSide\");\n }\n}\n\n/**\n * Works out what to do to a `source`-sized image to meet `rule`: the centred\n * crop of the right shape (the largest one), then the output size (scaled\n * down to the maximum, never below the minimum). Pure: no browser needed.\n *\n * Throws {@link ImageTooSmallError} when the crop is below the minimum (or\n * the exact size) and `allowUpscale` is off, and {@link ImageRuleError} for a\n * contradictory rule.\n *\n * @example\n * planImageFit({ width: 6000, height: 4000 }, { aspectRatio: \"100:77\", minWidth: 600, minHeight: 462, maxLongSide: 2000 })\n * // → crop { sx: 403, sy: 0, sw: 5194, sh: 4000 }, 2000 × 1540\n */\nexport function planImageFit(source: ImageSize, rule: ImageRule): ImageFitPlan {\n checkImageRule(rule);\n const { width: W, height: H } = source ?? ({} as ImageSize);\n if (!isPositive(W) || !isPositive(H)) throw new Error(\"planImageFit: the image has no size\");\n\n const exact = rule.width != null && rule.height != null;\n const ratio = exact ? (rule.width as number) / (rule.height as number) : rule.aspectRatio != null ? parseAspectRatio(rule.aspectRatio) : null;\n const crop: CropRect = ratio == null ? { sx: 0, sy: 0, sw: W, sh: H } : coverCrop(W, H, ratio, 1);\n const { sw, sh } = crop;\n const kept = { width: sw, height: sh };\n\n let width: number;\n let height: number;\n if (exact) {\n width = rule.width as number;\n height = rule.height as number;\n if ((sw < width || sh < height) && !rule.allowUpscale) throw new ImageTooSmallError({ width, height }, kept);\n } else {\n const minW = rule.minWidth ?? 0;\n const minH = rule.minHeight ?? 0;\n if ((sw < minW || sh < minH) && !rule.allowUpscale) {\n throw new ImageTooSmallError(requiredSize(minW, minH, ratio), kept);\n }\n let scale = 1;\n if (rule.maxWidth != null) scale = Math.min(scale, rule.maxWidth / sw);\n if (rule.maxHeight != null) scale = Math.min(scale, rule.maxHeight / sh);\n if (rule.maxLongSide != null) scale = Math.min(scale, rule.maxLongSide / Math.max(sw, sh));\n // The minimum wins over the maximum (and enlarges a small image when allowed).\n scale = Math.max(scale, minW / sw, minH / sh);\n width = Math.max(1, Math.round(sw * scale));\n height = Math.max(1, Math.round(sh * scale));\n }\n\n const unchanged = crop.sx === 0 && crop.sy === 0 && sw === W && sh === H && width === W && height === H;\n return { crop, width, height, unchanged };\n}\n\n/** The minimum as a full size: a ratio rule with only one minimum implies the other. */\nfunction requiredSize(minW: number, minH: number, ratio: number | null): ImageSize {\n if (ratio == null) return { width: minW, height: minH };\n return {\n width: Math.max(minW, Math.ceil(minH * ratio)),\n height: Math.max(minH, Math.ceil(minW / ratio)),\n };\n}\n\nexport interface PrepareImageOptions {\n /** Output type. Default: the source's type when it's PNG, JPEG or WebP, else PNG. */\n type?: \"image/png\" | \"image/jpeg\" | \"image/webp\";\n /** 0–1, for JPEG and WebP. Default 0.9. */\n quality?: number;\n}\n\nconst OUTPUT_TYPES = [\"image/png\", \"image/jpeg\", \"image/webp\"];\n\n/**\n * Returns the image meeting `rule` ({@link planImageFit}): centre-cropped to\n * the right shape and scaled in a single draw. An image that already fits is\n * returned untouched (same bytes). Browser only.\n *\n * Throws {@link ImageTooSmallError} (show `required` and `actual` to the\n * person) or {@link ImageRuleError}; an unreadable file rejects with the\n * browser's decoding error.\n *\n * @example\n * const ready = await prepareImage(file, { aspectRatio: \"100:77\", minWidth: 600, minHeight: 462, maxLongSide: 2000 });\n * await uploadImage(ready, { kind: \"product\", prefix: \"product\" });\n */\nexport async function prepareImage(file: Blob, rule: ImageRule, options: PrepareImageOptions = {}): Promise<File> {\n checkImageRule(rule);\n requireBrowserImages(\"prepareImage\");\n const sourceType = file.type && OUTPUT_TYPES.includes(file.type) ? file.type : \"image/png\";\n const type = options.type ?? sourceType;\n const quality = options.quality ?? (type === \"image/png\" ? undefined : 0.9);\n\n const bitmap = await createImageBitmap(file);\n try {\n const plan = planImageFit({ width: bitmap.width, height: bitmap.height }, rule);\n if (plan.unchanged && type === file.type) {\n return file instanceof File ? file : new File([file], outputName(file, type), { type });\n }\n const blob = await drawCrop(bitmap, plan.crop, plan.width, plan.height, type, quality, \"prepareImage\");\n return new File([blob], outputName(file, type), { type });\n } finally {\n bitmap.close?.();\n }\n}\n","import { apiRequest, type RequestOptions } from \"./request\";\nimport { ShopwaveApiError, assertNoApiErrors } from \"./errors\";\nimport { SHOPWAVE_UPLOAD, isUploadKind, isUploadPrefix, type UploadKind, type UploadPrefix } from \"./resources\";\n\nexport interface UploadImageParams {\n /** What the image is for (sent to Shopwave as the uploader's `contentType`). */\n kind: UploadKind;\n /**\n * The image's role (sent to Shopwave as the uploader's `prefix` header), e.g.\n * `product_thumbnail` for a product's lead image. Leave it out for images\n * without a role (merchant logos).\n */\n prefix?: UploadPrefix;\n /** File name to send when `file` is a Blob without one. Defaults to `File.name`, else `\"upload\"`. */\n fileName?: string;\n}\n\nexport interface UploadedImage {\n /** The generated file name (products store it in `images`). */\n id: string;\n /** Public HTTPS URL of the file (see {@link getImageUrl}). */\n url: string;\n /** The URL exactly as the API returned it (`http://static.merchantstack.com/images/…`). */\n path: string;\n}\n\n/**\n * HTTPS host that serves the Shopwave CDN's images. The API returns\n * `http://static.merchantstack.com/images/…`, which has no HTTPS endpoint\n * (checked Sep 2026), so pages served over HTTPS can't show it.\n */\nexport const SHOPWAVE_IMAGE_BASE_URL = \"https://s3-eu-west-1.amazonaws.com/static.merchantstack.com/images\";\n\nconst STATIC_HOST = /^https?:\\/\\/static\\.merchantstack\\.com\\/images\\//i;\n\n/**\n * Turns an image path from the API into a URL a page can load over HTTPS:\n * `http://static.merchantstack.com/images/x.png` → `<base>/x.png`, and a\n * relative path (`merchant/5644/logo/x.png`) → `<base>/merchant/5644/logo/x.png`.\n * Other absolute URLs are returned unchanged; empty input gives `\"\"`.\n */\nexport function getImageUrl(path: string | null | undefined, base: string = SHOPWAVE_IMAGE_BASE_URL): string {\n if (!path) return \"\";\n const root = base.replace(/\\/+$/, \"\");\n if (STATIC_HOST.test(path)) return `${root}/${path.replace(STATIC_HOST, \"\")}`;\n if (/^[a-z][a-z0-9+.-]*:/i.test(path) || path.startsWith(\"//\")) return path;\n return `${root}/${path.replace(/^\\/+/, \"\")}`;\n}\n\n/**\n * Uploads one image to the Shopwave CDN through the app's `/api/upload` route\n * (`createShopwaveApi(...).upload()`), and returns its id and URL.\n *\n * The file only lands on the CDN (under `…/images/<kind>/<merchantId>/…`); a\n * record has to point at it to use it. Live API (Sep 2026): the upload answers\n * 201 `{ fileName, path }`.\n */\nexport async function uploadImage(\n file: Blob,\n params: UploadImageParams,\n options: RequestOptions = {}\n): Promise<UploadedImage> {\n if (!file || typeof (file as Blob).size !== \"number\") throw new Error(\"uploadImage: file must be a File or Blob\");\n if (!isUploadKind(params?.kind)) throw new Error(`uploadImage: unknown kind \"${params?.kind}\"`);\n if (params.prefix != null && !isUploadPrefix(params.prefix)) throw new Error(`uploadImage: unknown prefix \"${params.prefix}\"`);\n\n const name = params.fileName ?? (file as File).name ?? \"upload\";\n const form = new FormData();\n form.append(SHOPWAVE_UPLOAD.fileField, file, name);\n form.append(SHOPWAVE_UPLOAD.kindField, params.kind);\n if (params.prefix != null) form.append(SHOPWAVE_UPLOAD.prefixField, params.prefix);\n\n // No Content-Type: fetch sets the multipart boundary itself.\n const { status, body: raw } = await apiRequest(`/api/${SHOPWAVE_UPLOAD.route}`, { method: \"POST\", body: form }, options);\n const body = raw as { fileName?: unknown; path?: unknown } | null;\n assertNoApiErrors(body, status);\n\n const id = typeof body?.fileName === \"string\" ? body.fileName : \"\";\n const path = typeof body?.path === \"string\" ? body.path : \"\";\n if (!id) {\n throw new ShopwaveApiError(status, `Shopwave API error (${status}): the upload response has no fileName`, { body });\n }\n return { id, url: getImageUrl(path), path };\n}\n","import type { RequestOptions } from \"./request\";\nimport type { UploadKind } from \"./resources\";\nimport { fitImageToSize, type FitImageOptions } from \"./imageFit\";\nimport { getImageUrl, uploadImage, type UploadedImage } from \"./upload\";\n\n/**\n * Merchant images, as adminV1 saves them and adminV2 reads them:\n *\n * ```\n * { logo: { receipt: \"<file>\", square: \"<file>\" }, feature: [\"<file>\"] }\n * ```\n *\n * - **Writes** take the uploader's `fileName` (`<hash>.png`), not a URL.\n * - **Reads** return full URLs (`http://static.merchantstack.com/images/…`),\n * so a value read back must be turned into a file name before it's saved\n * again ({@link toStoredImageIds}; `updateMerchant` does this for you).\n * - Sending `imageIds` replaces the whole stored object (`{}` clears it);\n * leaving it out keeps it.\n * - The API builds each URL from the key, not from where the file is:\n * `logo.*` → `…/merchant/<id>/logo/<file>`, `feature` → `…/feature/<file>`.\n * So a featured image must be uploaded with kind `merchantFeature` (which\n * stores it under `feature/`); a `merchant` upload lands under `logo/`.\n *\n * Confirmed on the live API, merchant 5644, 30 Sep 2026.\n */\nexport interface MerchantImageIds {\n logo?: {\n receipt?: string | null;\n square?: string | null;\n [key: string]: string | null | undefined;\n } | null;\n feature?: string[] | null;\n [key: string]: unknown;\n}\n\n/** The three image slots on the merchant settings page. */\nexport const MERCHANT_IMAGE_SLOT_KEYS = [\"receipt\", \"square\", \"featured\"] as const;\nexport type MerchantImageSlot = (typeof MERCHANT_IMAGE_SLOT_KEYS)[number];\n\nexport interface MerchantImageSlotDefinition {\n /** Where it's stored: `logo.receipt`, `logo.square` or `feature[0]`. */\n field: \"logo.receipt\" | \"logo.square\" | \"feature[0]\";\n /** Uploader `contentType` (the featured image has its own). */\n uploadKind: UploadKind;\n /** Exact size in pixels. adminV1 refuses any other size; the API doesn't resize. */\n width: number;\n height: number;\n}\n\n/**\n * Where each slot lives, its upload kind and its exact size (from adminV1's\n * merchant settings page).\n */\nexport const MERCHANT_IMAGE_SLOTS: Readonly<Record<MerchantImageSlot, MerchantImageSlotDefinition>> = Object.freeze({\n receipt: { field: \"logo.receipt\", uploadKind: \"merchant\", width: 576, height: 325 },\n square: { field: \"logo.square\", uploadKind: \"merchant\", width: 1000, height: 1000 },\n featured: { field: \"feature[0]\", uploadKind: \"merchantFeature\", width: 1200, height: 600 },\n});\n\n/**\n * The file name in an image value: `\"http://…/logo/abc.png\"` → `\"abc.png\"`,\n * `\"abc.png\"` → `\"abc.png\"`. Query strings and fragments are ignored.\n * Returns `null` for empty or non-string values.\n */\nexport function imageFileName(value: unknown): string | null {\n if (typeof value !== \"string\") return null;\n const clean = value.trim().replace(/[?#].*$/, \"\");\n const last = clean.split(\"/\").pop() ?? \"\";\n return last ? decodeURIComponent(last) : null;\n}\n\n/** The stored value for a slot (a URL when read from the API; a file name right after an upload). */\nexport function getMerchantImage(imageIds: MerchantImageIds | null | undefined, slot: MerchantImageSlot): string | null {\n if (!imageIds || typeof imageIds !== \"object\") return null;\n let value: unknown;\n if (slot === \"featured\") {\n value = Array.isArray(imageIds.feature) ? imageIds.feature[0] : undefined;\n } else {\n const logo = imageIds.logo;\n value = logo && typeof logo === \"object\" ? logo[slot] : undefined;\n }\n return typeof value === \"string\" && value.trim() ? value.trim() : null;\n}\n\n/**\n * An HTTPS URL for a slot's stored image, or `\"\"` when the slot is empty or\n * holds only a file name (a fresh upload not yet saved; use the upload's `url`).\n */\nexport function getMerchantImageUrl(\n imageIds: MerchantImageIds | null | undefined,\n slot: MerchantImageSlot,\n base?: string\n): string {\n const value = getMerchantImage(imageIds, slot);\n if (!value || !value.includes(\"/\")) return \"\";\n return getImageUrl(value, base);\n}\n\n/**\n * Returns a copy of `imageIds` with one slot set to `fileName` (or cleared\n * with `null`). Other slots and unknown keys are kept; an empty `logo` or\n * `feature` is removed.\n */\nexport function setMerchantImage(\n imageIds: MerchantImageIds | null | undefined,\n slot: MerchantImageSlot,\n fileName: string | null\n): MerchantImageIds {\n const next: MerchantImageIds = { ...(imageIds && typeof imageIds === \"object\" ? imageIds : {}) };\n if (slot === \"featured\") {\n const rest = Array.isArray(next.feature) ? next.feature.slice(1) : [];\n const feature = fileName ? [fileName, ...rest] : rest;\n if (feature.length > 0) next.feature = feature;\n else delete next.feature;\n } else {\n const logo = { ...(next.logo && typeof next.logo === \"object\" ? next.logo : {}) };\n if (fileName) logo[slot] = fileName;\n else delete logo[slot];\n if (Object.keys(logo).length > 0) next.logo = logo;\n else delete next.logo;\n }\n return next;\n}\n\nfunction toStored(value: unknown): unknown {\n if (typeof value === \"string\") return imageFileName(value) ?? undefined;\n if (Array.isArray(value)) {\n const items = value.map(toStored).filter((v) => v !== undefined && v !== null && v !== \"\");\n return items.length > 0 ? items : undefined;\n }\n if (value && typeof value === \"object\") {\n const out: Record<string, unknown> = {};\n for (const [k, v] of Object.entries(value as Record<string, unknown>)) {\n const stored = toStored(v);\n if (stored !== undefined && stored !== null && stored !== \"\") out[k] = stored;\n }\n return Object.keys(out).length > 0 ? out : undefined;\n }\n return undefined;\n}\n\n/**\n * The `imageIds` to send on a save: every URL is turned back into its file\n * name (what adminV1 does with images you didn't change) and empty values\n * are dropped. No images gives `{}`.\n */\nexport function toStoredImageIds(imageIds: unknown): MerchantImageIds {\n return (toStored(imageIds) as MerchantImageIds | undefined) ?? {};\n}\n\nexport interface UploadMerchantImageOptions extends RequestOptions {\n /**\n * Scale and centre-crop the image to the slot's exact size before upload\n * (browser only). Default true. With `false` the file is sent as-is.\n */\n fit?: boolean;\n /** Output type and quality when fitting. Default PNG. */\n fitOptions?: FitImageOptions;\n}\n\n/**\n * Uploads an image for a merchant slot: fits it to the slot's size, uploads\n * it with the slot's kind, and returns `{ id, url, path }`. Put `id` into\n * `imageIds` with {@link setMerchantImage} and save with `updateMerchant`.\n */\nexport async function uploadMerchantImage(\n file: Blob,\n slot: MerchantImageSlot,\n options: UploadMerchantImageOptions = {}\n): Promise<UploadedImage> {\n const def = MERCHANT_IMAGE_SLOTS[slot];\n if (!def) throw new Error(`uploadMerchantImage: unknown slot \"${slot}\"`);\n const { fit = true, fitOptions, ...requestOptions } = options;\n const body = fit ? await fitImageToSize(file, def.width, def.height, fitOptions) : file;\n return uploadImage(body, { kind: def.uploadKind }, requestOptions);\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","import { apiGet, apiRequest, type RequestOptions } from \"./request\";\nimport { ShopwaveApiError, assertNoApiErrors } from \"./errors\";\nimport type { apiResponse } from \"./types\";\nimport { SHOPWAVE_RESOURCES } from \"./resources\";\nimport { toStoredImageIds, type MerchantImageIds } from \"./merchantImages\";\n\nconst MERCHANT_PATH = `/api/${SHOPWAVE_RESOURCES.merchant.route}`;\n\n/** A colour set: hex strings (`\"#1a2b3c\"`). */\nexport interface MerchantColourSet {\n main?: string | null;\n highlight?: string | null;\n contrast?: string | null;\n [key: string]: string | null | undefined;\n}\n\n/**\n * Brand colours, as adminV1 saves them and adminV2 reads them\n * (`colours.primary.main` is the logo background): `{ primary: { main,\n * highlight, contrast } }`. `{}` when unset. Confirmed live, 30 Sep 2026.\n */\nexport interface MerchantColours {\n primary?: MerchantColourSet | null;\n [key: string]: MerchantColourSet | null | undefined;\n}\n\n/** Colour keys inside `colours.primary`, in display order. */\nexport const MERCHANT_COLOUR_KEYS = [\"main\", \"highlight\", \"contrast\"] as const;\nexport type MerchantColourKey = (typeof MERCHANT_COLOUR_KEYS)[number];\n\n/**\n * Web and social links, as adminV1 saves them:\n * `{ website: { home }, social: { twitter, facebook, instagram } }`.\n * `{}` when empty (confirmed live, 30 Sep 2026). Use {@link normalizeMerchantLinks} to read older shapes and\n * {@link merchantLinksToForm} / {@link merchantLinksFromForm} for flat forms.\n */\nexport interface MerchantLinks {\n website?: { home?: string | null; [key: string]: string | null | undefined } | null;\n social?: {\n twitter?: string | null;\n facebook?: string | null;\n instagram?: string | null;\n [key: string]: string | null | undefined;\n } | null;\n [key: string]: unknown;\n}\n\n/** Flat link fields a form edits, in display order. */\nexport const MERCHANT_LINK_KEYS = [\"website\", \"twitter\", \"facebook\", \"instagram\"] as const;\nexport type MerchantLinkKey = (typeof MERCHANT_LINK_KEYS)[number];\n\n/** Where each flat link field lives in `merchant.links`. */\nexport const MERCHANT_LINK_PATHS: Readonly<Record<MerchantLinkKey, readonly [string, string]>> = Object.freeze({\n website: [\"website\", \"home\"],\n twitter: [\"social\", \"twitter\"],\n facebook: [\"social\", \"facebook\"],\n instagram: [\"social\", \"instagram\"],\n});\n\nfunction trimmed(value: unknown): string | undefined {\n return typeof value === \"string\" && value.trim() ? value.trim() : undefined;\n}\n\n/**\n * Reads `merchant.links` as flat fields (`{ website, twitter, facebook,\n * instagram }`), whatever shape it was saved in:\n * - the nested shape adminV1 saves (`{ website: { home }, social: { … } }`);\n * - flat keys (`{ website: \"…\" }`, what SDK 0.4.0 pre-releases sent);\n * - an array in {@link MERCHANT_LINK_KEYS} order (older AdminUI versions).\n * Empty values are dropped.\n */\nexport function merchantLinksToForm(links: unknown): Partial<Record<MerchantLinkKey, string>> {\n const out: Partial<Record<MerchantLinkKey, string>> = {};\n if (Array.isArray(links)) {\n links.forEach((value, i) => {\n const key = MERCHANT_LINK_KEYS[i];\n const v = trimmed(value);\n if (key && v) out[key] = v;\n });\n return out;\n }\n if (!links || typeof links !== \"object\") return out;\n const obj = links as Record<string, unknown>;\n for (const key of MERCHANT_LINK_KEYS) {\n const [group, field] = MERCHANT_LINK_PATHS[key];\n const nested = obj[group];\n const v =\n nested && typeof nested === \"object\" && !Array.isArray(nested)\n ? trimmed((nested as Record<string, unknown>)[field])\n : trimmed(obj[key]);\n if (v) out[key] = v;\n }\n return out;\n}\n\n/**\n * Builds the stored `links` object from flat fields. Empty values are left\n * out, and so is a group with nothing in it; no links gives `{}`.\n */\nexport function merchantLinksFromForm(form: Partial<Record<MerchantLinkKey, string | null | undefined>>): MerchantLinks {\n const out: Record<string, Record<string, string>> = {};\n for (const key of MERCHANT_LINK_KEYS) {\n const v = trimmed(form[key]);\n if (!v) continue;\n const [group, field] = MERCHANT_LINK_PATHS[key];\n (out[group] ??= {})[field] = v;\n }\n return out as MerchantLinks;\n}\n\n/** Reads any stored or older `links` value into the nested shape adminV1 saves. */\nexport function normalizeMerchantLinks(links: unknown): MerchantLinks {\n return merchantLinksFromForm(merchantLinksToForm(links));\n}\n\nexport interface Merchant {\n id: number;\n name: string;\n description?: string | null;\n /** Company registration number. */\n companyNumber?: string | null;\n vatNumber?: string | null;\n categoryId?: number | null;\n estAnnualRevenue?: number | string | null;\n note?: string | null;\n links?: MerchantLinks | null;\n colours?: MerchantColours | null;\n imageIds?: MerchantImageIds | null;\n createdDate?: string;\n modifiedDate?: string;\n /** Other fields the API returns are kept as-is. */\n [field: string]: unknown;\n}\n\nexport interface MerchantResponse {\n merchant: Merchant;\n api: apiResponse;\n}\n\n/** What `updateMerchant` accepts: any subset of the merchant's fields. */\nexport type MerchantPatch = Partial<Merchant> & { [field: string]: unknown };\n\n/**\n * Object fields of the merchant. The API keeps one you leave out, and\n * replaces it as a whole when you send it.\n */\nexport const MERCHANT_OBJECT_FIELDS: ReadonlyArray<string> = [\"colours\", \"links\", \"imageIds\"];\n\n/** Fields the API sets itself; `updateMerchant` never sends them back. */\nexport const MERCHANT_READ_ONLY_FIELDS: ReadonlyArray<string> = [\"createdDate\", \"modifiedDate\"];\n\n/**\n * Longest values the API stores. Longer values are cut off silently\n * (live API, Sep 2026), so `updateMerchant` rejects them instead.\n * `vatNumber` fits a UK VAT number without the `GB` prefix.\n */\nexport const MERCHANT_FIELD_MAX_LENGTH: Readonly<Record<string, number>> = Object.freeze({\n companyNumber: 11,\n vatNumber: 9,\n});\n\n/** Returns `\"<field> is longer than <n> characters\"` messages for over-long fields. */\nexport function validateMerchant(merchant: Record<string, unknown>): string[] {\n const problems: string[] = [];\n for (const [field, max] of Object.entries(MERCHANT_FIELD_MAX_LENGTH)) {\n const value = merchant[field];\n if (typeof value === \"string\" && value.length > max) problems.push(`${field} is longer than ${max} characters`);\n }\n return problems;\n}\n\n/**\n * Reads the logged-in user's merchant (a user has exactly one), or `null`\n * when the API returns none.\n */\nexport async function fetchMerchant(options: RequestOptions = {}): Promise<Merchant | null> {\n const json = await apiGet<MerchantResponse | null>(MERCHANT_PATH, {}, options);\n assertNoApiErrors(json, 200);\n return json?.merchant ?? null;\n}\n\nexport interface UpdateMerchantOptions extends RequestOptions {\n /**\n * Read the merchant first and send the patch merged over it (the default).\n * Scalar fields are merged over the stored ones. Object fields\n * (`colours`, `links`, `imageIds`) are sent only when the patch has them,\n * and then replace the stored one as a whole.\n *\n * Set to `false` to send only the patch (it must then include `id`). Only\n * do that if you know the API keeps the fields you leave out.\n */\n merge?: boolean;\n}\n\n/**\n * Updates the merchant and returns it as stored.\n *\n * Shopwave's `POST /merchant` replaces the whole record: a scalar field you\n * leave out (`companyNumber`, `vatNumber`, `note`, …) is set to null, while\n * an object field you leave out (`colours`, `links`, `imageIds`) is kept and\n * one you send replaces the stored one; sending `{}` clears it (live API,\n * Sep 2026). So by default the\n * current merchant is read first and the patch is merged over it.\n *\n * `imageIds` is always sent as file names ({@link toStoredImageIds}), so you\n * can pass back what you read. See `MerchantImageIds` for the shape.\n *\n * The API answers 205 with an empty body, so the merchant is read again after\n * the save and that is what's returned (it shows exactly what was kept).\n * Throws before sending when a value is longer than the API stores\n * ({@link MERCHANT_FIELD_MAX_LENGTH}).\n */\nexport async function updateMerchant(\n patch: MerchantPatch,\n options: UpdateMerchantOptions = {}\n): Promise<Merchant> {\n const { merge = true, ...requestOptions } = options;\n\n let merchant: Record<string, unknown> = { ...patch };\n if (merge) {\n const current = await fetchMerchant(requestOptions);\n if (!current) throw new ShopwaveApiError(404, \"Shopwave API error (404): no merchant to update\");\n if (patch.id != null && String(patch.id) !== String(current.id)) {\n throw new Error(`Merchant id ${patch.id} is not the logged-in merchant (${current.id})`);\n }\n merchant = { ...current, ...patch, id: current.id };\n // Object fields the patch doesn't touch are left out: the API keeps an\n // object that isn't sent, and sending back what was read would turn\n // image file names into URLs.\n for (const field of MERCHANT_OBJECT_FIELDS) {\n if (!(field in patch)) delete merchant[field];\n }\n }\n // Reads return image URLs; the API stores file names.\n if (merchant.imageIds && typeof merchant.imageIds === \"object\") {\n merchant.imageIds = toStoredImageIds(merchant.imageIds);\n }\n if (merchant.id == null || merchant.id === \"\") throw new Error(\"merchant id is missing\");\n for (const field of MERCHANT_READ_ONLY_FIELDS) delete merchant[field];\n const problems = validateMerchant(merchant);\n if (problems.length > 0) throw new Error(`Invalid merchant: ${problems.join(\"; \")}`);\n\n const { status, body: raw } = await apiRequest(\n MERCHANT_PATH,\n { method: \"PUT\", headers: { \"Content-Type\": \"application/json\" }, body: JSON.stringify({ merchant }) },\n requestOptions\n );\n const body = raw as { merchant?: Record<string, unknown> } | null;\n assertNoApiErrors(body, status);\n\n // An echo, if the API ever sends one, is used as-is (merged over what was sent).\n const echoed = body?.merchant && typeof body.merchant === \"object\" ? body.merchant : null;\n if (echoed) return { ...merchant, ...echoed, id: Number(echoed.id ?? merchant.id) } as Merchant;\n\n // Usual case: 205 + empty body. Read back what was actually stored.\n const stored = await fetchMerchant(requestOptions);\n return stored ?? ({ ...merchant, id: Number(merchant.id) } as Merchant);\n}\n","import { apiGet, type RequestOptions } from \"./request\";\nimport { assertNoApiErrors } from \"./errors\";\nimport type { apiResponse } from \"./types\";\nimport type { Merchant } from \"./merchant\";\nimport { SHOPWAVE_RESOURCES } from \"./resources\";\n\n/** The logged-in user's employee record (`GET /user` → `user.employee`). */\nexport interface UserEmployee {\n merchantId?: number;\n /** See `rolesById`. */\n roleId?: number;\n /** Store roles, keyed by store id. */\n stores?: Record<string, unknown>;\n [field: string]: unknown;\n}\n\n/** The logged-in user (`GET /user`). */\nexport interface User {\n id: number;\n firstName: string;\n lastName: string;\n email: string;\n employee?: UserEmployee | null;\n /** Documented in the API reference but not returned by the live API (Sep 2026). */\n createdDate?: string;\n /** Not returned by the live API (Sep 2026); use `fetchMerchant()`. */\n merchant?: Merchant | null;\n /** Other fields the API returns are kept as-is. */\n [field: string]: unknown;\n}\n\nexport interface UserResponse {\n user: User;\n api: apiResponse;\n}\n\n/** Reads the logged-in user, or `null` when the API returns none. */\nexport async function fetchUser(options: RequestOptions = {}): Promise<User | null> {\n const json = await apiGet<UserResponse | null>(`/api/${SHOPWAVE_RESOURCES.user.route}`, {}, options);\n assertNoApiErrors(json, 200);\n return json?.user ?? null;\n}\n","import { useCallback, useEffect, useReducer, useState } from \"react\";\nimport { ShopwaveApiError } from \"../core/errors\";\n\n/**\n * Consistent return shape for every auto-fetching (\"query\") hook.\n */\nexport interface QueryState<T> {\n /**\n * The latest data for the current params. Kept while a `refetch()` is in\n * flight (and after a failed refetch), so lists don't blank out and flash a\n * skeleton after every save. Reset to `null` when the params change.\n */\n data: T | null;\n /** True while there's no data to show yet (first load, or params changed). */\n loading: boolean;\n /** True whenever a request is in flight, including background refetches. */\n fetching: boolean;\n error: string | null;\n /** HTTP status of the last error (0 = no response), or `null`. */\n errorStatus: number | null;\n /** Re-runs the underlying fetch, keeping the current data visible meanwhile. */\n refetch: () => void;\n}\n\n/**\n * Consistent return shape for manually-triggered (\"mutation\") hooks.\n */\nexport interface MutationState<TData, TArgs extends any[]> {\n /** Triggers the request. Resolves to the result, or `null` on error. */\n mutate: (...args: TArgs) => Promise<TData | null>;\n data: TData | null;\n loading: boolean;\n error: string | null;\n /** HTTP status of the last error (0 = no response), or `null`. */\n errorStatus: number | null;\n}\n\nfunction toMessage(e: unknown): string {\n if (e instanceof Error) return e.message;\n return String(e);\n}\n\nfunction toStatus(e: unknown): number | null {\n return e instanceof ShopwaveApiError ? e.status : null;\n}\n\n// ---------------------------------------------------------------------------\n// Query state machine (pure, unit-tested)\n// ---------------------------------------------------------------------------\n\nexport interface QueryInternalState<T> {\n data: T | null;\n /** Params key the `data` belongs to. */\n key: string | null;\n fetching: boolean;\n error: string | null;\n errorStatus: number | null;\n}\n\nexport type QueryAction<T> =\n | { type: \"start\"; key: string }\n | { type: \"success\"; key: string; data: T }\n | { type: \"failure\"; key: string; error: string; errorStatus: number | null }\n | { type: \"idle\" };\n\nexport function queryReducer<T>(state: QueryInternalState<T>, action: QueryAction<T>): QueryInternalState<T> {\n switch (action.type) {\n case \"start\":\n // New params: the old data describes something else, so drop it.\n if (action.key !== state.key) {\n return { data: null, key: action.key, fetching: true, error: null, errorStatus: null };\n }\n // Same params (refetch): keep showing what we have.\n return { ...state, fetching: true, error: null, errorStatus: null };\n case \"success\":\n if (action.key !== state.key) return state;\n return { data: action.data, key: action.key, fetching: false, error: null, errorStatus: null };\n case \"failure\":\n if (action.key !== state.key) return state;\n return { ...state, fetching: false, error: action.error, errorStatus: action.errorStatus };\n case \"idle\":\n return state.fetching ? { ...state, fetching: false } : state;\n default:\n return state;\n }\n}\n\n/** Public view of the internal state for the params currently being rendered. */\nexport function selectQueryView<T>(\n state: QueryInternalState<T>,\n key: string,\n enabled: boolean\n): Pick<QueryState<T>, \"data\" | \"loading\" | \"fetching\" | \"error\" | \"errorStatus\"> {\n // Params changed but the effect hasn't run yet: don't show the old data.\n if (state.key !== key) {\n return { data: null, loading: enabled, fetching: enabled, error: null, errorStatus: null };\n }\n return {\n data: state.data,\n loading: state.fetching && state.data === null,\n fetching: state.fetching,\n error: state.error,\n errorStatus: state.errorStatus,\n };\n}\n\nfunction depsKey(deps: ReadonlyArray<unknown>): string {\n try {\n return JSON.stringify(deps);\n } catch {\n return String(deps);\n }\n}\n\n/**\n * Internal primitive powering the auto-fetch hooks. Runs `fetcher` on mount and\n * whenever `deps` change, tracks loading/error/data, cancels in-flight requests\n * on unmount or dep change, and exposes `refetch` (which keeps the current data\n * visible until the new data arrives).\n *\n * When `enabled` is false the fetch is skipped (used for hooks that need\n * arguments before they can run, e.g. consumers-by-id).\n */\nexport function useQuery<T>(\n fetcher: (signal: AbortSignal) => Promise<T>,\n deps: ReadonlyArray<unknown>,\n enabled = true\n): QueryState<T> {\n const key = depsKey(deps);\n const [state, dispatch] = useReducer(queryReducer<T>, {\n data: null,\n key: enabled ? key : null,\n fetching: enabled,\n error: null,\n errorStatus: null,\n } as QueryInternalState<T>);\n const [tick, setTick] = useState(0);\n\n const refetch = useCallback(() => setTick((t) => t + 1), []);\n\n useEffect(() => {\n if (!enabled) {\n dispatch({ type: \"idle\" });\n return;\n }\n\n const controller = new AbortController();\n dispatch({ type: \"start\", key });\n\n fetcher(controller.signal)\n .then((result) => {\n if (!controller.signal.aborted) dispatch({ type: \"success\", key, data: result });\n })\n .catch((e) => {\n if (!controller.signal.aborted && (e as { name?: string })?.name !== \"AbortError\") {\n dispatch({ type: \"failure\", key, error: toMessage(e), errorStatus: toStatus(e) });\n }\n });\n\n return () => {\n controller.abort();\n };\n // `fetcher` is intentionally excluded; callers pass stable `deps`.\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [key, tick, enabled]);\n\n return { ...selectQueryView(state, key, enabled), refetch };\n}\n\n/**\n * Internal primitive powering the manually-triggered hooks.\n */\nexport function useMutation<TData, TArgs extends any[]>(\n action: (...args: TArgs) => Promise<TData>,\n deps: ReadonlyArray<unknown>\n): MutationState<TData, TArgs> {\n const [data, setData] = useState<TData | null>(null);\n const [loading, setLoading] = useState(false);\n const [error, setError] = useState<string | null>(null);\n const [errorStatus, setErrorStatus] = useState<number | null>(null);\n\n const mutate = useCallback(\n async (...args: TArgs): Promise<TData | null> => {\n setLoading(true);\n setError(null);\n setErrorStatus(null);\n try {\n const result = await action(...args);\n setData(result);\n return result;\n } catch (e) {\n setError(toMessage(e));\n setErrorStatus(toStatus(e));\n return null;\n } finally {\n setLoading(false);\n }\n },\n // eslint-disable-next-line react-hooks/exhaustive-deps\n deps\n );\n\n return { mutate, data, loading, error, errorStatus };\n}\n","import {\n fetchCategories,\n type Category,\n type FetchCategoriesParams,\n type RequestOptions,\n} from \"../core\";\nimport { useQuery, type QueryState } from \"./shared\";\n\n/**\n * Auto-fetches categories on mount and whenever the params change.\n * Manual reloads are available via `refetch` (replaces the old `reloadFlag`).\n */\nexport function useCategory(\n params: FetchCategoriesParams = {},\n options?: RequestOptions\n): QueryState<Category[]> {\n return useQuery<Category[]>(\n (signal) => fetchCategories(params, { ...options, signal }),\n [JSON.stringify(params), options?.baseUrl, options?.token]\n );\n}\n\nexport default useCategory;\n","import {\n fetchConsumers,\n type Consumer,\n type RequestOptions,\n} from \"../core\";\nimport { useQuery, type QueryState } from \"./shared\";\n\n/**\n * Auto-fetches consumers by id. Disabled (no request) until at least one id is\n * supplied, mirroring the original early-return behaviour.\n */\nexport function useConsumer(\n consumerIds: number[] | null,\n options?: RequestOptions\n): QueryState<Consumer[]> {\n const enabled = !!consumerIds && consumerIds.length > 0;\n return useQuery<Consumer[]>(\n (signal) => fetchConsumers(consumerIds, { ...options, signal }),\n [JSON.stringify(consumerIds), options?.baseUrl, options?.token],\n enabled\n );\n}\n\nexport default useConsumer;\n","import {\n fetchEmployees,\n type Employee,\n type FetchEmployeesParams,\n type RequestOptions,\n} from \"../core\";\nimport { useQuery, type QueryState } from \"./shared\";\n\n/**\n * Auto-fetches employees on mount and whenever the params change.\n */\nexport function useEmployee(\n params: FetchEmployeesParams = {},\n options?: RequestOptions\n): QueryState<Employee[]> {\n return useQuery<Employee[]>(\n (signal) => fetchEmployees(params, { ...options, signal }),\n [JSON.stringify(params), options?.baseUrl, options?.token]\n );\n}\n\nexport default useEmployee;\n","import {\n fetchProducts,\n type Product,\n type FetchProductsParams,\n type RequestOptions,\n} from \"../core\";\nimport { useQuery, type QueryState } from \"./shared\";\n\n/**\n * Auto-fetches products on mount and whenever the params change.\n */\nexport function useProduct(\n params: FetchProductsParams = {},\n options?: RequestOptions\n): QueryState<Product[]> {\n return useQuery<Product[]>(\n (signal) => fetchProducts(params, { ...options, signal }),\n [JSON.stringify(params), options?.baseUrl, options?.token]\n );\n}\n\nexport default useProduct;\n","import { fetchProductInstanceTags, fetchProductTags, type RequestOptions } from \"../core\";\nimport { useQuery, type QueryState } from \"./shared\";\n\ntype LookupOptions = RequestOptions & {\n /** Fetch only when true (e.g. once a tag field is first opened). Defaults to true. */\n enabled?: boolean;\n};\n\n/**\n * The merchant's distinct product tags, for a tag field's suggestions.\n * `data` is `string[]` once loaded.\n *\n * @example\n * const [opened, setOpened] = useState(false);\n * const { data: tags, loading } = useProductTags({ enabled: opened });\n */\nexport function useProductTags(options?: LookupOptions): QueryState<string[]> {\n return useQuery<string[]>(\n (signal) => fetchProductTags({ ...options, signal }),\n [options?.baseUrl, options?.token],\n options?.enabled ?? true\n );\n}\n\n/**\n * The merchant's distinct price-instance (channel) tags: Eat In, Takeout, …\n * `data` is `string[]` once loaded.\n */\nexport function useProductInstanceTags(options?: LookupOptions): QueryState<string[]> {\n return useQuery<string[]>(\n (signal) => fetchProductInstanceTags({ ...options, signal }),\n [options?.baseUrl, options?.token],\n options?.enabled ?? true\n );\n}\n\nexport default useProductTags;\n","import {\n fetchPromotions,\n type Promotion,\n type FetchPromotionsParams,\n type RequestOptions,\n} from \"../core\";\nimport { useQuery, type QueryState } from \"./shared\";\n\n/**\n * Auto-fetches promotions on mount and whenever the params change.\n * Manual reloads are available via `refetch`.\n */\nexport function usePromotion(\n params: FetchPromotionsParams = {},\n options?: RequestOptions\n): QueryState<Promotion[]> {\n return useQuery<Promotion[]>(\n (signal) => fetchPromotions(params, { ...options, signal }),\n [JSON.stringify(params), options?.baseUrl, options?.token]\n );\n}\n\nexport default usePromotion;\n","import {\n fetchStores,\n type Store,\n type FetchStoresParams,\n type RequestOptions,\n} from \"../core\";\nimport { useQuery, type QueryState } from \"./shared\";\n\n/**\n * Auto-fetches stores on mount. Manual reloads via `refetch`\n * (replaces the old `reloadFlag` argument).\n */\nexport function useStore(\n params: FetchStoresParams = {},\n options?: RequestOptions\n): QueryState<Store[]> {\n return useQuery<Store[]>(\n (signal) => fetchStores(params, { ...options, signal }),\n [JSON.stringify(params), options?.baseUrl, options?.token]\n );\n}\n\nexport default useStore;\n","import {\n fetchReport,\n type ReportQueryMap,\n type ReportResponse,\n type RequestOptions,\n} from \"../core\";\nimport { useQuery, type QueryState } from \"./shared\";\n\n/**\n * Auto-fetches one or more reports. Disabled (no request) until a non-null\n * query is supplied, mirroring the original early-return behaviour.\n */\nexport function useReport(\n query: ReportQueryMap | null,\n options?: RequestOptions\n): QueryState<ReportResponse[\"reports\"]> {\n const enabled = !!query;\n return useQuery<ReportResponse[\"reports\"]>(\n (signal) => fetchReport(query as ReportQueryMap, { ...options, signal }),\n [JSON.stringify(query), options?.baseUrl, options?.token],\n enabled\n );\n}\n\nexport default useReport;\n","import { deleteEntity, type RequestOptions } from \"../core\";\nimport { useMutation, type MutationState } from \"./shared\";\n\n/**\n * Manually-triggered delete. Returns `{ mutate, loading, error, data }`.\n *\n * Note: the original `useHandleDelete` surfaced notistack toasts. That UI\n * concern is left to the host app — handle success/error from the returned\n * state or the promise resolved by `mutate`.\n *\n * @example\n * const { mutate, loading } = useDelete();\n * await mutate(\"products\", 42);\n */\nexport function useDelete(\n options?: RequestOptions\n): MutationState<void, [resourcePath: string, entityId: number | string]> {\n return useMutation<void, [string, number | string]>(\n (resourcePath, entityId) => deleteEntity(resourcePath, entityId, options),\n [options?.baseUrl, options?.token]\n );\n}\n\nexport default useDelete;\n","import {\n submitEntity,\n type SubmitEntityArgs,\n type RequestOptions,\n} from \"../core\";\nimport { useMutation, type MutationState } from \"./shared\";\n\n/**\n * Manually-triggered create/update. Returns `{ mutate, loading, error, data }`,\n * where `data` is the API `result` payload.\n *\n * The original `useHandleSubmit` bundled validation, payload transformation and\n * notistack toasts. Those are app concerns: resolve the endpoint/method/payload\n * in your component and pass them in, then react to the returned state.\n *\n * @example\n * const { mutate, loading } = useSubmit<Product>();\n * const saved = await mutate({\n * endpoint: id ? `/api/products/${id}` : \"/api/products\",\n * method: id ? \"PUT\" : \"POST\",\n * payload,\n * });\n */\nexport function useSubmit<R = any>(\n options?: RequestOptions\n): MutationState<R | undefined, [args: SubmitEntityArgs]> {\n return useMutation<R | undefined, [SubmitEntityArgs]>(\n (args) => submitEntity<R>(args, options),\n [options?.baseUrl, options?.token]\n );\n}\n\nexport default useSubmit;\n","import { logout, type RequestOptions } from \"../core\";\nimport { useMutation, type MutationState } from \"./shared\";\n\n/**\n * Manually-triggered logout (ends the server session).\n *\n * This is framework-agnostic and does NOT redirect. The original `useLogout`\n * also pushed the browser to the auth-server logout URL via `next/navigation`\n * and `'use server'` actions; that part is Next-specific and stays in your app.\n *\n * @example\n * const { mutate: doLogout } = useLogout();\n * await doLogout();\n * router.push(logoutUrl); // app-specific redirect\n */\nexport function useLogout(\n options?: RequestOptions\n): MutationState<void, []> {\n return useMutation<void, []>(\n () => logout(options),\n [options?.baseUrl, options?.token]\n );\n}\n\nexport default useLogout;\n","import { fetchSession, type RequestOptions, type SessionStatus } from \"../core\";\nimport { useQuery, type QueryState } from \"./shared\";\n\n/**\n * Current login status (`{ loggedIn, expiresAt }`) from `GET /api/session`.\n * Tokens are never exposed to the browser.\n *\n * @example\n * const { data: session, loading } = useSession();\n * if (!loading && !session?.loggedIn) window.location.href = loginPath(location.pathname);\n */\nexport function useSession(options?: RequestOptions): QueryState<SessionStatus> {\n return useQuery<SessionStatus>(\n (signal) => fetchSession({ ...options, signal }),\n [options?.baseUrl]\n );\n}\n\nexport default useSession;\n","import {\n fetchMerchant,\n updateMerchant,\n type Merchant,\n type MerchantPatch,\n type RequestOptions,\n type UpdateMerchantOptions,\n} from \"../core\";\nimport { useMutation, useQuery, type MutationState, type QueryState } from \"./shared\";\n\n/**\n * The logged-in user's merchant, fetched on mount. `data` is `null` while\n * loading or when the API has no merchant.\n *\n * @example\n * const { data: merchant, loading, refetch } = useMerchant();\n */\nexport function useMerchant(options?: RequestOptions): QueryState<Merchant | null> {\n return useQuery<Merchant | null>(\n (signal) => fetchMerchant({ ...options, signal }),\n [options?.baseUrl, options?.token]\n );\n}\n\n/**\n * Manually-triggered merchant update (see `updateMerchant`: the patch is merged\n * over the current merchant by default). Resolves to the saved merchant, or\n * `null` on error (then `error` / `errorStatus` are set).\n *\n * @example\n * const { mutate: saveMerchant, loading: saving } = useUpdateMerchant();\n * const saved = await saveMerchant({ vatNumber: \"GB123\" });\n */\nexport function useUpdateMerchant(options?: UpdateMerchantOptions): MutationState<Merchant, [MerchantPatch]> {\n return useMutation<Merchant, [MerchantPatch]>(\n (patch) => updateMerchant(patch, options),\n [options?.baseUrl, options?.token, options?.merge]\n );\n}\n\nexport default useMerchant;\n","import { fetchUser, type RequestOptions, type User } from \"../core\";\nimport { useQuery, type QueryState } from \"./shared\";\n\n/**\n * The logged-in user, fetched on mount. `errorStatus` is 401 when logged out.\n *\n * @example\n * const { data: user } = useUser();\n */\nexport function useUser(options?: RequestOptions & { enabled?: boolean }): QueryState<User | null> {\n const enabled = options?.enabled ?? true;\n return useQuery<User | null>(\n (signal) => fetchUser({ ...options, signal }),\n [options?.baseUrl, options?.token],\n enabled\n );\n}\n\nexport default useUser;\n","import {\n uploadImage,\n uploadMerchantImage,\n type MerchantImageSlot,\n type RequestOptions,\n type UploadImageParams,\n type UploadMerchantImageOptions,\n type UploadedImage,\n} from \"../core\";\nimport { useMutation, type MutationState } from \"./shared\";\n\n/**\n * Manually-triggered image upload. Resolves to `{ id, url }`, or `null` on\n * error (then `error` / `errorStatus` are set).\n *\n * @example\n * const { mutate: upload, loading: uploading } = useUploadImage();\n * const image = await upload(file, { kind: \"merchant\" });\n */\nexport function useUploadImage(options?: RequestOptions): MutationState<UploadedImage, [Blob, UploadImageParams]> {\n return useMutation<UploadedImage, [Blob, UploadImageParams]>(\n (file, params) => uploadImage(file, params, options),\n [options?.baseUrl, options?.token]\n );\n}\n\n/**\n * Manually-triggered upload for a merchant image slot (`receipt`, `square`,\n * `featured`): fits the image to the slot's exact size, uploads it with the\n * slot's kind, and resolves to `{ id, url }`. Put `id` into `imageIds` with\n * `setMerchantImage`.\n *\n * @example\n * const { mutate: upload } = useUploadMerchantImage();\n * const image = await upload(file, \"square\");\n */\nexport function useUploadMerchantImage(\n options?: UploadMerchantImageOptions\n): MutationState<UploadedImage, [Blob, MerchantImageSlot]> {\n return useMutation<UploadedImage, [Blob, MerchantImageSlot]>(\n (file, slot) => uploadMerchantImage(file, slot, options),\n [options?.baseUrl, options?.token, options?.fit]\n );\n}\n\nexport default useUploadImage;\n","import { useCallback, useEffect, useMemo, useState } from \"react\";\nimport {\n buildBasketReportQuery,\n buildConsumerMap,\n collectConsumerIds,\n collectProductIds,\n combineBasketRows,\n computeBasketSummary,\n fetchProductsMap,\n parseBasketReportData,\n DEFAULT_BASKET_FILTERS,\n DEFAULT_PAGINATION,\n type BasketFilters,\n type BasketPagination,\n type BasketReportRow,\n type BasketSummary,\n type Product,\n type RequestOptions,\n} from \"../core\";\nimport useReport from \"./useReport\";\nimport useConsumer from \"./useConsumer\";\n\nexport interface UseBasketReportReturn {\n rows: BasketReportRow[];\n summary: BasketSummary;\n loading: boolean;\n error: string | null;\n filters: BasketFilters;\n setFilters: (filters: Partial<BasketFilters>) => void;\n pagination: BasketPagination;\n setPagination: (page: number, pageSize: number) => void;\n refresh: () => void;\n}\n\n/**\n * Composite hook that orchestrates the report → consumers → products pipeline\n * for the basket report screen. All the data-shaping logic now lives in the\n * framework-agnostic `core/basket` helpers; this hook is the thin React glue\n * (state, memoisation, effects) on top of them.\n */\nexport function useBasketReport(\n options?: RequestOptions\n): UseBasketReportReturn {\n const [filters, setFiltersState] = useState<BasketFilters>(\n DEFAULT_BASKET_FILTERS\n );\n const [pagination, setPaginationState] = useState<BasketPagination>(\n DEFAULT_PAGINATION\n );\n const [refreshFlag, setRefreshFlag] = useState(0);\n\n // Build the report query from filters (core helper).\n const reportQuery = useMemo(\n () => buildBasketReportQuery(filters),\n // refreshFlag forces a new object identity to re-trigger the fetch.\n // eslint-disable-next-line react-hooks/exhaustive-deps\n [filters, refreshFlag]\n );\n\n const {\n data: reports,\n loading: basketLoading,\n error: basketError,\n } = useReport(reportQuery, options);\n\n // Flatten raw report rows into structured baskets (core helper).\n const basketData = useMemo(() => parseBasketReportData(reports), [reports]);\n\n // Derive id sets for the dependent fetches (core helpers).\n const consumerIds = useMemo(\n () => collectConsumerIds(basketData),\n [basketData]\n );\n const productIds = useMemo(\n () => collectProductIds(basketData),\n [basketData]\n );\n\n const {\n data: consumers,\n loading: consumerLoading,\n error: consumerError,\n } = useConsumer(consumerIds, options);\n\n // Batched product fetch (core helper handles the 200-per-request batching).\n const [productMap, setProductMap] = useState<{ [id: number]: Product }>({});\n const [productLoading, setProductLoading] = useState(false);\n const [productError, setProductError] = useState<string | null>(null);\n\n useEffect(() => {\n if (!productIds || productIds.length === 0) {\n setProductMap({});\n return;\n }\n\n let active = true;\n setProductLoading(true);\n setProductError(null);\n\n fetchProductsMap(productIds, options)\n .then((map) => {\n if (active) setProductMap(map);\n })\n .catch((e) => {\n if (active) {\n setProductError(\n e instanceof Error ? e.message : \"Failed to fetch products\"\n );\n }\n })\n .finally(() => {\n if (active) setProductLoading(false);\n });\n\n return () => {\n active = false;\n };\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [JSON.stringify(productIds), options?.baseUrl, options?.token]);\n\n // Consumer lookup (core helper).\n const consumerMap = useMemo(() => buildConsumerMap(consumers), [consumers]);\n\n // Combine into final rows (core helper: join + search + sort).\n const rows: BasketReportRow[] = useMemo(\n () =>\n combineBasketRows({\n baskets: basketData,\n consumerMap,\n productMap,\n search: filters.search,\n }),\n [basketData, consumerMap, productMap, filters.search]\n );\n\n // Summary (core helper).\n const summary: BasketSummary = useMemo(\n () => computeBasketSummary(rows),\n [rows]\n );\n\n // Keep pagination total in sync with fetched data.\n useEffect(() => {\n if (rows.length > 0 && rows.length === pagination.pageSize) {\n setPaginationState((prev) => ({\n ...prev,\n total: (pagination.page + 2) * pagination.pageSize,\n }));\n } else if (rows.length > 0) {\n setPaginationState((prev) => ({\n ...prev,\n total: pagination.page * pagination.pageSize + rows.length,\n }));\n }\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [rows.length, pagination.page, pagination.pageSize]);\n\n const setFilters = useCallback((newFilters: Partial<BasketFilters>) => {\n setFiltersState((prev) => ({ ...prev, ...newFilters }));\n setPaginationState((prev) => ({ ...prev, page: 0 }));\n }, []);\n\n const setPagination = useCallback((page: number, pageSize: number) => {\n setPaginationState((prev) => ({ ...prev, page, pageSize }));\n }, []);\n\n const refresh = useCallback(() => {\n setRefreshFlag((prev) => prev + 1);\n }, []);\n\n const loading = basketLoading || consumerLoading || productLoading;\n const error = basketError || consumerError || productError;\n\n return {\n rows,\n summary,\n loading,\n error,\n filters,\n setFilters,\n pagination,\n setPagination,\n refresh,\n };\n}\n\nexport default useBasketReport;\n"],"mappings":"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"}