/** * Stock Quick-Draft service — backs the /stock-orders modal that lets * a buyer add items to draft stock_purchases in an item-first rhythm. * * Reuses `generateStockPurchaseNumber` from `stock-purchases-service.ts` * for the find-or-create open-draft step; no schema changes, no new * statuses. Admin PB only (consistent with the other service modules). */ import type PocketBase from "pocketbase"; import { getAdminPocketBase } from "./pocketbase-admin"; import { generateStockPurchaseNumber } from "./stock-purchases-service"; const STOCK_ITEMS = "stock_items"; const STOCK_PURCHASES = "stock_purchases"; const STOCK_PURCHASE_ITEMS = "stock_purchase_items"; const SEARCH_LIMIT_MAX = 25; const SEARCH_LIMIT_DEFAULT = 10; /* ============================================================================ * Minimal PB client shape this module needs. * * The route passes in the real `getAdminPocketBase()`; the unit test * passes in a tiny mock. Only the methods we actually call are typed. * ========================================================================== */ export interface StockQuickDraftPb { collection: (name: string) => { getList: ( page?: number, perPage?: number, opts?: { filter?: string; sort?: string; expand?: string } ) => Promise<{ items: Array>; totalItems: number; page: number; perPage: number; totalPages: number; }>; getFullList: (opts?: { filter?: string }) => Promise>>; getOne: (id: string, opts?: { expand?: string }) => Promise>; create: (data: Record) => Promise>; update: (id: string, data: Record) => Promise>; delete: (id: string) => Promise>; }; } /* ============================================================================ * Public types * ========================================================================== */ export interface StockItemSearchResult { id: string; sku: string; name: string; description: string; unit?: string; } export interface SupplierHistoryEntry { supplierId: string; supplierName: string; lastCost: number; lastDate: string; timesBought: number; } export interface AddQuickDraftLineNewStockItem { name: string; sku: string; description?: string; unit?: string; } export interface AddQuickDraftLineInput { supplierId: string; stockItemId?: string; quantity: number; costPrice: number; newStockItem?: AddQuickDraftLineNewStockItem; } export type AddQuickDraftLineResult = | { ok: true; purchaseId: string; purchaseNumber: string; lineId: string; createdDraft: boolean } | { ok: false; reason: string; message: string }; export type RemoveQuickDraftLineResult = | { ok: true } | { ok: false; reason: string; message: string }; /* ============================================================================ * Helpers * ========================================================================== */ async function resolvePb(pb?: StockQuickDraftPb): Promise { if (pb) return pb; return (await getAdminPocketBase()) as unknown as StockQuickDraftPb; } /* ============================================================================ * searchStockItemsForQuickDraft * ========================================================================== */ /** * Typeahead search over `stock_items`. Matches `sku` OR `name` with * PocketBase's `~` (contains, case-insensitive). Excludes inactive * items. Limit defaults to 10, clamped to a max of 25. * * Returns a narrow projection — only the fields the modal needs. */ export async function searchStockItemsForQuickDraft( pb: StockQuickDraftPb, q: string, limit?: number, ): Promise { const safeLimit = Math.min(Math.max(limit ?? SEARCH_LIMIT_DEFAULT, 1), SEARCH_LIMIT_MAX); const escaped = q.replace(/"/g, '\\"'); const filter = `is_active=true && (sku ~ "${escaped}" || name ~ "${escaped}")`; const result = await pb.collection(STOCK_ITEMS).getList(1, safeLimit, { filter, sort: "name", }); return result.items.map((row) => ({ id: String(row.id), sku: String(row.sku ?? ""), name: String(row.name ?? ""), description: String(row.description ?? ""), unit: row.unit ? String(row.unit) : undefined, })); }