149 lines
4.4 KiB
Swift
149 lines
4.4 KiB
Swift
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?
|
|
}
|