react-shopwave-connect 0.4.1 → 0.4.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +17 -5
- package/dist/core/index.cjs +119 -48
- package/dist/core/index.cjs.map +1 -1
- package/dist/core/index.d.cts +80 -9
- package/dist/core/index.d.ts +80 -9
- package/dist/core/index.js +116 -49
- package/dist/core/index.js.map +1 -1
- package/dist/hooks/index.cjs +129 -44
- package/dist/hooks/index.cjs.map +1 -1
- package/dist/hooks/index.d.cts +21 -1
- package/dist/hooks/index.d.ts +21 -1
- package/dist/hooks/index.js +128 -45
- package/dist/hooks/index.js.map +1 -1
- package/dist/index.cjs +148 -48
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +100 -9
- package/dist/index.d.ts +100 -9
- package/dist/index.js +143 -49
- package/dist/index.js.map +1 -1
- package/dist/next/index.cjs +83 -2
- package/dist/next/index.cjs.map +1 -1
- package/dist/next/index.d.cts +63 -7
- package/dist/next/index.d.ts +63 -7
- package/dist/next/index.js +83 -3
- package/dist/next/index.js.map +1 -1
- package/dist/server/index.cjs +80 -2
- package/dist/server/index.cjs.map +1 -1
- package/dist/server/index.d.cts +60 -7
- package/dist/server/index.d.ts +60 -7
- package/dist/server/index.js +80 -3
- package/dist/server/index.js.map +1 -1
- package/package.json +1 -1
package/dist/core/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","names":["text","headers: Record<string, string>","merged: Extras","text","response: Response","extras: Record<string, unknown>","extras: Record<string, unknown>","extras: Record<string, unknown>","result: RepricedInstance[]","out: Record<string, Omit<T, \"id\"> & { id?: number | string }>","result: { [id: number]: Product }","extras: Record<string, unknown>","extras: Record<string, unknown>","SHOPWAVE_ENTITIES: Readonly<Record<EntityKind, EntityDefinition>>","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 }","SHOPWAVE_RESOURCES: Readonly<Record<ResourceKind, ResourceDefinition>>","UPLOAD_KINDS: Readonly<Record<UploadKind, string>>","blob: Blob","MERCHANT_IMAGE_SLOTS: Readonly<Record<MerchantImageSlot, MerchantImageSlotDefinition>>","value: unknown","next: MerchantImageIds","out: Record<string, unknown>","MERCHANT_LINK_PATHS: Readonly<Record<MerchantLinkKey, readonly [string, string]>>","out: Partial<Record<MerchantLinkKey, string>>","out: Record<string, Record<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>"],"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/promotion.ts","../../src/core/store.ts","../../src/core/report.ts","../../src/core/session.ts","../../src/core/entity.ts","../../src/core/entities.ts","../../src/core/basket.ts","../../src/core/resources.ts","../../src/core/imageFit.ts","../../src/core/upload.ts","../../src/core/merchantImages.ts","../../src/core/merchant.ts","../../src/core/user.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","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 { apiDelete, apiGet, apiRequest, type RequestOptions } from \"./request\";\nimport { ShopwaveApiError, assertNoApiErrors } from \"./errors\";\nimport { toProductInstancesForSave, type Product, type ProductSaveInput } from \"./product\";\nimport type { Category } from \"./category\";\nimport type { Store } from \"./store\";\nimport type { Promotion } from \"./promotion\";\nimport type { Employee } from \"./employee\";\nimport type { Consumer } from \"./consumer\";\n\n/**\n * How each Shopwave entity is addressed, shared by the client functions below\n * and the server route handlers in `react-shopwave-connect/next`, so the two\n * halves of the contract can't drift apart.\n */\nexport interface EntityDefinition {\n /** App route segment: `/api/<route>` and `/api/<route>/<id>`. */\n route: string;\n /** Key of the entity map in requests and responses (`{ products: { … } }`). */\n collection: string;\n /** Shopwave API path (`GET/POST/DELETE {apiUrl}/<upstream>`). */\n upstream: string;\n /** Header that filters reads by id (`productIds: 1,2`). */\n idsHeader: string;\n /** Header that names the record to delete (`productId: 1`). */\n idHeader: string;\n /** False for read-only entities (the API has no POST for them). */\n writable: boolean;\n /**\n * How a delete is done upstream:\n * - `\"delete\"`: `DELETE {apiUrl}/<upstream>` with the `idHeader` (205, empty body)\n * - `\"retire\"`: the API has no DELETE; the record is retired by POSTing\n * `{ id, [retireField]: <now> }` (employees: `exitDate`, promotions: `endDate`)\n * - `\"none\"`: can't be deleted\n */\n deleteMode: \"delete\" | \"retire\" | \"none\";\n /** Date field set to \"now\" when `deleteMode` is `\"retire\"`. */\n retireField?: string;\n}\n\nexport type EntityKind = \"product\" | \"category\" | \"store\" | \"promotion\" | \"employee\" | \"consumer\";\n\n/** Entities that can be created/updated through the API. */\nexport type WritableEntityKind = Exclude<EntityKind, \"consumer\">;\n\n/** Entities that can be deleted (or retired) through the API. */\nexport type DeletableEntityKind = Exclude<EntityKind, \"consumer\">;\n\n/**\n * Addressing per entity, following the Shopwave API reference\n * (https://developer.merchantstack.com/api-reference.html) and checked against\n * the live API where noted:\n * - category/product/store: GET filtered by `<x>Ids`, POST upsert, DELETE with `<x>Id` → 205.\n * - promotion: no DELETE; \"deleting\" a promotion ends it by setting `endDate` to now.\n * - employee: POST updates `roleId`, `joinedDate`, `exitDate` (names can't be\n * changed); no DELETE (live API: \"Cannot DELETE /employee\") → retired via `exitDate`.\n * - consumer: read-only (`GET` with `ids`, comma-separated); live POST → 404.\n */\nexport const SHOPWAVE_ENTITIES: Readonly<Record<EntityKind, EntityDefinition>> = Object.freeze({\n product: { route: \"products\", collection: \"products\", upstream: \"product\", idsHeader: \"productIds\", idHeader: \"productId\", writable: true, deleteMode: \"delete\" },\n category: { route: \"categories\", collection: \"categories\", upstream: \"category\", idsHeader: \"categoryIds\", idHeader: \"categoryId\", writable: true, deleteMode: \"delete\" },\n store: { route: \"stores\", collection: \"stores\", upstream: \"store\", idsHeader: \"storeIds\", idHeader: \"storeId\", writable: true, deleteMode: \"delete\" },\n promotion: { route: \"promotions\", collection: \"promotions\", upstream: \"promotion\", idsHeader: \"promotionIds\", idHeader: \"promotionId\", writable: true, deleteMode: \"retire\", retireField: \"endDate\" },\n employee: { route: \"employees\", collection: \"employees\", upstream: \"employee\", idsHeader: \"employeeIds\", idHeader: \"employeeId\", writable: true, deleteMode: \"retire\", retireField: \"exitDate\" },\n consumer: { route: \"consumer\", collection: \"consumers\", upstream: \"consumer\", idsHeader: \"ids\", idHeader: \"consumerId\", writable: false, deleteMode: \"none\" },\n});\n\nexport interface EntityTypes {\n product: Product;\n category: Category;\n store: Store;\n promotion: Promotion;\n employee: Employee;\n consumer: Consumer;\n}\n\n/**\n * What a save accepts: any subset of the entity's fields (plus extra API\n * fields the type doesn't list). Include `id` to update; leave it out to create.\n */\nexport type EntityInput<T> = Partial<T> & { [field: string]: unknown };\n\n/** A saved entity: always has its `id`. */\nexport type Saved<T> = T & { id: number };\n\nexport type EntityId = number | string;\n\nfunction definition(kind: EntityKind): EntityDefinition {\n const def = SHOPWAVE_ENTITIES[kind];\n if (!def) throw new Error(`Unknown Shopwave entity \"${kind}\"`);\n return def;\n}\n\nfunction assertId(kind: EntityKind, id: EntityId): string {\n const s = id == null ? \"\" : String(id).trim();\n if (!s) throw new Error(`${kind} id is missing`);\n return encodeURIComponent(s);\n}\n\n/**\n * Creates or updates several entities of one kind in a single request and\n * returns them as saved (with their ids), in the order given.\n *\n * The request is `{ <collection>: { \"0\": item0, \"1\": item1, … } }`. Shopwave\n * answers 201 and echoes every entity under the ref it was sent with, so each\n * result is matched back by ref. Throws {@link ShopwaveApiError} when the\n * response carries `api.message.errors` or any ref is missing from it.\n *\n * The API's partial-success semantics for multi-entity saves haven't been\n * confirmed yet; single saves (`saveProduct` etc.) are the tested path.\n */\nexport async function saveEntities<K extends WritableEntityKind>(\n kind: K,\n items: ReadonlyArray<EntityInput<EntityTypes[K]>>,\n options: RequestOptions = {}\n): Promise<Array<Saved<EntityTypes[K]>>> {\n const def = definition(kind);\n if (items.length === 0) return [];\n\n const refs = items.map((_, i) => String(i));\n const payload = { [def.collection]: Object.fromEntries(refs.map((ref, i) => [ref, items[i]])) };\n\n const { status, body: raw } = await apiRequest(\n `/api/${def.route}`,\n { method: \"POST\", headers: { \"Content-Type\": \"application/json\" }, body: JSON.stringify(payload) },\n options\n );\n const body = raw as Record<string, unknown> | null;\n assertNoApiErrors(body, status);\n\n const echoed = (body?.[def.collection] ?? {}) as Record<string, Record<string, unknown> | undefined>;\n return refs.map((ref, i) => {\n const saved = echoed[ref];\n if (!saved || saved.id == null) {\n throw new ShopwaveApiError(status, `Shopwave API error (${status}): the saved ${kind} (ref \"${ref}\") was not returned`, {\n body,\n });\n }\n // The write response echoes what was sent plus server fields; keep any\n // field it leaves out.\n return { ...items[i], ...saved, id: Number(saved.id) } as unknown as Saved<EntityTypes[K]>;\n });\n}\n\n/** Creates (no `id`) or updates (with `id`) one entity and returns it with its id. */\nexport async function saveEntity<K extends WritableEntityKind>(\n kind: K,\n item: EntityInput<EntityTypes[K]>,\n options: RequestOptions = {}\n): Promise<Saved<EntityTypes[K]>> {\n const [saved] = await saveEntities(kind, [item], options);\n return saved;\n}\n\n/**\n * Deletes one entity (a soft delete: it stays readable with `deleted: true`).\n * Employees and promotions can't be deleted in Shopwave; for them the route\n * sets `exitDate` / `endDate` to now instead (see `SHOPWAVE_ENTITIES`).\n *\n * Shopwave answers 205 with an empty body — also for ids that don't exist — so\n * a resolved promise means \"the API accepted the request\", not \"a record was\n * deleted\".\n */\nexport async function deleteEntityById(kind: DeletableEntityKind, id: EntityId, options: RequestOptions = {}): Promise<void> {\n await apiDelete(`/api/${definition(kind).route}/${assertId(kind, id)}`, options);\n}\n\nexport interface FetchByIdParams {\n /** Also return the record when it has been deleted. Defaults to `false`. */\n deleted?: boolean;\n}\n\n/** Reads one entity by id, or `null` when there's no such (non-deleted) record. */\nexport async function fetchEntityById<K extends EntityKind>(\n kind: K,\n id: EntityId,\n params: FetchByIdParams = {},\n options: RequestOptions = {}\n): Promise<EntityTypes[K] | null> {\n const def = definition(kind);\n const body = await apiGet<Record<string, unknown> | null>(\n `/api/${def.route}/${assertId(kind, id)}`,\n { deleted: params.deleted ?? false },\n options\n );\n assertNoApiErrors(body, 200);\n const map = (body?.[def.collection] ?? {}) as Record<string, EntityTypes[K]>;\n const match = map[String(id)] ?? Object.values(map).find((e) => String((e as { id?: unknown })?.id) === String(id));\n return match ?? null;\n}\n\n// ---------------------------------------------------------------------------\n// Typed per-entity functions\n// ---------------------------------------------------------------------------\n\n/**\n * Creates (no `id`) or updates (with `id`) a product.\n *\n * Shopwave instances (prices) can't be updated, only replaced. Build\n * `instances` with {@link toProductInstancesForSave} (passing the product as last\n * read) so an edited price is sent as a new instance. As a safeguard, a new\n * product (no `id`, e.g. a duplicate) never sends instance ids: an id copied from\n * another product would otherwise point the save at that product's instances.\n */\nexport const saveProduct = (product: ProductSaveInput, options?: RequestOptions) => {\n const isNew = product.id == null || String(product.id).trim() === \"\";\n const toSave = isNew && product.instances != null ? { ...product, instances: toProductInstancesForSave(product.instances) } : product;\n return saveEntity(\"product\", toSave as EntityInput<Product>, options);\n};\nexport const deleteProduct = (id: EntityId, options?: RequestOptions) => deleteEntityById(\"product\", id, options);\nexport const fetchProduct = (id: EntityId, params?: FetchByIdParams, options?: RequestOptions) =>\n fetchEntityById(\"product\", id, params, options);\n\nexport const saveCategory = (category: EntityInput<Category>, options?: RequestOptions) =>\n saveEntity(\"category\", category, options);\nexport const deleteCategory = (id: EntityId, options?: RequestOptions) => deleteEntityById(\"category\", id, options);\nexport const fetchCategory = (id: EntityId, params?: FetchByIdParams, options?: RequestOptions) =>\n fetchEntityById(\"category\", id, params, options);\n\nexport const saveStore = (store: EntityInput<Store>, options?: RequestOptions) => saveEntity(\"store\", store, options);\nexport const deleteStore = (id: EntityId, options?: RequestOptions) => deleteEntityById(\"store\", id, options);\nexport const fetchStore = (id: EntityId, params?: FetchByIdParams, options?: RequestOptions) =>\n fetchEntityById(\"store\", id, params, options);\n\nexport const savePromotion = (promotion: EntityInput<Promotion>, options?: RequestOptions) =>\n saveEntity(\"promotion\", promotion, options);\n/** Ends a promotion: Shopwave has no promotion DELETE, so this sets `endDate` to now. */\nexport const deletePromotion = (id: EntityId, options?: RequestOptions) => deleteEntityById(\"promotion\", id, options);\nexport const fetchPromotion = (id: EntityId, params?: FetchByIdParams, options?: RequestOptions) =>\n fetchEntityById(\"promotion\", id, params, options);\n\n/**\n * Creates an employee, or updates one (with `id`). On update Shopwave only\n * applies `roleId`, `joinedDate` and `exitDate` (and store roles); names and\n * email can't be changed, and the echo only carries the updated fields — re-read\n * with `fetchEmployee` if you need the stored record.\n */\nexport const saveEmployee = (employee: EntityInput<Employee>, options?: RequestOptions) =>\n saveEntity(\"employee\", employee, options);\n/** Retires an employee: Shopwave has no employee DELETE, so this sets `exitDate` to now. */\nexport const deleteEmployee = (id: EntityId, options?: RequestOptions) => deleteEntityById(\"employee\", id, options);\nexport const fetchEmployee = (id: EntityId, params?: FetchByIdParams, options?: RequestOptions) =>\n fetchEntityById(\"employee\", id, params, options);\n\n// Consumers are read-only in the Shopwave API: there is no save or delete.\nexport const fetchConsumer = (id: EntityId, params?: FetchByIdParams, options?: RequestOptions) =>\n fetchEntityById(\"consumer\", id, params, options);\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 * 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/next`, 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 * 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"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"index.js","names":["text","headers: Record<string, string>","merged: Extras","text","response: Response","extras: Record<string, unknown>","extras: Record<string, unknown>","extras: Record<string, unknown>","result: RepricedInstance[]","out: Record<string, Omit<T, \"id\"> & { id?: number | string }>","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>","SHOPWAVE_ENTITIES: Readonly<Record<EntityKind, EntityDefinition>>","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>>","value: unknown","next: MerchantImageIds","out: Record<string, unknown>","MERCHANT_LINK_PATHS: Readonly<Record<MerchantLinkKey, readonly [string, string]>>","out: Partial<Record<MerchantLinkKey, string>>","out: Record<string, Record<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>"],"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/entities.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"],"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 { apiDelete, apiGet, apiRequest, type RequestOptions } from \"./request\";\nimport { ShopwaveApiError, assertNoApiErrors } from \"./errors\";\nimport { toProductInstancesForSave, type Product, type ProductSaveInput } from \"./product\";\nimport type { Category } from \"./category\";\nimport type { Store } from \"./store\";\nimport type { Promotion } from \"./promotion\";\nimport type { Employee } from \"./employee\";\nimport type { Consumer } from \"./consumer\";\n\n/**\n * How each Shopwave entity is addressed, shared by the client functions below\n * and the server route handlers in `react-shopwave-connect/server` (wrapped with\n * the session by `react-shopwave-connect/next`), so the two halves of the\n * contract can't drift apart.\n */\nexport interface EntityDefinition {\n /** App route segment: `/api/<route>` and `/api/<route>/<id>`. */\n route: string;\n /** Key of the entity map in requests and responses (`{ products: { … } }`). */\n collection: string;\n /** Shopwave API path (`GET/POST/DELETE {apiUrl}/<upstream>`). */\n upstream: string;\n /** Header that filters reads by id (`productIds: 1,2`). */\n idsHeader: string;\n /** Header that names the record to delete (`productId: 1`). */\n idHeader: string;\n /** False for read-only entities (the API has no POST for them). */\n writable: boolean;\n /**\n * How a delete is done upstream:\n * - `\"delete\"`: `DELETE {apiUrl}/<upstream>` with the `idHeader` (205, empty body)\n * - `\"retire\"`: the API has no DELETE; the record is retired by setting\n * `retireField` to now (employees: `exitDate`, promotions: `endDate`)\n * - `\"none\"`: can't be deleted\n */\n deleteMode: \"delete\" | \"retire\" | \"none\";\n /** Date field set to \"now\" when `deleteMode` is `\"retire\"`. */\n retireField?: string;\n /**\n * Retire by reading the record and POSTing all of it back with the date(s)\n * changed, instead of a partial `{ id, [retireField]: <now> }`. Needed where\n * the API rejects a partial save (promotions: error 899, live 14 Oct 2026).\n */\n retireWithRecord?: boolean;\n /**\n * Start-date field of a record retired with `retireWithRecord`. If the record\n * hasn't started yet (start after now), the start moves to now as well, so\n * start = end and the record is expired without ever running.\n */\n retireStartField?: string;\n}\n\nexport type EntityKind = \"product\" | \"category\" | \"store\" | \"promotion\" | \"employee\" | \"consumer\";\n\n/** Entities that can be created/updated through the API. */\nexport type WritableEntityKind = Exclude<EntityKind, \"consumer\">;\n\n/** Entities that can be deleted (or retired) through the API. */\nexport type DeletableEntityKind = Exclude<EntityKind, \"consumer\">;\n\n/**\n * Addressing per entity, following the Shopwave API reference\n * (https://developer.merchantstack.com/api-reference.html) and checked against\n * the live API where noted:\n * - category/product/store: GET filtered by `<x>Ids`, POST upsert, DELETE with `<x>Id` → 205.\n * - promotion: no DELETE; promotions are never deleted, only expired. Expiring\n * reads the promotion and saves the whole record back with `endDate` = now (and\n * `startDate` = now too if it hadn't started); the live API rejects a partial\n * `{ id, endDate }` save with error 899. Dates are UTC `YYYY-MM-DD HH:mm:ss`.\n * - employee: POST updates `roleId`, `joinedDate`, `exitDate` (names can't be\n * changed); no DELETE (live API: \"Cannot DELETE /employee\") → retired via `exitDate`.\n * - consumer: read-only (`GET` with `ids`, comma-separated); live POST → 404.\n */\nexport const SHOPWAVE_ENTITIES: Readonly<Record<EntityKind, EntityDefinition>> = Object.freeze({\n product: { route: \"products\", collection: \"products\", upstream: \"product\", idsHeader: \"productIds\", idHeader: \"productId\", writable: true, deleteMode: \"delete\" },\n category: { route: \"categories\", collection: \"categories\", upstream: \"category\", idsHeader: \"categoryIds\", idHeader: \"categoryId\", writable: true, deleteMode: \"delete\" },\n store: { route: \"stores\", collection: \"stores\", upstream: \"store\", idsHeader: \"storeIds\", idHeader: \"storeId\", writable: true, deleteMode: \"delete\" },\n promotion: { route: \"promotions\", collection: \"promotions\", upstream: \"promotion\", idsHeader: \"promotionIds\", idHeader: \"promotionId\", writable: true, deleteMode: \"retire\", retireField: \"endDate\", retireWithRecord: true, retireStartField: \"startDate\" },\n employee: { route: \"employees\", collection: \"employees\", upstream: \"employee\", idsHeader: \"employeeIds\", idHeader: \"employeeId\", writable: true, deleteMode: \"retire\", retireField: \"exitDate\" },\n consumer: { route: \"consumer\", collection: \"consumers\", upstream: \"consumer\", idsHeader: \"ids\", idHeader: \"consumerId\", writable: false, deleteMode: \"none\" },\n});\n\nexport interface EntityTypes {\n product: Product;\n category: Category;\n store: Store;\n promotion: Promotion;\n employee: Employee;\n consumer: Consumer;\n}\n\n/**\n * What a save accepts: any subset of the entity's fields (plus extra API\n * fields the type doesn't list). Include `id` to update; leave it out to create.\n */\nexport type EntityInput<T> = Partial<T> & { [field: string]: unknown };\n\n/** A saved entity: always has its `id`. */\nexport type Saved<T> = T & { id: number };\n\nexport type EntityId = number | string;\n\nfunction definition(kind: EntityKind): EntityDefinition {\n const def = SHOPWAVE_ENTITIES[kind];\n if (!def) throw new Error(`Unknown Shopwave entity \"${kind}\"`);\n return def;\n}\n\nfunction assertId(kind: EntityKind, id: EntityId): string {\n const s = id == null ? \"\" : String(id).trim();\n if (!s) throw new Error(`${kind} id is missing`);\n return encodeURIComponent(s);\n}\n\n/**\n * Creates or updates several entities of one kind in a single request and\n * returns them as saved (with their ids), in the order given.\n *\n * The request is `{ <collection>: { \"0\": item0, \"1\": item1, … } }`. Shopwave\n * answers 201 and echoes every entity under the ref it was sent with, so each\n * result is matched back by ref. Throws {@link ShopwaveApiError} when the\n * response carries `api.message.errors` or any ref is missing from it.\n *\n * The API's partial-success semantics for multi-entity saves haven't been\n * confirmed yet; single saves (`saveProduct` etc.) are the tested path.\n */\nexport async function saveEntities<K extends WritableEntityKind>(\n kind: K,\n items: ReadonlyArray<EntityInput<EntityTypes[K]>>,\n options: RequestOptions = {}\n): Promise<Array<Saved<EntityTypes[K]>>> {\n const def = definition(kind);\n if (items.length === 0) return [];\n\n const refs = items.map((_, i) => String(i));\n const payload = { [def.collection]: Object.fromEntries(refs.map((ref, i) => [ref, items[i]])) };\n\n const { status, body: raw } = await apiRequest(\n `/api/${def.route}`,\n { method: \"POST\", headers: { \"Content-Type\": \"application/json\" }, body: JSON.stringify(payload) },\n options\n );\n const body = raw as Record<string, unknown> | null;\n assertNoApiErrors(body, status);\n\n const echoed = (body?.[def.collection] ?? {}) as Record<string, Record<string, unknown> | undefined>;\n return refs.map((ref, i) => {\n const saved = echoed[ref];\n if (!saved || saved.id == null) {\n throw new ShopwaveApiError(status, `Shopwave API error (${status}): the saved ${kind} (ref \"${ref}\") was not returned`, {\n body,\n });\n }\n // The write response echoes what was sent plus server fields; keep any\n // field it leaves out.\n return { ...items[i], ...saved, id: Number(saved.id) } as unknown as Saved<EntityTypes[K]>;\n });\n}\n\n/** Creates (no `id`) or updates (with `id`) one entity and returns it with its id. */\nexport async function saveEntity<K extends WritableEntityKind>(\n kind: K,\n item: EntityInput<EntityTypes[K]>,\n options: RequestOptions = {}\n): Promise<Saved<EntityTypes[K]>> {\n const [saved] = await saveEntities(kind, [item], options);\n return saved;\n}\n\n/**\n * Deletes one entity (a soft delete: it stays readable with `deleted: true`).\n * Employees and promotions can't be deleted in Shopwave; for them the route\n * sets `exitDate` / `endDate` to now instead (see `SHOPWAVE_ENTITIES`), and a\n * promotion is saved back whole (see {@link deletePromotion}).\n *\n * Shopwave answers 205 with an empty body — also for ids that don't exist — so\n * a resolved promise means \"the API accepted the request\", not \"a record was\n * deleted\".\n */\nexport async function deleteEntityById(kind: DeletableEntityKind, id: EntityId, options: RequestOptions = {}): Promise<void> {\n await apiDelete(`/api/${definition(kind).route}/${assertId(kind, id)}`, options);\n}\n\nexport interface FetchByIdParams {\n /** Also return the record when it has been deleted. Defaults to `false`. */\n deleted?: boolean;\n}\n\n/** Reads one entity by id, or `null` when there's no such (non-deleted) record. */\nexport async function fetchEntityById<K extends EntityKind>(\n kind: K,\n id: EntityId,\n params: FetchByIdParams = {},\n options: RequestOptions = {}\n): Promise<EntityTypes[K] | null> {\n const def = definition(kind);\n const body = await apiGet<Record<string, unknown> | null>(\n `/api/${def.route}/${assertId(kind, id)}`,\n { deleted: params.deleted ?? false },\n options\n );\n assertNoApiErrors(body, 200);\n const map = (body?.[def.collection] ?? {}) as Record<string, EntityTypes[K]>;\n const match = map[String(id)] ?? Object.values(map).find((e) => String((e as { id?: unknown })?.id) === String(id));\n return match ?? null;\n}\n\n// ---------------------------------------------------------------------------\n// Typed per-entity functions\n// ---------------------------------------------------------------------------\n\n/**\n * Creates (no `id`) or updates (with `id`) a product.\n *\n * Shopwave instances (prices) can't be updated, only replaced. Build\n * `instances` with {@link toProductInstancesForSave} (passing the product as last\n * read) so an edited price is sent as a new instance. As a safeguard, a new\n * product (no `id`, e.g. a duplicate) never sends instance ids: an id copied from\n * another product would otherwise point the save at that product's instances.\n */\nexport const saveProduct = (product: ProductSaveInput, options?: RequestOptions) => {\n const isNew = product.id == null || String(product.id).trim() === \"\";\n const toSave = isNew && product.instances != null ? { ...product, instances: toProductInstancesForSave(product.instances) } : product;\n return saveEntity(\"product\", toSave as EntityInput<Product>, options);\n};\nexport const deleteProduct = (id: EntityId, options?: RequestOptions) => deleteEntityById(\"product\", id, options);\nexport const fetchProduct = (id: EntityId, params?: FetchByIdParams, options?: RequestOptions) =>\n fetchEntityById(\"product\", id, params, options);\n\nexport const saveCategory = (category: EntityInput<Category>, options?: RequestOptions) =>\n saveEntity(\"category\", category, options);\nexport const deleteCategory = (id: EntityId, options?: RequestOptions) => deleteEntityById(\"category\", id, options);\nexport const fetchCategory = (id: EntityId, params?: FetchByIdParams, options?: RequestOptions) =>\n fetchEntityById(\"category\", id, params, options);\n\nexport const saveStore = (store: EntityInput<Store>, options?: RequestOptions) => saveEntity(\"store\", store, options);\nexport const deleteStore = (id: EntityId, options?: RequestOptions) => deleteEntityById(\"store\", id, options);\nexport const fetchStore = (id: EntityId, params?: FetchByIdParams, options?: RequestOptions) =>\n fetchEntityById(\"store\", id, params, options);\n\nexport const savePromotion = (promotion: EntityInput<Promotion>, options?: RequestOptions) =>\n saveEntity(\"promotion\", promotion, options);\n/**\n * Expires a promotion (Shopwave never deletes promotions). The route reads the\n * promotion and saves it back with `endDate` set to now; a promotion that hasn't\n * started yet gets `startDate` = `endDate` = now. Rejects if the promotion doesn't\n * exist or the save fails.\n */\nexport const deletePromotion = (id: EntityId, options?: RequestOptions) => deleteEntityById(\"promotion\", id, options);\nexport const fetchPromotion = (id: EntityId, params?: FetchByIdParams, options?: RequestOptions) =>\n fetchEntityById(\"promotion\", id, params, options);\n\n/**\n * Creates an employee, or updates one (with `id`). On update Shopwave only\n * applies `roleId`, `joinedDate` and `exitDate` (and store roles); names and\n * email can't be changed, and the echo only carries the updated fields — re-read\n * with `fetchEmployee` if you need the stored record.\n */\nexport const saveEmployee = (employee: EntityInput<Employee>, options?: RequestOptions) =>\n saveEntity(\"employee\", employee, options);\n/** Retires an employee: Shopwave has no employee DELETE, so this sets `exitDate` to now. */\nexport const deleteEmployee = (id: EntityId, options?: RequestOptions) => deleteEntityById(\"employee\", id, options);\nexport const fetchEmployee = (id: EntityId, params?: FetchByIdParams, options?: RequestOptions) =>\n fetchEntityById(\"employee\", id, params, options);\n\n// Consumers are read-only in the Shopwave API: there is no save or delete.\nexport const fetchConsumer = (id: EntityId, params?: FetchByIdParams, options?: RequestOptions) =>\n fetchEntityById(\"consumer\", id, params, options);\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"],"mappings":"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"}
|