@slotchain/sdk 1.1.3 → 1.2.0

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.
@@ -0,0 +1,165 @@
1
+ import { AxiosInstance } from 'axios';
2
+ import { ApiResponse, Customer, Booking } from '../types/api';
3
+
4
+ export class CustomerClient {
5
+ constructor(private client: AxiosInstance) {}
6
+
7
+ /**
8
+ * Get customer by ID
9
+ * GET /api/v1/customers/:id
10
+ *
11
+ * @param id - Customer ID
12
+ * @returns Customer details
13
+ */
14
+ async getById(id: string): Promise<ApiResponse<Customer>> {
15
+ const response = await this.client.get<ApiResponse<Customer>>(
16
+ `/api/v1/customers/${id}`
17
+ );
18
+ return response.data;
19
+ }
20
+
21
+ /**
22
+ * Get customer by email
23
+ * GET /api/v1/customers?email=:email&tenant_id=:id
24
+ *
25
+ * @param email - Customer email
26
+ * @param tenantId - Required tenant ID for scoping
27
+ * @returns Customer details
28
+ */
29
+ async getByEmail(email: string, tenantId: string): Promise<ApiResponse<Customer>> {
30
+ const response = await this.client.get<ApiResponse<Customer>>(
31
+ '/api/v1/customers',
32
+ { params: { email, tenant_id: tenantId } }
33
+ );
34
+ return response.data;
35
+ }
36
+
37
+ /**
38
+ * List customers with optional filters
39
+ * GET /api/v1/customers
40
+ *
41
+ * @param params - Query parameters (tenant_id required, page, limit, search, is_guest)
42
+ * @returns Paginated list of customers
43
+ */
44
+ async list(params: {
45
+ tenant_id: string;
46
+ page?: number;
47
+ limit?: number;
48
+ search?: string;
49
+ is_guest?: boolean;
50
+ }): Promise<ApiResponse<Customer[]>> {
51
+ const response = await this.client.get<ApiResponse<Customer[]>>(
52
+ '/api/v1/customers',
53
+ { params }
54
+ );
55
+ return response.data;
56
+ }
57
+
58
+ /**
59
+ * Create a new customer
60
+ * POST /api/v1/customers
61
+ *
62
+ * @param data - Customer creation data (tenant_id, name, email required)
63
+ * @returns Created customer
64
+ */
65
+ async create(data: Partial<Customer>): Promise<ApiResponse<Customer>> {
66
+ const response = await this.client.post<ApiResponse<Customer>>(
67
+ '/api/v1/customers',
68
+ data
69
+ );
70
+ return response.data;
71
+ }
72
+
73
+ /**
74
+ * Update customer by ID
75
+ * PUT /api/v1/customers/:id
76
+ *
77
+ * @param id - Customer ID
78
+ * @param data - Customer update data
79
+ * @returns Updated customer
80
+ */
81
+ async update(id: string, data: Partial<Customer>): Promise<ApiResponse<Customer>> {
82
+ const response = await this.client.put<ApiResponse<Customer>>(
83
+ `/api/v1/customers/${id}`,
84
+ data
85
+ );
86
+ return response.data;
87
+ }
88
+
89
+ /**
90
+ * Delete customer by ID
91
+ * DELETE /api/v1/customers/:id
92
+ *
93
+ * @param id - Customer ID
94
+ * @returns Empty response on success
95
+ */
96
+ async delete(id: string): Promise<ApiResponse<void>> {
97
+ const response = await this.client.delete<ApiResponse<void>>(
98
+ `/api/v1/customers/${id}`
99
+ );
100
+ return response.data;
101
+ }
102
+
103
+ /**
104
+ * Lookup customer by userId or tenantId
105
+ * GET /api/v1/customers/lookup
106
+ *
107
+ * @param params - Query parameters (userId optional, tenantId required)
108
+ * @returns Customer or null
109
+ */
110
+ async lookup(params: {
111
+ userId?: string;
112
+ tenantId: string;
113
+ }): Promise<ApiResponse<Customer | null>> {
114
+ const response = await this.client.get<ApiResponse<Customer | null>>(
115
+ '/api/v1/customers/lookup',
116
+ { params: { userId: params.userId, tenantId: params.tenantId } }
117
+ );
118
+ return response.data;
119
+ }
120
+
121
+ /**
122
+ * Get autocomplete data from existing customers and bookings
123
+ * GET /api/v1/customers/autocomplete
124
+ *
125
+ * @param tenantId - Tenant ID (required)
126
+ * @returns Autocomplete data
127
+ */
128
+ async autocomplete(tenantId: string): Promise<ApiResponse<any>> {
129
+ const response = await this.client.get<ApiResponse<any>>(
130
+ '/api/v1/customers/autocomplete',
131
+ { params: { tenantId } }
132
+ );
133
+ return response.data;
134
+ }
135
+
136
+ /**
137
+ * Get all bookings for a customer
138
+ * GET /api/v1/customers/bookings
139
+ *
140
+ * @param params - Query parameters (email optional, userId optional, tenantId required)
141
+ * @returns Customer bookings data
142
+ */
143
+ async getBookings(params: {
144
+ email?: string;
145
+ userId?: string;
146
+ tenantId: string;
147
+ }): Promise<ApiResponse<{
148
+ customer: Customer | null;
149
+ bookings: Booking[];
150
+ groupedBySlot: Record<string, Booking[]>;
151
+ total: number;
152
+ }>> {
153
+ const response = await this.client.get<ApiResponse<{
154
+ customer: Customer | null;
155
+ bookings: Booking[];
156
+ groupedBySlot: Record<string, Booking[]>;
157
+ total: number;
158
+ }>>(
159
+ '/api/v1/customers/bookings',
160
+ { params }
161
+ );
162
+ return response.data;
163
+ }
164
+ }
165
+
@@ -0,0 +1,148 @@
1
+ import type { AxiosInstance } from 'axios';
2
+ import type {
3
+ ApiResponse,
4
+ Artifact,
5
+ ArtifactWithUrl,
6
+ ListArtifactsOptions,
7
+ UploadArtifactOptions,
8
+ ArtifactEntityType,
9
+ } from '../types/api';
10
+
11
+ /**
12
+ * DocumentClient — manage file artifacts attached to Slotly entities.
13
+ *
14
+ * Artifacts are tenant-scoped files (CVs, invoices, images, etc.) linked
15
+ * to a customer, booking, slot, service, or tenant record.
16
+ *
17
+ * @example Upload a CV for a customer
18
+ * ```ts
19
+ * const artifact = await slotly.document.upload({
20
+ * file: buffer,
21
+ * filename: 'cv-2024.pdf',
22
+ * related_entity_type: 'customer',
23
+ * related_entity_id: customerId,
24
+ * label: 'CV',
25
+ * });
26
+ * ```
27
+ *
28
+ * @example List all CVs for a customer
29
+ * ```ts
30
+ * const { data } = await slotly.document.listByEntity('customer', customerId, { signed: true });
31
+ * ```
32
+ */
33
+ export class DocumentClient {
34
+ private readonly client: AxiosInstance;
35
+ private readonly base = '/api/v1/artifacts';
36
+
37
+ constructor(client: AxiosInstance) {
38
+ this.client = client;
39
+ }
40
+
41
+ /**
42
+ * Upload a file artifact.
43
+ * Sends as multipart/form-data.
44
+ */
45
+ async upload(options: UploadArtifactOptions): Promise<Artifact> {
46
+ const form = new FormData();
47
+
48
+ // Normalise file to Blob for FormData compatibility
49
+ let blob: Blob;
50
+ if (options.file instanceof Blob) {
51
+ blob = options.file;
52
+ } else {
53
+ blob = new Blob([options.file], { type: options.mime_type ?? 'application/octet-stream' });
54
+ }
55
+ form.append('file', blob, options.filename);
56
+ form.append('related_entity_type', options.related_entity_type);
57
+ form.append('related_entity_id', options.related_entity_id);
58
+ if (options.label) form.append('label', options.label);
59
+ if (options.mime_type) form.append('mime_type', options.mime_type);
60
+ if (options.metadata) form.append('metadata', JSON.stringify(options.metadata));
61
+
62
+ const response = await this.client.post<ApiResponse<Artifact>>(this.base, form, {
63
+ headers: { 'Content-Type': 'multipart/form-data' },
64
+ });
65
+ const { data } = response.data;
66
+ if (!data) throw new Error('No artifact returned from upload');
67
+ return data;
68
+ }
69
+
70
+ /**
71
+ * List artifacts for a specific entity.
72
+ * Pass `signed: true` to get download URLs in each result.
73
+ */
74
+ async listByEntity(
75
+ entityType: ArtifactEntityType,
76
+ entityId: string,
77
+ options?: Omit<ListArtifactsOptions, 'related_entity_type' | 'related_entity_id'>
78
+ ): Promise<ApiResponse<(Artifact | ArtifactWithUrl)[]>> {
79
+ const response = await this.client.get<ApiResponse<(Artifact | ArtifactWithUrl)[]>>(
80
+ this.base,
81
+ {
82
+ params: {
83
+ related_entity_type: entityType,
84
+ related_entity_id: entityId,
85
+ ...options,
86
+ },
87
+ }
88
+ );
89
+ return response.data;
90
+ }
91
+
92
+ /**
93
+ * List artifacts with full filter options.
94
+ */
95
+ async list(
96
+ options: ListArtifactsOptions
97
+ ): Promise<ApiResponse<(Artifact | ArtifactWithUrl)[]>> {
98
+ const response = await this.client.get<ApiResponse<(Artifact | ArtifactWithUrl)[]>>(
99
+ this.base,
100
+ { params: options }
101
+ );
102
+ return response.data;
103
+ }
104
+
105
+ /**
106
+ * Get a single artifact by ID.
107
+ */
108
+ async get(id: string, signed = false): Promise<Artifact | ArtifactWithUrl> {
109
+ const response = await this.client.get<ApiResponse<Artifact | ArtifactWithUrl>>(
110
+ `${this.base}/${id}`,
111
+ { params: signed ? { signed: true } : undefined }
112
+ );
113
+ const { data } = response.data;
114
+ if (!data) throw new Error(`Artifact ${id} not found`);
115
+ return data;
116
+ }
117
+
118
+ /**
119
+ * Get a signed download URL for an artifact.
120
+ * Returns the full ArtifactWithUrl including `signed_url` and `signed_url_expires_at`.
121
+ */
122
+ async getUrl(id: string): Promise<ArtifactWithUrl> {
123
+ return this.get(id, true) as Promise<ArtifactWithUrl>;
124
+ }
125
+
126
+ /**
127
+ * Delete an artifact and its stored file.
128
+ */
129
+ async delete(id: string): Promise<void> {
130
+ await this.client.delete(`${this.base}/${id}`);
131
+ }
132
+
133
+ /**
134
+ * Update artifact label or metadata.
135
+ */
136
+ async update(
137
+ id: string,
138
+ patch: { label?: string; metadata?: Record<string, unknown> }
139
+ ): Promise<Artifact> {
140
+ const response = await this.client.patch<ApiResponse<Artifact>>(
141
+ `${this.base}/${id}`,
142
+ patch
143
+ );
144
+ const { data } = response.data;
145
+ if (!data) throw new Error(`Artifact ${id} not found`);
146
+ return data;
147
+ }
148
+ }
@@ -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, ServiceWithItems, Tenant } from '../types/api';
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
- * @param id - Slot ID
117
- * @returns Services with nested service items
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<ServiceWithItems[]>> {
120
- const response = await this.client.get<ApiResponse<ServiceWithItems[]>>(
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
@@ -10,6 +10,7 @@ import { StudioClient } from './clients/studio-client';
10
10
  import { NotificationClient } from './clients/notification-client';
11
11
  import { DataQualityClient } from './clients/data-quality-client';
12
12
  import { OrganizationClient } from './clients/organization-client';
13
+ import { DocumentClient } from './clients/document-client';
13
14
  import { setupRequestInterceptor, setupResponseInterceptor, setupErrorInterceptor } from './interceptors';
14
15
  import { setupRetryInterceptor } from './retry';
15
16
  import { SlotlyConfigurationError } from './errors';
@@ -26,10 +27,17 @@ export type {
26
27
  Service,
27
28
  ServiceItem,
28
29
  ServiceWithItems,
30
+ SlotServiceWithItems,
29
31
  ServiceItemWithService,
30
32
  Category,
31
33
  Slot,
32
34
  Customer,
35
+ CustomerIdentity,
36
+ CustomerWithIdentities,
37
+ NotificationPreferences,
38
+ CustomerWithBookings,
39
+ ResolveCustomerOptions,
40
+ FindOrCreateCustomerOptions,
33
41
  Flow,
34
42
  FlowStep,
35
43
  Studio,
@@ -38,6 +46,12 @@ export type {
38
46
  Organization,
39
47
  OrganizationFullConfig,
40
48
  GetOrganizationOptions,
49
+ Artifact,
50
+ ArtifactWithUrl,
51
+ ArtifactEntityType,
52
+ ArtifactMimeCategory,
53
+ ListArtifactsOptions,
54
+ UploadArtifactOptions,
41
55
  } from './types/api';
42
56
 
43
57
  // Re-export errors
@@ -104,6 +118,8 @@ export interface SlotlyApi {
104
118
  notification: NotificationClient;
105
119
  dataQuality: DataQualityClient;
106
120
  organization: OrganizationClient;
121
+ /** Upload and retrieve file artifacts (CVs, invoices, images) linked to any Slotly entity */
122
+ document: DocumentClient;
107
123
  }
108
124
 
109
125
  const defaultBaseURL = typeof process !== 'undefined' && process.env?.SLOTLY_API_URL
@@ -297,6 +313,7 @@ export const useSlotly = (options: SlotlyClientOptions): SlotlyApi => {
297
313
  notification: new NotificationClient(client),
298
314
  dataQuality: new DataQualityClient(client),
299
315
  organization: new OrganizationClient(client),
316
+ document: new DocumentClient(client),
300
317
  };
301
318
  };
302
319