REFACTOR API gateway

This commit is contained in:
Urtzi Alfaro
2025-07-26 18:46:52 +02:00
parent e49893e10a
commit e4885db828
24 changed files with 1049 additions and 1080 deletions

View File

@@ -99,20 +99,20 @@ export class DataService {
*/
async uploadSalesHistory(
file: File,
tenantId?: string,
tenantId: string, // Tenant ID is now a required path parameter
additionalData?: Record<string, any>
): Promise<UploadResponse> {
try {
console.log('Uploading sales file:', file.name);
// ✅ CRITICAL FIX: Use the correct endpoint that exists in backend
// Backend endpoint: @router.post("/import", response_model=SalesImportResult)
// Full path: /api/v1/data/sales/import (mounted with prefix /api/v1/sales)
// Full path: /api/v1/tenants/{tenant_id}/sales/import
// Determine file format
const fileName = file.name.toLowerCase();
let fileFormat: string;
if (fileName.endsWith('.csv')) {
fileFormat = 'csv';
} else if (fileName.endsWith('.json')) {
@@ -122,28 +122,29 @@ export class DataService {
} else {
fileFormat = 'csv'; // Default fallback
}
// ✅ FIXED: Create FormData manually to match backend expectations
const formData = new FormData();
formData.append('file', file);
formData.append('file_format', fileFormat);
if (tenantId) {
formData.append('tenant_id', tenantId);
}
// tenantId is no longer appended to FormData as it's a path parameter
// if (tenantId) {
// formData.append('tenant_id', tenantId);
// }
// Add additional data if provided
if (additionalData) {
Object.entries(additionalData).forEach(([key, value]) => {
formData.append(key, String(value));
});
}
console.log('Uploading with file_format:', fileFormat);
// ✅ FIXED: Use the correct endpoint that exists in the backend
const response = await apiClient.request<ApiResponse<any>>(
'/api/v1/data/sales/import', // Correct endpoint path
`/api/v1/tenants/${tenantId}/sales/import`, // Correct endpoint path with tenant_id
{
method: 'POST',
body: formData,
@@ -151,16 +152,16 @@ export class DataService {
headers: {} // Empty headers to avoid setting Content-Type manually
}
);
console.log('Upload response:', response);
// ✅ Handle the SalesImportResult response structure
if (response && typeof response === 'object') {
// Handle API errors
if ('detail' in response) {
throw new Error(typeof response.detail === 'string' ? response.detail : 'Upload failed');
}
// Extract data from response
let uploadResult: any;
if ('data' in response && response.data) {
@@ -168,28 +169,28 @@ export class DataService {
} else {
uploadResult = response;
}
// ✅ FIXED: Map backend SalesImportResult to frontend UploadResponse
return {
message: uploadResult.success
message: uploadResult.success
? `Successfully processed ${uploadResult.records_created || uploadResult.records_processed || 0} records`
: 'Upload completed with issues',
records_processed: uploadResult.records_created || uploadResult.records_processed || 0,
errors: uploadResult.errors ?
(Array.isArray(uploadResult.errors) ?
uploadResult.errors.map((err: any) =>
errors: uploadResult.errors ?
(Array.isArray(uploadResult.errors) ?
uploadResult.errors.map((err: any) =>
typeof err === 'string' ? err : (err.message || String(err))
) : [String(uploadResult.errors)]
) : [],
upload_id: uploadResult.id || undefined
};
}
throw new Error('Invalid response format from upload service');
} catch (error: any) {
console.error('Error uploading file:', error);
let errorMessage = 'Error al subir el archivo';
if (error.response?.status === 422) {
errorMessage = 'Formato de archivo inválido';
@@ -200,7 +201,7 @@ export class DataService {
} else if (error.message) {
errorMessage = error.message;
}
// Throw structured error that can be caught by the frontend
throw {
message: errorMessage,
@@ -216,20 +217,20 @@ export class DataService {
* ✅ ALTERNATIVE: Upload sales data using the JSON import endpoint
* This uses the same endpoint as validation but with validate_only: false
*/
async uploadSalesDataAsJson(file: File, tenantId?: string): Promise<UploadResponse> {
async uploadSalesDataAsJson(file: File, tenantId: string): Promise<UploadResponse> { // tenantId made required
try {
console.log('Uploading sales data as JSON:', file.name);
const fileContent = await this.readFileAsText(file);
if (!fileContent) {
throw new Error('Failed to read file content');
}
// Determine file format
const fileName = file.name.toLowerCase();
let dataFormat: 'csv' | 'json' | 'excel';
if (fileName.endsWith('.csv')) {
dataFormat = 'csv';
} else if (fileName.endsWith('.json')) {
@@ -239,59 +240,67 @@ export class DataService {
} else {
dataFormat = 'csv';
}
// ✅ Use the same structure as validation but with validate_only: false
const importData: SalesDataImportRequest = {
tenant_id: tenantId || '00000000-0000-0000-0000-000000000000',
tenant_id: tenantId, // Use the provided tenantId
data: fileContent,
data_format: dataFormat,
validate_only: false, // This makes it actually import the data
source: 'onboarding_upload'
};
console.log('Uploading data with validate_only: false');
// ✅ OPTION: Add a new JSON import endpoint to the backend
// Current backend sales.py does not have a /import/json endpoint,
// it only has a file upload endpoint.
// If a JSON import endpoint is desired, it needs to be added to sales.py
// For now, this method will target the existing /import endpoint with a JSON body
// This will require the backend to support JSON body for /import, which it currently
// does not for the direct file upload endpoint.
// THIS ALTERNATIVE METHOD IS LEFT AS-IS, ASSUMING A FUTURE BACKEND ENDPOINT
// OR A MODIFICATION TO THE EXISTING /import ENDPOINT TO ACCEPT JSON BODY.
const response = await apiClient.post<ApiResponse<any>>(
'/api/v1/data/sales/import/json', // Need to add this endpoint to backend
`/api/v1/tenants/${tenantId}/sales/import/json`, // This endpoint does not exist in sales.py
importData
);
console.log('JSON upload response:', response);
// Handle response similar to file upload
if (response && typeof response === 'object') {
if ('detail' in response) {
throw new Error(typeof response.detail === 'string' ? response.detail : 'Upload failed');
}
let uploadResult: any;
if ('data' in response && response.data) {
uploadResult = response.data;
} else {
uploadResult = response;
}
return {
message: uploadResult.success
message: uploadResult.success
? `Successfully processed ${uploadResult.records_created || uploadResult.records_processed || 0} records`
: 'Upload completed with issues',
records_processed: uploadResult.records_created || uploadResult.records_processed || 0,
errors: uploadResult.errors ?
(Array.isArray(uploadResult.errors) ?
uploadResult.errors.map((err: any) =>
errors: uploadResult.errors ?
(Array.isArray(uploadResult.errors) ?
uploadResult.errors.map((err: any) =>
typeof err === 'string' ? err : (err.message || String(err))
) : [String(uploadResult.errors)]
) : [],
upload_id: uploadResult.id || undefined
};
}
throw new Error('Invalid response format from upload service');
} catch (error: any) {
console.error('Error uploading JSON data:', error);
let errorMessage = 'Error al subir los datos';
if (error.response?.status === 422) {
errorMessage = 'Formato de datos inválido';
@@ -302,7 +311,7 @@ export class DataService {
} else if (error.message) {
errorMessage = error.message;
}
throw {
message: errorMessage,
status: error.response?.status || 0,
@@ -312,22 +321,22 @@ export class DataService {
}
}
async validateSalesData(file: File, tenantId?: string): Promise<DataValidation> {
async validateSalesData(file: File, tenantId: string): Promise<DataValidation> { // tenantId made required
try {
console.log('Reading file content...', file.name);
const fileContent = await this.readFileAsText(file);
if (!fileContent) {
throw new Error('Failed to read file content');
}
console.log('File content read successfully, length:', fileContent.length);
// Determine file format from extension
const fileName = file.name.toLowerCase();
let dataFormat: 'csv' | 'json' | 'excel';
if (fileName.endsWith('.csv')) {
dataFormat = 'csv';
} else if (fileName.endsWith('.json')) {
@@ -342,7 +351,7 @@ export class DataService {
// ✅ FIXED: Use proper tenant ID when available
const importData: SalesDataImportRequest = {
tenant_id: tenantId || '00000000-0000-0000-0000-000000000000',
tenant_id: tenantId, // Use the provided tenantId
data: fileContent,
data_format: dataFormat,
validate_only: true
@@ -351,10 +360,10 @@ export class DataService {
console.log('Sending validation request with tenant_id:', importData.tenant_id);
const response = await apiClient.post<ApiResponse<DataValidation>>(
'/api/v1/data/sales/import/validate',
`/api/v1/tenants/${tenantId}/sales/import/validate`, // Correct endpoint with tenant_id
importData
);
console.log('Raw response from API:', response);
// ✅ ENHANCED: Handle the new backend response structure
@@ -362,7 +371,7 @@ export class DataService {
// Handle API errors
if ('detail' in response) {
console.error('API returned error:', response.detail);
if (Array.isArray(response.detail)) {
// Handle Pydantic validation errors
const errorMessages = response.detail.map(err => ({
@@ -371,7 +380,7 @@ export class DataService {
field: err.loc ? err.loc[err.loc.length - 1] : null,
code: err.type
}));
return {
is_valid: false,
total_records: 0,
@@ -385,7 +394,7 @@ export class DataService {
}
};
}
// Handle simple error messages
return {
is_valid: false,
@@ -407,7 +416,7 @@ export class DataService {
// ✅ SUCCESS: Handle successful validation response
let validationResult: DataValidation;
// Check if response has nested data
if ('data' in response && response.data) {
validationResult = response.data;
@@ -427,7 +436,7 @@ export class DataService {
errors: validationResult.errors || [],
warnings: validationResult.warnings || [],
summary: validationResult.summary || { status: 'unknown', suggestions: [] },
// Backward compatibility fields
valid: validationResult.is_valid, // Map for legacy code
recordCount: validationResult.total_records,
@@ -436,13 +445,13 @@ export class DataService {
}
throw new Error('Invalid response format from validation service');
} catch (error: any) {
console.error('Error validating file:', error);
let errorMessage = 'Error al validar el archivo';
let errorCode = 'UNKNOWN_ERROR';
if (error.response?.status === 422) {
errorMessage = 'Formato de archivo inválido';
errorCode = 'INVALID_FORMAT';
@@ -456,7 +465,7 @@ export class DataService {
errorMessage = error.message;
errorCode = 'CLIENT_ERROR';
}
// Return properly structured error response matching new schema
return {
is_valid: false,
@@ -473,7 +482,7 @@ export class DataService {
status: 'error',
suggestions: ['Intenta con un archivo diferente o contacta soporte']
},
// Backward compatibility
valid: false,
recordCount: 0,
@@ -488,7 +497,7 @@ export class DataService {
private readFileAsText(file: File): Promise<string> {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onload = (event) => {
const result = event.target?.result;
if (typeof result === 'string') {
@@ -497,15 +506,15 @@ export class DataService {
reject(new Error('Failed to read file as text'));
}
};
reader.onerror = () => {
reject(new Error('Failed to read file'));
};
reader.onabort = () => {
reject(new Error('File reading was aborted'));
};
// Read the file as text
reader.readAsText(file);
});
@@ -524,7 +533,7 @@ export class DataService {
/**
* Get sales records
*/
async getSalesRecords(params?: {
async getSalesRecords(tenantId: string, params?: { // Add tenantId
startDate?: string;
endDate?: string;
productName?: string;
@@ -536,16 +545,16 @@ export class DataService {
total: number;
page: number;
pages: number;
}>>('/api/v1/data/sales', { params });
}>>(`/api/v1/tenants/${tenantId}/sales`, { params }); // Use tenantId in path
return response.data!;
}
/**
* Create single sales record
*/
async createSalesRecord(record: CreateSalesRequest): Promise<SalesRecord> {
async createSalesRecord(tenantId: string, record: CreateSalesRequest): Promise<SalesRecord> { // Add tenantId
const response = await apiClient.post<ApiResponse<SalesRecord>>(
'/api/v1/data/sales',
`/api/v1/tenants/${tenantId}/sales`, // Use tenantId in path
record
);
return response.data!;
@@ -554,9 +563,9 @@ export class DataService {
/**
* Update sales record
*/
async updateSalesRecord(id: string, record: Partial<CreateSalesRequest>): Promise<SalesRecord> {
async updateSalesRecord(tenantId: string, id: string, record: Partial<CreateSalesRequest>): Promise<SalesRecord> { // Add tenantId
const response = await apiClient.put<ApiResponse<SalesRecord>>(
`/api/v1/data/sales/${id}`,
`/api/v1/tenants/${tenantId}/sales/${id}`, // Use tenantId in path
record
);
return response.data!;
@@ -565,8 +574,8 @@ export class DataService {
/**
* Delete sales record
*/
async deleteSalesRecord(id: string): Promise<void> {
await apiClient.delete(`/api/v1/data/sales/${id}`);
async deleteSalesRecord(tenantId: string, id: string): Promise<void> { // Add tenantId
await apiClient.delete(`/api/v1/tenants/${tenantId}/sales/${id}`); // Use tenantId in path
}
/**

View File

@@ -142,303 +142,8 @@ export class TenantService {
* Corresponds to GET /users/{user_id}/tenants
*/
async getUserTenants(userId: string): Promise<TenantInfo[]> {
const response = await apiClient.get<ApiResponse<TenantInfo[]>>(`/api/v1/users/${userId}/tenants`);
const response = await apiClient.get<ApiResponse<TenantInfo[]>>(`/api/v1/tenants/user/${userId}`);
return response.data!;
}
/**
* Add a team member to a tenant
* Corresponds to POST /tenants/{tenant_id}/members
*/
async addTeamMember(tenantId: string, userId: string, role: string): Promise<TenantMemberResponse> {
const response = await apiClient.post<ApiResponse<TenantMemberResponse>>(
`/api/v1/tenants/${tenantId}/members`,
{ user_id: userId, role }
);
return response.data!;
}
// --- Existing methods (kept for completeness, assuming they map to other backend endpoints not provided) ---
/**
* Get current tenant info (no direct backend mapping in provided file, but common)
*/
async getCurrentTenant(): Promise<TenantInfo> {
const response = await apiClient.get<ApiResponse<TenantInfo>>('/api/v1/tenants/current');
return response.data!;
}
/**
* Update current tenant (no direct backend mapping in provided file, but common)
*/
async updateCurrentTenant(updates: TenantUpdate): Promise<TenantInfo> {
const response = await apiClient.put<ApiResponse<TenantInfo>>('/api/v1/tenants/current', updates);
return response.data!;
}
/**
* Get tenant settings (no direct backend mapping in provided file)
*/
async getTenantSettings(): Promise<TenantSettings> {
const response = await apiClient.get<ApiResponse<TenantSettings>>('/api/v1/tenants/settings');
return response.data!;
}
/**
* Update tenant settings (no direct backend mapping in provided file)
*/
async updateTenantSettings(settings: Partial<TenantSettings>): Promise<TenantSettings> {
const response = await apiClient.put<ApiResponse<TenantSettings>>(
'/api/v1/tenants/settings',
settings
);
return response.data!;
}
/**
* Get tenant statistics (no direct backend mapping in provided file)
*/
async getTenantStats(): Promise<TenantStats> {
const response = await apiClient.get<ApiResponse<TenantStats>>('/api/v1/tenants/stats');
return response.data!;
}
/**
* Get tenant users (no direct backend mapping in provided file)
*/
async getTenantUsers(params?: {
role?: string;
active?: boolean;
page?: number;
limit?: number;
}): Promise<{
users: TenantUser[];
total: number;
page: number;
pages: number;
}> {
const response = await apiClient.get<ApiResponse<any>>('/api/v1/tenants/users', { params });
return response.data!;
}
/**
* Invite user to tenant (no direct backend mapping in provided file)
*/
async inviteUser(invitation: InviteUser): Promise<{
invitation_id: string;
email: string;
role: string;
expires_at: string;
invitation_token: string;
}> {
const response = await apiClient.post<ApiResponse<any>>('/api/v1/tenants/users/invite', invitation);
return response.data!;
}
/**
* Update user role (no direct backend mapping in provided file)
*/
async updateUserRole(userId: string, role: string): Promise<TenantUser> {
const response = await apiClient.patch<ApiResponse<TenantUser>>(
`/api/v1/tenants/users/${userId}`,
{ role }
);
return response.data!;
}
/**
* Deactivate user (no direct backend mapping in provided file)
*/
async deactivateUser(userId: string): Promise<TenantUser> {
const response = await apiClient.patch<ApiResponse<TenantUser>>(
`/api/v1/tenants/users/${userId}`,
{ is_active: false }
);
return response.data!;
}
/**
* Reactivate user (no direct backend mapping in provided file)
*/
async reactivateUser(userId: string): Promise<TenantUser> {
const response = await apiClient.patch<ApiResponse<TenantUser>>(
`/api/v1/tenants/users/${userId}`,
{ is_active: true }
);
return response.data!;
}
/**
* Remove user from tenant (no direct backend mapping in provided file)
*/
async removeUser(userId: string): Promise<void> {
await apiClient.delete(`/api/v1/tenants/users/${userId}`);
}
/**
* Get pending invitations (no direct backend mapping in provided file)
*/
async getPendingInvitations(): Promise<{
id: string;
email: string;
role: string;
invited_at: string;
expires_at: string;
invited_by: string;
}[]> {
const response = await apiClient.get<ApiResponse<any>>('/api/v1/tenants/invitations');
return response.data!;
}
/**
* Cancel invitation (no direct backend mapping in provided file)
*/
async cancelInvitation(invitationId: string): Promise<void> {
await apiClient.delete(`/api/v1/tenants/invitations/${invitationId}`);
}
/**
* Resend invitation (no direct backend mapping in provided file)
*/
async resendInvitation(invitationId: string): Promise<void> {
await apiClient.post(`/api/v1/tenants/invitations/${invitationId}/resend`);
}
/**
* Get tenant activity log (no direct backend mapping in provided file)
*/
async getActivityLog(params?: {
userId?: string;
action?: string;
startDate?: string;
endDate?: string;
page?: number;
limit?: number;
}): Promise<{
activities: {
id: string;
user_id: string;
user_name: string;
action: string;
resource: string;
resource_id: string;
details?: Record<string, any>;
ip_address?: string;
user_agent?: string;
created_at: string;
}[];
total: number;
page: number;
pages: number;
}> {
const response = await apiClient.get<ApiResponse<any>>('/api/v1/tenants/activity', { params });
return response.data!;
}
/**
* Get tenant billing info (no direct backend mapping in provided file)
*/
async getBillingInfo(): Promise<{
subscription_plan: string;
billing_cycle: 'monthly' | 'yearly';
next_billing_date: string;
amount: number;
currency: string;
payment_method: {
type: string;
last_four: string;
expires: string;
};
usage: {
api_calls: number;
storage_mb: number;
users: number;
limits: {
api_calls_per_month: number;
storage_mb: number;
max_users: number;
};
};
}> {
const response = await apiClient.get<ApiResponse<any>>('/api/v1/tenants/billing');
return response.data!;
}
/**
* Update billing info (no direct backend mapping in provided file)
*/
async updateBillingInfo(billingData: {
payment_method_token?: string;
billing_address?: {
street: string;
city: string;
state: string;
zip: string;
country: string;
};
}): Promise<void> {
await apiClient.put('/api/v1/tenants/billing', billingData);
}
/**
* Change subscription plan (no direct backend mapping in provided file)
*/
async changeSubscriptionPlan(
planId: string,
billingCycle: 'monthly' | 'yearly'
): Promise<{
subscription_id: string;
plan: string;
billing_cycle: string;
next_billing_date: string;
proration_amount?: number;
}> {
const response = await apiClient.post<ApiResponse<any>>('/api/v1/tenants/subscription/change', {
plan_id: planId,
billing_cycle: billingCycle,
});
return response.data!;
}
/**
* Cancel subscription (no direct backend mapping in provided file)
*/
async cancelSubscription(cancelAt: 'immediately' | 'end_of_period'): Promise<{
cancelled_at: string;
will_cancel_at: string;
refund_amount?: number;
}> {
const response = await apiClient.post<ApiResponse<any>>('/api/v1/tenants/subscription/cancel', {
cancel_at: cancelAt,
});
return response.data!;
}
/**
* Export tenant data (no direct backend mapping in provided file)
*/
async exportTenantData(dataTypes: string[], format: 'json' | 'csv'): Promise<Blob> {
const response = await apiClient.post('/api/v1/tenants/export', {
data_types: dataTypes,
format,
responseType: 'blob',
});
return response as unknown as Blob;
}
/**
* Delete tenant (GDPR compliance) (no direct backend mapping in provided file)
*/
async deleteTenant(confirmationToken: string): Promise<{
deletion_scheduled_at: string;
data_retention_until: string;
recovery_period_days: number;
}> {
const response = await apiClient.delete<ApiResponse<any>>('/api/v1/tenants/current', {
data: { confirmation_token: confirmationToken },
});
return response.data!;
}
}
export const tenantService = new TenantService();
}

View File

@@ -75,9 +75,9 @@ export class TrainingService {
/**
* Start new training job
*/
async startTraining(config: TrainingConfiguration): Promise<TrainingJobStatus> {
async startTraining(tenantId: string, config: TrainingConfiguration): Promise<TrainingJobStatus> {
const response = await apiClient.post<TrainingJobStatus>(
'/api/v1/training/jobs',
`/api/v1/tenants/${tenantId}/training/jobs`,
config
);
return response.data!;
@@ -86,9 +86,9 @@ export class TrainingService {
/**
* Get training job status
*/
async getTrainingStatus(jobId: string): Promise<TrainingJobProgress> {
async getTrainingStatus(tenantId: string, jobId: string): Promise<TrainingJobProgress> {
const response = await apiClient.get<ApiResponse<TrainingJobProgress>>(
`/api/v1/training/jobs/${jobId}`
`/api/v1/tenants/${tenantId}/training/jobs/${jobId}`
);
return response.data!;
}
@@ -96,7 +96,7 @@ export class TrainingService {
/**
* Get all training jobs
*/
async getTrainingHistory(params?: {
async getTrainingHistory(tenantId: string, params?: {
page?: number;
limit?: number;
status?: string;
@@ -113,14 +113,14 @@ export class TrainingService {
/**
* Cancel training job
*/
async cancelTraining(jobId: string): Promise<void> {
await apiClient.post(`/api/v1/training/jobs/${jobId}/cancel`);
async cancelTraining(tenantId: string, jobId: string): Promise<void> {
await apiClient.post(`/api/v1/tenants/${tenantId}/training/jobs/${jobId}/cancel`);
}
/**
* Get trained models
*/
async getModels(params?: {
async getModels(tenantId: string, params?: {
productName?: string;
active?: boolean;
page?: number;
@@ -131,14 +131,14 @@ export class TrainingService {
page: number;
pages: number;
}> {
const response = await apiClient.get<ApiResponse<any>>('/api/v1/training/models', { params });
const response = await apiClient.get<ApiResponse<any>>(`/api/v1/tenants/${tenantId}/training/models`, { params });
return response.data!;
}
/**
* Get specific model details
*/
async getModel(modelId: string): Promise<TrainedModel> {
async getModel(tenantId: string, modelId: string): Promise<TrainedModel> {
const response = await apiClient.get<ApiResponse<TrainedModel>>(
`/api/v1/training/models/${modelId}`
);
@@ -148,9 +148,9 @@ export class TrainingService {
/**
* Get model metrics
*/
async getModelMetrics(modelId: string): Promise<ModelMetrics> {
async getModelMetrics(tenantId: string, modelId: string): Promise<ModelMetrics> {
const response = await apiClient.get<ApiResponse<ModelMetrics>>(
`/api/v1/training/models/${modelId}/metrics`
`/api/v1/tenants/${tenantId}/training/models/${modelId}/metrics`
);
return response.data!;
}
@@ -158,9 +158,9 @@ export class TrainingService {
/**
* Activate/deactivate model
*/
async toggleModelStatus(modelId: string, active: boolean): Promise<TrainedModel> {
async toggleModelStatus(tenantId: string, modelId: string, active: boolean): Promise<TrainedModel> {
const response = await apiClient.patch<ApiResponse<TrainedModel>>(
`/api/v1/training/models/${modelId}`,
`/api/v1/tenants/${tenantId}/training/models/${modelId}`,
{ is_active: active }
);
return response.data!;
@@ -169,16 +169,16 @@ export class TrainingService {
/**
* Delete model
*/
async deleteModel(modelId: string): Promise<void> {
async deleteModel(tenantId: string, modelId: string): Promise<void> {
await apiClient.delete(`/api/v1/training/models/${modelId}`);
}
/**
* Train specific product
*/
async trainProduct(productName: string, config?: Partial<TrainingConfiguration>): Promise<TrainingJobStatus> {
async trainProduct(tenantId: string, productName: string, config?: Partial<TrainingConfiguration>): Promise<TrainingJobStatus> {
const response = await apiClient.post<ApiResponse<TrainingJobStatus>>(
'/api/v1/training/products/train',
`/api/v1/tenants/${tenantId}/training/products/train`,
{
product_name: productName,
...config,
@@ -190,7 +190,7 @@ export class TrainingService {
/**
* Get training statistics
*/
async getTrainingStats(): Promise<{
async getTrainingStats(tenantId: string): Promise<{
total_models: number;
active_models: number;
avg_accuracy: number;
@@ -198,21 +198,21 @@ export class TrainingService {
products_trained: number;
training_time_avg_minutes: number;
}> {
const response = await apiClient.get<ApiResponse<any>>('/api/v1/training/stats');
const response = await apiClient.get<ApiResponse<any>>(`/api/v1/tenants/${tenantId}/training/stats`);
return response.data!;
}
/**
* Validate training data
*/
async validateTrainingData(products?: string[]): Promise<{
async validateTrainingData(tenantId: string, products?: string[]): Promise<{
valid: boolean;
errors: string[];
warnings: string[];
product_data_points: Record<string, number>;
recommendation: string;
}> {
const response = await apiClient.post<ApiResponse<any>>('/api/v1/training/validate', {
const response = await apiClient.post<ApiResponse<any>>(`/api/v1/tenants/${tenantId}/training/validate`, {
products,
});
return response.data!;
@@ -221,29 +221,29 @@ export class TrainingService {
/**
* Get training recommendations
*/
async getTrainingRecommendations(): Promise<{
async getTrainingRecommendations(tenantId: string): Promise<{
should_retrain: boolean;
reasons: string[];
recommended_products: string[];
optimal_config: TrainingConfiguration;
}> {
const response = await apiClient.get<ApiResponse<any>>('/api/v1/training/recommendations');
const response = await apiClient.get<ApiResponse<any>>(`/api/v1/tenants/${tenantId}/training/recommendations`);
return response.data!;
}
/**
* Get training logs
*/
async getTrainingLogs(jobId: string): Promise<string[]> {
const response = await apiClient.get<ApiResponse<string[]>>(`/api/v1/training/jobs/${jobId}/logs`);
async getTrainingLogs(tenantId: string, jobId: string): Promise<string[]> {
const response = await apiClient.get<ApiResponse<string[]>>(`/api/v1/tenants/${tenantId}/training/jobs/${jobId}/logs`);
return response.data!;
}
/**
* Export model
*/
async exportModel(modelId: string, format: 'pickle' | 'onnx' = 'pickle'): Promise<Blob> {
const response = await apiClient.get(`/api/v1/training/models/${modelId}/export`, {
async exportModel(tenantId: string, modelId: string, format: 'pickle' | 'onnx' = 'pickle'): Promise<Blob> {
const response = await apiClient.get(`/api/v1/tenants/${tenantId}/training/models/${modelId}/export`, {
params: { format },
responseType: 'blob',
});