@slotchain/sdk 1.1.4 → 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.
@@ -1,5 +1,14 @@
1
1
  import { AxiosInstance } from 'axios';
2
- import { ApiResponse, Customer, Booking } from '../types/api';
2
+ import {
3
+ ApiResponse,
4
+ Customer,
5
+ CustomerIdentity,
6
+ CustomerWithIdentities,
7
+ CustomerWithBookings,
8
+ NotificationPreferences,
9
+ FindOrCreateCustomerOptions,
10
+ ResolveCustomerOptions,
11
+ } from '../types/api';
3
12
 
4
13
  export class CustomerClient {
5
14
  constructor(private client: AxiosInstance) {}
@@ -58,11 +67,8 @@ export class CustomerClient {
58
67
  /**
59
68
  * Create a new customer
60
69
  * POST /api/v1/customers
61
- *
62
- * @param data - Customer creation data (tenant_id, name, email required)
63
- * @returns Created customer
64
70
  */
65
- async create(data: Partial<Customer>): Promise<ApiResponse<Customer>> {
71
+ async create(data: Partial<Customer> & { tenant_id: string; name: string; email: string }): Promise<ApiResponse<Customer>> {
66
72
  const response = await this.client.post<ApiResponse<Customer>>(
67
73
  '/api/v1/customers',
68
74
  data
@@ -119,14 +125,25 @@ export class CustomerClient {
119
125
  }
120
126
 
121
127
  /**
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
128
+ * Get autocomplete data from existing customers and bookings.
129
+ * GET /api/v1/customers/autocomplete?tenantId=:tenantId
127
130
  */
128
- async autocomplete(tenantId: string): Promise<ApiResponse<any>> {
129
- const response = await this.client.get<ApiResponse<any>>(
131
+ async autocomplete(tenantId: string): Promise<ApiResponse<{
132
+ companies: string[];
133
+ jobTitles: string[];
134
+ experienceLevels: string[];
135
+ cities: string[];
136
+ states: string[];
137
+ zipCodes: string[];
138
+ }>> {
139
+ const response = await this.client.get<ApiResponse<{
140
+ companies: string[];
141
+ jobTitles: string[];
142
+ experienceLevels: string[];
143
+ cities: string[];
144
+ states: string[];
145
+ zipCodes: string[];
146
+ }>>(
130
147
  '/api/v1/customers/autocomplete',
131
148
  { params: { tenantId } }
132
149
  );
@@ -134,32 +151,115 @@ export class CustomerClient {
134
151
  }
135
152
 
136
153
  /**
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
154
+ * Get all bookings for a customer, grouped by slot.
155
+ * GET /api/v1/customers/bookings?tenantId=:id
142
156
  */
143
157
  async getBookings(params: {
144
158
  email?: string;
145
159
  userId?: string;
146
160
  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
- }>>(
161
+ }): Promise<ApiResponse<CustomerWithBookings>> {
162
+ const response = await this.client.get<ApiResponse<CustomerWithBookings>>(
159
163
  '/api/v1/customers/bookings',
160
164
  { params }
161
165
  );
162
166
  return response.data;
163
167
  }
168
+
169
+ // ─── Atomic upsert ────────────────────────────────────────────────────────
170
+
171
+ /**
172
+ * Atomic upsert: find an existing customer by email within a tenant, or create one.
173
+ * POST /api/v1/customers/find-or-create
174
+ */
175
+ async findOrCreate(options: FindOrCreateCustomerOptions): Promise<ApiResponse<Customer & { created: boolean }>> {
176
+ const response = await this.client.post<ApiResponse<Customer & { created: boolean }>>(
177
+ '/api/v1/customers/find-or-create',
178
+ options
179
+ );
180
+ return response.data;
181
+ }
182
+
183
+ /**
184
+ * Resolve a customer from an IDP identity or email.
185
+ * Tries idpId lookup (customer_users) first, then falls back to email.
186
+ * POST /api/v1/customers/resolve
187
+ */
188
+ async resolve(options: ResolveCustomerOptions): Promise<ApiResponse<Customer | null>> {
189
+ const response = await this.client.post<ApiResponse<Customer | null>>(
190
+ '/api/v1/customers/resolve',
191
+ options
192
+ );
193
+ return response.data;
194
+ }
195
+
196
+ // ─── IDP Identity Management ─────────────────────────────────────────────
197
+
198
+ /**
199
+ * Get a customer with their linked IDP identities (customer_users rows).
200
+ * GET /api/v1/customers/:id?include_identities=true
201
+ */
202
+ async getWithIdentities(id: string): Promise<ApiResponse<CustomerWithIdentities>> {
203
+ const response = await this.client.get<ApiResponse<CustomerWithIdentities>>(
204
+ `/api/v1/customers/${id}`,
205
+ { params: { include_identities: true } }
206
+ );
207
+ return response.data;
208
+ }
209
+
210
+ /**
211
+ * Link an IDP user ID to a customer (creates a customer_users row).
212
+ * POST /api/v1/customers/:id/users
213
+ */
214
+ async linkUser(
215
+ customerId: string,
216
+ idpId: string,
217
+ options?: { role?: CustomerIdentity['role']; is_primary?: boolean }
218
+ ): Promise<ApiResponse<CustomerIdentity>> {
219
+ const response = await this.client.post<ApiResponse<CustomerIdentity>>(
220
+ `/api/v1/customers/${customerId}/users`,
221
+ { idp_id: idpId, role: options?.role ?? 'member', is_primary: options?.is_primary ?? false }
222
+ );
223
+ return response.data;
224
+ }
225
+
226
+ /**
227
+ * Unlink an IDP user from a customer (deletes the customer_users row).
228
+ * DELETE /api/v1/customers/:id/users/:idpId
229
+ */
230
+ async unlinkUser(customerId: string, idpId: string): Promise<ApiResponse<void>> {
231
+ const response = await this.client.delete<ApiResponse<void>>(
232
+ `/api/v1/customers/${customerId}/users/${encodeURIComponent(idpId)}`
233
+ );
234
+ return response.data;
235
+ }
236
+
237
+ // ─── Notification Preferences ────────────────────────────────────────────
238
+
239
+ /**
240
+ * Get notification preferences for a customer.
241
+ * GET /api/v1/customers/:id/preferences/notifications
242
+ */
243
+ async getNotificationPreferences(customerId: string): Promise<ApiResponse<NotificationPreferences>> {
244
+ const response = await this.client.get<ApiResponse<NotificationPreferences>>(
245
+ `/api/v1/customers/${customerId}/preferences/notifications`
246
+ );
247
+ return response.data;
248
+ }
249
+
250
+ /**
251
+ * Update notification preferences for a customer (partial merge).
252
+ * PATCH /api/v1/customers/:id/preferences/notifications
253
+ */
254
+ async updateNotificationPreferences(
255
+ customerId: string,
256
+ preferences: Partial<NotificationPreferences>
257
+ ): Promise<ApiResponse<NotificationPreferences>> {
258
+ const response = await this.client.patch<ApiResponse<NotificationPreferences>>(
259
+ `/api/v1/customers/${customerId}/preferences/notifications`,
260
+ preferences
261
+ );
262
+ return response.data;
263
+ }
164
264
  }
165
265
 
@@ -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
+ }
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';
@@ -31,6 +32,12 @@ export type {
31
32
  Category,
32
33
  Slot,
33
34
  Customer,
35
+ CustomerIdentity,
36
+ CustomerWithIdentities,
37
+ NotificationPreferences,
38
+ CustomerWithBookings,
39
+ ResolveCustomerOptions,
40
+ FindOrCreateCustomerOptions,
34
41
  Flow,
35
42
  FlowStep,
36
43
  Studio,
@@ -39,6 +46,12 @@ export type {
39
46
  Organization,
40
47
  OrganizationFullConfig,
41
48
  GetOrganizationOptions,
49
+ Artifact,
50
+ ArtifactWithUrl,
51
+ ArtifactEntityType,
52
+ ArtifactMimeCategory,
53
+ ListArtifactsOptions,
54
+ UploadArtifactOptions,
42
55
  } from './types/api';
43
56
 
44
57
  // Re-export errors
@@ -105,6 +118,8 @@ export interface SlotlyApi {
105
118
  notification: NotificationClient;
106
119
  dataQuality: DataQualityClient;
107
120
  organization: OrganizationClient;
121
+ /** Upload and retrieve file artifacts (CVs, invoices, images) linked to any Slotly entity */
122
+ document: DocumentClient;
108
123
  }
109
124
 
110
125
  const defaultBaseURL = typeof process !== 'undefined' && process.env?.SLOTLY_API_URL
@@ -298,6 +313,7 @@ export const useSlotly = (options: SlotlyClientOptions): SlotlyApi => {
298
313
  notification: new NotificationClient(client),
299
314
  dataQuality: new DataQualityClient(client),
300
315
  organization: new OrganizationClient(client),
316
+ document: new DocumentClient(client),
301
317
  };
302
318
  };
303
319