Files
knsoandClaude Opus 5.5 a4ade5f649 Buchungskalender: Preise je Personenzahl (Server)
Jede Staffel darf personenCent tragen (Preis pro Nacht für 1 bis k Personen, k = 2 bis 10), ohne Schemaänderung im JSON-Feld staffeln. pruefePreise prüft Länge, Werte und Gleichheit über alle Staffeln und setzt preisProNachtCent = personenCent[0]. berechnePreis wählt die Spalte der Gästezahl (Posten mit Personenzahl), bei mehr Gästen als Spalten gilt die letzte plus Zusatzperson-Aufpreis. preiseVollstaendig prüft die Konsistenz. Ohne personenCent rechnet alles wie bisher. Tests mit Mimis Preismatrix, Doku in unterkunft-api.md.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 15:02:45 +00:00

7.9 KiB
Raw Permalink Blame History

Buchungskalender: API-Vertrag

Verbindlich für Strang 2 (Widget und Buchungsseite, bauen gegen die öffentliche Schnittstelle) und Strang 3 (Portal, baut gegen die Kunden-API). Task 6 bis 8 erfüllen ihn. Fehlerform überall { fehler: code, felder?: {feld: text} }.

Livegang-Reihenfolge

Vor dem Anheben von UNTERKUNFT_ENABLED über off zuerst scripts/unterkunft-setup.mjs (legt die Collections samt Index idx_account an) und das termine-Setup (Attribut anhang der Mail-Warteschlange) auf Produktion laufen lassen.

Öffentlich (/api/public/unterkunft/v1)

  • GET /:siteKey → { aktiv:true, anbieter:{ name, ort, datenschutzUrl }, akzentfarbe, formToken, unterkuenfte:[{ id, name, maxPersonen, mindestNaechte, checkIn, checkOut }] } oder { aktiv:false }.
  • GET /:siteKey/belegung?unterkunft=<id>&von=YYYY-MM-DD&bis=YYYY-MM-DD → { belegt:[{von,bis}], heute:'YYYY-MM-DD', bisMax:'YYYY-MM-DD' } (Quellen nicht offengelegt). Liegt bis hinter bisMax (heute + horizontMonate), wird es auf bisMax gekappt (kein 400); ein Zeitraum mit bis < von oder ungültigem Datum ergibt 400 ungueltig.
  • POST /:siteKey/anfrage Body { unterkunft, von, bis, personen, name, firma, email, telefon, nachricht, formToken, kontakt_url } → 201 { nummer }.
  • GET /:siteKey/buchung/:id?t= → { status, nummer, akzentfarbe, unterkunft:{name,checkIn,checkOut}, von, bis, naechte, personen, posten, summen, steuerart, fristAt, stornobedingungen, anbieter:{firma,inhaber,strasse,plz,ort,email,telefon,datenschutzUrl}, gast:{name,firma}, rechnungsadresse?, zahlarten:['vorort'|'online'], zahlart?, bezahlt:boolean, rechnungNummer? }.
  • POST /:siteKey/buchung/:id?t= nach Fristablauf: 410 { fehler:'frist_abgelaufen' }. Body { rechnungsadresse:{name,firma,strasse,plz,ort,land}, zahlart } → { status:'gebucht', rechnungNummer, zahlungUrl? } (bei online direkt Checkout-URL).
  • POST /:siteKey/buchung/:id/zahlung?t= → { url }.
  • GET /:siteKey/buchung/:id/rechnung.pdf?t=&art=rechnung|storno → PDF.

Kunde (/api/unterkunft, Session, ?projectId= steht überall in der Query, auch bei POST/PUT)

Antwortformen, die die Portal-Oberfläche erwartet (Geld in Cent, Daten YYYY-MM-DD; Fehler überall { fehler, felder? }, nie tokenHash/ipHash):

  • GET /status → { state, steuerart?, stripe:{ status:'keins'|'onboarding'|'aktiv', vorhandenesShopKonto }, onlineZahlung, unterkuenfte:Zahl, rechnungenVorhanden }; bei state:'off' zusätzlich anfrage:{ status:'offen'|'abgelehnt'|'angenommen', at, begruendung?, anfrage:{ art, einheiten, portale, nachricht } } | null.
  • GET /einstellungen → { einstellungen:{ firma, inhaber, strasse, plz, ort, email, telefon, steuernummer, ustId, iban, bic, bank, rechnungsPraefix, akzentfarbe, onlineZahlung:boolean, rechnungAuftrag:boolean }, stripe:{ status, vorhandenesShopKonto }, rechnungenVorhanden }.
  • PUT /einstellungen Body = genau diese Felder ohne Wrapper. rechnungAuftrag (boolean) leitet der Server aus rechnungAuftragAt ab: true setzt es, falls leer, false widerruft (null). onlineZahlung nur true, wenn Stripe aktiv ist; der Präfix ist bei vorhandenen Rechnungen nicht änderbar. Feldfehler als felder.<name>.
  • GET /unterkuenfte → { unterkuenfte:[{ id, name, beschreibung, maxPersonen, mindestNaechte, checkIn, checkOut, sort, aktiv, icalImporte:[{ id, name, url, letzterAbrufAt, letzterErfolgAt, fehler }], icalExportUrl }] } (icalExportUrl = volle URL mit Token).
  • POST /unterkuenfte, PUT /unterkuenfte/:id Body { name, beschreibung, maxPersonen, mindestNaechte, checkIn, checkOut, sort, aktiv } → { unterkunft }; Deaktivieren = PUT mit aktiv:false. DELETE /unterkuenfte/:id.
  • GET|PUT /unterkuenfte/:id/preise → { preise:{ staffeln:[{abNaechten,preisProNachtCent,personenCent?}], reinigungCent, personenInklusive, aufpreisPersonNachtCent, direktrabattProzent, steuerart:'kleinunternehmer'|'regel', ustUebernachtung, ustReinigung, fristTage, stornobedingungen } | null }; PUT sendet genau diese Felder.
    • Preismodell „pauschal“ (ohne personenCent): preisProNachtCent gilt für personenInklusive Gäste, jede weitere Person kostet aufpreisPersonNachtCent je Nacht (Posten „Zusatzperson“).
    • Preismodell „je Personenzahl“: jede Staffel trägt personenCent:[c1,c2,…,ck] = Preis pro Nacht für die ganze Unterkunft bei 1, 2, …, k Gästen (k = 2 bis 10, Werte ganze Cent 100 bis 100000). Entweder haben alle Staffeln personenCent mit gleicher Länge k oder keine; sonst 400 felder.staffeln. Der Server setzt preisProNachtCent = personenCent[0] (eine mitgeschickte Zahl wird überschrieben). Eine Staffel gilt weiterhin ab ihrer Nächtezahl für alle Nächte. Der Posten heißt „Übernachtung, 7 Nächte, 3 Personen“; personenInklusive wird ignoriert. Bei mehr als k Gästen gilt ck plus der Posten „Zusatzperson“ (aufpreisPersonNachtCent, nur wenn größer 0) für die Gäste über k. Der Direktrabatt gilt auf Übernachtung und Zusatzpersonen.
    • Kein neues Schema: personenCent liegt im JSON-Text ukPreise.staffeln (2000 Zeichen; der Höchstfall 6 Staffeln × 10 Werte braucht rund 800).
  • GET /unterkuenfte/:id/kalender?von&bis → { bereiche:[{ von, bis (Abreisetag, exklusiv), quelle:'gebucht'|'reserviert'|'ical'|'manuell' (auch 'ical:<importId>', 'anfrage:<id>' zählt als gebucht), anfrageNummer?, sperreId?, notiz? }] }.
  • POST /unterkuenfte/:id/sperren { von, bis, notiz } → 201, 409 zeitraum_belegt; DELETE /sperren/:id.
  • POST /unterkuenfte/:id/ical-importe { name, url } (nur https; Feldfehler felder.url) → 201; DELETE /unterkuenfte/:id/ical-importe/:importId; POST /unterkuenfte/:id/ical-export/erneuern.
  • GET /anfragen?filter=offen|gebucht|alle → { anfragen:[{ id, nummer, von, bis, naechte, personen, gast:{name}, status, summen:{bruttoCent}|null, unterkunftName }] }.
  • GET /anfragen/:id → { anfrage:{ id, nummer, status, von, bis, naechte, personen, unterkunftId, unterkunftName, gast:{name,firma,email,telefon,nachricht}, rechnungsadresse|null, posten|null, summen|null, steuerart, zahlart|null, bezahltVia|null, bezahltAt|null, erstattetCent, rechnungId|null, rechnungNummer|null, stornoRechnungId|null, fristAt|null, nachrichtVermieter }, vorschlag:{ posten, summen } } (vorschlag = berechnePreis).
  • POST /anfragen/:id/freigeben { posten, fristTage, nachricht } → { anfrage, buchungsLink }; POST /anfragen/:id/ablehnen { text }; POST /anfragen/:id/bezahlt; POST /anfragen/:id/stornieren { grund, erstattungCent } (502 erstattung_fehlgeschlagen, wenn die Stornierung galt, die Erstattung aber scheiterte).
  • POST /preisvorschau { unterkunftId, von, bis, personen } → { naechte, posten, summen:{ bruttoCent } } (rechnet mit den gespeicherten Preisen).
  • GET /rechnungen → { rechnungen:[{ id, nummer, datum, art:'rechnung'|'storno', gastName, bruttoCent (Storno negativ), anfrageNummer? }] }; GET /rechnungen/:id.pdf (nur Einzel-PDF, kein ZIP, Spec N7).
  • POST /stripe/verbinden → { url }; POST /stripe/vorhandenes-konto und POST /stripe/aktualisieren → { stripe:{ status } }.
  • POST /anfrage-funktion { art:'ferienwohnung'|'monteurzimmer'|'pension'|'sonstiges' ('monteur' wird als Alias angenommen), einheiten:1-999, portale:Liste aus 'booking'|'airbnb', nachricht } → { anfrage }; Feldfehler felder.art|einheiten|portale|nachricht; 409, wenn schon eine Anfrage läuft. POST /anfrage-funktion/zurueckziehen.

Team (/api/team/unterkunft)

  • GET /:customerId → { projects:[{ projectId, name, state, anfrage:{ status:''|'offen'|'angenommen'|'abgelehnt'|'zurueckgezogen', at, art:'ferienwohnung'|'monteurzimmer'|'pension'|'sonstiges', einheiten, portale:boolean, nachricht, entschiedenAt, entschiedenVon, begruendung }, stripe, unterkuenfte:Zahl, offeneAnfragen:Zahl }] } (Hülle wie GET /api/team/shop/:customerId).
  • POST /:customerId/anfrage { projectId, entscheidung:'annehmen'|'ablehnen', begruendung } → { ok, anfrage, mailFehler? }.