feat: add iOS support and harden receipt scanning

This commit is contained in:
Timo
2026-08-20 23:48:26 +02:00
parent f5e06c32b0
commit cf1b799e1b
107 changed files with 11755 additions and 122 deletions

View File

@@ -0,0 +1,148 @@
import Foundation
/// Mirrors `ProcessedReceipt` (src/lib/schema/receipt.ts) exactly this is
/// the shape returned by `GET /api/receipts` and accepted by
/// `POST /api/receipts`. Keep this file and that Zod schema in sync; when the
/// web schema gains a field, add it here too rather than silently dropping it
/// on decode (optional fields decode fine as `nil` when absent, so older
/// receipts synced before a schema change won't crash the app).
struct Receipt: Codable, Identifiable, Equatable {
let id: String
var projectId: String?
let imageHash: String
let originalFileName: String
let fileSizeBytes: Int
var previewUrl: String?
let createdAt: String
let updatedAt: String
var status: ReceiptStatus
var merchant: Merchant
var date: ReceiptDate
var documentType: DocumentType
var receiptNumber: String?
var currency: String
var totalAmount: AmountWithConfidence
var netAmount: Double?
var tipAmount: Double?
var taxBreakdown: [TaxBreakdownItem]
var lineItems: [LineItem]
var suggestedCategory: ReceiptCategory
var paymentMethod: PaymentMethod?
var hospitality: Hospitality?
var validation: Validation
/// Total actually paid: `totalAmount.value + tipAmount`. Mirrors
/// `grossWithTip()` on the backend tip is deliberately excluded from
/// `totalAmount` there because VAT is never charged on it.
var grossWithTip: Double {
((totalAmount.value + (tipAmount ?? 0)) * 100).rounded() / 100
}
struct Merchant: Codable, Equatable {
var name: String
var address: String?
var taxId: String?
var confidence: Double
}
struct ReceiptDate: Codable, Equatable {
var isoDate: String
var time: String?
var confidence: Double
}
struct AmountWithConfidence: Codable, Equatable {
var value: Double
var confidence: Double
}
struct TaxBreakdownItem: Codable, Equatable, Identifiable {
var ratePercent: Double
var taxAmount: Double
var netAmount: Double?
var id: String { "\(ratePercent)-\(taxAmount)" }
}
struct LineItem: Codable, Equatable, Identifiable {
var id = UUID()
var description: String
var quantity: Double
var price: Double
var unitPrice: Double?
var taxRate: Double?
enum CodingKeys: String, CodingKey {
case description, quantity, price, unitPrice, taxRate
}
}
struct Hospitality: Codable, Equatable {
var occasion: String?
var participants: String?
}
struct Validation: Codable, Equatable {
var isMathValid: Bool
var isDuplicateSuspected: Bool
var needsUserReview: Bool
var reviewField: String
var reviewReason: String?
var issues: [Issue]?
var userConfirmed: Bool?
struct Issue: Codable, Equatable {
var field: String
var severity: String
var message: String
}
}
enum ReceiptStatus: String, Codable {
case pending, processing, ready, needsReview = "needs_review", error
}
enum DocumentType: String, Codable, CaseIterable {
case kassenbon = "KASSENBON"
case rechnung = "RECHNUNG"
case tankbeleg = "TANKBELEG"
case bewirtungsbeleg = "BEWIRTUNGSBELEG"
case parkticket = "PARKTICKET"
case sonstiges = "SONSTIGES"
}
enum PaymentMethod: String, Codable, CaseIterable {
case bar = "BAR"
case ecKarte = "EC_KARTE"
case kreditkarte = "KREDITKARTE"
case ueberweisung = "UEBERWEISUNG"
case paypal = "PAYPAL"
case applePay = "APPLE_PAY"
case googlePay = "GOOGLE_PAY"
case sonstige = "SONSTIGE"
}
enum ReceiptCategory: String, Codable, CaseIterable {
case bewirtung = "Bewirtung"
case reisekosten = "Reisekosten & Hotel"
case tanken = "Tanken & KFZ"
case buerobedarf = "Bürobedarf & IT"
case verpflegungsmehraufwand = "Verpflegungsmehraufwand"
case material = "Material & Einkauf"
case sonstiges = "Sonstiges"
}
}
/// `GET /api/receipts` response envelope.
struct ReceiptListResponse: Decodable {
let success: Bool
let receipts: [Receipt]
let count: Int
}
/// `POST /api/receipts` response envelope (batch upsert).
struct ReceiptSyncResponse: Decodable {
let success: Bool
let syncedCount: Int?
let message: String?
}