CLN-04: Replaced any with unknown, proper interfaces, or concrete types across 17 files. Focus: error handlers, API responses, WebSocket data, and function parameters.
272 lines
9.8 KiB
TypeScript
272 lines
9.8 KiB
TypeScript
import { Product } from '../types';
|
|
import { apiClient } from './api/client';
|
|
|
|
export const marketplaceService = {
|
|
listProducts: async (
|
|
params?: {
|
|
status?: string;
|
|
seller_id?: string;
|
|
search?: string;
|
|
product_type?: string;
|
|
min_price?: number;
|
|
max_price?: number;
|
|
bpm?: number;
|
|
musical_key?: string;
|
|
category?: string;
|
|
},
|
|
pagination?: { page: number; limit: number },
|
|
) => {
|
|
const queryParams: Record<string, string | number | boolean | undefined> = {
|
|
...params,
|
|
page: pagination?.page || 1,
|
|
limit: pagination?.limit || 12,
|
|
};
|
|
|
|
// Rename search to q for backend
|
|
if (params?.search) {
|
|
queryParams.q = params.search;
|
|
delete queryParams.search;
|
|
}
|
|
// Rename product_type to type for backend
|
|
if (params?.product_type) {
|
|
queryParams.type = params.product_type;
|
|
delete queryParams.product_type;
|
|
}
|
|
// bpm, musical_key, category passed as-is (v0.401 M1)
|
|
|
|
// FIX: Désactiver les retries pour marketplace/products si l'erreur persiste
|
|
// car les erreurs 500 peuvent être dues à une liste vide (problème backend)
|
|
const response = await apiClient.get<Product[]>('/marketplace/products', {
|
|
params: queryParams,
|
|
// Désactiver les retries après le premier échec pour éviter les boucles
|
|
_disableRetry: false, // On garde les retries mais limités
|
|
} as any);
|
|
|
|
// Backend returns just the array for now? Or wrapped?
|
|
// Handler says: response.Success(c, products) which wraps in { success: true, data: [...] }
|
|
// apiClient unwraps response.data.
|
|
// However, backend ListProducts returns just Slice of products.
|
|
// It does NOT return total count yet in the standard response?
|
|
// The handler uses response.Success which usually wraps in "data".
|
|
// But pagination metadata (total, pages) is missing from the backend response structure in the current simplistic implementation.
|
|
// For MVP, if backend doesn't return count, we might have to just return the array and fake total or update backend to return metadata.
|
|
// The previous implementation returned { products, total, page, limit, total_pages }.
|
|
// Let's assume for MVP we return what we have. APIClient unwraps "data".
|
|
|
|
// STARTUP FIX: Backend currently just returns []Product in data.
|
|
// We need to match the return type expected by MarketplaceHome: { products: [], total: number ... }
|
|
const items = response.data || [];
|
|
|
|
return {
|
|
products: items,
|
|
total: items.length, // Approximate for MVP since backend doesn't return count yet
|
|
page: pagination?.page || 1,
|
|
limit: pagination?.limit || 12,
|
|
total_pages: 1, // Approximate
|
|
};
|
|
},
|
|
|
|
// Alias for listProducts to satisfy MarketplaceHome
|
|
fetchProducts: async (
|
|
filters?: {
|
|
status?: string;
|
|
seller_id?: string;
|
|
search?: string;
|
|
product_type?: string;
|
|
min_price?: number;
|
|
max_price?: number;
|
|
bpm?: number;
|
|
musical_key?: string;
|
|
category?: string;
|
|
},
|
|
pagination?: { page: number; limit: number },
|
|
) => {
|
|
return marketplaceService.listProducts(filters, pagination);
|
|
},
|
|
|
|
getProduct: async (productId: string) => {
|
|
const response = await apiClient.get<Product>(`/marketplace/products/${productId}`);
|
|
const product = response.data;
|
|
if (product?.images && Array.isArray(product.images) && product.images.length > 0) {
|
|
const first = product.images[0];
|
|
if (typeof first === 'object' && first !== null && 'url' in first) {
|
|
(product as any).images = (product.images as { url: string }[]).map((i) => i.url);
|
|
}
|
|
}
|
|
if (product?.previews?.length && product.id) {
|
|
const baseUrl = import.meta.env.VITE_API_URL?.replace(/\/api\/v1\/?$/, '') || '';
|
|
(product as any).preview_url = `${baseUrl}/api/v1/marketplace/products/${product.id}/preview`;
|
|
}
|
|
return product;
|
|
},
|
|
|
|
createProduct: async (productData: Partial<Product>) => {
|
|
const response = await apiClient.post<Product>(
|
|
'/marketplace/products',
|
|
productData,
|
|
);
|
|
return response.data;
|
|
},
|
|
|
|
uploadProductPreview: async (productId: string, file: File) => {
|
|
const formData = new FormData();
|
|
formData.append('file', file);
|
|
const response = await apiClient.post<{ id: string; file_path: string }>(
|
|
`/marketplace/products/${productId}/preview`,
|
|
formData,
|
|
{
|
|
headers: { 'Content-Type': 'multipart/form-data' },
|
|
},
|
|
);
|
|
return response.data;
|
|
},
|
|
|
|
updateProductImages: async (productId: string, images: { url: string; sort_order?: number }[]) => {
|
|
const response = await apiClient.put<{ url: string; sort_order: number }[]>(
|
|
`/marketplace/products/${productId}/images`,
|
|
{ images },
|
|
);
|
|
return response.data;
|
|
},
|
|
|
|
createOrder: async (items: { product_id: string }[], promoCode?: string) => {
|
|
const body: { items: { product_id: string }[]; promo_code?: string } = { items };
|
|
if (promoCode?.trim()) body.promo_code = promoCode.trim();
|
|
const response = await apiClient.post<{
|
|
order: { id: string; status: string; [key: string]: unknown };
|
|
client_secret?: string;
|
|
payment_id?: string;
|
|
}>('/marketplace/orders', body);
|
|
return response.data;
|
|
},
|
|
|
|
// Alias/Wrapper for purchase to satisfy MarketplaceHome
|
|
purchaseProduct: async (productId: string) => {
|
|
return marketplaceService.createOrder([{ product_id: productId }]);
|
|
},
|
|
|
|
listOrders: async () => {
|
|
const response = await apiClient.get<Array<{ id: string; status: string; [key: string]: unknown }>>('/marketplace/orders');
|
|
return response.data;
|
|
},
|
|
|
|
getOrder: async (orderId: string) => {
|
|
const response = await apiClient.get<{
|
|
id: string;
|
|
status: string;
|
|
total_amount: number;
|
|
currency: string;
|
|
items?: Array<{ product_id: string; price: number }>;
|
|
created_at?: string;
|
|
}>(`/marketplace/orders/${orderId}`);
|
|
return response.data;
|
|
},
|
|
|
|
// v0.403 F1: Download invoice PDF
|
|
downloadInvoice: async (orderId: string) => {
|
|
const response = await apiClient.get(`/marketplace/orders/${orderId}/invoice`, {
|
|
responseType: 'blob',
|
|
});
|
|
const blob = new Blob([response.data], { type: 'application/pdf' });
|
|
const url = URL.createObjectURL(blob);
|
|
const a = document.createElement('a');
|
|
a.href = url;
|
|
a.download = `invoice-${orderId.slice(0, 8)}.pdf`;
|
|
document.body.appendChild(a);
|
|
a.click();
|
|
document.body.removeChild(a);
|
|
URL.revokeObjectURL(url);
|
|
},
|
|
|
|
// Wishlist (requires auth)
|
|
getWishlist: async (): Promise<Product[]> => {
|
|
const response = await apiClient.get<{
|
|
items: Array<{ product?: Product; product_id?: string }>;
|
|
}>('/marketplace/wishlist');
|
|
const items = response.data?.items ?? [];
|
|
return items
|
|
.map((i) => (i.product ? i.product : (i.product_id ? ({ id: i.product_id } as Product) : null)))
|
|
.filter((p): p is Product => p != null);
|
|
},
|
|
addToWishlist: async (productId: string) => {
|
|
await apiClient.post('/marketplace/wishlist', { product_id: productId });
|
|
},
|
|
removeFromWishlist: async (productId: string) => {
|
|
await apiClient.delete(`/marketplace/wishlist/${productId}`);
|
|
},
|
|
|
|
// v0.401 M2: User's purchased licenses
|
|
getMyLicenses: async () => {
|
|
const response = await apiClient.get<{ licenses: Array<{
|
|
license: { id: string; product_id: string; order_id: string; type: string; created_at: string };
|
|
product: { id: string; title: string };
|
|
order: { id: string; total_amount: number; created_at: string };
|
|
download_url: string;
|
|
}> }>('/marketplace/licenses/mine');
|
|
return response.data?.licenses ?? [];
|
|
},
|
|
|
|
// Cart (requires auth)
|
|
getCart: async () => {
|
|
const response = await apiClient.get<{ items: Array<{ product_id: string; quantity?: number; [key: string]: unknown }> }>('/commerce/cart');
|
|
return response.data?.items ?? [];
|
|
},
|
|
addToCart: async (productId: string, quantity = 1) => {
|
|
await apiClient.post('/commerce/cart/items', { product_id: productId, quantity });
|
|
},
|
|
removeFromCart: async (itemId: string) => {
|
|
await apiClient.delete(`/commerce/cart/items/${itemId}`);
|
|
},
|
|
checkoutCart: async (promoCode?: string) => {
|
|
const body = promoCode?.trim() ? { promo_code: promoCode.trim() } : {};
|
|
const response = await apiClient.post<{ order?: { id: string; [key: string]: unknown }; client_secret?: string }>('/commerce/cart/checkout', body);
|
|
return response.data;
|
|
},
|
|
|
|
// v0.403 R2: Refund order
|
|
refundOrder: async (orderId: string, reason: string, details?: string) => {
|
|
await apiClient.post(`/marketplace/orders/${orderId}/refund`, {
|
|
reason: reason || 'Requested by customer',
|
|
details: details || '',
|
|
});
|
|
},
|
|
|
|
// v0.403 R1: Product reviews
|
|
createReview: async (productId: string, rating: number, comment: string) => {
|
|
const response = await apiClient.post<{
|
|
id: string;
|
|
product_id: string;
|
|
buyer_id: string;
|
|
order_id: string;
|
|
rating: number;
|
|
comment: string;
|
|
created_at: string;
|
|
}>(`/marketplace/products/${productId}/reviews`, { rating, comment });
|
|
return response.data;
|
|
},
|
|
|
|
listReviews: async (productId: string, page = 1, limit = 20) => {
|
|
const response = await apiClient.get<{ reviews: Array<{
|
|
id: string;
|
|
product_id: string;
|
|
buyer_id: string;
|
|
order_id: string;
|
|
rating: number;
|
|
comment: string;
|
|
created_at: string;
|
|
}> }>(`/marketplace/products/${productId}/reviews`, {
|
|
params: { limit, offset: (page - 1) * limit },
|
|
});
|
|
return response.data?.reviews ?? [];
|
|
},
|
|
|
|
// v0.402 P2: Validate promo code
|
|
validatePromoCode: async (code: string) => {
|
|
const response = await apiClient.get<{
|
|
code: string;
|
|
discount_type: 'percent' | 'fixed';
|
|
discount_value_cents: number;
|
|
}>(`/commerce/promo/${encodeURIComponent(code.trim())}`);
|
|
return response.data;
|
|
},
|
|
};
|