Spaces:
Runtime error
Runtime error
File size: 4,795 Bytes
07c37a1 163916c 07c37a1 c5a5343 07c37a1 6822ee4 07c37a1 c5a5343 07c37a1 b207060 07c37a1 6822ee4 07c37a1 163916c 07c37a1 6822ee4 07c37a1 747e1c6 07c37a1 747e1c6 07c37a1 0b0f8c2 747e1c6 0b0f8c2 07c37a1 b207060 c5a5343 b207060 07c37a1 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 | 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<EnqueueSlipsResult> {
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<EnqueueSlipsResult>
}
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<SlipQueuePage> {
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<SlipQueuePage>
}
export async function getSlipQueueCounts(): Promise<Record<SlipStatus, number>> {
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<Record<SlipStatus, number>>
}
export async function retrySlip(id: string): Promise<SlipQueueItem> {
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<SlipQueueItem>
}
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<void> {
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<SlipQueueItem> {
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<SlipQueueItem>
}
export async function discardSlip(id: string): Promise<void> {
const res = await apiFetch(`/v1/slip-queue/${id}`, { method: 'DELETE' })
if (!res.ok) throw new Error('Failed to discard slip')
}
|