import { apiFetch } from './client' export type SlipStatus = 'pending' | 'processing' | 'success' | 'needs_review' | 'failed' | 'confirmed' | 'duplicate' // Mirrors ParsedExpense (apps/api/src/services/expense/ports/index.ts) // exactly — narrower ParsedExpenseResult (./expenses, the older // /v1/expense/parse response) doesn't carry `reference`/`template_id`. export interface SlipParsedExpense { amount: number currency: string date: string | null merchant: string | null category: string | null confidence: number raw_description: string reference: string | null template_id?: string | null } export interface SlipQueueItem { id: string image_base64: string | null image_url: string | null status: SlipStatus retry_count: number parsed_result: SlipParsedExpense | null error_message: string | null // Best-candidate template match info, present whenever a template match // was attempted (regardless of whether it cleared the confidence bar) — // see slip-queue-match-review-ui. match_confidence: number | null match_template_id: string | null created_at: string processed_at: string | null } export interface EnqueueSlipsResult { succeeded: SlipQueueItem[] failed: Array<{ error: string }> } export async function enqueueSlips(images: string[]): Promise { const res = await apiFetch('/v1/slip-queue', { method: 'POST', body: JSON.stringify({ images, format: 'json' }), }) if (!res.ok) { const err = await res.json() as { error?: string } throw new Error(err.error ?? 'Failed to enqueue slips') } return res.json() as Promise } export interface SlipQueueCursor { createdAt: string id: string } export interface SlipQueuePage { items: SlipQueueItem[] hasMore: boolean } export async function listSlipQueuePage(status: SlipStatus, opts: { limit?: number; after?: SlipQueueCursor } = {}): Promise { const params = new URLSearchParams({ status, limit: String(opts.limit ?? 30) }) if (opts.after) { params.set('after_created_at', opts.after.createdAt) params.set('after_id', opts.after.id) } const res = await apiFetch(`/v1/slip-queue?${params.toString()}`) if (!res.ok) throw new Error('Failed to load slip queue') return res.json() as Promise } export async function getSlipQueueCounts(): Promise> { const res = await apiFetch('/v1/slip-queue/counts') if (!res.ok) throw new Error('Failed to load slip queue counts') return res.json() as Promise> } export async function retrySlip(id: string): Promise { const res = await apiFetch(`/v1/slip-queue/${id}/retry`, { method: 'POST' }) if (!res.ok) { const err = await res.json() as { error?: string } throw new Error(err.error ?? 'Failed to retry slip') } return res.json() as Promise } export interface SlipConfirmOverrides { date?: string merchant?: string currency?: string description?: string shares?: Array<{ person_id: string; amount: number }> } export interface SlipConfirmDetails extends SlipConfirmOverrides { category_id: string } export async function confirmSlip(id: string, categoryId: string, overrides?: SlipConfirmOverrides): Promise { const res = await apiFetch(`/v1/slip-queue/${id}/confirm`, { method: 'POST', body: JSON.stringify({ category_id: categoryId, ...overrides }), }) if (!res.ok) { const err = await res.json() as { error?: string } throw new Error(err.error ?? 'Failed to confirm slip') } } export interface ApplyMatchOverride { parsed_result: SlipParsedExpense match_confidence: number match_template_id: string | null } // POST /v1/slip-queue/:id/apply-match — resolves a needs_review slip via one // of slip-queue-match-review-ui's two option boxes. No `override` reuses the // slip's own already-persisted match ("use matched template"); an `override` // carries a freshly-taught template's match result instead ("set up a new // template"). Either way the slip lands on 'success', same as today's // confident-auto-match path. export async function applySlipMatch(id: string, override?: ApplyMatchOverride): Promise { const res = await apiFetch(`/v1/slip-queue/${id}/apply-match`, { method: 'POST', body: JSON.stringify(override ?? {}), }) if (!res.ok) { const err = await res.json() as { error?: string } throw new Error(err.error ?? 'Failed to apply the template match') } return res.json() as Promise } export async function discardSlip(id: string): Promise { const res = await apiFetch(`/v1/slip-queue/${id}`, { method: 'DELETE' }) if (!res.ok) throw new Error('Failed to discard slip') }