veza/apps/web/src/services/commerceService.ts

236 lines
7.3 KiB
TypeScript
Raw Normal View History

import { apiClient } from '@/services/api/client';
import { Purchase, Product } from '../types';
interface OrderItemApi {
id?: string;
product_id: string;
price: number;
title?: string;
}
interface OrderApi {
id: string;
status: string;
total_amount?: number;
total?: number;
created_at: string;
items?: OrderItemApi[];
}
interface SellerSale {
order_id: string;
product_id: string;
product_title: string;
buyer_id: string;
amount: number;
date: string;
}
interface StatsEvolutionPoint {
date: string;
revenue: number;
sales_count: number;
}
interface TopProductStats {
product_id: string;
title: string;
revenue: number;
sales_count: number;
}
export interface SellerTransfer {
id: string;
seller_id?: string;
order_id: string;
amount_cents: number;
platform_fee_cents: number;
currency: string;
status: 'pending' | 'completed' | 'failed' | 'permanently_failed' | 'skipped';
error_message?: string;
retry_count?: number;
next_retry_at?: string;
created_at: string;
}
function formatRelativeDate(dateStr: string): string {
const d = new Date(dateStr);
const now = new Date();
const diffMs = now.getTime() - d.getTime();
const diffMins = Math.floor(diffMs / 60000);
const diffHours = Math.floor(diffMs / 3600000);
const diffDays = Math.floor(diffMs / 86400000);
if (diffMins < 60) return `${diffMins} mins ago`;
if (diffHours < 24) return `${diffHours} hour${diffHours > 1 ? 's' : ''} ago`;
if (diffDays < 7) return `${diffDays} day${diffDays > 1 ? 's' : ''} ago`;
return d.toLocaleDateString();
}
function mapOrderToPurchases(order: OrderApi): Purchase[] {
const orderId = typeof order.id === 'string' ? order.id : String(order.id);
const dateStr = order.created_at
? new Date(order.created_at).toISOString().slice(0, 10)
: '';
const items = order.items ?? [];
if (items.length === 0) {
const total = order.total_amount ?? order.total ?? 0;
return [
{
id: orderId,
orderId,
date: dateStr,
price: total,
status: order.status,
downloadUrl: '#',
license: { id: 'lic-default', name: 'Standard', price: total, features: [] },
product: {
id: 'unknown',
title: 'Order',
type: 'pack',
price: total,
currency: 'USD',
coverUrl: '',
} as Partial<Product> as Product,
},
];
}
return items.map((item, idx) => {
const productId = item.product_id ?? 'unknown';
const price = item.price ?? 0;
const title = item.title ?? productId;
return {
id: `${orderId}-${item.id ?? idx}`,
orderId,
date: dateStr,
price,
status: order.status,
downloadUrl: '#',
license: { id: `lic-${productId}`, name: 'Standard', price, features: [] },
product: {
id: productId,
title,
type: 'pack',
price,
currency: 'USD',
coverUrl: '',
} as Partial<Product> as Product,
};
});
}
export const commerceService = {
getPurchases: async (): Promise<Purchase[]> => {
const response = await apiClient.get<OrderApi[]>('/marketplace/orders');
const orders = Array.isArray(response.data) ? response.data : [];
return orders.flatMap(mapOrderToPurchases);
},
// v0.401 M3: Real API call to GET /sell/sales
getSales: async () => {
const response = await apiClient.get<{ data?: { sales?: SellerSale[] }; sales?: SellerSale[] }>('/sell/sales');
const data = response.data?.data ?? response.data;
const sales = (data as { sales?: SellerSale[] })?.sales ?? [];
return sales.map((s) => ({
id: s.order_id,
product: s.product_title,
date: formatRelativeDate(s.date),
amount: s.amount,
buyer: s.buyer_id?.slice(0, 8) ?? '—',
}));
},
getSellerStats: async () => {
const response = await apiClient.get<{ data?: { revenue?: number; sales?: number; sales_count?: number } }>(
'/sell/stats'
);
const data = (response.data?.data ?? response.data) as Record<string, unknown> | undefined;
return {
revenue: (data?.revenue as number) ?? 0,
sales: (data?.sales_count ?? data?.sales) as number ?? 0,
views: 0,
conversion: null as number | null,
};
},
// v0.401 M3: Stats evolution for chart
getSellerStatsEvolution: async (period: 'day' | 'week' | 'month' = 'week') => {
const response = await apiClient.get<{ data?: { evolution?: StatsEvolutionPoint[] }; evolution?: StatsEvolutionPoint[] }>(
`/sell/stats/evolution?period=${period}`
);
const data = response.data?.data ?? response.data;
return (data as { evolution?: StatsEvolutionPoint[] })?.evolution ?? [];
},
// v0.401 M3: Top products by revenue
getSellerTopProducts: async (limit = 10) => {
const response = await apiClient.get<{ data?: { top_products?: TopProductStats[] }; top_products?: TopProductStats[] }>(
`/sell/stats/top-products?limit=${limit}`
);
const data = response.data?.data ?? response.data;
return (data as { top_products?: TopProductStats[] })?.top_products ?? [];
},
requestRefund: async (orderId: string, reason: string) => {
await apiClient.post(`/marketplace/orders/${orderId}/refund`, {
reason: reason || 'Requested by customer',
details: '',
});
return { success: true };
},
// v0.602 P3: Stripe Connect seller balance (amounts in cents from API, returned in euros)
getSellerBalance: async (): Promise<{ connected: boolean; available: number; pending: number }> => {
const res = await apiClient.get<{
data?: { connected?: boolean; available?: number; pending?: number };
}>('/sell/balance');
const data = res.data?.data ?? res.data;
const raw = data as { connected?: boolean; available?: number; pending?: number };
return {
connected: raw?.connected ?? false,
available: (raw?.available ?? 0) / 100,
pending: (raw?.pending ?? 0) / 100,
};
},
// v0.603: Seller transfer history
getSellerTransfers: async (): Promise<SellerTransfer[]> => {
const res = await apiClient.get<{ data?: SellerTransfer[] }>('/sell/transfers');
return (res.data?.data ?? []) as SellerTransfer[];
},
// v0.701: Admin transfer management
getAdminTransfers: async (params?: {
status?: string;
seller_id?: string;
from?: string;
to?: string;
limit?: number;
offset?: number;
}): Promise<{ transfers: SellerTransfer[]; total: number }> => {
const res = await apiClient.get<{ data?: { transfers: SellerTransfer[]; total: number } }>(
'/admin/transfers',
{ params }
);
return (res.data?.data ?? { transfers: [], total: 0 }) as {
transfers: SellerTransfer[];
total: number;
};
},
retryAdminTransfer: async (transferId: string): Promise<SellerTransfer> => {
const res = await apiClient.post<{ data?: SellerTransfer }>(
`/admin/transfers/${transferId}/retry`
);
return (res.data?.data ?? res.data) as SellerTransfer;
},
// v0.602 P3: Stripe Connect onboarding
connectStripeOnboard: async (): Promise<{ onboarding_url: string }> => {
const res = await apiClient.post<{ data?: { onboarding_url?: string } }>('/sell/connect/onboard', {});
const data = res.data?.data ?? res.data;
return {
onboarding_url: (data as { onboarding_url?: string })?.onboarding_url ?? '',
};
},
};