n8n_node_librebooking/nodes/LibreBookingTrigger/LibreBookingTrigger.node.ts

992 lines
26 KiB
TypeScript

import {
INodeType,
INodeTypeDescription,
IPollFunctions,
INodeExecutionData,
NodeApiError,
NodeOperationError,
} from 'n8n-workflow';
interface LibreBookingSession {
sessionToken: string;
userId: number;
sessionExpires: string;
}
interface ReservationData {
referenceNumber: string;
startDate: string;
endDate: string;
title: string;
resourceId: number;
userId: number;
description?: string;
resourceName?: string;
startDateTime?: string;
endDateTime?: string;
[key: string]: any;
}
interface WorkflowStaticData {
seenIds?: string[];
reservationHashes?: Record<string, string>;
isFirstPoll?: boolean;
lastPollTime?: string;
}
/**
* Authentifizierung bei LibreBooking
*/
async function authenticateTrigger(
pollFunctions: IPollFunctions,
baseUrl: string,
username: string,
password: string,
): Promise<LibreBookingSession> {
try {
const response = await pollFunctions.helpers.httpRequest({
method: 'POST',
url: `${baseUrl}/Web/Services/index.php/Authentication/Authenticate`,
headers: { 'Content-Type': 'application/json' },
body: { username, password },
json: true,
});
if (!response.isAuthenticated) {
throw new NodeOperationError(
pollFunctions.getNode(),
'Authentifizierung fehlgeschlagen. Überprüfen Sie Ihre Zugangsdaten.',
);
}
return {
sessionToken: response.sessionToken,
userId: response.userId,
sessionExpires: response.sessionExpires,
};
} catch (error: any) {
throw new NodeApiError(pollFunctions.getNode(), error, {
message: 'Authentifizierung fehlgeschlagen',
description: 'Überprüfen Sie die LibreBooking URL und Ihre Zugangsdaten.',
});
}
}
/**
* Abmeldung
*/
async function signOutTrigger(
pollFunctions: IPollFunctions,
baseUrl: string,
session: LibreBookingSession,
): Promise<void> {
try {
await pollFunctions.helpers.httpRequest({
method: 'POST',
url: `${baseUrl}/Web/Services/index.php/Authentication/SignOut`,
headers: { 'Content-Type': 'application/json' },
body: {
userId: session.userId,
sessionToken: session.sessionToken,
},
json: true,
});
} catch (error) {
// Ignoriere SignOut-Fehler
}
}
/**
* Reservierungen abrufen
*/
async function getReservations(
pollFunctions: IPollFunctions,
baseUrl: string,
session: LibreBookingSession,
startDateTime: string,
endDateTime: string,
filters: any,
): Promise<ReservationData[]> {
const qs: any = {
startDateTime,
endDateTime,
};
if (filters.resourceId) qs.resourceId = filters.resourceId;
if (filters.scheduleId) qs.scheduleId = filters.scheduleId;
if (filters.userId) qs.userId = filters.userId;
try {
const response = await pollFunctions.helpers.httpRequest({
method: 'GET',
url: `${baseUrl}/Web/Services/index.php/Reservations/`,
headers: {
'Content-Type': 'application/json',
'X-Booked-SessionToken': session.sessionToken,
'X-Booked-UserId': session.userId.toString(),
},
qs,
json: true,
});
return response.reservations || [];
} catch (error: any) {
throw new NodeApiError(pollFunctions.getNode(), error, {
message: 'Fehler beim Abrufen der Reservierungen',
});
}
}
/**
* Detaillierte Reservierungsdaten abrufen (inkl. Custom Attributes)
*/
async function getReservationDetails(
pollFunctions: IPollFunctions,
baseUrl: string,
session: LibreBookingSession,
referenceNumber: string,
): Promise<any> {
try {
const response = await pollFunctions.helpers.httpRequest({
method: 'GET',
url: `${baseUrl}/Web/Services/index.php/Reservations/${referenceNumber}`,
headers: {
'Content-Type': 'application/json',
'X-Booked-SessionToken': session.sessionToken,
'X-Booked-UserId': session.userId.toString(),
},
json: true,
});
return response;
} catch (error) {
return null;
}
}
/**
* Berechnet Zeitraum basierend auf der gewählten Option
*/
function getDateRange(dateRange: string): { startDate: string; endDate: string } {
const now = new Date();
let startDate: Date;
let endDate: Date;
switch (dateRange) {
case 'thisWeek':
// Montag dieser Woche (ISO: Montag = 1, Sonntag = 0)
startDate = new Date(now);
const dayOfWeek = now.getDay();
const diffToMonday = dayOfWeek === 0 ? -6 : 1 - dayOfWeek;
startDate.setDate(now.getDate() + diffToMonday);
startDate.setHours(0, 0, 0, 0);
// Sonntag dieser Woche
endDate = new Date(startDate);
endDate.setDate(startDate.getDate() + 6);
endDate.setHours(23, 59, 59, 999);
break;
case 'next2Weeks':
startDate = new Date(now);
startDate.setHours(0, 0, 0, 0);
endDate = new Date(now);
endDate.setDate(now.getDate() + 14);
endDate.setHours(23, 59, 59, 999);
break;
case 'thisMonth':
startDate = new Date(now.getFullYear(), now.getMonth(), 1);
startDate.setHours(0, 0, 0, 0);
endDate = new Date(now.getFullYear(), now.getMonth() + 1, 0);
endDate.setHours(23, 59, 59, 999);
break;
case 'next2Months':
startDate = new Date(now);
startDate.setHours(0, 0, 0, 0);
endDate = new Date(now);
endDate.setMonth(now.getMonth() + 2);
endDate.setHours(23, 59, 59, 999);
break;
case 'thisYear':
startDate = new Date(now.getFullYear(), 0, 1);
startDate.setHours(0, 0, 0, 0);
endDate = new Date(now.getFullYear(), 11, 31);
endDate.setHours(23, 59, 59, 999);
break;
default: // custom
return { startDate: '', endDate: '' };
}
return {
startDate: startDate.toISOString(),
endDate: endDate.toISOString(),
};
}
/**
* Zeitfenster berechnen für "Get All" Mode
*/
function getTimeWindowForGetAll(
dateRange: string,
customStartDate?: string,
customEndDate?: string,
defaultDays: number = 14,
): { start: string; end: string } {
// Wenn ein vordefinierter Zeitraum gewählt wurde
if (dateRange && dateRange !== 'custom') {
const { startDate, endDate } = getDateRange(dateRange);
return { start: startDate, end: endDate };
}
// Custom Modus mit manuellen Daten
if (customStartDate && customEndDate) {
return {
start: new Date(customStartDate).toISOString(),
end: new Date(customEndDate).toISOString(),
};
}
// Fallback: Ab heute für defaultDays
const now = new Date();
const endDate = new Date(now);
endDate.setDate(endDate.getDate() + defaultDays);
return {
start: now.toISOString(),
end: endDate.toISOString(),
};
}
/**
* Zeitfenster berechnen für Polling
*/
function getTimeWindowForPolling(timeWindow: string): { start: string; end: string } {
const now = new Date();
const start = now.toISOString();
let endDate = new Date(now);
switch (timeWindow) {
case '7days':
endDate.setDate(endDate.getDate() + 7);
break;
case '14days':
endDate.setDate(endDate.getDate() + 14);
break;
case '30days':
endDate.setDate(endDate.getDate() + 30);
break;
case '90days':
endDate.setDate(endDate.getDate() + 90);
break;
case '180days':
endDate.setDate(endDate.getDate() + 180);
break;
default:
endDate.setDate(endDate.getDate() + 14);
}
return {
start,
end: endDate.toISOString(),
};
}
/**
* Filter Reservierungen nach Zeitpunkt
*/
function filterByTime(reservations: ReservationData[], timeFilter: string): ReservationData[] {
if (timeFilter === 'all') {
return reservations;
}
const now = new Date();
now.setHours(0, 0, 0, 0);
return reservations.filter((reservation) => {
// Verwende startDate oder startDateTime
const dateStr = reservation.startDateTime || reservation.startDate;
if (!dateStr) return true;
const startDate = new Date(dateStr);
startDate.setHours(0, 0, 0, 0);
if (timeFilter === 'today') {
return startDate.getTime() === now.getTime();
}
if (timeFilter === 'next3Days') {
const threeDaysFromNow = new Date(now);
threeDaysFromNow.setDate(now.getDate() + 3);
return startDate >= now && startDate <= threeDaysFromNow;
}
if (timeFilter === 'next7Days') {
const sevenDaysFromNow = new Date(now);
sevenDaysFromNow.setDate(now.getDate() + 7);
return startDate >= now && startDate <= sevenDaysFromNow;
}
return true;
});
}
/**
* Hash für Reservierung generieren (für Änderungserkennung)
*/
function getReservationHash(reservation: ReservationData): string {
const relevantData = {
referenceNumber: reservation.referenceNumber,
startDate: reservation.startDate || reservation.startDateTime,
endDate: reservation.endDate || reservation.endDateTime,
title: reservation.title || '',
description: reservation.description || '',
resourceId: reservation.resourceId,
resourceName: reservation.resourceName || '',
userId: reservation.userId,
requiresApproval: reservation.requiresApproval,
participants: reservation.participants || [],
invitees: reservation.invitees || [],
statusId: reservation.statusId,
};
return JSON.stringify(relevantData);
}
/**
* LibreBooking Trigger Node
*
* Drei Modi:
* 1. Get All (One-Time): Alle Reservierungen für einen Zeitraum abrufen
* 2. New Reservations (Poll): Bei neuen Reservierungen triggern
* 3. Updated Reservations (Poll): Bei geänderten Reservierungen triggern
*/
export class LibreBookingTrigger implements INodeType {
description: INodeTypeDescription = {
displayName: 'LibreBooking Trigger',
name: 'libreBookingTrigger',
icon: 'file:librebooking.svg',
group: ['trigger'],
version: 1,
description: 'Wird bei neuen oder geänderten Reservierungen in LibreBooking ausgelöst',
subtitle: '={{$parameter["triggerMode"]}}',
defaults: {
name: 'LibreBooking Trigger',
},
inputs: [],
outputs: ['main'],
credentials: [
{
name: 'libreBookingApi',
required: true,
},
],
polling: true,
properties: [
// =====================================================
// TRIGGER MODE SELECTOR
// =====================================================
{
displayName: 'Trigger-Modus',
name: 'triggerMode',
type: 'options',
options: [
{
name: 'Alle Abrufen (Einmalig)',
value: 'getAll',
description: 'Alle Reservierungen für einen Zeitraum abrufen (bei jedem Poll)',
},
{
name: 'Neue Reservierungen (Polling)',
value: 'newReservations',
description: 'Nur bei neuen Reservierungen triggern',
},
{
name: 'Geänderte Reservierungen (Polling)',
value: 'updatedReservations',
description: 'Nur bei geänderten Reservierungen triggern',
},
],
default: 'getAll',
description: 'Wählen Sie den Trigger-Modus',
},
// =====================================================
// GET ALL MODE - DATE RANGE SELECTOR
// =====================================================
{
displayName: 'Zeitraum',
name: 'dateRange',
type: 'options',
displayOptions: {
show: {
triggerMode: ['getAll'],
},
},
options: [
{
name: 'Benutzerdefiniert',
value: 'custom',
description: 'Start- und Enddatum manuell angeben',
},
{
name: 'Diese Woche',
value: 'thisWeek',
description: 'Von Montag bis Sonntag der aktuellen Woche',
},
{
name: 'Nächste 2 Wochen',
value: 'next2Weeks',
description: 'Ab heute bis 14 Tage in die Zukunft',
},
{
name: 'Dieser Monat',
value: 'thisMonth',
description: 'Vom 1. bis zum letzten Tag des aktuellen Monats',
},
{
name: 'Nächste 2 Monate',
value: 'next2Months',
description: 'Ab heute bis 2 Monate in die Zukunft',
},
{
name: 'Dieses Jahr',
value: 'thisYear',
description: 'Vom 1. Januar bis 31. Dezember des aktuellen Jahres',
},
],
default: 'custom',
description: 'Vordefinierter Zeitraum für den Abruf',
},
{
displayName: 'Startdatum',
name: 'startDate',
type: 'dateTime',
displayOptions: {
show: {
triggerMode: ['getAll'],
dateRange: ['custom'],
},
},
default: '',
description: 'Startdatum für den Abruf (leer = heute)',
},
{
displayName: 'Enddatum',
name: 'endDate',
type: 'dateTime',
displayOptions: {
show: {
triggerMode: ['getAll'],
dateRange: ['custom'],
},
},
default: '',
description: 'Enddatum für den Abruf (leer = 14 Tage in der Zukunft)',
},
// =====================================================
// POLLING MODE - TIME WINDOW
// =====================================================
{
displayName: 'Zeitfenster',
name: 'timeWindow',
type: 'options',
displayOptions: {
show: {
triggerMode: ['newReservations', 'updatedReservations'],
},
},
options: [
{ name: 'Nächste 7 Tage', value: '7days' },
{ name: 'Nächste 14 Tage', value: '14days' },
{ name: 'Nächste 30 Tage', value: '30days' },
{ name: 'Nächste 90 Tage', value: '90days' },
{ name: 'Nächste 180 Tage (6 Monate)', value: '180days' },
],
default: '14days',
description: 'Zeitfenster für die Überwachung von Reservierungen',
},
// =====================================================
// TIME FILTER FOR NEW/UPDATED MODES
// =====================================================
{
displayName: 'Zeit-Filter',
name: 'timeFilter',
type: 'options',
displayOptions: {
show: {
triggerMode: ['newReservations', 'updatedReservations'],
},
},
options: [
{
name: 'Alle (Kein Filter)',
value: 'all',
description: 'Alle neuen/geänderten Reservierungen, unabhängig vom Datum',
},
{
name: 'Nur Heute',
value: 'today',
description: 'Nur Reservierungen, die heute stattfinden',
},
{
name: 'Nächste 3 Tage',
value: 'next3Days',
description: 'Nur Reservierungen, die in den nächsten 3 Tagen stattfinden',
},
{
name: 'Nächste 7 Tage',
value: 'next7Days',
description: 'Nur Reservierungen, die in den nächsten 7 Tagen stattfinden',
},
],
default: 'all',
description: 'Filtert Reservierungen nach ihrem Startdatum',
},
{
displayName: 'Hinweis',
name: 'pollingNotice',
type: 'notice',
default: '',
displayOptions: {
show: {
triggerMode: ['newReservations', 'updatedReservations'],
},
},
description:
'Beim ersten Poll werden existierende Reservierungen gespeichert, aber nicht getriggert. Nur nachfolgende Änderungen lösen den Trigger aus.',
},
// =====================================================
// FILTERS (ALL MODES)
// =====================================================
{
displayName: 'Filter',
name: 'filters',
type: 'collection',
placeholder: 'Filter hinzufügen',
default: {},
options: [
{
displayName: 'Ressourcen-ID',
name: 'resourceId',
type: 'number',
default: '',
description: 'Nur Reservierungen für diese Ressource',
},
{
displayName: 'Zeitplan-ID',
name: 'scheduleId',
type: 'number',
default: '',
description: 'Nur Reservierungen für diesen Zeitplan',
},
{
displayName: 'Benutzer-ID',
name: 'userId',
type: 'number',
default: '',
description: 'Nur Reservierungen für diesen Benutzer',
},
],
},
// =====================================================
// OPTIONS (ALL MODES)
// =====================================================
{
displayName: 'Optionen',
name: 'options',
type: 'collection',
placeholder: 'Option hinzufügen',
default: {},
options: [
{
displayName: 'Detaillierte Daten Abrufen',
name: 'fetchDetails',
type: 'boolean',
default: false,
description:
'Ruft vollständige Reservierungsdaten inkl. Custom Attributes ab (zusätzliche API-Aufrufe)',
},
{
displayName: 'Debug-Modus',
name: 'debugMode',
type: 'boolean',
default: false,
description: 'Gibt zusätzliche Debug-Informationen aus',
},
],
},
],
};
async poll(this: IPollFunctions): Promise<INodeExecutionData[][] | null> {
const credentials = await this.getCredentials('libreBookingApi');
const baseUrl = (credentials.url as string).replace(/\/$/, '');
const username = credentials.username as string;
const password = credentials.password as string;
const triggerMode = this.getNodeParameter('triggerMode') as string;
const filters = this.getNodeParameter('filters', {}) as any;
const options = this.getNodeParameter('options', {}) as any;
// Debug-Modus
const debugMode = options.debugMode || false;
const fetchDetails = options.fetchDetails || false;
// Workflow Static Data für State-Management
const webhookData = this.getWorkflowStaticData('node') as WorkflowStaticData;
let session: LibreBookingSession;
try {
session = await authenticateTrigger(this, baseUrl, username, password);
} catch (error) {
throw error;
}
try {
const returnData: INodeExecutionData[] = [];
// ==========================================
// MODE: Get All (One-Time / Every Poll)
// ==========================================
if (triggerMode === 'getAll') {
const dateRange = this.getNodeParameter('dateRange', 'custom') as string;
const startDate = this.getNodeParameter('startDate', '') as string;
const endDate = this.getNodeParameter('endDate', '') as string;
const { start, end } = getTimeWindowForGetAll(
dateRange,
startDate || undefined,
endDate || undefined,
14,
);
const reservations = await getReservations(this, baseUrl, session, start, end, filters);
if (debugMode) {
console.log(
`[LibreBooking Trigger] Get All Mode - Found ${reservations.length} reservations`,
);
console.log(`[LibreBooking Trigger] Date Range: ${dateRange}`);
console.log(`[LibreBooking Trigger] Period: ${start} to ${end}`);
}
if (reservations.length === 0) {
if (debugMode) {
return [
[
{
json: {
_debug: true,
_message: 'Keine Reservierungen im Zeitraum gefunden',
_dateRange: dateRange,
_startDate: start,
_endDate: end,
_count: 0,
},
},
],
];
}
return null;
}
// Alle Reservierungen zurückgeben
for (const reservation of reservations) {
let reservationData = reservation;
if (fetchDetails) {
try {
const details = await getReservationDetails(
this,
baseUrl,
session,
reservation.referenceNumber,
);
if (details) {
reservationData = details;
}
} catch (error) {
// Fallback auf Basisdaten
}
}
returnData.push({
json: {
...reservationData,
_eventType: 'getAll',
_dateRange: dateRange,
_triggeredAt: new Date().toISOString(),
},
});
}
}
// ==========================================
// MODE: New Reservations (Polling)
// ==========================================
else if (triggerMode === 'newReservations') {
const timeWindow = this.getNodeParameter('timeWindow', '14days') as string;
const timeFilter = this.getNodeParameter('timeFilter', 'all') as string;
const { start, end } = getTimeWindowForPolling(timeWindow);
const reservations = await getReservations(this, baseUrl, session, start, end, filters);
// Initialisiere seenIds beim ersten Poll
if (!webhookData.seenIds) {
webhookData.seenIds = [];
webhookData.isFirstPoll = true;
}
const currentIds = reservations.map((r: ReservationData) => r.referenceNumber);
if (debugMode) {
console.log(`[LibreBooking Trigger] New Reservations Mode`);
console.log(`[LibreBooking Trigger] First Poll: ${webhookData.isFirstPoll}`);
console.log(`[LibreBooking Trigger] Time Filter: ${timeFilter}`);
console.log(
`[LibreBooking Trigger] Current IDs: ${currentIds.length}, Seen IDs: ${webhookData.seenIds.length}`,
);
}
// Beim ersten Poll: Nur IDs speichern, NICHT triggern
if (webhookData.isFirstPoll) {
webhookData.seenIds = currentIds;
webhookData.isFirstPoll = false;
webhookData.lastPollTime = new Date().toISOString();
if (debugMode) {
return [
[
{
json: {
_debug: true,
_message: 'Erster Poll - IDs wurden gespeichert, keine Events getriggert',
_savedIds: currentIds.length,
_ids: currentIds,
_timestamp: webhookData.lastPollTime,
},
},
],
];
}
return null; // Nichts triggern beim ersten Poll
}
// Nur NEUE Reservierungen (die wir noch nicht gesehen haben)
let newReservations = reservations.filter(
(r: ReservationData) => !webhookData.seenIds!.includes(r.referenceNumber),
);
// Update seenIds mit allen aktuellen IDs
webhookData.seenIds = currentIds;
webhookData.lastPollTime = new Date().toISOString();
if (debugMode) {
console.log(
`[LibreBooking Trigger] Found ${newReservations.length} new reservations before filter`,
);
}
if (newReservations.length === 0) {
return null;
}
// Zeit-Filter anwenden
newReservations = filterByTime(newReservations, timeFilter);
if (debugMode) {
console.log(
`[LibreBooking Trigger] ${newReservations.length} reservations after time filter (${timeFilter})`,
);
}
if (newReservations.length === 0) {
return null;
}
// Neue Reservierungen verarbeiten
for (const reservation of newReservations) {
let reservationData = reservation;
if (fetchDetails) {
try {
const details = await getReservationDetails(
this,
baseUrl,
session,
reservation.referenceNumber,
);
if (details) {
reservationData = details;
}
} catch (error) {
// Fallback auf Basisdaten
}
}
returnData.push({
json: {
...reservationData,
_eventType: 'new',
_timeFilter: timeFilter,
_triggeredAt: new Date().toISOString(),
},
});
}
if (debugMode && returnData.length > 0) {
console.log(`[LibreBooking Trigger] Triggering ${returnData.length} new reservations`);
}
}
// ==========================================
// MODE: Updated Reservations (Polling)
// ==========================================
else if (triggerMode === 'updatedReservations') {
const timeWindow = this.getNodeParameter('timeWindow', '14days') as string;
const timeFilter = this.getNodeParameter('timeFilter', 'all') as string;
const { start, end } = getTimeWindowForPolling(timeWindow);
const reservations = await getReservations(this, baseUrl, session, start, end, filters);
// Initialisiere reservationHashes beim ersten Poll
if (!webhookData.reservationHashes) {
webhookData.reservationHashes = {};
webhookData.isFirstPoll = true;
}
if (debugMode) {
console.log(`[LibreBooking Trigger] Updated Reservations Mode`);
console.log(`[LibreBooking Trigger] First Poll: ${webhookData.isFirstPoll}`);
console.log(`[LibreBooking Trigger] Time Filter: ${timeFilter}`);
console.log(
`[LibreBooking Trigger] Current: ${reservations.length}, Stored hashes: ${Object.keys(webhookData.reservationHashes).length}`,
);
}
// Beim ersten Poll: Nur Hashes speichern, NICHT triggern
if (webhookData.isFirstPoll) {
for (const reservation of reservations) {
webhookData.reservationHashes[reservation.referenceNumber] =
getReservationHash(reservation);
}
webhookData.isFirstPoll = false;
webhookData.lastPollTime = new Date().toISOString();
if (debugMode) {
return [
[
{
json: {
_debug: true,
_message: 'Erster Poll - Hashes wurden gespeichert, keine Events getriggert',
_savedHashes: Object.keys(webhookData.reservationHashes).length,
_timestamp: webhookData.lastPollTime,
},
},
],
];
}
return null; // Nichts triggern beim ersten Poll
}
// Geänderte Reservierungen finden
let updatedReservations: ReservationData[] = [];
const newHashes: Record<string, string> = {};
for (const reservation of reservations) {
const currentHash = getReservationHash(reservation);
const oldHash = webhookData.reservationHashes[reservation.referenceNumber];
newHashes[reservation.referenceNumber] = currentHash;
// Nur als "geändert" markieren, wenn:
// 1. Wir die Reservierung schon kennen (nicht neu)
// 2. Der Hash sich geändert hat
if (oldHash && currentHash !== oldHash) {
updatedReservations.push(reservation);
}
}
// Update Hashes mit allen aktuellen Reservierungen
webhookData.reservationHashes = newHashes;
webhookData.lastPollTime = new Date().toISOString();
if (debugMode) {
console.log(
`[LibreBooking Trigger] Found ${updatedReservations.length} updated reservations before filter`,
);
}
if (updatedReservations.length === 0) {
return null;
}
// Zeit-Filter anwenden
updatedReservations = filterByTime(updatedReservations, timeFilter);
if (debugMode) {
console.log(
`[LibreBooking Trigger] ${updatedReservations.length} reservations after time filter (${timeFilter})`,
);
}
if (updatedReservations.length === 0) {
return null;
}
// Geänderte Reservierungen verarbeiten
for (const reservation of updatedReservations) {
let reservationData = reservation;
if (fetchDetails) {
try {
const details = await getReservationDetails(
this,
baseUrl,
session,
reservation.referenceNumber,
);
if (details) {
reservationData = details;
}
} catch (error) {
// Fallback auf Basisdaten
}
}
returnData.push({
json: {
...reservationData,
_eventType: 'updated',
_timeFilter: timeFilter,
_triggeredAt: new Date().toISOString(),
},
});
}
if (debugMode && returnData.length > 0) {
console.log(
`[LibreBooking Trigger] Triggering ${returnData.length} updated reservations`,
);
}
}
if (returnData.length === 0) {
return null;
}
return [returnData];
} finally {
await signOutTrigger(this, baseUrl, session);
}
}
}