@slotchain/sdk 1.0.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.
- package/README.md +167 -0
- package/dist/index.cjs.js +1463 -0
- package/dist/index.cjs.js.map +1 -0
- package/dist/index.d.mts +1149 -0
- package/dist/index.d.ts +1149 -0
- package/dist/index.esm.js +1422 -0
- package/dist/index.esm.js.map +1 -0
- package/package.json +52 -0
- package/src/client.ts +161 -0
- package/src/clients/__tests__/tenant-client.spec.ts +236 -0
- package/src/clients/booking-client.ts +98 -0
- package/src/clients/customer-client.ts +165 -0
- package/src/clients/data-quality-client.ts +93 -0
- package/src/clients/flow-client.ts +107 -0
- package/src/clients/notification-client.ts +108 -0
- package/src/clients/register-client.ts +19 -0
- package/src/clients/service-client.ts +215 -0
- package/src/clients/service-item-client.ts +103 -0
- package/src/clients/slot-client.ts +140 -0
- package/src/clients/studio-client.ts +128 -0
- package/src/clients/tenant-client.ts +148 -0
- package/src/errors.ts +41 -0
- package/src/index.ts +296 -0
- package/src/interceptors.ts +81 -0
- package/src/middleware/validateSlotlyRequest.ts +337 -0
- package/src/mocks/mockAdapter.ts +32 -0
- package/src/retry.ts +82 -0
- package/src/types/api.ts +192 -0
|
@@ -0,0 +1,337 @@
|
|
|
1
|
+
import type { NextApiRequest, NextApiResponse } from 'next';
|
|
2
|
+
import { jwtDecode } from 'jwt-decode';
|
|
3
|
+
import { SlotlyAuthError } from '../errors';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Authentication context extracted from validated Slotly request
|
|
7
|
+
*/
|
|
8
|
+
export interface SlotlyAuthContext {
|
|
9
|
+
clientKey: string;
|
|
10
|
+
userId?: string;
|
|
11
|
+
tenantId?: string;
|
|
12
|
+
permissions?: string[];
|
|
13
|
+
tokenClaims?: Record<string, any>;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Extended Next.js request with Slotly context
|
|
18
|
+
*/
|
|
19
|
+
export interface SlotlyRequest extends NextApiRequest {
|
|
20
|
+
slotlyContext: SlotlyAuthContext;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Next.js API route handler type
|
|
25
|
+
*/
|
|
26
|
+
export type NextApiHandler = (
|
|
27
|
+
req: NextApiRequest,
|
|
28
|
+
res: NextApiResponse
|
|
29
|
+
) => Promise<void> | void;
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Mock allowlist for client keys
|
|
33
|
+
*
|
|
34
|
+
* ⚠️ DEVELOPMENT ONLY: This is a mock allowlist for development/testing purposes.
|
|
35
|
+
*
|
|
36
|
+
* In production, this MUST be replaced with:
|
|
37
|
+
* - A secure database lookup (e.g., PostgreSQL, DynamoDB)
|
|
38
|
+
* - A secrets management system (e.g., AWS Secrets Manager, HashiCorp Vault)
|
|
39
|
+
* - A Redis cache with appropriate TTL for high-throughput scenarios
|
|
40
|
+
* - Rate limiting per client key to prevent abuse
|
|
41
|
+
*
|
|
42
|
+
* Consider implementing:
|
|
43
|
+
* - Key rotation policies
|
|
44
|
+
* - Per-key rate limiting (especially important for public booking flows)
|
|
45
|
+
* - Audit logging of key usage
|
|
46
|
+
* - Revocation mechanisms
|
|
47
|
+
*
|
|
48
|
+
* TODO: Replace with actual database or cache lookup
|
|
49
|
+
*/
|
|
50
|
+
const allowedKeys: Record<
|
|
51
|
+
string,
|
|
52
|
+
{ tenantId: string; permissions: string[] }
|
|
53
|
+
> = {
|
|
54
|
+
'test-key-123': {
|
|
55
|
+
tenantId: 'bright-accountants',
|
|
56
|
+
permissions: ['booking:create'],
|
|
57
|
+
},
|
|
58
|
+
'admin-ui': {
|
|
59
|
+
tenantId: 'slotly-core',
|
|
60
|
+
permissions: ['*'],
|
|
61
|
+
},
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Validates an API key against the allowlist
|
|
66
|
+
* Returns the associated tenant and permissions if valid
|
|
67
|
+
*/
|
|
68
|
+
function validateApiKey(clientKey: string): {
|
|
69
|
+
tenantId: string;
|
|
70
|
+
permissions: string[];
|
|
71
|
+
} | null {
|
|
72
|
+
if (!clientKey || typeof clientKey !== 'string') {
|
|
73
|
+
return null;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const keyInfo = allowedKeys[clientKey];
|
|
77
|
+
if (!keyInfo) {
|
|
78
|
+
return null;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
return {
|
|
82
|
+
tenantId: keyInfo.tenantId,
|
|
83
|
+
permissions: keyInfo.permissions,
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Extracts and decodes JWT token from Authorization header
|
|
89
|
+
*
|
|
90
|
+
* If present, extracts user identity from the token. This is optional:
|
|
91
|
+
* - Anonymous/client-only flows: Authorization header not required
|
|
92
|
+
* - User-authenticated flows: Authorization header provides user traceability
|
|
93
|
+
*
|
|
94
|
+
* Returns userId from 'sub' claim and full token claims for additional context.
|
|
95
|
+
* Note: Currently decodes without signature validation (development).
|
|
96
|
+
* TODO: Add signature validation in production using Clerk or JWT library.
|
|
97
|
+
*/
|
|
98
|
+
function extractUserToken(authHeader?: string): {
|
|
99
|
+
userId?: string;
|
|
100
|
+
tokenClaims?: Record<string, any>;
|
|
101
|
+
} | null {
|
|
102
|
+
if (!authHeader || typeof authHeader !== 'string') {
|
|
103
|
+
return null;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// Extract Bearer token
|
|
107
|
+
const bearerMatch = authHeader.match(/^Bearer\s+(.+)$/i);
|
|
108
|
+
if (!bearerMatch || !bearerMatch[1]) {
|
|
109
|
+
return null;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const token = bearerMatch[1];
|
|
113
|
+
|
|
114
|
+
try {
|
|
115
|
+
// Decode JWT without signature validation
|
|
116
|
+
// WARNING: This is for development only. Production should validate signatures.
|
|
117
|
+
// TODO: Add signature validation in production (e.g., using Clerk's verifyToken or jsonwebtoken)
|
|
118
|
+
const claims = jwtDecode(token) as Record<string, any>;
|
|
119
|
+
|
|
120
|
+
if (!claims || typeof claims !== 'object') {
|
|
121
|
+
// eslint-disable-next-line no-console
|
|
122
|
+
console.error('[Slotly SDK] Invalid JWT token: claims is not an object');
|
|
123
|
+
return null;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// Extract userId from 'sub' claim
|
|
127
|
+
const userId = claims.sub || claims.user_id || claims.userId;
|
|
128
|
+
|
|
129
|
+
return {
|
|
130
|
+
userId: typeof userId === 'string' ? userId : undefined,
|
|
131
|
+
tokenClaims: claims,
|
|
132
|
+
};
|
|
133
|
+
} catch (error) {
|
|
134
|
+
// eslint-disable-next-line no-console
|
|
135
|
+
console.error('[Slotly SDK] Failed to decode JWT token:', error);
|
|
136
|
+
return null;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Validates Slotly request headers and extracts authentication context
|
|
142
|
+
*
|
|
143
|
+
* Validates:
|
|
144
|
+
* - `x-slotly-api-key`: Required header identifying the client/application
|
|
145
|
+
* - `Authorization: Bearer ...`: Optional header providing user identity (if present, extracts user identity)
|
|
146
|
+
*
|
|
147
|
+
* Throws SlotlyAuthError if validation fails. Error includes code, message, and details
|
|
148
|
+
* fields that align with the ApiResponse<T> error shape for consistent error handling.
|
|
149
|
+
*
|
|
150
|
+
* @throws {SlotlyAuthError} If x-slotly-api-key is missing, invalid, or not in allowlist
|
|
151
|
+
*/
|
|
152
|
+
function validateAndExtractContext(req: NextApiRequest): SlotlyAuthContext {
|
|
153
|
+
// Extract x-slotly-api-key header (required)
|
|
154
|
+
// Support both lowercase and mixed case headers
|
|
155
|
+
const clientKey =
|
|
156
|
+
(req.headers['x-slotly-api-key'] as string) ||
|
|
157
|
+
(req.headers['X-Slotly-Api-Key'] as string) ||
|
|
158
|
+
(req.headers['x-slotly-api-key'] as string);
|
|
159
|
+
|
|
160
|
+
if (!clientKey || typeof clientKey !== 'string' || clientKey.trim().length === 0) {
|
|
161
|
+
const error = new SlotlyAuthError(
|
|
162
|
+
'Missing or invalid x-slotly-api-key header. API key is required.',
|
|
163
|
+
{ header: 'x-slotly-api-key' }
|
|
164
|
+
);
|
|
165
|
+
// eslint-disable-next-line no-console
|
|
166
|
+
console.error('[Slotly SDK] Validation failed:', error.message);
|
|
167
|
+
throw error;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// Validate API key against allowlist
|
|
171
|
+
const keyInfo = validateApiKey(clientKey);
|
|
172
|
+
if (!keyInfo) {
|
|
173
|
+
const error = new SlotlyAuthError(
|
|
174
|
+
'Invalid API key. Client key not found in allowlist.',
|
|
175
|
+
{ clientKey: clientKey.substring(0, 8) + '...' }
|
|
176
|
+
);
|
|
177
|
+
// eslint-disable-next-line no-console
|
|
178
|
+
console.error('[Slotly SDK] Validation failed:', error.message);
|
|
179
|
+
throw error;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// Extract Authorization header (optional)
|
|
183
|
+
// Support both lowercase and mixed case headers
|
|
184
|
+
const authHeader =
|
|
185
|
+
(req.headers.authorization as string) ||
|
|
186
|
+
(req.headers.Authorization as string);
|
|
187
|
+
|
|
188
|
+
const tokenInfo = authHeader ? extractUserToken(authHeader) : null;
|
|
189
|
+
|
|
190
|
+
// Build context
|
|
191
|
+
const context: SlotlyAuthContext = {
|
|
192
|
+
clientKey,
|
|
193
|
+
tenantId: keyInfo.tenantId,
|
|
194
|
+
permissions: keyInfo.permissions,
|
|
195
|
+
...(tokenInfo?.userId && { userId: tokenInfo.userId }),
|
|
196
|
+
...(tokenInfo?.tokenClaims && { tokenClaims: tokenInfo.tokenClaims }),
|
|
197
|
+
};
|
|
198
|
+
|
|
199
|
+
return context;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* Safely extracts Slotly context from Next.js request
|
|
204
|
+
*
|
|
205
|
+
* Use this helper when you need to access the context outside of the middleware wrapper,
|
|
206
|
+
* or to check if the context exists.
|
|
207
|
+
*
|
|
208
|
+
* **Important:** Other route handlers should guard user-required flows:
|
|
209
|
+
* ```ts
|
|
210
|
+
* const context = getSlotlyContext(req);
|
|
211
|
+
* if (!context?.userId) {
|
|
212
|
+
* return res.status(403).json({
|
|
213
|
+
* success: false,
|
|
214
|
+
* error: { code: 'USER_REQUIRED', message: 'User authentication required' }
|
|
215
|
+
* });
|
|
216
|
+
* }
|
|
217
|
+
* ```
|
|
218
|
+
*
|
|
219
|
+
* @param req Next.js API request
|
|
220
|
+
* @returns SlotlyAuthContext if present, null otherwise
|
|
221
|
+
*/
|
|
222
|
+
export function getSlotlyContext(
|
|
223
|
+
req: NextApiRequest
|
|
224
|
+
): SlotlyAuthContext | null {
|
|
225
|
+
const slotlyReq = req as SlotlyRequest;
|
|
226
|
+
return slotlyReq.slotlyContext || null;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* Middleware wrapper for Next.js API routes and Edge Functions
|
|
231
|
+
* Validates Slotly authentication headers and injects context into request
|
|
232
|
+
*
|
|
233
|
+
* **Supported Environments:**
|
|
234
|
+
* - Next.js API Routes (serverless functions)
|
|
235
|
+
* - Next.js Edge Functions (Edge Runtime)
|
|
236
|
+
* - Vercel Edge Functions
|
|
237
|
+
*
|
|
238
|
+
* **Note on Edge Functions:**
|
|
239
|
+
* Header names are normalized, but edge runtime may have different header casing.
|
|
240
|
+
* This middleware handles both `x-slotly-api-key` and `X-Slotly-Api-Key` variants.
|
|
241
|
+
*
|
|
242
|
+
* **Error Response Format:**
|
|
243
|
+
* All error responses follow the `ApiResponse<T>` format for consistency:
|
|
244
|
+
* ```ts
|
|
245
|
+
* {
|
|
246
|
+
* success: false,
|
|
247
|
+
* error: {
|
|
248
|
+
* code: string, // Error code (e.g., 'AUTH_ERROR', 'INTERNAL_ERROR')
|
|
249
|
+
* message: string, // Human-readable error message
|
|
250
|
+
* details?: any // Additional error details
|
|
251
|
+
* }
|
|
252
|
+
* }
|
|
253
|
+
* ```
|
|
254
|
+
*
|
|
255
|
+
* **Usage:**
|
|
256
|
+
* ```ts
|
|
257
|
+
* import { validateSlotlyRequest } from "@slotly/sdk/middleware/validateSlotlyRequest";
|
|
258
|
+
*
|
|
259
|
+
* export default validateSlotlyRequest(async (req, res) => {
|
|
260
|
+
* const { userId, clientKey, tenantId, permissions } = req.slotlyContext;
|
|
261
|
+
*
|
|
262
|
+
* // Check if user authentication is required for this endpoint
|
|
263
|
+
* if (!userId) {
|
|
264
|
+
* return res.status(403).json({
|
|
265
|
+
* success: false,
|
|
266
|
+
* error: { code: 'USER_REQUIRED', message: 'User authentication required' }
|
|
267
|
+
* });
|
|
268
|
+
* }
|
|
269
|
+
*
|
|
270
|
+
* // Authenticated + scoped — safe to proceed
|
|
271
|
+
* res.json({ success: true, data: { userId, tenantId } });
|
|
272
|
+
* });
|
|
273
|
+
* ```
|
|
274
|
+
*
|
|
275
|
+
* **Performance Considerations:**
|
|
276
|
+
* - For high-throughput scenarios (e.g., many public booking flows), consider:
|
|
277
|
+
* - Caching API key lookups with TTL (Redis, in-memory cache)
|
|
278
|
+
* - Rate limiting per client key
|
|
279
|
+
* - Database connection pooling for key validation
|
|
280
|
+
*
|
|
281
|
+
* @param handler Next.js API route handler or Edge Function handler
|
|
282
|
+
* @returns Wrapped handler with Slotly validation
|
|
283
|
+
*/
|
|
284
|
+
export function validateSlotlyRequest(
|
|
285
|
+
handler: (req: SlotlyRequest, res: NextApiResponse) => Promise<void> | void
|
|
286
|
+
) {
|
|
287
|
+
return async (req: NextApiRequest, res: NextApiResponse): Promise<void> => {
|
|
288
|
+
try {
|
|
289
|
+
// Validate and extract context
|
|
290
|
+
const context = validateAndExtractContext(req);
|
|
291
|
+
|
|
292
|
+
// Inject context into request
|
|
293
|
+
const slotlyReq = req as SlotlyRequest;
|
|
294
|
+
slotlyReq.slotlyContext = context;
|
|
295
|
+
|
|
296
|
+
// Call the actual handler with validated request
|
|
297
|
+
await handler(slotlyReq, res);
|
|
298
|
+
} catch (error) {
|
|
299
|
+
// Handle SlotlyAuthError
|
|
300
|
+
if (error instanceof SlotlyAuthError) {
|
|
301
|
+
// eslint-disable-next-line no-console
|
|
302
|
+
console.error(
|
|
303
|
+
'[Slotly SDK] Authentication failed:',
|
|
304
|
+
error.message,
|
|
305
|
+
error.details
|
|
306
|
+
);
|
|
307
|
+
|
|
308
|
+
// Error response follows ApiResponse<T> format for consistency
|
|
309
|
+
// SlotlyAuthError includes code, message, and details fields that align
|
|
310
|
+
// with the ApiResponse<T> error shape
|
|
311
|
+
res.status(error.statusCode || 401).json({
|
|
312
|
+
success: false,
|
|
313
|
+
error: {
|
|
314
|
+
code: error.code,
|
|
315
|
+
message: error.message,
|
|
316
|
+
details: error.details,
|
|
317
|
+
},
|
|
318
|
+
});
|
|
319
|
+
return;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
// Handle other errors
|
|
323
|
+
// eslint-disable-next-line no-console
|
|
324
|
+
console.error('[Slotly SDK] Unexpected error:', error);
|
|
325
|
+
|
|
326
|
+
// Error response follows ApiResponse<T> format for consistency
|
|
327
|
+
res.status(500).json({
|
|
328
|
+
success: false,
|
|
329
|
+
error: {
|
|
330
|
+
code: 'INTERNAL_ERROR',
|
|
331
|
+
message: 'An unexpected error occurred',
|
|
332
|
+
},
|
|
333
|
+
});
|
|
334
|
+
}
|
|
335
|
+
};
|
|
336
|
+
}
|
|
337
|
+
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import axiosMockAdapter from 'axios-mock-adapter';
|
|
2
|
+
import { AxiosInstance } from 'axios';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Setup mock adapter for testing
|
|
6
|
+
* This allows you to mock API responses during development and testing
|
|
7
|
+
*
|
|
8
|
+
* Usage:
|
|
9
|
+
* ```ts
|
|
10
|
+
* import { createSlotlyClient } from '../client';
|
|
11
|
+
* import { setupMockAdapter } from './mocks/mockAdapter';
|
|
12
|
+
*
|
|
13
|
+
* const client = createSlotlyClient();
|
|
14
|
+
* const mockAdapter = setupMockAdapter(client);
|
|
15
|
+
*
|
|
16
|
+
* // Mock a response
|
|
17
|
+
* mockAdapter.onGet('/api/v1/tenant-config/my-slug').reply(200, {
|
|
18
|
+
* success: true,
|
|
19
|
+
* data: { id: '1', slug: 'my-slug', name: 'My Tenant' }
|
|
20
|
+
* });
|
|
21
|
+
* ```
|
|
22
|
+
*/
|
|
23
|
+
export function setupMockAdapter(client: AxiosInstance): axiosMockAdapter {
|
|
24
|
+
const mockAdapter = new axiosMockAdapter(client, {
|
|
25
|
+
delayResponse: 0, // Set to simulate network delay if needed
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
// TODO: Add default mock responses for common endpoints if needed
|
|
29
|
+
|
|
30
|
+
return mockAdapter;
|
|
31
|
+
}
|
|
32
|
+
|
package/src/retry.ts
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import axios, { AxiosError, InternalAxiosRequestConfig } from 'axios';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Exponential backoff retry configuration
|
|
5
|
+
*/
|
|
6
|
+
export interface RetryConfig {
|
|
7
|
+
maxRetries: number;
|
|
8
|
+
retryDelay: number; // Base delay in milliseconds
|
|
9
|
+
retryCondition?: (error: AxiosError) => boolean;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const defaultRetryConfig: RetryConfig = {
|
|
13
|
+
maxRetries: 3,
|
|
14
|
+
retryDelay: 1000, // 1 second
|
|
15
|
+
retryCondition: (error: AxiosError) => {
|
|
16
|
+
// Retry on network errors and 5xx server errors
|
|
17
|
+
if (!error.response) {
|
|
18
|
+
return true; // Network error
|
|
19
|
+
}
|
|
20
|
+
const status = error.response.status;
|
|
21
|
+
return status >= 500 && status < 600;
|
|
22
|
+
},
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Calculate exponential backoff delay with jitter
|
|
27
|
+
*/
|
|
28
|
+
function calculateRetryDelay(attempt: number, baseDelay: number): number {
|
|
29
|
+
const exponentialDelay = baseDelay * Math.pow(2, attempt);
|
|
30
|
+
const jitter = Math.random() * 0.3 * exponentialDelay; // Add up to 30% jitter
|
|
31
|
+
return exponentialDelay + jitter;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Axios interceptor for retry logic
|
|
36
|
+
*/
|
|
37
|
+
export function setupRetryInterceptor(
|
|
38
|
+
config: RetryConfig = defaultRetryConfig
|
|
39
|
+
) {
|
|
40
|
+
return async (error: AxiosError) => {
|
|
41
|
+
const requestConfig = error.config as InternalAxiosRequestConfig & {
|
|
42
|
+
_retry?: boolean;
|
|
43
|
+
_retryCount?: number;
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
// If no config, reject
|
|
47
|
+
if (!requestConfig) {
|
|
48
|
+
return Promise.reject(error);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Initialize retry count
|
|
52
|
+
if (requestConfig._retryCount === undefined) {
|
|
53
|
+
requestConfig._retryCount = 0;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Check if we should retry
|
|
57
|
+
const shouldRetry =
|
|
58
|
+
requestConfig._retryCount < config.maxRetries &&
|
|
59
|
+
(!config.retryCondition || config.retryCondition(error));
|
|
60
|
+
|
|
61
|
+
if (!shouldRetry) {
|
|
62
|
+
return Promise.reject(error);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Mark as retrying
|
|
66
|
+
requestConfig._retry = true;
|
|
67
|
+
requestConfig._retryCount += 1;
|
|
68
|
+
|
|
69
|
+
// Calculate delay
|
|
70
|
+
const delay = calculateRetryDelay(
|
|
71
|
+
requestConfig._retryCount - 1,
|
|
72
|
+
config.retryDelay
|
|
73
|
+
);
|
|
74
|
+
|
|
75
|
+
// Wait before retrying
|
|
76
|
+
await new Promise((resolve) => setTimeout(resolve, delay));
|
|
77
|
+
|
|
78
|
+
// Retry the request
|
|
79
|
+
return axios(requestConfig);
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
|
package/src/types/api.ts
ADDED
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Standard API response format for all Slotly API endpoints
|
|
3
|
+
*/
|
|
4
|
+
export interface ApiResponse<T> {
|
|
5
|
+
success: boolean;
|
|
6
|
+
data?: T;
|
|
7
|
+
error?: {
|
|
8
|
+
code: string;
|
|
9
|
+
message: string;
|
|
10
|
+
details?: any;
|
|
11
|
+
};
|
|
12
|
+
pagination?: {
|
|
13
|
+
page: number;
|
|
14
|
+
limit: number;
|
|
15
|
+
total: number;
|
|
16
|
+
totalPages: number;
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Placeholder types for domain models
|
|
22
|
+
* TODO: Replace with actual types from the Slotly API
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
export interface Tenant {
|
|
26
|
+
id: string;
|
|
27
|
+
slug: string;
|
|
28
|
+
name: string;
|
|
29
|
+
// TODO: Add actual tenant properties
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Tenant branding configuration
|
|
34
|
+
*/
|
|
35
|
+
export interface TenantBranding {
|
|
36
|
+
logoUrl?: string;
|
|
37
|
+
primaryColor?: string;
|
|
38
|
+
secondaryColor?: string;
|
|
39
|
+
faviconUrl?: string;
|
|
40
|
+
customCss?: string;
|
|
41
|
+
// TODO: Add additional branding properties
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Service item within a service
|
|
46
|
+
*/
|
|
47
|
+
export interface ServiceItem {
|
|
48
|
+
id: string;
|
|
49
|
+
serviceId: string;
|
|
50
|
+
name: string;
|
|
51
|
+
description?: string;
|
|
52
|
+
price?: number;
|
|
53
|
+
duration?: number;
|
|
54
|
+
available?: boolean;
|
|
55
|
+
// TODO: Add actual service item properties
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Enhanced Service type with nested service items
|
|
60
|
+
*/
|
|
61
|
+
export interface ServiceWithItems extends Service {
|
|
62
|
+
/** Array of service items nested within this service (optional, may be empty array) */
|
|
63
|
+
serviceItems?: ServiceItem[];
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Service item with parent service reference
|
|
68
|
+
*/
|
|
69
|
+
export interface ServiceItemWithService extends ServiceItem {
|
|
70
|
+
/** Parent service reference (optional, for convenience when fetching items separately) */
|
|
71
|
+
parentService?: Service;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Category extracted from services
|
|
76
|
+
*/
|
|
77
|
+
export interface Category {
|
|
78
|
+
id: string;
|
|
79
|
+
name: string;
|
|
80
|
+
slug?: string;
|
|
81
|
+
description?: string;
|
|
82
|
+
/** Array of service IDs in this category */
|
|
83
|
+
serviceIds?: string[];
|
|
84
|
+
/** Optional: services in this category (if expanded) */
|
|
85
|
+
services?: Service[];
|
|
86
|
+
// TODO: Add actual category properties
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export interface Booking {
|
|
90
|
+
id: string;
|
|
91
|
+
tenantId: string;
|
|
92
|
+
slotId?: string;
|
|
93
|
+
customerId?: string;
|
|
94
|
+
status?: string;
|
|
95
|
+
// TODO: Add actual booking properties
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export interface Service {
|
|
99
|
+
id: string;
|
|
100
|
+
tenantId: string;
|
|
101
|
+
name: string;
|
|
102
|
+
description?: string;
|
|
103
|
+
duration?: number;
|
|
104
|
+
price?: number;
|
|
105
|
+
// TODO: Add actual service properties
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export interface Slot {
|
|
109
|
+
id: string;
|
|
110
|
+
tenantId: string;
|
|
111
|
+
serviceId?: string;
|
|
112
|
+
startTime: string;
|
|
113
|
+
endTime: string;
|
|
114
|
+
available: boolean;
|
|
115
|
+
// TODO: Add actual slot properties
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export interface Customer {
|
|
119
|
+
id: string;
|
|
120
|
+
tenantId: string;
|
|
121
|
+
email?: string;
|
|
122
|
+
name?: string;
|
|
123
|
+
phone?: string;
|
|
124
|
+
// TODO: Add actual customer properties
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
export interface Flow {
|
|
128
|
+
id: string;
|
|
129
|
+
tenantId: string;
|
|
130
|
+
name: string;
|
|
131
|
+
steps?: FlowStep[];
|
|
132
|
+
// TODO: Add actual flow properties
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
export interface FlowStep {
|
|
136
|
+
id: string;
|
|
137
|
+
type: string;
|
|
138
|
+
config?: Record<string, any>;
|
|
139
|
+
// TODO: Add actual flow step properties
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
export interface Studio {
|
|
143
|
+
id: string;
|
|
144
|
+
name: string;
|
|
145
|
+
slug?: string;
|
|
146
|
+
// TODO: Add actual studio properties
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
export interface Notification {
|
|
150
|
+
id: string;
|
|
151
|
+
tenantId?: string;
|
|
152
|
+
type: string;
|
|
153
|
+
recipient: string;
|
|
154
|
+
content: string;
|
|
155
|
+
sentAt?: string;
|
|
156
|
+
// TODO: Add actual notification properties
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
export interface DataQualityIssue {
|
|
160
|
+
id: string;
|
|
161
|
+
tenantId?: string;
|
|
162
|
+
type: string;
|
|
163
|
+
severity: 'low' | 'medium' | 'high';
|
|
164
|
+
message: string;
|
|
165
|
+
resolved: boolean;
|
|
166
|
+
// TODO: Add actual data quality issue properties
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Full tenant configuration including branding, services, and service items
|
|
171
|
+
* Service items are nested within each service object (not as a separate top-level array)
|
|
172
|
+
*/
|
|
173
|
+
export interface TenantFullConfig {
|
|
174
|
+
/** Tenant ID (required, non-nullable) */
|
|
175
|
+
id: string;
|
|
176
|
+
/** Tenant slug (required, non-nullable) */
|
|
177
|
+
slug: string;
|
|
178
|
+
/** Tenant name (required, non-nullable) */
|
|
179
|
+
name: string;
|
|
180
|
+
/** Branding configuration (optional) */
|
|
181
|
+
branding?: TenantBranding;
|
|
182
|
+
/** Array of services with nested service items (required, non-nullable, may be empty array) */
|
|
183
|
+
services: ServiceWithItems[];
|
|
184
|
+
/** Enabled features (optional) */
|
|
185
|
+
featuresEnabled?: string[];
|
|
186
|
+
/** Subscription level (optional) */
|
|
187
|
+
subscriptionLevel?: string;
|
|
188
|
+
/** Additional configuration (optional) */
|
|
189
|
+
config?: Record<string, any>;
|
|
190
|
+
// TODO: Add additional tenant config properties
|
|
191
|
+
}
|
|
192
|
+
|