@pinecall/pinecall-booking-sdk 1.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Pinecall
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,340 @@
1
+ <div align="center">
2
+
3
+ # @pinecall/pinecall-booking-sdk
4
+
5
+ **Official JavaScript SDK for the Pinecall Booking API**
6
+
7
+ [![npm version](https://img.shields.io/npm/v/@pinecall/pinecall-booking-sdk.svg)](https://www.npmjs.com/package/@pinecall/pinecall-booking-sdk)
8
+ [![Node.js](https://img.shields.io/badge/node-%3E%3D18.0.0-brightgreen.svg)](https://nodejs.org/)
9
+ [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE)
10
+
11
+ [Installation](#installation) •
12
+ [Quick Start](#quick-start) •
13
+ [API Reference](#api-reference) •
14
+ [Examples](#examples) •
15
+ [License](#license)
16
+
17
+ </div>
18
+
19
+ ---
20
+
21
+ ## Installation
22
+
23
+ ```bash
24
+ npm install @pinecall/pinecall-booking-sdk
25
+ ```
26
+
27
+ ---
28
+
29
+ ## Quick Start
30
+
31
+ ```javascript
32
+ import { Pinecall } from '@pinecall/pinecall-booking-sdk';
33
+
34
+ const client = new Pinecall('pk_your_api_key');
35
+
36
+ // Create a restaurant booking system
37
+ const config = await client.configurations.create({
38
+ name: 'My Restaurant',
39
+ businessType: 'restaurant',
40
+ resourceDefinition: {
41
+ name: 'table',
42
+ pluralName: 'tables',
43
+ attributes: [
44
+ { key: 'seats', type: 'number', required: true }
45
+ ]
46
+ },
47
+ businessRules: {
48
+ minBookingTime: '1h',
49
+ maxBookingTime: '3h',
50
+ bufferTime: '15m'
51
+ }
52
+ });
53
+
54
+ // Create tables
55
+ await client.resources.bulkCreate({
56
+ configurationId: config.configuration._id,
57
+ resources: [
58
+ { name: 'Table 1', attributes: { seats: 2 } },
59
+ { name: 'Table 2', attributes: { seats: 4 } },
60
+ { name: 'Table 3', attributes: { seats: 6 } }
61
+ ]
62
+ });
63
+
64
+ // Check availability
65
+ const availability = await client.availability.query({
66
+ configurationId: config.configuration._id,
67
+ startDate: '2025-01-20',
68
+ endDate: '2025-01-20'
69
+ });
70
+
71
+ // Create a booking
72
+ const booking = await client.bookings.create({
73
+ configurationId: config.configuration._id,
74
+ resourceId: availability.resources[0]._id,
75
+ startTime: '2025-01-20T19:00:00Z',
76
+ endTime: '2025-01-20T21:00:00Z',
77
+ customer: {
78
+ name: 'John Doe',
79
+ email: 'john@example.com',
80
+ phone: '+1234567890'
81
+ },
82
+ attributes: {
83
+ partySize: 4
84
+ }
85
+ });
86
+
87
+ // Confirm the booking
88
+ await client.bookings.confirm(booking.booking._id);
89
+ ```
90
+
91
+ ---
92
+
93
+ ## API Reference
94
+
95
+ ### Client Initialization
96
+
97
+ ```javascript
98
+ import { Pinecall } from '@pinecall/pinecall-booking-sdk';
99
+
100
+ // Simple
101
+ const client = new Pinecall('pk_your_api_key');
102
+
103
+ // With options
104
+ const client = new Pinecall('pk_your_api_key', {
105
+ baseUrl: 'https://api.example.com', // Custom API URL
106
+ timeout: 30000, // Request timeout (ms)
107
+ maxRetries: 3 // Retry attempts
108
+ });
109
+ ```
110
+
111
+ ### Configurations
112
+
113
+ ```javascript
114
+ // List all configurations
115
+ const configs = await client.configurations.list();
116
+
117
+ // Get a specific configuration
118
+ const config = await client.configurations.get('config_id');
119
+
120
+ // Create a configuration
121
+ const config = await client.configurations.create({
122
+ name: 'My Business',
123
+ businessType: 'restaurant',
124
+ resourceDefinition: { ... },
125
+ bookingDefinition: { ... },
126
+ businessRules: { ... },
127
+ defaultSchedule: [ ... ]
128
+ });
129
+
130
+ // Update a configuration
131
+ await client.configurations.update('config_id', { name: 'New Name' });
132
+ ```
133
+
134
+ ### Resources
135
+
136
+ ```javascript
137
+ // List resources
138
+ const resources = await client.resources.list('config_id');
139
+
140
+ // List with filters
141
+ const tables = await client.resources.list('config_id', {
142
+ seats: { $gte: 4 }
143
+ });
144
+
145
+ // Get a resource
146
+ const resource = await client.resources.get('resource_id');
147
+
148
+ // Create a resource
149
+ const resource = await client.resources.create({
150
+ configurationId: 'config_id',
151
+ name: 'Table 5',
152
+ attributes: { seats: 4, location: 'terrace' }
153
+ });
154
+
155
+ // Bulk create resources
156
+ const result = await client.resources.bulkCreate({
157
+ configurationId: 'config_id',
158
+ resources: [
159
+ { name: 'Table 1', attributes: { seats: 2 } },
160
+ { name: 'Table 2', attributes: { seats: 4 } }
161
+ ]
162
+ });
163
+
164
+ // Update a resource
165
+ await client.resources.update('resource_id', { name: 'VIP Table' });
166
+
167
+ // Delete a resource
168
+ await client.resources.delete('resource_id');
169
+ ```
170
+
171
+ ### Availability
172
+
173
+ ```javascript
174
+ // Query available slots
175
+ const availability = await client.availability.query({
176
+ configurationId: 'config_id',
177
+ startDate: '2025-01-20',
178
+ endDate: '2025-01-20'
179
+ });
180
+
181
+ // Query with filters
182
+ const availability = await client.availability.query({
183
+ configurationId: 'config_id',
184
+ startDate: '2025-01-20',
185
+ endDate: '2025-01-20',
186
+ duration: 60,
187
+ resourceFilters: { seats: { $gte: 4 } }
188
+ });
189
+
190
+ // Check specific slot
191
+ const isAvailable = await client.availability.check(
192
+ 'resource_id',
193
+ '2025-01-20T19:00:00Z',
194
+ '2025-01-20T21:00:00Z'
195
+ );
196
+ ```
197
+
198
+ ### Bookings
199
+
200
+ ```javascript
201
+ // Create a booking
202
+ const booking = await client.bookings.create({
203
+ configurationId: 'config_id',
204
+ resourceId: 'resource_id',
205
+ startTime: '2025-01-20T19:00:00Z',
206
+ endTime: '2025-01-20T21:00:00Z',
207
+ customer: {
208
+ name: 'John Doe',
209
+ email: 'john@example.com'
210
+ },
211
+ attributes: {
212
+ partySize: 4
213
+ }
214
+ });
215
+
216
+ // Get a booking
217
+ const booking = await client.bookings.get('booking_id');
218
+
219
+ // List bookings
220
+ const bookings = await client.bookings.list({
221
+ configurationId: 'config_id',
222
+ startDate: '2025-01-01',
223
+ endDate: '2025-01-31'
224
+ });
225
+
226
+ // List with filters
227
+ const bookings = await client.bookings.list({
228
+ configurationId: 'config_id',
229
+ startDate: '2025-01-01',
230
+ endDate: '2025-01-31',
231
+ status: 'confirmed',
232
+ resourceId: 'resource_id'
233
+ });
234
+
235
+ // Confirm a booking
236
+ await client.bookings.confirm('booking_id');
237
+
238
+ // Cancel a booking
239
+ await client.bookings.cancel('booking_id', 'Customer request');
240
+
241
+ // Reschedule a booking
242
+ await client.bookings.reschedule(
243
+ 'booking_id',
244
+ '2025-01-21T19:00:00Z',
245
+ '2025-01-21T21:00:00Z'
246
+ );
247
+
248
+ // Mark as completed
249
+ await client.bookings.complete('booking_id');
250
+
251
+ // Mark as no-show
252
+ await client.bookings.noShow('booking_id');
253
+
254
+ // Get statistics
255
+ const stats = await client.bookings.stats('config_id', '2025-01-01', '2025-01-31');
256
+ ```
257
+
258
+ ### Static Helpers
259
+
260
+ ```javascript
261
+ import { Pinecall } from '@pinecall/pinecall-booking-sdk';
262
+
263
+ // Get date N days in the future (YYYY-MM-DD)
264
+ Pinecall.futureDate(7); // '2025-01-27'
265
+
266
+ // Get datetime N days in the future at specific time
267
+ Pinecall.futureDateTime(7, 19, 0); // '2025-01-27T19:00:00.000Z'
268
+
269
+ // Format date for API
270
+ Pinecall.formatDate(new Date()); // '2025-01-20'
271
+
272
+ // Format datetime for API
273
+ Pinecall.formatDateTime(new Date()); // '2025-01-20T15:30:00.000Z'
274
+ ```
275
+
276
+ ---
277
+
278
+ ## Error Handling
279
+
280
+ ```javascript
281
+ import { Pinecall, PinecallError, ValidationError, NotFoundError, ConflictError } from '@pinecall/pinecall-booking-sdk';
282
+
283
+ try {
284
+ const booking = await client.bookings.create({ ... });
285
+ } catch (error) {
286
+ if (error instanceof ValidationError) {
287
+ console.log('Invalid data:', error.message);
288
+ } else if (error instanceof NotFoundError) {
289
+ console.log('Resource not found:', error.message);
290
+ } else if (error instanceof ConflictError) {
291
+ console.log('Booking conflict:', error.message);
292
+ } else if (error instanceof PinecallError) {
293
+ console.log('API error:', error.message, error.statusCode);
294
+ }
295
+ }
296
+ ```
297
+
298
+ ---
299
+
300
+ ## Constants
301
+
302
+ ```javascript
303
+ import { BOOKING_STATUS, BUSINESS_TYPES } from '@pinecall/pinecall-booking-sdk';
304
+
305
+ BOOKING_STATUS.PENDING // 'pending'
306
+ BOOKING_STATUS.CONFIRMED // 'confirmed'
307
+ BOOKING_STATUS.CANCELLED // 'cancelled'
308
+ BOOKING_STATUS.COMPLETED // 'completed'
309
+ BOOKING_STATUS.NO_SHOW // 'no_show'
310
+
311
+ BUSINESS_TYPES.RESTAURANT // 'restaurant'
312
+ BUSINESS_TYPES.SPA // 'spa'
313
+ BUSINESS_TYPES.HOTEL // 'hotel'
314
+ BUSINESS_TYPES.CLINIC // 'clinic'
315
+ BUSINESS_TYPES.SPORTS // 'sports'
316
+ BUSINESS_TYPES.COWORKING // 'coworking'
317
+ BUSINESS_TYPES.CUSTOM // 'custom'
318
+ ```
319
+
320
+ ---
321
+
322
+ ## Examples
323
+
324
+ See the [examples](./examples) directory for complete working examples:
325
+
326
+ - [Restaurant Booking](./examples/basic.js) - Tables, time slots, reservations
327
+ - [Spa Appointments](./examples/spa.js) - Therapists, treatments, scheduling
328
+
329
+ ---
330
+
331
+ ## Requirements
332
+
333
+ - Node.js >= 18.0.0
334
+ - ES Modules support
335
+
336
+ ---
337
+
338
+ ## License
339
+
340
+ MIT © [Pinecall](https://pinecall.io)
package/package.json ADDED
@@ -0,0 +1,47 @@
1
+ {
2
+ "name": "@pinecall/pinecall-booking-sdk",
3
+ "version": "1.0.1",
4
+ "description": "Simple, elegant SDK for Pinecall Booking API. Zero dependencies.",
5
+ "main": "src/index.js",
6
+ "types": "src/index.d.ts",
7
+ "type": "module",
8
+ "exports": {
9
+ ".": {
10
+ "import": "./src/index.js",
11
+ "require": "./src/index.cjs"
12
+ }
13
+ },
14
+ "scripts": {
15
+ "test": "node --test test/*.test.js",
16
+ "test:watch": "node --test --watch test/*.test.js",
17
+ "example": "node examples/basic.js"
18
+ },
19
+ "keywords": [
20
+ "booking",
21
+ "reservations",
22
+ "scheduling",
23
+ "api",
24
+ "sdk",
25
+ "pinecall",
26
+ "restaurant",
27
+ "appointments"
28
+ ],
29
+ "author": "Pinecall <support@pinecall.com>",
30
+ "license": "MIT",
31
+ "repository": {
32
+ "type": "git",
33
+ "url": "git+https://github.com/pinecall/pinecall-booking-sdk.git"
34
+ },
35
+ "bugs": {
36
+ "url": "https://github.com/pinecall/pinecall-booking-sdk/issues"
37
+ },
38
+ "homepage": "https://pinecall.io/docs/pinecall-booking-sdk",
39
+ "engines": {
40
+ "node": ">=18.0.0"
41
+ },
42
+ "files": [
43
+ "src",
44
+ "README.md",
45
+ "LICENSE"
46
+ ]
47
+ }
package/src/client.js ADDED
@@ -0,0 +1,121 @@
1
+ import { HttpClient } from './http.js';
2
+ import { Configurations } from './resources/configurations.js';
3
+ import { Resources } from './resources/resources.js';
4
+ import { Availability } from './resources/availability.js';
5
+ import { Bookings } from './resources/bookings.js';
6
+ import { ValidationError } from './errors.js';
7
+
8
+ /**
9
+ * Pinecall Booking SDK Client
10
+ *
11
+ * A simple, elegant client for the Pinecall Booking API.
12
+ * Inspired by Stripe's SDK design.
13
+ *
14
+ * @example
15
+ * ```js
16
+ * import { Pinecall } from '@pinecall/booking-sdk';
17
+ *
18
+ * const client = new Pinecall('pk_your_api_key');
19
+ *
20
+ * // Or with options
21
+ * const client = new Pinecall('pk_your_api_key', {
22
+ * baseUrl: 'https://api.pinecall.com',
23
+ * timeout: 30000
24
+ * });
25
+ * ```
26
+ */
27
+ export class Pinecall {
28
+ /** @type {Configurations} */
29
+ configurations;
30
+
31
+ /** @type {Resources} */
32
+ resources;
33
+
34
+ /** @type {Availability} */
35
+ availability;
36
+
37
+ /** @type {Bookings} */
38
+ bookings;
39
+
40
+ /**
41
+ * Create a new Pinecall client
42
+ * @param {string} apiKey - Your Pinecall API key
43
+ * @param {Object} [options]
44
+ * @param {string} [options.baseUrl='https://api.pinecall.com'] - API base URL
45
+ * @param {number} [options.timeout=30000] - Request timeout in milliseconds
46
+ * @param {number} [options.maxRetries=3] - Maximum retry attempts
47
+ */
48
+ constructor(apiKey, options = {}) {
49
+ if (!apiKey || typeof apiKey !== 'string') {
50
+ throw new ValidationError('API key is required');
51
+ }
52
+
53
+ const http = new HttpClient({
54
+ apiKey,
55
+ baseUrl: options.baseUrl || 'https://api.pinecall.com',
56
+ timeout: options.timeout || 30000,
57
+ maxRetries: options.maxRetries || 3
58
+ });
59
+
60
+ // Initialize resource managers
61
+ this.configurations = new Configurations(http);
62
+ this.resources = new Resources(http);
63
+ this.availability = new Availability(http);
64
+ this.bookings = new Bookings(http);
65
+ }
66
+
67
+ // ═══════════════════════════════════════════════════════════════
68
+ // STATIC HELPERS
69
+ // ═══════════════════════════════════════════════════════════════
70
+
71
+ /**
72
+ * Get a date string for N days in the future
73
+ * @param {number} [days=1] - Days ahead
74
+ * @returns {string} Date string (YYYY-MM-DD)
75
+ *
76
+ * @example
77
+ * Pinecall.futureDate(7) // '2025-01-27'
78
+ */
79
+ static futureDate(days = 1) {
80
+ const date = new Date();
81
+ date.setDate(date.getDate() + days);
82
+ return date.toISOString().split('T')[0];
83
+ }
84
+
85
+ /**
86
+ * Get a datetime string for N days in the future at specific time
87
+ * @param {number} [days=1] - Days ahead
88
+ * @param {number} [hour=12] - Hour (0-23)
89
+ * @param {number} [minute=0] - Minute (0-59)
90
+ * @returns {string} ISO 8601 datetime string
91
+ *
92
+ * @example
93
+ * Pinecall.futureDateTime(7, 19, 0) // '2025-01-27T19:00:00.000Z'
94
+ */
95
+ static futureDateTime(days = 1, hour = 12, minute = 0) {
96
+ const date = new Date();
97
+ date.setDate(date.getDate() + days);
98
+ date.setUTCHours(hour, minute, 0, 0);
99
+ return date.toISOString();
100
+ }
101
+
102
+ /**
103
+ * Format a Date object to API date format
104
+ * @param {Date|string} date - Date to format
105
+ * @returns {string} Date string (YYYY-MM-DD)
106
+ */
107
+ static formatDate(date) {
108
+ const d = typeof date === 'string' ? new Date(date) : date;
109
+ return d.toISOString().split('T')[0];
110
+ }
111
+
112
+ /**
113
+ * Format a Date object to API datetime format
114
+ * @param {Date|string} date - Date to format
115
+ * @returns {string} ISO 8601 datetime string
116
+ */
117
+ static formatDateTime(date) {
118
+ const d = typeof date === 'string' ? new Date(date) : date;
119
+ return d.toISOString();
120
+ }
121
+ }
@@ -0,0 +1,41 @@
1
+ /**
2
+ * Booking status constants
3
+ * @readonly
4
+ * @enum {string}
5
+ */
6
+ export const BOOKING_STATUS = Object.freeze({
7
+ PENDING: 'pending',
8
+ CONFIRMED: 'confirmed',
9
+ CANCELLED: 'cancelled',
10
+ COMPLETED: 'completed',
11
+ NO_SHOW: 'no_show'
12
+ });
13
+
14
+ /**
15
+ * Common business types
16
+ * @readonly
17
+ * @enum {string}
18
+ */
19
+ export const BUSINESS_TYPES = Object.freeze({
20
+ RESTAURANT: 'restaurant',
21
+ SPA: 'spa',
22
+ SALON: 'salon',
23
+ HOTEL: 'hotel',
24
+ SPORTS: 'sports',
25
+ COWORKING: 'coworking',
26
+ MEDICAL: 'medical',
27
+ CUSTOM: 'custom'
28
+ });
29
+
30
+ /**
31
+ * Booking actions
32
+ * @readonly
33
+ * @enum {string}
34
+ */
35
+ export const BOOKING_ACTIONS = Object.freeze({
36
+ CONFIRM: 'confirm',
37
+ CANCEL: 'cancel',
38
+ COMPLETE: 'complete',
39
+ NO_SHOW: 'no_show',
40
+ RESCHEDULE: 'reschedule'
41
+ });
package/src/errors.js ADDED
@@ -0,0 +1,78 @@
1
+ /**
2
+ * Base error class for Pinecall SDK
3
+ */
4
+ export class PinecallError extends Error {
5
+ /**
6
+ * @param {string} message - Error message
7
+ * @param {string} [code] - Error code
8
+ * @param {number} [status] - HTTP status code
9
+ * @param {Object} [details] - Additional error details
10
+ */
11
+ constructor(message, code = 'PINECALL_ERROR', status = 500, details = {}) {
12
+ super(message);
13
+ this.name = 'PinecallError';
14
+ this.code = code;
15
+ this.status = status;
16
+ this.details = details;
17
+ }
18
+
19
+ toJSON() {
20
+ return {
21
+ name: this.name,
22
+ message: this.message,
23
+ code: this.code,
24
+ status: this.status,
25
+ details: this.details
26
+ };
27
+ }
28
+ }
29
+
30
+ /**
31
+ * Validation error (400)
32
+ */
33
+ export class ValidationError extends PinecallError {
34
+ constructor(message, details = {}) {
35
+ super(message, 'VALIDATION_ERROR', 400, details);
36
+ this.name = 'ValidationError';
37
+ }
38
+ }
39
+
40
+ /**
41
+ * Not found error (404)
42
+ */
43
+ export class NotFoundError extends PinecallError {
44
+ constructor(message = 'Resource not found', details = {}) {
45
+ super(message, 'NOT_FOUND', 404, details);
46
+ this.name = 'NotFoundError';
47
+ }
48
+ }
49
+
50
+ /**
51
+ * Conflict error (409) - e.g., double booking
52
+ */
53
+ export class ConflictError extends PinecallError {
54
+ constructor(message = 'Resource conflict', details = {}) {
55
+ super(message, 'CONFLICT', 409, details);
56
+ this.name = 'ConflictError';
57
+ }
58
+ }
59
+
60
+ /**
61
+ * Authentication error (401)
62
+ */
63
+ export class AuthenticationError extends PinecallError {
64
+ constructor(message = 'Invalid API key') {
65
+ super(message, 'UNAUTHORIZED', 401);
66
+ this.name = 'AuthenticationError';
67
+ }
68
+ }
69
+
70
+ /**
71
+ * Rate limit error (429)
72
+ */
73
+ export class RateLimitError extends PinecallError {
74
+ constructor(message = 'Rate limit exceeded', retryAfter = 60) {
75
+ super(message, 'RATE_LIMIT', 429, { retryAfter });
76
+ this.name = 'RateLimitError';
77
+ }
78
+ }
package/src/http.js ADDED
@@ -0,0 +1,156 @@
1
+ import { PinecallError, AuthenticationError, NotFoundError, ValidationError, RateLimitError } from './errors.js';
2
+
3
+ /**
4
+ * HTTP client with retry logic and error handling
5
+ */
6
+ export class HttpClient {
7
+ #apiKey;
8
+ #baseUrl;
9
+ #timeout;
10
+ #maxRetries;
11
+
12
+ /**
13
+ * @param {Object} config
14
+ * @param {string} config.apiKey - API key
15
+ * @param {string} [config.baseUrl] - Base URL
16
+ * @param {number} [config.timeout] - Request timeout in ms
17
+ * @param {number} [config.maxRetries] - Max retry attempts
18
+ */
19
+ constructor({ apiKey, baseUrl = 'https://api.pinecall.com', timeout = 30000, maxRetries = 3 }) {
20
+ this.#apiKey = apiKey;
21
+ this.#baseUrl = baseUrl.replace(/\/$/, '');
22
+ this.#timeout = timeout;
23
+ this.#maxRetries = maxRetries;
24
+ }
25
+
26
+ /**
27
+ * Make HTTP request with automatic retry
28
+ * @param {string} method - HTTP method
29
+ * @param {string} path - API path
30
+ * @param {Object} [options] - Request options
31
+ * @returns {Promise<any>}
32
+ */
33
+ async request(method, path, options = {}) {
34
+ const url = `${this.#baseUrl}${path}`;
35
+ const { body, query, headers: customHeaders = {} } = options;
36
+
37
+ // Build URL with query params
38
+ const urlWithQuery = query
39
+ ? `${url}?${new URLSearchParams(this.#flattenQuery(query))}`
40
+ : url;
41
+
42
+ const headers = {
43
+ 'Content-Type': 'application/json',
44
+ 'X-API-Key': this.#apiKey,
45
+ 'User-Agent': '@pinecall/booking-sdk/1.0.0',
46
+ ...customHeaders
47
+ };
48
+
49
+ const fetchOptions = {
50
+ method,
51
+ headers,
52
+ ...(body && { body: JSON.stringify(body) })
53
+ };
54
+
55
+ return this.#executeWithRetry(urlWithQuery, fetchOptions);
56
+ }
57
+
58
+ /**
59
+ * Execute request with retry logic
60
+ */
61
+ async #executeWithRetry(url, options, attempt = 1) {
62
+ const controller = new AbortController();
63
+ const timeoutId = setTimeout(() => controller.abort(), this.#timeout);
64
+
65
+ try {
66
+ const response = await fetch(url, { ...options, signal: controller.signal });
67
+ clearTimeout(timeoutId);
68
+
69
+ const data = await response.json().catch(() => ({}));
70
+
71
+ if (!response.ok) {
72
+ throw this.#createError(response.status, data);
73
+ }
74
+
75
+ return data;
76
+ } catch (error) {
77
+ clearTimeout(timeoutId);
78
+
79
+ // Retry on network errors or 5xx
80
+ if (this.#shouldRetry(error, attempt)) {
81
+ const delay = this.#getRetryDelay(attempt);
82
+ await this.#sleep(delay);
83
+ return this.#executeWithRetry(url, options, attempt + 1);
84
+ }
85
+
86
+ if (error.name === 'AbortError') {
87
+ throw new PinecallError(`Request timeout after ${this.#timeout}ms`, 'TIMEOUT', 408);
88
+ }
89
+
90
+ throw error instanceof PinecallError ? error : new PinecallError(error.message);
91
+ }
92
+ }
93
+
94
+ /**
95
+ * Create appropriate error from response
96
+ */
97
+ #createError(status, data) {
98
+ const message = data.error || data.message || 'Unknown error';
99
+
100
+ switch (status) {
101
+ case 400: return new ValidationError(message, data.details);
102
+ case 401: return new AuthenticationError(message);
103
+ case 404: return new NotFoundError(message);
104
+ case 429: return new RateLimitError(message, data.retryAfter);
105
+ default: return new PinecallError(message, data.code || 'API_ERROR', status, data);
106
+ }
107
+ }
108
+
109
+ #shouldRetry(error, attempt) {
110
+ if (attempt >= this.#maxRetries) return false;
111
+ if (error instanceof RateLimitError) return true;
112
+ if (error.status >= 500) return true;
113
+ if (error.name === 'AbortError') return true;
114
+ return false;
115
+ }
116
+
117
+ #getRetryDelay(attempt) {
118
+ // Exponential backoff: 1s, 2s, 4s...
119
+ return Math.min(1000 * Math.pow(2, attempt - 1), 10000);
120
+ }
121
+
122
+ #sleep(ms) {
123
+ return new Promise(resolve => setTimeout(resolve, ms));
124
+ }
125
+
126
+ #flattenQuery(query, prefix = '') {
127
+ const params = {};
128
+
129
+ for (const [key, value] of Object.entries(query)) {
130
+ if (value === undefined || value === null) continue;
131
+
132
+ const paramKey = prefix ? `${prefix}_${key}` : key;
133
+
134
+ if (typeof value === 'object' && !Array.isArray(value)) {
135
+ // For attribute filters, stringify the object
136
+ if (key.startsWith('attr') || prefix === 'attr') {
137
+ params[paramKey] = JSON.stringify(value);
138
+ } else {
139
+ Object.assign(params, this.#flattenQuery(value, paramKey));
140
+ }
141
+ } else if (Array.isArray(value)) {
142
+ params[paramKey] = value.join(',');
143
+ } else {
144
+ params[paramKey] = String(value);
145
+ }
146
+ }
147
+
148
+ return params;
149
+ }
150
+
151
+ // Convenience methods
152
+ get(path, query) { return this.request('GET', path, { query }); }
153
+ post(path, body) { return this.request('POST', path, { body }); }
154
+ put(path, body) { return this.request('PUT', path, { body }); }
155
+ delete(path, body) { return this.request('DELETE', path, { body }); }
156
+ }
package/src/index.js ADDED
@@ -0,0 +1,20 @@
1
+ /**
2
+ * @pinecall/booking-sdk
3
+ *
4
+ * Simple, elegant SDK for Pinecall Booking API.
5
+ * Zero dependencies. Full JSDoc support.
6
+ *
7
+ * @example
8
+ * ```js
9
+ * import { Pinecall } from '@pinecall/booking-sdk';
10
+ *
11
+ * const client = new Pinecall('pk_your_api_key');
12
+ * const config = await client.configurations.create({ name: 'My Restaurant' });
13
+ * ```
14
+ *
15
+ * @module @pinecall/booking-sdk
16
+ */
17
+
18
+ export { Pinecall } from './client.js';
19
+ export { PinecallError, ValidationError, NotFoundError, ConflictError } from './errors.js';
20
+ export { BOOKING_STATUS, BUSINESS_TYPES } from './constants.js';
@@ -0,0 +1,44 @@
1
+ /**
2
+ * Availability - Query available time slots
3
+ */
4
+ export class Availability {
5
+ #http;
6
+
7
+ constructor(http) {
8
+ this.#http = http;
9
+ }
10
+
11
+ /**
12
+ * Query available slots
13
+ * @param {Object} params
14
+ * @returns {Promise<{summary: Object, resources: Array, availableSlots: Array}>}
15
+ */
16
+ query(params) {
17
+ const query = {
18
+ configId: params.configurationId,
19
+ startDate: params.startDate,
20
+ endDate: params.endDate
21
+ };
22
+
23
+ if (params.duration) {
24
+ query.duration = params.duration;
25
+ }
26
+
27
+ if (params.resourceFilters) {
28
+ for (const [key, value] of Object.entries(params.resourceFilters)) {
29
+ query[`attr_${key}`] = typeof value === 'object' ? JSON.stringify(value) : value;
30
+ }
31
+ }
32
+
33
+ return this.#http.get('/api/bookings/availability', query);
34
+ }
35
+
36
+ /**
37
+ * Check if a specific slot is available
38
+ * @param {Object} params
39
+ * @returns {Promise<{available: boolean, reason?: string}>}
40
+ */
41
+ check(params) {
42
+ return this.#http.post('/api/bookings/availability', params);
43
+ }
44
+ }
@@ -0,0 +1,126 @@
1
+ import { BOOKING_ACTIONS } from '../constants.js';
2
+
3
+ /**
4
+ * Bookings - Create and manage reservations
5
+ */
6
+ export class Bookings {
7
+ #http;
8
+
9
+ constructor(http) {
10
+ this.#http = http;
11
+ }
12
+
13
+ /**
14
+ * List bookings
15
+ * @param {Object} params
16
+ * @returns {Promise<{bookings: Array}>}
17
+ */
18
+ list(params) {
19
+ const query = {
20
+ configId: params.configurationId,
21
+ startDate: params.startDate,
22
+ endDate: params.endDate
23
+ };
24
+
25
+ if (params.status) {
26
+ query.status = Array.isArray(params.status) ? params.status.join(',') : params.status;
27
+ }
28
+ if (params.resourceId) query.resourceId = params.resourceId;
29
+ if (params.search) query.search = params.search;
30
+
31
+ return this.#http.get('/api/bookings/manage', query);
32
+ }
33
+
34
+ /**
35
+ * Get a booking by ID
36
+ * @param {string} id - Booking ID
37
+ * @returns {Promise<{booking: Object}>}
38
+ */
39
+ get(id) {
40
+ return this.#http.get('/api/bookings/manage', { id });
41
+ }
42
+
43
+ /**
44
+ * Create a new booking
45
+ * @param {Object} params
46
+ * @returns {Promise<{booking: Object}>}
47
+ */
48
+ create(params) {
49
+ return this.#http.post('/api/bookings/create', params);
50
+ }
51
+
52
+ /**
53
+ * Confirm a booking
54
+ * @param {string} id - Booking ID
55
+ * @param {string} [reason] - Reason
56
+ * @returns {Promise<{booking: Object}>}
57
+ */
58
+ confirm(id, reason) {
59
+ return this.#action(id, BOOKING_ACTIONS.CONFIRM, { reason });
60
+ }
61
+
62
+ /**
63
+ * Cancel a booking
64
+ * @param {string} id - Booking ID
65
+ * @param {string} [reason] - Reason
66
+ * @returns {Promise<{booking: Object}>}
67
+ */
68
+ cancel(id, reason) {
69
+ return this.#action(id, BOOKING_ACTIONS.CANCEL, { reason });
70
+ }
71
+
72
+ /**
73
+ * Mark booking as completed
74
+ * @param {string} id - Booking ID
75
+ * @param {string} [reason] - Reason
76
+ * @returns {Promise<{booking: Object}>}
77
+ */
78
+ complete(id, reason) {
79
+ return this.#action(id, BOOKING_ACTIONS.COMPLETE, { reason });
80
+ }
81
+
82
+ /**
83
+ * Mark booking as no-show
84
+ * @param {string} id - Booking ID
85
+ * @param {string} [reason] - Reason
86
+ * @returns {Promise<{booking: Object}>}
87
+ */
88
+ noShow(id, reason) {
89
+ return this.#action(id, BOOKING_ACTIONS.NO_SHOW, { reason });
90
+ }
91
+
92
+ /**
93
+ * Reschedule a booking
94
+ * @param {string} id - Booking ID
95
+ * @param {Object} params
96
+ * @returns {Promise<{booking: Object}>}
97
+ */
98
+ reschedule(id, params) {
99
+ return this.#action(id, BOOKING_ACTIONS.RESCHEDULE, {
100
+ newStartTime: params.newStartTime,
101
+ newEndTime: params.newEndTime
102
+ });
103
+ }
104
+
105
+ /**
106
+ * Get booking statistics
107
+ * @param {string} configId - Configuration ID
108
+ * @param {Object} [params]
109
+ * @returns {Promise<{stats: Object}>}
110
+ */
111
+ stats(configId, params = {}) {
112
+ return this.#http.get('/api/bookings/manage', {
113
+ configId,
114
+ stats: 'true',
115
+ ...params
116
+ });
117
+ }
118
+
119
+ #action(bookingId, action, params = {}) {
120
+ return this.#http.post('/api/bookings/manage', {
121
+ bookingId,
122
+ action,
123
+ ...params
124
+ });
125
+ }
126
+ }
@@ -0,0 +1,46 @@
1
+ /**
2
+ * Configuration resource - Manage booking system configurations
3
+ */
4
+ export class Configurations {
5
+ #http;
6
+
7
+ constructor(http) {
8
+ this.#http = http;
9
+ }
10
+
11
+ /**
12
+ * List all configurations
13
+ * @returns {Promise<{configurations: Array}>}
14
+ */
15
+ list() {
16
+ return this.#http.get('/api/bookings/configure');
17
+ }
18
+
19
+ /**
20
+ * Get a configuration by ID
21
+ * @param {string} id - Configuration ID
22
+ * @returns {Promise<{configuration: Object}>}
23
+ */
24
+ get(id) {
25
+ return this.#http.get('/api/bookings/configure', { id });
26
+ }
27
+
28
+ /**
29
+ * Create a new configuration
30
+ * @param {Object} params
31
+ * @returns {Promise<{configuration: Object}>}
32
+ */
33
+ create(params) {
34
+ return this.#http.post('/api/bookings/configure', params);
35
+ }
36
+
37
+ /**
38
+ * Update a configuration
39
+ * @param {string} id - Configuration ID
40
+ * @param {Object} params - Fields to update
41
+ * @returns {Promise<{configuration: Object}>}
42
+ */
43
+ update(id, params) {
44
+ return this.#http.put('/api/bookings/configure', { configurationId: id, ...params });
45
+ }
46
+ }
@@ -0,0 +1,72 @@
1
+ /**
2
+ * Resources - Manage bookable entities (tables, rooms, vehicles, etc.)
3
+ */
4
+ export class Resources {
5
+ #http;
6
+
7
+ constructor(http) {
8
+ this.#http = http;
9
+ }
10
+
11
+ /**
12
+ * List resources for a configuration
13
+ * @param {string} configId - Configuration ID
14
+ * @param {Object} [filters] - Attribute filters
15
+ * @returns {Promise<{resources: Array, count: number}>}
16
+ */
17
+ list(configId, filters = {}) {
18
+ const query = { configId };
19
+
20
+ for (const [key, value] of Object.entries(filters)) {
21
+ query[`attr_${key}`] = typeof value === 'object' ? JSON.stringify(value) : value;
22
+ }
23
+
24
+ return this.#http.get('/api/bookings/resources', query);
25
+ }
26
+
27
+ /**
28
+ * Get a resource by ID
29
+ * @param {string} id - Resource ID
30
+ * @returns {Promise<{resource: Object}>}
31
+ */
32
+ get(id) {
33
+ return this.#http.get('/api/bookings/resources', { id });
34
+ }
35
+
36
+ /**
37
+ * Create a single resource
38
+ * @param {Object} params
39
+ * @returns {Promise<{resource: Object}>}
40
+ */
41
+ create(params) {
42
+ return this.#http.post('/api/bookings/resources', params);
43
+ }
44
+
45
+ /**
46
+ * Bulk create resources
47
+ * @param {Object} params
48
+ * @returns {Promise<{created: number, resources: Array}>}
49
+ */
50
+ bulkCreate(params) {
51
+ return this.#http.post('/api/bookings/resources', params);
52
+ }
53
+
54
+ /**
55
+ * Update a resource
56
+ * @param {string} id - Resource ID
57
+ * @param {Object} params - Fields to update
58
+ * @returns {Promise<{resource: Object}>}
59
+ */
60
+ update(id, params) {
61
+ return this.#http.put('/api/bookings/resources', { resourceId: id, ...params });
62
+ }
63
+
64
+ /**
65
+ * Delete a resource
66
+ * @param {string} id - Resource ID
67
+ * @returns {Promise<{success: boolean}>}
68
+ */
69
+ delete(id) {
70
+ return this.#http.delete('/api/bookings/resources', { resourceId: id });
71
+ }
72
+ }