+
+
+
diff --git a/docs/.vitepress/components/ExpandAll.vue b/docs/.vitepress/components/ExpandAll.vue
new file mode 100644
index 000000000..85c12a95b
--- /dev/null
+++ b/docs/.vitepress/components/ExpandAll.vue
@@ -0,0 +1,53 @@
+
+
+
+
+
+
+
diff --git a/docs/.vitepress/config.mts b/docs/.vitepress/config.mts
index cbf4fdbc8..a1ae2fa7d 100644
--- a/docs/.vitepress/config.mts
+++ b/docs/.vitepress/config.mts
@@ -273,19 +273,7 @@ export function sidebarDocs_de(): DefaultTheme.SidebarItem[] {
{ text: 'Häufige Fragen (Version 0.9.x)', link: 'faq' }
]
},
- {
- text: 'Häufige Fragen (FAQ)', base: '/de/documentation/faq/',
- link: '/',
- collapsed: true,
- items: [
- { text: 'Die Seite ist sehr langsam', link: 'site-slow' },
- { text: 'Probleme und Antworten', link: 'problems-and-answers' },
- { text: 'Wie bekomme ich den Buchungskommentar ...', link: 'booking-comment-emails' },
- { text: 'Kann ich Zahlenschloss-Codes in E-Mails einfügen?', link: 'lock-codes-email' },
- { text: 'Artikeldetailseite übersichtlicher gestalten', link: 'organise-article-page' },
- { text: 'Wie verhindere ich Spam Registrierungen', link: 'avoid-spam-registrations'}
- ]
- },
+ { text: 'Häufige Fragen (FAQ)', link: '/de/documentation/faq/', items: [] },
]
}
@@ -395,19 +383,7 @@ export function sidebarDocs_en(): DefaultTheme.SidebarItem[] {
{ text: 'Overview of releases and planned further development', link: 'release-overview' }
]
},
- {
- text: 'Frequently Asked Questions (FAQ)', base: '/en/documentation/faq/',
- link: '/',
- collapsed: true,
- items: [
- { text: 'The site is very slow', link: 'site-slow' },
- { text: 'Problems and answers', link: 'problems-and-answers' },
- { text: 'How do I show the booking comment on the page and in the email?', link: 'booking-comment-emails' },
- { text: 'How do I show lock codes in emails?', link: 'lock-codes-email' },
- { text: 'How can I make the item detail page clearer?', link: 'organise-article-page' },
- { text: 'How do I prevent spam registrations?', link: 'avoid-spam-registrations'}
- ]
- },
+ { text: 'Frequently Asked Questions (FAQ)', link: '/en/documentation/faq/', items: [] },
]
}
diff --git a/docs/.vitepress/theme/index.js b/docs/.vitepress/theme/index.js
index fd81d720f..293df5935 100644
--- a/docs/.vitepress/theme/index.js
+++ b/docs/.vitepress/theme/index.js
@@ -1,25 +1,28 @@
// .vitepress/theme/index.js
+import { h, onMounted } from 'vue'
import DefaultTheme from 'vitepress/theme'
import './custom.css'
-import { onMounted } from 'vue';
-import mediumZoom from 'medium-zoom';
-import { useData } from 'vitepress';
+import mediumZoom from 'medium-zoom'
+import { useData } from 'vitepress'
+import AnnouncementBanner from '../components/AnnouncementBanner.vue'
export default {
- ...DefaultTheme,
- setup() {
- const { page } = useData();
-
- onMounted(() => {
- const LOCALE_STORAGE_KEY = 'cb-doc-locale';
- const path = window.location.pathname;
- const locale = path.startsWith('/en/') ? '/en/' : path.startsWith('/de/') ? '/de/' : '/de/';
-
- // Persist the currently viewed locale so root can reuse the choice later
- localStorage.setItem(LOCALE_STORAGE_KEY, locale);
-
- // Keep zoom working across locales
- mediumZoom('[data-zoomable]', { background: 'var(--vp-c-bg)' });
- });
- },
+ ...DefaultTheme,
+ Layout() {
+ return h(DefaultTheme.Layout, null, {
+ 'home-hero-before': () => h(AnnouncementBanner),
+ })
+ },
+ setup() {
+ const { page } = useData();
+ onMounted(() => {
+ const LOCALE_STORAGE_KEY = 'cb-doc-locale';
+ const path = window.location.pathname;
+ const locale = path.startsWith('/en/') ? '/en/' : path.startsWith('/de/') ? '/de/' : '/de/';
+ // Persist the currently viewed locale so root can reuse the choice later
+ localStorage.setItem(LOCALE_STORAGE_KEY, locale);
+ // Keep zoom working across locales
+ mediumZoom('[data-zoomable]', { background: 'var(--vp-c-bg)' });
+ });
+ },
};
diff --git a/docs/de/documentation/advanced-functionality/cache.md b/docs/de/documentation/advanced-functionality/cache.md
index 8e280ba5b..b1f37169d 100644
--- a/docs/de/documentation/advanced-functionality/cache.md
+++ b/docs/de/documentation/advanced-functionality/cache.md
@@ -36,7 +36,7 @@ Ein falsch konfigurierter Cache kann deine Seite verlangsamen.
Probiere dann einen anderen Pfad für den Cache oder einen anderen Cache Adapter.
* Falls deine Seite sehr langsam ist, kann das auch auf Problem mit dem Cache hindeuten.
- Mehr dazu: [Die Seite ist sehr langsam](../faq/site-slow).
+ Mehr dazu: [Die Seite ist sehr langsam](../faq/index#my-site-is-very-slow-what-can-i-do).
* Siehe auch die Seite [Häufige Fragen](../faq/) für bekannte Inkompatibilitäten.
* **Regelmäßiges Aufwärmen des Caches durch Cronjob**:
diff --git a/docs/de/documentation/advanced-functionality/hooks-and-filters.md b/docs/de/documentation/advanced-functionality/hooks-and-filters.md
index 01f7587be..1b48d8452 100644
--- a/docs/de/documentation/advanced-functionality/hooks-and-filters.md
+++ b/docs/de/documentation/advanced-functionality/hooks-and-filters.md
@@ -50,6 +50,10 @@ add_action( 'commonsbooking_before_item-single', 'itemsingle_callback' );
* commonsbooking_before_item-single
* commonsbooking_after_item-single
* commonsbooking_mail_sent
+ * [commonsbooking_booking_created](#buchungs-lifecycle-hooks-seit-2-11-0)
+ * [commonsbooking_booking_confirmed](#buchungs-lifecycle-hooks-seit-2-11-0)
+ * [commonsbooking_booking_cancelled](#buchungs-lifecycle-hooks-seit-2-11-0)
+ * [commonsbooking_booking_status_changed](#buchungs-lifecycle-hooks-seit-2-11-0)
### Hooks im Objektkontext (seit 2.10.8)
@@ -75,6 +79,43 @@ function my_cb_before_booking_single( $booking_id, $booking ) {
add_action( 'commonsbooking_before_booking-single', 'my_cb_before_booking_single', 10, 2 );
```
+### Buchungs-Lifecycle-Hooks (seit 2.11.0)
+
+Mit diesen Action Hooks kannst du auf den Lebenszyklus einer Buchung reagieren –
+zum Beispiel um ein elektronisches Schloss anzusteuern, einen externen Kalender
+zu synchronisieren oder ein Protokoll zu schreiben. Sie werden unabhängig davon
+ausgelöst, wo die Änderung ausgelöst wurde (Frontend-Kalender, Admin-Backend,
+REST-API oder WP-CLI), da sie am Statuswechsel des WordPress-Posts und nicht an
+einem einzelnen Formular-Handler hängen.
+
+Jeder Hook erhält die Post-ID der Buchung und die zugehörige Instanz aus
+`\CommonsBooking\Model\Booking` (Ausnahme: `..._status_changed`, hier werden
+zusätzlich der alte und der neue Status übergeben).
+
+ * `commonsbooking_booking_created`
+ * Wird **einmalig** ausgelöst, sobald eine Buchung zum ersten Mal einen echten Status erhält.
+ * Parameter: `int $booking_id`, `\CommonsBooking\Model\Booking $booking`
+ * `commonsbooking_booking_confirmed`
+ * Wird ausgelöst, sobald eine Buchung den Status `confirmed` erhält. Das ist der empfohlene Hook für Integrationen, die auf eine aktiv werdende Buchung reagieren müssen.
+ * Parameter: `int $booking_id`, `\CommonsBooking\Model\Booking $booking`
+ * `commonsbooking_booking_cancelled`
+ * Wird ausgelöst, wenn eine Buchung storniert wird.
+ * Parameter: `int $booking_id`, `\CommonsBooking\Model\Booking $booking`
+ * `commonsbooking_booking_status_changed`
+ * Wird bei jedem Statuswechsel einer Buchung ausgelöst (Obermenge der Hooks oben). Hinweis: Da eine Stornierung den Status direkt in die Datenbank schreibt, sollten Stornierungen besser über `commonsbooking_booking_cancelled` als über diesen Hook beobachtet werden.
+ * Parameter: `int $booking_id`, `string $old_status`, `string $new_status`, `\CommonsBooking\Model\Booking $booking`
+
+#### Beispiel: Schloss ansteuern, wenn eine Buchung bestätigt wird
+
+```php
+add_action( 'commonsbooking_booking_confirmed', function ( $booking_id, $booking ) {
+ $item = $booking->getItem();
+ $lockCode = $booking->formattedBookingCode();
+ // Code / Buchungszeitraum hier an das Schließsystem übergeben …
+ my_locking_system_program( $item->ID, $lockCode, $booking->getStartDate(), $booking->getEndDate() );
+}, 10, 2 );
+```
+
## Filter Hooks
Filter Hooks (https://developer.wordpress.org/plugins/hooks/filters) funktionieren
@@ -85,7 +126,7 @@ zurückgibt.
### Alle Filter Hooks im Überblick:
* commonsbooking_custom_metadata
- * [commonsbooking_isCurrentUserAdmin](../basics/permission-management#filterhook-isCurrentUserAdmin)
+ * [commonsbooking_isUserAdmin](../basics/permission-management#filterhook-isUserAdmin)
* commonsbooking_isCurrentUserSubscriber
* commonsbooking_get_template_part
* commonsbooking_template_tag
@@ -96,6 +137,7 @@ zurückgibt.
* commonsbooking_mail_body
* commonsbooking_mail_attachment
* commonsbooking_disableCache
+ * commonsbooking_gbfs_feeds
Es gibt auch Filter Hooks, mit denen du zusätzliche Benutzerrollen, die
zusätzlich zum CB Manager Artikel und Standorte administrieren können,
diff --git a/docs/de/documentation/api/commonsbooking-api.md b/docs/de/documentation/api/commonsbooking-api.md
index e42d0fe44..976fc7865 100644
--- a/docs/de/documentation/api/commonsbooking-api.md
+++ b/docs/de/documentation/api/commonsbooking-api.md
@@ -29,6 +29,10 @@ Die API erreichst du über _Einstellungen_ -> _CommonsBooking_ -> Tab: _API / Ex
* Push-URL: Hier könnt ihr die URL des empfangenden Systems eingeben. CommonsBooking wird diese URL bei jeder Änderung der Daten aufrufen, sodass das entfernte System dann über eine Änderung der Daten informiert und diese über einen separaten API-Call abrufen kann. So kann ein Datenaustausch in Echtzeit ermöglicht werden.
* API-Schlüssel: Hier könnt ihr einen selbstgewählten API-Schlüssel eingeben. Wenn der Schlüssel gesetzt ist, muss das abfragende System in jeder Abfrage den Parameter apikey= _[Schlüssel]_ mitliefern.
+## Artikel von der API ausschliessen
+
+Wenn Artikel nicht in der API erscheinen sollen, müssen diese explizit ausgeschlossen werden. Diese Einstellungen kann auf Artikelebene vorgenommen werden, wenn dort der entsprechende Haken gesetzt ist, tauchen diese Artikel nicht in den API Routen auf.
+
## Spezifikation der API-Routen
**Verfügbarkeit**
diff --git a/docs/de/documentation/api/gbfs.md b/docs/de/documentation/api/gbfs.md
index aca58d32b..c8ea24219 100644
--- a/docs/de/documentation/api/gbfs.md
+++ b/docs/de/documentation/api/gbfs.md
@@ -1,17 +1,22 @@
# GBFS
-::: danger Fehler in der API
-Mit [Issue 1388](https://github.com/wielebenwir/commonsbooking/issues/1388) ist ein Fehler in der API dokumentiert. Die zeitliche Verfügbarkeit ist u.U. also nicht korrekt.
-:::
-
-Seit 2.5
-
Diese Schnittstelle stellt Daten der [Standorte](../first-steps/create-location),
[Artikel](../first-steps/create-item) und deren Verfügbarkeit über die
[Zeitrahmen](../first-steps/booking-timeframes-manage) in einem stadardisierten Schema bereit.
-Die folgenden Endpunkte der _General Bikeshare Feed Specification_ ([GBFS](https://www.gbfs.org/documentation/)) Schema werden unterstützt:
+Aktuell wird die Version 3.1-RC3 der _General Bikeshare Feed Specification_ ([GBFS](https://www.gbfs.org/documentation/)) unterstützt und die folgenden Endpunkte werden vom Plugin exponiert:
+
+* station_status.json
+* station_information.json
+* system_information.json
+* vehicle_status.json
+* vehicle_availability.json
+* vehicle_types.json
+* gbfs.json (Discovery)
-* station_status
-* station_information
-* system_information
-* gbfs.json (Discovery)
\ No newline at end of file
+## Zu vehicle_types.json
+
+Da über CommonsBooking hauptsächlich Lastenräder verliehen werden, gibt die API standardmäßig den `form_factor` als `cargo_bicycle` zurück. Da `propulsion_type` ein Pflichtfeld ist, wird hier standardmäßig `human` gesetzt. Wenn das nicht der Fall wäre, müssten noch viel mehr zusätzliche Informationen zum Antrieb zur Verfügung gestellt werden.
+
+::: info Behobenes Problem
+Seit Juni 2026 (Version 2.11) ist der [Fehler zur Bereitstellung der zeitlichen Verfügbarkeit](https://github.com/wielebenwir/commonsbooking/issues/1388) behoben.
+:::
diff --git a/docs/de/documentation/basics/permission-management.md b/docs/de/documentation/basics/permission-management.md
index af22ab9f6..7415c1fc2 100644
--- a/docs/de/documentation/basics/permission-management.md
+++ b/docs/de/documentation/basics/permission-management.md
@@ -60,14 +60,14 @@ Folgende Dinge darf der CommonsBooking Manager zum Beispiel nicht:
* Das Design der Seite ändern
* usw.
-## CommonsBooking Manager zum Manager für alle Artikel / Standorte machen {#filterhook-isCurrentUserAdmin}
+## CommonsBooking Manager zum Manager für alle Artikel / Standorte machen {#filterhook-isUserAdmin}
Mithilfe eines [Filter Hooks](../advanced-functionality/hooks-and-filters) kannst du eine bestimmte Rolle so einstellen,
dass sie automatisch Manager für alle Artikel / Standorte wird. Das unten genannte Beispiel macht das mit der Rolle 'cb_manager', also konfiguriert den CB-Manager so, dass er automatisch allen Artikeln und Standorten der Instanz zugerechnet wird. Falls das mit einer anderen Rolle passieren soll, muss diese Rolle auch den Manager Rollen mit einem [weiteren Codeschnipsel hinzugefügt werden](#filterhook-manager-roles).
```php
add_filter(
- 'commonsbooking_isCurrentUserAdmin',
+ 'commonsbooking_isUserAdmin',
function ( bool $isAdmin, WP_User $user ) {
return in_array( 'cb_manager', $user->roles, true ) ? true : $isAdmin;
},
diff --git a/docs/de/documentation/faq/avoid-spam-registrations.md b/docs/de/documentation/faq/avoid-spam-registrations.md
deleted file mode 100644
index 475a5f90f..000000000
--- a/docs/de/documentation/faq/avoid-spam-registrations.md
+++ /dev/null
@@ -1,9 +0,0 @@
-# Wie verhindere ich Spam Registrierungen
-
-Dafür gibt es verschiedene Möglichkeiten (Vorschläge aus der Community):
-
-* Ein HoneyPot lenkt Bots ab, ohne Menschen zu nerven: https://wordpress.org/plugins/honeypot/
-
-* "Ich hatte mal für **UltimateMember ein winziges Plugin geschrieben mit dem man einfach einen Text eingeben muss um sich zu registrieren**. Barrierearm und hält alle Bots ab: [Download von Github](https://github.com/hansmorb/um-captchaquiz/raw/refs/heads/master/um-captchaquiz.zip). Dazu einfach eine Textbox erstellen und den Metaschlüssel in den Plugin Einstellungen eintragen."
-
-* "Wir nutzen Hcaptcha für wordpress. Nach der installation in den Einstellungen des Plugins die von euch genutzte Registrierung (z.B. UltraMember auswählen, standardmäßig sollte die WordPress-eigene Registrierung gewählt werden). Für die Nutzung muss ein Hcaptcha Account erstellt werden. Sie werben mit Privacy-First und das keine Nutzerdaten verkauft werden. Ich selber habe das nicht geprüft." — [Download aus dem Plugin-Verzeichnis](https://wordpress.org/plugins/hcaptcha-for-forms-and-more)
diff --git a/docs/de/documentation/faq/booking-comment-emails.md b/docs/de/documentation/faq/booking-comment-emails.md
deleted file mode 100644
index 6a80d0592..000000000
--- a/docs/de/documentation/faq/booking-comment-emails.md
+++ /dev/null
@@ -1,5 +0,0 @@
-# Wie bekomme ich den Buchungskommentar auf die Seite und in die Email?
-
-In den Einstellungen kannst du die Buchungskommentare aktivieren. In den email
-Vorlagen musst du dann folgenden Code einfügen: `{{booking:returnComment}}`
-
diff --git a/docs/de/documentation/faq/index.md b/docs/de/documentation/faq/index.md
index 973d42c8c..01cc4b9c5 100644
--- a/docs/de/documentation/faq/index.md
+++ b/docs/de/documentation/faq/index.md
@@ -1,105 +1,172 @@
-# Häufige Fragen (FAQ)
+# Häufige Fragen (FAQ) {#faq}
+
-### Artikel
+Diese Seite ist in zwei Bereiche unterteilt: [allgemeine **FAQ**](#faq) mit häufigen Fragen zur Nutzung, und [**Plugin- und Theme-Inkompatibilitäten**](#plugin-incompatibilities) mit bekannten Problemen durch Drittanbieter-Software.
- * [ Wie bekomme ich den Buchungskommentar auf die Seite und in die Email? ](booking-comment-emails)
- * [ Wie kann ich die Artikeldetailseite übersichtlicher gestalten? ](organise-article-page)
- * [ Die Seite ist sehr langsam ](site-slow)
- * [ Wie füge ich Zahlenschloss-Codes in E-Mails ein? ](lock-codes-email)
+
+## Wie bekomme ich den Buchungskommentar auf die Seite und in die Email?
-## Ich möchte einzelne Nutzer vorübergehend sperren
+::: details Antwort anzeigen
+In den Einstellungen kannst du die Buchungskommentare aktivieren. In den E-Mail-Vorlagen musst du dann folgenden Code einfügen: `{{booking:returnComment}}`
+:::
+
+## Wie kann ich die Artikeldetailseite übersichtlicher gestalten?
+
+::: details Antwort anzeigen
+Lange Artikelseiten bedeuten, dass Menschen lange bis zum Buchungskalender scrollen müssen.
+
+Hier empfiehlt sich ein Plugin wie [Show-Hide/Collapse-Expand](https://de.wordpress.org/plugins/show-hidecollapse-expand) zu nutzen, mit dem Informationen eingeklappt werden können.
+
+
+:::
+
+## Meine Seite ist sehr langsam – was kann ich tun? {#my-site-is-very-slow-what-can-i-do}
+
+:::: details Antwort anzeigen
+Wenn deine CommonsBooking Seite sehr langsam ist, kann das verschiedene Gründe haben.
+Wir nutzen eine Technologie namens Caching, mit der wir häufig gestellte Anfragen in einem Zwischenspeicher
+zurückhalten, um Serverkapazitäten einzusparen.
+
+Das Caching kann unter Umständen nicht funktionieren, wenn:
+
+ * [WP_DEBUG](https://developer.wordpress.org/advanced-administration/debug/debug-wordpress/) aktiviert ist, dafür musst du deine wp-config.php bearbeiten
+ * Der Ordner /tmp/ auf deinem Server nicht beschreibbar ist. Falls dies der Fall sein sollte, dann kontaktiere bitte deinen Webhoster und bitte ihn darum den Ordner beschreibbar zu machen.
+ * Falls das nicht möglich sein sollte, kannst du in den CommonsBooking Einstellungen unter "Erweiterte Optionen" den Pfad für den Dateisystem-Cache einstellen. Bitte frage bei deinem Webhoster nach, welche Ordner auf dem Server für temporäre Dateien verfügbar sind.
+ * Falls das auch nicht möglich sein sollte: Gehe zu deiner [Webseiten-Info](https://wordpress.org/documentation/article/site-health-screen/) unter (http://DEINE-URL/wp-admin/site-health.php?tab=debug).
+ Dort findest du unter **Verzeichnisse** den Pfad deines WordPress-Verzeichnisses. Wähle alternativ einen Ordner im Format `DEIN_VERZEICHNIS/symfony` als Cache-Ziel.
+ :::danger Achtung
+ Dies kann dazu führen, dass dein WordPress-Verzeichnis sehr groß wird.
+ :::
+
+Alternativ kannst du auch [REDIS](https://redis.io) auf deinem Server installieren und den Cache durch REDIS verwalten lassen.
+Da REDIS den Cache im RAM speichert, statt im Dateisystem, ist das meistens etwas schneller.
+::::
-Ihr möchtet Nutzende vorübergehend für einen bestimmten Zeitraum sperren, weil diese z.B. die Ausleihe übermäßig nutzen oder gegen Ausleihbedingungen verstoßen?
+## Wie kann ich Konfigurationsprobleme debuggen?
-Ich würde euch dazu raten mit einem entsprechenden Plugin zum Blockieren von Nutzenden für einen bestimmten Zeitraum einfach den gesamten Login zu blockieren. Da gibt es auch schon einige Plugins dafür. Wenn eure Nutzenden sowieso nichts anderes auf der Seite machen können als Räder zu buchen erscheint mir das auch sinnvoll es auf diese Art und Weise zu machen.
+:::: details Antwort anzeigen
+:::warning Technische Expertise nötig!
+:::
+
+Mit dem Plugin `query-monitor` kannst du Anfragen deiner Seite live überwachen. Damit ist es z.B. möglich einen
+falsch konfigurierten Cache schnell zu identifizieren.
+::::
+
+## Wie füge ich Zahlenschloss-Codes in E-Mails ein?
-Ich habe nur kurz bei mir mal das Plugin https://wordpress.org/plugins/user-blocker/ ausprobiert und das hat problemlos funktioniert, auch mit einer Zeitschaltfunktion. Das ist aber nicht das einzige Plugin, was diese Funktion hat.
+::: details Antwort anzeigen
+Eine häufige Frage ist, ob Codes für Zahlenschlösser an Artikeln oder Standorten hinzugefügt werden können, um sie in den versendeten E-Mails anzuzeigen.
-Das Blockieren bestimmter Benutzergruppen ist kein Feature in CommonsBooking und wird wahrscheinlich auch so bald nicht als Feature kommen, da es schon einige Plugins gibt die genau das erledigen.
+Das funktioniert über sog. Meta-Felder, welche Artikel und Standort zugewiesen werden. Diese Felder können dann auch in den E-Mail-Templates verwendet werden.
-## Überbuchbare Tage erlauben / Buchung übers Wochenende
+Der Artikel [Template-Tags](../administration/template-tags#eigene-metafelder-fur-standorte-und-artikel-verwenden) der Dokumentation enthält eine ausführliche Anleitung.
+:::
-Wenn ihr euren Nutzenden ermöglichen wollt, den Artikel z.B. über das Wochenende zu buchen, könnt ihr diese Einstellung in den Standort-Einstellungen vornehmen.
+## Wie sperre ich einzelne Nutzer vorübergehend?
-Infos dazu unter: https://commonsbooking.org/documentation/first-steps/create-location/
+::: details Antwort anzeigen
+Möchtest du Nutzende vorübergehend für einen bestimmten Zeitraum sperren, weil diese z.B. die Ausleihe üdermäßig nutzen oder gegen Ausleihbedingungen verstoßen?
-## Anzahl der Artikel in der cb_items Liste erhöhen
+Wir empfehlen, ein dediziertes WordPress-Plugin zum Blockieren von Nutzenden zu verwenden. Wenn deine Nutzenden auf der Seite sowieso nichts anderes tun können als buchen, ist das auch die einfachste Lösung.
-Die Anzahl der Artikel pro Seite wird über die globalen Einstellungen von Wordpress übernommen.
+Das Plugin [User Blocker](https://wordpress.org/plugins/user-blocker/) wurde getestet und funktioniert problemlos, auch mit einer Zeitschaltfunktion. Es gibt weitere Plugins mit dieser Funktionalität.
-Diese globale Einstellungen können hier geändert werden:
+Das Blockieren bestimmter Benutzergruppen ist kein eingebautes Feature von CommonsBooking und wird wahrscheinlich auch in naher Zukunft nicht kommen, da es bereits WordPress-Plugins gibt, die genau das abdecken.
+:::
-Als Wordpress Administrator einlogge:
+## Wie erlaube ich Buchungen über geschlossene Tage hinweg?
-Einstellungen -> Lesen -> Blogseiten zeigen maximal
+::: details Antwort anzeigen
+Wenn du deinen Nutzenden ermöglichen möchtest, einen Artikel über geschlossene Tage hinweg zu buchen (z.B. über das Wochenende, wenn die Station geschlossen ist), kannst du das in den Standort-Einstellungen konfigurieren.
-## Fehlerhafte Anzeige des Kalender-Widget im Admin-Bereich
+Ausführliche Informationen findest du unter [Standorte anlegen](../first-steps/create-location).
+:::
+
+## Wie erhöhe ich die Anzahl der Artikel in der cb_items-Liste?
+
+::: details Antwort anzeigen
+Die Anzahl der Artikel pro Seite wird über die globalen WordPress-Leseeinstellungen übernommen.
+
+So änderst du sie:
+
+1. Als WordPress-Administrator einloggen
+2. **Einstellungen -> Lesen** aufrufen
+3. Den Wert bei **Blogseiten zeigen maximal** anpassen
+:::
-Treten Probleme bei der Anzeige des Kalenders im Admin-Bereich der Buchungen
-auf (sog. Admin-Backend), siehe das folgende Bild rechts unten, kann eine
-mögliche Lösung sein, das [ Plugin "Lightstart" (wp-maintenance-mode) ](https://wordpress.org/plugins/wp-maintenance-mode) zu deaktivieren oder zu
-entfernen und neu zu installieren. Das Problem ist eine Inkompatibilität von
-Lightstart mit CommonsBooking und kein Fehler im Code von CommonsBooking. Das
-Problem tritt nicht mehr auf, wenn eine Neuinstallation von Lightstart
-vorgenommen wurde. Mehr dazu auf [ Github im CommonsBooking Quellcode-Repository ](https://github.com/wielebenwir/commonsbooking/issues/1646).
+## Wie verhindere ich Spam-Registrierungen?
+
+::: details Antwort anzeigen
+Dafür gibt es verschiedene Möglichkeiten (Vorschläge aus der Community):
+
+* Ein HoneyPot lenkt Bots ab, ohne Menschen zu nerven: [Honeypot Plugin](https://wordpress.org/plugins/honeypot/)
+
+* "Ich hatte mal für **UltimateMember ein winziges Plugin geschrieben mit dem man einfach einen Text eingeben muss um sich zu registrieren**. Barrierearm und hält alle Bots ab: [Download von Github](https://github.com/hansmorb/um-captchaquiz/raw/refs/heads/master/um-captchaquiz.zip). Dazu einfach eine Textbox erstellen und den Metaschlüssel in den Plugin Einstellungen eintragen."
+
+* "Wir nutzen Hcaptcha für wordpress. Nach der Installation in den Einstellungen des Plugins die von euch genutzte Registrierung (z.B. UltimateMember) auswählen. Für die Nutzung muss ein Hcaptcha Account erstellt werden. Sie werben mit Privacy-First und das keine Nutzerdaten verkauft werden. Ich selber habe das nicht geprüft." - [Download aus dem Plugin-Verzeichnis](https://wordpress.org/plugins/hcaptcha-for-forms-and-more)
+:::
+
+# Plugin- und Theme-Inkompatibilitäten {#plugin-incompatibilities}
+
+## Plugin: Lightstart
+
+::: details Antwort anzeigen
+Treten Probleme bei der Anzeige des Kalenders im Admin-Bereich der Buchungen auf (sog. Admin-Backend), kann eine mögliche Lösung sein, das [Plugin "Lightstart" (wp-maintenance-mode)](https://wordpress.org/plugins/wp-maintenance-mode) zu deaktivieren oder zu entfernen und neu zu installieren. Das Problem ist eine Inkompatibilität von Lightstart mit CommonsBooking und kein Fehler im Code von CommonsBooking. Das Problem tritt nicht mehr auf, wenn eine Neuinstallation von Lightstart vorgenommen wurde. Mehr dazu auf [Github im CommonsBooking Quellcode-Repository](https://github.com/wielebenwir/commonsbooking/issues/1646).

+:::
-## Inkompatibles Plugin All-in-one-Event Plugins
+## Theme: GridBulletin
+
+::: details Antwort anzeigen
+In der letzten Version von [GridBulletin](https://wordpress.org/themes/gridbulletin) kommt es zu einer Inkompatibilität mit CommonsBooking. Probleme tauchen auf, wenn der Footer aktiviert ist. Konkrete Probleme sind z.B. das Fehlen des Buchungs-Kalenders auf der Artikelseite. Aus technischer Sicht liegt es daran, dass die nötigen Javascript-Quellen von CommonsBooking nicht ausgeliefert werden. Der Grund innerhalb des GridBulletin Themes oder eine Lösung konnte bisher nicht gefunden werden.
+:::
+## Plugin: All-in-one-Event
+
+:::: details Antwort anzeigen
:::info Behoben seit 2.7.2 (06.2022)
Für Experten siehe [Issue 675](https://github.com/wielebenwir/commonsbooking/issues/675)
:::
-Leider kommt es bei der gleichzeitigen Nutzung des Plugins "All-in-one-Event" zu Fehlern, sodass Seiten, die von CommonsBooking erzeugt werden nicht angezeigt werden.
+Leider kommt es bei der gleichzeitigen Nutzung des Plugins "All-in-one-Event" zu Fehlern, sodass Seiten, die von CommonsBooking erzeugt werden, nicht angezeigt werden.
Die Ursache ist leider auf eine schlechte Programmierung des All-in-one-Event-Plugins zurückzuführen, welches sich nicht an die Wordpress-Standards hält und so tief in Wordpress eingreift, dass es die Programmlogik von CommonsBooking quasi überschreibt.
Wir haben einiges versucht, um eine parallele Nutzung zu ermöglichen, leider aber bisher keine Lösung gefunden.
Wenn ihr das Problem auch habt, schreibt gerne direkt an den Support des Plugins, vielleicht passen sie ihr Plugin doch noch irgendwann an.
+::::
-## Inkompatibles Plugin REDIS Object Cache
-
-Im Zusammenhang mit dem [Cache](../advanced-functionality/) gab
-es in der Vergangenheit bereits Probleme mit anderen Wordpress-Plugins wie
-z.B. 'REDIS Object Cache'. Aus diesem Grund raten wir von der Nutzung solcher
-Plugins ab.
+## Plugin: REDIS Object Cache
-Deshalb sollten die von CommonsBooking generierten Seiten von der Optimierung durch Dritt-Plugins ausgenommen werden.
-CommonsBooking verwendet ein eigenes Caching.
+::: details Antwort anzeigen
+Im Zusammenhang mit dem [Cache](../advanced-functionality/) gab es in der Vergangenheit bereits Probleme mit anderen Wordpress-Plugins wie z.B. 'REDIS Object Cache'. Aus diesem Grund raten wir von der Nutzung solcher Plugins ab.
+Die von CommonsBooking generierten Seiten sollten von der Optimierung durch Dritt-Plugins ausgenommen werden. CommonsBooking verwendet ein eigenes Caching.
+:::
-## Inkompatibles Plugin Ultimate Member
+## Plugin: Ultimate Member
-Wenn ihr das Plugin Ultimate Member benutzt und die Benutzerrolle "CommonsBooking Manager" nutzen möchtet, müsst ihr in Ultimate Member für die Rolle cb_manager noch ein Häkchen setzen, um diese für den AP-Admin-Zugang zu aktivieren.
+::: details Antwort anzeigen
+Wenn ihr das Plugin Ultimate Member benutzt und die Benutzerrolle "CommonsBooking Manager" nutzen möchtet, müsst ihr in Ultimate Member für die Rolle `cb_manager` noch ein Häkchen setzen, um diese für den Admin-Zugang zu aktivieren.
+:::
-## Inkompatible Plugins: Autoptimize / Caching
+## Plugin: Autoptimize / Caching-Plugins
-Optimierungs-Plugins oder weitere Caching-Plugins können dazu führen, dass CommonsBooking nicht alle Seiten anzeigen kann.
+:::: details Antwort anzeigen
+Optimierungs-Plugins oder weitere Caching-Plugins können dazu führen, dass CommonsBooking nicht alle Seiten korrekt anzeigen kann.
-Nicht vollständige Liste:
+Betroffene Plugins (nicht vollständig):
* Autoptimize
-Deshalb sollten die von CommonsBooking generierten Seiten von der Optimierung durch Dritt-Plugins ausgenommen werden.
-CommonsBooking verwendet ein eigenes Caching.
+Die von CommonsBooking generierten Seiten sollten von der Optimierung durch Dritt-Plugins ausgenommen werden. CommonsBooking verwendet ein eigenes Caching.
-::: info Hast du ein Problem festgestellt?
+:::info Hast du ein Problem festgestellt?
Dann trage die Inkompatibilität hier ein!
:::
-
-
-## Inkompatibles Theme Gridbulletin
-
-In der letzten Version von [ GridBulletin ](https://wordpress.org/themes/gridbulletin) kommt es zu einer
-Inkompatibilität mit CommonsBooking. Probleme tauchen auf, wenn der Footer
-aktiviert ist. Konkrete Probleme sind z.B. das Fehlen des Buchungs-Kalenders
-auf der Artikelseite. Aus technischer Sicht liegt es daran, dass die nötigen
-Javascript-Quellen von CommonsBooking nicht ausgeliefert werden. Der Grund
-innerhalb des GridBulletin Themes oder eine Lösung konnte bisher nicht
-gefunden werden.
-
-
-
+::::
diff --git a/docs/de/documentation/faq/lock-codes-email.md b/docs/de/documentation/faq/lock-codes-email.md
deleted file mode 100644
index 1d000040d..000000000
--- a/docs/de/documentation/faq/lock-codes-email.md
+++ /dev/null
@@ -1,11 +0,0 @@
-# Wie füge ich Zahlenschloss-Codes in E-Mails ein?
-
-Eine häufige Frage ist, ob Codes für Zahlenschlösser an Artikeln oder
-Standorten hinzugefügt werden können um Sie in den versendeten E-Mails
-anzuzeigen.
-
-Das funktioniert über sog. Meta-Felder, welche Artikel und Standort zugewiesen
-werden. Diese Felder können dann auch in den E-Mail Templates verwendet
-werden.
-
-Der Aritkel [Template-Tags](../administration/template-tags#eigene-metafelder-fur-standorte-und-artikel-verwenden) der Dokumentation enthält eine Ausführliche Anleitung.
diff --git a/docs/de/documentation/faq/organise-article-page.md b/docs/de/documentation/faq/organise-article-page.md
deleted file mode 100644
index cbce05f12..000000000
--- a/docs/de/documentation/faq/organise-article-page.md
+++ /dev/null
@@ -1,13 +0,0 @@
-# Wie kann ich die Artikeldetailseite übersichtlicher gestalten?
-
-## Informationen erst auf Klick anzeigen
-
-Lange Artikelseiten bedeuten, dass Menschen lange bis zum Buchungskalender
-scrollen müssen.
-
-Hier empfiehlt sich ein Plugin wie [ Show-Hide/Collapse-Expand
-](https://de.wordpress.org/plugins/show-hidecollapse-expand) zu nutzen, mit
-dem Informationen eingeklappt werden können.
-
-
-
diff --git a/docs/de/documentation/faq/problems-and-answers.md b/docs/de/documentation/faq/problems-and-answers.md
deleted file mode 100644
index aebab7705..000000000
--- a/docs/de/documentation/faq/problems-and-answers.md
+++ /dev/null
@@ -1,27 +0,0 @@
-# Probleme und Antworten
-
-### Anzeige Kalender-Widget im Admin-Bereich
-
-Treten Probleme bei der Anzeige des Kalenders im Admin-Bereich der Buchungen
-auf (sog. Admin-Backend), siehe das folgende Bild rechts unten, kann eine
-mögliche Lösung sein, das [ Plugin "Lightstart" (wp-maintenance-mode)
-](https://wordpress.org/plugins/wp-maintenance-mode) zu deaktivieren oder zu
-entfernen und neu zu installieren. Das Problem ist eine Inkompatibilität von
-Lightstart mit CommonsBooking und kein Fehler im Code von CommonsBooking. Das
-Problem tritt nicht mehr auf, wenn eine Neuinstallation von Lightstart
-vorgenommen wurde. Mehr dazu auf [ Github im CommonsBooking Quellcode-
-Repository ](https://github.com/wielebenwir/commonsbooking/issues/1646) .
-
-
-
-### Inkompatibles Theme Gridbulletin
-
-In der letzten Version von [ GridBulletin
-](https://wordpress.org/themes/gridbulletin) kommt es zu einer
-Inkompatibilität mit CommonsBooking. Probleme tauchen auf, wenn der Footer
-aktiviert ist. Konkrete Probleme sind z.B. das Fehlen des Buchungs-Kalenders
-auf der Artikelseite. Aus technischer Sicht liegt es daran, dass die nötigen
-Javascript-Quellen von CommonsBooking nicht ausgeliefert werden. Der Grund
-innerhalb des GridBulletin Themes oder eine Lösung konnte bisher nicht
-gefunden werden.
-
diff --git a/docs/de/documentation/faq/site-slow.md b/docs/de/documentation/faq/site-slow.md
deleted file mode 100644
index 582b2f284..000000000
--- a/docs/de/documentation/faq/site-slow.md
+++ /dev/null
@@ -1,28 +0,0 @@
-# Die Seite ist sehr langsam
-
-Wenn deine CommonsBooking Seite sehr langsam ist, kann das verschiedene Gründe haben.
-Wir nutzen eine Technologie namens Caching, mit der wir häufig gestellte Anfragen in einem Zwischenspeicher
-zurückhalten, um Serverkapazitäten einzusparen.
-
-Das Caching kann unter Umständen nicht funktionieren, wenn:
-
- * [WP_DEBUG](https://developer.wordpress.org/advanced-administration/debug/debug-wordpress/) aktiviert ist, dafür musst du deine wp-config.php bearbeiten
- * Der Ordner /tmp/ auf deinem Server nicht beschreibbar ist. Falls dies der Fall sein sollte, dann kontaktiere bitte deinen Webhoster und bitte ihn darum den Ordner beschreibbar zu machen.
- * Falls das nicht möglich sein sollte, kannst du in den CommonsBooking Einstellungen unter “Erweiterte Optionen” den Pfad für den Dateisystem-Cache einstellen. Bitte frage bei deinem Webhoster nach, welche Ordner auf dem Server für temporäre Dateien verfügbar sind.
- * Falls das auch nicht möglich sein sollte: Gehe zu deiner [Webseiten-Info](https://wordpress.org/documentation/article/site-health-screen/) unter (http://DEINE-URL/wp-admin/site-health.php?tab=debug).
- Dort findest du unter **Verzeichnisse** den Pfad deines WordPress-Verzeichnisses. Wähle alternativ einen Ordner im Format `DEIN_VERZEICHNIS/symfony` als Cache-Ziel.
- ::: danger Achtung
- Dies kann dazu führen, dass dein WordPress-Verzeichnis sehr groß wird.
- :::
-
-Alternativ kannst du auch [REDIS](https://redis.io) auf deinem Server installieren und den Cache durch REDIS verwalten lassen.
-Da REDIS den Cache im RAM speichert, statt im Dateisystem, ist das meistens etwas schneller.
-
-## Troubleshooting
-
-::: warning Technische Expertise nötig!
-:::
-
-Mit dem Plugin `query-monitor` kannst du Anfragen deiner Seite live überwachen. Damit es es z.B. möglich ein
-falsch konfigurierten Cache schnell zu identifizieren.
-
diff --git a/docs/de/documentation/setup/migration-from-cb1.md b/docs/de/documentation/setup/migration-from-cb1.md
index b35fc55b9..6610466cc 100644
--- a/docs/de/documentation/setup/migration-from-cb1.md
+++ b/docs/de/documentation/setup/migration-from-cb1.md
@@ -1,5 +1,9 @@
# Migration von Version 0.9.x
+:::danger ACHTUNG
+Mit CommonsBooking 2.12 (Veröffentlichung Anfang 2027) wird die Migration von CommonsBooking 0.9.X auf die neuste Version eingestellt.
+Alle Nutzenden von CommonsBooking 0.9.X sind dazu angehalten **zeitnah zu migrieren**, bei Fragen unterstützen wir gerne und helfen euch auch gerne bei der Migration.
+:::
Die Migration von Version CB 0.9.x zu CB 2.x.x könnt ihr per Knopfdruck
erledigen. Die Migration importiert folgende Daten:
diff --git a/docs/en/documentation/advanced-functionality/cache.md b/docs/en/documentation/advanced-functionality/cache.md
index 03fa863d0..a6dc8563d 100644
--- a/docs/en/documentation/advanced-functionality/cache.md
+++ b/docs/en/documentation/advanced-functionality/cache.md
@@ -36,7 +36,7 @@ A misconfigured cache can slow down your site!
working properly. In that case, try a different path for the cache or a different cache adapter.
* If your site is very slow, this may also indicate a problem with the cache.
- More about this: [The site is very slow](../faq/site-slow).
+ More about this: [The site is very slow](../faq/index#my-site-is-very-slow-what-can-i-do).
* **Periodical cache warmup through cron job**:
:::warning
diff --git a/docs/en/documentation/advanced-functionality/hooks-and-filters.md b/docs/en/documentation/advanced-functionality/hooks-and-filters.md
index fa83c1623..3a44386da 100644
--- a/docs/en/documentation/advanced-functionality/hooks-and-filters.md
+++ b/docs/en/documentation/advanced-functionality/hooks-and-filters.md
@@ -43,6 +43,10 @@ add_action( 'commonsbooking_before_item-single', 'itemsingle_callback' );
* commonsbooking_before_item-single
* commonsbooking_after_item-single
* commonsbooking_mail_sent
+ * [commonsbooking_booking_created](#booking-lifecycle-hooks-since-2-11-0)
+ * [commonsbooking_booking_confirmed](#booking-lifecycle-hooks-since-2-11-0)
+ * [commonsbooking_booking_cancelled](#booking-lifecycle-hooks-since-2-11-0)
+ * [commonsbooking_booking_status_changed](#booking-lifecycle-hooks-since-2-11-0)
### Hooks in the context of an object (since 2.10.8)
@@ -68,6 +72,75 @@ function my_cb_before_booking_single( $booking_id, $booking ) {
add_action( 'commonsbooking_before_booking-single', 'my_cb_before_booking_single', 10, 2 );
```
+### Booking lifecycle hooks (since 2.11.0)
+
+These action hooks let you react to the lifecycle of a booking — for example to
+program a smart lock, sync an external calendar, or write an audit log. They fire
+regardless of where the change originated (frontend calendar, admin backend, REST
+API or WP-CLI), because they are dispatched from WordPress' post status
+transition rather than from a single form handler.
+
+Every hook receives the booking post ID and the corresponding
+`\CommonsBooking\Model\Booking` instance (except `..._status_changed`, which also
+passes the old and new status).
+
+ * `commonsbooking_booking_created`
+ * Fires **once** when a booking first enters a real status.
+ * Parameters: `int $booking_id`, `\CommonsBooking\Model\Booking $booking`
+ * `commonsbooking_booking_confirmed`
+ * Fires whenever a booking becomes `confirmed`. This is the recommended hook for integrations that need to react to a booking becoming active.
+ * Parameters: `int $booking_id`, `\CommonsBooking\Model\Booking $booking`
+ * `commonsbooking_booking_cancelled`
+ * Fires when a booking is cancelled.
+ * Parameters: `int $booking_id`, `\CommonsBooking\Model\Booking $booking`
+ * `commonsbooking_booking_status_changed`
+ * Fires on every booking status transition (audit superset of the hooks above). Note: because cancellation writes the status directly to the database, cancellations are best observed via `commonsbooking_booking_cancelled` rather than this hook.
+ * Parameters: `int $booking_id`, `string $old_status`, `string $new_status`, `\CommonsBooking\Model\Booking $booking`
+
+#### Example: trigger a smart lock when a booking is confirmed
+
+```php
+add_action( 'commonsbooking_booking_confirmed', function ( $booking_id, $booking ) {
+ $item = $booking->getItem();
+ $lockCode = $booking->formattedBookingCode();
+ // hand the code / booking window over to your locking system here …
+ my_locking_system_program( $item->ID, $lockCode, $booking->getStartDate(), $booking->getEndDate() );
+}, 10, 2 );
+```
+
+#### Example: release the lock again when a booking is cancelled
+
+```php
+add_action( 'commonsbooking_booking_cancelled', function ( $booking_id, $booking ) {
+ my_locking_system_revoke( $booking->getItem()->ID, $booking_id );
+}, 10, 2 );
+```
+
+#### No-code automations via Uncanny Automator
+
+If the [Uncanny Automator](https://wordpress.org/plugins/uncanny-automator/) plugin
+is active, CommonsBooking registers an integration with two triggers:
+
+ * **A booking is confirmed** (built on `commonsbooking_booking_confirmed`)
+ * **A booking is cancelled** (built on `commonsbooking_booking_cancelled`)
+
+Both expose the same tokens — booking ID, item, location, start/end, booking
+code, user email, **user phone** and booking URL — so you can route booking
+events to any gateway Automator supports (SMS via Twilio, Slack, Telegram,
+Google Calendar, Mailchimp, a webhook, …) without writing code. The integration
+loads only when Automator is active and adds no overhead otherwise.
+
+**Example — SMS an access code when a booking is confirmed:**
+*Trigger:* CommonsBooking → *A booking is confirmed*.
+*Action:* Twilio → Send SMS, with **To** = `{{CB_BOOKING_USER_PHONE}}` and
+**Body** = "Your booking of {{CB_ITEM_NAME}} at {{CB_LOCATION_NAME}} is confirmed.
+Access code: {{CB_BOOKING_CODE}}".
+
+**Example — alert the team on Slack/SMS when a booking is cancelled:**
+*Trigger:* CommonsBooking → *A booking is cancelled*.
+*Action:* Slack (or Twilio SMS to the team number) — "{{CB_ITEM_NAME}} at
+{{CB_LOCATION_NAME}} just freed up for {{CB_BOOKING_START}}–{{CB_BOOKING_END}}".
+
## Filter hooks
Filter hooks (https://developer.wordpress.org/plugins/hooks/filters) work
@@ -77,7 +150,7 @@ receives a value, modifies it, and then returns it.
### Overview of all filter hooks
* commonsbooking_custom_metadata
- * [commonsbooking_isCurrentUserAdmin](../basics/permission-management#filterhook-isCurrentUserAdmin)
+ * [commonsbooking_isUserAdmin](../basics/permission-management#filterhook-isUserAdmin)
* commonsbooking_isCurrentUserSubscriber
* commonsbooking_get_template_part
* commonsbooking_template_tag
@@ -88,6 +161,15 @@ receives a value, modifies it, and then returns it.
* commonsbooking_mail_body
* commonsbooking_mail_attachment
* commonsbooking_disableCache
+ * commonsbooking_gbfs_feeds
+ * commonsbooking_can_cancel_booking
+ * commonsbooking_booking_before_save
+ * commonsbooking_is_timeframe_bookable
+ * commonsbooking_day_availability
+ * commonsbooking_bookable_timeframes
+ * commonsbooking_calendar_data
+ * commonsbooking_api_item_response
+ * commonsbooking_api_availability_response
There are also filter hooks that allow you to add additional user roles
akin to the CB Manager that can manage items and locations.
@@ -156,6 +238,133 @@ add_filter('commonsbooking_tag_cb_item_yourFunction', function( $value, $obj) {
}, 10, 2);
```
+### Filter `commonsbooking_api_availability_response`
+
+::: tip Since version 2.11.0
+:::
+
+Adjusts the availability slots exposed through the CommonsAPI, e.g. to reflect an
+external booking source. Receives the slots array and the item ID (or `null` for
+all items).
+
+```php
+add_filter('commonsbooking_api_availability_response', function (array $slots, $id): array {
+ // adjust $slots as needed
+ return $slots;
+}, 10, 2);
+```
+
+### Filter `commonsbooking_api_item_response`
+
+::: tip Since version 2.11.0
+:::
+
+Adds or adjusts fields exposed for an item in the CommonsAPI. Receives the
+prepared item object and the source `WP_Post`. Added fields must conform to the
+CommonsAPI JSON schema, otherwise response validation will reject them.
+
+```php
+add_filter('commonsbooking_api_item_response', function ($preparedItem, $item) {
+ $preparedItem->myField = get_post_meta($item->ID, 'my_field', true);
+ return $preparedItem;
+}, 10, 2);
+```
+
+### Filter `commonsbooking_calendar_data`
+
+::: tip Since version 2.11.0
+:::
+
+Adjusts the data array that drives the frontend booking calendar (and its AJAX
+endpoint) before it is rendered or returned as JSON. Receives the calendar data
+and the item and location it is for.
+
+```php
+add_filter('commonsbooking_calendar_data', function (array $calendarData, $item, $location): array {
+ // adjust $calendarData as needed
+ return $calendarData;
+}, 10, 3);
+```
+
+### Filter `commonsbooking_bookable_timeframes`
+
+::: tip Since version 2.11.0
+:::
+
+Restricts or extends which timeframes are offered for booking. Receives the
+timeframes and the location/item IDs the query was scoped to. The element type
+follows the repository's `$returnAsModel` argument (post IDs, `WP_Post` or
+`\CommonsBooking\Model\Timeframe`).
+
+```php
+add_filter('commonsbooking_bookable_timeframes', function (array $timeframes, array $locations, array $items): array {
+ // filter $timeframes as needed
+ return $timeframes;
+}, 10, 3);
+```
+
+### Filter `commonsbooking_day_availability`
+
+::: tip Since version 2.11.0
+:::
+
+Adjusts the bookable slots computed for a single day, e.g. to reflect an external
+availability source. Receives the slots array and the `\CommonsBooking\Model\Day`.
+
+```php
+add_filter('commonsbooking_day_availability', function (array $slots, $day): array {
+ // inspect $day->getDate() and filter $slots as needed
+ return $slots;
+}, 10, 2);
+```
+
+### Filter `commonsbooking_is_timeframe_bookable`
+
+::: tip Since version 2.11.0
+:::
+
+Adds custom booking-window rules on top of the default advance-booking-days check.
+Receives the default decision (`bool`) and the `\CommonsBooking\Model\Timeframe`.
+
+```php
+// Block bookings on the timeframe's item while it is flagged for maintenance.
+add_filter('commonsbooking_is_timeframe_bookable', function (bool $bookable, $timeframe): bool {
+ return $bookable && ! get_post_meta($timeframe->getItem()->ID, 'in_maintenance', true);
+}, 10, 2);
+```
+
+### Filter `commonsbooking_booking_before_save`
+
+::: tip Since version 2.11.0
+:::
+
+Adjusts the post array right before a booking is inserted or updated, e.g. to add
+meta data. Receives the `wp_insert_post()`/`wp_update_post()` array and the
+existing `\CommonsBooking\Model\Booking` (or `null` for a new booking); return the
+modified array.
+
+```php
+add_filter('commonsbooking_booking_before_save', function (array $postarr, $booking): array {
+ $postarr['meta_input']['my_external_ref'] = 'ext-123';
+ return $postarr;
+}, 10, 2);
+```
+
+### Filter `commonsbooking_can_cancel_booking`
+
+::: tip Since version 2.11.0
+:::
+
+Overrides whether the current user may cancel a booking. Receives the default
+decision (`bool`) and the `\CommonsBooking\Model\Booking` instance.
+
+```php
+// Forbid cancelling a booking that starts within the next 24 hours.
+add_filter('commonsbooking_can_cancel_booking', function (bool $canCancel, $booking): bool {
+ return $canCancel && $booking->getStartDate() > time() + DAY_IN_SECONDS;
+}, 10, 2);
+```
+
### Filter `commonsbooking_mobile_calendar_month_count`
::: tip Since version 2.10.5
diff --git a/docs/en/documentation/api/commonsbooking-api.md b/docs/en/documentation/api/commonsbooking-api.md
index 5eb756105..51420d144 100644
--- a/docs/en/documentation/api/commonsbooking-api.md
+++ b/docs/en/documentation/api/commonsbooking-api.md
@@ -26,6 +26,10 @@ You can access the API via _Settings_ -> _CommonsBooking_ -> Tab: _API / Export_
* Push URL: Here you can enter the URL of the receiving system. CommonsBooking will call this URL whenever the data changes, so that the remote system is informed about a data change and can retrieve it via a separate API call. This enables real-time data exchange.
* API Key: Here you can enter a custom API key. If the key is set, the requesting system must provide the parameter apikey= _[key]_ in every query.
+## Excluding items from the API
+
+If you do not want certain items to appear in the API, they must be explicitly excluded. These settings can be configured at item level; if the relevant checkbox is ticked, these items will not appear in the API routes.
+
## API route specification
**Availability**
diff --git a/docs/en/documentation/api/gbfs.md b/docs/en/documentation/api/gbfs.md
index e34ae5aea..22ce62af2 100644
--- a/docs/en/documentation/api/gbfs.md
+++ b/docs/en/documentation/api/gbfs.md
@@ -1,17 +1,21 @@
# GBFS
-::: danger API bug
-A bug in the API is documented in [Issue 1388](https://github.com/wielebenwir/commonsbooking/issues/1388). The temporal availability may therefore not be correct.
-:::
-
-Since 2.5
-
This interface provides data of [locations](../first-steps/create-location),
[items](../first-steps/create-item) and their availability via
[timeframes](../first-steps/booking-timeframes-manage) in a standardized schema.
-The following endpoints of the _General Bikeshare Feed Specification_ ([GBFS](https://www.gbfs.org/documentation/)) schema are supported:
+Currently, version 3.1-RC3 of the _General Bikeshare Feed Specification_ ([GBFS](https://www.gbfs.org/documentation/)) is supported with the following routes:
-* station_status
-* station_information
-* system_information
+* station_status.json
+* station_information.json
+* system_information.json
+* vehicle_status.json
+* vehicle_availability.json
+* vehicle_types.json
* gbfs.json (Discovery)
+
+## Regarding vehicle_types.json
+
+Since CommonsBooking is primarily used to rent out cargo bikes, the API returns `cargo_bicycle` as the default value for `form_factor`. As `propulsion_type` is a required field, the default value here is set to `human`. If this were not the case, much more additional information about the propulsion method would need to be provided.
+
+::: info Fixed Issue
+Since June 2026 (version 2.11) the [issue regarding a wrong display of availability](https://github.com/wielebenwir/commonsbooking/issues/1388) is fixed.
diff --git a/docs/en/documentation/basics/permission-management.md b/docs/en/documentation/basics/permission-management.md
index b45b586d8..888a95ad5 100644
--- a/docs/en/documentation/basics/permission-management.md
+++ b/docs/en/documentation/basics/permission-management.md
@@ -47,7 +47,7 @@ In addition, CommonsBooking Managers have no special permissions regarding other
* Change the site design
* etc.
-## Make a CommonsBooking Manager a manager for all items / locations {#filterhook-isCurrentUserAdmin}
+## Make a CommonsBooking Manager a manager for all items / locations {#filterhook-isUserAdmin}
With a [filter hook](../advanced-functionality/hooks-and-filters) you can set a specific role so that it automatically becomes a manager for all items / locations.
The example below does this for the role 'cb_manager', i.e. it configures the CB Manager to be automatically assigned to all items and locations in the instance.
@@ -55,7 +55,7 @@ If this should happen with a different role, that role must also be added to the
```php
add_filter(
- 'commonsbooking_isCurrentUserAdmin',
+ 'commonsbooking_isUserAdmin',
function ( bool $isAdmin, WP_User $user ) {
return in_array( 'cb_manager', $user->roles, true ) ? true : $isAdmin;
},
diff --git a/docs/en/documentation/faq/avoid-spam-registrations.md b/docs/en/documentation/faq/avoid-spam-registrations.md
deleted file mode 100644
index 8c0efe268..000000000
--- a/docs/en/documentation/faq/avoid-spam-registrations.md
+++ /dev/null
@@ -1,9 +0,0 @@
-# How do I prevent spam registrations
-
-There are several ways to do this (suggestions from the community):
-
-* A honeypot diverts bots without bothering people: https://wordpress.org/plugins/honeypot/
-
-* "I once wrote a tiny plugin for **UltimateMember where you simply have to enter a text to register**. Accessible and it keeps all bots out: [Download from GitHub](https://github.com/hansmorb/um-captchaquiz/raw/refs/heads/master/um-captchaquiz.zip). Just create a text box and enter the meta key in the plugin settings."
-
-* "We use hCaptcha for WordPress. After installation, select the registration you use in the plugin settings (e.g., UltimateMember; by default the built-in WordPress registration should be selected). To use it, you need to create an hCaptcha account. They advertise Privacy-First and that no user data is sold. I have not checked this myself." — [Download from the plugin directory](https://wordpress.org/plugins/hcaptcha-for-forms-and-more)
diff --git a/docs/en/documentation/faq/booking-comment-emails.md b/docs/en/documentation/faq/booking-comment-emails.md
deleted file mode 100644
index 4598c49f1..000000000
--- a/docs/en/documentation/faq/booking-comment-emails.md
+++ /dev/null
@@ -1,3 +0,0 @@
-# How do I show the booking comment on the page and in the email?
-
-In the settings you can enable booking comments. In the email templates you then need to insert the following code:
`{{booking:returnComment}}`
diff --git a/docs/en/documentation/faq/index.md b/docs/en/documentation/faq/index.md
index 958700318..04d4f5557 100644
--- a/docs/en/documentation/faq/index.md
+++ b/docs/en/documentation/faq/index.md
@@ -1,46 +1,121 @@
-# Frequently Asked Questions (FAQ)
+# Frequently Asked Questions (FAQ) {#faq}
-### Pages
+
- * [How do I show the booking comment on the page and in the email?](booking-comment-emails)
- * [How can I make the item detail page clearer?](organise-article-page)
- * [The site is very slow](site-slow)
- * [How do I show lock codes in emails?](lock-codes-email)
+This page is split into two sections: [general **FAQ**](#faq) with common how-to questions, and [**Plugin & Theme Incompatibilities**](#plugin-incompatibilities) listing known issues with third-party software.
-## I want to temporarily block individual users
+
+## How do I show the booking comment on the page and in the email?
+
+::: details Expand for answer
+In the settings you can enable booking comments. In the email templates you then need to insert the following code:
`{{booking:returnComment}}`
+:::
+
+## How can I make the item detail page clearer?
+
+::: details Expand for answer
+Long item pages mean users have to scroll a long way to reach the booking calendar.
+
+A plugin such as [Show-Hide/Collapse-Expand](https://de.wordpress.org/plugins/show-hidecollapse-expand) can be used to collapse information.
+
+
+:::
+
+## My site is very slow, what can I do?
+
+::: details Expand for answer
+If your CommonsBooking site is very slow, there may be several underlying causes.
+We use a technology called caching, which keeps frequently requested data
+in temporary storage to save server resources.
+Caching may not work under certain conditions, for example when:
+
+ * [WP_DEBUG](https://wordpress.org/documentation/article/debugging-in-wordpress) is enabled; in that case you need to edit your wp-config.php
+ * The /tmp/ folder on your server is not writable. If that is the case, contact your web host and ask them to make the folder writable.
+ * If that is not possible, you can set the path for the filesystem cache in the CommonsBooking settings under "Advanced Options". Ask your web host which folders on the server are available for temporary files.
+ * If that is also not possible: Go to your Site Health screen at (http://YOUR-URL/wp-admin/site-health.php?tab=debug). There you will find the path to your WordPress directory under **Directories**. Alternatively, choose a folder in the format `YOUR_DIRECTORY/symfony` as the cache target. **Warning:** This can cause your WordPress directory to grow very large.
+
+Alternatively, you can install [Redis](https://redis.io) on your server and let Redis manage the cache. Since Redis stores the cache in RAM instead of the filesystem, it is usually a bit faster.
+:::
+
+## How do I show lock codes in emails?
+
+::: details Expand for answer
+A frequent question is whether lock codes for combination locks can be added to items or locations so that they are displayed in the sent emails.
+
+This is possible via so-called meta fields, which are assigned to items and locations. These fields can then also be used in email templates.
+
+[This page of the documentation contains a detailed guide.](../administration/template-tags)
+:::
+
+## How do I temporarily block individual users?
+
+::: details Expand for answer
Do you want to temporarily block users for a certain period of time, e.g. because they use the rental service excessively or violate the rental conditions?
-I would advise you to simply block the entire login with an appropriate plugin for blocking users for a certain period of time. There are already some plugins for this. If your users can't do anything else on the site anyway other than booking bikes, it seems to make sense to do it this way.
+The recommended approach is to use a dedicated WordPress plugin for blocking users. If your users cannot do anything on the site other than making bookings, this is also the simplest solution.
-I just briefly tried the plugin https://wordpress.org/plugins/user-blocker/ and it worked without any problems, even with a timer function. But this is not the only plugin that has this function.
+The plugin [User Blocker](https://wordpress.org/plugins/user-blocker/) has been tested and works without issues, including a timer function. There are other plugins available with the same functionality.
-Blocking specific user groups is not a feature in CommonsBooking and probably won't be added as a feature anytime soon, as there are already some plugins that do exactly that.
+Blocking specific user groups is not a built-in feature of CommonsBooking and is unlikely to be added soon, as existing WordPress plugins already cover this use case well.
+:::
-## Allow overbookable days / booking over the weekend
+## How do I allow booking across closed days?
-If you want to allow your users to book the item over the weekend, for example, you can make this setting in the location settings.
+::: details Expand for answer
+If you want to allow your users to book an item across closed days (e.g. over a weekend when the station is closed), you can configure this in the location settings.
-More information at: https://commonsbooking.org/documentation/first-steps/create-location/
+For detailed instructions, see [Create Locations](../first-steps/create-location).
+:::
-## Increase the number of items in the cb_items list
+## How do I increase the number of items shown in the cb_items list?
-The number of items per page is taken from the global WordPress settings.
+::: details Expand for answer
+The number of items shown per page is taken from the global WordPress reading settings.
-These global settings can be changed here:
+To change it:
-Log in as WordPress administrator:
+1. Log in as a WordPress administrator
+2. Go to **Settings -> Reading**
+3. Change the value for **Blog pages show at most**
+:::
-Settings -> Reading -> Blog pages show at most
+## How do I prevent spam registrations?
-## Incorrect display of the calendar widget in the admin area
+::: details Expand for answer
+There are several ways to do this (suggestions from the community):
+
+* A honeypot diverts bots without bothering people: [Honeypot plugin](https://wordpress.org/plugins/honeypot/)
+
+* "I once wrote a tiny plugin for **UltimateMember where you simply have to enter a text to register**. Accessible and it keeps all bots out: [Download from GitHub](https://github.com/hansmorb/um-captchaquiz/raw/refs/heads/master/um-captchaquiz.zip). Just create a text box and enter the meta key in the plugin settings."
+
+* "We use hCaptcha for WordPress. After installation, select the registration you use in the plugin settings (e.g., UltimateMember; by default the built-in WordPress registration should be selected). To use it, you need to create an hCaptcha account. They advertise Privacy-First and that no user data is sold. I have not checked this myself." - [Download from the plugin directory](https://wordpress.org/plugins/hcaptcha-for-forms-and-more)
+:::
-If there are problems displaying the calendar in the admin area of the bookings (so-called admin backend), see the following picture on the bottom right, a possible solution can be to deactivate or remove and reinstall the [ "Lightstart" (wp-maintenance-mode) plugin ](https://wordpress.org/plugins/wp-maintenance-mode). The problem is an incompatibility of Lightstart with CommonsBooking and not an error in the CommonsBooking code. The problem no longer occurs if Lightstart has been reinstalled. Read more about this on [ Github in the CommonsBooking source code repository ](https://github.com/wielebenwir/commonsbooking/issues/1646).
+# Plugin & Theme Incompatibilities {#plugin-incompatibilities}
+
+## Plugin: Lightstart
+
+::: details Expand for answer
+If there are problems displaying the calendar in the booking admin area (the admin backend), see the image below on the right, one possible solution is to disable or remove and reinstall the ["Lightstart" (wp-maintenance-mode) plugin](https://wordpress.org/plugins/wp-maintenance-mode).
+The issue is an incompatibility between Lightstart and CommonsBooking and not a bug in CommonsBooking's code.
+The problem does not occur after reinstalling Lightstart. More details on [GitHub in the CommonsBooking source repository](https://github.com/wielebenwir/commonsbooking/issues/1646).

+:::
+
+## Theme: GridBulletin
-## Incompatible plugin All-in-one-Event Plugins
+::: details Expand for answer
+The latest version of [GridBulletin](https://wordpress.org/themes/gridbulletin) is incompatible with CommonsBooking.
+Problems occur when the footer is enabled. One concrete issue is the missing booking calendar on the item page. From a technical perspective, the required JavaScript sources from CommonsBooking are not being loaded. The root cause within the GridBulletin theme or a solution has not yet been found.
+:::
+
+## Plugin: All-in-one-Event
+:::: details Expand for answer
:::info Fixed since 2.7.2 (06.2022)
For experts see [Issue 675](https://github.com/wielebenwir/commonsbooking/issues/675)
:::
@@ -52,28 +127,35 @@ The cause is unfortunately due to poor programming of the All-in-one-Event plugi
We have tried a few things to enable parallel use, but unfortunately have not found a solution yet.
If you also have the problem, please write directly to the plugin's support, maybe they will adapt their plugin at some point.
+::::
-## Incompatible plugin REDIS Object Cache
+## Plugin: REDIS Object Cache
+::: details Expand for answer
In connection with the [Cache](../advanced-functionality/), there have been problems with other WordPress plugins such as 'REDIS Object Cache' in the past. For this reason, we advise against using such plugins.
-Therefore, the pages generated by CommonsBooking should be excluded from optimization by third-party plugins.
+The pages generated by CommonsBooking should be excluded from optimization by third-party plugins.
CommonsBooking uses its own caching.
+:::
-## Incompatible plugin Ultimate Member
+## Plugin: Ultimate Member
-If you use the Ultimate Member plugin and want to use the "CommonsBooking Manager" user role, you have to check a box in Ultimate Member for the cb_manager role to activate it for AP admin access.
+::: details Expand for answer
+If you use the Ultimate Member plugin and want to use the "CommonsBooking Manager" user role, you have to check a box in Ultimate Member for the `cb_manager` role to activate it for admin access.
+:::
-## Incompatible plugins: Autoptimize / Caching
+## Plugin: Autoptimize / Caching plugins
-Optimization plugins or other caching plugins can cause CommonsBooking to not be able to display all pages.
+:::: details Expand for answer
+Optimization plugins or other caching plugins can cause CommonsBooking to not display all pages correctly.
-Incomplete list:
+Affected plugins include (incomplete list):
* Autoptimize
-Therefore, the pages generated by CommonsBooking should be excluded from optimization by third-party plugins.
+The pages generated by CommonsBooking should be excluded from optimization by third-party plugins.
CommonsBooking uses its own caching.
-::: info Have you noticed a problem?
-Then enter the incompatibility here!
+:::info Have you noticed a problem?
+Add incompatible plugins or themes here!
:::
+::::
diff --git a/docs/en/documentation/faq/lock-codes-email.md b/docs/en/documentation/faq/lock-codes-email.md
deleted file mode 100644
index c3ddad0b3..000000000
--- a/docs/en/documentation/faq/lock-codes-email.md
+++ /dev/null
@@ -1,8 +0,0 @@
-# How do I show lock codes in emails?
-
-A frequent question is whether lock codes for combination locks can be added to items or locations so that they are displayed in the sent emails.
-
-This is possible via so-called meta fields, which are assigned to items and locations. These fields can then also be used in email templates.
-werden.
-
-[This page of the documentation contains a detailed guide.](../administration/template-tags)
diff --git a/docs/en/documentation/faq/organise-article-page.md b/docs/en/documentation/faq/organise-article-page.md
deleted file mode 100644
index a5794ecca..000000000
--- a/docs/en/documentation/faq/organise-article-page.md
+++ /dev/null
@@ -1,9 +0,0 @@
-# How can I make the item detail page clearer?
-
-## Information only shown on click
-
-Long item pages mean users have to scroll a long way to reach the booking calendar.
-
-A plugin such as [Show-Hide/Collapse-Expand](https://de.wordpress.org/plugins/show-hidecollapse-expand) can be used to collapse information.
-
-
diff --git a/docs/en/documentation/faq/problems-and-answers.md b/docs/en/documentation/faq/problems-and-answers.md
deleted file mode 100644
index 3a8582ec6..000000000
--- a/docs/en/documentation/faq/problems-and-answers.md
+++ /dev/null
@@ -1,14 +0,0 @@
-# Problems and answers
-
-### Calendar widget display in the admin area
-
-If there are problems displaying the calendar in the booking admin area (the admin backend), see the image below on the right, one possible solution is to disable or remove and reinstall the ["Lightstart" (wp-maintenance-mode) plugin](https://wordpress.org/plugins/wp-maintenance-mode).
-The issue is an incompatibility between Lightstart and CommonsBooking and not a bug in CommonsBooking's code.
-The problem does not occur after reinstalling Lightstart. More details on [GitHub in the CommonsBooking source repository](https://github.com/wielebenwir/commonsbooking/issues/1646).
-
-
-
-### Incompatible theme: GridBulletin
-
-The latest version of [GridBulletin](https://wordpress.org/themes/gridbulletin) is incompatible with CommonsBooking.
-Problems occur when the footer is enabled. One concrete issue is the missing booking calendar on the item page. From a technical perspective, the required JavaScript sources from CommonsBooking are not being loaded. The root cause within the GridBulletin theme or a solution has not yet been found.
diff --git a/docs/en/documentation/faq/site-slow.md b/docs/en/documentation/faq/site-slow.md
deleted file mode 100644
index a2c19002b..000000000
--- a/docs/en/documentation/faq/site-slow.md
+++ /dev/null
@@ -1,13 +0,0 @@
-# The site is very slow
-
-If your CommonsBooking site is very slow, there may be several underlying causes.
-We use a technology called caching, which keeps frequently requested data
-in temporary storage to save server resources.
-Caching may not work under certain conditions, for example when:
-
- * [WP_DEBUG](https://wordpress.org/documentation/article/debugging-in-wordpress) is enabled; in that case you need to edit your wp-config.php
- * The /tmp/ folder on your server is not writable. If that is the case, contact your web host and ask them to make the folder writable.
- * If that is not possible, you can set the path for the filesystem cache in the CommonsBooking settings under "Advanced Options". Ask your web host which folders on the server are available for temporary files.
- * If that is also not possible: Go to your Site Health screen at (http://YOUR-URL/wp-admin/site-health.php?tab=debug). There you will find the path to your WordPress directory under **Directories**. Alternatively, choose a folder in the format `YOUR_DIRECTORY/symfony` as the cache target. **Warning:** This can cause your WordPress directory to grow very large.
-
-Alternatively, you can install [Redis](https://redis.io) on your server and let Redis manage the cache. Since Redis stores the cache in RAM instead of the filesystem, it is usually a bit faster.
diff --git a/docs/en/documentation/setup/migration-from-cb1.md b/docs/en/documentation/setup/migration-from-cb1.md
index 89a92dba3..04c390412 100644
--- a/docs/en/documentation/setup/migration-from-cb1.md
+++ b/docs/en/documentation/setup/migration-from-cb1.md
@@ -1,5 +1,10 @@
# Migration from version 0.9.x
+:::danger danger
+With CommonsBooking 2.12 (releasing early 2027), migration support from CommonsBooking 0.9.X to the latest version will end.
+All users of CommonsBooking 0.9.X are urged to **migrate as soon as possible**. We are happy to help with any questions and will support you in migration.
+:::
+
You can migrate from CB 0.9.x to CB 2.x.x with a single click. The migration imports the following data:
* Items
diff --git a/includes/Users.php b/includes/Users.php
index c3220544b..9b9f5272a 100644
--- a/includes/Users.php
+++ b/includes/Users.php
@@ -171,17 +171,7 @@ function commonsbooking_isCurrentUserAdmin() {
return false; }
$user = wp_get_current_user();
- $isAdmin = commonsbooking_isUserAdmin( $user );
- /**
- * Default value if current user is admin.
- *
- * @since 2.10.0 add $user param
- * @since 2.4.3
- *
- * @param bool $isAdmin true or false, if current user is admin
- * @param null|WP_User $user current user
- */
- return apply_filters( 'commonsbooking_isCurrentUserAdmin', $isAdmin, $user );
+ return commonsbooking_isUserAdmin( $user );
}
/**
@@ -195,12 +185,24 @@ function commonsbooking_isCurrentUserAdmin() {
* @return bool
*/
function commonsbooking_isUserAdmin( \WP_User $user ) {
+ $isAdmin = false;
foreach ( \CommonsBooking\Repository\UserRepository::getAdminRoles() as $adminRole ) {
if ( in_array( $adminRole, $user->roles ) ) {
- return true;
+ $isAdmin = true;
+ break;
}
}
- return false;
+ /**
+ * Default value if user is admin.
+ *
+ * @since 2.11 changed from commonsbooking_isCurrentUserAdmin to commonsbooking_isUserAdmin
+ * @since 2.10.0 add $user param
+ * @since 2.4.3
+ *
+ * @param bool $isAdmin true or false, if current user is admin
+ * @param null|WP_User $user current user
+ */
+ return apply_filters( 'commonsbooking_isUserAdmin', $isAdmin, $user );
}
/**
diff --git a/includes/gbfs-json-schema/gbfs.json b/includes/gbfs-json-schema/gbfs.json
index 2898226e2..0bd292143 100644
--- a/includes/gbfs-json-schema/gbfs.json
+++ b/includes/gbfs-json-schema/gbfs.json
@@ -1,6 +1,6 @@
{
"$schema": "http://json-schema.org/draft-07/schema",
- "$id": "https://github.com/MobilityData/gbfs-json-schema/blob/master/v3.1-RC2/gbfs.json",
+ "$id": "https://github.com/MobilityData/gbfs-json-schema/blob/master/v3.1-RC3/gbfs.json",
"description": "Auto-discovery file that links to all of the other files published by the system.",
"type": "object",
"properties": {
@@ -17,7 +17,7 @@
"version": {
"description": "GBFS version number to which the feed conforms, according to the versioning framework (added in v1.1).",
"type": "string",
- "const": "3.1-RC2"
+ "const": "3.1-RC3"
},
"data": {
"type": "object",
diff --git a/includes/gbfs-json-schema/station_information.json b/includes/gbfs-json-schema/station_information.json
index d801ed3e6..8b9ca5700 100644
--- a/includes/gbfs-json-schema/station_information.json
+++ b/includes/gbfs-json-schema/station_information.json
@@ -1,7 +1,7 @@
{
"$schema": "http://json-schema.org/draft-07/schema",
"$id":
- "https://github.com/MobilityData/gbfs-json-schema/blob/master/v3.1-RC2/station_information.json",
+ "https://github.com/MobilityData/gbfs-json-schema/blob/master/v3.1-RC3/station_information.json",
"description":
"List of all stations, their capacities and locations. REQUIRED of systems utilizing docks.",
"type": "object",
@@ -22,7 +22,7 @@
"description":
"GBFS version number to which the feed conforms, according to the versioning framework (added in v1.1).",
"type": "string",
- "const": "3.1-RC2"
+ "const": "3.1-RC3"
},
"data": {
"description":
@@ -107,7 +107,7 @@
"type": "string"
},
"city": {
- "description": "City where station is located. (added in v3.1-RC2)",
+ "description": "City where station is located. (added in v3.1-RC3)",
"type": "string"
},
"station_opening_hours": {
diff --git a/includes/gbfs-json-schema/station_status.json b/includes/gbfs-json-schema/station_status.json
index 7249ab6b8..19cf5e119 100644
--- a/includes/gbfs-json-schema/station_status.json
+++ b/includes/gbfs-json-schema/station_status.json
@@ -1,6 +1,6 @@
{
"$schema": "http://json-schema.org/draft-07/schema",
- "$id": "https://github.com/MobilityData/gbfs-json-schema/blob/master/v3.1-RC2/station_status.json",
+ "$id": "https://github.com/MobilityData/gbfs-json-schema/blob/master/v3.1-RC3/station_status.json",
"description":
"Describes the capacity and rental availability of the station",
"type": "object",
@@ -21,7 +21,7 @@
"description":
"GBFS version number to which the feed conforms, according to the versioning framework (added in v1.1).",
"type": "string",
- "const": "3.1-RC2"
+ "const": "3.1-RC3"
},
"data": {
"description":
diff --git a/includes/gbfs-json-schema/system_information.json b/includes/gbfs-json-schema/system_information.json
index 1f29028f6..8c29b2d28 100644
--- a/includes/gbfs-json-schema/system_information.json
+++ b/includes/gbfs-json-schema/system_information.json
@@ -1,7 +1,7 @@
{
"$schema": "http://json-schema.org/draft-07/schema",
"$id":
- "https://github.com/MobilityData/gbfs-json-schema/blob/master/v3.1-RC2/system_information.json",
+ "https://github.com/MobilityData/gbfs-json-schema/blob/master/v3.1-RC3/system_information.json",
"description":
"Details including system operator, system location, year implemented, URL, contact info, time zone.",
"type": "object",
@@ -22,7 +22,7 @@
"description":
"GBFS version number to which the feed conforms, according to the versioning framework (added in v1.1).",
"type": "string",
- "const": "3.1-RC2"
+ "const": "3.1-RC3"
},
"data": {
"description": "Response data in the form of name:value pairs.",
diff --git a/includes/gbfs-json-schema/vehicle_availability.json b/includes/gbfs-json-schema/vehicle_availability.json
new file mode 100644
index 000000000..56ac9c593
--- /dev/null
+++ b/includes/gbfs-json-schema/vehicle_availability.json
@@ -0,0 +1,91 @@
+{
+ "$schema": "http://json-schema.org/draft-07/schema#",
+ "$id": "https://github.com/MobilityData/gbfs-json-schema/blob/master/v3.1-RC3/vehicle_availability.json",
+ "description": "Describes the vehicle availabilities of the system.",
+ "type": "object",
+ "properties": {
+ "last_updated": {
+ "description": "Last time the data in the feed was updated in RFC3339 format.",
+ "type": "string",
+ "format": "date-time"
+ },
+ "ttl": {
+ "description": "Number of seconds before the data in the feed will be updated again (0 if the data should always be refreshed).",
+ "type": "integer",
+ "minimum": 0
+ },
+ "version": {
+ "description": "GBFS version number to which the feed conforms, according to the versioning framework (added in v1.1).",
+ "type": "string",
+ "const": "3.1-RC3"
+ },
+ "data": {
+ "type": "object",
+ "properties": {
+ "vehicles": {
+ "type": "array",
+ "description": "Contains one object per vehicle.",
+ "items": {
+ "type": "object",
+ "properties": {
+ "vehicle_id": {
+ "type": "string",
+ "description": "Identifier of a vehicle"
+ },
+ "vehicle_type_id": {
+ "type": "string",
+ "description": "Unique identifier of a vehicle type as defined in vehicle_types.json"
+ },
+ "station_id": {
+ "type": "string",
+ "description": " The id of the station where this vehicle is located when available"
+ },
+ "pricing_plan_id": {
+ "type": "string",
+ "description": "The plan_id of the pricing plan this vehicle is eligible for"
+ },
+ "vehicle_equipment": {
+ "type": "array",
+ "description": "List of vehicle equipment provided by the operator",
+ "items": {
+ "type": "string"
+ }
+ },
+ "availabilities": {
+ "type": "array",
+ "description": "Array of time slots during which the specified vehicle is available.",
+ "items": {
+ "type": "object",
+ "properties": {
+ "from": {
+ "type": "string",
+ "description": "Start date and time of available time slot.",
+ "format": "date-time"
+ },
+ "until": {
+ "type": "string",
+ "description": "End date and time of available time slot.",
+ "format": "date-time"
+ }
+ },
+ "required": ["from"],
+ "additionalProperties": false
+ }
+ }
+ },
+ "required": [
+ "vehicle_id",
+ "station_id",
+ "availabilities"
+ ],
+ "additionalProperties": false
+ }
+ }
+ },
+ "required": ["vehicles"],
+ "additionalProperties": false
+ }
+ },
+ "required": ["last_updated", "ttl", "version", "data"],
+ "additionalProperties": false
+}
diff --git a/includes/gbfs-json-schema/vehicle_status.json b/includes/gbfs-json-schema/vehicle_status.json
new file mode 100644
index 000000000..87e280408
--- /dev/null
+++ b/includes/gbfs-json-schema/vehicle_status.json
@@ -0,0 +1,153 @@
+{
+ "$schema": "http://json-schema.org/draft-07/schema",
+ "$id":
+ "https://github.com/MobilityData/gbfs-json-schema/blob/master/v3.1-RC3/vehicle_status.json",
+ "description":
+ "Describes the vehicles that are available for rent (as of v3.0, formerly free_bike_status).",
+ "type": "object",
+ "properties": {
+ "last_updated": {
+ "description":
+ "Last time the data in the feed was updated in RFC3339 format.",
+ "type": "string",
+ "format": "date-time"
+ },
+ "ttl": {
+ "description":
+ "Number of seconds before the data in the feed will be updated again (0 if the data should always be refreshed).",
+ "type": "integer",
+ "minimum": 0
+ },
+ "version": {
+ "description":
+ "GBFS version number to which the feed conforms, according to the versioning framework (added in v1.1).",
+ "type": "string",
+ "const": "3.1-RC3"
+ },
+ "data": {
+ "description":
+ "Array that contains one object per vehicle as defined below.",
+ "type": "object",
+ "properties": {
+ "vehicles": {
+ "type": "array",
+ "items": {
+ "type": "object",
+ "properties": {
+ "vehicle_id": {
+ "description": "Rotating (as of v2.0) identifier of a vehicle.",
+ "type": "string"
+ },
+ "lat": {
+ "description": "The latitude of the vehicle.",
+ "type": "number",
+ "minimum": -90,
+ "maximum": 90
+ },
+ "lon": {
+ "description": "The longitude of the vehicle.",
+ "type": "number",
+ "minimum": -180,
+ "maximum": 180
+ },
+ "is_reserved": {
+ "description": "Is the vehicle currently reserved?",
+ "type": "boolean"
+ },
+ "is_disabled": {
+ "description": "Is the vehicle currently disabled (broken)?",
+ "type": "boolean"
+ },
+ "rental_uris": {
+ "description": "Contains rental uris for Android, iOS, and web in the android, ios, and web fields (added in v1.1).",
+ "type": "object",
+ "properties": {
+ "android": {
+ "description": "URI that can be passed to an Android app with an intent (added in v1.1).",
+ "type": "string",
+ "format": "uri"
+ },
+ "ios": {
+ "description": "URI that can be used on iOS to launch the rental app for this vehicle (added in v1.1).",
+ "type": "string",
+ "format": "uri"
+ },
+ "web": {
+ "description": "URL that can be used by a web browser to show more information about renting this vehicle (added in v1.1).",
+ "type": "string",
+ "format": "uri"
+ }
+ }
+ },
+ "vehicle_type_id": {
+ "description": "The vehicle_type_id of this vehicle (added in v2.1-RC).",
+ "type": "string"
+ },
+ "last_reported": {
+ "description": "The last time this vehicle reported its status to the operator's backend in RFC3339 format (added in v2.1-RC).",
+ "type": "string",
+ "format": "date-time"
+ },
+ "current_range_meters": {
+ "description": "The furthest distance in meters that the vehicle can travel without recharging or refueling with the vehicle's current charge or fuel (added in v2.1-RC).",
+ "type": "number",
+ "minimum": 0
+ },
+ "current_fuel_percent": {
+ "description": "This value represents the current percentage, expressed from 0 to 1, of fuel or battery power remaining in the vehicle. Added in v2.3-RC.",
+ "type": "number",
+ "minimum": 0,
+ "maximum": 1
+ },
+ "station_id": {
+ "description": "Identifier referencing the station_id if the vehicle is currently at a station (added in v2.1-RC2).",
+ "type": "string"
+ },
+ "home_station_id": {
+ "description": "The station_id of the station this vehicle must be returned to (added in v2.3-RC).",
+ "type": "string"
+ },
+ "pricing_plan_id": {
+ "description": "The plan_id of the pricing plan this vehicle is eligible for (added in v2.2).",
+ "type": "string"
+ },
+ "vehicle_equipment": {
+ "description": "List of vehicle equipment provided by the operator in addition to the accessories already provided in the vehicle. Added in v2.3.",
+ "type": "array",
+ "items": {
+ "enum": ["child_seat_a", "child_seat_b", "child_seat_c", "winter_tires", "snow_chains"]
+ }
+ },
+ "available_until": {
+ "description": "The date and time when any rental of the vehicle must be completed. Added in v2.3.",
+ "type": "string",
+ "pattern": "^([0-9]{4})-([0-9]{2})-([0-9]{2})T([0-9]{2}):([0-9]{2}):([0-9]{2})(([+-]([0-9]{2}):([0-9]{2}))|Z)$"
+ }
+ },
+ "anyOf": [
+ {
+ "required": ["lat", "lon"],
+ "errorMessage": "Both 'lat' and 'lon' are required."
+ },
+ {
+ "required": ["station_id"],
+ "properties": {
+ "lat": {
+ "not": {}
+ },
+ "lon": {
+ "not": {}
+ }
+ },
+ "errorMessage": "'station_id' is required if 'lat' and 'lon' are not present."
+ }
+ ],
+ "required": ["vehicle_id", "is_reserved", "is_disabled"]
+ }
+ }
+ },
+ "required": ["vehicles"]
+ }
+ },
+ "required": ["last_updated", "ttl", "version", "data"]
+}
diff --git a/includes/gbfs-json-schema/vehicle_types.json b/includes/gbfs-json-schema/vehicle_types.json
new file mode 100644
index 000000000..02eb9644c
--- /dev/null
+++ b/includes/gbfs-json-schema/vehicle_types.json
@@ -0,0 +1,273 @@
+{
+ "$schema": "http://json-schema.org/draft-07/schema",
+ "$id":
+ "https://github.com/MobilityData/gbfs-json-schema/blob/master/v3.1-RC3/vehicle_types.json",
+ "description":
+ "Describes the types of vehicles that System operator has available for rent (added in v2.1-RC).",
+ "type": "object",
+ "properties": {
+ "last_updated": {
+ "description":
+ "Last time the data in the feed was updated in RFC3339 format.",
+ "type": "string",
+ "format": "date-time"
+ },
+ "ttl": {
+ "description":
+ "Number of seconds before the data in the feed will be updated again (0 if the data should always be refreshed).",
+ "type": "integer",
+ "minimum": 0
+ },
+ "version": {
+ "description":
+ "GBFS version number to which the feed conforms, according to the versioning framework.",
+ "type": "string",
+ "const": "3.1-RC3"
+ },
+ "data": {
+ "description": "Response data in the form of name:value pairs.",
+ "type": "object",
+ "properties": {
+ "vehicle_types": {
+ "description":
+ "Array that contains one object per vehicle type in the system as defined below.",
+ "type": "array",
+ "items": {
+ "type": "object",
+ "properties": {
+ "vehicle_type_id": {
+ "description": "Unique identifier of a vehicle type.",
+ "type": "string"
+ },
+ "form_factor": {
+ "description": "The vehicle's general form factor.",
+ "type": "string",
+ "enum": ["bicycle", "cargo_bicycle" ,"car", "moped", "scooter_standing", "scooter_seated", "other"]
+ },
+ "rider_capacity": {
+ "description": "The number of riders (driver included) the vehicle can legally accommodate",
+ "type": "integer",
+ "minimum": 0
+ },
+ "cargo_volume_capacity": {
+ "description": "Cargo volume available in the vehicle, expressed in liters.",
+ "type": "integer",
+ "minimum": 0
+ },
+ "cargo_load_capacity": {
+ "description": "The capacity of the vehicle cargo space (excluding passengers), expressed in kilograms.",
+ "type": "integer",
+ "minimum": 0
+ },
+
+ "propulsion_type": {
+ "description": "The primary propulsion type of the vehicle. Updated in v2.3 to represent car-sharing",
+ "type": "string",
+ "enum": ["human", "electric_assist", "electric", "combustion", "combustion_diesel", "hybrid", "plug_in_hybrid", "hydrogen_fuel_cell"]
+ },
+ "eco_labels": {
+ "description": "Vehicle air quality certificate. added in v2.3.",
+ "type": "array",
+ "items": {
+ "type": "object",
+ "properties": {
+ "country_code": {
+ "description": " Country code following the ISO 3166-1 alpha-2 notation. Added in v2.3.",
+ "type": "string",
+ "pattern": "^[A-Z]{2}"
+ },
+ "eco_sticker": {
+ "description": " Name of the eco label. Added in v2.3.",
+ "type": "string"
+ }
+ },
+ "required": ["country_code", "eco_sticker"]
+ }
+ },
+ "max_range_meters": {
+ "description":
+ "The furthest distance in meters that the vehicle can travel without recharging or refueling when it has the maximum amount of energy potential.",
+ "type": "number",
+ "minimum": 0
+ },
+ "name": {
+ "description": "The public name of this vehicle type. An array with one object per supported language with the following keys:",
+ "type": "array",
+ "items": {
+ "type": "object",
+ "properties": {
+ "text": {
+ "description": "The translated text.",
+ "type": "string"
+ },
+ "language": {
+ "description": "IETF BCP 47 language code.",
+ "type": "string",
+ "pattern": "^[a-z]{2,3}(-[A-Z]{2})?$"
+ }
+ },
+ "required": ["text", "language"]
+ }
+ },
+ "vehicle_accessories": {
+ "description": "Description of accessories available in the vehicle.",
+ "type": "array",
+ "items": {
+ "enum": ["air_conditioning", "automatic", "manual", "convertible", "cruise_control", "doors_2", "doors_3", "doors_4", "doors_5", "navigation"]
+ }
+ },
+ "g_CO2_km": {
+ "description": "Maximum quantity of CO2, in grams, emitted per kilometer, according to the WLTP. Added in v2.3",
+ "type": "integer",
+ "minimum": 0
+ },
+ "vehicle_image": {
+ "description": "URL to an image that would assist the user in identifying the vehicle. JPEG or PNG. Added in v2.3",
+ "type": "string",
+ "format": "uri"
+ },
+ "make": {
+ "description": "The name of the vehicle manufacturer. An array with one object per supported language with the following keys:",
+ "type": "array",
+ "items": {
+ "type": "object",
+ "properties": {
+ "text": {
+ "description": "The translated text.",
+ "type": "string"
+ },
+ "language": {
+ "description": "IETF BCP 47 language code.",
+ "type": "string",
+ "pattern": "^[a-z]{2,3}(-[A-Z]{2})?$"
+ }
+ },
+ "required": ["text", "language"]
+ }
+ },
+ "model": {
+ "description": "The name of the vehicle model. An array with one object per supported language with the following keys:",
+ "type": "array",
+ "items": {
+ "type": "object",
+ "properties": {
+ "text": {
+ "description": "The translated text.",
+ "type": "string"
+ },
+ "language": {
+ "description": "IETF BCP 47 language code.",
+ "type": "string",
+ "pattern": "^[a-z]{2,3}(-[A-Z]{2})?$"
+ }
+ },
+ "required": ["text", "language"]
+ }
+ },
+ "color": {
+ "description": "The color of the vehicle. Added in v2.3",
+ "type": "string"
+ },
+ "description": {
+ "description": "Customer-readable description of the vehicle type outlining special features or how-tos. An array with one object per supported language with the following keys:",
+ "type": "array",
+ "items": {
+ "type": "object",
+ "properties": {
+ "text": {
+ "description": "The translated text.",
+ "type": "string"
+ },
+ "language": {
+ "description": "IETF BCP 47 language code.",
+ "type": "string",
+ "pattern": "^[a-z]{2,3}(-[A-Z]{2})?$"
+ }
+ },
+ "required": ["text", "language"]
+ }
+ },
+ "wheel_count": {
+ "description": "Number of wheels this vehicle type has. Added in v2.3",
+ "type": "integer",
+ "minimum": 0
+ },
+ "max_permitted_speed": {
+ "description": "The maximum speed in kilometers per hour this vehicle is permitted to reach in accordance with local permit and regulations. Added in v2.3",
+ "type": "integer",
+ "minimum": 0
+ },
+ "rated_power": {
+ "description": "The rated power of the motor for this vehicle type in watts. Added in v2.3",
+ "type": "integer",
+ "minimum": 0
+ },
+ "default_reserve_time": {
+ "description": "Maximum time in minutes that a vehicle can be reserved before a rental begins added in v2.3-RC.",
+ "type": "integer",
+ "minimum": 0
+ },
+ "return_constraint": {
+ "description": "The conditions for returning the vehicle at the end of the trip. Added in v2.3-RC as return_type, and updated to return_constraint in v2.3.",
+ "type": "string",
+ "enum": ["free_floating", "roundtrip_station", "any_station", "hybrid"]
+ },
+ "min_age": {
+ "description": "Minimum age required to use this vehicle. Added in v3.1-RC3.",
+ "type": "integer",
+ "minimum": 0
+ },
+ "vehicle_assets": {
+ "description": "An object where each key defines one of the items listed below added in v2.3-RC.",
+ "type": "object",
+ "properties": {
+ "icon_url": {
+ "description": "A fully qualified URL pointing to the location of a graphic icon file that MAY be used to represent this vehicle type on maps and in other applications added in v2.3-RC.",
+ "type": "string",
+ "format": "uri"
+ },
+ "icon_url_dark": {
+ "description": "A fully qualified URL pointing to the location of a graphic icon file to be used to represent this vehicle type when in dark mode added in v2.3-RC.",
+ "type": "string",
+ "format": "uri"
+ },
+ "icon_last_modified": {
+ "description": "Date that indicates the last time any included vehicle icon images were modified or updated added in v2.3-RC.",
+ "type": "string",
+ "format": "date"
+ }
+ },
+ "required": ["icon_url", "icon_last_modified"]
+ },
+ "default_pricing_plan_id": {
+ "description": "A plan_id as defined in system_pricing_plans.json added in v2.3-RC.",
+ "type": "string"
+ },
+ "pricing_plan_ids": {
+ "description": "Array of all pricing plan IDs as defined in system_pricing_plans.json added in v2.3-RC.",
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ }
+ },
+ "required": ["vehicle_type_id", "form_factor", "propulsion_type"],
+ "if": {
+ "properties": {
+ "propulsion_type": {
+ "enum": ["electric", "electric_assist", "combustion", "combustion_diesel", "hybrid", "plug_in_hybrid", "hydrogen_fuel_cell"]
+ }
+ }
+ },
+ "then": {
+ "required": ["max_range_meters"]
+ }
+ }
+ }
+ },
+ "required": ["vehicle_types"]
+ }
+ },
+ "required": ["last_updated", "ttl", "version", "data"]
+}
+
diff --git a/integrations/uncanny-automator/bootstrap.php b/integrations/uncanny-automator/bootstrap.php
new file mode 100644
index 000000000..37cfaac40
--- /dev/null
+++ b/integrations/uncanny-automator/bootstrap.php
@@ -0,0 +1,41 @@
+set_trigger_type( 'anonymous' );
+
+ $this->set_integration( 'COMMONSBOOKING' );
+ $this->set_trigger_code( 'CB_BOOKING_CANCELLED' );
+ $this->set_trigger_meta( 'CBBOOKINGCANCELLED' );
+ $this->set_sentence( esc_attr__( 'A booking is cancelled', 'commonsbooking' ) );
+ $this->set_readable_sentence( esc_attr__( 'A booking is cancelled', 'commonsbooking' ) );
+
+ // The CommonsBooking lifecycle action passes ( int $booking_id, Booking $booking ).
+ $this->add_action( 'commonsbooking_booking_cancelled', 10, 2 );
+ }
+
+ /**
+ * This trigger has no configurable options; it fires for every cancelled booking.
+ *
+ * @return array
+ */
+ public function options() {
+ return array();
+ }
+
+ /**
+ * The trigger fires for every cancelled booking.
+ *
+ * @param array $trigger The trigger configuration.
+ * @param array $hook_args The arguments passed by the WordPress hook.
+ *
+ * @return bool
+ */
+ public function validate( $trigger, $hook_args ) {
+ return true;
+ }
+
+ // define_tokens() and hydrate_tokens() are provided by the Booking_Tokens trait.
+}
diff --git a/integrations/uncanny-automator/class-booking-confirmed-trigger.php b/integrations/uncanny-automator/class-booking-confirmed-trigger.php
new file mode 100644
index 000000000..d7757216d
--- /dev/null
+++ b/integrations/uncanny-automator/class-booking-confirmed-trigger.php
@@ -0,0 +1,65 @@
+set_trigger_type( 'anonymous' );
+
+ $this->set_integration( 'COMMONSBOOKING' );
+ $this->set_trigger_code( 'CB_BOOKING_CONFIRMED' );
+ $this->set_trigger_meta( 'CBBOOKINGCONFIRMED' );
+ $this->set_sentence( esc_attr__( 'A booking is confirmed', 'commonsbooking' ) );
+ $this->set_readable_sentence( esc_attr__( 'A booking is confirmed', 'commonsbooking' ) );
+
+ // The CommonsBooking lifecycle action passes ( int $booking_id, Booking $booking ).
+ $this->add_action( 'commonsbooking_booking_confirmed', 10, 2 );
+ }
+
+ /**
+ * This trigger has no configurable options; it fires for every confirmed booking.
+ *
+ * @return array
+ */
+ public function options() {
+ return array();
+ }
+
+ /**
+ * The trigger fires for every confirmed booking.
+ *
+ * @param array $trigger The trigger configuration.
+ * @param array $hook_args The arguments passed by the WordPress hook.
+ *
+ * @return bool
+ */
+ public function validate( $trigger, $hook_args ) {
+ return true;
+ }
+
+ // define_tokens() and hydrate_tokens() are provided by the Booking_Tokens trait.
+}
diff --git a/integrations/uncanny-automator/class-cb-integration.php b/integrations/uncanny-automator/class-cb-integration.php
new file mode 100644
index 000000000..9aea5a6f5
--- /dev/null
+++ b/integrations/uncanny-automator/class-cb-integration.php
@@ -0,0 +1,27 @@
+set_integration( 'COMMONSBOOKING' );
+ $this->set_name( 'CommonsBooking' );
+ $this->set_icon_url( plugin_dir_url( __FILE__ ) . 'icon.svg' );
+ }
+}
diff --git a/integrations/uncanny-automator/icon.svg b/integrations/uncanny-automator/icon.svg
new file mode 100644
index 000000000..de536375a
--- /dev/null
+++ b/integrations/uncanny-automator/icon.svg
@@ -0,0 +1,7 @@
+
diff --git a/integrations/uncanny-automator/trait-booking-tokens.php b/integrations/uncanny-automator/trait-booking-tokens.php
new file mode 100644
index 000000000..60e259b43
--- /dev/null
+++ b/integrations/uncanny-automator/trait-booking-tokens.php
@@ -0,0 +1,124 @@
+ 'CB_BOOKING_ID',
+ 'tokenName' => __( 'Booking ID', 'commonsbooking' ),
+ 'tokenType' => 'int',
+ );
+ $tokens[] = array(
+ 'tokenId' => 'CB_ITEM_NAME',
+ 'tokenName' => __( 'Item name', 'commonsbooking' ),
+ 'tokenType' => 'text',
+ );
+ $tokens[] = array(
+ 'tokenId' => 'CB_LOCATION_NAME',
+ 'tokenName' => __( 'Location name', 'commonsbooking' ),
+ 'tokenType' => 'text',
+ );
+ $tokens[] = array(
+ 'tokenId' => 'CB_BOOKING_START',
+ 'tokenName' => __( 'Booking start', 'commonsbooking' ),
+ 'tokenType' => 'text',
+ );
+ $tokens[] = array(
+ 'tokenId' => 'CB_BOOKING_END',
+ 'tokenName' => __( 'Booking end', 'commonsbooking' ),
+ 'tokenType' => 'text',
+ );
+ $tokens[] = array(
+ 'tokenId' => 'CB_BOOKING_CODE',
+ 'tokenName' => __( 'Booking code', 'commonsbooking' ),
+ 'tokenType' => 'text',
+ );
+ $tokens[] = array(
+ 'tokenId' => 'CB_BOOKING_USER_EMAIL',
+ 'tokenName' => __( 'Booking user email', 'commonsbooking' ),
+ 'tokenType' => 'email',
+ );
+ $tokens[] = array(
+ 'tokenId' => 'CB_BOOKING_USER_PHONE',
+ 'tokenName' => __( 'Booking user phone', 'commonsbooking' ),
+ 'tokenType' => 'text',
+ );
+ $tokens[] = array(
+ 'tokenId' => 'CB_BOOKING_URL',
+ 'tokenName' => __( 'Booking URL', 'commonsbooking' ),
+ 'tokenType' => 'url',
+ );
+
+ return $tokens;
+ }
+
+ /**
+ * Populates the token values from the booking passed by the lifecycle hook.
+ *
+ * @param array $trigger The trigger configuration.
+ * @param array $hook_args The arguments passed by the lifecycle action ( int $booking_id, Booking $booking ).
+ *
+ * @return array
+ */
+ public function hydrate_tokens( $trigger, $hook_args ) {
+ $booking_id = isset( $hook_args[0] ) ? (int) $hook_args[0] : 0;
+ $booking = isset( $hook_args[1] ) ? $hook_args[1] : null;
+
+ if ( ! $booking instanceof \CommonsBooking\Model\Booking ) {
+ $booking = $booking_id ? new \CommonsBooking\Model\Booking( $booking_id ) : null;
+ }
+
+ if ( ! $booking ) {
+ return array( 'CB_BOOKING_ID' => $booking_id );
+ }
+
+ $item = $booking->getItem();
+ $location = $booking->getLocation();
+ $author_id = (int) $booking->post_author;
+ $user = get_userdata( $author_id );
+
+ // The booking code is optional and may throw if unavailable.
+ $code = '';
+ try {
+ $code = $booking->formattedBookingCode();
+ } catch ( \Exception $e ) {
+ $code = '';
+ }
+
+ return array(
+ 'CB_BOOKING_ID' => $booking_id,
+ 'CB_ITEM_NAME' => $item ? $item->post_title : '',
+ 'CB_LOCATION_NAME' => $location ? $location->post_title : '',
+ 'CB_BOOKING_START' => $booking->getFormattedStartDate(),
+ 'CB_BOOKING_END' => $booking->getFormattedEndDate(),
+ 'CB_BOOKING_CODE' => $code,
+ 'CB_BOOKING_USER_EMAIL' => $user ? $user->user_email : '',
+ 'CB_BOOKING_USER_PHONE' => $author_id ? (string) get_user_meta( $author_id, 'phone', true ) : '',
+ 'CB_BOOKING_URL' => $booking->bookingLinkUrl(),
+ );
+ }
+}
diff --git a/languages/commonsbooking-de_DE.mo b/languages/commonsbooking-de_DE.mo
index c79a46cd8..fae1c549b 100644
Binary files a/languages/commonsbooking-de_DE.mo and b/languages/commonsbooking-de_DE.mo differ
diff --git a/languages/commonsbooking-de_DE.po b/languages/commonsbooking-de_DE.po
index 0be42d0b2..1aa95cf0f 100644
--- a/languages/commonsbooking-de_DE.po
+++ b/languages/commonsbooking-de_DE.po
@@ -8,10 +8,10 @@ msgstr ""
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"POT-Creation-Date: 2023-09-15T04:04:14+02:00\n"
-"PO-Revision-Date: 2026-02-19 18:07+0100\n"
+"PO-Revision-Date: 2026-06-17 15:48+0200\n"
"Language: de_DE\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
-"X-Generator: Poedit 3.8\n"
+"X-Generator: Poedit 3.9\n"
#. Plugin Name of the plugin
#: commonsbooking.php
@@ -1006,71 +1006,71 @@ msgstr "Erlaubt die Anpassung der Optionen für das Caching-System"
msgid "Current connection status"
msgstr "Aktueller Verbindungsstatus"
-#: src/CB/CB1UserFields.php:69
+#: src/CB/CB1UserFields.php:72
msgid "First Name"
msgstr "Vorname"
-#: src/CB/CB1UserFields.php:72
+#: src/CB/CB1UserFields.php:75
msgid "Please enter your first name"
msgstr "Bitte gib deinen Vornamen ein"
-#: src/CB/CB1UserFields.php:76
+#: src/CB/CB1UserFields.php:79
msgid "Last Name"
msgstr "Nachname"
-#: src/CB/CB1UserFields.php:79
+#: src/CB/CB1UserFields.php:82
msgid "Please enter your last name"
msgstr "Bitte gib Deinen Nachnamen ein"
-#: src/CB/CB1UserFields.php:83
+#: src/CB/CB1UserFields.php:86
msgid "Phone Number"
msgstr "Telefonnummer"
-#: src/CB/CB1UserFields.php:86
+#: src/CB/CB1UserFields.php:89
msgid "Please enter your phone number"
msgstr "Bitte gib Deine Telefonnummer ein"
-#: src/CB/CB1UserFields.php:90
-#: src/CB/CB1UserFields.php:279
+#: src/CB/CB1UserFields.php:93
+#: src/CB/CB1UserFields.php:283
#: src/Wordpress/CustomPostType/Location.php:237
#: templates/booking-single.php:77
msgid "Address"
msgstr "Adresse"
-#: src/CB/CB1UserFields.php:93
+#: src/CB/CB1UserFields.php:96
msgid "Please enter your address"
msgstr "Bitte gib deine Adresse ein"
-#: src/CB/CB1UserFields.php:96
+#: src/CB/CB1UserFields.php:99
msgid "Terms and Conditions"
msgstr "Nutzungsbedingungen"
-#: src/CB/CB1UserFields.php:99
+#: src/CB/CB1UserFields.php:102
msgid "I accept the terms & conditions"
msgstr "Ich akzeptiere die Nutzungsbedingungen"
-#: src/CB/CB1UserFields.php:100
+#: src/CB/CB1UserFields.php:103
msgid "Please accept the terms & conditions"
msgstr "Bitte akzeptiere die Nutzungsbedingungen"
-#: src/CB/CB1UserFields.php:188
+#: src/CB/CB1UserFields.php:191
#, php-format
msgid "Read the terms and services"
msgstr "Lies die Nutzungsbedingungen"
-#: src/CB/CB1UserFields.php:267
+#: src/CB/CB1UserFields.php:271
msgid "Extra Fields"
msgstr "Zusatzfelder"
-#: src/CB/CB1UserFields.php:271
+#: src/CB/CB1UserFields.php:275
msgid "Phone number"
msgstr "Telefonnummer"
-#: src/CB/CB1UserFields.php:287
+#: src/CB/CB1UserFields.php:291
msgid "Terms and conditions"
msgstr "Nutzungsbedingungen"
-#: src/CB/CB1UserFields.php:297
+#: src/CB/CB1UserFields.php:301
msgid "Accepted Terms & Conditions"
msgstr "Nutzungsbedingungen akzeptiert"
@@ -1179,56 +1179,56 @@ msgstr "Link zu deiner Buchung"
msgid "Please contact the contact persons at the location directly if you have any questions regarding collection or return:"
msgstr "Bitte kontaktiere die Kontaktpersonen vor Ort direkt, wenn du Fragen zur Abholung oder Rückgabe hast:"
-#: src/Model/Restriction.php:244
-#: src/Model/Restriction.php:268
+#: src/Model/Restriction.php:248
+#: src/Model/Restriction.php:275
msgid "Not set"
msgstr "Nicht gesetzt"
-#: src/Model/Timeframe.php:255
+#: src/Model/Timeframe.php:331
msgid "Available here"
msgstr "Hier verfügbar"
#. translators: %s = date in WordPress defined format
#: src/Model/Booking.php:437
-#: src/Model/Timeframe.php:260
+#: src/Model/Timeframe.php:336
#, php-format
msgid "on %s"
msgstr "am %s"
#. translators: %s = date in WordPress defined format
-#: src/Model/Timeframe.php:265
+#: src/Model/Timeframe.php:341
#, php-format
msgid "from %s"
msgstr "von %s"
-#: src/Model/Timeframe.php:269
+#: src/Model/Timeframe.php:345
msgid "permanently"
msgstr "dauerhaft"
#. translators: %1 = startdate, %2 = enddate in WordPress defined format
#. translators: %1$s = startdate, second %2$s = enddate in WordPress defined format
#: src/Model/Booking.php:440
-#: src/Model/Timeframe.php:275
+#: src/Model/Timeframe.php:351
#, php-format
msgid "from %1$s until %2$s"
msgstr "von %1$s bis %2$s"
#. translators: %s = enddate in WordPress defined format
-#: src/Model/Timeframe.php:282
+#: src/Model/Timeframe.php:358
#, php-format
msgid "until %s"
msgstr "bis %s"
-#: src/Model/Timeframe.php:610
+#: src/Model/Timeframe.php:686
msgid "A pickup time but no return time has been set. Please set the return time."
msgstr "Eine Abholzeit, aber keine Rückgabezeit wurde festgelegt. Lege bitte eine Rückgabezeit fest."
-#: src/Model/Timeframe.php:620
+#: src/Model/Timeframe.php:696
msgid "End date is before start date. Please set a valid end date."
msgstr "Enddatum liegt vor dem Startdatum. Bitte setze ein gültiges Enddatum."
#. translators: %1$s = timeframe-ID, %2$s is timeframe post_title
-#: src/Model/Timeframe.php:655
+#: src/Model/Timeframe.php:731
#, php-format
msgid "Item is already bookable at another location within the same date range. See other timeframe ID: %1$s: %2$s"
msgstr "Artikel ist bereits an einem anderen Ort innerhalb desselben Datumsbereichs buchbar. Siehe andere Zeitrahmen ID: %1$s: %2$s"
@@ -1243,8 +1243,8 @@ msgstr "Artikel-Kategorien"
msgid "Location Categories"
msgstr "Standort-Kategorien"
-#: src/Wordpress/CustomPostType/Item.php:324
-#: src/Wordpress/CustomPostType/Item.php:344
+#: src/Wordpress/CustomPostType/Item.php:334
+#: src/Wordpress/CustomPostType/Item.php:354
msgid "Item Category"
msgstr "Artikel-Kategorie"
@@ -1275,14 +1275,14 @@ msgid "Directory %s could not be written to."
msgstr "Der Ordner %s ist nicht beschreibbar."
#: src/View/Admin/Filter.php:58
-#: src/Wordpress/CustomPostType/Booking.php:779
+#: src/Wordpress/CustomPostType/Booking.php:792
#: src/Wordpress/CustomPostType/Restriction.php:451
#: src/Wordpress/CustomPostType/Timeframe.php:678
msgid "Start date"
msgstr "Startdatum"
#: src/View/Admin/Filter.php:62
-#: src/Wordpress/CustomPostType/Booking.php:796
+#: src/Wordpress/CustomPostType/Booking.php:809
#: src/Wordpress/CustomPostType/Restriction.php:461
#: src/Wordpress/CustomPostType/Timeframe.php:702
msgid "End date"
@@ -1307,7 +1307,7 @@ msgstr "Nicht verfügbar"
#: src/View/Booking.php:196
#: src/View/MassOperations.php:38
-#: src/Wordpress/CustomPostType/Booking.php:446
+#: src/Wordpress/CustomPostType/Booking.php:459
#: src/Wordpress/CustomPostType/Timeframe.php:97
#: templates/shortcode-bookings.php:74
msgid "User"
@@ -1315,7 +1315,7 @@ msgstr "Nutzende*r"
#: src/View/Booking.php:200
#: src/View/MassOperations.php:42
-#: src/Wordpress/CustomPostType/Booking.php:973
+#: src/Wordpress/CustomPostType/Booking.php:986
#: src/Wordpress/CustomPostType/Restriction.php:477
msgid "Status"
msgstr "Status"
@@ -1332,10 +1332,10 @@ msgstr "Die Datei wird als .txt Datei durch Tabstopp getrennt exportiert, sodass
msgid "Booking codes list"
msgstr "Liste der Buchungscodes"
-#: src/View/Calendar.php:233
-#: src/Wordpress/CustomPostType/Booking.php:447
-#: src/Wordpress/CustomPostType/Booking.php:761
-#: src/Wordpress/CustomPostType/Booking.php:989
+#: src/View/Calendar.php:237
+#: src/Wordpress/CustomPostType/Booking.php:460
+#: src/Wordpress/CustomPostType/Booking.php:774
+#: src/Wordpress/CustomPostType/Booking.php:1002
#: src/Wordpress/CustomPostType/Item.php:117
#: src/Wordpress/CustomPostType/Restriction.php:48
#: src/Wordpress/CustomPostType/Restriction.php:438
@@ -1344,14 +1344,14 @@ msgstr "Liste der Buchungscodes"
msgid "Item"
msgstr "Artikel"
-#: src/View/Calendar.php:177
+#: src/View/Calendar.php:181
msgid "No items found."
msgstr "Keine Artikel gefunden."
-#: src/View/Calendar.php:234
-#: src/Wordpress/CustomPostType/Booking.php:448
-#: src/Wordpress/CustomPostType/Booking.php:767
-#: src/Wordpress/CustomPostType/Booking.php:985
+#: src/View/Calendar.php:238
+#: src/Wordpress/CustomPostType/Booking.php:461
+#: src/Wordpress/CustomPostType/Booking.php:780
+#: src/Wordpress/CustomPostType/Booking.php:998
#: src/Wordpress/CustomPostType/Location.php:139
#: src/Wordpress/CustomPostType/Restriction.php:49
#: src/Wordpress/CustomPostType/Restriction.php:431
@@ -1503,189 +1503,189 @@ msgstr "Standort existiert nicht. (%s)"
msgid "Start- and/or end-date is missing."
msgstr "Das Start- und/oder Enddatum fehlt."
-#: src/Wordpress/CustomPostType/Booking.php:307
+#: src/Wordpress/CustomPostType/Booking.php:320
msgid "Booking canceled."
msgstr "Buchung storniert."
#: src/Wordpress/CustomPostType/Booking.php:291
-#: src/Wordpress/CustomPostType/Booking.php:323
+#: src/Wordpress/CustomPostType/Booking.php:336
msgid "There is already a booking in this time-range. This notice may also appear if there is an unconfirmed booking in the requested period. Unconfirmed bookings are deleted after about 10 minutes. Please try again in a few minutes."
msgstr "Es gibt bereits eine Buchung in diesem Zeitraum. Dieser Hinweis kann auch erscheinen, wenn es eine unbestätigte Buchung in dem gewünschten Zeitraum gibt. Unbestätigte Buchungen werden nach etwa 10 Minuten gelöscht. Bitte versuche es in ein paar Minuten erneut."
-#: src/Wordpress/CustomPostType/Booking.php:331
-#: src/Wordpress/CustomPostType/Booking.php:524
-#: src/Wordpress/CustomPostType/Booking.php:712
+#: src/Wordpress/CustomPostType/Booking.php:344
+#: src/Wordpress/CustomPostType/Booking.php:537
+#: src/Wordpress/CustomPostType/Booking.php:725
#: src/Wordpress/CustomPostType/Timeframe.php:132
msgid "Booking"
msgstr "Buchung"
-#: src/Wordpress/CustomPostType/Booking.php:375
+#: src/Wordpress/CustomPostType/Booking.php:388
msgid "There was an error while saving the booking. Please try again. Thrown error:"
msgstr "Es gab einen Fehler während der Buchung. Geworfener Fehler:"
-#: src/Wordpress/CustomPostType/Booking.php:392
+#: src/Wordpress/CustomPostType/Booking.php:405
msgid "There was an error while saving the booking. Please try again. Resulting WP_ERROR: "
msgstr "Es gab einen Fehler während der Buchung. WP_ERROR: "
-#: src/Wordpress/CustomPostType/Booking.php:449
+#: src/Wordpress/CustomPostType/Booking.php:462
msgid "Bookingdate"
msgstr "Buchungsdatum"
-#: src/Wordpress/CustomPostType/Booking.php:450
+#: src/Wordpress/CustomPostType/Booking.php:463
#: src/Wordpress/CustomPostType/Restriction.php:50
#: src/Wordpress/CustomPostType/Timeframe.php:101
#: src/Wordpress/CustomPostType/Timeframe.php:758
msgid "Start Date"
msgstr "Startdatum"
-#: src/Wordpress/CustomPostType/Booking.php:451
+#: src/Wordpress/CustomPostType/Booking.php:464
#: src/Wordpress/CustomPostType/Restriction.php:51
#: src/Wordpress/CustomPostType/Timeframe.php:102
msgid "End Date"
msgstr "Enddatum"
-#: src/Wordpress/CustomPostType/Booking.php:452
+#: src/Wordpress/CustomPostType/Booking.php:465
msgid "Booking Status"
msgstr "Buchungsstatus"
-#: src/Wordpress/CustomPostType/Booking.php:453
-#: src/Wordpress/CustomPostType/Booking.php:981
+#: src/Wordpress/CustomPostType/Booking.php:466
+#: src/Wordpress/CustomPostType/Booking.php:994
#: src/Wordpress/CustomPostType/Timeframe.php:448
msgid "Comment"
msgstr "Kommentar"
-#: src/Wordpress/CustomPostType/Booking.php:523
+#: src/Wordpress/CustomPostType/Booking.php:536
#: templates/dashboard-index.php:45
msgid "Bookings"
msgstr "Buchungen"
-#: src/Wordpress/CustomPostType/Booking.php:525
+#: src/Wordpress/CustomPostType/Booking.php:538
#: src/Wordpress/CustomPostType/Item.php:118
#: src/Wordpress/CustomPostType/Location.php:140
#: src/Wordpress/CustomPostType/Restriction.php:307
-#: src/Wordpress/CustomPostType/Timeframe.php:1152
+#: src/Wordpress/CustomPostType/Timeframe.php:1153
msgid "Add new"
msgstr "Neu hinzufügen"
-#: src/Wordpress/CustomPostType/Booking.php:526
-#: src/Wordpress/CustomPostType/Booking.php:528
+#: src/Wordpress/CustomPostType/Booking.php:539
+#: src/Wordpress/CustomPostType/Booking.php:541
msgid "Add new booking"
msgstr "Neue Buchung hinzufügen"
-#: src/Wordpress/CustomPostType/Booking.php:527
-#: src/Wordpress/CustomPostType/Booking.php:743
+#: src/Wordpress/CustomPostType/Booking.php:540
+#: src/Wordpress/CustomPostType/Booking.php:756
msgid "Edit booking"
msgstr "Buchung bearbeiten"
-#: src/Wordpress/CustomPostType/Booking.php:529
+#: src/Wordpress/CustomPostType/Booking.php:542
msgid "Show booking"
msgstr "Buchung anzeigen"
-#: src/Wordpress/CustomPostType/Booking.php:530
+#: src/Wordpress/CustomPostType/Booking.php:543
msgid "Show bookings"
msgstr "Buchungen anzeigen"
-#: src/Wordpress/CustomPostType/Booking.php:531
+#: src/Wordpress/CustomPostType/Booking.php:544
msgid "Search bookings"
msgstr "Buchungen suchen"
-#: src/Wordpress/CustomPostType/Timeframe.php:1159
+#: src/Wordpress/CustomPostType/Timeframe.php:1160
msgid "Timeframes not found"
msgstr "Zeitrahmen wurden nicht gefunden"
-#: src/Wordpress/CustomPostType/Booking.php:533
+#: src/Wordpress/CustomPostType/Booking.php:546
msgid "No bookings found in trash"
msgstr "Keine Buchungen im Papierkorb gefunden"
-#: src/Wordpress/CustomPostType/Booking.php:534
+#: src/Wordpress/CustomPostType/Booking.php:547
msgid "Parent bookings:"
msgstr "Übergeordnete Buchungen:"
-#: src/Wordpress/CustomPostType/Booking.php:535
+#: src/Wordpress/CustomPostType/Booking.php:548
msgid "All bookings"
msgstr "Alle Buchungen"
-#: src/Wordpress/CustomPostType/Booking.php:536
-#: src/Wordpress/CustomPostType/Timeframe.php:1163
+#: src/Wordpress/CustomPostType/Booking.php:549
+#: src/Wordpress/CustomPostType/Timeframe.php:1164
msgid "Timeframe archive"
msgstr "Zeitrahmenarchiv"
-#: src/Wordpress/CustomPostType/Booking.php:537
-#: src/Wordpress/CustomPostType/Timeframe.php:1164
+#: src/Wordpress/CustomPostType/Booking.php:550
+#: src/Wordpress/CustomPostType/Timeframe.php:1165
msgid "Timeframe attributes"
msgstr "Zeitrahmenattribute"
-#: src/Wordpress/CustomPostType/Booking.php:538
+#: src/Wordpress/CustomPostType/Booking.php:551
msgid "Add to booking"
msgstr "Der Buchung hinzufügen"
-#: src/Wordpress/CustomPostType/Booking.php:539
+#: src/Wordpress/CustomPostType/Booking.php:552
msgid "Added to booking"
msgstr "Zur Buchung hinzugefügt"
-#: src/Wordpress/CustomPostType/Booking.php:540
-#: src/Wordpress/CustomPostType/Timeframe.php:1167
+#: src/Wordpress/CustomPostType/Booking.php:553
+#: src/Wordpress/CustomPostType/Timeframe.php:1168
msgid "Timeframe image"
msgstr "Zeitrahmenbild"
-#: src/Wordpress/CustomPostType/Booking.php:541
+#: src/Wordpress/CustomPostType/Booking.php:554
msgid "set booking image"
msgstr "Buchungsbild festlegen"
-#: src/Wordpress/CustomPostType/Booking.php:542
+#: src/Wordpress/CustomPostType/Booking.php:555
msgid "remove booking image"
msgstr "Buchungsbild entfernen"
-#: src/Wordpress/CustomPostType/Booking.php:543
+#: src/Wordpress/CustomPostType/Booking.php:556
msgid "use as booking image"
msgstr "als Buchungsbild verwenden"
-#: src/Wordpress/CustomPostType/Booking.php:544
-#: src/Wordpress/CustomPostType/Timeframe.php:1150
-#: src/Wordpress/CustomPostType/Timeframe.php:1171
+#: src/Wordpress/CustomPostType/Booking.php:557
+#: src/Wordpress/CustomPostType/Timeframe.php:1151
+#: src/Wordpress/CustomPostType/Timeframe.php:1172
#: templates/dashboard-index.php:37
msgid "Timeframes"
msgstr "Zeitrahmen"
-#: src/Wordpress/CustomPostType/Booking.php:852
+#: src/Wordpress/CustomPostType/Booking.php:865
msgid "External comment"
msgstr "Öffentlicher Kommentar"
-#: src/Wordpress/CustomPostType/Booking.php:853
+#: src/Wordpress/CustomPostType/Booking.php:866
msgid "This comment can be seen by users in booking details. It can be set by users during the booking confirmation process if comments are enabled in settings."
msgstr "Dieser Kommentar ist intern für Zeitrahmen wie buchbar, Reparatur, Urlaub. Wenn es sich um eine Buchung handelt, kann dieser Kommentar von den Benutzenden während des Buchungsbestätigungsprozesses eingegeben werden."
-#: src/Wordpress/CustomPostType/Booking.php:858
+#: src/Wordpress/CustomPostType/Booking.php:871
#: templates/booking-single.php:131
msgid "Internal comment"
msgstr "Interner Kommentar"
-#: src/Wordpress/CustomPostType/Booking.php:859
+#: src/Wordpress/CustomPostType/Booking.php:872
msgid "This internal comment can only be seen in the backend by privileged users like admins or cb-managers"
msgstr "Dieser interne Kommentar kann im Backend nur von autorisierten Nutzenden wie Admins oder CB-Managern gesehen werden"
-#: src/Wordpress/CustomPostType/Booking.php:780
+#: src/Wordpress/CustomPostType/Booking.php:793
msgid "Set the start date. You must set the time to 00:00 if you want to book the full day "
msgstr "Lege das Startdatum fest. Setze die Uhrzeit auf 00:00, wenn du den ganzen Tag buchen willst "
-#: src/Wordpress/CustomPostType/Booking.php:797
+#: src/Wordpress/CustomPostType/Booking.php:810
msgid "Set the end date. You must set time to 23:59 if you want to book the full day"
msgstr "Lege das Enddatum fest. Setze die Zeit auf 23:59 Uhr, wenn du den ganzen Tag buchen willst"
-#: src/Wordpress/CustomPostType/Booking.php:813
+#: src/Wordpress/CustomPostType/Booking.php:826
#: templates/booking-single.php:59
msgid "Booking Code"
msgstr "Buchungscode"
-#: src/Wordpress/CustomPostType/Booking.php:819
+#: src/Wordpress/CustomPostType/Booking.php:832
msgid "Booking User"
msgstr "Buchende Person"
-#: src/Wordpress/CustomPostType/Booking.php:834
+#: src/Wordpress/CustomPostType/Booking.php:847
msgid "Admin Booking User"
msgstr "Admin Booking Konto"
-#: src/Wordpress/CustomPostType/Booking.php:845
+#: src/Wordpress/CustomPostType/Booking.php:858
msgid "This is the admin user who created or modified this booking."
msgstr "Dies ist der/die Admin-Nutzende, der diese Buchung erstellt oder geändert hat."
@@ -1792,7 +1792,7 @@ msgstr "E-Mail-Adresse des Artikelbetreuers"
msgid "Email addresses to which notifications about a change of item status (restriction, breakdown) shall be sent. You can enter multiple addresses separated by commas."
msgstr "E-Mail-Adressen die benachrichtigt werden soll, wenn eine Nutzungseinschränkung für den Artikel eingerichtet wurde (Hinweis oder Totalausfall). Es können mehrere Adressen durch Komma getrennt eingeben werden."
-#: src/Wordpress/CustomPostType/Item.php:293
+#: src/Wordpress/CustomPostType/Item.php:303
msgid "Item Meta-Data"
msgstr "Artikel Metadaten"
@@ -2185,7 +2185,7 @@ msgid "Blocked (not overbookable)"
msgstr "Geblockt (nicht überbuchbar)"
#: src/Wordpress/CustomPostType/Timeframe.php:420
-#: src/Wordpress/CustomPostType/Timeframe.php:1151
+#: src/Wordpress/CustomPostType/Timeframe.php:1152
msgid "Timeframe"
msgstr "Zeitrahmen"
@@ -2364,56 +2364,56 @@ msgstr "Monatlich"
msgid "Yearly"
msgstr "Jährlich"
-#: src/Wordpress/CustomPostType/Timeframe.php:1153
-#: src/Wordpress/CustomPostType/Timeframe.php:1155
+#: src/Wordpress/CustomPostType/Timeframe.php:1154
+#: src/Wordpress/CustomPostType/Timeframe.php:1156
msgid "Add new timeframe"
msgstr "Hinzufügen eines neuen Zeitrahmens"
-#: src/Wordpress/CustomPostType/Timeframe.php:1154
+#: src/Wordpress/CustomPostType/Timeframe.php:1155
msgid "Edit timeframe"
msgstr "Zeitrahmen bearbeiten"
-#: src/Wordpress/CustomPostType/Timeframe.php:1156
+#: src/Wordpress/CustomPostType/Timeframe.php:1157
msgid "Show timeframe"
msgstr "Zeitrahmen anzeigen"
-#: src/Wordpress/CustomPostType/Timeframe.php:1157
+#: src/Wordpress/CustomPostType/Timeframe.php:1158
msgid "Show timeframes"
msgstr "Zeitrahmen anzeigen"
-#: src/Wordpress/CustomPostType/Timeframe.php:1158
+#: src/Wordpress/CustomPostType/Timeframe.php:1159
msgid "Search timeframes"
msgstr "Zeitrahmen suchen"
-#: src/Wordpress/CustomPostType/Timeframe.php:1160
+#: src/Wordpress/CustomPostType/Timeframe.php:1161
msgid "No timeframes found in trash"
msgstr "Keine Zeitrahmen im Papierkorb gefunden"
-#: src/Wordpress/CustomPostType/Timeframe.php:1161
+#: src/Wordpress/CustomPostType/Timeframe.php:1162
msgid "Parent timeframes:"
msgstr "Übergeordnete Zeitrahmen:"
-#: src/Wordpress/CustomPostType/Timeframe.php:1162
+#: src/Wordpress/CustomPostType/Timeframe.php:1163
msgid "All timeframes"
msgstr "Alle Zeitrahmen"
-#: src/Wordpress/CustomPostType/Timeframe.php:1165
+#: src/Wordpress/CustomPostType/Timeframe.php:1166
msgid "Add to timeframe"
msgstr "Zum Zeitrahmen hinzufügen"
-#: src/Wordpress/CustomPostType/Timeframe.php:1166
+#: src/Wordpress/CustomPostType/Timeframe.php:1167
msgid "Added to timeframe"
msgstr "Zum Zeitrahmen hinzugefügt"
-#: src/Wordpress/CustomPostType/Timeframe.php:1168
+#: src/Wordpress/CustomPostType/Timeframe.php:1169
msgid "set timeframe image"
msgstr "Festlegen des Zeitrahmenbilds"
-#: src/Wordpress/CustomPostType/Timeframe.php:1169
+#: src/Wordpress/CustomPostType/Timeframe.php:1170
msgid "remove timeframe image"
msgstr "Zeitrahmenbild entfernen"
-#: src/Wordpress/CustomPostType/Timeframe.php:1170
+#: src/Wordpress/CustomPostType/Timeframe.php:1171
msgid "use as timeframe image"
msgstr "Verwendung als Zeitrahmenbild"
@@ -3025,41 +3025,41 @@ msgstr "Es ist nicht möglich, diesen Zeitrahmen im Frontend abzurufen. Bitte be
msgid "Cancelled"
msgstr "Storniert"
-#: src/Model/Timeframe.php:630
+#: src/Model/Timeframe.php:706
msgid "The start- and end-time of the timeframe can not be the same. Please check the full-day checkbox if you want users to be able to book the full day."
msgstr "Die Start- und Endzeit des Zeitrahmens dürfen nicht gleich sein. Wenn der gesamte Tag buchbar sein soll muss die Option \"Ganzer Tag\" angewählt sein."
#: src/Plugin.php:720
#: src/Plugin.php:735
-#: src/Wordpress/CustomPostType/Booking.php:936
+#: src/Wordpress/CustomPostType/Booking.php:949
msgid "CommonsBooking Bookings"
msgstr "CommonsBooking Buchungen"
-#: src/Wordpress/CustomPostType/Booking.php:532
+#: src/Wordpress/CustomPostType/Booking.php:545
msgid "Bookings not found"
msgstr "Keine Buchungen gefunden"
-#: src/Wordpress/CustomPostType/Booking.php:961
+#: src/Wordpress/CustomPostType/Booking.php:974
msgid "Booking start"
msgstr "Buchungsbeginn"
-#: src/Wordpress/CustomPostType/Booking.php:965
+#: src/Wordpress/CustomPostType/Booking.php:978
msgid "Booking end"
msgstr "Buchungsende"
-#: src/Wordpress/CustomPostType/Booking.php:969
+#: src/Wordpress/CustomPostType/Booking.php:982
msgid "Time of booking"
msgstr "Buchungszeit"
-#: src/Wordpress/CustomPostType/Booking.php:977
+#: src/Wordpress/CustomPostType/Booking.php:990
msgid "Booking code"
msgstr "Buchungscode"
-#: src/Wordpress/CustomPostType/Booking.php:993
+#: src/Wordpress/CustomPostType/Booking.php:1006
msgid "Time of cancellation"
msgstr "Storniert am"
-#: src/Wordpress/CustomPostType/Booking.php:997
+#: src/Wordpress/CustomPostType/Booking.php:1010
msgid "Admin booking by"
msgstr "Adminbuchung von"
@@ -3110,25 +3110,25 @@ msgid "If selected, days that are overbooked will be counted towards the maximum
msgstr "Ist diese Option aktiviert, werden überbuchte Tage auf die maximale Anzahl buchbarer Tage angerechnet. Wenn diese Option deaktiviert ist, dann werden überbuchte Tage nicht zusätzlich gezählt und ermöglichen Buchungszeiträume die länger sind als die im Zeitrahmen konfigurierte maximale Anzahl an gebuchten Tagen."
#. translators: first %s = timeframe-ID, second %s is timeframe post_title
-#: src/Model/Timeframe.php:691
+#: src/Model/Timeframe.php:767
#, php-format
msgid "See overlapping timeframe ID: %1$s %2$s"
msgstr "Sich überschneidende Zeitrahmen ID: %1$s %2$s"
-#: src/Model/Timeframe.php:790
+#: src/Model/Timeframe.php:866
msgid "Overlapping bookable timeframes are only allowed to have the same grid."
msgstr "Sich überschneidende buchbare Zeiträume müssen das gleiche Raster haben."
-#: src/Model/Timeframe.php:820
+#: src/Model/Timeframe.php:896
msgid "Overlapping bookable timeframes are not allowed to have the same weekdays."
msgstr "Mehrere buchbare Zeiträume überschneiden sich in den definierten Wochentagen."
-#: src/Model/Timeframe.php:832
+#: src/Model/Timeframe.php:908
msgid "Overlapping bookable timeframes are not allowed to have the same dates."
msgstr "Mehrere buchbare Zeiträume überschneiden sich in dem definierten Datumsbereich."
-#: src/Model/Timeframe.php:839
-#: src/Model/Timeframe.php:846
+#: src/Model/Timeframe.php:915
+#: src/Model/Timeframe.php:922
msgid "The other timeframe is overlapping with your weekly configuration."
msgstr "Der andere Zeitrahmen überschneidet sich mit einer wöchentlichen Konfiguration."
@@ -3168,7 +3168,7 @@ msgstr "Feiertage importieren"
msgid "Select the year and state to import holidays for (as of now only German holidays are supported)"
msgstr "Wählen Sie das Jahr und das Bundesland, für das Sie Feiertage importieren möchten (derzeit werden Feiertage in Deutschland unterstützt)"
-#: src/Model/Timeframe.php:806
+#: src/Model/Timeframe.php:882
msgid "Daily repeated time periods are not allowed to overlap."
msgstr "Täglich wiederholende Zeiträume dürfen sich nicht überschneiden."
@@ -3318,11 +3318,11 @@ msgstr "Fehlende ID des Zeitrahmens"
msgid "Error sending booking codes"
msgstr "Fehler beim Senden von Buchungscodes"
-#: src/View/BookingCodes.php:556
+#: src/View/BookingCodes.php:555
msgid "An unknown error occured"
msgstr "Ein unbekannter Fehler ist aufgetreten"
-#: src/View/BookingCodes.php:557
+#: src/View/BookingCodes.php:556
msgid "Email booking codes"
msgstr "Buchungscodes per E-Mail versenden"
@@ -3867,64 +3867,44 @@ msgstr "Bitte passe das Start- oder Enddatum an."
msgid "Affected Bookings: %s"
msgstr "Betroffene Buchungen: %s"
-#: src/Model/Timeframe.php:555
+#: src/Model/Timeframe.php:631
msgid "Could not get item or location. Please set a valid item and location."
msgstr "Artikel oder Standort konnte nicht gefunden werden. Bitte wähle einen gültigen Artikel oder Standort."
-#: src/Model/Timeframe.php:564
+#: src/Model/Timeframe.php:640
msgid "Item or location is missing. Please set item and location."
msgstr "Artikel oder Standort fehlen. Bitte Artikel und Standort festlegen."
-#: src/Model/Timeframe.php:577
+#: src/Model/Timeframe.php:653
msgid "No dates selected. Please select at least one date."
msgstr "Kein Datum ausgewählt. Bitte mindestens ein Datum auswählen."
-#: src/Model/Timeframe.php:587
+#: src/Model/Timeframe.php:663
msgid "The same date was selected multiple times. Please select each date only once."
msgstr "Das gleiche Datum wurde mehrere Male ausgewählt. Bitte ein Datum nur einmal auswählen."
-#: src/Model/Timeframe.php:596
+#: src/Model/Timeframe.php:672
msgid "Startdate is missing. Please enter a start date to publish this timeframe."
msgstr "Das Startdatum fehlt. Ein Startdatum muss eingegeben werden, damit der Zeitrahmen veröffentlicht werden kann."
-#: src/View/Booking.php:581
-#: src/Wordpress/CustomPostType/Booking.php:864
+#: src/View/Booking.php:578
+#: src/Wordpress/CustomPostType/Booking.php:877
msgid "Submit booking"
msgstr "Buchung abschicken"
-#: src/Wordpress/CustomPostType/Booking.php:745
-msgid ""
-"
Notice
In this view, you as an admin can create or modify existing bookings. Please use it with caution. \n"
-"\t\t\t\t
\n"
-"
Click on the preview button on the right panel to view more booking details and to cancel the booking via the cancel button.
\n"
-"
Click on the Submit booking button at the end of the page to submit a new booking.
\n"
-"
\n"
-"\t\t\t\tPlease note: Only a few basic checks against existing bookings are performed. Please be wary of overlapping bookings.\n"
-" \n"
-"\t\t\t\t"
-msgstr ""
-"
Achtung
In dieser Ansicht kannst du als Administrator*in Buchungen erstellen oder ändern. Bitte nutze die Möglichkeit mit Vorsicht. \n"
-"\t\t\t\t
\n"
-"
Klicke auf die Vorschau-Schaltfläche auf der rechten Seite, um weitere Buchungsdetails zu sehen und die Buchung über die Schaltfläche „Stornieren“ zu stornieren.
\n"
-"
Klicke auf den Buchung abschicken Knopf am Ende der Seite um die Buchung zu bestätigen.
\n"
-"
\n"
-"\t\t\t\tBitte beachte: Es werden nur grundlegende Überprüfungen gegen bestehende Buchungen durchgeführt. Bitte prüfe, ob es keine Buchungskonflikte mit anderen Buchungen gibt. \n"
-" \n"
-"\t\t\t\t"
-
-#: src/Wordpress/CustomPostType/Booking.php:773
+#: src/Wordpress/CustomPostType/Booking.php:786
msgid "Book full day"
msgstr "Ganztägig buchen"
-#: src/Wordpress/CustomPostType/Booking.php:776
+#: src/Wordpress/CustomPostType/Booking.php:789
msgid "The booking should apply to the entire day(s)"
msgstr "Die Buchung soll auf den gesamten Tag / die gesamten Tage angewendet werden"
-#: src/Wordpress/CustomPostType/Booking.php:816
+#: src/Wordpress/CustomPostType/Booking.php:829
msgid "Valid booking code will be automatically retrieved for bookings that apply to the full day."
msgstr "Ein gültiger Buchungscode wird automatisch für Buchungen, die sich auf den ganzen Tag beziehen, generiert."
-#: src/Wordpress/CustomPostType/Booking.php:825
+#: src/Wordpress/CustomPostType/Booking.php:838
msgid ""
"Here you must select the user for whom the booking is made. \n"
" If the booking was made by a user via frontend booking process, the user will be shown in this field.\n"
@@ -3934,7 +3914,7 @@ msgstr ""
" Wenn die Buchung von einem/einer Nutzenden über den Frontend-Buchungsprozess vorgenommen wurde, wird der/die Nutzende in diesem Feld angezeigt.\n"
" Hinweis: Der/die Nutzende erhält eine Buchungsbestätigung, sobald die Buchung abgeschickt wurde."
-#: src/Wordpress/CustomPostType/Booking.php:865
+#: src/Wordpress/CustomPostType/Booking.php:878
msgid "This will create the specified booking and send out the booking confirmation email."
msgstr "Dadurch wird die angegebene Buchung erstellt und die Buchungsbestätigung per E-Mail verschickt."
@@ -4006,11 +3986,11 @@ msgstr "Buchungen bei Totalausfall nicht stornieren"
msgid "If checked, bookings will not be cancelled if the item has broken down. The user will be notified and once the item becomes available again, the old bookings are still valid."
msgstr "Wenn ausgewählt, werden Buchungen nicht storniert, wenn ein Totalausfall für den Artikel gesetzt ist. Der / die Benutzer*in wird benachrichtigt und sobald der Artikel wieder verfügbar ist, sind die alten Buchungen weiterhin gültig."
-#: src/Wordpress/CustomPostType/Item.php:353
+#: src/Wordpress/CustomPostType/Item.php:363
msgid "Add custom title for filter"
msgstr "Benutzerdefinierten Titel für Filtergruppe hinzufügen"
-#: src/Wordpress/CustomPostType/Item.php:356
+#: src/Wordpress/CustomPostType/Item.php:366
msgid "Define name that should be used for the category if it is displayed in the map as a filter group. You can also use this to add custom HTML to the category name. When left empty, the defined name of the category will be used."
msgstr "Legt den Namen fest, der für die Kategorie verwendet werden soll, wenn sie in der Karte als Teil der Filtergruppe angezeigt wird. Hier kann auch benutzerdefinierter HTML-Code zum Kategorienamen hinzugefügt werden. Bleibt der Eintrag leer, wird der Standard Name der Kategorie verwendet."
@@ -4408,23 +4388,6 @@ msgstr "Wenn diese Option ausgewählt ist, ist die API ohne API-Schlüssel zugä
msgid "You can define on or more API shares. Read the documentation for more information about API shares and configuration %1$sAPI documentation%2$s"
msgstr "Du kannst ein oder mehrere API-Freigaben definieren. Weitere Informationen zu API-Freigaben und zur Konfiguration findest du in der %1$sAPI-Dokumentation%2$s"
-#: src/Wordpress/CustomPostType/Booking.php:888
-#, php-format
-msgid ""
-"Bookings should be created via frontend booking calendar. \n"
-"\t\tAs an admin you can create bookings via this admin interface. Please be aware that admin bookings are not validated\n"
-"\t\tand checked. Use this function with care. \n"
-"\t\tClick on preview to show booking details in frontend \n"
-"\t\tTo search and filter bookings please integrate the frontend booking list via shortcode. \n"
-"\t\tSee here %1$sHow to display the booking list%2$s"
-msgstr ""
-"Buchungen sollten über den Frontend-Buchungskalender erstellt werden. \r\n"
-"\t\tAls Admin kannst du über diese Admin-Oberfläche Buchungen anlegen. Bitte beachte, dass Admin-Buchungen nicht validiert\r\n"
-"\t\tund überprüft werden. Verwende diese Funktion mit Vorsicht. \r\n"
-"\t\tKlicke auf Vorschau, um die Buchungsdetails im Frontend anzuzeigen. \r\n"
-"\t\tZur Suche und Filterung von Buchungen, binde bitte die Frontend-Buchungsliste per Shortcode ein. \r\n"
-"\t\tMehr dazu %1$sBinde die Buchungsliste im Frontend ein.%2$s"
-
#: src/Wordpress/CustomPostType/Location.php:540
#, php-format
msgid "If selected, all not selected days in any bookable timeframe that is connected to this location can be overbooked. Read the documentation %1$sCreate Locations%2$s for more information."
@@ -4450,25 +4413,17 @@ msgstr ""
"\t\t\t\t%1$sWeitere Informationen in der Dokumentation%2$s\r\n"
"\t\t\t\t"
-#: src/Model/Booking.php:1024
-#~ msgid "Unconfirmed"
-#~ msgstr "Unbestätigt"
-
-#: src/Model/Booking.php:1022
-#~ msgid "Confirmed"
-#~ msgstr "Bestätigt"
-
#: includes/OptionsArray.php:1553
msgid "Enables users to copy a url for a dynamic iCalendar feed into their own digital calendars."
-msgstr ""
+msgstr "Erlaubt Nutzenden eine URL für einen dynamischen iCalendar in ihre eigenen digitalen Kalender zu kopieren."
#: includes/OptionsArray.php:1567
msgid "This is the event title that will be given to bookings that do not belong to the current user. This is what the CB-Manager will see when they subscribe to the station calendar. You can use template tags here as well"
-msgstr ""
+msgstr "Dies ist der Titel für Kalendereinträge von Buchungen, die nicht dem aktuell angemeldeten Nutzenden zugeordnet sind. Solche Einträge sind für Personen mit der Rolle „CB-Manager“ sichtbar, wenn sie den Kalender einer Station abonnieren. Hier können auch Template-Tags genutzt werden."
#: includes/OptionsArray.php:1574
msgid "This is the event description that will be given to bookings that do not belong to the current user. This is what the CB-Manager will see when they subscribe to the station calendar. You can use template tags here."
-msgstr ""
+msgstr "Dies ist die Beschreibung für Kalendereinträge von Buchungen, die nicht dem aktuell angemeldeten Nutzenden zugeordnet sind. Solche Einträge sind für Personen mit der Rolle „CB-Manager“ sichtbar, wenn sie den Kalender einer Station abonnieren. Hier können auch Template-Tags genutzt werden."
#: includes/OptionsArray.php:1576
msgid ""
@@ -4478,3 +4433,65 @@ msgid ""
"Booking code: {{booking:formattedBookingCode}}\n"
"User Email: {{user:user_email}}"
msgstr ""
+"\n"
+"Abholung: {{booking:pickupDatetime}}\r\n"
+"Rückgabe: {{booking:returnDatetime}}\r\n"
+"Buchungscode: {{booking:formattedBookingCode}}\r\n"
+"E-Mail des Nutzenden: {{user:user_email}}"
+
+#: src/Wordpress/CustomPostType/Item.php:287
+msgid "Exclude from API"
+msgstr "In API verstecken"
+
+#: src/Wordpress/CustomPostType/Item.php:288
+msgid "When this box is checked, the item will not appear in any of the API shares."
+msgstr "Wenn diese Box angewählt ist taucht der Artikel in keiner der API Freigaben auf."
+
+#: src/Wordpress/CustomPostType/Booking.php:758
+msgid ""
+"
Notice
In this view, you as an admin can create or modify existing bookings. Please use it with caution. \n"
+"\t\t\t\t
\n"
+"
Click on the preview button on the right panel to view more booking details and to cancel the booking via the cancel button.
\n"
+"
Click on the Submit booking button at the end of the page to submit a new booking.
\n"
+"
\n"
+"\t\t\t\tPlease note: Only a few basic checks against existing bookings are performed. Please be wary of overlapping bookings.\n"
+" \n"
+"\t\t\t\t"
+msgstr ""
+"
Achtung
In dieser Ansicht kannst du als Administrator*in Buchungen erstellen oder ändern. Bitte nutze die Möglichkeit mit Vorsicht. \r\n"
+"\t\t\t\t
\r\n"
+"
Klicke auf den Vorschau Knopf auf der rechten Seite, um weitere Buchungsdetails zu sehen und die Buchung über die Schaltfläche „Stornieren“ zu stornieren.
\r\n"
+"
Klicke auf den Buchung abschicken Knopf am Ende der Seite um die Buchung zu bestätigen..
\r\n"
+"
\r\n"
+"\t\t\t\tBitte beachte:: Es werden nur grundlegende Überprüfungen gegen bestehende Buchungen durchgeführt. Bitte prüfe, ob es keine Buchungskonflikte mit anderen Buchungen gibt.\r\n"
+" \r\n"
+"\t\t\t\t"
+
+#: src/Wordpress/CustomPostType/Booking.php:901
+#, php-format
+msgid ""
+"Bookings should be created via frontend booking calendar. \n"
+"\t\tAs an admin you can create bookings via this admin interface. Please be aware that admin bookings are not validated\n"
+"\t\tand checked. Use this function with care. \n"
+"\t\tClick on preview to show booking details in frontend \n"
+"\t\tTo search and filter bookings please integrate the frontend booking list via shortcode.\n"
+"\t\tSee here %1$sHow to display the booking list%2$s"
+msgstr ""
+"Buchungen sollten über den Buchungskalender im Frontend erstellt werden. \r\n"
+"\t\tAls Admin kannst du Buchungen über das Backend erstellen. Bitte beachte, dass die Buchungen dort nicht validiert werden.\r\n"
+"\t\tNutze diese Funktion mit Vorsicht. \r\n"
+"\t\tKlicke auf den Vorschau Knopf, um die Details der Buchung im Frontend anzuzeigen. \r\n"
+"\t\tUm Buchungen zu filtern und durchsuchen, kann der Buchungsshortcode auf einer Frontend Seite eingefügt werden.\r\n"
+"\t\tIn der Dokumentation %1$sBuchungsliste anzeigen%2$s"
+
+#: src/Wordpress/CustomPostType/Booking.php:304
+msgid "Invalid booking request. Please try again."
+msgstr "Ungültige Buchungsanfrage. Bitte erneut versuchen."
+
+#: src/Model/Booking.php:1024
+#~ msgid "Unconfirmed"
+#~ msgstr "Unbestätigt"
+
+#: src/Model/Booking.php:1022
+#~ msgid "Confirmed"
+#~ msgstr "Bestätigt"
diff --git a/languages/commonsbooking.pot b/languages/commonsbooking.pot
index 1feb6de32..ccc305c2b 100644
--- a/languages/commonsbooking.pot
+++ b/languages/commonsbooking.pot
@@ -9,7 +9,7 @@ msgstr ""
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
-"POT-Creation-Date: 2026-04-09T19:23:59+00:00\n"
+"POT-Creation-Date: 2026-06-17T13:53:30+00:00\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"X-Generator: WP-CLI 2.12.0\n"
"X-Domain: commonsbooking\n"
@@ -1299,71 +1299,71 @@ msgstr ""
msgid "Every day"
msgstr ""
-#: src/CB/CB1UserFields.php:69
+#: src/CB/CB1UserFields.php:72
msgid "First Name"
msgstr ""
-#: src/CB/CB1UserFields.php:72
+#: src/CB/CB1UserFields.php:75
msgid "Please enter your first name"
msgstr ""
-#: src/CB/CB1UserFields.php:76
+#: src/CB/CB1UserFields.php:79
msgid "Last Name"
msgstr ""
-#: src/CB/CB1UserFields.php:79
+#: src/CB/CB1UserFields.php:82
msgid "Please enter your last name"
msgstr ""
-#: src/CB/CB1UserFields.php:83
+#: src/CB/CB1UserFields.php:86
msgid "Phone Number"
msgstr ""
-#: src/CB/CB1UserFields.php:86
+#: src/CB/CB1UserFields.php:89
msgid "Please enter your phone number"
msgstr ""
-#: src/CB/CB1UserFields.php:90
-#: src/CB/CB1UserFields.php:279
+#: src/CB/CB1UserFields.php:93
+#: src/CB/CB1UserFields.php:283
#: src/Wordpress/CustomPostType/Location.php:237
#: templates/booking-single.php:77
msgid "Address"
msgstr ""
-#: src/CB/CB1UserFields.php:93
+#: src/CB/CB1UserFields.php:96
msgid "Please enter your address"
msgstr ""
-#: src/CB/CB1UserFields.php:96
+#: src/CB/CB1UserFields.php:99
msgid "Terms and Conditions"
msgstr ""
-#: src/CB/CB1UserFields.php:99
+#: src/CB/CB1UserFields.php:102
msgid "I accept the terms & conditions"
msgstr ""
-#: src/CB/CB1UserFields.php:100
+#: src/CB/CB1UserFields.php:103
msgid "Please accept the terms & conditions"
msgstr ""
-#: src/CB/CB1UserFields.php:188
+#: src/CB/CB1UserFields.php:191
#, php-format
msgid "Read the terms and services"
msgstr ""
-#: src/CB/CB1UserFields.php:267
+#: src/CB/CB1UserFields.php:271
msgid "Extra Fields"
msgstr ""
-#: src/CB/CB1UserFields.php:271
+#: src/CB/CB1UserFields.php:275
msgid "Phone number"
msgstr ""
-#: src/CB/CB1UserFields.php:287
+#: src/CB/CB1UserFields.php:291
msgid "Terms and conditions"
msgstr ""
-#: src/CB/CB1UserFields.php:297
+#: src/CB/CB1UserFields.php:301
msgid "Accepted Terms & Conditions"
msgstr ""
@@ -1476,7 +1476,7 @@ msgstr ""
#. translators: %s = date in WordPress defined format
#: src/Model/Booking.php:437
-#: src/Model/Timeframe.php:260
+#: src/Model/Timeframe.php:336
#, php-format
msgid "on %s"
msgstr ""
@@ -1484,7 +1484,7 @@ msgstr ""
#. translators: %1 = startdate, %2 = enddate in WordPress defined format
#. translators: %1$s = startdate, second %2$s = enddate in WordPress defined format
#: src/Model/Booking.php:440
-#: src/Model/Timeframe.php:275
+#: src/Model/Timeframe.php:351
#, php-format
msgid "from %1$s until %2$s"
msgstr ""
@@ -1559,93 +1559,93 @@ msgstr ""
msgid "Please contact the contact persons at the location directly if you have any questions regarding collection or return:"
msgstr ""
-#: src/Model/Restriction.php:244
-#: src/Model/Restriction.php:268
+#: src/Model/Restriction.php:248
+#: src/Model/Restriction.php:275
msgid "Not set"
msgstr ""
-#: src/Model/Timeframe.php:255
+#: src/Model/Timeframe.php:331
msgid "Available here"
msgstr ""
#. translators: %s = date in WordPress defined format
-#: src/Model/Timeframe.php:265
+#: src/Model/Timeframe.php:341
#, php-format
msgid "from %s"
msgstr ""
-#: src/Model/Timeframe.php:269
+#: src/Model/Timeframe.php:345
msgid "permanently"
msgstr ""
#. translators: %s = enddate in WordPress defined format
-#: src/Model/Timeframe.php:282
+#: src/Model/Timeframe.php:358
#, php-format
msgid "until %s"
msgstr ""
-#: src/Model/Timeframe.php:555
+#: src/Model/Timeframe.php:631
msgid "Could not get item or location. Please set a valid item and location."
msgstr ""
-#: src/Model/Timeframe.php:564
+#: src/Model/Timeframe.php:640
msgid "Item or location is missing. Please set item and location."
msgstr ""
-#: src/Model/Timeframe.php:577
+#: src/Model/Timeframe.php:653
msgid "No dates selected. Please select at least one date."
msgstr ""
-#: src/Model/Timeframe.php:587
+#: src/Model/Timeframe.php:663
msgid "The same date was selected multiple times. Please select each date only once."
msgstr ""
-#: src/Model/Timeframe.php:596
+#: src/Model/Timeframe.php:672
msgid "Startdate is missing. Please enter a start date to publish this timeframe."
msgstr ""
-#: src/Model/Timeframe.php:610
+#: src/Model/Timeframe.php:686
msgid "A pickup time but no return time has been set. Please set the return time."
msgstr ""
-#: src/Model/Timeframe.php:620
+#: src/Model/Timeframe.php:696
msgid "End date is before start date. Please set a valid end date."
msgstr ""
-#: src/Model/Timeframe.php:630
+#: src/Model/Timeframe.php:706
msgid "The start- and end-time of the timeframe can not be the same. Please check the full-day checkbox if you want users to be able to book the full day."
msgstr ""
#. translators: %1$s = timeframe-ID, %2$s is timeframe post_title
-#: src/Model/Timeframe.php:655
+#: src/Model/Timeframe.php:731
#, php-format
msgid "Item is already bookable at another location within the same date range. See other timeframe ID: %1$s: %2$s"
msgstr ""
#. translators: first %s = timeframe-ID, second %s is timeframe post_title
-#: src/Model/Timeframe.php:691
+#: src/Model/Timeframe.php:767
#, php-format
msgid "See overlapping timeframe ID: %1$s %2$s"
msgstr ""
-#: src/Model/Timeframe.php:790
+#: src/Model/Timeframe.php:866
msgid "Overlapping bookable timeframes are only allowed to have the same grid."
msgstr ""
-#: src/Model/Timeframe.php:806
+#: src/Model/Timeframe.php:882
msgid "Daily repeated time periods are not allowed to overlap."
msgstr ""
-#: src/Model/Timeframe.php:820
+#: src/Model/Timeframe.php:896
msgid "Overlapping bookable timeframes are not allowed to have the same weekdays."
msgstr ""
-#: src/Model/Timeframe.php:832
+#: src/Model/Timeframe.php:908
msgid "Overlapping bookable timeframes are not allowed to have the same dates."
msgstr ""
-#: src/Model/Timeframe.php:839
-#: src/Model/Timeframe.php:846
+#: src/Model/Timeframe.php:915
+#: src/Model/Timeframe.php:922
msgid "The other timeframe is overlapping with your weekly configuration."
msgstr ""
@@ -1666,7 +1666,7 @@ msgstr ""
#: src/Plugin.php:720
#: src/Plugin.php:735
-#: src/Wordpress/CustomPostType/Booking.php:936
+#: src/Wordpress/CustomPostType/Booking.php:949
msgid "CommonsBooking Bookings"
msgstr ""
@@ -1992,14 +1992,14 @@ msgid ""
msgstr ""
#: src/View/Admin/Filter.php:58
-#: src/Wordpress/CustomPostType/Booking.php:779
+#: src/Wordpress/CustomPostType/Booking.php:792
#: src/Wordpress/CustomPostType/Restriction.php:451
#: src/Wordpress/CustomPostType/Timeframe.php:678
msgid "Start date"
msgstr ""
#: src/View/Admin/Filter.php:62
-#: src/Wordpress/CustomPostType/Booking.php:796
+#: src/Wordpress/CustomPostType/Booking.php:809
#: src/Wordpress/CustomPostType/Restriction.php:461
#: src/Wordpress/CustomPostType/Timeframe.php:702
msgid "End date"
@@ -2024,7 +2024,7 @@ msgstr ""
#: src/View/Booking.php:196
#: src/View/MassOperations.php:38
-#: src/Wordpress/CustomPostType/Booking.php:446
+#: src/Wordpress/CustomPostType/Booking.php:459
#: src/Wordpress/CustomPostType/Timeframe.php:97
#: templates/shortcode-bookings.php:74
msgid "User"
@@ -2032,7 +2032,7 @@ msgstr ""
#: src/View/Booking.php:200
#: src/View/MassOperations.php:42
-#: src/Wordpress/CustomPostType/Booking.php:973
+#: src/Wordpress/CustomPostType/Booking.php:986
#: src/Wordpress/CustomPostType/Restriction.php:477
msgid "Status"
msgstr ""
@@ -2041,8 +2041,8 @@ msgstr ""
msgid "Code"
msgstr ""
-#: src/View/Booking.php:581
-#: src/Wordpress/CustomPostType/Booking.php:864
+#: src/View/Booking.php:578
+#: src/Wordpress/CustomPostType/Booking.php:877
msgid "Submit booking"
msgstr ""
@@ -2135,22 +2135,22 @@ msgstr ""
msgid "Error sending booking codes"
msgstr ""
-#: src/View/BookingCodes.php:556
+#: src/View/BookingCodes.php:555
msgid "An unknown error occured"
msgstr ""
-#: src/View/BookingCodes.php:557
+#: src/View/BookingCodes.php:556
msgid "Email booking codes"
msgstr ""
-#: src/View/Calendar.php:177
+#: src/View/Calendar.php:181
msgid "No items found."
msgstr ""
-#: src/View/Calendar.php:233
-#: src/Wordpress/CustomPostType/Booking.php:447
-#: src/Wordpress/CustomPostType/Booking.php:761
-#: src/Wordpress/CustomPostType/Booking.php:989
+#: src/View/Calendar.php:237
+#: src/Wordpress/CustomPostType/Booking.php:460
+#: src/Wordpress/CustomPostType/Booking.php:774
+#: src/Wordpress/CustomPostType/Booking.php:1002
#: src/Wordpress/CustomPostType/Item.php:117
#: src/Wordpress/CustomPostType/Restriction.php:48
#: src/Wordpress/CustomPostType/Restriction.php:438
@@ -2159,10 +2159,10 @@ msgstr ""
msgid "Item"
msgstr ""
-#: src/View/Calendar.php:234
-#: src/Wordpress/CustomPostType/Booking.php:448
-#: src/Wordpress/CustomPostType/Booking.php:767
-#: src/Wordpress/CustomPostType/Booking.php:985
+#: src/View/Calendar.php:238
+#: src/Wordpress/CustomPostType/Booking.php:461
+#: src/Wordpress/CustomPostType/Booking.php:780
+#: src/Wordpress/CustomPostType/Booking.php:998
#: src/Wordpress/CustomPostType/Location.php:139
#: src/Wordpress/CustomPostType/Restriction.php:49
#: src/Wordpress/CustomPostType/Restriction.php:431
@@ -2362,150 +2362,154 @@ msgid "Your reservation has expired, please try to book again"
msgstr ""
#: src/Wordpress/CustomPostType/Booking.php:291
-#: src/Wordpress/CustomPostType/Booking.php:323
+#: src/Wordpress/CustomPostType/Booking.php:336
msgid "There is already a booking in this time-range. This notice may also appear if there is an unconfirmed booking in the requested period. Unconfirmed bookings are deleted after about 10 minutes. Please try again in a few minutes."
msgstr ""
-#: src/Wordpress/CustomPostType/Booking.php:307
+#: src/Wordpress/CustomPostType/Booking.php:304
+msgid "Invalid booking request. Please try again."
+msgstr ""
+
+#: src/Wordpress/CustomPostType/Booking.php:320
msgid "Booking canceled."
msgstr ""
-#: src/Wordpress/CustomPostType/Booking.php:331
-#: src/Wordpress/CustomPostType/Booking.php:524
-#: src/Wordpress/CustomPostType/Booking.php:712
+#: src/Wordpress/CustomPostType/Booking.php:344
+#: src/Wordpress/CustomPostType/Booking.php:537
+#: src/Wordpress/CustomPostType/Booking.php:725
#: src/Wordpress/CustomPostType/Timeframe.php:132
msgid "Booking"
msgstr ""
-#: src/Wordpress/CustomPostType/Booking.php:375
+#: src/Wordpress/CustomPostType/Booking.php:388
msgid "There was an error while saving the booking. Please try again. Thrown error:"
msgstr ""
-#: src/Wordpress/CustomPostType/Booking.php:392
+#: src/Wordpress/CustomPostType/Booking.php:405
msgid "There was an error while saving the booking. Please try again. Resulting WP_ERROR: "
msgstr ""
-#: src/Wordpress/CustomPostType/Booking.php:449
+#: src/Wordpress/CustomPostType/Booking.php:462
msgid "Bookingdate"
msgstr ""
-#: src/Wordpress/CustomPostType/Booking.php:450
+#: src/Wordpress/CustomPostType/Booking.php:463
#: src/Wordpress/CustomPostType/Restriction.php:50
#: src/Wordpress/CustomPostType/Timeframe.php:101
#: src/Wordpress/CustomPostType/Timeframe.php:758
msgid "Start Date"
msgstr ""
-#: src/Wordpress/CustomPostType/Booking.php:451
+#: src/Wordpress/CustomPostType/Booking.php:464
#: src/Wordpress/CustomPostType/Restriction.php:51
#: src/Wordpress/CustomPostType/Timeframe.php:102
msgid "End Date"
msgstr ""
-#: src/Wordpress/CustomPostType/Booking.php:452
+#: src/Wordpress/CustomPostType/Booking.php:465
msgid "Booking Status"
msgstr ""
-#: src/Wordpress/CustomPostType/Booking.php:453
-#: src/Wordpress/CustomPostType/Booking.php:981
+#: src/Wordpress/CustomPostType/Booking.php:466
+#: src/Wordpress/CustomPostType/Booking.php:994
#: src/Wordpress/CustomPostType/Timeframe.php:448
msgid "Comment"
msgstr ""
-#: src/Wordpress/CustomPostType/Booking.php:523
+#: src/Wordpress/CustomPostType/Booking.php:536
#: templates/dashboard-index.php:45
msgid "Bookings"
msgstr ""
-#: src/Wordpress/CustomPostType/Booking.php:525
+#: src/Wordpress/CustomPostType/Booking.php:538
#: src/Wordpress/CustomPostType/Item.php:118
#: src/Wordpress/CustomPostType/Location.php:140
#: src/Wordpress/CustomPostType/Restriction.php:307
-#: src/Wordpress/CustomPostType/Timeframe.php:1152
+#: src/Wordpress/CustomPostType/Timeframe.php:1153
msgid "Add new"
msgstr ""
-#: src/Wordpress/CustomPostType/Booking.php:526
-#: src/Wordpress/CustomPostType/Booking.php:528
+#: src/Wordpress/CustomPostType/Booking.php:539
+#: src/Wordpress/CustomPostType/Booking.php:541
msgid "Add new booking"
msgstr ""
-#: src/Wordpress/CustomPostType/Booking.php:527
-#: src/Wordpress/CustomPostType/Booking.php:743
+#: src/Wordpress/CustomPostType/Booking.php:540
+#: src/Wordpress/CustomPostType/Booking.php:756
msgid "Edit booking"
msgstr ""
-#: src/Wordpress/CustomPostType/Booking.php:529
+#: src/Wordpress/CustomPostType/Booking.php:542
msgid "Show booking"
msgstr ""
-#: src/Wordpress/CustomPostType/Booking.php:530
+#: src/Wordpress/CustomPostType/Booking.php:543
msgid "Show bookings"
msgstr ""
-#: src/Wordpress/CustomPostType/Booking.php:531
+#: src/Wordpress/CustomPostType/Booking.php:544
msgid "Search bookings"
msgstr ""
-#: src/Wordpress/CustomPostType/Booking.php:532
+#: src/Wordpress/CustomPostType/Booking.php:545
msgid "Bookings not found"
msgstr ""
-#: src/Wordpress/CustomPostType/Booking.php:533
+#: src/Wordpress/CustomPostType/Booking.php:546
msgid "No bookings found in trash"
msgstr ""
-#: src/Wordpress/CustomPostType/Booking.php:534
+#: src/Wordpress/CustomPostType/Booking.php:547
msgid "Parent bookings:"
msgstr ""
-#: src/Wordpress/CustomPostType/Booking.php:535
+#: src/Wordpress/CustomPostType/Booking.php:548
msgid "All bookings"
msgstr ""
-#: src/Wordpress/CustomPostType/Booking.php:536
-#: src/Wordpress/CustomPostType/Timeframe.php:1163
+#: src/Wordpress/CustomPostType/Booking.php:549
+#: src/Wordpress/CustomPostType/Timeframe.php:1164
msgid "Timeframe archive"
msgstr ""
-#: src/Wordpress/CustomPostType/Booking.php:537
-#: src/Wordpress/CustomPostType/Timeframe.php:1164
+#: src/Wordpress/CustomPostType/Booking.php:550
+#: src/Wordpress/CustomPostType/Timeframe.php:1165
msgid "Timeframe attributes"
msgstr ""
-#: src/Wordpress/CustomPostType/Booking.php:538
+#: src/Wordpress/CustomPostType/Booking.php:551
msgid "Add to booking"
msgstr ""
-#: src/Wordpress/CustomPostType/Booking.php:539
+#: src/Wordpress/CustomPostType/Booking.php:552
msgid "Added to booking"
msgstr ""
-#: src/Wordpress/CustomPostType/Booking.php:540
-#: src/Wordpress/CustomPostType/Timeframe.php:1167
+#: src/Wordpress/CustomPostType/Booking.php:553
+#: src/Wordpress/CustomPostType/Timeframe.php:1168
msgid "Timeframe image"
msgstr ""
-#: src/Wordpress/CustomPostType/Booking.php:541
+#: src/Wordpress/CustomPostType/Booking.php:554
msgid "set booking image"
msgstr ""
-#: src/Wordpress/CustomPostType/Booking.php:542
+#: src/Wordpress/CustomPostType/Booking.php:555
msgid "remove booking image"
msgstr ""
-#: src/Wordpress/CustomPostType/Booking.php:543
+#: src/Wordpress/CustomPostType/Booking.php:556
msgid "use as booking image"
msgstr ""
-#: src/Wordpress/CustomPostType/Booking.php:544
-#: src/Wordpress/CustomPostType/Timeframe.php:1150
-#: src/Wordpress/CustomPostType/Timeframe.php:1171
+#: src/Wordpress/CustomPostType/Booking.php:557
+#: src/Wordpress/CustomPostType/Timeframe.php:1151
+#: src/Wordpress/CustomPostType/Timeframe.php:1172
#: templates/dashboard-index.php:37
msgid "Timeframes"
msgstr ""
-#: src/Wordpress/CustomPostType/Booking.php:745
+#: src/Wordpress/CustomPostType/Booking.php:758
msgid ""
"
Notice
In this view, you as an admin can create or modify existing bookings. Please use it with caution. \n"
"\t\t\t\t
\n"
@@ -2513,107 +2517,107 @@ msgid ""
"
Click on the Submit booking button at the end of the page to submit a new booking.
' .
@@ -410,7 +410,7 @@ public static function renderTable( $timeframeId ) {
';
- if ( $timeframe->hasBookingCodes() ) {
+ if ( $timeframe->usesBookingCodes() ) {
echo self::renderTableFor( 'timeframe_form', $bookingCodes );
echo ' ';
@@ -547,7 +547,6 @@ function ( $action, $result ) use ( $timeframeId ) {
2
);
- // @phpstan-ignore deadCode.unreachable (false positive, covered by unit tests)
$booking_msg = new BookingCodesMessage( $timeframeId, 'codes', $tsFrom, $tsTo );
$booking_msg->sendMessage();
diff --git a/src/View/Calendar.php b/src/View/Calendar.php
index dba371928..b04233f26 100644
--- a/src/View/Calendar.php
+++ b/src/View/Calendar.php
@@ -147,7 +147,11 @@ public static function renderTable( $atts ): string {
$locations = [];
foreach ( $timeframes as $timeframe ) {
// TODO #507
- $locations[ $timeframe->getLocationID() ] = $timeframe->getLocation()->post_title;
+ $location = $timeframe->getLocation();
+ if ( ! $location ) {
+ continue;
+ }
+ $locations[ $location->ID ] = $location->post_title;
}
// loop through location
@@ -407,7 +411,21 @@ public static function getCalendarDataArray( $item, $location, string $startDate
}
}
- return self::prepareJsonResponse( $startDate, $endDate, [ $location ], [ $item ], $advanceBookingDays, $lastBookableDate, $firstBookableDay );
+ $calendarData = self::prepareJsonResponse( $startDate, $endDate, [ $location ], [ $item ], $advanceBookingDays, $lastBookableDate, $firstBookableDay );
+
+ /**
+ * Filters the calendar data array before it is rendered or returned as JSON.
+ *
+ * This is the payload that drives the frontend booking calendar (and its
+ * AJAX endpoint), so integrations can adjust the data shown to users.
+ *
+ * @since 2.11.0
+ *
+ * @param array $calendarData The prepared calendar data.
+ * @param mixed $item The item (post ID or WP_Post) the calendar is for.
+ * @param mixed $location The location (post ID or WP_Post) the calendar is for.
+ */
+ return apply_filters( 'commonsbooking_calendar_data', $calendarData, $item, $location );
}
/**
diff --git a/src/Wordpress/CustomPostType/Booking.php b/src/Wordpress/CustomPostType/Booking.php
index 2d92d7e77..9cd5e15c0 100644
--- a/src/Wordpress/CustomPostType/Booking.php
+++ b/src/Wordpress/CustomPostType/Booking.php
@@ -291,6 +291,19 @@ public static function handleBookingRequest(
throw new BookingDeniedException( __( 'There is already a booking in this time-range. This notice may also appear if there is an unconfirmed booking in the requested period. Unconfirmed bookings are deleted after about 10 minutes. Please try again in a few minutes.', 'commonsbooking' ) );
}
+ // The initial calendar flow always submits post_status=unconfirmed without a post_ID.
+ // If an exact-slot booking already exists for the current user, redirect to that booking
+ // instead of downgrading it back to unconfirmed via the update path below.
+ if ( $booking && $post_status === 'unconfirmed' && $post_ID === null ) {
+ return $booking->ID;
+ }
+
+ // Frontend requests should never reopen an existing booking as unconfirmed by ID.
+ // Treat this as an invalid request instead of mutating the current booking state.
+ if ( $post_status === 'unconfirmed' && $post_ID !== null ) {
+ throw new BookingDeniedException( __( 'Invalid booking request. Please try again.', 'commonsbooking' ) );
+ }
+
$existingBookings =
\CommonsBooking\Repository\Booking::getExistingBookings(
$itemId,
@@ -316,7 +329,7 @@ public static function handleBookingRequest(
array_values( $existingBookings )[0]->getPost()->post_name === $requestedPostName &&
intval( array_values( $existingBookings )[0]->getPost()->post_author ) === get_current_user_id();
- if ( ( ! $isEdit || count( $existingBookings ) > 1 ) && $post_status !== 'canceled' ) {
+ if ( ! $isEdit && $post_status !== 'canceled' ) {
if ( $booking ) {
$post_status = 'unconfirmed';
} else {
@@ -342,6 +355,7 @@ public static function handleBookingRequest(
'type' => Timeframe::BOOKING_ID,
);
+ $postarr = self::filterBookingBeforeSave( $postarr, null );
$postId = wp_insert_post( $postarr, true );
$needsValidation = true;
@@ -351,7 +365,8 @@ public static function handleBookingRequest(
if ( $postarr['post_status'] === 'canceled' ) {
$postarr['meta_input']['cancellation_time'] = current_time( 'timestamp' );
}
- $postId = wp_update_post( $postarr );
+ $postarr = self::filterBookingBeforeSave( $postarr, $booking );
+ $postId = wp_update_post( $postarr );
// we check if this is an already denied booking and demand validation again
if ( $postarr['post_status'] == 'unconfirmed' ) {
@@ -397,6 +412,25 @@ public static function handleBookingRequest(
return $postId;
}
+ /**
+ * Applies the `commonsbooking_booking_before_save` filter to the post array
+ * that is about to be inserted or updated for a booking.
+ *
+ * Lets integrations adjust or add meta data before a booking is persisted
+ * (e.g. attach an external reference or default values). Return the modified
+ * `$postarr` (a wp_insert_post()/wp_update_post() array).
+ *
+ * @since 2.11.0
+ *
+ * @param array $postarr The post array to be saved.
+ * @param \CommonsBooking\Model\Booking|null $booking The existing booking being updated, or null for a new booking.
+ *
+ * @return array
+ */
+ private static function filterBookingBeforeSave( array $postarr, ?\CommonsBooking\Model\Booking $booking ): array {
+ return apply_filters( 'commonsbooking_booking_before_save', $postarr, $booking );
+ }
+
/**
* Multi grid size
* We need to save the grid size for timeframes with full slot grid.
@@ -749,7 +783,7 @@ protected function getCustomFields() {
Click on the Submit booking button at the end of the page to submit a new booking.
Please note: Only a few basic checks against existing bookings are performed. Please be wary of overlapping bookings.
-
+
',
'commonsbooking'
) . '
'
@@ -890,7 +924,7 @@ public function displayBookingsAdminListNotice() {
As an admin you can create bookings via this admin interface. Please be aware that admin bookings are not validated
and checked. Use this function with care.
Click on preview to show booking details in frontend
- To search and filter bookings please integrate the frontend booking list via shortcode.
+ To search and filter bookings please integrate the frontend booking list via shortcode.
See here %1$sHow to display the booking list%2$s',
'commonsbooking'
),
diff --git a/src/Wordpress/CustomPostType/Item.php b/src/Wordpress/CustomPostType/Item.php
index d29cb0eae..cea8b3d12 100644
--- a/src/Wordpress/CustomPostType/Item.php
+++ b/src/Wordpress/CustomPostType/Item.php
@@ -280,6 +280,16 @@ public function registerMetabox() {
'show_on_cb' => 'cmb2_hide_if_no_cats', // function should return a bool value
)
);
+
+ // checkbox if item should be excluded from API shares
+ $cmb->add_field(
+ array(
+ 'name' => esc_html__( 'Exclude from API', 'commonsbooking' ),
+ 'desc' => esc_html__( 'When this box is checked, the item will not appear in any of the API shares.', 'commonsbooking' ),
+ 'id' => COMMONSBOOKING_METABOX_PREFIX . 'api_exclude',
+ 'type' => 'checkbox',
+ )
+ );
}
// Check if custom meta fields are set in CB Options and generate MetaData-Box and fields
diff --git a/src/Wordpress/CustomPostType/Timeframe.php b/src/Wordpress/CustomPostType/Timeframe.php
index d20f8571f..b6bc88ac4 100644
--- a/src/Wordpress/CustomPostType/Timeframe.php
+++ b/src/Wordpress/CustomPostType/Timeframe.php
@@ -1115,7 +1115,7 @@ public static function removeIrrelevantPostmeta( \CommonsBooking\Model\Timeframe
\CommonsBooking\Model\Timeframe::META_SHOW_BOOKING_CODES,
];
// remove multi-select postmeta if not relevant (#507)
- $onlyRelevantForHolidays = [
+ $onlyRelevantForHolidaysOrRepair = [
\CommonsBooking\Model\Timeframe::META_ITEM_ID_LIST,
\CommonsBooking\Model\Timeframe::META_LOCATION_ID_LIST,
\CommonsBooking\Model\Timeframe::META_ITEM_CATEGORY_IDS,
@@ -1124,14 +1124,15 @@ public static function removeIrrelevantPostmeta( \CommonsBooking\Model\Timeframe
\CommonsBooking\Model\Timeframe::META_LOCATION_SELECTION_TYPE,
];
- if ( $timeframe->getType() != self::BOOKABLE_ID ) {
+ $type = $timeframe->getType();
+ if ( $type !== self::BOOKABLE_ID ) {
foreach ( $onlyRelevantForBookable as $metaKey ) {
delete_post_meta( $timeframe->ID, $metaKey );
}
}
- if ( $timeframe->getType() != self::HOLIDAYS_ID ) {
- foreach ( $onlyRelevantForHolidays as $metaKey ) {
+ if ( ! ( $type === self::HOLIDAYS_ID || $type === self::REPAIR_ID ) ) {
+ foreach ( $onlyRelevantForHolidaysOrRepair as $metaKey ) {
delete_post_meta( $timeframe->ID, $metaKey );
}
// reset to manual selection
diff --git a/tests/cypress/e2e/02-litepicker-overbooking.cy.js b/tests/cypress/e2e/02-litepicker-overbooking.cy.js
index e1d975b6e..ff19df689 100644
--- a/tests/cypress/e2e/02-litepicker-overbooking.cy.js
+++ b/tests/cypress/e2e/02-litepicker-overbooking.cy.js
@@ -18,7 +18,7 @@ describe('test overbooking process', () => {
function updatePostMetaAndReload(postID, metaKey, metaValue) {
cy.exec(
- 'npx wp-env run tests-cli wp post meta update ' +
+ 'npx wp-env --config=.wp-env.test.json run cli wp post meta update ' +
postID +
' ' +
metaKey +
diff --git a/tests/cypress/wordpress-files/e2e-override.json b/tests/cypress/wordpress-files/e2e-override.json
index 8127d5ca2..7b08cdb57 100644
--- a/tests/cypress/wordpress-files/e2e-override.json
+++ b/tests/cypress/wordpress-files/e2e-override.json
@@ -8,12 +8,8 @@
"themes": [
"flegfleg/kasimir-theme"
],
- "env": {
- "tests": {
- "mappings": {
- "wp-content/plugins/commonsbooking": "./build/commonsbooking",
- "wp-content/plugins/commonsbooking/tests/cypress/wordpress-files": "./tests/cypress/wordpress-files"
- }
- }
+ "mappings": {
+ "wp-content/plugins/commonsbooking": "./build/commonsbooking",
+ "wp-content/plugins/commonsbooking/tests/cypress/wordpress-files": "./tests/cypress/wordpress-files"
}
}
diff --git a/tests/php/API/AvailabilityRouteTest.php b/tests/php/API/AvailabilityRouteTest.php
index 475ee313f..5f71839e1 100644
--- a/tests/php/API/AvailabilityRouteTest.php
+++ b/tests/php/API/AvailabilityRouteTest.php
@@ -43,11 +43,37 @@ public function testsAvailabilitySuccess() {
$this->assertSame( 200, $response->get_status() );
$this->assertSame( 2, count( $response->get_data()->availability ) );
+ $availabilityStart = new \DateTime( self::CURRENT_DATE );
+ $availabilityEnd = new \DateTime( self::CURRENT_DATE );
+ $availabilityEnd->modify( '23:59:59' );
+
// Checks availability for the first day
$this->assertEquals( $this->locationId, $response->get_data()->availability[0]->locationId );
$this->assertEquals( $this->itemId, $response->get_data()->availability[0]->itemId );
- $this->assertEquals( self::CURRENT_DATE . 'T00:00:00+00:00', $response->get_data()->availability[0]->start );
- $this->assertEquals( self::CURRENT_DATE . 'T23:59:59+00:00', $response->get_data()->availability[0]->end );
+ $this->assertEquals( $availabilityStart->format( 'c' ), $response->get_data()->availability[0]->start );
+ $this->assertEquals( $availabilityEnd->format( 'c' ), $response->get_data()->availability[0]->end );
+
+ ClockMock::reset();
+ }
+
+ public function testApiAvailabilityResponseFilter() {
+ ClockMock::freeze( new \DateTime( self::CURRENT_DATE ) );
+
+ // Default: the item has availability slots.
+ $this->assertNotEmpty( \CommonsBooking\API\AvailabilityRoute::getItemData( $this->itemId ) );
+
+ $received = 'unset';
+ $filter = function ( array $slots, $id ) use ( &$received ) {
+ $received = $id;
+ return array(); // clear availability
+ };
+
+ add_filter( 'commonsbooking_api_availability_response', $filter, 10, 2 );
+ $slots = \CommonsBooking\API\AvailabilityRoute::getItemData( $this->itemId );
+ remove_filter( 'commonsbooking_api_availability_response', $filter, 10 );
+
+ $this->assertSame( array(), $slots );
+ $this->assertEquals( $this->itemId, $received );
ClockMock::reset();
}
diff --git a/tests/php/API/CB_REST_Route_UnitTestCase.php b/tests/php/API/CB_REST_Route_UnitTestCase.php
index 8292e463b..02ab5b103 100644
--- a/tests/php/API/CB_REST_Route_UnitTestCase.php
+++ b/tests/php/API/CB_REST_Route_UnitTestCase.php
@@ -3,6 +3,7 @@
namespace CommonsBooking\Tests\API;
use CommonsBooking\Tests\CPTCreationTrait;
+use CommonsBooking\Tests\Wordpress\CustomPostTypeTest;
/**
* Abstract test case which implicitly tests REST Routes
@@ -12,7 +13,7 @@ class CB_REST_Route_UnitTestCase extends CB_REST_UnitTestCase {
use CPTCreationTrait;
- const CURRENT_DATE = '2021-05-21';
+ const CURRENT_DATE = CustomPostTypeTest::CURRENT_DATE;
protected $ENDPOINT;
diff --git a/tests/php/API/GBFS/DiscoveryRouteTest.php b/tests/php/API/GBFS/DiscoveryRouteTest.php
index dea527f80..28a9cb075 100644
--- a/tests/php/API/GBFS/DiscoveryRouteTest.php
+++ b/tests/php/API/GBFS/DiscoveryRouteTest.php
@@ -4,10 +4,25 @@
use CommonsBooking\Tests\API\CB_REST_Route_UnitTestCase;
-/**
- * TODO: add result unit test
- */
class DiscoveryRouteTest extends CB_REST_Route_UnitTestCase {
protected $ENDPOINT = '/commonsbooking/v1/gbfs.json';
+
+ public function testRoute() {
+ $request = new \WP_REST_Request( 'GET', $this->ENDPOINT );
+ $response = rest_do_request( $request );
+
+ $this->assertSame( 200, $response->get_status() );
+ $data = $response->get_data()->data;
+ $this->assertNotEmpty( $data->feeds );
+ $this->assertIsArray( $data->feeds );
+
+ $header = $response->get_data(); // the top level info that is always present
+
+ $this->assertNotFalse( \DateTime::createFromFormat( \DateTime::ATOM, $header->last_updated ) );
+ $this->assertIsInt( $header->ttl );
+ $this->assertIsString( $header->version );
+
+ parent::testRoute();
+ }
}
diff --git a/tests/php/API/GBFS/StationInformationRouteTest.php b/tests/php/API/GBFS/StationInformationRouteTest.php
index fd9bd65d8..465af31fd 100644
--- a/tests/php/API/GBFS/StationInformationRouteTest.php
+++ b/tests/php/API/GBFS/StationInformationRouteTest.php
@@ -3,11 +3,61 @@
namespace CommonsBooking\Tests\API\GBFS;
use CommonsBooking\Tests\API\CB_REST_Route_UnitTestCase;
+use CommonsBooking\Tests\Helper\GeoHelperTest;
+use WP_REST_Request;
-/**
- * TODO: add result unit test
- */
class StationInformationRouteTest extends CB_REST_Route_UnitTestCase {
protected $ENDPOINT = '/commonsbooking/v1/station_information.json';
+
+ public function testBasicStationInformation_withLatLonMeta() {
+ update_post_meta( $this->locationId, 'geo_latitude', '50.123' );
+ update_post_meta( $this->locationId, 'geo_longitude', '8.123' );
+
+ $request = new WP_REST_Request( 'GET', $this->ENDPOINT );
+ $response = rest_do_request( $request );
+
+ $this->assertSame( 200, $response->get_status() );
+
+ $data = $response->get_data()->data;
+ $this->assertNotEmpty( $data->stations );
+ $this->assertCount( 1, $data->stations );
+
+ $station = $data->stations[0];
+
+ $this->assertEquals( (string) $this->locationId, $station->station_id );
+ $this->assertNotEmpty( $station->name );
+ $this->assertNotEmpty( $station->rental_uris->web );
+
+ $this->assertEquals( 50.123, $station->lat );
+ $this->assertEquals( 8.123, $station->lon );
+ }
+
+ // no gps data and no address defined, skip location
+ public function testStationInformation_geocodingNonExistent() {
+ delete_post_meta( $this->locationId, 'geo_latitude' );
+ delete_post_meta( $this->locationId, 'geo_longitude' );
+
+ $mockedLocationCoordinates = GeoHelperTest::mockedLocation()->getCoordinates();
+
+ $request = new WP_REST_Request( 'GET', $this->ENDPOINT );
+ $response = rest_do_request( $request );
+
+ $data = $response->get_data()->data;
+ $this->assertEmpty( $data->stations );
+ }
+
+ public function setUp(): void {
+ parent::setUp();
+
+ $this->locationId = $this->createLocation( 'Test Location', 'publish' );
+ $this->itemId = $this->createItem( 'TestItem', 'publish' );
+
+ $this->createTimeframe(
+ $this->locationId,
+ $this->itemId,
+ strtotime( '-1 day', strtotime( self::CURRENT_DATE ) ),
+ strtotime( '+10 days', strtotime( self::CURRENT_DATE ) )
+ );
+ }
}
diff --git a/tests/php/API/GBFS/StationStatusRouteTest.php b/tests/php/API/GBFS/StationStatusRouteTest.php
index 12fb49807..6d59a174e 100644
--- a/tests/php/API/GBFS/StationStatusRouteTest.php
+++ b/tests/php/API/GBFS/StationStatusRouteTest.php
@@ -2,12 +2,161 @@
namespace CommonsBooking\Tests\API\GBFS;
+use CommonsBooking\Model\Restriction;
use CommonsBooking\Tests\API\CB_REST_Route_UnitTestCase;
+use CommonsBooking\Tests\Wordpress\CustomPostTypeTest;
+use SlopeIt\ClockMock\ClockMock;
-/**
- * TODO: add result unit test
- */
class StationStatusRouteTest extends CB_REST_Route_UnitTestCase {
protected $ENDPOINT = '/commonsbooking/v1/station_status.json';
+ public function testBasicStationStatus() {
+ $request = new \WP_REST_Request( 'GET', $this->ENDPOINT );
+ $response = rest_do_request( $request );
+
+ $this->assertSame( 200, $response->get_status() );
+
+ $data = $response->get_data()->data;
+
+ $this->assertNotEmpty( $data->stations );
+ $this->assertCount( 1, $data->stations );
+
+ $station = $data->stations[0];
+
+ $this->assertEquals( (string) $this->locationId, $station->station_id );
+ $this->assertEquals( 1, $station->num_vehicles_available );
+ $this->assertTrue( $station->is_installed );
+ $this->assertTrue( $station->is_renting );
+ $this->assertTrue( $station->is_returning );
+
+ $reported = new \DateTime( $station->last_reported );
+ $now = new \DateTime( self::CURRENT_DATE );
+
+ $this->assertEqualsWithDelta( $now->getTimestamp(), $reported->getTimestamp(), 1.0 );
+ }
+
+ public function testStationStatus_whenBooked_isEmpty() {
+ $booking = $this->createConfirmedBookingStartingToday();
+
+ $request = new \WP_REST_Request( 'GET', $this->ENDPOINT );
+ $response = rest_do_request( $request );
+
+ $data = $response->get_data()->data;
+ $station = $data->stations[0];
+
+ $this->assertEquals( 0, $station->num_vehicles_available );
+ }
+
+ public function testStationStatus_afterTimeframeEnd_isEmpty() {
+ $future = new \DateTime( self::CURRENT_DATE );
+ $future->modify( '+11 days' );
+ ClockMock::freeze( $future );
+
+ $request = new \WP_REST_Request( 'GET', $this->ENDPOINT );
+ $response = rest_do_request( $request );
+
+ $data = $response->get_data()->data;
+ $station = $data->stations[0];
+
+ $this->assertEquals( 0, $station->num_vehicles_available );
+ }
+
+ public function testStationStatus_withBookingOffset() {
+ $otherLocationId = $this->createLocation( 'Other Location', );
+ $otherItemId = $this->createItem( 'Other Item', );
+
+ $timeframeID = $this->createTimeframe(
+ $otherLocationId,
+ $otherItemId,
+ strtotime( '-1 day', strtotime( self::CURRENT_DATE ) ),
+ strtotime( '+10 days', strtotime( self::CURRENT_DATE ) ),
+ \CommonsBooking\Wordpress\CustomPostType\Timeframe::BOOKABLE_ID,
+ 'on',
+ 'd',
+ 0,
+ '8:00 AM',
+ '12:00 PM',
+ 'publish',
+ [],
+ '',
+ CustomPostTypeTest::USER_ID,
+ 3,
+ 30,
+ 2
+ );
+
+ // with offset → still counted as available because it is not booked
+ $request = new \WP_REST_Request( 'GET', $this->ENDPOINT );
+ $response = rest_do_request( $request );
+ $data = $response->get_data()->data;
+
+ $relevantStation = current(
+ array_filter(
+ $data->stations,
+ function ( $station ) use ( $otherLocationId ) {
+ return $station->station_id === (string) $otherLocationId;
+ }
+ )
+ );
+ $this->assertEquals( 1, $relevantStation->num_vehicles_available );
+
+ // remove offset → still available
+ update_post_meta( $timeframeID, \CommonsBooking\Model\Timeframe::META_BOOKING_START_DAY_OFFSET, 0 );
+
+ $response = rest_do_request( $request );
+ $data = $response->get_data()->data;
+ $relevantStation = current(
+ array_filter(
+ $data->stations,
+ function ( $station ) use ( $otherLocationId ) {
+ return $station->station_id === (string) $otherLocationId;
+ }
+ )
+ );
+ $this->assertEquals( 1, $relevantStation->num_vehicles_available );
+ }
+ public function testStationStatusWithRestriction() {
+ $this->createRestriction(
+ Restriction::TYPE_REPAIR,
+ $this->locationId,
+ $this->itemId,
+ strtotime( '-1 day', strtotime( self::CURRENT_DATE ) ),
+ strtotime( '+1 day', strtotime( self::CURRENT_DATE ) ),
+ );
+ $request = new \WP_REST_Request( 'GET', $this->ENDPOINT );
+ $response = rest_do_request( $request );
+ $data = $response->get_data()->data;
+ $station = $data->stations[0];
+ $this->assertEquals( 0, $station->num_vehicles_available );
+ }
+
+ public function testNotCountedWhenItemExcluded() {
+ update_post_meta( $this->itemId, COMMONSBOOKING_METABOX_PREFIX . 'api_exclude', 'on' );
+ $request = new \WP_REST_Request( 'GET', $this->ENDPOINT );
+ $response = rest_do_request( $request );
+ $data = $response->get_data()->data;
+ $station = $data->stations[0];
+ $this->assertEquals( 0, $station->num_vehicles_available );
+ }
+
+ public function setUp(): void {
+ parent::setUp();
+
+ ClockMock::freeze( new \DateTime( self::CURRENT_DATE ) );
+
+ $this->locationId = $this->createLocation( 'Testlocation', 'publish' );
+ $this->itemId = $this->createItem( 'TestItem', 'publish' );
+
+ $this->createTimeframe(
+ $this->locationId,
+ $this->itemId,
+ strtotime( '-1 day', strtotime( self::CURRENT_DATE ) ),
+ strtotime( '+10 days', strtotime( self::CURRENT_DATE ) )
+ );
+ }
+
+ public function tearDown(): void {
+ ClockMock::reset();
+ parent::tearDown();
+ }
}
diff --git a/tests/php/API/GBFS/StationStatusTest.php b/tests/php/API/GBFS/StationStatusTest.php
deleted file mode 100644
index 9bac7d5f3..000000000
--- a/tests/php/API/GBFS/StationStatusTest.php
+++ /dev/null
@@ -1,124 +0,0 @@
-locationId );
- ClockMock::freeze( $currDate );
- $routeObject = new StationStatus();
- $spanningTimeframe = $this->createTimeframe(
- $this->locationId,
- $this->itemId,
- strtotime( '-1 day', strtotime( self::CURRENT_DATE ) ),
- strtotime( '+10 days', strtotime( self::CURRENT_DATE ) )
- );
- $stationStatus = $routeObject->prepare_item_for_response( $locationObject, null )->get_data();
- $this->assertEquals( $this->locationId, $stationStatus->station_id );
- $this->assertEquals( 1, $stationStatus->num_vehicles_available );
- $this->assertTrue( $stationStatus->is_installed );
- $this->assertTrue( $stationStatus->is_renting );
- $this->assertTrue( $stationStatus->is_returning );
- $this->assertEquals( date( 'c' ), $stationStatus->last_reported );
-
- // now let's book the current day and check, that the station is empty
- $this->createConfirmedBookingStartingToday();
- $this->assertEquals( 0, $routeObject->prepare_item_for_response( $locationObject, null )->get_data()->num_vehicles_available );
-
- // the timeframe has ended now, so the station should be empty
- $currDate->modify( '+11 days' );
- ClockMock::freeze( $currDate );
- $this->assertEquals( 0, $routeObject->prepare_item_for_response( $locationObject, null )->get_data()->num_vehicles_available );
-
- // very important for GBFS: when bookings are only allowed with a certain offset (time difference between booking and start of booking), the station should be empty
- ClockMock::freeze( new \DateTime( self::CURRENT_DATE ) );
- $otherLocationId = $this->createLocation( 'Other Location', 'publish' );
- $otherItemId = $this->createItem( 'Other Item', 'publish' );
- $timeframeID = $this->createTimeframe(
- $otherLocationId,
- $otherItemId,
- strtotime( '-1 day', strtotime( self::CURRENT_DATE ) ),
- strtotime( '+10 days', strtotime( self::CURRENT_DATE ) ),
- \CommonsBooking\Wordpress\CustomPostType\Timeframe::BOOKABLE_ID,
- 'on',
- 'd',
- 0,
- '8:00 AM',
- '12:00 PM',
- 'publish',
- [],
- '',
- self::USER_ID,
- 3,
- 30,
- 2
- );
- $this->assertEquals( 0, $routeObject->prepare_item_for_response( new Location( $otherLocationId ), null )->get_data()->num_vehicles_available );
- // remove the offset and the station should have the item
- update_post_meta( $timeframeID, 'booking-startday-offset', 0 );
- $this->assertEquals( 1, $routeObject->prepare_item_for_response( new Location( $otherLocationId ), null )->get_data()->num_vehicles_available );
- }
-
- public function testPrepare_item_for_response_hourly() {
- $currDate = new \DateTime( self::CURRENT_DATE );
- $currDate->setTime( 8, 0, 0 );
- $locationObject = new Location( $this->locationId );
- ClockMock::freeze( $currDate );
- $routeObject = new StationStatus();
- $hourlyTimeframe = $this->createTimeframe(
- $this->locationId,
- $this->itemId,
- strtotime( '-1 day', strtotime( self::CURRENT_DATE ) ),
- strtotime( '+2 days', strtotime( self::CURRENT_DATE ) ),
- \CommonsBooking\Wordpress\CustomPostType\Timeframe::BOOKABLE_ID,
- 'off',
- 'd',
- 1,
- '8:00 AM',
- '11:59 PM'
- );
- $this->assertEquals( 1, $routeObject->prepare_item_for_response( $locationObject, null )->get_data()->num_vehicles_available );
-
- // before 08:00AM the bike is not available
- $currDate->setTime( 6, 0, 0 );
- ClockMock::freeze( $currDate );
- $this->assertEquals( 0, $routeObject->prepare_item_for_response( $locationObject, null )->get_data()->num_vehicles_available );
-
- // now let's book two hours out of the timeframe and check that the station is empty for those two hours
- $startBooking = new \DateTime( self::CURRENT_DATE );
- $startBooking->setTime( 10, 0, 0 );
- $endBooking = clone $startBooking;
- $endBooking->setTime( 13, 0, 0 );
- $this->createBooking(
- $this->locationId,
- $this->itemId,
- strtotime( '10:00 AM', strtotime( self::CURRENT_DATE ) ),
- strtotime( '01:00 PM', strtotime( self::CURRENT_DATE ) ),
- '10:00 AM',
- '01:00 PM'
- );
- ClockMock::freeze( $startBooking );
- $this->assertEquals( 0, $routeObject->prepare_item_for_response( $locationObject, null )->get_data()->num_vehicles_available );
- $startBooking->modify( '+1 hour' );
- ClockMock::freeze( $startBooking );
- $this->assertEquals( 0, $routeObject->prepare_item_for_response( $locationObject, null )->get_data()->num_vehicles_available );
- ClockMock::freeze( $endBooking );
- $this->assertEquals( 1, $routeObject->prepare_item_for_response( $locationObject, null )->get_data()->num_vehicles_available );
- }
-
- protected function setUp(): void {
- parent::setUp();
- }
-
- protected function tearDown(): void {
- parent::tearDown();
- }
-}
diff --git a/tests/php/API/GBFS/SystemInformationRouteTest.php b/tests/php/API/GBFS/SystemInformationRouteTest.php
index 0dc4b8b26..bd8c4fb12 100644
--- a/tests/php/API/GBFS/SystemInformationRouteTest.php
+++ b/tests/php/API/GBFS/SystemInformationRouteTest.php
@@ -3,11 +3,54 @@
namespace CommonsBooking\Tests\API\GBFS;
use CommonsBooking\Tests\API\CB_REST_Route_UnitTestCase;
+use SlopeIt\ClockMock\ClockMock;
-/**
- * TODO: add result unit test
- */
class SystemInformationRouteTest extends CB_REST_Route_UnitTestCase {
protected $ENDPOINT = '/commonsbooking/v1/system_information.json';
+
+ public function testRoute() {
+ $request = new \WP_REST_Request( 'GET', $this->ENDPOINT );
+ $response = rest_do_request( $request );
+
+ $data = $response->get_data()->data;
+ $this->assertNotEmpty( $data->name );
+ $this->assertNotEmpty( $data->system_id );
+
+ $header = $response->get_data(); // the top level info that is always present
+
+ $this->assertNotFalse( \DateTime::createFromFormat( \DateTime::ATOM, $header->last_updated ) );
+ $this->assertIsInt( $header->ttl );
+ $this->assertIsString( $header->version );
+ parent::testRoute();
+ }
+
+ public function testOpeningHours() {
+ // no timeframe setup, meaning no items to book -> closed
+ ClockMock::freeze( new \DateTime( self::CURRENT_DATE ) );
+ $request = new \WP_REST_Request( 'GET', $this->ENDPOINT );
+ $response = rest_do_request( $request );
+ $data = $response->get_data()->data;
+ $this->assertEquals( '24/7 closed', $data->opening_hours );
+
+ // with timeframe, is open
+ $this->createBookableTimeFrameIncludingCurrentDay();
+ $request = new \WP_REST_Request( 'GET', $this->ENDPOINT );
+ $response = rest_do_request( $request );
+ $data = $response->get_data()->data;
+ $this->assertEquals( '24/7', $data->opening_hours );
+
+ // outside of the timeframe, closed again
+ $inAWeek = new \DateTime( self::CURRENT_DATE );
+ $inAWeek->modify( '+1 week' );
+ ClockMock::freeze( $inAWeek );
+ $request = new \WP_REST_Request( 'GET', $this->ENDPOINT );
+ $response = rest_do_request( $request );
+ $data = $response->get_data()->data;
+ $this->assertEquals( '24/7 closed', $data->opening_hours );
+ }
+
+ public function setUp(): void {
+ parent::setUp();
+ }
}
diff --git a/tests/php/API/GBFS/VehicleAvailabilityRouteTest.php b/tests/php/API/GBFS/VehicleAvailabilityRouteTest.php
new file mode 100644
index 000000000..7f8bd7a05
--- /dev/null
+++ b/tests/php/API/GBFS/VehicleAvailabilityRouteTest.php
@@ -0,0 +1,173 @@
+ENDPOINT );
+ $response = rest_do_request( $request );
+
+ $this->assertSame( 200, $response->get_status() );
+
+ $data = $response->get_data()->data;
+ $this->assertNotEmpty( $data->vehicles );
+ $this->assertCount( 1, $data->vehicles );
+ $availabilities = $data->vehicles[0]->availabilities;
+ $this->assertCount( 1, $availabilities );
+
+ $startDT = new \DateTime( $availabilities[0]->from );
+ $today = new \DateTime( self::CURRENT_DATE );
+
+ $this->assertEqualsWithDelta( $today->getTimestamp(), $startDT->getTimestamp(), 1.0 );
+ }
+
+ public function testAvailabilityWithOffset() {
+ ClockMock::freeze( new \DateTime( self::CURRENT_DATE ) );
+
+ // delete other timeframe so it doesn't mess with our tests
+ wp_delete_post( $this->timeframe, true );
+ $otherLocationId = $this->createLocation( 'Other Location', );
+ $otherItemId = $this->createItem( 'Other Item', );
+
+ $timeframeID = $this->createTimeframe(
+ $otherLocationId,
+ $otherItemId,
+ strtotime( '-1 day', strtotime( self::CURRENT_DATE ) ),
+ strtotime( '+10 days', strtotime( self::CURRENT_DATE ) ),
+ \CommonsBooking\Wordpress\CustomPostType\Timeframe::BOOKABLE_ID,
+ 'on',
+ 'd',
+ 0,
+ '8:00 AM',
+ '12:00 PM',
+ 'publish',
+ [],
+ '',
+ CustomPostTypeTest::USER_ID,
+ 3,
+ 30,
+ 2
+ );
+
+ // with offset → not available right now
+ $request = new \WP_REST_Request( 'GET', $this->ENDPOINT );
+ $response = rest_do_request( $request );
+ $data = $response->get_data()->data;
+
+ $availabilities = $data->vehicles[0]->availabilities;
+
+ $startDT = new \DateTime( $availabilities[0]->from );
+ $today = new \DateTime( self::CURRENT_DATE );
+
+ $this->assertNotEqualsWithDelta( $today->getTimestamp(), $startDT->getTimestamp(), 1.0 );
+
+ // remove offset → now available today
+ update_post_meta( $timeframeID, \CommonsBooking\Model\Timeframe::META_BOOKING_START_DAY_OFFSET, 0 );
+
+ $response = rest_do_request( $request );
+ $data = $response->get_data()->data;
+ $availabilities = $data->vehicles[0]->availabilities;
+ $this->assertCount( 1, $availabilities );
+
+ $startDT = new \DateTime( $availabilities[0]->from );
+ $today = new \DateTime( self::CURRENT_DATE );
+
+ $this->assertEqualsWithDelta( $today->getTimestamp(), $startDT->getTimestamp(), 1.0 );
+ }
+
+ public function testHourlyAvailability() {
+ delete_post_meta( $this->timeframe, 'full-day', 'on' );
+ update_post_meta( $this->timeframe, 'grid', 1 ); // hourly grid
+ update_post_meta( $this->timeframe, 'start-time', '08:00 AM' );
+ update_post_meta( $this->timeframe, 'end-time', '01:00 PM' );
+
+ $startDT = new \DateTime();
+ $startDT->modify( '08:00 AM' );
+ $endDT = new \DateTime();
+ $endDT->modify( '01:00 PM' );
+ $endDT->modify( '-1 second' ); // timeframes always have one second cut
+
+ $request = new \WP_REST_Request( 'GET', $this->ENDPOINT );
+ $response = rest_do_request( $request );
+ $this->assertSame( 200, $response->get_status() );
+
+ $data = $response->get_data()->data;
+ $this->assertNotEmpty( $data->vehicles );
+ $this->assertCount( 1, $data->vehicles );
+ $availabilities = $data->vehicles[0]->availabilities;
+ $this->assertCount( 2, $availabilities ); // today and tomorrow
+ $this->assertEquals( $startDT->format( 'c' ), $availabilities[0]->from );
+ $this->assertEquals( $endDT->format( 'c' ), $availabilities[0]->until );
+ }
+
+ public function testVehicleIDChanges() {
+ // test, that the vehicle ID rotates after each trip
+ $request = new \WP_REST_Request( 'GET', $this->ENDPOINT );
+ $response = rest_do_request( $request );
+ $this->assertSame( 200, $response->get_status() );
+
+ $data = $response->get_data()->data;
+ $id = $data->vehicles[0]->vehicle_id;
+
+ // add a trip in the past
+ $this->createBooking(
+ $this->locationId,
+ $this->itemId,
+ strtotime( '-2 days', strtotime( self::CURRENT_DATE ) ),
+ strtotime( '-1 day', strtotime( self::CURRENT_DATE ) )
+ );
+
+ // after the trip, the vehicle ID should have changed
+ $request = new \WP_REST_Request( 'GET', $this->ENDPOINT );
+ $response = rest_do_request( $request );
+ $this->assertSame( 200, $response->get_status() );
+ $data = $response->get_data()->data;
+
+ $this->assertNotEquals( $id, $data->vehicles[0]->vehicle_id );
+ }
+
+ public function testExcludedIfBoxChecked() {
+ update_post_meta( $this->itemId, COMMONSBOOKING_METABOX_PREFIX . 'api_exclude', 'on' );
+ $request = new \WP_REST_Request( 'GET', $this->ENDPOINT );
+ $response = rest_do_request( $request );
+ $this->assertSame( 200, $response->get_status() );
+ $data = $response->get_data()->data;
+ $this->assertEmpty( $data->vehicles );
+ }
+
+ public function setUp(): void {
+ parent::setUp();
+
+ ClockMock::freeze( new \DateTime( self::CURRENT_DATE ) );
+
+ $this->locationId = $this->createLocation( 'Testlocation', 'publish', [] );
+ $this->itemId = $this->createItem( 'TestItem', 'publish' );
+
+ $mocked = new \DateTimeImmutable( self::CURRENT_DATE );
+ $this->start = $mocked->modify( '-1 days' );
+ $this->end = $mocked->modify( '+1 days' );
+
+ $this->timeframe = $this->createTimeframe(
+ $this->locationId,
+ $this->itemId,
+ $this->start->getTimestamp(),
+ $this->end->getTimestamp()
+ );
+ }
+
+ public function tearDown(): void {
+ ClockMock::reset();
+ parent::tearDown();
+ }
+}
diff --git a/tests/php/API/GBFS/VehicleStatusRouteTest.php b/tests/php/API/GBFS/VehicleStatusRouteTest.php
new file mode 100644
index 000000000..7918b06a5
--- /dev/null
+++ b/tests/php/API/GBFS/VehicleStatusRouteTest.php
@@ -0,0 +1,326 @@
+ENDPOINT );
+ $response = rest_do_request( $request );
+
+ $this->assertSame( 200, $response->get_status() );
+
+ $data = $response->get_data()->data;
+
+ $this->assertCount( 1, $data->vehicles );
+
+ $this->createConfirmedBookingStartingToday();
+ $request = new \WP_REST_Request( 'GET', $this->ENDPOINT );
+ $response = rest_do_request( $request );
+ $this->assertSame( 200, $response->get_status() );
+ $data = $response->get_data()->data;
+ $this->assertEmpty( $data->vehicles );
+ }
+
+ /**
+ * Item should not appear, when it is currently not available for rent.
+ *
+ * @return void
+ */
+ public function testNoTimeframe() {
+ // base case, not disabled
+ $request = new \WP_REST_Request( 'GET', $this->ENDPOINT );
+ $response = rest_do_request( $request );
+ $this->assertSame( 200, $response->get_status() );
+ $data = $response->get_data()->data;
+ $this->assertFalse( $data->vehicles[0]->is_disabled );
+
+ // timeframe expired: item vanishes from feed
+ $future = $this->end->modify( '+1 day' );
+ ClockMock::freeze( $future );
+ $request = new \WP_REST_Request( 'GET', $this->ENDPOINT );
+ $response = rest_do_request( $request );
+ $this->assertSame( 200, $response->get_status() );
+ $data = $response->get_data()->data;
+ $this->assertEmpty( $data->vehicles );
+ }
+
+ /**
+ * This field is used to indicate vehicles that are in the field but not available for rental due to a mechanical issue or low battery etc.
+ * Publishing this data may prevent users from attempting to rent vehicles that are disabled and not available for rental.
+ * This field SHOULD NOT be set to true when the system is closed for vehicles that would otherwise be rentable.
+ *
+ * In CB, this is handled through restrictions. When a total breakdown is present, the item is disabled.
+ * @return void
+ */
+ public function testIsDisabled() {
+ // base case, not disabled
+ $request = new \WP_REST_Request( 'GET', $this->ENDPOINT );
+ $response = rest_do_request( $request );
+ $this->assertSame( 200, $response->get_status() );
+ $data = $response->get_data()->data;
+ $this->assertFalse( $data->vehicles[0]->is_disabled );
+
+ // restriction present: should not disappear from feed, but is considered disabled
+ $this->createRestriction(
+ \CommonsBooking\Model\Restriction::TYPE_REPAIR,
+ $this->locationId,
+ $this->itemId,
+ strtotime( self::CURRENT_DATE ),
+ strtotime( '+1 day', strtotime( self::CURRENT_DATE ) )
+ );
+ $request = new \WP_REST_Request( 'GET', $this->ENDPOINT );
+ $response = rest_do_request( $request );
+ $this->assertSame( 200, $response->get_status() );
+ $data = $response->get_data()->data;
+ $this->assertCount( 1, $data->vehicles );
+ $this->assertTrue( $data->vehicles[0]->is_disabled );
+ }
+
+ public function testIsDisabled_hint() {
+ // hints should not affect availability
+ $this->createRestriction(
+ \CommonsBooking\Model\Restriction::TYPE_HINT,
+ $this->locationId,
+ $this->itemId,
+ strtotime( self::CURRENT_DATE ),
+ strtotime( '+2 days', strtotime( self::CURRENT_DATE ) )
+ );
+
+ $request = new \WP_REST_Request( 'GET', $this->ENDPOINT );
+ $response = rest_do_request( $request );
+ $this->assertSame( 200, $response->get_status() );
+ $data = $response->get_data()->data;
+ $this->assertFalse( $data->vehicles[0]->is_disabled );
+ }
+
+ public function testIsDisabled_inFuture() {
+ // restrictions in the future should not affect current state
+ $this->createRestriction(
+ \CommonsBooking\Model\Restriction::TYPE_REPAIR,
+ $this->locationId,
+ $this->itemId,
+ strtotime( '+1 day', strtotime( self::CURRENT_DATE ) ),
+ strtotime( '+2 days', strtotime( self::CURRENT_DATE ) )
+ );
+
+ $request = new \WP_REST_Request( 'GET', $this->ENDPOINT );
+ $response = rest_do_request( $request );
+ $this->assertSame( 200, $response->get_status() );
+ $data = $response->get_data()->data;
+ $this->assertFalse( $data->vehicles[0]->is_disabled );
+ }
+
+ public function testIsDisabled_inactiveRestriction() {
+ // inactive restrictions should also not trigger
+ $this->createRestriction(
+ \CommonsBooking\Model\Restriction::TYPE_REPAIR,
+ $this->locationId,
+ $this->itemId,
+ strtotime( self::CURRENT_DATE ),
+ null,
+ \CommonsBooking\Model\Restriction::STATE_SOLVED
+ );
+
+ $request = new \WP_REST_Request( 'GET', $this->ENDPOINT );
+ $response = rest_do_request( $request );
+ $this->assertSame( 200, $response->get_status() );
+ $data = $response->get_data()->data;
+ $this->assertFalse( $data->vehicles[0]->is_disabled );
+ }
+
+ public function testExclusion() {
+ ClockMock::freeze( new \DateTime( self::CURRENT_DATE ) );
+ update_post_meta( $this->itemId, COMMONSBOOKING_METABOX_PREFIX . 'api_exclude', 'on' );
+ $request = new \WP_REST_Request( 'GET', $this->ENDPOINT );
+ $response = rest_do_request( $request );
+ $this->assertSame( 200, $response->get_status() );
+ $data = $response->get_data()->data;
+ $this->assertEmpty( $data->vehicles );
+ }
+
+ public function testAvailableUntil_BaseCase() {
+ $rightNow = new \DateTime( self::CURRENT_DATE );
+ ClockMock::freeze( $rightNow );
+ $request = new \WP_REST_Request( 'GET', $this->ENDPOINT );
+ $response = rest_do_request( $request );
+ $this->assertSame( 200, $response->get_status() );
+ $data = $response->get_data()->data;
+ $inThreeDays = clone $rightNow;
+ $inThreeDays->modify( '+3 days' );
+ $inThreeDays->modify( '-1 second' ); // because the item is only available exactly three days, not on the fourth
+
+ $this->assertCount( 1, $data->vehicles );
+ $this->assertSame( $inThreeDays->format( 'c' ), $data->vehicles[0]->available_until );
+ }
+
+ public function testAvailableUntil_hourlyBooking() {
+ // delete our original timeframe so we don't have multiple vehicles
+ wp_delete_post( $this->timeframe, true );
+
+ // create a timeframe with hourly availability and one day max booking
+ $this->createTimeframe(
+ $this->locationId,
+ $this->itemId,
+ strtotime( self::CURRENT_DATE ),
+ null,
+ Timeframe::BOOKABLE_ID,
+ 'off',
+ 'd',
+ 1,
+ '8:00 AM',
+ '5:00 PM',
+ 'publish',
+ [ '1', '2', '3', '4', '5', '6', '7' ],
+ '',
+ CustomPostTypeTest::USER_ID,
+ 1
+ );
+
+ $rightNow = new \DateTime( self::CURRENT_DATE . ' 10:00 AM' );
+ ClockMock::freeze( $rightNow );
+ $request = new \WP_REST_Request( 'GET', $this->ENDPOINT );
+ $response = rest_do_request( $request );
+ $this->assertSame( 200, $response->get_status() );
+ $data = $response->get_data()->data;
+ $sameDayFivePM = clone $rightNow;
+ $sameDayFivePM->modify( '5:00 PM' );
+ $sameDayFivePM->modify( '-1 second' );
+
+ $this->assertCount( 1, $data->vehicles );
+ $this->assertSame( $sameDayFivePM->format( 'c' ), $data->vehicles[0]->available_until );
+ }
+
+ public function testAvailableUntil_slotBased() {
+ // delete our original timeframe so we don't have multiple vehicles
+ wp_delete_post( $this->timeframe, true );
+
+ // create a timeframe a slot based timeframe and one day max booking
+ $this->createTimeframe(
+ $this->locationId,
+ $this->itemId,
+ strtotime( self::CURRENT_DATE ),
+ null,
+ Timeframe::BOOKABLE_ID,
+ 'off',
+ 'd',
+ 0,
+ '8:00 AM',
+ '10:00 AM',
+ 'publish',
+ [ '1', '2', '3', '4', '5', '6', '7' ],
+ '',
+ CustomPostTypeTest::USER_ID,
+ 1
+ );
+
+ // create another slot for the afternoon
+ $this->createTimeframe(
+ $this->locationId,
+ $this->itemId,
+ strtotime( self::CURRENT_DATE ),
+ null,
+ Timeframe::BOOKABLE_ID,
+ 'off',
+ 'd',
+ 0,
+ '1:00 PM',
+ '5:00 PM',
+ 'publish',
+ [ '1', '2', '3', '4', '5', '6', '7' ],
+ '',
+ CustomPostTypeTest::USER_ID,
+ 1
+ );
+
+ $rightNow = new \DateTime( self::CURRENT_DATE . ' 9:00 AM' );
+ ClockMock::freeze( $rightNow );
+ $request = new \WP_REST_Request( 'GET', $this->ENDPOINT );
+ $response = rest_do_request( $request );
+ $this->assertSame( 200, $response->get_status() );
+ $data = $response->get_data()->data;
+ $sameDayFivePM = clone $rightNow;
+ $sameDayFivePM->modify( '5:00 PM' );
+ $sameDayFivePM->modify( '-1 second' );
+
+ $this->assertCount( 1, $data->vehicles );
+ $this->assertSame( $sameDayFivePM->format( 'c' ), $data->vehicles[0]->available_until );
+ }
+
+ public function testAvailableUntil_offset() {
+ // delete our original timeframe so we don't have multiple vehicles
+ wp_delete_post( $this->timeframe, true );
+
+ $rightNow = new \DateTime( self::CURRENT_DATE );
+ ClockMock::freeze( $rightNow );
+ $otherLocationId = $this->createLocation( 'Other Location', );
+ $otherItemId = $this->createItem( 'Other Item', );
+
+ $timeframeID = $this->createTimeframe(
+ $otherLocationId,
+ $otherItemId,
+ strtotime( '-1 day', strtotime( self::CURRENT_DATE ) ),
+ strtotime( '+10 days', strtotime( self::CURRENT_DATE ) ),
+ \CommonsBooking\Wordpress\CustomPostType\Timeframe::BOOKABLE_ID,
+ 'on',
+ 'd',
+ 0,
+ '8:00 AM',
+ '12:00 PM',
+ 'publish',
+ [],
+ '',
+ CustomPostTypeTest::USER_ID,
+ 3,
+ 30,
+ 2
+ );
+
+ // with offset - available until is extended to account for the offset
+ $availableUntil = clone $rightNow;
+ $availableUntil->modify( '+5 days' );
+ $availableUntil->modify( '-1 second' );
+
+ $request = new \WP_REST_Request( 'GET', $this->ENDPOINT );
+ $response = rest_do_request( $request );
+ $data = $response->get_data()->data;
+
+ $this->assertCount( 1, $data->vehicles );
+ $this->assertSame( $availableUntil->format( 'c' ), $data->vehicles[0]->available_until );
+ }
+
+ public function setUp(): void {
+ parent::setUp();
+
+ ClockMock::freeze( new \DateTime( self::CURRENT_DATE ) );
+
+ $this->locationId = $this->createLocation( 'Testlocation', 'publish', [] );
+ $this->itemId = $this->createItem( 'TestItem', 'publish' );
+
+ $mocked = new \DateTimeImmutable( self::CURRENT_DATE );
+ $this->start = $mocked->modify( '-1 days' );
+ $this->end = $mocked->modify( '+5 days' );
+
+ $this->timeframe = $this->createTimeframe(
+ $this->locationId,
+ $this->itemId,
+ $this->start->getTimestamp(),
+ $this->end->getTimestamp()
+ );
+ }
+
+ public function tearDown(): void {
+ ClockMock::reset();
+ parent::tearDown();
+ }
+}
diff --git a/tests/php/API/GBFS/VehicleTypesRouteTest.php b/tests/php/API/GBFS/VehicleTypesRouteTest.php
new file mode 100644
index 000000000..dfbb6e0ab
--- /dev/null
+++ b/tests/php/API/GBFS/VehicleTypesRouteTest.php
@@ -0,0 +1,22 @@
+ENDPOINT );
+ $response = rest_do_request( $request );
+
+ $this->assertSame( 200, $response->get_status() );
+
+ $data = $response->get_data()->data;
+
+ $this->assertCount( 1, $data->vehicle_types ); // just one vehicle type with the default settings
+
+ parent::testRoute();
+ }
+}
diff --git a/tests/php/API/ItemsRouteTest.php b/tests/php/API/ItemsRouteTest.php
index 52f9f7b3e..86ed45052 100644
--- a/tests/php/API/ItemsRouteTest.php
+++ b/tests/php/API/ItemsRouteTest.php
@@ -46,6 +46,26 @@ public function testItemsSuccess() {
$this->assertNotEmpty( $item->url );
}
+ public function testApiItemResponseFilter() {
+ // Modify an existing (schema-conforming) field so validation still passes.
+ $received = null;
+ $filter = function ( $preparedItem, $item ) use ( &$received ) {
+ $received = $item;
+ $preparedItem->name = 'Filtered Name';
+ return $preparedItem;
+ };
+
+ add_filter( 'commonsbooking_api_item_response', $filter, 10, 2 );
+ $request = new \WP_REST_Request( 'GET', $this->ENDPOINT );
+ $response = rest_do_request( $request );
+ remove_filter( 'commonsbooking_api_item_response', $filter, 10 );
+
+ $data = $response->get_data();
+ $this->assertEquals( 'Filtered Name', $data->items[0]->name );
+ $this->assertInstanceOf( \WP_Post::class, $received );
+ $this->assertEquals( $this->itemId, $received->ID );
+ }
+
public function testItemsIncludesLocations() {
$request = new \WP_REST_Request( 'GET', $this->ENDPOINT );
$response = rest_do_request( $request );
@@ -81,4 +101,16 @@ public function testSingleItem() {
$this->assertCount( 1, $data->items );
$this->assertEquals( (string) $this->itemId, $data->items[0]->id );
}
+
+ public function testExcludedIfBoxChecked() {
+ update_post_meta( $this->itemId, COMMONSBOOKING_METABOX_PREFIX . 'api_exclude', 'on' );
+ $request = new \WP_REST_Request( 'GET', $this->ENDPOINT . '/' . $this->itemId );
+ $response = rest_do_request( $request );
+
+ $this->assertSame( 200, $response->get_status() );
+
+ $data = $response->get_data();
+
+ $this->assertEmpty( $data->items );
+ }
}
diff --git a/tests/php/Model/BookingTest.php b/tests/php/Model/BookingTest.php
index 2b3c72f63..65491b736 100644
--- a/tests/php/Model/BookingTest.php
+++ b/tests/php/Model/BookingTest.php
@@ -352,6 +352,31 @@ public function testCanCancelBaseCase() {
$this->assertTrue( commonsbooking_isCurrentUserAdmin() );
}
+ public function testCanCancelFilterOverridesDecision() {
+ ClockMock::freeze( new \DateTime( self::CURRENT_DATE ) );
+ wp_set_current_user( $this->subscriberId );
+
+ // Default: the subscriber may cancel their own future booking.
+ $this->assertTrue( $this->subscriberBookingInFuture->canCancel() );
+
+ // The filter can forbid it, and receives the booking instance.
+ $received = null;
+ $deny = function ( $canCancel, $booking ) use ( &$received ) {
+ $received = $booking;
+ return false;
+ };
+ add_filter( 'commonsbooking_can_cancel_booking', $deny, 10, 2 );
+ $this->assertFalse( $this->subscriberBookingInFuture->canCancel() );
+ $this->assertInstanceOf( Booking::class, $received );
+ remove_filter( 'commonsbooking_can_cancel_booking', $deny, 10 );
+
+ // The filter can also allow cancelling a booking that is denied by default (a past booking).
+ $allow = fn() => true;
+ add_filter( 'commonsbooking_can_cancel_booking', $allow, 10, 2 );
+ $this->assertTrue( $this->testBookingPast->canCancel() );
+ remove_filter( 'commonsbooking_can_cancel_booking', $allow, 10 );
+ }
+
public function testHasTotalBreakdown() {
$testBooking = new Booking( $this->createConfirmedBookingStartingToday() );
$this->assertFalse( $this->testBookingTomorrow->hasTotalBreakdown() );
diff --git a/tests/php/Model/CalendarTest.php b/tests/php/Model/CalendarTest.php
index 65481071b..ae414af6a 100644
--- a/tests/php/Model/CalendarTest.php
+++ b/tests/php/Model/CalendarTest.php
@@ -92,6 +92,22 @@ public function testGetAvailabilitySlots() {
],
];
$this->assertEquals( $expectedSlotObject, $availabilitySlots );
+
+ // book the other day too, assert an empty
+ $this->createBooking(
+ $this->locationId,
+ $this->itemId,
+ strtotime( $tomorrow ),
+ $tomorrowEnd
+ );
+ // recreate the calendar object to get the updated availability
+ $this->calendar = new Calendar(
+ new Day( $today, [ $this->locationId ], [ $this->itemId ] ),
+ new Day( $tomorrow, [ $this->locationId ], [ $this->itemId ] ),
+ [ $this->locationId ],
+ [ $this->itemId ]
+ );
+ $this->assertEmpty( $this->calendar->getAvailabilitySlots() );
}
public function testGetAvailabilitySlotsWithHourlyTimeframe() {
diff --git a/tests/php/Model/DayTest.php b/tests/php/Model/DayTest.php
index 34155f021..aeb1be8e6 100644
--- a/tests/php/Model/DayTest.php
+++ b/tests/php/Model/DayTest.php
@@ -217,6 +217,22 @@ public function testGetEndTimestamp() {
$this->assertEquals( $end, $this->instance->getEndTimestamp() );
}
+ public function testDayAvailabilityFilterOverridesSlots() {
+ // Default: one slot spanning the whole day.
+ $this->assertCount( 1, $this->instance->getGrid() );
+
+ // The filter can adjust the slots and receives the day instance.
+ $received = null;
+ $filter = function ( array $slots, $day ) use ( &$received ) {
+ $received = $day;
+ return array(); // remove all availability
+ };
+ add_filter( 'commonsbooking_day_availability', $filter, 10, 2 );
+ $this->assertCount( 0, $this->instance->getGrid() );
+ $this->assertInstanceOf( Day::class, $received );
+ remove_filter( 'commonsbooking_day_availability', $filter, 10 );
+ }
+
public function testGetGrid() {
// one grid entry spanning whole day
$grid = $this->instance->getGrid();
diff --git a/tests/php/Model/ItemTest.php b/tests/php/Model/ItemTest.php
index abfa1c997..e564cd5bd 100644
--- a/tests/php/Model/ItemTest.php
+++ b/tests/php/Model/ItemTest.php
@@ -6,6 +6,7 @@
use CommonsBooking\Model\Restriction;
use CommonsBooking\Model\Timeframe;
use CommonsBooking\Tests\Wordpress\CustomPostTypeTest;
+use SlopeIt\ClockMock\ClockMock;
class ItemTest extends CustomPostTypeTest {
@@ -55,6 +56,280 @@ public function testGetRestrictions() {
}
*/
+ public function testGetLocation() {
+ $dt = new \DateTime( self::CURRENT_DATE );
+ ClockMock::freeze( $dt );
+
+ // just one item that is currently bookable
+ $this->assertEquals( $this->locationId, $this->itemModel->getLocation()->ID );
+
+ // in two weeks, the timeframe is expired, so the item will not be at the location anymore
+ $dt->modify( '+2 weeks' );
+ ClockMock::freeze( $dt );
+ $this->assertNull( $this->itemModel->getLocation() );
+
+ // item that is in location A monday-thursday and in location B friday-sunday. Position should change depending on the day of the week
+ $locationA = $this->createLocation( 'location a' );
+ $locationB = $this->createLocation( 'location b' );
+ $movingItem = $this->createItem( 'location changing item' );
+ $movingItemModel = new Item( $movingItem );
+ $tf1 = new Timeframe(
+ $this->createTimeframe(
+ $locationA,
+ $movingItem,
+ strtotime( '-1 day', strtotime( \CommonsBooking\Tests\Wordpress\CustomPostTypeTest::CURRENT_DATE ) ),
+ strtotime( '+14 days', strtotime( \CommonsBooking\Tests\Wordpress\CustomPostTypeTest::CURRENT_DATE ) ),
+ \CommonsBooking\Wordpress\CustomPostType\Timeframe::BOOKABLE_ID,
+ '',
+ 'w',
+ 0,
+ '8:00 AM',
+ '12:00 PM',
+ 'publish',
+ [ '1','2','3','4' ]
+ )
+ );
+ $tf2 = new Timeframe(
+ $this->createTimeframe(
+ $locationB,
+ $movingItem,
+ strtotime( '-1 day', strtotime( \CommonsBooking\Tests\Wordpress\CustomPostTypeTest::CURRENT_DATE ) ),
+ strtotime( '+14 days', strtotime( \CommonsBooking\Tests\Wordpress\CustomPostTypeTest::CURRENT_DATE ) ),
+ \CommonsBooking\Wordpress\CustomPostType\Timeframe::BOOKABLE_ID,
+ '',
+ 'w',
+ 0,
+ '8:00 AM',
+ '12:00 PM',
+ 'publish',
+ [ '5','6','7' ]
+ )
+ );
+ $this->assertFalse( $tf1->overlaps( $tf2 ) );
+ $dt = new \DateTime( self::CURRENT_DATE );
+ $dt->modify( 'monday' );
+ ClockMock::freeze( $dt );
+ $this->assertEquals( $locationA, $movingItemModel->getLocation()->ID );
+ $dt->modify( 'friday' );
+ ClockMock::freeze( $dt );
+ $this->assertEquals( $locationB, $movingItemModel->getLocation()->ID );
+ }
+
+ public function testGetCloakedID() {
+ ClockMock::freeze( new \DateTime( self::CURRENT_DATE ) );
+ $cloakedID = $this->itemModel->getCloakedId();
+ $this->assertIsString( $cloakedID );
+ // create a booking, assert that it has changed
+ $this->createBooking(
+ $this->locationId,
+ $this->itemId,
+ strtotime( '-2 days', strtotime( self::CURRENT_DATE ) ),
+ strtotime( '-1 day', strtotime( self::CURRENT_DATE ) )
+ );
+ $cloakedIDWithBooking = $this->itemModel->getCloakedId();
+ $this->assertNotEquals( $cloakedID, $cloakedIDWithBooking );
+
+ // this booking is not in the past, trip has not ended. Therefore it should not affect the ID
+ $booking = $this->createConfirmedBookingStartingToday();
+ $cloakedIDwithTwoBookings = $this->itemModel->getCloakedId();
+ $this->assertEquals( $cloakedIDWithBooking, $cloakedIDwithTwoBookings );
+ }
+
+ public function testGetClosestBooking() {
+ ClockMock::freeze( new \DateTime( self::CURRENT_DATE ) );
+ // no booking present
+ $this->assertNull( $this->itemModel->getClosestBooking() );
+
+ // one past booking
+ $bookingInPast = $this->createBooking(
+ $this->locationId,
+ $this->itemId,
+ strtotime( '-4 days', strtotime( self::CURRENT_DATE ) ),
+ strtotime( '-3 days', strtotime( self::CURRENT_DATE ) )
+ );
+ $closestBooking = $this->itemModel->getClosestBooking();
+ $this->assertEquals( $bookingInPast, $closestBooking->ID );
+
+ // newer booking in front of that
+ $newerBooking = $this->createBooking(
+ $this->locationId,
+ $this->itemId,
+ strtotime( '-2 days', strtotime( self::CURRENT_DATE ) ),
+ strtotime( '-1 day', strtotime( self::CURRENT_DATE ) )
+ );
+ $closestBooking = $this->itemModel->getClosestBooking();
+ $this->assertEquals( $newerBooking, $closestBooking->ID );
+
+ // booking in future (should not affect)
+ $this->createBooking(
+ $this->locationId,
+ $this->itemId,
+ strtotime( '+3 days' ),
+ strtotime( '+4 days' )
+ );
+ $this->assertEquals( $newerBooking, $this->itemModel->getClosestBooking()->ID );
+ }
+
+ // Test case where last booking is way in the past but other booking is in the nearer future. Should still return booking in the past
+ public function testGetClosestBooking_notIncludingFutureBookings() {
+ ClockMock::freeze( new \DateTime( self::CURRENT_DATE ) );
+ // one past booking
+ $pastBooking = $this->createBooking(
+ $this->locationId,
+ $this->itemId,
+ strtotime( '-7 days' ),
+ strtotime( '-5 days' )
+ );
+ // one future booking
+ $bookingStartingTomorrow = $this->createBooking(
+ $this->locationId,
+ $this->itemId,
+ strtotime( '+1 day' ),
+ strtotime( '+2 days' )
+ );
+ $closestBooking = $this->itemModel->getClosestBooking();
+ $this->assertEquals( $pastBooking, $closestBooking->ID );
+ }
+
+ public function testGetClosestBooking_picksMostRecentPastBooking() {
+ ClockMock::freeze( new \DateTime( self::CURRENT_DATE ) );
+ $this->createBooking(
+ $this->locationId,
+ $this->itemId,
+ strtotime( '-14 days' ),
+ strtotime( '-12 days' )
+ );
+ $recentPastBooking = $this->createBooking(
+ $this->locationId,
+ $this->itemId,
+ strtotime( '-3 days' ),
+ strtotime( '-1 day' )
+ );
+ $this->createBooking(
+ $this->locationId,
+ $this->itemId,
+ strtotime( '+1 day' ),
+ strtotime( '+2 days' )
+ );
+
+ $closestBooking = $this->itemModel->getClosestBooking();
+ $this->assertEquals( $recentPastBooking, $closestBooking->ID );
+ }
+
+ public function testIsCurrentlyFreeAtLocation() {
+ ClockMock::freeze( new \DateTime( self::CURRENT_DATE ) );
+
+ // Item has a bookable timeframe including today, should be free
+ $this->assertTrue( $this->itemModel->isCurrentlyFreeAtLocation( $this->locationId ) );
+
+ // A confirmed booking starting today means the item is now rented
+ $this->createConfirmedBookingStartingToday();
+ $this->assertFalse( $this->itemModel->isCurrentlyFreeAtLocation( $this->locationId ) );
+
+ // In two weeks, the timeframe is expired, should no longer be free
+ $dt = new \DateTime( self::CURRENT_DATE );
+ $dt->modify( '+2 weeks' );
+ ClockMock::freeze( $dt );
+ $this->assertFalse( $this->itemModel->isCurrentlyFreeAtLocation( $this->locationId ) );
+
+ // Wrong location should not be free
+ ClockMock::freeze( new \DateTime( self::CURRENT_DATE ) );
+ $otherLocationId = $this->createLocation( 'other location' );
+ $this->assertFalse( $this->itemModel->isCurrentlyFreeAtLocation( $otherLocationId ) );
+ }
+
+ public function testIsCurrentlyFreeAtLocation_withBookingOffset() {
+ $otherLocationId = $this->createLocation( 'Other Location' );
+ $otherItemId = $this->createItem( 'Other Item' );
+ $otherItemModel = new Item( $otherItemId );
+
+ $timeframeId = $this->createTimeframe(
+ $otherLocationId,
+ $otherItemId,
+ strtotime( '-1 day', strtotime( self::CURRENT_DATE ) ),
+ strtotime( '+10 days', strtotime( self::CURRENT_DATE ) ),
+ \CommonsBooking\Wordpress\CustomPostType\Timeframe::BOOKABLE_ID,
+ 'on',
+ 'd',
+ 0,
+ '8:00 AM',
+ '12:00 PM',
+ 'publish',
+ [],
+ '',
+ CustomPostTypeTest::USER_ID,
+ 3, // booking start day offset
+ 30,
+ 2
+ );
+
+ ClockMock::freeze( new \DateTime( self::CURRENT_DATE ) );
+
+ // With offset → item is still free, it's just not possible to pick it up right now
+ $this->assertTrue( $otherItemModel->isCurrentlyFreeAtLocation( $otherLocationId, true ) );
+
+ // Remove offset → still free, no change in rental status
+ update_post_meta( $timeframeId, \CommonsBooking\Model\Timeframe::META_BOOKING_START_DAY_OFFSET, 0 );
+ $this->assertTrue( $otherItemModel->isCurrentlyFreeAtLocation( $otherLocationId, true ) );
+ }
+
+ public function testGetClosestBookableTimeframe() {
+ $dt = new \DateTime( self::CURRENT_DATE );
+ ClockMock::freeze( $dt );
+ $this->assertEquals( $this->timeframeModel->ID, $this->itemModel->getClosestBookableTimeframe()->ID );
+ // in two weeks, the timeframe is expired, so the item will not be at the location anymore
+ $dt->modify( '+2 weeks' );
+ ClockMock::freeze( $dt );
+ $this->assertNull( $this->itemModel->getClosestBookableTimeframe() );
+ }
+
+ public function testGetClosestBookableTimeframe_withBookingOffset() {
+ $otherLocationId = $this->createLocation( 'Other Location' );
+ $otherItemId = $this->createItem( 'Other Item' );
+ $otherItemModel = new Item( $otherItemId );
+
+ $timeframeId = $this->createTimeframe(
+ $otherLocationId,
+ $otherItemId,
+ strtotime( '-1 day', strtotime( self::CURRENT_DATE ) ),
+ strtotime( '+10 days', strtotime( self::CURRENT_DATE ) ),
+ \CommonsBooking\Wordpress\CustomPostType\Timeframe::BOOKABLE_ID,
+ 'on',
+ 'd',
+ 0,
+ '8:00 AM',
+ '12:00 PM',
+ 'publish',
+ [],
+ '',
+ CustomPostTypeTest::USER_ID,
+ 3, // booking start day offset
+ 30,
+ 2
+ );
+
+ ClockMock::freeze( new \DateTime( self::CURRENT_DATE ) );
+
+ // should still show the timeframe as the closest
+ $this->assertEquals( $timeframeId, $otherItemModel->getClosestBookableTimeframe()->ID );
+ }
+
+ public function testGetClosestBookableTimeframe_withSlots() {
+ $otherLocationId = $this->createLocation( 'Other Location' );
+ $otherItemId = $this->createItem( 'Other Item' );
+ $timeframes = $this->createTwoBookableTimeframeSlotsIncludingCurrentDay( $otherLocationId, $otherItemId );
+ $otherItemModel = new Item( $otherItemId );
+ $dt = new \DateTime( self::CURRENT_DATE );
+ ClockMock::freeze( $dt );
+
+ // even though it is not open yet (bc we have midnight), the first slot should be the closest
+ $this->assertEquals( $timeframes[0], $otherItemModel->getClosestBookableTimeframe()->ID );
+
+ // After 3PM, the other one should be picked
+ $dt->modify( '15:01' );
+ ClockMock::freeze( $dt );
+ $this->assertEquals( $timeframes[1], $otherItemModel->getClosestBookableTimeframe()->ID );
+ }
protected function setUp(): void {
parent::setUp();
$this->restrictionIds[] = $this->createRestriction(
diff --git a/tests/php/Model/TimeframeTest.php b/tests/php/Model/TimeframeTest.php
index b6fb99ce6..590fd2131 100644
--- a/tests/php/Model/TimeframeTest.php
+++ b/tests/php/Model/TimeframeTest.php
@@ -731,6 +731,22 @@ public function testIsBookable() {
// This test does not work, function maybe broken?
}
+ public function testIsBookableFilterOverridesDecision() {
+ // Default: the timeframe is bookable.
+ $this->assertTrue( $this->validTF->isBookable() );
+
+ // The filter can forbid it and receives the timeframe instance.
+ $received = null;
+ $filter = function ( $bookable, $timeframe ) use ( &$received ) {
+ $received = $timeframe;
+ return false;
+ };
+ add_filter( 'commonsbooking_is_timeframe_bookable', $filter, 10, 2 );
+ $this->assertFalse( $this->validTF->isBookable() );
+ $this->assertInstanceOf( Timeframe::class, $received );
+ remove_filter( 'commonsbooking_is_timeframe_bookable', $filter, 10 );
+ }
+
/**
* Tests all validity concerns of timeframes with manual repetition
* @return void
diff --git a/tests/php/Repository/ItemTest.php b/tests/php/Repository/ItemTest.php
index 64baef6f6..d559fbe95 100644
--- a/tests/php/Repository/ItemTest.php
+++ b/tests/php/Repository/ItemTest.php
@@ -4,21 +4,44 @@
use CommonsBooking\Repository\Item;
use CommonsBooking\Tests\Wordpress\CustomPostTypeTest;
+use SlopeIt\ClockMock\ClockMock;
class ItemTest extends CustomPostTypeTest {
protected function setUp(): void {
parent::setUp();
-
- // Create timeframe with location and item, so that we can search vor it
+ ClockMock::freeze( new \DateTime( self::CURRENT_DATE ) );
+ // Create timeframe with location and item, so that we can search for it
$this->createTimeframe(
$this->locationId,
$this->itemId,
- strtotime( 'midnight' ),
- strtotime( '+90 days' )
+ strtotime( 'midnight', strtotime( self::CURRENT_DATE ) ),
+ strtotime( '+90 days', strtotime( self::CURRENT_DATE ) )
);
}
+ public function testGetByCloakedId(): void {
+ $itemModel = new \CommonsBooking\Model\Item( $this->itemId );
+
+ // basic test, without any booking
+ $cloakedId = $itemModel->getCloakedId();
+ $this->assertEquals( $this->itemId, Item::getByCloakedId( $cloakedId )->ID );
+
+ // with booking in past
+ $this->createBooking(
+ $this->locationId,
+ $this->itemId,
+ strtotime( '-2 days', strtotime( self::CURRENT_DATE ) ),
+ strtotime( '-1 day', strtotime( self::CURRENT_DATE ) )
+ );
+ $bookingCloakedId = $itemModel->getCloakedId();
+ $this->assertNotEquals( $bookingCloakedId, $cloakedId );
+ $this->assertEquals( $this->itemId, Item::getByCloakedId( $bookingCloakedId )->ID );
+
+ // test for invalid cloaked id
+ $this->assertNull( Item::getByCloakedId( 'invalid-cloaked-id' ) );
+ }
+
public function testGetByLocation(): void {
$this->assertEquals(
[ $this->itemId ],
diff --git a/tests/php/Repository/TimeframeTest.php b/tests/php/Repository/TimeframeTest.php
index d65927e8c..00776ca3a 100644
--- a/tests/php/Repository/TimeframeTest.php
+++ b/tests/php/Repository/TimeframeTest.php
@@ -52,6 +52,27 @@ private function createOtherTimeframe( $start = null, $end = null ) {
}
+ public function testBookableTimeframesFilterOverridesResult() {
+ $sentinel = array( 'sentinel' );
+ $receivedLocations = null;
+ $receivedItems = null;
+ $filter = function ( array $timeframes, array $locations, array $items ) use ( $sentinel, &$receivedLocations, &$receivedItems ) {
+ $receivedLocations = $locations;
+ $receivedItems = $items;
+ return $sentinel;
+ };
+
+ add_filter( 'commonsbooking_bookable_timeframes', $filter, 10, 3 );
+ $result = Timeframe::getBookable( array( $this->locationId ), array( $this->itemId ) );
+ remove_filter( 'commonsbooking_bookable_timeframes', $filter, 10 );
+
+ // The returned value is exactly what the filter produced.
+ $this->assertSame( $sentinel, $result );
+ // The filter received the location/item scope of the query.
+ $this->assertEquals( array( $this->locationId ), $receivedLocations );
+ $this->assertEquals( array( $this->itemId ), $receivedItems );
+ }
+
public function testGetInRange() {
$inRangeTimeFrames = Timeframe::getInRange( $this->repetition_start, $this->repetition_end );
// All timeframes should be in range
diff --git a/tests/php/Service/BookingLifecycleTest.php b/tests/php/Service/BookingLifecycleTest.php
new file mode 100644
index 000000000..bbd84e889
--- /dev/null
+++ b/tests/php/Service/BookingLifecycleTest.php
@@ -0,0 +1,175 @@
+>
+ */
+ private array $fired = array();
+
+ /**
+ * Inserts a booking post the way the plugin does (type meta as part of the
+ * insert), so that the `transition_post_status` action sees a real booking.
+ */
+ private function insertBooking( string $status, int $end = null ): int {
+ $end = $end ?? ( time() + DAY_IN_SECONDS );
+
+ return wp_insert_post(
+ array(
+ 'post_title' => 'Lifecycle booking',
+ 'post_type' => BookingPostType::$postType,
+ 'post_status' => $status,
+ 'post_author' => self::USER_ID,
+ 'meta_input' => array(
+ 'type' => TimeframeCPT::BOOKING_ID,
+ Timeframe::META_LOCATION_ID => $this->locationId,
+ Timeframe::META_ITEM_ID => $this->itemId,
+ Timeframe::REPETITION_START => time(),
+ Timeframe::REPETITION_END => $end,
+ ),
+ )
+ );
+ }
+
+ /**
+ * Registers a spy for a hook that records every invocation's arguments.
+ */
+ private function spyOn( string $hook, int $args = 2 ): void {
+ $this->fired[ $hook ] = array();
+ add_action(
+ $hook,
+ function ( ...$received ) use ( $hook ) {
+ $this->fired[ $hook ][] = $received;
+ },
+ 10,
+ $args
+ );
+ }
+
+ public function testConfirmedHookFiresOnConfirmation() {
+ $this->spyOn( 'commonsbooking_booking_confirmed' );
+ $bookingId = $this->insertBooking( 'unconfirmed' );
+
+ $this->assertCount( 0, $this->fired['commonsbooking_booking_confirmed'] );
+
+ wp_update_post(
+ array(
+ 'ID' => $bookingId,
+ 'post_status' => 'confirmed',
+ )
+ );
+
+ $this->assertCount( 1, $this->fired['commonsbooking_booking_confirmed'] );
+ [ $id, $booking ] = $this->fired['commonsbooking_booking_confirmed'][0];
+ $this->assertEquals( $bookingId, $id );
+ $this->assertInstanceOf( Booking::class, $booking );
+ }
+
+ public function testConfirmedHookFiresWhenInsertedAsConfirmed() {
+ $this->spyOn( 'commonsbooking_booking_confirmed' );
+ $bookingId = $this->insertBooking( 'confirmed' );
+
+ $this->assertCount( 1, $this->fired['commonsbooking_booking_confirmed'] );
+ $this->assertEquals( $bookingId, $this->fired['commonsbooking_booking_confirmed'][0][0] );
+ }
+
+ public function testStatusChangedHookCarriesOldAndNewStatus() {
+ $this->spyOn( 'commonsbooking_booking_status_changed', 4 );
+ $bookingId = $this->insertBooking( 'unconfirmed' );
+
+ // Reset to only assert the confirm transition below.
+ $this->fired['commonsbooking_booking_status_changed'] = array();
+
+ wp_update_post(
+ array(
+ 'ID' => $bookingId,
+ 'post_status' => 'confirmed',
+ )
+ );
+
+ $this->assertCount( 1, $this->fired['commonsbooking_booking_status_changed'] );
+ [ $id, $old, $new, $booking ] = $this->fired['commonsbooking_booking_status_changed'][0];
+ $this->assertEquals( $bookingId, $id );
+ $this->assertEquals( 'unconfirmed', $old );
+ $this->assertEquals( 'confirmed', $new );
+ $this->assertInstanceOf( Booking::class, $booking );
+ }
+
+ public function testCreatedHookFiresExactlyOnce() {
+ $this->spyOn( 'commonsbooking_booking_created' );
+ $bookingId = $this->insertBooking( 'unconfirmed' );
+
+ $this->assertCount( 1, $this->fired['commonsbooking_booking_created'] );
+ $this->assertEquals( $bookingId, $this->fired['commonsbooking_booking_created'][0][0] );
+
+ // A later status change must not fire "created" again.
+ wp_update_post(
+ array(
+ 'ID' => $bookingId,
+ 'post_status' => 'confirmed',
+ )
+ );
+
+ $this->assertCount( 1, $this->fired['commonsbooking_booking_created'] );
+ }
+
+ public function testCancelledHookFiresOnCancel() {
+ $this->spyOn( 'commonsbooking_booking_cancelled' );
+ $bookingId = $this->insertBooking( 'confirmed' );
+ $this->fired['commonsbooking_booking_cancelled'] = array();
+
+ $booking = new Booking( $bookingId );
+ $booking->cancel();
+
+ $this->assertCount( 1, $this->fired['commonsbooking_booking_cancelled'] );
+ [ $id, $cancelledBooking ] = $this->fired['commonsbooking_booking_cancelled'][0];
+ $this->assertEquals( $bookingId, $id );
+ $this->assertInstanceOf( Booking::class, $cancelledBooking );
+ }
+
+ public function testNoHooksForPostsWithoutBookingTypeMeta() {
+ $this->spyOn( 'commonsbooking_booking_created' );
+ $this->spyOn( 'commonsbooking_booking_status_changed', 4 );
+
+ // A cb_booking post without the booking type meta is not a real booking.
+ $postId = wp_insert_post(
+ array(
+ 'post_title' => 'Not a booking',
+ 'post_type' => BookingPostType::$postType,
+ 'post_status' => 'confirmed',
+ 'post_author' => self::USER_ID,
+ )
+ );
+
+ wp_update_post(
+ array(
+ 'ID' => $postId,
+ 'post_status' => 'draft',
+ )
+ );
+
+ $this->assertCount( 0, $this->fired['commonsbooking_booking_created'] );
+ $this->assertCount( 0, $this->fired['commonsbooking_booking_status_changed'] );
+ }
+
+ public function testCreatedMetaFlagConstantIsSet() {
+ $bookingId = $this->insertBooking( 'unconfirmed' );
+ $this->assertEquals( 1, get_post_meta( $bookingId, BookingLifecycle::CREATED_FIRED_META, true ) );
+ }
+}
diff --git a/tests/php/Service/BookingRuleTest.php b/tests/php/Service/BookingRuleTest.php
index 50d0b9142..64f9f2a93 100644
--- a/tests/php/Service/BookingRuleTest.php
+++ b/tests/php/Service/BookingRuleTest.php
@@ -277,6 +277,58 @@ public function testCheckMaxBookingDays_earlyCancel() {
$this->assertNull( BookingRule::checkMaxBookingDays( $allowedBooking, array( 4, 30 ) ) );
}
+ /**
+ * Regression test for #2158
+ * @return void
+ */
+ public function testCheckMaxBookingDays_unevenDays() {
+ ClockMock::freeze( new \DateTime( self::CURRENT_DATE ) );
+ $testBookingOne = new Booking(
+ get_post(
+ $this->createBooking(
+ $this->locationId,
+ $this->itemId,
+ strtotime( '+1 day' ),
+ strtotime( '+2 days' ),
+ '8:00 AM',
+ '12:00 PM',
+ 'confirmed',
+ $this->subscriberId
+ )
+ )
+ );
+ $testBookingTwo = new Booking(
+ get_post(
+ $this->createBooking(
+ $this->locationId,
+ $this->itemId,
+ strtotime( '+4 day' ),
+ strtotime( '+5 days' ),
+ '8:00 AM',
+ '12:00 PM',
+ 'confirmed',
+ $this->subscriberId
+ )
+ )
+ );
+
+ $testBookingThree = new Booking(
+ get_post(
+ $this->createBooking(
+ $this->locationId,
+ $this->itemId,
+ strtotime( '+6 day' ),
+ strtotime( '+7 days' ),
+ '8:00 AM',
+ '12:00 PM',
+ 'unconfirmed',
+ $this->subscriberId
+ )
+ )
+ );
+ $this->assertBookingsPresent( array( $testBookingOne, $testBookingTwo ), BookingRule::checkMaxBookingDays( $testBookingThree, array( 2, 25 ) ) );
+ }
+
public function testCheckMaxBookingDaysPerWeek() {
// rule settings
$allowedPerWeek = 2;
diff --git a/tests/php/View/CalendarTest.php b/tests/php/View/CalendarTest.php
index e84f5080a..2fe5e992f 100644
--- a/tests/php/View/CalendarTest.php
+++ b/tests/php/View/CalendarTest.php
@@ -7,6 +7,7 @@
use CommonsBooking\Wordpress\CustomPostType\Item;
use CommonsBooking\View\Calendar;
use DateTime;
+use Override;
use SlopeIt\ClockMock\ClockMock;
/**
@@ -43,6 +44,36 @@ public function testKeepDateRangeParam() {
$this->assertTrue( $firstDay == $startDate );
}
+ public function testCalendarDataFilterAdjustsData() {
+ $startDate = date( 'Y-m-d', strtotime( self::CURRENT_DATE ) );
+
+ $receivedItem = null;
+ $receivedLocation = null;
+ $filter = function ( array $calendarData, $item, $location ) use ( &$receivedItem, &$receivedLocation ) {
+ $receivedItem = $item;
+ $receivedLocation = $location;
+ $calendarData['injectedByFilter'] = true;
+ return $calendarData;
+ };
+
+ add_filter( 'commonsbooking_calendar_data', $filter, 10, 3 );
+ $jsonresponse = Calendar::getCalendarDataArray(
+ $this->itemId,
+ $this->locationId,
+ $startDate,
+ date( 'Y-m-d', strtotime( '+20 days', strtotime( self::CURRENT_DATE ) ) ),
+ true
+ );
+ remove_filter( 'commonsbooking_calendar_data', $filter, 10 );
+
+ // The filter's change is reflected in the returned data.
+ $this->assertArrayHasKey( 'injectedByFilter', $jsonresponse );
+ $this->assertTrue( $jsonresponse['injectedByFilter'] );
+ // The filter received the item and location the calendar is for.
+ $this->assertEquals( $this->itemId, $receivedItem );
+ $this->assertEquals( $this->locationId, $receivedLocation );
+ }
+
public function testAdvancedBookingDays() {
$startDate = date( 'Y-m-d', strtotime( 'midnight' ) );
$endDate = date( 'Y-m-d', strtotime( '+60 days midnight' ) );
@@ -229,6 +260,7 @@ public function testRepetition() {
}
public function testRenderTable() {
+ \CommonsBooking\Wordpress\CustomPostType\Item::registerPostTypeTaxonomy();
$calendar = Calendar::renderTable( [] );
$item = new \CommonsBooking\Model\Item( $this->itemId );
$location = new \CommonsBooking\Model\Location( $this->locationId );
@@ -241,15 +273,11 @@ public function testRenderTable() {
$taxonomyItem = new \CommonsBooking\Model\Item( $taxonomyItem );
$term = wp_create_term( 'Test Category', Item::getTaxonomyName() );
wp_set_post_terms( $taxonomyItem->ID, [ $term['term_id'] ], Item::getTaxonomyName() );
- $termObj = get_term_by( 'id', $term['term_id'], Item::getTaxonomyName() );
- $slug = $termObj->slug;
- $timeframe = $this->createTimeframe(
- $this->locationId,
- $taxonomyItem->ID,
- strtotime( '+' . self::timeframeStart . ' days midnight', $this->now ),
- strtotime( '+' . self::timeframeEnd . ' days midnight', $this->now )
- );
- $calendar = Calendar::renderTable( [ 'itemcat' => $slug ] );
+ $termObj = get_term_by( 'id', $term['term_id'], Item::getTaxonomyName() );
+ $slug = $termObj->slug;
+ $this->createBookableTimeFrameIncludingCurrentDay( $location->ID, $taxonomyItem->ID );
+ wp_cache_flush(); // for some reason, the tests were failing without this
+ $calendar = Calendar::renderTable( [ 'itemcat' => $slug ] );
$this->assertStringContainsString( $taxonomyItem->post_title, $calendar );
$this->assertStringNotContainsString( $item->post_title, $calendar );
@@ -474,8 +502,9 @@ private function createTimeframeFromConfig( string $name, int $itemId, int $loca
}
protected function setUp(): void {
- parent::setUp();
+ ClockMock::freeze( new \DateTime( self::CURRENT_DATE ) );
+ parent::setUp();
$this->now = time();
$this->timeframeId = $this->createTimeframe(
$this->locationId,
@@ -500,4 +529,9 @@ protected function setUp(): void {
strtotime( '+300 days midnight' )
);
}
+
+ protected function tearDown(): void {
+ ClockMock::reset();
+ parent::tearDown();
+ }
}
diff --git a/tests/php/View/ViewTest.php b/tests/php/View/ViewTest.php
index 30982d9a3..0f59d0c9a 100644
--- a/tests/php/View/ViewTest.php
+++ b/tests/php/View/ViewTest.php
@@ -89,62 +89,54 @@ protected function setUp(): void {
parent::setUp();
ClockMock::freeze( new \DateTime( self::CURRENT_DATE ) );
- $now = time();
- $this->now = $now;
-
$this->item = new Item( $this->itemId );
$this->location = new Location( $this->locationId );
-
- $timeframeId = $this->createTimeframe(
+ $timeframeId = $this->createTimeframe(
$this->locationId,
$this->itemId,
- strtotime( '+5 days midnight', $now ),
- strtotime( '+6 days midnight', $now ),
+ strtotime( '+5 days midnight', strtotime( self::CURRENT_DATE ) ),
+ strtotime( '+6 days midnight', strtotime( self::CURRENT_DATE ) ),
);
// set booking days in advance
update_post_meta( $timeframeId, \CommonsBooking\Model\Timeframe::META_TIMEFRAME_ADVANCE_BOOKING_DAYS, self::bookingDaysInAdvance );
-
$timeframeId = $this->createTimeframe(
$this->locationId,
$this->itemId,
- strtotime( '+2 days midnight', $now ),
- strtotime( '+3 days midnight', $now ),
- );// set booking days in advance
+ strtotime( '+2 days midnight', strtotime( self::CURRENT_DATE ) ),
+ strtotime( '+3 days midnight', strtotime( self::CURRENT_DATE ) ),
+ );
+ // set booking days in advance
update_post_meta( $timeframeId, \CommonsBooking\Model\Timeframe::META_TIMEFRAME_ADVANCE_BOOKING_DAYS, self::bookingDaysInAdvance );
-
$timeframeId = $this->createTimeframe(
$this->locationId,
$this->itemId,
- strtotime( '+8 days midnight', $now ),
- strtotime( '+9 days midnight', $now ),
+ strtotime( '+8 days midnight', strtotime( self::CURRENT_DATE ) ),
+ strtotime( '+9 days midnight', strtotime( self::CURRENT_DATE ) ),
);
// set booking days in advance
update_post_meta( $timeframeId, \CommonsBooking\Model\Timeframe::META_TIMEFRAME_ADVANCE_BOOKING_DAYS, self::bookingDaysInAdvance );
-
$timeframeId = $this->createTimeframe(
$this->locationId,
$this->itemId,
- strtotime( '+12 days midnight', $now ),
- strtotime( '+13 days midnight', $now ),
+ strtotime( '+12 days midnight', strtotime( self::CURRENT_DATE ) ),
+ strtotime( '+13 days midnight', strtotime( self::CURRENT_DATE ) ),
);
// set booking days in advance
update_post_meta( $timeframeId, \CommonsBooking\Model\Timeframe::META_TIMEFRAME_ADVANCE_BOOKING_DAYS, self::bookingDaysInAdvance );
-
$timeframeId = $this->createTimeframe(
$this->locationId,
$this->itemId,
- strtotime( '+14 days midnight', $now ),
- strtotime( '+15 days midnight', $now ),
+ strtotime( '+14 days midnight', strtotime( self::CURRENT_DATE ) ),
+ strtotime( '+15 days midnight', strtotime( self::CURRENT_DATE ) ),
);
// set booking days in advance
update_post_meta( $timeframeId, \CommonsBooking\Model\Timeframe::META_TIMEFRAME_ADVANCE_BOOKING_DAYS, self::bookingDaysInAdvance );
-
// this timeframe should not be in shortcode data, because it's out of 30 days advanced booking range
$timeframeId = $this->createTimeframe(
$this->locationId,
$this->itemId,
- strtotime( '+32 days midnight', $now ),
- strtotime( '+33 days midnight', $now ),
+ strtotime( '+32 days midnight', strtotime( self::CURRENT_DATE ) ),
+ strtotime( '+33 days midnight', strtotime( self::CURRENT_DATE ) ),
);
// set booking days in advance
update_post_meta( $timeframeId, \CommonsBooking\Model\Timeframe::META_TIMEFRAME_ADVANCE_BOOKING_DAYS, self::bookingDaysInAdvance );
@@ -152,5 +144,6 @@ protected function setUp(): void {
protected function tearDown(): void {
parent::tearDown();
+ ClockMock::reset();
}
}
diff --git a/tests/php/Wordpress/CustomPostType/BookingTest.php b/tests/php/Wordpress/CustomPostType/BookingTest.php
index b1a5eff59..9e9a4ac89 100644
--- a/tests/php/Wordpress/CustomPostType/BookingTest.php
+++ b/tests/php/Wordpress/CustomPostType/BookingTest.php
@@ -50,7 +50,7 @@ public function testHandleBookingRequest_Default() {
$this->assertFalse( $bookingModel->isConfirmed() );
// Case 2: We now confirm the booking. The booking should be confirmed
- $newBookingId = Booking::handleBookingRequest(
+ $confirmedBookingId = Booking::handleBookingRequest(
$this->itemId,
$this->locationId,
'confirmed',
@@ -61,10 +61,10 @@ public function testHandleBookingRequest_Default() {
$postName,
null
);
- $this->bookingIds[] = $newBookingId;
+ $this->bookingIds[] = $confirmedBookingId;
// the id should be the same
- $this->assertEquals( $bookingId, $newBookingId );
+ $this->assertEquals( $bookingId, $confirmedBookingId );
// we create a new model, just to be sure
$bookingModel = new \CommonsBooking\Model\Booking( $bookingId );
$this->assertTrue( $bookingModel->isConfirmed() );
@@ -97,6 +97,46 @@ public function testHandleBookingRequest_Default() {
$this->assertEquals( $date->format( 'Y-m-d H:i:s' ), $cancelDate->format( 'Y-m-d H:i:s' ) );
}
+ /**
+ * The commonsbooking_booking_before_save filter can add meta to a booking
+ * before it is persisted, and receives null as the "existing booking" for a
+ * newly created booking.
+ */
+ public function testBookingBeforeSaveFilterAddsMeta() {
+ $date = new \DateTime( self::CURRENT_DATE );
+ $date->modify( '-1 day' );
+ ClockMock::freeze( $date );
+
+ $received = 'unset';
+ $filter = function ( array $postarr, $booking ) use ( &$received ) {
+ $received = $booking;
+ $postarr['meta_input']['my_external_ref'] = 'ext-123';
+ return $postarr;
+ };
+ add_filter( 'commonsbooking_booking_before_save', $filter, 10, 2 );
+
+ $bookingId = Booking::handleBookingRequest(
+ $this->itemId,
+ $this->locationId,
+ 'unconfirmed',
+ null,
+ null,
+ strtotime( self::CURRENT_DATE ),
+ strtotime( '+1 day', strtotime( self::CURRENT_DATE ) ),
+ null,
+ null
+ );
+ $this->bookingIds[] = $bookingId;
+
+ remove_filter( 'commonsbooking_booking_before_save', $filter, 10 );
+
+ $this->assertIsInt( $bookingId );
+ // The meta added by the filter was persisted with the new booking.
+ $this->assertEquals( 'ext-123', get_post_meta( $bookingId, 'my_external_ref', true ) );
+ // For a new booking, the existing-booking argument is null.
+ $this->assertNull( $received );
+ }
+
public function testHandleBookingRequest_deleteUnconfirmed() {
ClockMock::freeze( new \DateTime( self::CURRENT_DATE ) );
// We create an unconfirmed booking and then cancel the booking. The booking should be canceled
@@ -163,7 +203,7 @@ public function testHandleBookingRequest_Overbooking() {
$this->assertFalse( $bookingModel->isConfirmed() );
// The overbooked days are not present anymore when confirming the booking cause they are only calculated on the Litepicker screen
- $newBookingId = Booking::handleBookingRequest(
+ $confirmedBookingId = Booking::handleBookingRequest(
$this->itemId,
$this->locationId,
'confirmed',
@@ -174,10 +214,10 @@ public function testHandleBookingRequest_Overbooking() {
$postName,
null
);
- $this->bookingIds[] = $newBookingId;
+ $this->bookingIds[] = $confirmedBookingId;
// the id should be the same
- $this->assertEquals( $bookingId, $newBookingId );
+ $this->assertEquals( $bookingId, $confirmedBookingId );
// we create a new model, just to be sure
$bookingModel = new \CommonsBooking\Model\Booking( $bookingId );
$this->assertTrue( $bookingModel->isConfirmed() );
@@ -347,6 +387,65 @@ public function testHandleBookingRequest_deleted_confirm() {
);
}
+ /**
+ * Regression test for #2217
+ * When a booking is confirmed and another booking is created in the exact same timeframe afterwards,
+ * the previously confirmed booking was set to unconfirmed again. This test is in place to ensure that
+ * this does not happen again.
+ *
+ * @return void
+ */
+ public function testHandleBookingRequest_noRecreation() {
+ $date = new \DateTime( self::CURRENT_DATE );
+ $date->modify( '-1 day' );
+ ClockMock::freeze( $date );
+ // create regular booking through unconfirmed -> confirmed route
+ $bookingId = Booking::handleBookingRequest(
+ $this->itemId,
+ $this->locationId,
+ 'unconfirmed',
+ null,
+ null,
+ strtotime( self::CURRENT_DATE ),
+ strtotime( '+1 day', strtotime( self::CURRENT_DATE ) ),
+ null,
+ null
+ );
+ $this->bookingIds[] = $bookingId;
+
+ $bookingModel = new \CommonsBooking\Model\Booking( $bookingId );
+ $postName = $bookingModel->post_name;
+ $confirmedBookingId = Booking::handleBookingRequest(
+ $this->itemId,
+ $this->locationId,
+ 'confirmed',
+ $bookingId,
+ null,
+ strtotime( self::CURRENT_DATE ),
+ strtotime( '+1 day', strtotime( self::CURRENT_DATE ) ),
+ $postName,
+ null
+ );
+ $this->bookingIds[] = $confirmedBookingId;
+
+ // attempt to recreate the booking, should keep status as "confirmed" because it was not explicitly cancelled
+ $bookingId = Booking::handleBookingRequest(
+ $this->itemId,
+ $this->locationId,
+ 'unconfirmed',
+ null,
+ null,
+ strtotime( self::CURRENT_DATE ),
+ strtotime( '+1 day', strtotime( self::CURRENT_DATE ) ),
+ null,
+ null
+ );
+ $this->bookingIds[] = $bookingId;
+
+ $bookingModel = new \CommonsBooking\Model\Booking( $bookingId );
+ $this->assertTrue( $bookingModel->isConfirmed() );
+ }
+
/**
* This will check if the bookings can be exported through the WordPress personal data export tool
* @return void
diff --git a/tests/php/Wordpress/CustomPostTypeTest.php b/tests/php/Wordpress/CustomPostTypeTest.php
index 3543bee7f..671d4f9cd 100644
--- a/tests/php/Wordpress/CustomPostTypeTest.php
+++ b/tests/php/Wordpress/CustomPostTypeTest.php
@@ -110,6 +110,9 @@ public function createCBManager() {
protected function setUp(): void {
parent::setUp();
+ Plugin::clearCache();
+ wp_cache_flush();
+
$this->dateFormatted = date( 'Y-m-d', strtotime( self::CURRENT_DATE ) );
$this->setUpBookingCodesTable();
@@ -142,6 +145,8 @@ protected function tearDown(): void {
ClockMock::reset();
$this->tearDownAllPosts();
+ Plugin::clearCache();
+ wp_cache_flush();
$this->tearDownBookingCodesTable();
wp_logout();