officernd-mcp-server
Version:
MCP server for OfficeRnD workspace management - create, search, update and cancel bookings
831 lines • 35.3 kB
JavaScript
import fetch from 'node-fetch';
import { ResourceCache } from './resource-cache.js';
import { ResourceRateCache } from './resource-rate-cache.js';
export class OfficeRnDClient {
clientId;
clientSecret;
orgSlug;
accessToken = null;
tokenExpiry = null;
baseUrl = 'https://app.officernd.com/api/v2';
identityUrl = 'https://identity.officernd.com/oauth/token';
resourceCache;
resourceRateCache;
API_LIMIT_MAX = 50;
constructor(clientId, clientSecret, orgSlug, cacheMinutes = 60) {
this.clientId = clientId;
this.clientSecret = clientSecret;
this.orgSlug = orgSlug;
this.resourceCache = new ResourceCache(cacheMinutes);
this.resourceRateCache = new ResourceRateCache(cacheMinutes);
}
/**
* Validates and normalizes the limit parameter to ensure it doesn't exceed OfficeRnD's API limit
*/
validateLimit(limit) {
if (limit === undefined) {
return undefined;
}
if (limit < 1) {
throw new Error(`Limit must be at least 1, got ${limit}`);
}
if (limit > this.API_LIMIT_MAX) {
throw new Error(`Limit cannot exceed ${this.API_LIMIT_MAX} (OfficeRnD API hard cap), got ${limit}`);
}
return limit;
}
async ensureAuthenticated() {
// Check if we have a valid token
if (this.accessToken && this.tokenExpiry && this.tokenExpiry > new Date()) {
return;
}
// Get new token
await this.authenticate();
}
async authenticate() {
const params = new URLSearchParams();
params.set('grant_type', 'client_credentials');
params.set('client_id', this.clientId);
params.set('client_secret', this.clientSecret);
params.set('scope', 'flex.billing.resourceRates.read flex.community.members.read flex.community.companies.read flex.space.locations.read flex.space.resources.read flex.space.resourceTypes.read flex.space.bookings.read flex.space.bookings.validate flex.space.bookings.create flex.space.bookings.update flex.space.bookings.delete flex.space.bookings.cancel');
try {
const response = await fetch(this.identityUrl, {
method: 'POST',
headers: {
'accept': 'application/json',
'content-type': 'application/x-www-form-urlencoded',
},
body: params,
});
if (!response.ok) {
const errorText = await response.text();
throw new Error(`Authentication failed: ${response.status} ${response.statusText} - ${errorText}`);
}
const data = await response.json();
this.accessToken = data.access_token;
// Set expiry to 5 minutes before actual expiry to be safe
this.tokenExpiry = new Date(Date.now() + (data.expires_in - 300) * 1000);
}
catch (error) {
console.error('Authentication error:', error);
throw new Error(`Failed to authenticate with OfficeRnD: ${error instanceof Error ? error.message : String(error)}`);
}
}
async searchBookings(params) {
await this.ensureAuthenticated();
const url = new URL(`${this.baseUrl}/organizations/${this.orgSlug}/bookings`);
// Add query parameters
if (params.bookingId)
url.searchParams.append('_id', params.bookingId);
if (params.companyId)
url.searchParams.append('company', params.companyId);
if (params.memberId)
url.searchParams.append('member', params.memberId);
if (params.locationId)
url.searchParams.append('location', params.locationId);
if (params.resourceId)
url.searchParams.append('resource', params.resourceId);
// Handle date filters
if (params.seriesStartFrom) {
url.searchParams.append('seriesStart[$gte]', params.seriesStartFrom);
}
if (params.seriesStartTo) {
url.searchParams.append('seriesStart[$lte]', params.seriesStartTo);
}
if (params.seriesEndFrom) {
url.searchParams.append('seriesEnd[$gte]', params.seriesEndFrom);
}
if (params.seriesEndTo) {
url.searchParams.append('seriesEnd[$lte]', params.seriesEndTo);
}
// Pagination
const validatedLimit = this.validateLimit(params.limit);
if (validatedLimit)
url.searchParams.append('$limit', validatedLimit.toString());
if (params.cursorNext)
url.searchParams.append('$cursorNext', params.cursorNext);
if (params.cursorPrev)
url.searchParams.append('$cursorPrev', params.cursorPrev);
// Field selection
if (params.fields && params.fields.length > 0) {
url.searchParams.append('$select', params.fields.join(','));
}
try {
const response = await fetch(url.toString(), {
method: 'GET',
headers: {
'accept': 'application/json',
'authorization': `Bearer ${this.accessToken}`,
},
});
if (!response.ok) {
const errorText = await response.text();
throw new Error(`API request failed: ${response.status} ${response.statusText} - ${errorText}`);
}
const data = await response.json();
return data;
}
catch (error) {
console.error('Search bookings error:', error);
throw new Error(`Failed to search bookings: ${error instanceof Error ? error.message : String(error)}`);
}
}
async getBookingDetails(bookingId) {
await this.ensureAuthenticated();
const url = `${this.baseUrl}/organizations/${this.orgSlug}/bookings/${bookingId}`;
try {
const response = await fetch(url, {
method: 'GET',
headers: {
'accept': 'application/json',
'authorization': `Bearer ${this.accessToken}`,
},
});
if (!response.ok) {
if (response.status === 404) {
throw new Error(`Booking with ID ${bookingId} not found`);
}
const errorText = await response.text();
throw new Error(`API request failed: ${response.status} ${response.statusText} - ${errorText}`);
}
const data = await response.json();
return data;
}
catch (error) {
console.error('Get booking details error:', error);
throw new Error(`Failed to get booking details: ${error instanceof Error ? error.message : String(error)}`);
}
}
/**
* Get booking details with enriched resource information
* This method fetches the full booking details and includes the resource information if available
*/
async getBookingDetailsWithResource(bookingId) {
const booking = await this.getBookingDetails(bookingId);
if (!booking.resource) {
return { booking };
}
// Try to get resource from cache first
let resource = this.resourceCache.get(booking.resource);
if (!resource) {
// Not in cache, fetch from API
try {
resource = await this.getResourceDetails(booking.resource);
// Store in cache for future use
this.resourceCache.set(resource);
}
catch (error) {
console.error(`Failed to fetch resource ${booking.resource}:`, error);
// Return booking without resource if fetch fails
return { booking };
}
}
return { booking, resource };
}
/**
* Search bookings and enrich them with resource information
* This method performs additional API calls to get full booking details including resources
*/
async searchBookingsWithResources(params) {
// First, get the basic booking search results
const searchResults = await this.searchBookings(params);
// For each booking, fetch full details to get resource information
const enrichedResults = await Promise.all(searchResults.results.map(async (basicBooking) => {
try {
return await this.getBookingDetailsWithResource(basicBooking._id);
}
catch (error) {
console.error(`Failed to enrich booking ${basicBooking._id}:`, error);
// Return basic booking if enrichment fails
return { booking: basicBooking };
}
}));
return {
rangeStart: searchResults.rangeStart,
rangeEnd: searchResults.rangeEnd,
cursorNext: searchResults.cursorNext,
cursorPrev: searchResults.cursorPrev,
results: enrichedResults,
};
}
async searchMembers(params) {
await this.ensureAuthenticated();
const url = new URL(`${this.baseUrl}/organizations/${this.orgSlug}/members`);
// Add query parameters
if (params.memberId)
url.searchParams.append('_id', params.memberId);
if (params.name)
url.searchParams.append('name', params.name);
if (params.email)
url.searchParams.append('email', params.email);
if (params.locationId)
url.searchParams.append('location', params.locationId);
if (params.companyId)
url.searchParams.append('company', params.companyId);
if (params.status)
url.searchParams.append('status', params.status);
// Handle date filters
if (params.createdAtFrom) {
url.searchParams.append('createdAt[$gte]', params.createdAtFrom);
}
if (params.createdAtTo) {
url.searchParams.append('createdAt[$lte]', params.createdAtTo);
}
if (params.modifiedAtFrom) {
url.searchParams.append('modifiedAt[$gte]', params.modifiedAtFrom);
}
if (params.modifiedAtTo) {
url.searchParams.append('modifiedAt[$lte]', params.modifiedAtTo);
}
// Pagination and sorting
const validatedLimit = this.validateLimit(params.limit);
if (validatedLimit)
url.searchParams.append('$limit', validatedLimit.toString());
if (params.cursorNext)
url.searchParams.append('$cursorNext', params.cursorNext);
if (params.cursorPrev)
url.searchParams.append('$cursorPrev', params.cursorPrev);
if (params.sort)
url.searchParams.append('$sort', params.sort);
// Field selection
if (params.fields && params.fields.length > 0) {
url.searchParams.append('$select', params.fields.join(','));
}
try {
const response = await fetch(url.toString(), {
method: 'GET',
headers: {
'accept': 'application/json',
'authorization': `Bearer ${this.accessToken}`,
},
});
if (!response.ok) {
const errorText = await response.text();
throw new Error(`API request failed: ${response.status} ${response.statusText} - ${errorText}`);
}
const data = await response.json();
return data;
}
catch (error) {
console.error('Search members error:', error);
throw new Error(`Failed to search members: ${error instanceof Error ? error.message : String(error)}`);
}
}
async getMemberDetails(memberId) {
await this.ensureAuthenticated();
const url = `${this.baseUrl}/organizations/${this.orgSlug}/members/${memberId}`;
try {
const response = await fetch(url, {
method: 'GET',
headers: {
'accept': 'application/json',
'authorization': `Bearer ${this.accessToken}`,
},
});
if (!response.ok) {
if (response.status === 404) {
throw new Error(`Member with ID ${memberId} not found`);
}
const errorText = await response.text();
throw new Error(`API request failed: ${response.status} ${response.statusText} - ${errorText}`);
}
const data = await response.json();
return data;
}
catch (error) {
console.error('Get member details error:', error);
throw new Error(`Failed to get member details: ${error instanceof Error ? error.message : String(error)}`);
}
}
async searchResources(params) {
await this.ensureAuthenticated();
const url = new URL(`${this.baseUrl}/organizations/${this.orgSlug}/resources`);
// Add query parameters
if (params.resourceId)
url.searchParams.append('_id', params.resourceId);
if (params.name)
url.searchParams.append('name', params.name);
if (params.locationId)
url.searchParams.append('location', params.locationId);
if (params.resourceTypeId)
url.searchParams.append('type', params.resourceTypeId);
// Pagination and sorting
const validatedLimit = this.validateLimit(params.limit);
if (validatedLimit)
url.searchParams.append('$limit', validatedLimit.toString());
if (params.cursorNext)
url.searchParams.append('$cursorNext', params.cursorNext);
if (params.cursorPrev)
url.searchParams.append('$cursorPrev', params.cursorPrev);
if (params.sort)
url.searchParams.append('$sort', params.sort);
// Field selection
if (params.fields && params.fields.length > 0) {
url.searchParams.append('$select', params.fields.join(','));
}
try {
const response = await fetch(url.toString(), {
method: 'GET',
headers: {
'accept': 'application/json',
'authorization': `Bearer ${this.accessToken}`,
},
});
if (!response.ok) {
const errorText = await response.text();
throw new Error(`API request failed: ${response.status} ${response.statusText} - ${errorText}`);
}
const data = await response.json();
// Cache all resources for future use
if (data.results && data.results.length > 0) {
this.resourceCache.setMany(data.results);
}
return data;
}
catch (error) {
console.error('Search resources error:', error);
throw new Error(`Failed to search resources: ${error instanceof Error ? error.message : String(error)}`);
}
}
async getResourceDetails(resourceId) {
// Check cache first
const cached = this.resourceCache.get(resourceId);
if (cached) {
return cached;
}
await this.ensureAuthenticated();
const url = `${this.baseUrl}/organizations/${this.orgSlug}/resources/${resourceId}`;
try {
const response = await fetch(url, {
method: 'GET',
headers: {
'accept': 'application/json',
'authorization': `Bearer ${this.accessToken}`,
},
});
if (!response.ok) {
if (response.status === 404) {
throw new Error(`Resource with ID ${resourceId} not found`);
}
const errorText = await response.text();
throw new Error(`API request failed: ${response.status} ${response.statusText} - ${errorText}`);
}
const data = await response.json();
// Store in cache
this.resourceCache.set(data);
return data;
}
catch (error) {
console.error('Get resource details error:', error);
throw new Error(`Failed to get resource details: ${error instanceof Error ? error.message : String(error)}`);
}
}
async getResourceTypes() {
await this.ensureAuthenticated();
const url = `${this.baseUrl}/organizations/${this.orgSlug}/resource-types`;
try {
const response = await fetch(url, {
method: 'GET',
headers: {
'accept': 'application/json',
'authorization': `Bearer ${this.accessToken}`,
},
});
if (!response.ok) {
const errorText = await response.text();
throw new Error(`API request failed: ${response.status} ${response.statusText} - ${errorText}`);
}
const data = await response.json();
return data;
}
catch (error) {
console.error('Get resource types error:', error);
throw new Error(`Failed to get resource types: ${error instanceof Error ? error.message : String(error)}`);
}
}
/**
* Preload all resources into cache
* Useful for warming up the cache with all available resources
*/
async preloadResourceCache() {
try {
const allResources = [];
let cursor;
// Fetch all resources page by page
do {
const result = await this.searchResources({
limit: this.API_LIMIT_MAX,
cursorNext: cursor
});
if (result.results && result.results.length > 0) {
allResources.push(...result.results);
}
cursor = result.cursorNext;
} while (cursor);
// Store all resources in cache
this.resourceCache.setMany(allResources);
return allResources.length;
}
catch (error) {
console.error('Failed to preload resource cache:', error);
throw new Error(`Failed to preload resources: ${error instanceof Error ? error.message : String(error)}`);
}
}
/**
* Get cache statistics
*/
getResourceCacheStats() {
return this.resourceCache.getStats();
}
/**
* Clear the resource cache
*/
clearResourceCache() {
this.resourceCache.clear();
}
async searchCompanies(params) {
await this.ensureAuthenticated();
const url = new URL(`${this.baseUrl}/organizations/${this.orgSlug}/companies`);
// Add query parameters
if (params.companyId)
url.searchParams.append('_id', params.companyId);
if (params.name)
url.searchParams.append('name', params.name);
if (params.email)
url.searchParams.append('email', params.email);
if (params.locationId)
url.searchParams.append('location', params.locationId);
if (params.status)
url.searchParams.append('status', params.status);
// Handle date filters
if (params.createdAtFrom) {
url.searchParams.append('createdAt[$gte]', params.createdAtFrom);
}
if (params.createdAtTo) {
url.searchParams.append('createdAt[$lte]', params.createdAtTo);
}
if (params.modifiedAtFrom) {
url.searchParams.append('modifiedAt[$gte]', params.modifiedAtFrom);
}
if (params.modifiedAtTo) {
url.searchParams.append('modifiedAt[$lte]', params.modifiedAtTo);
}
// Pagination and sorting
const validatedLimit = this.validateLimit(params.limit);
if (validatedLimit)
url.searchParams.append('$limit', validatedLimit.toString());
if (params.cursorNext)
url.searchParams.append('$cursorNext', params.cursorNext);
if (params.cursorPrev)
url.searchParams.append('$cursorPrev', params.cursorPrev);
if (params.sort)
url.searchParams.append('$sort', params.sort);
// Field selection
if (params.fields && params.fields.length > 0) {
url.searchParams.append('$select', params.fields.join(','));
}
try {
const response = await fetch(url.toString(), {
method: 'GET',
headers: {
'accept': 'application/json',
'authorization': `Bearer ${this.accessToken}`,
},
});
if (!response.ok) {
const errorText = await response.text();
throw new Error(`API request failed: ${response.status} ${response.statusText} - ${errorText}`);
}
const data = await response.json();
return data;
}
catch (error) {
console.error('Search companies error:', error);
throw new Error(`Failed to search companies: ${error instanceof Error ? error.message : String(error)}`);
}
}
async getCompanyDetails(companyId) {
await this.ensureAuthenticated();
const url = `${this.baseUrl}/organizations/${this.orgSlug}/companies/${companyId}`;
try {
const response = await fetch(url, {
method: 'GET',
headers: {
'accept': 'application/json',
'authorization': `Bearer ${this.accessToken}`,
},
});
if (!response.ok) {
if (response.status === 404) {
throw new Error(`Company with ID ${companyId} not found`);
}
const errorText = await response.text();
throw new Error(`API request failed: ${response.status} ${response.statusText} - ${errorText}`);
}
const data = await response.json();
return data;
}
catch (error) {
console.error('Get company details error:', error);
throw new Error(`Failed to get company details: ${error instanceof Error ? error.message : String(error)}`);
}
}
async createBooking(params, options) {
await this.ensureAuthenticated();
const url = new URL(`${this.baseUrl}/organizations/${this.orgSlug}/bookings`);
// Add isSilent query parameter if specified
if (options?.isSilent) {
url.searchParams.append('isSilent', 'true');
}
// Validate that either member or company is provided
if (!params.member && !params.company) {
throw new Error('Either member or company must be provided when creating a booking');
}
try {
const response = await fetch(url, {
method: 'POST',
headers: {
'accept': 'application/json',
'content-type': 'application/json',
'authorization': `Bearer ${this.accessToken}`,
},
body: JSON.stringify(params),
});
if (!response.ok) {
const errorText = await response.text();
throw new Error(`API request failed: ${response.status} ${response.statusText} - ${errorText}`);
}
const data = await response.json();
return data;
}
catch (error) {
console.error('Create booking error:', error);
throw new Error(`Failed to create booking: ${error instanceof Error ? error.message : String(error)}`);
}
}
async updateBooking(bookingId, params) {
await this.ensureAuthenticated();
// First, fetch the existing booking to get current values for required fields
let existingBooking;
try {
existingBooking = await this.getBookingDetails(bookingId);
}
catch (error) {
if (error instanceof Error && error.message.includes('not found')) {
throw new Error(`Booking with ID ${bookingId} not found`);
}
throw error;
}
// Merge existing values with update params
// The API requires start, end, and resource fields to be present
const updatePayload = {
start: params.start || existingBooking.start,
end: params.end || existingBooking.end,
resource: params.resource || existingBooking.resource,
// Include other fields only if they are provided in params
...(params.member !== undefined && { member: params.member }),
...(params.company !== undefined && { company: params.company }),
...(params.title !== undefined && { title: params.title }),
...(params.description !== undefined && { description: params.description }),
...(params.isTentative !== undefined && { isTentative: params.isTentative }),
...(params.isPrivate !== undefined && { isPrivate: params.isPrivate }),
...(params.isFree !== undefined && { isFree: params.isFree }),
};
const url = new URL(`${this.baseUrl}/organizations/${this.orgSlug}/bookings/${bookingId}`);
// Add isSilent parameter to avoid validation issues
url.searchParams.append('isSilent', 'true');
try {
const response = await fetch(url.toString(), {
method: 'PUT',
headers: {
'accept': 'application/json',
'content-type': 'application/json',
'authorization': `Bearer ${this.accessToken}`,
},
body: JSON.stringify(updatePayload),
});
if (!response.ok) {
const errorText = await response.text();
throw new Error(`API request failed: ${response.status} ${response.statusText} - ${errorText}`);
}
const data = await response.json();
return data;
}
catch (error) {
console.error('Update booking error:', error);
throw new Error(`Failed to update booking: ${error instanceof Error ? error.message : String(error)}`);
}
}
async cancelBooking(bookingId) {
await this.ensureAuthenticated();
const url = `${this.baseUrl}/organizations/${this.orgSlug}/bookings/${bookingId}/cancel`;
try {
const response = await fetch(url, {
method: 'POST',
headers: {
'accept': 'application/json',
'authorization': `Bearer ${this.accessToken}`,
},
});
if (!response.ok) {
if (response.status === 404) {
throw new Error(`Booking with ID ${bookingId} not found`);
}
const errorText = await response.text();
throw new Error(`API request failed: ${response.status} ${response.statusText} - ${errorText}`);
}
// The API returns the updated booking with isCancelled: true
const booking = await response.json();
return {
success: true,
booking,
message: `Booking ${bookingId} cancelled successfully`,
};
}
catch (error) {
console.error('Cancel booking error:', error);
throw new Error(`Failed to cancel booking: ${error instanceof Error ? error.message : String(error)}`);
}
}
async searchResourceRates(params) {
await this.ensureAuthenticated();
const url = new URL(`${this.baseUrl}/organizations/${this.orgSlug}/resource-rates`);
// Add query parameters
if (params.resourceRateId)
url.searchParams.append('_id', params.resourceRateId);
if (params.resourceId)
url.searchParams.append('resource', params.resourceId);
if (params.name)
url.searchParams.append('name', params.name);
// Pagination and sorting
const validatedLimit = this.validateLimit(params.limit);
if (validatedLimit)
url.searchParams.append('$limit', validatedLimit.toString());
if (params.cursorNext)
url.searchParams.append('$cursorNext', params.cursorNext);
if (params.cursorPrev)
url.searchParams.append('$cursorPrev', params.cursorPrev);
if (params.sort)
url.searchParams.append('$sort', params.sort);
// Field selection
if (params.fields && params.fields.length > 0) {
url.searchParams.append('$select', params.fields.join(','));
}
try {
const response = await fetch(url.toString(), {
method: 'GET',
headers: {
'accept': 'application/json',
'authorization': `Bearer ${this.accessToken}`,
},
});
if (!response.ok) {
const errorText = await response.text();
throw new Error(`API request failed: ${response.status} ${response.statusText} - ${errorText}`);
}
const data = await response.json();
// Cache all rates for future use
if (data.results && data.results.length > 0) {
this.resourceRateCache.setRates(data.results);
}
return data;
}
catch (error) {
console.error('Search resource rates error:', error);
throw new Error(`Failed to search resource rates: ${error instanceof Error ? error.message : String(error)}`);
}
}
async getResourceRateDetails(resourceRateId) {
// Check cache first
const cached = this.resourceRateCache.getRate(resourceRateId);
if (cached) {
return cached;
}
await this.ensureAuthenticated();
const url = `${this.baseUrl}/organizations/${this.orgSlug}/resource-rates/${resourceRateId}`;
try {
const response = await fetch(url, {
method: 'GET',
headers: {
'accept': 'application/json',
'authorization': `Bearer ${this.accessToken}`,
},
});
if (!response.ok) {
if (response.status === 404) {
throw new Error(`Resource rate with ID ${resourceRateId} not found`);
}
const errorText = await response.text();
throw new Error(`API request failed: ${response.status} ${response.statusText} - ${errorText}`);
}
const data = await response.json();
// Store in cache
this.resourceRateCache.setRate(data);
return data;
}
catch (error) {
console.error('Get resource rate details error:', error);
throw new Error(`Failed to get resource rate details: ${error instanceof Error ? error.message : String(error)}`);
}
}
/**
* Get all rates for a specific resource
*/
async getRatesForResource(resourceId) {
// Check cache first
const cached = this.resourceRateCache.getRatesForResource(resourceId);
if (cached) {
return cached;
}
// Fetch from API
const result = await this.searchResourceRates({ resourceId, limit: this.API_LIMIT_MAX });
return result.results || [];
}
/**
* Preload all resource rates into cache
*/
async preloadResourceRateCache() {
try {
const allRates = [];
let cursor;
// Fetch all rates page by page
do {
const result = await this.searchResourceRates({
limit: this.API_LIMIT_MAX,
cursorNext: cursor
});
if (result.results && result.results.length > 0) {
allRates.push(...result.results);
}
cursor = result.cursorNext;
} while (cursor);
// Store all rates in cache
this.resourceRateCache.setRates(allRates);
return allRates.length;
}
catch (error) {
console.error('Failed to preload resource rate cache:', error);
throw new Error(`Failed to preload resource rates: ${error instanceof Error ? error.message : String(error)}`);
}
}
/**
* Get resource rate cache statistics
*/
getResourceRateCacheStats() {
return this.resourceRateCache.getStats();
}
/**
* Clear the resource rate cache
*/
clearResourceRateCache() {
this.resourceRateCache.clear();
}
/**
* Get bookable resources with their rates
* This method combines resource information with pricing data
*/
async getBookableResourcesWithRates(params) {
try {
// Validate limit if provided
const validatedLimit = params?.limit ? this.validateLimit(params.limit) : this.API_LIMIT_MAX;
// First, get the resources
const resourceParams = {
locationId: params?.locationId,
resourceTypeId: params?.resourceTypeId,
limit: validatedLimit,
};
const resourcesResponse = await this.searchResources(resourceParams);
const resources = resourcesResponse.results || [];
// Filter only bookable resources
const bookableResources = resources.filter(r => r.bookable !== false);
// For each bookable resource, get its rates
const resourcesWithRates = await Promise.all(bookableResources.map(async (resource) => {
try {
const rates = await this.getRatesForResource(resource._id);
// Filter only active rates
const activeRates = rates.filter(rate => rate.isActive !== false);
return {
resource,
rates: activeRates,
};
}
catch (error) {
console.error(`Failed to get rates for resource ${resource._id}:`, error);
// Return resource with empty rates if fetch fails
return {
resource,
rates: [],
};
}
}));
// Sort by resource name
resourcesWithRates.sort((a, b) => a.resource.name.localeCompare(b.resource.name));
return resourcesWithRates;
}
catch (error) {
console.error('Failed to get bookable resources with rates:', error);
throw new Error(`Failed to get bookable resources with rates: ${error instanceof Error ? error.message : String(error)}`);
}
}
}
//# sourceMappingURL=officernd-client.js.map