Buchungen anlegen, aendern und loeschen; Budget-Zeilen setzen, fuer den Monat oder fuer die Vorlage; Vertraege und Konten anlegen und aendern. Damit kann die App alles, wofuer man bisher an den Rechner musste - ausser Import, Arbeitsliste und Verwaltung. Ein Formular je Sache, nicht zwei: Anlegen und Aendern teilen sich die Maske, weil die Felder dieselben sind und zwei Kopien genau die Stelle waeren, an der sie auseinanderlaufen. Was sich unterscheidet, steht in einem Zweck-Typ. Geteilt in Bausteine/Formular.swift: ein Speichervorgang, der den Knopf sperrt und den Fehler festhaelt (das Formular bleibt offen - zugeklappt waere die Eingabe weg und die Meldung nutzlos), ein Betragsfeld, das an Cent gebunden ist statt an Text, und die Auswahlfelder fuer Kategorie, Konto und Schlagwoerter. Drei Dinge, die stillschweigend falsch waeren: Eine Teilaenderung schickt nur, was gesetzt ist. Ginge ein nicht angefasstes Feld als null hinaus, loeschte jedes Speichern den Empfaenger, die Notiz und die Kategorie gleich mit. Eine leere Tag-Liste geht dagegen sehr wohl mit - sie bedeutet "alle abnehmen" und darf nicht mit "nicht angefasst" zusammenfallen. Bei einem Uebertrag wird keine Kategorie mitgeschickt: Das Geld wechselt nur die Tasche, es wird nicht eingeordnet. Bei Betriebsart "berechnet" bleibt planned_cents unberuehrt - das Backend liest es dann nicht, und es zu ueberschreiben verloere den Wert, auf den ein Zurueckschalten auf "fest" zurueckfallen soll. Die Waehrung eines bestehenden Kontos laesst sich nicht aendern: Jeder gespeicherte Betrag wuerde danach etwas anderes bedeuten. 49 Tests gruen. Texte in Deutsch und Englisch, zeilengleich. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
187 lines
6.0 KiB
Swift
187 lines
6.0 KiB
Swift
import Foundation
|
||
|
||
// Was die App zum Server schickt. Getrennt von den Lesemodellen, weil das
|
||
// Backend es ebenso trennt: `TransactionCreate` kennt Felder, die
|
||
// `TransactionOut` nicht hat, und umgekehrt (`subscription_id` etwa steht nur
|
||
// in der Ausgabe – beim Anlegen gibt man es nicht mit).
|
||
//
|
||
// Alle Änderungstypen sind **teilweise**: Nur was gesetzt ist, geht mit. Ein
|
||
// `nil` darf nicht als `null` hinausgehen, sonst löschte jedes Speichern die
|
||
// Felder, die das Formular gar nicht anfasst.
|
||
//
|
||
// Das macht Swift von selbst – die erzeugte `encode(to:)` ruft für Optionals
|
||
// `encodeIfPresent`. Hier stand das eine Weile von Hand ausgeschrieben, bis
|
||
// ein Test zeigte, dass es dasselbe tut. Der Test bleibt: Die Eigenschaft ist
|
||
// zu wichtig, um sie ungeprüft einer Spracheigenheit zu überlassen.
|
||
|
||
/// `TransactionCreate`
|
||
struct BuchungAnlegen: Encodable, Sendable {
|
||
var type: Buchungsart
|
||
var date: Tagesdatum
|
||
var accountId: Int
|
||
var toAccountId: Int?
|
||
/// Immer positiv – die Richtung steckt in `type`, nicht im Vorzeichen.
|
||
var amountCents: Int
|
||
var toAmountCents: Int?
|
||
var categoryId: Int?
|
||
var payee: String?
|
||
var description: String?
|
||
var notes: String?
|
||
var tagIds: [Int] = []
|
||
|
||
private enum CodingKeys: String, CodingKey {
|
||
case type, date, payee, description, notes
|
||
case accountId = "account_id"
|
||
case toAccountId = "to_account_id"
|
||
case amountCents = "amount_cents"
|
||
case toAmountCents = "to_amount_cents"
|
||
case categoryId = "category_id"
|
||
case tagIds = "tag_ids"
|
||
}
|
||
}
|
||
|
||
/// `TransactionUpdate` – nur die gesetzten Felder gehen mit.
|
||
struct BuchungAendern: Encodable, Sendable {
|
||
var type: Buchungsart?
|
||
var date: Tagesdatum?
|
||
var accountId: Int?
|
||
var toAccountId: Int?
|
||
var amountCents: Int?
|
||
var categoryId: Int?
|
||
var payee: String?
|
||
var description: String?
|
||
var notes: String?
|
||
/// Mitgeschickt heisst **ersetzen**; eine leere Liste nimmt alle Tags ab.
|
||
/// Weggelassen lässt sie unberührt.
|
||
var tagIds: [Int]?
|
||
|
||
private enum CodingKeys: String, CodingKey {
|
||
case type, date, payee, description, notes
|
||
case accountId = "account_id"
|
||
case toAccountId = "to_account_id"
|
||
case amountCents = "amount_cents"
|
||
case categoryId = "category_id"
|
||
case tagIds = "tag_ids"
|
||
}
|
||
}
|
||
|
||
/// `AccountCreate`
|
||
struct KontoAnlegen: Encodable, Sendable {
|
||
var name: String
|
||
var type: Kontoart = .girokonto
|
||
var currency: String = "EUR"
|
||
var icon: String = "wallet"
|
||
var color: String = "#3f51b5"
|
||
var openingBalanceCents: Int = 0
|
||
var openingDate: Tagesdatum?
|
||
var creditLimitCents: Int?
|
||
var isLocked: Bool = false
|
||
|
||
private enum CodingKeys: String, CodingKey {
|
||
case name, type, currency, icon, color
|
||
case openingBalanceCents = "opening_balance_cents"
|
||
case openingDate = "opening_date"
|
||
case creditLimitCents = "credit_limit_cents"
|
||
case isLocked = "is_locked"
|
||
}
|
||
}
|
||
|
||
/// `AccountUpdate` – nur die gesetzten Felder gehen mit.
|
||
///
|
||
/// Die Währung fehlt mit Absicht: Sie nachträglich zu wechseln liesse jeden
|
||
/// gespeicherten Betrag etwas anderes bedeuten. Wer das will, legt ein neues
|
||
/// Konto an.
|
||
struct KontoAendern: Encodable, Sendable {
|
||
var name: String?
|
||
var type: Kontoart?
|
||
var icon: String?
|
||
var color: String?
|
||
var openingBalanceCents: Int?
|
||
var openingDate: Tagesdatum?
|
||
var creditLimitCents: Int?
|
||
var isLocked: Bool?
|
||
var isArchived: Bool?
|
||
|
||
private enum CodingKeys: String, CodingKey {
|
||
case name, type, icon, color
|
||
case openingBalanceCents = "opening_balance_cents"
|
||
case openingDate = "opening_date"
|
||
case creditLimitCents = "credit_limit_cents"
|
||
case isLocked = "is_locked"
|
||
case isArchived = "is_archived"
|
||
}
|
||
}
|
||
|
||
/// `SubscriptionIn`
|
||
struct AboAnlegen: Encodable, Sendable {
|
||
var name: String
|
||
var icon: String = "refresh-ccw"
|
||
var color: String = "#6b7480"
|
||
var amountCents: Int
|
||
var currency: String = "EUR"
|
||
/// Nur 1, 2, 3, 6 oder 12 – das Backend weist alles andere ab.
|
||
var intervalMonths: Int = 1
|
||
var categoryId: Int?
|
||
var accountId: Int?
|
||
var nextDue: Tagesdatum?
|
||
var payeePattern: String?
|
||
var notes: String?
|
||
|
||
private enum CodingKeys: String, CodingKey {
|
||
case name, icon, color, currency, notes
|
||
case amountCents = "amount_cents"
|
||
case intervalMonths = "interval_months"
|
||
case categoryId = "category_id"
|
||
case accountId = "account_id"
|
||
case nextDue = "next_due"
|
||
case payeePattern = "payee_pattern"
|
||
}
|
||
}
|
||
|
||
/// `SubscriptionUpdate` – nur die gesetzten Felder gehen mit.
|
||
struct AboAendern: Encodable, Sendable {
|
||
var name: String?
|
||
var icon: String?
|
||
var color: String?
|
||
var amountCents: Int?
|
||
var intervalMonths: Int?
|
||
var categoryId: Int?
|
||
var accountId: Int?
|
||
var nextDue: Tagesdatum?
|
||
var payeePattern: String?
|
||
var isActive: Bool?
|
||
var notes: String?
|
||
|
||
private enum CodingKeys: String, CodingKey {
|
||
case name, icon, color, notes
|
||
case amountCents = "amount_cents"
|
||
case intervalMonths = "interval_months"
|
||
case categoryId = "category_id"
|
||
case accountId = "account_id"
|
||
case nextDue = "next_due"
|
||
case payeePattern = "payee_pattern"
|
||
case isActive = "is_active"
|
||
}
|
||
}
|
||
|
||
/// Die möglichen Zahlungsrhythmen. Mehr nimmt das Backend nicht an.
|
||
enum Zahlungsrhythmus: Int, CaseIterable, Identifiable, Sendable {
|
||
case monatlich = 1
|
||
case zweimonatlich = 2
|
||
case vierteljaehrlich = 3
|
||
case halbjaehrlich = 6
|
||
case jaehrlich = 12
|
||
|
||
var id: Int { rawValue }
|
||
|
||
var name: String {
|
||
switch self {
|
||
case .monatlich: return String(localized: "abo.monatlich")
|
||
case .zweimonatlich: return String(format: String(localized: "abo.alleNMonate"), 2)
|
||
case .vierteljaehrlich: return String(localized: "abo.vierteljaehrlich")
|
||
case .halbjaehrlich: return String(localized: "abo.halbjaehrlich")
|
||
case .jaehrlich: return String(localized: "abo.jaehrlich")
|
||
}
|
||
}
|
||
}
|