WFDASHDRIVER-APP/lib/invoiceCheck.ts
Dave 30c30ed821 feat: invoice match on by default with the WFDASH address built in
The match never ran on drivers' phones: it was off by default and the
build had no WFDASH address, so photos were stored but nothing collected.
Now it is on for every driver (switched on once for phones saved while
it was off; a later "off" sticks) and falls back to wfdash.apphub.co.za.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-26 10:46:27 +02:00

95 lines
3.5 KiB
TypeScript

// 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<string, unknown>;
}
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<InvoiceMatchResult> {
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<void> {
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<void> {
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<void> {
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 });
}