2025-10-06 15:27:01 +02:00
|
|
|
// ================================================================
|
|
|
|
|
// frontend/src/api/services/sales.ts
|
|
|
|
|
// ================================================================
|
2025-09-05 17:49:48 +02:00
|
|
|
/**
|
2025-10-06 15:27:01 +02:00
|
|
|
* Sales Service - Complete backend alignment
|
|
|
|
|
*
|
|
|
|
|
* Backend API structure (3-tier architecture):
|
|
|
|
|
* - ATOMIC: sales_records.py
|
|
|
|
|
* - OPERATIONS: sales_operations.py (validation, import, aggregation)
|
|
|
|
|
* - ANALYTICS: analytics.py
|
|
|
|
|
*
|
|
|
|
|
* Last Updated: 2025-10-05
|
|
|
|
|
* Status: ✅ Complete - Zero drift with backend
|
2025-09-05 17:49:48 +02:00
|
|
|
*/
|
2025-10-06 15:27:01 +02:00
|
|
|
|
2025-09-05 17:49:48 +02:00
|
|
|
import { apiClient } from '../client';
|
|
|
|
|
import {
|
2025-10-06 15:27:01 +02:00
|
|
|
// Sales Data
|
2025-09-05 17:49:48 +02:00
|
|
|
SalesDataCreate,
|
|
|
|
|
SalesDataUpdate,
|
|
|
|
|
SalesDataResponse,
|
|
|
|
|
SalesDataQuery,
|
2025-10-06 15:27:01 +02:00
|
|
|
// Import
|
|
|
|
|
ImportValidationResult,
|
|
|
|
|
BulkImportResponse,
|
|
|
|
|
ImportSummary,
|
|
|
|
|
// Analytics
|
2025-09-05 17:49:48 +02:00
|
|
|
SalesAnalytics,
|
2025-10-06 15:27:01 +02:00
|
|
|
ProductSalesAnalytics,
|
|
|
|
|
CategorySalesAnalytics,
|
|
|
|
|
ChannelPerformance,
|
2025-09-05 17:49:48 +02:00
|
|
|
} from '../types/sales';
|
|
|
|
|
|
|
|
|
|
export class SalesService {
|
|
|
|
|
private readonly baseUrl = '/tenants';
|
|
|
|
|
|
2025-10-06 15:27:01 +02:00
|
|
|
// ===================================================================
|
|
|
|
|
// ATOMIC: Sales Records CRUD
|
|
|
|
|
// Backend: services/sales/app/api/sales_records.py
|
|
|
|
|
// ===================================================================
|
|
|
|
|
|
2025-09-05 17:49:48 +02:00
|
|
|
async createSalesRecord(
|
2025-10-06 15:27:01 +02:00
|
|
|
tenantId: string,
|
2025-09-05 17:49:48 +02:00
|
|
|
salesData: SalesDataCreate
|
|
|
|
|
): Promise<SalesDataResponse> {
|
2025-10-06 15:27:01 +02:00
|
|
|
return apiClient.post<SalesDataResponse>(
|
|
|
|
|
`${this.baseUrl}/${tenantId}/sales/sales`,
|
|
|
|
|
salesData
|
|
|
|
|
);
|
2025-09-05 17:49:48 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async getSalesRecords(
|
2025-10-06 15:27:01 +02:00
|
|
|
tenantId: string,
|
2025-09-05 17:49:48 +02:00
|
|
|
query?: SalesDataQuery
|
|
|
|
|
): Promise<SalesDataResponse[]> {
|
|
|
|
|
const queryParams = new URLSearchParams();
|
2025-10-06 15:27:01 +02:00
|
|
|
|
2025-09-05 17:49:48 +02:00
|
|
|
if (query?.start_date) queryParams.append('start_date', query.start_date);
|
|
|
|
|
if (query?.end_date) queryParams.append('end_date', query.end_date);
|
|
|
|
|
if (query?.product_name) queryParams.append('product_name', query.product_name);
|
|
|
|
|
if (query?.product_category) queryParams.append('product_category', query.product_category);
|
|
|
|
|
if (query?.location_id) queryParams.append('location_id', query.location_id);
|
|
|
|
|
if (query?.sales_channel) queryParams.append('sales_channel', query.sales_channel);
|
|
|
|
|
if (query?.source) queryParams.append('source', query.source);
|
2025-10-06 15:27:01 +02:00
|
|
|
if (query?.is_validated !== undefined)
|
|
|
|
|
queryParams.append('is_validated', query.is_validated.toString());
|
2025-09-05 17:49:48 +02:00
|
|
|
if (query?.limit !== undefined) queryParams.append('limit', query.limit.toString());
|
|
|
|
|
if (query?.offset !== undefined) queryParams.append('offset', query.offset.toString());
|
|
|
|
|
if (query?.order_by) queryParams.append('order_by', query.order_by);
|
|
|
|
|
if (query?.order_direction) queryParams.append('order_direction', query.order_direction);
|
|
|
|
|
|
2025-10-06 15:27:01 +02:00
|
|
|
const url = queryParams.toString()
|
|
|
|
|
? `${this.baseUrl}/${tenantId}/sales/sales?${queryParams.toString()}`
|
|
|
|
|
: `${this.baseUrl}/${tenantId}/sales/sales`;
|
2025-09-05 17:49:48 +02:00
|
|
|
|
|
|
|
|
return apiClient.get<SalesDataResponse[]>(url);
|
|
|
|
|
}
|
|
|
|
|
|
2025-10-06 15:27:01 +02:00
|
|
|
async getSalesRecord(tenantId: string, recordId: string): Promise<SalesDataResponse> {
|
|
|
|
|
return apiClient.get<SalesDataResponse>(
|
|
|
|
|
`${this.baseUrl}/${tenantId}/sales/sales/${recordId}`
|
|
|
|
|
);
|
2025-09-05 17:49:48 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async updateSalesRecord(
|
2025-10-06 15:27:01 +02:00
|
|
|
tenantId: string,
|
|
|
|
|
recordId: string,
|
2025-09-05 17:49:48 +02:00
|
|
|
updateData: SalesDataUpdate
|
|
|
|
|
): Promise<SalesDataResponse> {
|
2025-10-06 15:27:01 +02:00
|
|
|
return apiClient.put<SalesDataResponse>(
|
|
|
|
|
`${this.baseUrl}/${tenantId}/sales/sales/${recordId}`,
|
|
|
|
|
updateData
|
|
|
|
|
);
|
2025-09-05 17:49:48 +02:00
|
|
|
}
|
|
|
|
|
|
2025-10-06 15:27:01 +02:00
|
|
|
async deleteSalesRecord(tenantId: string, recordId: string): Promise<{ message: string }> {
|
|
|
|
|
return apiClient.delete<{ message: string }>(
|
|
|
|
|
`${this.baseUrl}/${tenantId}/sales/sales/${recordId}`
|
|
|
|
|
);
|
2025-09-05 17:49:48 +02:00
|
|
|
}
|
|
|
|
|
|
2025-10-06 15:27:01 +02:00
|
|
|
async getProductCategories(tenantId: string): Promise<string[]> {
|
2025-10-07 07:15:07 +02:00
|
|
|
return apiClient.get<string[]>(`${this.baseUrl}/${tenantId}/sales/categories`);
|
2025-10-06 15:27:01 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ===================================================================
|
|
|
|
|
// OPERATIONS: Validation
|
|
|
|
|
// Backend: services/sales/app/api/sales_operations.py
|
|
|
|
|
// ===================================================================
|
|
|
|
|
|
2025-09-05 17:49:48 +02:00
|
|
|
async validateSalesRecord(
|
2025-10-06 15:27:01 +02:00
|
|
|
tenantId: string,
|
|
|
|
|
recordId: string,
|
2025-09-05 17:49:48 +02:00
|
|
|
validationNotes?: string
|
|
|
|
|
): Promise<SalesDataResponse> {
|
|
|
|
|
const queryParams = new URLSearchParams();
|
|
|
|
|
if (validationNotes) queryParams.append('validation_notes', validationNotes);
|
|
|
|
|
|
2025-10-06 15:27:01 +02:00
|
|
|
const url = queryParams.toString()
|
|
|
|
|
? `${this.baseUrl}/${tenantId}/sales/operations/validate-record/${recordId}?${queryParams.toString()}`
|
|
|
|
|
: `${this.baseUrl}/${tenantId}/sales/operations/validate-record/${recordId}`;
|
2025-09-05 17:49:48 +02:00
|
|
|
|
|
|
|
|
return apiClient.post<SalesDataResponse>(url);
|
|
|
|
|
}
|
|
|
|
|
|
2025-10-06 15:27:01 +02:00
|
|
|
// ===================================================================
|
|
|
|
|
// OPERATIONS: Cross-Service Queries
|
|
|
|
|
// Backend: services/sales/app/api/sales_operations.py
|
|
|
|
|
// ===================================================================
|
|
|
|
|
|
|
|
|
|
async getProductSales(
|
2025-09-05 17:49:48 +02:00
|
|
|
tenantId: string,
|
2025-10-06 15:27:01 +02:00
|
|
|
inventoryProductId: string,
|
2025-09-05 17:49:48 +02:00
|
|
|
startDate?: string,
|
|
|
|
|
endDate?: string
|
2025-10-06 15:27:01 +02:00
|
|
|
): Promise<SalesDataResponse[]> {
|
2025-09-05 17:49:48 +02:00
|
|
|
const queryParams = new URLSearchParams();
|
|
|
|
|
if (startDate) queryParams.append('start_date', startDate);
|
|
|
|
|
if (endDate) queryParams.append('end_date', endDate);
|
|
|
|
|
|
2025-10-06 15:27:01 +02:00
|
|
|
const url = queryParams.toString()
|
|
|
|
|
? `${this.baseUrl}/${tenantId}/inventory-products/${inventoryProductId}/sales?${queryParams.toString()}`
|
|
|
|
|
: `${this.baseUrl}/${tenantId}/inventory-products/${inventoryProductId}/sales`;
|
2025-09-05 17:49:48 +02:00
|
|
|
|
2025-10-06 15:27:01 +02:00
|
|
|
return apiClient.get<SalesDataResponse[]>(url);
|
2025-09-05 17:49:48 +02:00
|
|
|
}
|
|
|
|
|
|
2025-10-06 15:27:01 +02:00
|
|
|
// ===================================================================
|
|
|
|
|
// OPERATIONS: Data Import
|
|
|
|
|
// Backend: services/sales/app/api/sales_operations.py
|
|
|
|
|
// ===================================================================
|
|
|
|
|
|
|
|
|
|
async validateImportFile(tenantId: string, file: File): Promise<ImportValidationResult> {
|
|
|
|
|
const formData = new FormData();
|
|
|
|
|
formData.append('file', file);
|
|
|
|
|
|
|
|
|
|
return apiClient.uploadFile<ImportValidationResult>(
|
|
|
|
|
`${this.baseUrl}/${tenantId}/sales/operations/import/validate`,
|
|
|
|
|
formData
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async importSalesData(
|
|
|
|
|
tenantId: string,
|
|
|
|
|
file: File,
|
|
|
|
|
skipValidation: boolean = false
|
|
|
|
|
): Promise<BulkImportResponse> {
|
|
|
|
|
const formData = new FormData();
|
|
|
|
|
formData.append('file', file);
|
|
|
|
|
formData.append('skip_validation', skipValidation.toString());
|
|
|
|
|
|
|
|
|
|
return apiClient.uploadFile<BulkImportResponse>(
|
|
|
|
|
`${this.baseUrl}/${tenantId}/sales/operations/import`,
|
|
|
|
|
formData
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async getImportHistory(
|
|
|
|
|
tenantId: string,
|
|
|
|
|
limit: number = 50,
|
|
|
|
|
offset: number = 0
|
|
|
|
|
): Promise<ImportSummary[]> {
|
|
|
|
|
const queryParams = new URLSearchParams();
|
|
|
|
|
queryParams.append('limit', limit.toString());
|
|
|
|
|
queryParams.append('offset', offset.toString());
|
|
|
|
|
|
|
|
|
|
return apiClient.get<ImportSummary[]>(
|
|
|
|
|
`${this.baseUrl}/${tenantId}/sales/operations/import/history?${queryParams.toString()}`
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async downloadImportTemplate(tenantId: string): Promise<Blob> {
|
|
|
|
|
return apiClient.get<Blob>(
|
|
|
|
|
`${this.baseUrl}/${tenantId}/sales/operations/import/template`,
|
|
|
|
|
{ responseType: 'blob' }
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ===================================================================
|
|
|
|
|
// OPERATIONS: Aggregation
|
|
|
|
|
// Backend: services/sales/app/api/sales_operations.py
|
|
|
|
|
// ===================================================================
|
|
|
|
|
|
|
|
|
|
async aggregateSalesByProduct(
|
2025-09-05 17:49:48 +02:00
|
|
|
tenantId: string,
|
|
|
|
|
startDate?: string,
|
|
|
|
|
endDate?: string
|
2025-10-06 15:27:01 +02:00
|
|
|
): Promise<ProductSalesAnalytics[]> {
|
2025-09-05 17:49:48 +02:00
|
|
|
const queryParams = new URLSearchParams();
|
|
|
|
|
if (startDate) queryParams.append('start_date', startDate);
|
|
|
|
|
if (endDate) queryParams.append('end_date', endDate);
|
|
|
|
|
|
2025-10-06 15:27:01 +02:00
|
|
|
const url = queryParams.toString()
|
|
|
|
|
? `${this.baseUrl}/${tenantId}/sales/operations/aggregate/by-product?${queryParams.toString()}`
|
|
|
|
|
: `${this.baseUrl}/${tenantId}/sales/operations/aggregate/by-product`;
|
2025-09-05 17:49:48 +02:00
|
|
|
|
2025-10-06 15:27:01 +02:00
|
|
|
return apiClient.get<ProductSalesAnalytics[]>(url);
|
2025-09-05 17:49:48 +02:00
|
|
|
}
|
|
|
|
|
|
2025-10-06 15:27:01 +02:00
|
|
|
async aggregateSalesByCategory(
|
|
|
|
|
tenantId: string,
|
|
|
|
|
startDate?: string,
|
|
|
|
|
endDate?: string
|
|
|
|
|
): Promise<CategorySalesAnalytics[]> {
|
|
|
|
|
const queryParams = new URLSearchParams();
|
|
|
|
|
if (startDate) queryParams.append('start_date', startDate);
|
|
|
|
|
if (endDate) queryParams.append('end_date', endDate);
|
|
|
|
|
|
|
|
|
|
const url = queryParams.toString()
|
|
|
|
|
? `${this.baseUrl}/${tenantId}/sales/operations/aggregate/by-category?${queryParams.toString()}`
|
|
|
|
|
: `${this.baseUrl}/${tenantId}/sales/operations/aggregate/by-category`;
|
|
|
|
|
|
|
|
|
|
return apiClient.get<CategorySalesAnalytics[]>(url);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async aggregateSalesByChannel(
|
|
|
|
|
tenantId: string,
|
|
|
|
|
startDate?: string,
|
|
|
|
|
endDate?: string
|
|
|
|
|
): Promise<ChannelPerformance[]> {
|
|
|
|
|
const queryParams = new URLSearchParams();
|
|
|
|
|
if (startDate) queryParams.append('start_date', startDate);
|
|
|
|
|
if (endDate) queryParams.append('end_date', endDate);
|
|
|
|
|
|
|
|
|
|
const url = queryParams.toString()
|
|
|
|
|
? `${this.baseUrl}/${tenantId}/sales/operations/aggregate/by-channel?${queryParams.toString()}`
|
|
|
|
|
: `${this.baseUrl}/${tenantId}/sales/operations/aggregate/by-channel`;
|
|
|
|
|
|
|
|
|
|
return apiClient.get<ChannelPerformance[]>(url);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ===================================================================
|
|
|
|
|
// ANALYTICS: Sales Summary
|
|
|
|
|
// Backend: services/sales/app/api/analytics.py
|
|
|
|
|
// ===================================================================
|
|
|
|
|
|
|
|
|
|
async getSalesAnalytics(
|
|
|
|
|
tenantId: string,
|
|
|
|
|
startDate?: string,
|
|
|
|
|
endDate?: string
|
|
|
|
|
): Promise<SalesAnalytics> {
|
|
|
|
|
const queryParams = new URLSearchParams();
|
|
|
|
|
if (startDate) queryParams.append('start_date', startDate);
|
|
|
|
|
if (endDate) queryParams.append('end_date', endDate);
|
|
|
|
|
|
|
|
|
|
const url = queryParams.toString()
|
|
|
|
|
? `${this.baseUrl}/${tenantId}/sales/analytics/summary?${queryParams.toString()}`
|
|
|
|
|
: `${this.baseUrl}/${tenantId}/sales/analytics/summary`;
|
|
|
|
|
|
|
|
|
|
return apiClient.get<SalesAnalytics>(url);
|
2025-09-05 17:49:48 +02:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-10-06 15:27:01 +02:00
|
|
|
export const salesService = new SalesService();
|