// Beta helper: photograph the invoice and let WFDASH match it against // everything assigned to this driver at that supplier. Exact matches (same // product, invoice quantity = ordered quantity) are marked collected through // the same writes the bulk-collect button uses, and can be undone. // On by default; a driver can switch it off in Settings → Beta. import { pb, getSettings, updateStockCollectionStatus } from './pocketbase'; /** Used when neither the phone's setting nor the build sets an address. */ export const DEFAULT_WFDASH_URL = 'https://wfdash.apphub.co.za'; export interface MatchedLine { id: string; kind: 'customer' | 'stock'; po_number: string; description: string; quantity: number; previous: Record; } export interface ReviewLine { id: string; kind: 'customer' | 'stock'; po_number: string; description: string; reason: string; } export interface InvoiceMatchResult { po_number: string; matched: MatchedLine[]; review: ReviewLine[]; error: string; } /** Fired by the PO buttons; InvoiceMatchHost (once per page) does the work. */ export const RUN_EVENT = 'invoice-match:run'; /** The WFDASH address to call, or '' when the driver switched the helper off. */ export function invoiceCheckBase(): string { const s = getSettings(); if (!s.aiCheckEnabled) return ''; const url = (s.aiCheckUrl || process.env.NEXT_PUBLIC_WFDASH_URL || DEFAULT_WFDASH_URL).trim(); return url.replace(/\/+$/, ''); } export async function matchInvoice(poNumber: string): Promise { const base = invoiceCheckBase(); if (!base) throw new Error('Invoice check is not set up (Settings → Beta).'); const res = await fetch(`${base}/api/v1/driver/invoice-match`, { method: 'POST', headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${pb.authStore.token}` }, body: JSON.stringify({ po_number: poNumber }), signal: AbortSignal.timeout(120_000), }); const body = await res.json().catch(() => ({})); if (!res.ok) throw new Error(body.error || `Check failed (${res.status})`); return body as InvoiceMatchResult; } /** Mark one matched line collected, exactly as bulkCollect / the stock modal do. */ export async function collectMatched(line: MatchedLine): Promise { if (line.kind === 'stock') { await updateStockCollectionStatus(line.id, { qty_collected: line.quantity, expectedQty: line.quantity }); return; } const now = new Date().toISOString(); await pb.collection('order_items').update(line.id, { item_status: 'collected', drv_status: 'collected', drv_collected_at: now, qty_collected: line.quantity, collected_date: now, }); } /** Put a matched line back to how it was before the auto-collect. */ export async function undoMatched(line: MatchedLine): Promise { await pb .collection(line.kind === 'stock' ? 'stock_purchase_items' : 'order_items') .update(line.id, line.previous); } /** * Record why a line differs from the invoice. Only the note is written: the * item is not collected and nothing else about it changes. Any earlier note * on the line is kept. */ export async function saveReviewNote(line: ReviewLine, reason: string): Promise { const col = line.kind === 'stock' ? 'stock_purchase_items' : 'order_items'; const rec = await pb.collection(col).getOne(line.id); const before = String(rec.notes ?? '').trim(); const entry = `[Invoice differs: ${line.reason}] ${reason.trim()}`; await pb.collection(col).update(line.id, { notes: before ? `${before}\n${entry}` : entry }); }