@slotchain/sdk 1.1.3 → 1.1.4
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/dist/index.cjs.js +75 -2
- package/dist/index.cjs.js.map +1 -1
- package/dist/index.d.mts +64 -8
- package/dist/index.d.ts +64 -8
- package/dist/index.esm.js +75 -2
- package/dist/index.esm.js.map +1 -1
- package/package.json +1 -1
- package/src/clients/__tests__/service-client.spec.ts +450 -0
- package/src/clients/service-client.ts +87 -1
- package/src/clients/slot-client.ts +8 -5
- package/src/index.ts +1 -0
- package/src/types/api.ts +22 -6
|
@@ -0,0 +1,450 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Unit tests for ServiceClient.getServicesByTenant() and getServicesBySlot()
|
|
3
|
+
*
|
|
4
|
+
* Tests cover:
|
|
5
|
+
* - getServicesByTenant() with various parameter combinations
|
|
6
|
+
* - getServicesBySlot() with valid/invalid slot IDs
|
|
7
|
+
* - Type safety for ServiceWithItems vs SlotServiceWithItems
|
|
8
|
+
* - Error handling for all error codes
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import axios from 'axios';
|
|
12
|
+
import axiosMockAdapter from 'axios-mock-adapter';
|
|
13
|
+
import { ServiceClient } from '../service-client';
|
|
14
|
+
import type { Service, ServiceWithItems, SlotServiceWithItems } from '../../types/api';
|
|
15
|
+
|
|
16
|
+
describe('ServiceClient', () => {
|
|
17
|
+
let mockAdapter: axiosMockAdapter;
|
|
18
|
+
let client: axios.AxiosInstance;
|
|
19
|
+
let serviceClient: ServiceClient;
|
|
20
|
+
|
|
21
|
+
beforeEach(() => {
|
|
22
|
+
// Create a new axios instance for each test
|
|
23
|
+
client = axios.create({
|
|
24
|
+
baseURL: 'https://api.slotly.dev',
|
|
25
|
+
});
|
|
26
|
+
mockAdapter = new axiosMockAdapter(client, { delayResponse: 0 });
|
|
27
|
+
serviceClient = new ServiceClient(client);
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
afterEach(() => {
|
|
31
|
+
mockAdapter.restore();
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
describe('getServicesByTenant()', () => {
|
|
35
|
+
describe('With tenantId', () => {
|
|
36
|
+
it('should return services without items when includeItems is false', async () => {
|
|
37
|
+
const mockServices: Service[] = [
|
|
38
|
+
{
|
|
39
|
+
id: 'service-1',
|
|
40
|
+
tenant_id: 'tenant-123',
|
|
41
|
+
name: 'Consultation',
|
|
42
|
+
description: 'Initial consultation',
|
|
43
|
+
is_active: true,
|
|
44
|
+
created_at: '2024-01-01T00:00:00Z',
|
|
45
|
+
updated_at: '2024-01-01T00:00:00Z',
|
|
46
|
+
},
|
|
47
|
+
];
|
|
48
|
+
|
|
49
|
+
mockAdapter
|
|
50
|
+
.onGet('/api/v1/services', { params: { tenant_id: 'tenant-123' } })
|
|
51
|
+
.reply(200, {
|
|
52
|
+
success: true,
|
|
53
|
+
data: mockServices,
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
const result = await serviceClient.getServicesByTenant({
|
|
57
|
+
tenantId: 'tenant-123',
|
|
58
|
+
includeItems: false,
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
expect(result.success).toBe(true);
|
|
62
|
+
expect(result.data).toBeDefined();
|
|
63
|
+
expect(Array.isArray(result.data)).toBe(true);
|
|
64
|
+
expect(result.data?.length).toBe(1);
|
|
65
|
+
expect(result.data?.[0].id).toBe('service-1');
|
|
66
|
+
expect('serviceItems' in (result.data?.[0] || {})).toBe(false);
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it('should return services with items when includeItems is true', async () => {
|
|
70
|
+
const mockServices: ServiceWithItems[] = [
|
|
71
|
+
{
|
|
72
|
+
id: 'service-1',
|
|
73
|
+
tenant_id: 'tenant-123',
|
|
74
|
+
name: 'Consultation',
|
|
75
|
+
description: 'Initial consultation',
|
|
76
|
+
is_active: true,
|
|
77
|
+
created_at: '2024-01-01T00:00:00Z',
|
|
78
|
+
updated_at: '2024-01-01T00:00:00Z',
|
|
79
|
+
serviceItems: [
|
|
80
|
+
{
|
|
81
|
+
id: 'item-1',
|
|
82
|
+
service_id: 'service-1',
|
|
83
|
+
name: 'Standard Consultation',
|
|
84
|
+
price: 100,
|
|
85
|
+
is_available: true,
|
|
86
|
+
created_at: '2024-01-01T00:00:00Z',
|
|
87
|
+
updated_at: '2024-01-01T00:00:00Z',
|
|
88
|
+
},
|
|
89
|
+
],
|
|
90
|
+
},
|
|
91
|
+
];
|
|
92
|
+
|
|
93
|
+
mockAdapter
|
|
94
|
+
.onGet('/api/v1/services', { params: { tenant_id: 'tenant-123', includeItems: true } })
|
|
95
|
+
.reply(200, {
|
|
96
|
+
success: true,
|
|
97
|
+
data: mockServices,
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
const result = await serviceClient.getServicesByTenant({
|
|
101
|
+
tenantId: 'tenant-123',
|
|
102
|
+
includeItems: true,
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
expect(result.success).toBe(true);
|
|
106
|
+
expect(result.data).toBeDefined();
|
|
107
|
+
expect(Array.isArray(result.data)).toBe(true);
|
|
108
|
+
const firstService = result.data?.[0] as ServiceWithItems;
|
|
109
|
+
expect(firstService.serviceItems).toBeDefined();
|
|
110
|
+
expect(firstService.serviceItems?.length).toBe(1);
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
it('should include pagination metadata when provided', async () => {
|
|
114
|
+
const mockServices: Service[] = [
|
|
115
|
+
{
|
|
116
|
+
id: 'service-1',
|
|
117
|
+
tenant_id: 'tenant-123',
|
|
118
|
+
name: 'Consultation',
|
|
119
|
+
is_active: true,
|
|
120
|
+
created_at: '2024-01-01T00:00:00Z',
|
|
121
|
+
updated_at: '2024-01-01T00:00:00Z',
|
|
122
|
+
},
|
|
123
|
+
];
|
|
124
|
+
|
|
125
|
+
mockAdapter
|
|
126
|
+
.onGet('/api/v1/services', { params: { tenant_id: 'tenant-123', page: 1, limit: 20 } })
|
|
127
|
+
.reply(200, {
|
|
128
|
+
success: true,
|
|
129
|
+
data: mockServices,
|
|
130
|
+
pagination: {
|
|
131
|
+
page: 1,
|
|
132
|
+
limit: 20,
|
|
133
|
+
total: 50,
|
|
134
|
+
totalPages: 3,
|
|
135
|
+
},
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
const result = await serviceClient.getServicesByTenant({
|
|
139
|
+
tenantId: 'tenant-123',
|
|
140
|
+
page: 1,
|
|
141
|
+
limit: 20,
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
expect(result.success).toBe(true);
|
|
145
|
+
expect(result.pagination).toBeDefined();
|
|
146
|
+
expect(result.pagination?.page).toBe(1);
|
|
147
|
+
expect(result.pagination?.total).toBe(50);
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
it('should filter by isActive when provided', async () => {
|
|
151
|
+
const mockServices: Service[] = [
|
|
152
|
+
{
|
|
153
|
+
id: 'service-1',
|
|
154
|
+
tenant_id: 'tenant-123',
|
|
155
|
+
name: 'Active Service',
|
|
156
|
+
is_active: true,
|
|
157
|
+
created_at: '2024-01-01T00:00:00Z',
|
|
158
|
+
updated_at: '2024-01-01T00:00:00Z',
|
|
159
|
+
},
|
|
160
|
+
];
|
|
161
|
+
|
|
162
|
+
mockAdapter
|
|
163
|
+
.onGet('/api/v1/services', { params: { tenant_id: 'tenant-123', is_active: true } })
|
|
164
|
+
.reply(200, {
|
|
165
|
+
success: true,
|
|
166
|
+
data: mockServices,
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
const result = await serviceClient.getServicesByTenant({
|
|
170
|
+
tenantId: 'tenant-123',
|
|
171
|
+
isActive: true,
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
expect(result.success).toBe(true);
|
|
175
|
+
expect(result.data?.length).toBe(1);
|
|
176
|
+
expect(result.data?.[0].is_active).toBe(true);
|
|
177
|
+
});
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
describe('With tenantSlug', () => {
|
|
181
|
+
it('should return services when using tenantSlug', async () => {
|
|
182
|
+
const mockServices: Service[] = [
|
|
183
|
+
{
|
|
184
|
+
id: 'service-1',
|
|
185
|
+
tenant_id: 'tenant-123',
|
|
186
|
+
name: 'Consultation',
|
|
187
|
+
is_active: true,
|
|
188
|
+
created_at: '2024-01-01T00:00:00Z',
|
|
189
|
+
updated_at: '2024-01-01T00:00:00Z',
|
|
190
|
+
},
|
|
191
|
+
];
|
|
192
|
+
|
|
193
|
+
mockAdapter
|
|
194
|
+
.onGet('/api/v1/services', { params: { tenantSlug: 'sn-cleaning-co' } })
|
|
195
|
+
.reply(200, {
|
|
196
|
+
success: true,
|
|
197
|
+
data: mockServices,
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
const result = await serviceClient.getServicesByTenant({
|
|
201
|
+
tenantSlug: 'sn-cleaning-co',
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
expect(result.success).toBe(true);
|
|
205
|
+
expect(result.data).toBeDefined();
|
|
206
|
+
expect(result.data?.length).toBe(1);
|
|
207
|
+
});
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
describe('Error handling', () => {
|
|
211
|
+
it('should return MISSING_TENANT_ID error when neither tenantId nor tenantSlug provided', async () => {
|
|
212
|
+
const result = await serviceClient.getServicesByTenant({});
|
|
213
|
+
|
|
214
|
+
expect(result.success).toBe(false);
|
|
215
|
+
expect(result.error).toBeDefined();
|
|
216
|
+
expect(result.error?.code).toBe('MISSING_TENANT_ID');
|
|
217
|
+
expect(result.error?.message).toContain('tenantId or tenantSlug is required');
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
it('should handle TENANT_NOT_FOUND error', async () => {
|
|
221
|
+
mockAdapter
|
|
222
|
+
.onGet('/api/v1/services', { params: { tenantSlug: 'invalid-tenant' } })
|
|
223
|
+
.reply(404, {
|
|
224
|
+
success: false,
|
|
225
|
+
error: {
|
|
226
|
+
code: 'TENANT_NOT_FOUND',
|
|
227
|
+
message: 'Tenant with slug "invalid-tenant" not found',
|
|
228
|
+
},
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
const result = await serviceClient.getServicesByTenant({
|
|
232
|
+
tenantSlug: 'invalid-tenant',
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
expect(result.success).toBe(false);
|
|
236
|
+
expect(result.error?.code).toBe('TENANT_NOT_FOUND');
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
it('should handle INTERNAL_ERROR', async () => {
|
|
240
|
+
mockAdapter
|
|
241
|
+
.onGet('/api/v1/services', { params: { tenant_id: 'tenant-123' } })
|
|
242
|
+
.reply(500, {
|
|
243
|
+
success: false,
|
|
244
|
+
error: {
|
|
245
|
+
code: 'INTERNAL_ERROR',
|
|
246
|
+
message: 'Internal server error',
|
|
247
|
+
},
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
const result = await serviceClient.getServicesByTenant({
|
|
251
|
+
tenantId: 'tenant-123',
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
expect(result.success).toBe(false);
|
|
255
|
+
expect(result.error?.code).toBe('INTERNAL_ERROR');
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
it('should enforce max limit of 100', async () => {
|
|
259
|
+
const mockServices: Service[] = [];
|
|
260
|
+
|
|
261
|
+
mockAdapter
|
|
262
|
+
.onGet('/api/v1/services', { params: { tenant_id: 'tenant-123', limit: 100 } })
|
|
263
|
+
.reply(200, {
|
|
264
|
+
success: true,
|
|
265
|
+
data: mockServices,
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
// Request with limit > 100 should be capped at 100
|
|
269
|
+
await serviceClient.getServicesByTenant({
|
|
270
|
+
tenantId: 'tenant-123',
|
|
271
|
+
limit: 150,
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
// Verify the request was made with limit: 100
|
|
275
|
+
expect(mockAdapter.history.get.length).toBe(1);
|
|
276
|
+
const request = mockAdapter.history.get[0];
|
|
277
|
+
expect(request.params?.limit).toBe(100);
|
|
278
|
+
});
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
describe('Deprecated slotId parameter', () => {
|
|
282
|
+
it('should accept slotId parameter for compatibility', async () => {
|
|
283
|
+
const mockServices: Service[] = [
|
|
284
|
+
{
|
|
285
|
+
id: 'service-1',
|
|
286
|
+
tenant_id: 'tenant-123',
|
|
287
|
+
name: 'Consultation',
|
|
288
|
+
is_active: true,
|
|
289
|
+
created_at: '2024-01-01T00:00:00Z',
|
|
290
|
+
updated_at: '2024-01-01T00:00:00Z',
|
|
291
|
+
},
|
|
292
|
+
];
|
|
293
|
+
|
|
294
|
+
mockAdapter
|
|
295
|
+
.onGet('/api/v1/services', { params: { tenant_id: 'tenant-123', slot_id: 'slot-123' } })
|
|
296
|
+
.reply(200, {
|
|
297
|
+
success: true,
|
|
298
|
+
data: mockServices,
|
|
299
|
+
});
|
|
300
|
+
|
|
301
|
+
const result = await serviceClient.getServicesByTenant({
|
|
302
|
+
tenantId: 'tenant-123',
|
|
303
|
+
slotId: 'slot-123', // Deprecated but accepted
|
|
304
|
+
});
|
|
305
|
+
|
|
306
|
+
expect(result.success).toBe(true);
|
|
307
|
+
// Verify slot_id was included in request (even though API ignores it)
|
|
308
|
+
expect(mockAdapter.history.get[0].params?.slot_id).toBe('slot-123');
|
|
309
|
+
});
|
|
310
|
+
});
|
|
311
|
+
});
|
|
312
|
+
|
|
313
|
+
describe('getServicesBySlot()', () => {
|
|
314
|
+
describe('Valid slot ID', () => {
|
|
315
|
+
it('should return services with items property (not serviceItems)', async () => {
|
|
316
|
+
const mockServices: SlotServiceWithItems[] = [
|
|
317
|
+
{
|
|
318
|
+
id: 'service-1',
|
|
319
|
+
tenant_id: 'tenant-123',
|
|
320
|
+
name: 'Consultation',
|
|
321
|
+
is_active: true,
|
|
322
|
+
created_at: '2024-01-01T00:00:00Z',
|
|
323
|
+
updated_at: '2024-01-01T00:00:00Z',
|
|
324
|
+
items: [
|
|
325
|
+
{
|
|
326
|
+
id: 'item-1',
|
|
327
|
+
service_id: 'service-1',
|
|
328
|
+
name: 'Standard Consultation',
|
|
329
|
+
price: 100,
|
|
330
|
+
is_available: true,
|
|
331
|
+
created_at: '2024-01-01T00:00:00Z',
|
|
332
|
+
updated_at: '2024-01-01T00:00:00Z',
|
|
333
|
+
},
|
|
334
|
+
],
|
|
335
|
+
},
|
|
336
|
+
];
|
|
337
|
+
|
|
338
|
+
mockAdapter
|
|
339
|
+
.onGet('/api/v1/slots/slot-123/services')
|
|
340
|
+
.reply(200, {
|
|
341
|
+
success: true,
|
|
342
|
+
data: mockServices,
|
|
343
|
+
});
|
|
344
|
+
|
|
345
|
+
const result = await serviceClient.getServicesBySlot('slot-123');
|
|
346
|
+
|
|
347
|
+
expect(result.success).toBe(true);
|
|
348
|
+
expect(result.data).toBeDefined();
|
|
349
|
+
expect(Array.isArray(result.data)).toBe(true);
|
|
350
|
+
expect(result.data?.length).toBe(1);
|
|
351
|
+
|
|
352
|
+
const firstService = result.data?.[0];
|
|
353
|
+
expect(firstService?.items).toBeDefined();
|
|
354
|
+
expect(firstService?.items.length).toBe(1);
|
|
355
|
+
// Verify it uses 'items' not 'serviceItems'
|
|
356
|
+
expect('serviceItems' in (firstService || {})).toBe(false);
|
|
357
|
+
});
|
|
358
|
+
|
|
359
|
+
it('should only return active services', async () => {
|
|
360
|
+
const mockServices: SlotServiceWithItems[] = [
|
|
361
|
+
{
|
|
362
|
+
id: 'service-1',
|
|
363
|
+
tenant_id: 'tenant-123',
|
|
364
|
+
name: 'Active Service',
|
|
365
|
+
is_active: true,
|
|
366
|
+
created_at: '2024-01-01T00:00:00Z',
|
|
367
|
+
updated_at: '2024-01-01T00:00:00Z',
|
|
368
|
+
items: [],
|
|
369
|
+
},
|
|
370
|
+
];
|
|
371
|
+
|
|
372
|
+
mockAdapter
|
|
373
|
+
.onGet('/api/v1/slots/slot-123/services')
|
|
374
|
+
.reply(200, {
|
|
375
|
+
success: true,
|
|
376
|
+
data: mockServices,
|
|
377
|
+
});
|
|
378
|
+
|
|
379
|
+
const result = await serviceClient.getServicesBySlot('slot-123');
|
|
380
|
+
|
|
381
|
+
expect(result.success).toBe(true);
|
|
382
|
+
expect(result.data?.every(s => s.is_active === true)).toBe(true);
|
|
383
|
+
});
|
|
384
|
+
|
|
385
|
+
it('should always include items (even if empty array)', async () => {
|
|
386
|
+
const mockServices: SlotServiceWithItems[] = [
|
|
387
|
+
{
|
|
388
|
+
id: 'service-1',
|
|
389
|
+
tenant_id: 'tenant-123',
|
|
390
|
+
name: 'Service without items',
|
|
391
|
+
is_active: true,
|
|
392
|
+
created_at: '2024-01-01T00:00:00Z',
|
|
393
|
+
updated_at: '2024-01-01T00:00:00Z',
|
|
394
|
+
items: [],
|
|
395
|
+
},
|
|
396
|
+
];
|
|
397
|
+
|
|
398
|
+
mockAdapter
|
|
399
|
+
.onGet('/api/v1/slots/slot-123/services')
|
|
400
|
+
.reply(200, {
|
|
401
|
+
success: true,
|
|
402
|
+
data: mockServices,
|
|
403
|
+
});
|
|
404
|
+
|
|
405
|
+
const result = await serviceClient.getServicesBySlot('slot-123');
|
|
406
|
+
|
|
407
|
+
expect(result.success).toBe(true);
|
|
408
|
+
expect(result.data?.[0].items).toBeDefined();
|
|
409
|
+
expect(Array.isArray(result.data?.[0].items)).toBe(true);
|
|
410
|
+
});
|
|
411
|
+
});
|
|
412
|
+
|
|
413
|
+
describe('Error handling', () => {
|
|
414
|
+
it('should handle SLOT_NOT_FOUND error', async () => {
|
|
415
|
+
mockAdapter
|
|
416
|
+
.onGet('/api/v1/slots/invalid-slot/services')
|
|
417
|
+
.reply(404, {
|
|
418
|
+
success: false,
|
|
419
|
+
error: {
|
|
420
|
+
code: 'SLOT_NOT_FOUND',
|
|
421
|
+
message: 'Slot with id "invalid-slot" not found',
|
|
422
|
+
},
|
|
423
|
+
});
|
|
424
|
+
|
|
425
|
+
const result = await serviceClient.getServicesBySlot('invalid-slot');
|
|
426
|
+
|
|
427
|
+
expect(result.success).toBe(false);
|
|
428
|
+
expect(result.error?.code).toBe('SLOT_NOT_FOUND');
|
|
429
|
+
});
|
|
430
|
+
|
|
431
|
+
it('should handle INTERNAL_ERROR', async () => {
|
|
432
|
+
mockAdapter
|
|
433
|
+
.onGet('/api/v1/slots/slot-123/services')
|
|
434
|
+
.reply(500, {
|
|
435
|
+
success: false,
|
|
436
|
+
error: {
|
|
437
|
+
code: 'INTERNAL_ERROR',
|
|
438
|
+
message: 'Internal server error',
|
|
439
|
+
},
|
|
440
|
+
});
|
|
441
|
+
|
|
442
|
+
const result = await serviceClient.getServicesBySlot('slot-123');
|
|
443
|
+
|
|
444
|
+
expect(result.success).toBe(false);
|
|
445
|
+
expect(result.error?.code).toBe('INTERNAL_ERROR');
|
|
446
|
+
});
|
|
447
|
+
});
|
|
448
|
+
});
|
|
449
|
+
});
|
|
450
|
+
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { AxiosInstance } from 'axios';
|
|
2
|
-
import { ApiResponse, Service, ServiceWithItems, Category } from '../types/api';
|
|
2
|
+
import { ApiResponse, Service, ServiceWithItems, SlotServiceWithItems, Category } from '../types/api';
|
|
3
3
|
|
|
4
4
|
export class ServiceClient {
|
|
5
5
|
constructor(private client: AxiosInstance) {}
|
|
@@ -20,10 +20,95 @@ export class ServiceClient {
|
|
|
20
20
|
return response.data;
|
|
21
21
|
}
|
|
22
22
|
|
|
23
|
+
/**
|
|
24
|
+
* Get services for a tenant (full list with optional filters)
|
|
25
|
+
* GET /api/v1/services
|
|
26
|
+
*
|
|
27
|
+
* @param options - Query options
|
|
28
|
+
* @param options.tenantId - Tenant ID (UUID) - required if tenantSlug not provided
|
|
29
|
+
* @param options.tenantSlug - Tenant slug - required if tenantId not provided
|
|
30
|
+
* @param options.slotId - Slot ID (DEPRECATED - accepted but ignored for API compatibility)
|
|
31
|
+
* @param options.isActive - Filter by active status
|
|
32
|
+
* @param options.includeItems - Include nested service items (default: false)
|
|
33
|
+
* @param options.page - Page number (default: 1)
|
|
34
|
+
* @param options.limit - Items per page (default: 20, max: 100)
|
|
35
|
+
* @returns Services with optional pagination metadata
|
|
36
|
+
*/
|
|
37
|
+
async getServicesByTenant(options: {
|
|
38
|
+
tenantId?: string;
|
|
39
|
+
tenantSlug?: string;
|
|
40
|
+
slotId?: string; // DEPRECATED - accepted but ignored
|
|
41
|
+
isActive?: boolean;
|
|
42
|
+
includeItems?: boolean;
|
|
43
|
+
page?: number;
|
|
44
|
+
limit?: number;
|
|
45
|
+
}): Promise<ApiResponse<Service[] | ServiceWithItems[]>> {
|
|
46
|
+
// Validate that either tenantId or tenantSlug is provided
|
|
47
|
+
if (!options.tenantId && !options.tenantSlug) {
|
|
48
|
+
return {
|
|
49
|
+
success: false,
|
|
50
|
+
error: {
|
|
51
|
+
code: 'MISSING_TENANT_ID',
|
|
52
|
+
message: 'tenantId or tenantSlug is required',
|
|
53
|
+
},
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Build query parameters
|
|
58
|
+
const params: Record<string, any> = {};
|
|
59
|
+
|
|
60
|
+
if (options.tenantId) {
|
|
61
|
+
params.tenant_id = options.tenantId;
|
|
62
|
+
}
|
|
63
|
+
if (options.tenantSlug) {
|
|
64
|
+
params.tenantSlug = options.tenantSlug;
|
|
65
|
+
}
|
|
66
|
+
if (options.slotId) {
|
|
67
|
+
// Include for API compatibility, even though it's ignored
|
|
68
|
+
params.slot_id = options.slotId;
|
|
69
|
+
}
|
|
70
|
+
if (options.isActive !== undefined) {
|
|
71
|
+
params.is_active = options.isActive;
|
|
72
|
+
}
|
|
73
|
+
if (options.includeItems) {
|
|
74
|
+
params.includeItems = true;
|
|
75
|
+
}
|
|
76
|
+
if (options.page !== undefined) {
|
|
77
|
+
params.page = options.page;
|
|
78
|
+
}
|
|
79
|
+
if (options.limit !== undefined) {
|
|
80
|
+
params.limit = Math.min(options.limit, 100); // Enforce max limit
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const response = await this.client.get<ApiResponse<Service[] | ServiceWithItems[]>>(
|
|
84
|
+
'/api/v1/services',
|
|
85
|
+
{ params }
|
|
86
|
+
);
|
|
87
|
+
return response.data;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Get services for a specific slot
|
|
92
|
+
* GET /api/v1/slots/:id/services
|
|
93
|
+
*
|
|
94
|
+
* Note: Services are tenant-level entities. This endpoint returns all active services
|
|
95
|
+
* for the slot's tenant with items included. The response uses 'items' property (not 'serviceItems').
|
|
96
|
+
*
|
|
97
|
+
* @param slotId - Slot ID (UUID)
|
|
98
|
+
* @returns Services with nested items (always includes items, only active services)
|
|
99
|
+
*/
|
|
100
|
+
async getServicesBySlot(slotId: string): Promise<ApiResponse<SlotServiceWithItems[]>> {
|
|
101
|
+
const response = await this.client.get<ApiResponse<SlotServiceWithItems[]>>(
|
|
102
|
+
`/api/v1/slots/${slotId}/services`
|
|
103
|
+
);
|
|
104
|
+
return response.data;
|
|
105
|
+
}
|
|
106
|
+
|
|
23
107
|
/**
|
|
24
108
|
* List services by tenant slug
|
|
25
109
|
* GET /api/v1/services?tenantSlug=:slug
|
|
26
110
|
*
|
|
111
|
+
* @deprecated Consider using getServicesByTenant() for better type safety and validation
|
|
27
112
|
* @param slug - Tenant slug (or use tenant_id in list() method)
|
|
28
113
|
* @param params - Optional parameters (includeItems, page, limit, is_active)
|
|
29
114
|
* @returns List of services for the tenant
|
|
@@ -48,6 +133,7 @@ export class ServiceClient {
|
|
|
48
133
|
* List services with optional filters
|
|
49
134
|
* GET /api/v1/services
|
|
50
135
|
*
|
|
136
|
+
* @deprecated Consider using getServicesByTenant() for better type safety and validation
|
|
51
137
|
* @param params - Query parameters (tenant_id, tenantSlug, slot_id, page, limit, is_active, includeItems)
|
|
52
138
|
* @returns Paginated list of services
|
|
53
139
|
*/
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { AxiosInstance } from 'axios';
|
|
2
|
-
import { ApiResponse, Slot,
|
|
2
|
+
import { ApiResponse, Slot, SlotServiceWithItems, Tenant } from '../types/api';
|
|
3
3
|
|
|
4
4
|
export class SlotClient {
|
|
5
5
|
constructor(private client: AxiosInstance) {}
|
|
@@ -113,11 +113,14 @@ export class SlotClient {
|
|
|
113
113
|
* Get all active services for a slot with their items
|
|
114
114
|
* GET /api/v1/slots/:id/services
|
|
115
115
|
*
|
|
116
|
-
*
|
|
117
|
-
*
|
|
116
|
+
* Note: Services are tenant-level entities. This endpoint returns all active services
|
|
117
|
+
* for the slot's tenant with items included. The response uses 'items' property (not 'serviceItems').
|
|
118
|
+
*
|
|
119
|
+
* @param id - Slot ID (UUID)
|
|
120
|
+
* @returns Services with nested items (always includes items, only active services)
|
|
118
121
|
*/
|
|
119
|
-
async getServices(id: string): Promise<ApiResponse<
|
|
120
|
-
const response = await this.client.get<ApiResponse<
|
|
122
|
+
async getServices(id: string): Promise<ApiResponse<SlotServiceWithItems[]>> {
|
|
123
|
+
const response = await this.client.get<ApiResponse<SlotServiceWithItems[]>>(
|
|
121
124
|
`/api/v1/slots/${id}/services`
|
|
122
125
|
);
|
|
123
126
|
return response.data;
|
package/src/index.ts
CHANGED
package/src/types/api.ts
CHANGED
|
@@ -59,23 +59,35 @@ export interface TenantBranding {
|
|
|
59
59
|
*/
|
|
60
60
|
export interface ServiceItem {
|
|
61
61
|
id: string;
|
|
62
|
-
|
|
62
|
+
service_id: string;
|
|
63
63
|
name: string;
|
|
64
64
|
description?: string;
|
|
65
65
|
price?: number;
|
|
66
66
|
duration?: number;
|
|
67
|
-
|
|
68
|
-
|
|
67
|
+
is_available?: boolean;
|
|
68
|
+
sort_order?: number;
|
|
69
|
+
created_at: string;
|
|
70
|
+
updated_at: string;
|
|
69
71
|
}
|
|
70
72
|
|
|
71
73
|
/**
|
|
72
|
-
* Enhanced Service type with nested service items
|
|
74
|
+
* Enhanced Service type with nested service items (for tenant endpoint)
|
|
75
|
+
* Uses 'serviceItems' property name
|
|
73
76
|
*/
|
|
74
77
|
export interface ServiceWithItems extends Service {
|
|
75
78
|
/** Array of service items nested within this service (optional, may be empty array) */
|
|
76
79
|
serviceItems?: ServiceItem[];
|
|
77
80
|
}
|
|
78
81
|
|
|
82
|
+
/**
|
|
83
|
+
* Service with items for slot endpoint
|
|
84
|
+
* Uses 'items' property name (different from tenant endpoint)
|
|
85
|
+
*/
|
|
86
|
+
export interface SlotServiceWithItems extends Service {
|
|
87
|
+
/** Array of service items nested within this service (always included for slot endpoint) */
|
|
88
|
+
items: ServiceItem[];
|
|
89
|
+
}
|
|
90
|
+
|
|
79
91
|
/**
|
|
80
92
|
* Service item with parent service reference
|
|
81
93
|
*/
|
|
@@ -148,12 +160,16 @@ export interface ListBookingsOptions {
|
|
|
148
160
|
|
|
149
161
|
export interface Service {
|
|
150
162
|
id: string;
|
|
151
|
-
|
|
163
|
+
tenant_id: string;
|
|
164
|
+
slot_id?: string; // Present but not used in queries
|
|
152
165
|
name: string;
|
|
153
166
|
description?: string;
|
|
167
|
+
category?: string;
|
|
168
|
+
is_active?: boolean;
|
|
154
169
|
duration?: number;
|
|
155
170
|
price?: number;
|
|
156
|
-
|
|
171
|
+
created_at: string;
|
|
172
|
+
updated_at: string;
|
|
157
173
|
}
|
|
158
174
|
|
|
159
175
|
export interface Slot {
|