veza/apps/web/src/services/marketplaceService.ts
senke 8efd398239 refactor(frontend): eliminate ~45 'any' types in production code
CLN-04: Replaced any with unknown, proper interfaces, or concrete
types across 17 files. Focus: error handlers, API responses,
WebSocket data, and function parameters.
2026-02-22 17:44:49 +01:00

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;
},
};