feat(zahlung): eigener Step "Zahlung" vor der Uebersicht

- Neuer Workflow-Step "Zahlung" zeigt ausgelieferte Artikel mit Menge und
  Preis, Betragsaufstellung, Zahlungsstatus und einen grossen Button
  "Zahlung abwickeln" (fest unten)
- Zahlungs-Modal: grosser offener Betrag, Methoden als Cards aus den
  Backend-Stammdaten, bei EC-Karte gross die Kundennummer; schliesst nur
  ueber Abbrechen, "Zahlung erhalten" oder "Lieferung abbrechen" (mit Grund)
- Bestaetigung wird am Server protokolliert (RecordDeliveryPayment); bis
  dahin sind "Weiter" und die Uebersicht gesperrt, ausser es ist nichts
  offen (bereits bezahlt). Aendert sich der Betrag danach, wird die Zahlung
  als veraltet markiert und muss erneut abgewickelt werden
- Uebersicht zeigt die abgewickelte Zahlung statt eigener Methodenauswahl;
  Unterschrift-Flow ohne doppelte Inkasso-Stufe; Abschluss sendet Methode
  und Inkasso-Flag der gueltigen Zahlung
- DeliveryPaymentStatus als einzige Quelle fuer offenen Betrag
  (gleiche Formel wie Backend); Artikelliste/Betragskarte als geteilte
  Widgets; Begruendungs-Dialog geteilt mit Info-Step
- API-Client aus aktueller Backend-Spec neu generiert
- Tests fuer Zahlungsstatus und Modal

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Dennis Nemec
2026-09-25 14:04:36 +02:00
parent f832b69b9d
commit dd7fe9a8eb
78 changed files with 7466 additions and 874 deletions

View File

@ -57,6 +57,154 @@
]
}
},
"/admin/belege/{belegnummer}": {
"get": {
"tags": [
"admin"
],
"summary": "Liefert das **volle Detail-Paket** zu einer ERP-Belegnummer: die Lieferung\nmit Positionen (inkl. Scan-St\u00e4nden), Kunde + Ansprechpartner, referenzierte\nArtikel/Lager, Notizen, Geld-Gutschrift, Dienstleistungswerte und Kontakte \u2014\ndestilliert aus dem Tour-Aggregat. Jede Lieferung, unabh\u00e4ngig vom Status.\n`404`, wenn die Belegnummer unbekannt ist.",
"operationId": "belege_details",
"parameters": [
{
"name": "belegnummer",
"in": "path",
"description": "ERP-Belegnummer, z. B. V-30690291",
"required": true,
"schema": {
"type": "string"
}
}
],
"responses": {
"200": {
"description": "Volle Lieferdetails",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/DeliveryDetails"
}
}
}
},
"401": {
"description": "Admin-API-Key fehlt/ung\u00fcltig"
},
"404": {
"description": "Belegnummer unbekannt"
}
},
"security": [
{
"admin_api_key": []
}
]
}
},
"/admin/belege/{belegnummer}/positions-modified": {
"get": {
"tags": [
"admin"
],
"summary": "Liefert zu **einer** ERP-Belegnummer, ob an der Lieferung Positionen\nver\u00e4ndert wurden (Menge reduziert/Zeile entfernt oder Geld-Gutschrift) \u2014\nunabh\u00e4ngig vom Zustand der Lieferung. `404`, wenn die Belegnummer unbekannt\nist. Gibt es mehrere Lieferungen mit derselben Belegnummer, ist das Flag\n`true`, sobald **eine** davon ver\u00e4ndert ist.",
"operationId": "positions_modified",
"parameters": [
{
"name": "belegnummer",
"in": "path",
"description": "ERP-Belegnummer, z. B. V-30690291",
"required": true,
"schema": {
"type": "string"
}
}
],
"responses": {
"200": {
"description": "\u00c4nderungs-Flag der Belegpositionen",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/PositionsModifiedResponse"
}
}
}
},
"401": {
"description": "Admin-API-Key fehlt/ung\u00fcltig"
},
"404": {
"description": "Belegnummer unbekannt"
}
},
"security": [
{
"admin_api_key": []
}
]
}
},
"/admin/completed-deliveries": {
"get": {
"tags": [
"admin"
],
"summary": "Liefert **alle** abgeschlossenen (ausgelieferten) Lieferungen \u2014 optional auf\neinen Datumsbereich eingegrenzt, unabh\u00e4ngig vom Mail-Versand-Status.\nGefiltert wird \u00fcber den **Berliner** Kalendertag des Abschluss-Zeitpunkts\n(`completed_at`).",
"description": "Parameter (alle `DD-MM-YYYY`, alle optional): `day` = Einzeltag (Kurzform\n`from=to=day`), sonst `from`/`to` als **inklusive** Bereichsgrenzen (je\noffen). **Ohne jeden Parameter \u2192 ALLE ausgelieferten Belege** (kein\nDatumsfilter). Pro Lieferung: Belegnummer + `positions_modified` (Menge\nreduziert/Zeile entfernt oder Geld-Gutschrift). Die Halb-Grenzen-Variante ist\nf\u00fcr Range-Filter gedacht: `?from=\u2026` und `?to=\u2026` liefern je eine Menge, deren\nSQL-`AND`-Schnitt den Zeitraum ergibt.",
"operationId": "completed_deliveries",
"parameters": [
{
"name": "day",
"in": "query",
"description": "Einzeltag DD-MM-YYYY (Kurzform from=to)",
"required": false,
"schema": {
"type": "string"
}
},
{
"name": "from",
"in": "query",
"description": "Untere Grenze DD-MM-YYYY (inklusive)",
"required": false,
"schema": {
"type": "string"
}
},
{
"name": "to",
"in": "query",
"description": "Obere Grenze DD-MM-YYYY (inklusive)",
"required": false,
"schema": {
"type": "string"
}
}
],
"responses": {
"200": {
"description": "Ausgelieferte Lieferungen (optional bereichsgefiltert)",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/CompletedDeliveriesResponse"
}
}
}
},
"400": {
"description": "Ung\u00fcltiges Datum"
},
"401": {
"description": "Admin-API-Key fehlt/ung\u00fcltig"
}
},
"security": [
{
"admin_api_key": []
}
]
}
},
"/admin/delivered-belegnummern": {
"get": {
"tags": [
@ -227,6 +375,87 @@
]
}
},
"/admin/reviews": {
"get": {
"tags": [
"admin"
],
"summary": "Listet alle ge\u00e4nderten Lieferscheine, die noch auf eine manuelle\nBest\u00e4tigung (Vier-Augen) warten \u2014 Entfernungen und/oder Geld-Gutschriften.",
"operationId": "list_reviews",
"responses": {
"200": {
"description": "Offene Pr\u00fcfungen",
"content": {
"application/json": {
"schema": {
"type": "array",
"items": {
"$ref": "#/components/schemas/PendingReviewResponse"
}
}
}
}
},
"401": {
"description": "Admin-API-Key fehlt/ung\u00fcltig"
}
},
"security": [
{
"admin_api_key": []
}
]
}
},
"/admin/reviews/{delivery_id}/resolve": {
"post": {
"tags": [
"admin"
],
"summary": "Best\u00e4tigt die Pr\u00fcfung einer ge\u00e4nderten Lieferung (Vier-Augen) \u2014 die\nLieferung verschwindet danach aus `GET /admin/reviews` (sofern nicht\nerneut ge\u00e4ndert).",
"operationId": "resolve_review",
"parameters": [
{
"name": "delivery_id",
"in": "path",
"description": "UUID der Lieferung",
"required": true,
"schema": {
"type": "string"
}
}
],
"requestBody": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ResolveReviewRequest"
}
}
},
"required": true
},
"responses": {
"204": {
"description": "Pr\u00fcfung best\u00e4tigt"
},
"400": {
"description": "Ung\u00fcltige delivery_id / Bearbeiter leer"
},
"401": {
"description": "Admin-API-Key fehlt/ung\u00fcltig"
},
"404": {
"description": "Lieferung nicht gefunden"
}
},
"security": [
{
"admin_api_key": []
}
]
}
},
"/attachments/{id}": {
"get": {
"tags": [
@ -814,6 +1043,62 @@
]
}
},
"/deliveries/{delivery_id}/payment": {
"post": {
"tags": [
"deliveries"
],
"summary": "Protokolliert die Zahlungsabwicklung (\u201eAbkassieren\") einer Lieferung.\nAppend-only, idempotent \u00fcber `clientEventId`. Der Server berechnet den\noffenen Betrag selbst; er muss > 0 sein und `expectedAmountCents`\nentsprechen. Nur bei aktiver Lieferung und aktiver Zahlungsmethode.",
"operationId": "record_payment",
"parameters": [
{
"name": "delivery_id",
"in": "path",
"required": true,
"schema": {
"type": "string",
"format": "uuid"
}
}
],
"requestBody": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/RecordDeliveryPaymentRequest"
}
}
},
"required": true
},
"responses": {
"200": {
"description": "Zahlung protokolliert",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/DeliveryPaymentResponse"
}
}
}
},
"400": {
"description": "Lieferung nicht aktiv, Methode ung\u00fcltig, kein offener Betrag oder Betrag abweichend"
},
"401": {
"description": "Authentifizierung fehlgeschlagen"
},
"404": {
"description": "Lieferung nicht gefunden"
}
},
"security": [
{
"bearer_auth": []
}
]
}
},
"/deliveries/{delivery_id}/resume": {
"post": {
"tags": [
@ -1129,16 +1414,27 @@
]
}
},
"/me/tours/today": {
"/me/tours": {
"get": {
"tags": [
"tours"
],
"summary": "Listet heutige Touren des angemeldeten Fahrers (Filter aus dem JWT).",
"operationId": "list_my_tours_today",
"summary": "Listet die Touren des angemeldeten Fahrers (Filter aus dem JWT) f\u00fcr ein\nZiel-Datum. Ohne `?date=` gilt \u201eheute\". Der Fahrer w\u00e4hlt das Datum aktiv\nim Kopf-Kalender der App (z. B. um die Tour von morgen vorab zu sehen).",
"operationId": "list_my_tours",
"parameters": [
{
"name": "date",
"in": "query",
"description": "Ziel-Tourdatum YYYY-MM-DD (Default: heute)",
"required": false,
"schema": {
"type": "string"
}
}
],
"responses": {
"200": {
"description": "Liste der heutigen Touren",
"description": "Liste der Touren am Ziel-Datum",
"content": {
"application/json": {
"schema": {
@ -1147,6 +1443,9 @@
}
}
},
"400": {
"description": "Ung\u00fcltiges Datum"
},
"401": {
"description": "Authentifizierung fehlgeschlagen"
}
@ -1932,6 +2231,59 @@
}
}
},
"CompletedDeliveriesResponse": {
"type": "object",
"required": [
"count",
"deliveries"
],
"properties": {
"count": {
"type": "integer",
"description": "Anzahl der abgeschlossenen Lieferungen im Bereich.",
"minimum": 0
},
"deliveries": {
"type": "array",
"items": {
"$ref": "#/components/schemas/CompletedDeliveryItem"
},
"description": "Die abgeschlossenen Lieferungen, aufsteigend nach Abschluss-Zeitpunkt."
},
"from": {
"type": [
"string",
"null"
],
"description": "Wirksame untere Grenze (ISO `YYYY-MM-DD`) oder `null` (offen)."
},
"to": {
"type": [
"string",
"null"
],
"description": "Wirksame obere Grenze (ISO `YYYY-MM-DD`) oder `null` (offen)."
}
}
},
"CompletedDeliveryItem": {
"type": "object",
"description": "Eine abgeschlossene Lieferung im Tagesabruf.",
"required": [
"belegnummer",
"positions_modified"
],
"properties": {
"belegnummer": {
"type": "string",
"description": "ERP-Belegnummer der Lieferung."
},
"positions_modified": {
"type": "boolean",
"description": "`true`, wenn an der Lieferung Positionen ver\u00e4ndert wurden \u2014 eine Zeile\nwurde entfernt oder in der Menge reduziert (St\u00fcck-Gutschrift) **oder** es\nliegt eine aktive Geld-Gutschrift vor."
}
}
},
"ContactChannel": {
"type": "object",
"description": "Ein einzelner Kontaktkanal (Telefonnummer / Mobil / E-Mail / Web).\nMehrere pro [`ContactSource`] m\u00f6glich, die `position` h\u00e4lt die\n1-basierte ERP-Reihenfolge (`Telefon` \u2192 1, `Telefon2` \u2192 2 usw.) fest,\ndamit der \u201eprim\u00e4re\" Kanal je Art stabil identifizierbar bleibt.",
@ -2421,6 +2773,120 @@
}
}
},
"DeliveryDetails": {
"type": "object",
"required": [
"tour",
"delivery",
"customerContacts",
"articles",
"warehouses",
"notes",
"services",
"deliveryServices",
"contactSources",
"contactChannels"
],
"properties": {
"articles": {
"type": "array",
"items": {
"$ref": "#/components/schemas/Article"
},
"description": "Nur die von den Positionen referenzierten Artikel."
},
"contactChannels": {
"type": "array",
"items": {
"$ref": "#/components/schemas/ContactChannel"
},
"description": "Die zu `contactSources` geh\u00f6renden Einzel-Kan\u00e4le. Join per `sourceId`."
},
"contactSources": {
"type": "array",
"items": {
"$ref": "#/components/schemas/ContactSource"
},
"description": "Kontaktquellen der Lieferung (Liefer-/Rechnungsadresse, Ansprechpartner \u2026)."
},
"credit": {
"oneOf": [
{
"type": "null"
},
{
"$ref": "#/components/schemas/DeliveryCredit",
"description": "Aktuelle Betrags-Gutschrift (`null`, wenn keine aktiv)."
}
]
},
"customer": {
"oneOf": [
{
"type": "null"
},
{
"$ref": "#/components/schemas/Customer",
"description": "Der Kunde der Lieferung (`null`, falls unauffindbar \u2014 sollte nicht\nvorkommen)."
}
]
},
"customerContacts": {
"type": "array",
"items": {
"$ref": "#/components/schemas/CustomerContact"
},
"description": "Ansprechpartner des Kunden."
},
"delivery": {
"$ref": "#/components/schemas/DeliveryWithItems",
"description": "Die Lieferung selbst inkl. Positionen (mit Scan-St\u00e4nden) und `sortOrder`."
},
"deliveryServices": {
"type": "array",
"items": {
"$ref": "#/components/schemas/DeliveryServiceValue"
},
"description": "F\u00fcr diese Lieferung gesetzte Service-Werte."
},
"notes": {
"type": "array",
"items": {
"$ref": "#/components/schemas/DeliveryNote"
},
"description": "Notizen der Lieferung, aufsteigend nach `createdAt`."
},
"payment": {
"oneOf": [
{
"type": "null"
},
{
"$ref": "#/components/schemas/DeliveryPayment",
"description": "J\u00fcngste protokollierte Zahlungsabwicklung (`None` = keine)."
}
]
},
"services": {
"type": "array",
"items": {
"$ref": "#/components/schemas/Service"
},
"description": "Aktive Service-Definitionen (Stammdaten)."
},
"tour": {
"$ref": "#/components/schemas/Tour",
"description": "Die Tour, zu der die Lieferung geh\u00f6rt."
},
"warehouses": {
"type": "array",
"items": {
"$ref": "#/components/schemas/Warehouse"
},
"description": "Nur die von den Positionen referenzierten Lager."
}
}
},
"DeliveryItem": {
"type": "object",
"description": "Einzelposition einer Lieferung. Vereint regul\u00e4re Belegzeilen und\nSt\u00fccklisten-Komponenten zu einer flachen Liste \u2014 die St\u00fccklisten-\nHierarchie ist ein ERP-Konstrukt und wird beim Sync aufgel\u00f6st.\n\n\u00dcber die Felder `belegzeilen_nr` und `komponenten_artikel_nr` bleibt\ndie ERP-Herkunft aufl\u00f6sbar.",
@ -2579,6 +3045,68 @@
}
}
},
"DeliveryPayment": {
"type": "object",
"description": "Protokollierte Zahlungsabwicklung (\u201eAbkassieren\") einer Lieferung.\n\nAppend-only: jede Best\u00e4tigung des Fahrers ist ein eigener Eintrag; im\nTour-Aggregat wird pro Lieferung nur der j\u00fcngste mitgeliefert. Der Betrag\nist der server-seitig berechnete offene Betrag zum Zeitpunkt der\nBest\u00e4tigung. Weicht der aktuelle offene Betrag davon ab (z. B. nach einer\nneuen Gutschrift), ist der Eintrag veraltet und muss erneuert werden.",
"required": [
"id",
"deliveryId",
"paymentMethodId",
"paymentMethodCode",
"amountCents",
"recordedByPersonalnummer",
"recordedAt"
],
"properties": {
"amountCents": {
"type": "integer",
"format": "int64",
"description": "Abgewickelter Betrag in Cent (offener Betrag bei Best\u00e4tigung)."
},
"deliveryId": {
"type": "string",
"format": "uuid"
},
"id": {
"type": "string",
"format": "uuid"
},
"paymentMethodCode": {
"type": "string",
"description": "Snapshot des Methoden-Codes (`cash`, `ec_card`, `invoice`, \u2026)."
},
"paymentMethodId": {
"type": "string",
"format": "uuid"
},
"recordedAt": {
"type": "string",
"format": "date-time"
},
"recordedByCarId": {
"type": [
"string",
"null"
],
"format": "uuid"
},
"recordedByPersonalnummer": {
"type": "integer",
"format": "int64"
}
}
},
"DeliveryPaymentResponse": {
"type": "object",
"required": [
"payment"
],
"properties": {
"payment": {
"$ref": "#/components/schemas/DeliveryPayment"
}
}
},
"DeliveryResponse": {
"type": "object",
"required": [
@ -2814,6 +3342,165 @@
}
}
},
"PendingReviewResponse": {
"type": "object",
"required": [
"delivery_id",
"erp_belegart_id",
"erp_belegnummer",
"customer_name",
"tour_date",
"last_change_at",
"credited_items",
"money_credit_cents"
],
"properties": {
"credited_items": {
"type": "array",
"items": {
"$ref": "#/components/schemas/ReviewedItemResponse"
},
"description": "Entfernte/teil-gutgeschriebene Positionen."
},
"customer_name": {
"type": "string"
},
"delivery_id": {
"type": "string"
},
"erp_belegart_id": {
"type": "integer",
"format": "int64"
},
"erp_belegnummer": {
"type": "string"
},
"last_change_at": {
"type": "string",
"description": "Zeitpunkt der letzten beleg-\u00e4ndernden Aktion (RFC 3339)."
},
"money_credit_cents": {
"type": "integer",
"format": "int64",
"description": "Geld-Gutschrift in Cent (0 = keine)."
},
"money_credit_reason": {
"type": [
"string",
"null"
]
},
"tour_date": {
"type": "string",
"description": "Tourdatum (ISO `YYYY-MM-DD`)."
}
}
},
"PositionsModifiedResponse": {
"type": "object",
"required": [
"belegnummer",
"positions_modified"
],
"properties": {
"belegnummer": {
"type": "string",
"description": "ERP-Belegnummer, nach der gefragt wurde."
},
"positions_modified": {
"type": "boolean",
"description": "`true`, wenn an der Lieferung Positionen ver\u00e4ndert wurden \u2014 eine Zeile\nwurde entfernt oder in der Menge reduziert (St\u00fcck-Gutschrift) **oder** es\nliegt eine aktive Geld-Gutschrift vor."
}
}
},
"RecordDeliveryPaymentRequest": {
"type": "object",
"required": [
"clientEventId",
"paymentMethodId",
"expectedAmountCents"
],
"properties": {
"authorCarId": {
"type": [
"string",
"null"
],
"format": "uuid",
"description": "Fahrzeug des Akteurs (Audit-Spur). Muss zum Account geh\u00f6ren."
},
"clientEventId": {
"type": "string",
"format": "uuid",
"description": "Idempotenz-Schl\u00fcssel \u2014 pro Best\u00e4tigung genau einmal vergeben."
},
"expectedAmountCents": {
"type": "integer",
"format": "int64",
"description": "Betrag in Cent, den der Fahrer in der App gesehen und best\u00e4tigt hat.\nDer Server berechnet den offenen Betrag selbst und lehnt ab, wenn er\ndavon abweicht \u2014 so landet nie ein Betrag im Protokoll, den der\nFahrer nicht gesehen hat."
},
"paymentMethodId": {
"type": "string",
"format": "uuid",
"description": "Gew\u00e4hlte Zahlungsmethode. Muss existieren und aktiv sein."
}
}
},
"ResolveReviewRequest": {
"type": "object",
"required": [
"resolved_by"
],
"properties": {
"note": {
"type": [
"string",
"null"
],
"description": "Optionale Notiz zur getroffenen Entscheidung."
},
"resolved_by": {
"type": "string",
"description": "Bearbeiter (Name/K\u00fcrzel), der die Pr\u00fcfung best\u00e4tigt."
}
}
},
"ReviewedItemResponse": {
"type": "object",
"required": [
"belegzeilen_nr",
"artikel_nr",
"article_name",
"required_quantity",
"credited_quantity"
],
"properties": {
"article_name": {
"type": "string"
},
"artikel_nr": {
"type": "string"
},
"belegzeilen_nr": {
"type": "integer",
"format": "int32"
},
"credited_quantity": {
"type": "integer",
"format": "int32"
},
"reason": {
"type": [
"string",
"null"
]
},
"required_quantity": {
"type": "integer",
"format": "int32"
}
}
},
"ScanEvent": {
"type": "object",
"required": [
@ -3416,6 +4103,7 @@
"warehouses",
"notes",
"credits",
"payments",
"services",
"deliveryServices",
"contactSources",
@ -3481,6 +4169,13 @@
},
"description": "Alle Notizen aller Lieferungen dieser Tour, in einer Liste.\nDie App joint clientseitig per `delivery_id`. Reihenfolge:\npro Lieferung aufsteigend nach `created_at`."
},
"payments": {
"type": "array",
"items": {
"$ref": "#/components/schemas/DeliveryPayment"
},
"description": "J\u00fcngste protokollierte Zahlungsabwicklung pro Lieferung (nur\nLieferungen mit mindestens einem Eintrag). Join per `delivery_id`."
},
"services": {
"type": "array",
"items": {