import Foundation /// `POST /api/scan` response (src/app/api/scan/route.ts). This does NOT /// persist anything — it only runs the AI extraction and hands back the /// result. The caller must follow up with `ReceiptsAPI.sync(...)` once the /// user has reviewed/confirmed the receipt, exactly like the web dashboard's /// scan → review → save flow. struct ScanResponse: Decodable { let success: Bool let receipt: Receipt? let receipts: [Receipt] let pageCount: Int? let sourcePageCount: Int? let truncated: Bool? } enum ReceiptsAPI { /// `POST /api/scan` — uploads one image/PDF for AI extraction. Requires a /// verified session (401 `unauthorized` / 403 `email_not_verified` /// otherwise) and is subject to the account's monthly/daily scan quota /// (402 `scan_limit_reached` / 429 `daily_scan_limit_reached`) — surface /// those via `APIError.server(code:...)`. static func scan(fileData: Data, fileName: String, mimeType: String) async throws -> ScanResponse { let request = APIRequest.multipartFile( path: "/api/scan", fileName: fileName, mimeType: mimeType, fileData: fileData ) return try await APIClient.shared.send(request) } /// `GET /api/receipts` — the signed-in user's receipts, newest first. /// `includePreview` pulls the base64 preview image inline for each row — /// keep it `false` for list screens and only request it (or fetch a /// single id) when actually displaying an image. static func list(limit: Int = 100, includePreview: Bool = false) async throws -> [Receipt] { var query = [URLQueryItem(name: "limit", value: String(limit))] if includePreview { query.append(URLQueryItem(name: "includePreview", value: "1")) } let request = APIRequest(path: "/api/receipts", method: .get, query: query) let response: ReceiptListResponse = try await APIClient.shared.send(request) return response.receipts } static func get(id: String) async throws -> Receipt? { let request = APIRequest( path: "/api/receipts", method: .get, query: [URLQueryItem(name: "id", value: id), URLQueryItem(name: "includePreview", value: "1")] ) let response: ReceiptListResponse = try await APIClient.shared.send(request) return response.receipts.first } /// `POST /api/receipts` — upserts one or more receipts (matched by `id`). /// This is how a scanned-and-reviewed receipt actually gets saved. @discardableResult static func sync(_ receipts: [Receipt]) async throws -> ReceiptSyncResponse { let request = try APIRequest.json(path: "/api/receipts", method: .post, body: receipts) return try await APIClient.shared.send(request) } /// `DELETE /api/receipts?id=...` static func delete(id: String) async throws { let request = APIRequest( path: "/api/receipts", method: .delete, query: [URLQueryItem(name: "id", value: id)] ) try await APIClient.shared.sendDiscardingResponse(request) } }