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>
7.9 KiB
7.9 KiB
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). LiegtbishinterbisMax(heute +horizontMonate), wird es aufbisMaxgekappt (kein 400); ein Zeitraum mitbis < vonoder ungültigem Datum ergibt 400ungueltig.POST /:siteKey/anfrageBody{ 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? }(beionlinedirekt 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 }; beistate:'off'zusätzlichanfrage:{ 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 /einstellungenBody = genau diese Felder ohne Wrapper.rechnungAuftrag(boolean) leitet der Server ausrechnungAuftragAtab:truesetzt es, falls leer,falsewiderruft (null).onlineZahlungnurtrue, wenn Stripe aktiv ist; der Präfix ist bei vorhandenen Rechnungen nicht änderbar. Feldfehler alsfelder.<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/:idBody{ name, beschreibung, maxPersonen, mindestNaechte, checkIn, checkOut, sort, aktiv }→{ unterkunft }; Deaktivieren =PUTmitaktiv: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):preisProNachtCentgilt fürpersonenInklusiveGäste, jede weitere Person kostetaufpreisPersonNachtCentje 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 StaffelnpersonenCentmit gleicher Länge k oder keine; sonst 400felder.staffeln. Der Server setztpreisProNachtCent = 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“;personenInklusivewird ignoriert. Bei mehr als k Gästen giltckplus 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:
personenCentliegt im JSON-TextukPreise.staffeln(2000 Zeichen; der Höchstfall 6 Staffeln × 10 Werte braucht rund 800).
- Preismodell „pauschal“ (ohne
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, 409zeitraum_belegt;DELETE /sperren/:id.POST /unterkuenfte/:id/ical-importe{ name, url }(nur https; Feldfehlerfelder.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 }(502erstattung_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-kontoundPOST /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 }; Feldfehlerfelder.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 wieGET /api/team/shop/:customerId).POST /:customerId/anfrage{ projectId, entscheidung:'annehmen'|'ablehnen', begruendung }→{ ok, anfrage, mailFehler? }.