feat: Vierte Stufe der iOS-App - die Arbeitsliste

Ein neuer Reiter "Eingang" mit allem, worueber jemand entscheiden muss. Vier
Abschnitte, jeweils mit Anzahl im Kopf:

- Zu buchen: offene Eingangseintraege aus Kurzbefehl, Postfach oder Bank.
  Buchen, als Uebertrag buchen, verwerfen - oder alle auf einmal, bei denen
  das Konto feststeht. Unklare tragen einen Hinweis ueber den Knoepfen, nicht
  darunter: Man soll sie ansehen, nicht wegbuchen.
- Koennten Uebertraege sein: zwei getrennte Buchungen, die zusammengehoeren
  koennten. Verbinden oder ablehnen. Hat eine der beiden eine Kategorie,
  steht dabei, dass sie durchs Verbinden aus dem Budget faellt.
- Ohne Kategorie: gebuchtes Geld, das in keiner Budget-Zeile mitzaehlt. Ein
  Tipp, eine Kategorie - kein Formular mit zwoelf Feldern davor.
- Sieht doppelt aus: gleiches Konto, gleicher Tag, gleicher Betrag. Mit der
  Kennung der Bank je Zeile, denn verschiedene Kennungen heissen, dass die
  Bank sie als verschiedene Zahlungen gemeldet hat.

Alles in einem Abruf. Vier getrennte Zwischenspeicher koennten offline aus
vier verschiedenen Momenten stammen und sich widersprechen.

Hier sitzt der einzige 409-Fall der App: Beim Buchen als Uebertrag kann das
Backend Bedenken haben. Das ist eine Frage, kein Fehler - die App zeigt den
Hinweis und wiederholt den Aufruf auf Wunsch mit trotzdem=true. Ihn zu
verschlucken waere falsch, ihn als blossen Fehler zu zeigen auch.

Zwei Dinge, die die Tests festhalten: Ohne Konto laesst sich nichts buchen
(das Backend lehnt mit 400 ab, der Knopf bleibt also grau - und die Zeile sagt
auch warum). Und ein Uebertrag braucht keine Kategorie, sonst fragte die Liste
ewig nach einer, die es nie geben wird.

Die Pruefdatei deckt jetzt auch Eingang, Dubletten und Uebertragsvorschlaege
ab. Die aelteren Pruefungen zaehlten exakt und wurden dadurch rot - sie
pruefen jetzt Eigenschaften statt Mengen, damit sie das naechste Wachstum
ueberleben. 54 Tests gruen.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Guido
2026-10-21 21:28:00 +02:00
co-authored by Claude Opus 5
parent adbc3e0aa4
commit 9edf610d59
11 changed files with 1401 additions and 44 deletions
+82
View File
@@ -288,4 +288,86 @@ extension CertainClient {
func loescheKonto(_ id: Int) async throws {
try await schickeStumm(.delete, "accounts/\(id)")
}
// MARK: - Arbeitsliste: Eingang
func eingang(grenze: Int = 200) async throws -> [Eingangseintrag] {
try await hole("inbox", abfrage: [URLQueryItem(name: "limit", value: String(grenze))])
}
/// Macht aus dem Eintrag eine Buchung. Braucht ein Konto – sonst 400.
@discardableResult
func bucheEingang(_ id: Int) async throws -> Eingangseintrag {
try await schicke(.post, "inbox/\(id)/book")
}
/// Bucht den Eintrag als Kontoübertrag statt als Ausgabe oder Einnahme.
///
/// Hat das Backend Bedenken, antwortet es mit 409 (`APIFehler.konflikt`).
/// Dann den Nutzer fragen und mit `trotzdem: true` wiederholen – nicht
/// stillschweigend übergehen und nicht einfach aufgeben.
@discardableResult
func bucheEingangAlsUebertrag(_ id: Int, aufKonto: Int,
trotzdem: Bool = false) async throws -> Eingangseintrag {
try await schicke(.post, "inbox/\(id)/transfer",
koerper: AlsUebertragBuchen(toAccountId: aufKonto,
trotzdem: trotzdem))
}
/// Alle offenen Einträge buchen, die ein Konto haben. Unklare bleiben
/// liegen – die soll man ansehen, nicht im Stapel durchwinken.
@discardableResult
func bucheGanzenEingang() async throws -> SammelErgebnis {
try await schicke(.post, "inbox/book-all")
}
/// Verwirft den Eintrag samt der Buchung, die daraus entstanden ist.
func verwerfeEingang(_ id: Int) async throws {
try await schickeStumm(.delete, "inbox/\(id)")
}
/// Setzt die Kategorie auf dem Eintrag **und** auf der erzeugten Buchung.
@discardableResult
func setzeEingangsKategorie(_ id: Int, kategorie: Int) async throws -> Eingangseintrag {
try await schicke(.post, "inbox/\(id)/category/\(kategorie)")
}
// MARK: - Arbeitsliste: Überträge
/// Fragt nur – beim Hinsehen wird nichts verknüpft.
func uebertragsvorschlaege() async throws -> Uebertragsvorschlaege {
try await hole("uebertraege/vorschlaege")
}
/// Sucht aktiv: Eindeutige Paare mit Übertrags-Hinweis werden dabei
/// verknüpft, der Rest kommt als Vorschlag zurück.
@discardableResult
func sucheUebertraege() async throws -> Uebertragsvorschlaege {
try await schicke(.post, "uebertraege/suchen")
}
func verbindeUebertrag(_ a: Int, _ b: Int) async throws {
try await schickeStumm(.post, "uebertraege/verbinden",
koerper: Buchungspaar(aId: a, bId: b))
}
/// Lehnt einen Vorschlag ab, damit er bei jedem weiteren Suchlauf
/// wegbleibt.
func lehneUebertragAb(_ a: Int, _ b: Int) async throws {
try await schickeStumm(.post, "uebertraege/ignorieren",
koerper: Buchungspaar(aId: a, bId: b))
}
// MARK: - Arbeitsliste: Dubletten
func dubletten() async throws -> Dublettengruppen {
try await hole("transactions/dubletten")
}
/// „Sieht gleich aus, ist aber richtig so." Die Gruppe verschwindet erst,
/// wenn **alle** ihre Zeilen den Haken tragen.
func behalteDubletten(_ ids: [Int]) async throws {
try await schickeStumm(.post, "transactions/dubletten/behalten",
koerper: ["ids": ids])
}
}