import type { NotificationTypeEnum } from "./notification"; declare global { /** Write shape for user create/update — carries the transient photo controls. */ type UserUpsertInput = Partial & PhotoUploadControls; interface User { storeId: string; userId: string; createdAt: number; fullName: string; phone: string; email: string; password?: string; /** * Canonical role string. The api's write path also accepts a legacy * singular `role` alias (not declared here) and normalizes it into this * field — but legacy rows created through that alias may still carry a * stray persisted `role` attribute until the api-side cleanup lands. * Always read `roles`; never the alias. */ roles: string; photoURL: string; /** @deprecated Request-only upload control, never persisted or returned — use `UserUpsertInput.photoData`. */ photoData?: string; /** @deprecated Request-only control, never persisted or returned — use `UserUpsertInput.removePhotoURL`. */ removePhotoURL?: string; disabled: boolean; /** * @deprecated Lowercase WRITE-SIDE index for backend filtering. Internal — * not part of the read contract, even where legacy responses still include * it; never consume it. */ search?: string; accessToken: string; roleSeller?: boolean; roleProducts?: boolean; roleCustomers?: boolean; roleAfip?: boolean; notifications?: UserNotifications; notificationSound?: boolean; permissions?: UserPermissions; emailVerified?: boolean; emailVerifiedAt?: number; totp?: { enabled: boolean; secretRef?: string; pendingSecretRef?: string; pendingAt?: number; enrolledAt?: number; lastUsedAt?: number; lastCounter?: number; recoveryCodes?: { hash: string; usedAt?: number; }[]; recoveryCodesGeneratedAt?: number; failedAttempts?: number; lockedUntil?: number; }; warnings?: StoreWarning[]; login?: { failedAttempts?: number; lockedUntil?: number; lastFailedAt?: number; }; } type UserNotifications = Partial>; type UserPermissions = { currency?: boolean; customers?: boolean; products?: boolean; seller?: boolean; accountant?: boolean; payments?: boolean; cash?: boolean; packOrder?: boolean; /** * May grant a discount at the till — a per-line `setLineDiscount`, or * applying/removing a cart coupon. * * ⚠️ Checked **IN ADDITION TO** the store-wide `config.changePrice` switch, * never instead of it. A store that turned the price switch off must not * find discounts still reachable; a per-line discount is a price override in * everything but spelling. * * Absent/false means no. Before this key existed, every `USER`-role cashier * at a store with `changePrice` on could grant an unbounded per-line cut, * because `UserPermissions` had eight keys and none of them was a discount. * * ℹ️ This is the industry floor, not a novel control: Shopify POS has * separate toggles for custom discounts and for discount codes, Toast * requires a manager permission level, and Clover prompts for a manager PIN * stating the rationale outright — so employees do not inadvertently (or * maliciously) abstain from accepting payment for the full amount. */ discount?: boolean; }; /** * Wire error codes for the paths that create or update a USER row: * `POST /users` (create AND update — one handler serves both) and * `POST /auth?mode=register` (self-registration). They ride `data.error`; * `data.message` carries human copy the FE never discriminates on, because * the FE owns the operator-facing wording via its own literals. * * **The two codes carry different HTTP statuses, deliberately:** * * - `EMAIL_IN_USE` — **400**. The address already belongs to another * account. Raised from the `email-PK` probe and, when a concurrent write * wins the race, from the global email-uniqueness constraint. The caller * must change the address; retrying as-is cannot succeed. * - `USER_ID_COLLISION` — **409**. The server-minted `userId` was taken * between the probe and the write. Nothing the caller typed is wrong and * the operation is retryable, so it must not share 400 with the above — a * client treating 4xx-except-409 as "surface a field error, do not retry" * would pin an unactionable message to a form field. Never a duplicate * address, which is why it is not folded into `EMAIL_IN_USE`. * * Distinct from `LoginErrorCode`, which covers the sign-in lockout flow. */ type UserWriteErrorCode = "EMAIL_IN_USE" | "USER_ID_COLLISION"; interface UserGoogle extends User { displayName: string; } /** * A stored `User` plus the refresh token, where one is delivered in the body * at all. * * ⚠️ This is the STORED shape, and the auth responses built from it are * SANITIZED — several inherited fields never reach the wire: the * brute-force `login` counters, the legacy singular `role` write-alias, and * the write-side `search` index are all dropped, and `totp` arrives reduced * to `{ enabled, enrolledAt, recoveryCodesRemaining }` with its KMS * ciphertext handles and replay counters stripped inside the Lambda. So an * inherited field being declared here is not evidence it arrives; check it * survives the sanitize before consuming it off a login body. */ interface AuthUser extends User { /** * ⚠️ Delivered in the body ONLY under the body refresh transport — the * native-mobile opt-in. The DEFAULT is cookie transport, where the token * ships in an HttpOnly `Set-Cookie` and this key is absent from the body * entirely. * * The tell for having trusted it as guaranteed: it reads `undefined` on an * ordinary browser login and NOTHING breaks, because the cookie the client * cannot see is doing the work. A client that persists this value and * refreshes from it therefore looks correct wherever body transport is on, * and silently never refreshes in the browser — i.e. everywhere real. */ refreshToken?: string; } } export {};