73 lines
3.1 KiB
Swift
73 lines
3.1 KiB
Swift
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)
|
|
}
|
|
}
|