You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
sims-hq/apps/pos/src/api.ts

89 lines
3.8 KiB
TypeScript

import type { Item } from '@sims/domain'
import type { LineInput, TaxClassRow } from '@sims/billing-engine'
/** POS ↔ store-server client. Same origin in production (served under /pos/). */
export interface BootstrapStore {
id: string; name: string; storeCode: string; stateCode: string
gstin: string; counterId: string; counterCode: string
}
export interface PosUser { id: string; name: string; role: string }
export interface Bootstrap { store: BootstrapStore; users: PosUser[]; taxClasses: TaxClassRow[] }
let token = ''
async function call<T>(path: string, init?: RequestInit): Promise<T> {
const res = await fetch(`/api${path}`, {
...init,
headers: {
'content-type': 'application/json',
...(token !== '' ? { authorization: `Bearer ${token}` } : {}),
},
}).catch(() => undefined)
if (res === undefined) throw new Error('Store-server unreachable (E-3301)')
const body = (await res.json().catch(() => ({}))) as T & { error?: string }
if (!res.ok) throw new Error(body.error ?? `Server error HTTP ${res.status}`)
return body
}
export const fetchBootstrap = (counter: string): Promise<Bootstrap> =>
call(`/bootstrap?counter=${encodeURIComponent(counter)}`)
export async function pinLogin(userId: string, pin: string): Promise<void> {
const s = await call<{ token: string }>('/auth/pin', {
method: 'POST',
body: JSON.stringify({ userId, pin }),
})
token = s.token
}
/**
* In-flow supervisor gate (spec P0-2): verify a supervisor's PIN without touching
* the module-level `token` — the cashier stays logged in and no session is created.
* Distinct from `pinLogin` on purpose; reusing that would hijack the cashier's session.
*/
export const verifySupervisorPin = (userId: string, pin: string): Promise<{ ok: true; userId: string; displayName: string }> =>
call('/auth/verify-pin', { method: 'POST', body: JSON.stringify({ userId, pin }) })
// The counter caches the whole sellable catalog (no silent cap — R13); ?all=1
// returns every non-inactive item unpaginated.
export const fetchItemsCache = (): Promise<Item[]> => call('/items?all=1')
export const fetchStock = (): Promise<{ code: string; on_hand: number }[]> => call('/stock')
export interface CommitResult { id: string; docNo: string; payablePaise: number }
/** Supervisor-approved override, audited server-side in the commit transaction (spec P0-2). */
export interface BillOverride {
itemId: string; kind: 'price' | 'qty' | 'discount'; before: number; after: number; approvedByUserId?: string
}
export const postBill = (body: {
storeId: string; counterId: string; shiftId: string; customerId?: string
businessDate: string; lines: LineInput[]
payments: { mode: string; amountPaise: number }[]
clientPayablePaise: number
overrides?: BillOverride[]
}): Promise<CommitResult> => call('/bills', { method: 'POST', body: JSON.stringify(body) })
export const fetchBills = (date: string, counterId: string): Promise<Record<string, unknown>[]> =>
call(`/bills?date=${date}&counterId=${encodeURIComponent(counterId)}`)
export const createDraftItem = (body: {
code: string; name: string; hsn: string; taxClassCode: string
unitCode: string; unitDecimals: number; salePricePaise: number
barcode?: string; status: string
}): Promise<Item> => call('/items', { method: 'POST', body: JSON.stringify(body) })
/** Attached-customer identity the counter needs: name plus B2B place-of-supply inputs. */
export interface PosCustomer { id: string; name: string; phone?: string; gstin?: string; stateCode?: string }
export const findCustomers = (phone: string): Promise<PosCustomer[]> =>
call(`/parties?phone=${encodeURIComponent(phone)}`)
export const createCustomer = (name: string, phone: string, gstin?: string): Promise<PosCustomer> =>
call('/parties', {
method: 'POST',
body: JSON.stringify({ name, phone, ...(gstin !== undefined && gstin !== '' ? { gstin } : {}) }),
})