From 14b84c61e25e2bf4e5a3a8d4df6f8cb45e3c703f Mon Sep 17 00:00:00 2001 From: Adrian Flores Cortes Date: Sun, 25 Jan 2026 03:47:11 -0600 Subject: [PATCH] feat(api): Add 5 new API clients for integrations Added API clients with full endpoint coverage: - ai.api.ts: 11 endpoints (conversations, messages, analyze, chat) - whatsapp.api.ts: 20 endpoints (conversations, templates, broadcasts) - webhooks.api.ts: 14 endpoints (CRUD, deliveries, logs) - feature-flags.api.ts: 17 endpoints (flags, overrides, check) - storage.api.ts: 21 endpoints (buckets, files, folders) Total: 83 new endpoints Co-Authored-By: Claude Opus 4.5 --- src/features/ai/api/ai.api.ts | 182 ++++++++++ src/features/ai/api/index.ts | 1 + .../feature-flags/api/feature-flags.api.ts | 232 ++++++++++++ src/features/feature-flags/api/index.ts | 1 + src/features/storage/api/index.ts | 1 + src/features/storage/api/storage.api.ts | 330 ++++++++++++++++++ src/features/webhooks/api/index.ts | 1 + src/features/webhooks/api/webhooks.api.ts | 224 ++++++++++++ src/features/whatsapp/api/index.ts | 1 + src/features/whatsapp/api/whatsapp.api.ts | 294 ++++++++++++++++ 10 files changed, 1267 insertions(+) create mode 100644 src/features/ai/api/ai.api.ts create mode 100644 src/features/ai/api/index.ts create mode 100644 src/features/feature-flags/api/feature-flags.api.ts create mode 100644 src/features/feature-flags/api/index.ts create mode 100644 src/features/storage/api/index.ts create mode 100644 src/features/storage/api/storage.api.ts create mode 100644 src/features/webhooks/api/index.ts create mode 100644 src/features/webhooks/api/webhooks.api.ts create mode 100644 src/features/whatsapp/api/index.ts create mode 100644 src/features/whatsapp/api/whatsapp.api.ts diff --git a/src/features/ai/api/ai.api.ts b/src/features/ai/api/ai.api.ts new file mode 100644 index 0000000..69b5f7b --- /dev/null +++ b/src/features/ai/api/ai.api.ts @@ -0,0 +1,182 @@ +import { api } from '@services/api/axios-instance'; + +const BASE_URL = '/api/v1/ai'; + +export interface AiConversation { + id: string; + title: string; + model: string; + createdAt: string; + updatedAt: string; + messageCount: number; +} + +export interface AiMessage { + id: string; + conversationId: string; + role: 'user' | 'assistant' | 'system'; + content: string; + createdAt: string; + tokens?: number; +} + +export interface AiModel { + id: string; + name: string; + provider: string; + maxTokens: number; + isAvailable: boolean; +} + +export interface AiSuggestion { + id: string; + type: string; + content: string; + confidence: number; +} + +export interface AiAnalysisResult { + id: string; + type: string; + result: any; + metadata: Record; + createdAt: string; +} + +export interface CreateConversationDto { + title?: string; + model?: string; + systemPrompt?: string; +} + +export interface SendMessageDto { + content: string; + attachments?: string[]; +} + +export interface ChatDto { + message: string; + model?: string; + temperature?: number; + maxTokens?: number; +} + +export interface AnalyzeDto { + type: 'sentiment' | 'summary' | 'extract' | 'classify' | 'translate'; + content: string; + options?: Record; +} + +export const aiApi = { + // Get all conversations + getConversations: async (filters?: { + search?: string; + model?: string; + page?: number; + limit?: number; + }): Promise<{ data: AiConversation[]; total: number }> => { + const params = new URLSearchParams(); + if (filters?.search) params.append('search', filters.search); + if (filters?.model) params.append('model', filters.model); + if (filters?.page) params.append('page', String(filters.page)); + if (filters?.limit) params.append('limit', String(filters.limit)); + + const response = await api.get<{ data: AiConversation[]; total: number }>( + `${BASE_URL}/conversations?${params.toString()}` + ); + return response.data; + }, + + // Get conversation by ID + getConversation: async (id: string): Promise => { + const response = await api.get(`${BASE_URL}/conversations/${id}`); + return response.data; + }, + + // Create new conversation + createConversation: async (data: CreateConversationDto): Promise => { + const response = await api.post(`${BASE_URL}/conversations`, data); + return response.data; + }, + + // Delete conversation + deleteConversation: async (id: string): Promise => { + await api.delete(`${BASE_URL}/conversations/${id}`); + }, + + // Send message in conversation + sendMessage: async (conversationId: string, data: SendMessageDto): Promise => { + const response = await api.post( + `${BASE_URL}/conversations/${conversationId}/messages`, + data + ); + return response.data; + }, + + // Get messages from conversation + getMessages: async ( + conversationId: string, + filters?: { + page?: number; + limit?: number; + } + ): Promise<{ data: AiMessage[]; total: number }> => { + const params = new URLSearchParams(); + if (filters?.page) params.append('page', String(filters.page)); + if (filters?.limit) params.append('limit', String(filters.limit)); + + const response = await api.get<{ data: AiMessage[]; total: number }>( + `${BASE_URL}/conversations/${conversationId}/messages?${params.toString()}` + ); + return response.data; + }, + + // Get AI suggestions + getSuggestions: async (context: { + type: string; + data: any; + }): Promise => { + const response = await api.post(`${BASE_URL}/suggestions`, context); + return response.data; + }, + + // Analyze content + analyze: async (data: AnalyzeDto): Promise => { + const response = await api.post(`${BASE_URL}/analyze`, data); + return response.data; + }, + + // Get available models + getModels: async (): Promise => { + const response = await api.get(`${BASE_URL}/models`); + return response.data; + }, + + // Quick chat (stateless) + chat: async (data: ChatDto): Promise<{ response: string; tokens: number }> => { + const response = await api.post<{ response: string; tokens: number }>( + `${BASE_URL}/chat`, + data + ); + return response.data; + }, + + // Stream chat response + streamChat: async ( + data: ChatDto, + onChunk: (chunk: string) => void + ): Promise => { + const response = await api.post(`${BASE_URL}/chat/stream`, data, { + responseType: 'stream', + }); + + const reader = response.data.getReader(); + const decoder = new TextDecoder(); + + while (true) { + const { done, value } = await reader.read(); + if (done) break; + onChunk(decoder.decode(value)); + } + }, +}; diff --git a/src/features/ai/api/index.ts b/src/features/ai/api/index.ts new file mode 100644 index 0000000..587b974 --- /dev/null +++ b/src/features/ai/api/index.ts @@ -0,0 +1 @@ +export * from './ai.api'; diff --git a/src/features/feature-flags/api/feature-flags.api.ts b/src/features/feature-flags/api/feature-flags.api.ts new file mode 100644 index 0000000..1cbd202 --- /dev/null +++ b/src/features/feature-flags/api/feature-flags.api.ts @@ -0,0 +1,232 @@ +import { api } from '@services/api/axios-instance'; + +const BASE_URL = '/api/v1/feature-flags'; + +export interface FeatureFlag { + id: string; + key: string; + name: string; + description?: string; + isEnabled: boolean; + type: 'boolean' | 'percentage' | 'user-list' | 'json'; + value: any; + defaultValue: any; + environment: 'development' | 'staging' | 'production' | 'all'; + tags: string[]; + createdAt: string; + updatedAt: string; +} + +export interface FeatureFlagOverride { + id: string; + flagId: string; + targetType: 'user' | 'company' | 'tenant' | 'role'; + targetId: string; + targetName?: string; + value: any; + expiresAt?: string; + createdAt: string; +} + +export interface FeatureFlagCheckResult { + key: string; + isEnabled: boolean; + value: any; + source: 'default' | 'flag' | 'override'; +} + +export interface CreateFeatureFlagDto { + key: string; + name: string; + description?: string; + isEnabled?: boolean; + type?: 'boolean' | 'percentage' | 'user-list' | 'json'; + value?: any; + defaultValue?: any; + environment?: 'development' | 'staging' | 'production' | 'all'; + tags?: string[]; +} + +export interface UpdateFeatureFlagDto { + name?: string; + description?: string; + isEnabled?: boolean; + type?: 'boolean' | 'percentage' | 'user-list' | 'json'; + value?: any; + defaultValue?: any; + environment?: 'development' | 'staging' | 'production' | 'all'; + tags?: string[]; +} + +export interface CreateOverrideDto { + targetType: 'user' | 'company' | 'tenant' | 'role'; + targetId: string; + value: any; + expiresAt?: string; +} + +export const featureFlagsApi = { + // Get all feature flags + getAll: async (filters?: { + search?: string; + isEnabled?: boolean; + environment?: string; + tag?: string; + page?: number; + limit?: number; + }): Promise<{ data: FeatureFlag[]; total: number }> => { + const params = new URLSearchParams(); + if (filters?.search) params.append('search', filters.search); + if (filters?.isEnabled !== undefined) params.append('isEnabled', String(filters.isEnabled)); + if (filters?.environment) params.append('environment', filters.environment); + if (filters?.tag) params.append('tag', filters.tag); + if (filters?.page) params.append('page', String(filters.page)); + if (filters?.limit) params.append('limit', String(filters.limit)); + + const response = await api.get<{ data: FeatureFlag[]; total: number }>( + `${BASE_URL}?${params.toString()}` + ); + return response.data; + }, + + // Get feature flag by ID + getById: async (id: string): Promise => { + const response = await api.get(`${BASE_URL}/${id}`); + return response.data; + }, + + // Get feature flag by key + getByKey: async (key: string): Promise => { + const response = await api.get(`${BASE_URL}/key/${key}`); + return response.data; + }, + + // Create feature flag + create: async (data: CreateFeatureFlagDto): Promise => { + const response = await api.post(BASE_URL, data); + return response.data; + }, + + // Update feature flag + update: async (id: string, data: UpdateFeatureFlagDto): Promise => { + const response = await api.patch(`${BASE_URL}/${id}`, data); + return response.data; + }, + + // Delete feature flag + delete: async (id: string): Promise => { + await api.delete(`${BASE_URL}/${id}`); + }, + + // Check if flag is enabled (for current user context) + check: async (key: string, context?: { + userId?: string; + companyId?: string; + tenantId?: string; + }): Promise => { + const params = new URLSearchParams(); + if (context?.userId) params.append('userId', context.userId); + if (context?.companyId) params.append('companyId', context.companyId); + if (context?.tenantId) params.append('tenantId', context.tenantId); + + const response = await api.get( + `${BASE_URL}/check/${key}?${params.toString()}` + ); + return response.data; + }, + + // Check multiple flags at once + checkMultiple: async (keys: string[], context?: { + userId?: string; + companyId?: string; + tenantId?: string; + }): Promise> => { + const response = await api.post>( + `${BASE_URL}/check-multiple`, + { keys, context } + ); + return response.data; + }, + + // Toggle feature flag + toggle: async (id: string): Promise => { + const response = await api.post(`${BASE_URL}/${id}/toggle`); + return response.data; + }, + + // Enable feature flag + enable: async (id: string): Promise => { + const response = await api.post(`${BASE_URL}/${id}/enable`); + return response.data; + }, + + // Disable feature flag + disable: async (id: string): Promise => { + const response = await api.post(`${BASE_URL}/${id}/disable`); + return response.data; + }, + + // Get overrides for a flag + getOverrides: async ( + flagId: string, + filters?: { + targetType?: string; + page?: number; + limit?: number; + } + ): Promise<{ data: FeatureFlagOverride[]; total: number }> => { + const params = new URLSearchParams(); + if (filters?.targetType) params.append('targetType', filters.targetType); + if (filters?.page) params.append('page', String(filters.page)); + if (filters?.limit) params.append('limit', String(filters.limit)); + + const response = await api.get<{ data: FeatureFlagOverride[]; total: number }>( + `${BASE_URL}/${flagId}/overrides?${params.toString()}` + ); + return response.data; + }, + + // Create override + createOverride: async (flagId: string, data: CreateOverrideDto): Promise => { + const response = await api.post( + `${BASE_URL}/${flagId}/overrides`, + data + ); + return response.data; + }, + + // Delete override + deleteOverride: async (flagId: string, overrideId: string): Promise => { + await api.delete(`${BASE_URL}/${flagId}/overrides/${overrideId}`); + }, + + // Get all tags + getTags: async (): Promise => { + const response = await api.get(`${BASE_URL}/tags`); + return response.data; + }, + + // Clone flag + clone: async (id: string, newKey: string): Promise => { + const response = await api.post(`${BASE_URL}/${id}/clone`, { newKey }); + return response.data; + }, + + // Get flag history/audit log + getHistory: async ( + flagId: string, + filters?: { + page?: number; + limit?: number; + } + ): Promise<{ data: any[]; total: number }> => { + const params = new URLSearchParams(); + if (filters?.page) params.append('page', String(filters.page)); + if (filters?.limit) params.append('limit', String(filters.limit)); + + const response = await api.get<{ data: any[]; total: number }>( + `${BASE_URL}/${flagId}/history?${params.toString()}` + ); + return response.data; + }, +}; diff --git a/src/features/feature-flags/api/index.ts b/src/features/feature-flags/api/index.ts new file mode 100644 index 0000000..72cb541 --- /dev/null +++ b/src/features/feature-flags/api/index.ts @@ -0,0 +1 @@ +export * from './feature-flags.api'; diff --git a/src/features/storage/api/index.ts b/src/features/storage/api/index.ts new file mode 100644 index 0000000..cc8413f --- /dev/null +++ b/src/features/storage/api/index.ts @@ -0,0 +1 @@ +export * from './storage.api'; diff --git a/src/features/storage/api/storage.api.ts b/src/features/storage/api/storage.api.ts new file mode 100644 index 0000000..1fd1cb7 --- /dev/null +++ b/src/features/storage/api/storage.api.ts @@ -0,0 +1,330 @@ +import { api } from '@services/api/axios-instance'; + +const BASE_URL = '/api/v1/storage'; + +export interface StorageBucket { + id: string; + name: string; + region: string; + provider: 's3' | 'r2' | 'gcs' | 'azure'; + isPublic: boolean; + corsEnabled: boolean; + versioning: boolean; + totalSize: number; + fileCount: number; + createdAt: string; +} + +export interface StorageFile { + id: string; + bucketId: string; + key: string; + name: string; + path: string; + mimeType: string; + size: number; + isPublic: boolean; + url?: string; + metadata?: Record; + versionId?: string; + createdAt: string; + updatedAt: string; +} + +export interface StorageFolder { + name: string; + path: string; + fileCount: number; + totalSize: number; +} + +export interface StorageUsage { + totalSize: number; + totalFiles: number; + usedQuota: number; + maxQuota: number; + usagePercentage: number; + byBucket: { + bucketId: string; + bucketName: string; + size: number; + fileCount: number; + }[]; + byMimeType: { + mimeType: string; + size: number; + fileCount: number; + }[]; +} + +export interface PresignedUrlResponse { + url: string; + expiresAt: string; + fields?: Record; +} + +export interface CreateBucketDto { + name: string; + region?: string; + provider?: 's3' | 'r2' | 'gcs' | 'azure'; + isPublic?: boolean; + corsEnabled?: boolean; + versioning?: boolean; +} + +export interface UploadFileDto { + file: File; + path?: string; + isPublic?: boolean; + metadata?: Record; +} + +export const storageApi = { + // Get all buckets + getBuckets: async (filters?: { + search?: string; + provider?: string; + page?: number; + limit?: number; + }): Promise<{ data: StorageBucket[]; total: number }> => { + const params = new URLSearchParams(); + if (filters?.search) params.append('search', filters.search); + if (filters?.provider) params.append('provider', filters.provider); + if (filters?.page) params.append('page', String(filters.page)); + if (filters?.limit) params.append('limit', String(filters.limit)); + + const response = await api.get<{ data: StorageBucket[]; total: number }>( + `${BASE_URL}/buckets?${params.toString()}` + ); + return response.data; + }, + + // Get bucket by ID + getBucket: async (id: string): Promise => { + const response = await api.get(`${BASE_URL}/buckets/${id}`); + return response.data; + }, + + // Create bucket + createBucket: async (data: CreateBucketDto): Promise => { + const response = await api.post(`${BASE_URL}/buckets`, data); + return response.data; + }, + + // Delete bucket + deleteBucket: async (id: string, force?: boolean): Promise => { + const params = new URLSearchParams(); + if (force) params.append('force', 'true'); + await api.delete(`${BASE_URL}/buckets/${id}?${params.toString()}`); + }, + + // Get files in bucket + getFiles: async ( + bucketId: string, + filters?: { + path?: string; + search?: string; + mimeType?: string; + page?: number; + limit?: number; + } + ): Promise<{ data: StorageFile[]; total: number }> => { + const params = new URLSearchParams(); + if (filters?.path) params.append('path', filters.path); + if (filters?.search) params.append('search', filters.search); + if (filters?.mimeType) params.append('mimeType', filters.mimeType); + if (filters?.page) params.append('page', String(filters.page)); + if (filters?.limit) params.append('limit', String(filters.limit)); + + const response = await api.get<{ data: StorageFile[]; total: number }>( + `${BASE_URL}/buckets/${bucketId}/files?${params.toString()}` + ); + return response.data; + }, + + // Get file by ID + getFile: async (bucketId: string, fileId: string): Promise => { + const response = await api.get( + `${BASE_URL}/buckets/${bucketId}/files/${fileId}` + ); + return response.data; + }, + + // Upload file + uploadFile: async (bucketId: string, data: UploadFileDto): Promise => { + const formData = new FormData(); + formData.append('file', data.file); + if (data.path) formData.append('path', data.path); + if (data.isPublic !== undefined) formData.append('isPublic', String(data.isPublic)); + if (data.metadata) formData.append('metadata', JSON.stringify(data.metadata)); + + const response = await api.post( + `${BASE_URL}/buckets/${bucketId}/files`, + formData, + { + headers: { + 'Content-Type': 'multipart/form-data', + }, + } + ); + return response.data; + }, + + // Upload multiple files + uploadFiles: async ( + bucketId: string, + files: File[], + path?: string + ): Promise => { + const formData = new FormData(); + files.forEach(file => formData.append('files', file)); + if (path) formData.append('path', path); + + const response = await api.post( + `${BASE_URL}/buckets/${bucketId}/files/batch`, + formData, + { + headers: { + 'Content-Type': 'multipart/form-data', + }, + } + ); + return response.data; + }, + + // Download file + downloadFile: async (bucketId: string, fileId: string): Promise => { + const response = await api.get(`${BASE_URL}/buckets/${bucketId}/files/${fileId}/download`, { + responseType: 'blob', + }); + return response.data; + }, + + // Delete file + deleteFile: async (bucketId: string, fileId: string): Promise => { + await api.delete(`${BASE_URL}/buckets/${bucketId}/files/${fileId}`); + }, + + // Delete multiple files + deleteFiles: async (bucketId: string, fileIds: string[]): Promise => { + await api.post(`${BASE_URL}/buckets/${bucketId}/files/delete-batch`, { fileIds }); + }, + + // Get folders in bucket + getFolders: async ( + bucketId: string, + path?: string + ): Promise => { + const params = new URLSearchParams(); + if (path) params.append('path', path); + + const response = await api.get( + `${BASE_URL}/buckets/${bucketId}/folders?${params.toString()}` + ); + return response.data; + }, + + // Create folder + createFolder: async (bucketId: string, path: string): Promise => { + const response = await api.post( + `${BASE_URL}/buckets/${bucketId}/folders`, + { path } + ); + return response.data; + }, + + // Delete folder + deleteFolder: async (bucketId: string, path: string, recursive?: boolean): Promise => { + const params = new URLSearchParams(); + params.append('path', path); + if (recursive) params.append('recursive', 'true'); + await api.delete(`${BASE_URL}/buckets/${bucketId}/folders?${params.toString()}`); + }, + + // Get presigned URL for upload + getPresignedUploadUrl: async ( + bucketId: string, + fileName: string, + mimeType: string, + expiresIn?: number + ): Promise => { + const response = await api.post( + `${BASE_URL}/buckets/${bucketId}/presigned-url/upload`, + { fileName, mimeType, expiresIn } + ); + return response.data; + }, + + // Get presigned URL for download + getPresignedUrl: async ( + bucketId: string, + fileId: string, + expiresIn?: number + ): Promise => { + const params = new URLSearchParams(); + if (expiresIn) params.append('expiresIn', String(expiresIn)); + + const response = await api.get( + `${BASE_URL}/buckets/${bucketId}/files/${fileId}/presigned-url?${params.toString()}` + ); + return response.data; + }, + + // Get storage usage + getUsage: async (): Promise => { + const response = await api.get(`${BASE_URL}/usage`); + return response.data; + }, + + // Copy file + copyFile: async ( + bucketId: string, + fileId: string, + destinationPath: string, + destinationBucketId?: string + ): Promise => { + const response = await api.post( + `${BASE_URL}/buckets/${bucketId}/files/${fileId}/copy`, + { destinationPath, destinationBucketId } + ); + return response.data; + }, + + // Move file + moveFile: async ( + bucketId: string, + fileId: string, + destinationPath: string, + destinationBucketId?: string + ): Promise => { + const response = await api.post( + `${BASE_URL}/buckets/${bucketId}/files/${fileId}/move`, + { destinationPath, destinationBucketId } + ); + return response.data; + }, + + // Update file metadata + updateFileMetadata: async ( + bucketId: string, + fileId: string, + metadata: Record + ): Promise => { + const response = await api.patch( + `${BASE_URL}/buckets/${bucketId}/files/${fileId}/metadata`, + { metadata } + ); + return response.data; + }, + + // Get file versions + getFileVersions: async ( + bucketId: string, + fileId: string + ): Promise => { + const response = await api.get( + `${BASE_URL}/buckets/${bucketId}/files/${fileId}/versions` + ); + return response.data; + }, +}; diff --git a/src/features/webhooks/api/index.ts b/src/features/webhooks/api/index.ts new file mode 100644 index 0000000..74969b6 --- /dev/null +++ b/src/features/webhooks/api/index.ts @@ -0,0 +1 @@ +export * from './webhooks.api'; diff --git a/src/features/webhooks/api/webhooks.api.ts b/src/features/webhooks/api/webhooks.api.ts new file mode 100644 index 0000000..422e3d7 --- /dev/null +++ b/src/features/webhooks/api/webhooks.api.ts @@ -0,0 +1,224 @@ +import { api } from '@services/api/axios-instance'; + +const BASE_URL = '/api/v1/webhooks'; + +export interface Webhook { + id: string; + name: string; + url: string; + events: string[]; + secret?: string; + isActive: boolean; + retryCount: number; + retryDelay: number; + headers?: Record; + createdAt: string; + updatedAt: string; +} + +export interface WebhookDelivery { + id: string; + webhookId: string; + event: string; + payload: any; + status: 'pending' | 'success' | 'failed'; + statusCode?: number; + response?: string; + attempts: number; + lastAttemptAt?: string; + nextRetryAt?: string; + createdAt: string; +} + +export interface WebhookLog { + id: string; + webhookId: string; + deliveryId: string; + event: string; + request: { + url: string; + method: string; + headers: Record; + body: any; + }; + response?: { + statusCode: number; + headers: Record; + body: string; + }; + duration: number; + error?: string; + createdAt: string; +} + +export interface WebhookTestResult { + success: boolean; + statusCode?: number; + response?: string; + duration: number; + error?: string; +} + +export interface CreateWebhookDto { + name: string; + url: string; + events: string[]; + secret?: string; + retryCount?: number; + retryDelay?: number; + headers?: Record; +} + +export interface UpdateWebhookDto { + name?: string; + url?: string; + events?: string[]; + secret?: string; + retryCount?: number; + retryDelay?: number; + headers?: Record; +} + +export const webhooksApi = { + // Get all webhooks + getAll: async (filters?: { + search?: string; + isActive?: boolean; + event?: string; + page?: number; + limit?: number; + }): Promise<{ data: Webhook[]; total: number }> => { + const params = new URLSearchParams(); + if (filters?.search) params.append('search', filters.search); + if (filters?.isActive !== undefined) params.append('isActive', String(filters.isActive)); + if (filters?.event) params.append('event', filters.event); + if (filters?.page) params.append('page', String(filters.page)); + if (filters?.limit) params.append('limit', String(filters.limit)); + + const response = await api.get<{ data: Webhook[]; total: number }>( + `${BASE_URL}?${params.toString()}` + ); + return response.data; + }, + + // Get webhook by ID + getById: async (id: string): Promise => { + const response = await api.get(`${BASE_URL}/${id}`); + return response.data; + }, + + // Create webhook + create: async (data: CreateWebhookDto): Promise => { + const response = await api.post(BASE_URL, data); + return response.data; + }, + + // Update webhook + update: async (id: string, data: UpdateWebhookDto): Promise => { + const response = await api.patch(`${BASE_URL}/${id}`, data); + return response.data; + }, + + // Delete webhook + delete: async (id: string): Promise => { + await api.delete(`${BASE_URL}/${id}`); + }, + + // Test webhook + test: async (id: string, payload?: any): Promise => { + const response = await api.post(`${BASE_URL}/${id}/test`, { payload }); + return response.data; + }, + + // Enable webhook + enable: async (id: string): Promise => { + const response = await api.post(`${BASE_URL}/${id}/enable`); + return response.data; + }, + + // Disable webhook + disable: async (id: string): Promise => { + const response = await api.post(`${BASE_URL}/${id}/disable`); + return response.data; + }, + + // Get webhook deliveries + getDeliveries: async ( + webhookId: string, + filters?: { + status?: string; + event?: string; + startDate?: string; + endDate?: string; + page?: number; + limit?: number; + } + ): Promise<{ data: WebhookDelivery[]; total: number }> => { + const params = new URLSearchParams(); + if (filters?.status) params.append('status', filters.status); + if (filters?.event) params.append('event', filters.event); + if (filters?.startDate) params.append('startDate', filters.startDate); + if (filters?.endDate) params.append('endDate', filters.endDate); + if (filters?.page) params.append('page', String(filters.page)); + if (filters?.limit) params.append('limit', String(filters.limit)); + + const response = await api.get<{ data: WebhookDelivery[]; total: number }>( + `${BASE_URL}/${webhookId}/deliveries?${params.toString()}` + ); + return response.data; + }, + + // Get delivery by ID + getDelivery: async (webhookId: string, deliveryId: string): Promise => { + const response = await api.get( + `${BASE_URL}/${webhookId}/deliveries/${deliveryId}` + ); + return response.data; + }, + + // Retry delivery + retryDelivery: async (webhookId: string, deliveryId: string): Promise => { + const response = await api.post( + `${BASE_URL}/${webhookId}/deliveries/${deliveryId}/retry` + ); + return response.data; + }, + + // Get webhook logs + getLogs: async ( + webhookId: string, + filters?: { + deliveryId?: string; + event?: string; + startDate?: string; + endDate?: string; + page?: number; + limit?: number; + } + ): Promise<{ data: WebhookLog[]; total: number }> => { + const params = new URLSearchParams(); + if (filters?.deliveryId) params.append('deliveryId', filters.deliveryId); + if (filters?.event) params.append('event', filters.event); + if (filters?.startDate) params.append('startDate', filters.startDate); + if (filters?.endDate) params.append('endDate', filters.endDate); + if (filters?.page) params.append('page', String(filters.page)); + if (filters?.limit) params.append('limit', String(filters.limit)); + + const response = await api.get<{ data: WebhookLog[]; total: number }>( + `${BASE_URL}/${webhookId}/logs?${params.toString()}` + ); + return response.data; + }, + + // Get available events + getAvailableEvents: async (): Promise => { + const response = await api.get(`${BASE_URL}/events`); + return response.data; + }, + + // Regenerate secret + regenerateSecret: async (id: string): Promise<{ secret: string }> => { + const response = await api.post<{ secret: string }>(`${BASE_URL}/${id}/regenerate-secret`); + return response.data; + }, +}; diff --git a/src/features/whatsapp/api/index.ts b/src/features/whatsapp/api/index.ts new file mode 100644 index 0000000..2d67523 --- /dev/null +++ b/src/features/whatsapp/api/index.ts @@ -0,0 +1 @@ +export * from './whatsapp.api'; diff --git a/src/features/whatsapp/api/whatsapp.api.ts b/src/features/whatsapp/api/whatsapp.api.ts new file mode 100644 index 0000000..fdc8a19 --- /dev/null +++ b/src/features/whatsapp/api/whatsapp.api.ts @@ -0,0 +1,294 @@ +import { api } from '@services/api/axios-instance'; + +const BASE_URL = '/api/v1/whatsapp'; + +export interface WhatsappConversation { + id: string; + contactId: string; + contactName: string; + contactPhone: string; + lastMessage: string; + lastMessageAt: string; + unreadCount: number; + status: 'active' | 'archived' | 'blocked'; + createdAt: string; +} + +export interface WhatsappMessage { + id: string; + conversationId: string; + direction: 'inbound' | 'outbound'; + type: 'text' | 'image' | 'video' | 'audio' | 'document' | 'template'; + content: string; + mediaUrl?: string; + status: 'sent' | 'delivered' | 'read' | 'failed'; + createdAt: string; +} + +export interface WhatsappTemplate { + id: string; + name: string; + language: string; + category: string; + status: 'approved' | 'pending' | 'rejected'; + components: any[]; +} + +export interface WhatsappContact { + id: string; + phone: string; + name: string; + email?: string; + tags: string[]; + optedIn: boolean; + createdAt: string; +} + +export interface WhatsappBroadcast { + id: string; + name: string; + templateId: string; + status: 'draft' | 'scheduled' | 'sending' | 'completed' | 'failed'; + recipientCount: number; + sentCount: number; + deliveredCount: number; + readCount: number; + scheduledAt?: string; + completedAt?: string; + createdAt: string; +} + +export interface WhatsappWebhook { + id: string; + event: string; + url: string; + isActive: boolean; + createdAt: string; +} + +export interface WhatsappStats { + totalConversations: number; + activeConversations: number; + messagesSent: number; + messagesReceived: number; + templatesUsed: number; + broadcastsSent: number; +} + +export interface SendMessageDto { + phone: string; + type: 'text' | 'image' | 'video' | 'audio' | 'document'; + content: string; + mediaUrl?: string; +} + +export interface SendTemplateDto { + phone: string; + templateName: string; + language: string; + parameters?: Record; +} + +export interface CreateBroadcastDto { + name: string; + templateId: string; + recipientFilter?: { + tags?: string[]; + optedInOnly?: boolean; + }; + scheduledAt?: string; + parameters?: Record; +} + +export const whatsappApi = { + // Get all conversations + getConversations: async (filters?: { + search?: string; + status?: string; + page?: number; + limit?: number; + }): Promise<{ data: WhatsappConversation[]; total: number }> => { + const params = new URLSearchParams(); + if (filters?.search) params.append('search', filters.search); + if (filters?.status) params.append('status', filters.status); + if (filters?.page) params.append('page', String(filters.page)); + if (filters?.limit) params.append('limit', String(filters.limit)); + + const response = await api.get<{ data: WhatsappConversation[]; total: number }>( + `${BASE_URL}/conversations?${params.toString()}` + ); + return response.data; + }, + + // Get conversation by ID + getConversation: async (id: string): Promise => { + const response = await api.get(`${BASE_URL}/conversations/${id}`); + return response.data; + }, + + // Get messages from conversation + getMessages: async ( + conversationId: string, + filters?: { + page?: number; + limit?: number; + } + ): Promise<{ data: WhatsappMessage[]; total: number }> => { + const params = new URLSearchParams(); + if (filters?.page) params.append('page', String(filters.page)); + if (filters?.limit) params.append('limit', String(filters.limit)); + + const response = await api.get<{ data: WhatsappMessage[]; total: number }>( + `${BASE_URL}/conversations/${conversationId}/messages?${params.toString()}` + ); + return response.data; + }, + + // Send message + sendMessage: async (data: SendMessageDto): Promise => { + const response = await api.post(`${BASE_URL}/messages`, data); + return response.data; + }, + + // Send template message + sendTemplate: async (data: SendTemplateDto): Promise => { + const response = await api.post(`${BASE_URL}/messages/template`, data); + return response.data; + }, + + // Get all broadcasts + getBroadcasts: async (filters?: { + status?: string; + page?: number; + limit?: number; + }): Promise<{ data: WhatsappBroadcast[]; total: number }> => { + const params = new URLSearchParams(); + if (filters?.status) params.append('status', filters.status); + if (filters?.page) params.append('page', String(filters.page)); + if (filters?.limit) params.append('limit', String(filters.limit)); + + const response = await api.get<{ data: WhatsappBroadcast[]; total: number }>( + `${BASE_URL}/broadcasts?${params.toString()}` + ); + return response.data; + }, + + // Get broadcast by ID + getBroadcast: async (id: string): Promise => { + const response = await api.get(`${BASE_URL}/broadcasts/${id}`); + return response.data; + }, + + // Create broadcast + createBroadcast: async (data: CreateBroadcastDto): Promise => { + const response = await api.post(`${BASE_URL}/broadcasts`, data); + return response.data; + }, + + // Cancel broadcast + cancelBroadcast: async (id: string): Promise => { + const response = await api.post(`${BASE_URL}/broadcasts/${id}/cancel`); + return response.data; + }, + + // Get all templates + getTemplates: async (filters?: { + status?: string; + category?: string; + page?: number; + limit?: number; + }): Promise<{ data: WhatsappTemplate[]; total: number }> => { + const params = new URLSearchParams(); + if (filters?.status) params.append('status', filters.status); + if (filters?.category) params.append('category', filters.category); + if (filters?.page) params.append('page', String(filters.page)); + if (filters?.limit) params.append('limit', String(filters.limit)); + + const response = await api.get<{ data: WhatsappTemplate[]; total: number }>( + `${BASE_URL}/templates?${params.toString()}` + ); + return response.data; + }, + + // Get template by ID + getTemplate: async (id: string): Promise => { + const response = await api.get(`${BASE_URL}/templates/${id}`); + return response.data; + }, + + // Get all contacts + getContacts: async (filters?: { + search?: string; + tags?: string[]; + optedIn?: boolean; + page?: number; + limit?: number; + }): Promise<{ data: WhatsappContact[]; total: number }> => { + const params = new URLSearchParams(); + if (filters?.search) params.append('search', filters.search); + if (filters?.tags) filters.tags.forEach(tag => params.append('tags', tag)); + if (filters?.optedIn !== undefined) params.append('optedIn', String(filters.optedIn)); + if (filters?.page) params.append('page', String(filters.page)); + if (filters?.limit) params.append('limit', String(filters.limit)); + + const response = await api.get<{ data: WhatsappContact[]; total: number }>( + `${BASE_URL}/contacts?${params.toString()}` + ); + return response.data; + }, + + // Get contact by ID + getContact: async (id: string): Promise => { + const response = await api.get(`${BASE_URL}/contacts/${id}`); + return response.data; + }, + + // Create or update contact + upsertContact: async (data: Partial): Promise => { + const response = await api.post(`${BASE_URL}/contacts`, data); + return response.data; + }, + + // Get webhooks + getWebhooks: async (): Promise => { + const response = await api.get(`${BASE_URL}/webhooks`); + return response.data; + }, + + // Create webhook + createWebhook: async (data: { event: string; url: string }): Promise => { + const response = await api.post(`${BASE_URL}/webhooks`, data); + return response.data; + }, + + // Delete webhook + deleteWebhook: async (id: string): Promise => { + await api.delete(`${BASE_URL}/webhooks/${id}`); + }, + + // Get stats + getStats: async (filters?: { + startDate?: string; + endDate?: string; + }): Promise => { + const params = new URLSearchParams(); + if (filters?.startDate) params.append('startDate', filters.startDate); + if (filters?.endDate) params.append('endDate', filters.endDate); + + const response = await api.get(`${BASE_URL}/stats?${params.toString()}`); + return response.data; + }, + + // Mark conversation as read + markAsRead: async (conversationId: string): Promise => { + await api.post(`${BASE_URL}/conversations/${conversationId}/read`); + }, + + // Archive conversation + archiveConversation: async (conversationId: string): Promise => { + const response = await api.post( + `${BASE_URL}/conversations/${conversationId}/archive` + ); + return response.data; + }, +};