import { ApiError } from '@/types'; /** * All requests go to /api/* (relative paths) — the Next.js catch-all * route handler proxies them to the backend. This keeps all traffic * within the same origin, avoiding CORS preflights entirely. */ async function request(method: string, path: string, body?: unknown): Promise { const res = await fetch(path, { method, headers: { 'Content-Type': 'application/json' }, body: body != null ? JSON.stringify(body) : undefined, credentials: 'include', }); if (res.status === 401) { if (typeof window !== 'undefined') { window.location.href = '/login'; } throw new ApiError(401, 'UNAUTHORIZED', 'Authentication required'); } if (res.status === 403) { throw new ApiError(403, 'FORBIDDEN', 'Insufficient permissions'); } if (!res.ok) { const body = await res.json().catch(() => ({ message: 'Request failed' })); const envelope = body as { code?: string; message?: string; error?: { code?: string; message?: string } }; const error = envelope.error ?? envelope; throw new ApiError( res.status, error.code ?? 'REQUEST_FAILED', error.message ?? 'Request failed', ); } return res.json() as Promise; } export const api = { get: (path: string) => request('GET', path), post: (path: string, body?: unknown) => request('POST', path, body), patch: (path: string, body?: unknown) => request('PATCH', path, body), put: (path: string, body?: unknown) => request('PUT', path, body), delete: (path: string) => request('DELETE', path), }; // ── Auth ────────────────────────────────────────────────────────────────────── // FIX-14: admin panel auth resolves through the backoffice endpoint. const BACKOFFICE_AUTH_BASE = '/api/backoffice/auth'; export const authApi = { login: (email: string, password: string) => api.post<{ id: string; email: string; role: string }>(`${BACKOFFICE_AUTH_BASE}/login`, { email, password }), logout: () => api.post(`${BACKOFFICE_AUTH_BASE}/logout`), me: () => api.get<{ id: string; email: string; role: string } | { user: null }>(`${BACKOFFICE_AUTH_BASE}/me`), }; // ── Products ────────────────────────────────────────────────────────────────── // Backend: /catalog/products (admin list), /products/:id (admin CRUD) export const productsApi = { list: (params?: { limit?: number; offset?: number; q?: string }) => { const sp = new URLSearchParams(); if (params?.limit) sp.set('limit', String(params.limit)); if (params?.offset) sp.set('offset', String(params.offset)); if (params?.q) sp.set('q', params.q); const qs = sp.toString(); return api.get<{ items: import('@/types').Product[]; total: number }>( `/api/catalog/products${qs ? `?${qs}` : ''}`, ); }, get: (id: string) => api.get(`/api/products/${id}`), getVariants: (id: string) => api.get<{ items: import('@/types').ProductVariant[] }>(`/api/products/${id}/variants`), createVariant: (productId: string, data: { sku: string; ean?: string | null }) => api.post(`/api/products/${productId}/variants`, data), create: (data: unknown) => api.post('/api/products', data), update: (id: string, data: unknown) => api.patch(`/api/products/${id}`, data), generateSeo: (id: string) => api.post(`/api/products/${id}/generate-seo`), setState: (id: string, state: 'active' | 'archived') => api.patch(`/api/products/${id}/state`, { state }), delete: (id: string) => api.delete(`/api/products/${id}`), updateVariant: (productId: string, variantId: string, data: unknown) => api.patch(`/api/products/${productId}/variants/${variantId}`, data), }; // ── Orders ──────────────────────────────────────────────────────────────────── export const ordersApi = { list: (params?: { limit?: number; offset?: number; status?: string; q?: string }) => { const sp = new URLSearchParams(); if (params?.limit) sp.set('limit', String(params.limit)); if (params?.offset) sp.set('offset', String(params.offset)); if (params?.status) sp.set('status', params.status); if (params?.q) sp.set('q', params.q); const qs = sp.toString(); return api.get<{ items: import('@/types').Order[]; total: number } | import('@/types').Order[]>( `/api/orders${qs ? `?${qs}` : ''}`, ).then((data) => (Array.isArray(data) ? { items: data, total: data.length } : data)); }, get: (id: string) => api.get(`/api/orders/${id}/admin`), transition: (id: string, state: string, trackingNumber?: string, courier?: string) => api.post( `/api/orders/${id}/transitions/admin`, { state, ...(trackingNumber?.trim() ? { trackingNumber: trackingNumber.trim() } : {}), ...(courier?.trim() ? { courier: courier.trim() } : {}), }, ), editItems: (id: string, items: { variantId: string; quantity: number }[]) => api.put(`/api/orders/${id}/items`, { items }), updateShipping: (id: string, trackingNumber: string, note?: string, courier?: string) => api.post( `/api/orders/${id}/shipping`, { trackingNumber, ...(courier?.trim() ? { courier: courier.trim() } : {}), ...(note?.trim() ? { note } : {}), }, ), history: (id: string) => api.get<{ items: import('@/types').OrderHistoryEvent[] }>(`/api/orders/${id}/history`), }; // ── Customers ───────────────────────────────────────────────────────────────── export const customersApi = { list: (params?: { offset?: number; limit?: number; q?: string }) => { const sp = new URLSearchParams(); if (params?.offset !== undefined) sp.set('offset', String(params.offset)); if (params?.limit !== undefined) sp.set('limit', String(params.limit)); if (params?.q) sp.set('q', params.q); const qs = sp.toString(); return api.get<{ items: import('@/types').Customer[]; total: number }>(`/api/users${qs ? `?${qs}` : ''}`); }, get: (id: string) => api.get(`/api/users/${id}`), update: (id: string, data: { displayName?: string; phone?: string }) => api.patch(`/api/users/${id}`, data), create: (data: { email: string; password: string; displayName?: string; phone?: string }) => api.post('/api/auth/register', data), sendPasswordResetLink: (email: string) => api.post<{ ok: boolean }>('/api/auth/password-reset/request', { email }), // ── Direcciones (FIX-18) ─────────────────────────────────────────────────── listAddresses: (id: string) => api.get<{ items: import('@/types').Address[] }>(`/api/users/${id}/addresses`), createAddress: (id: string, data: Omit) => api.post(`/api/users/${id}/addresses`, data), updateAddress: (id: string, addressId: string, data: Partial>) => api.patch(`/api/users/${id}/addresses/${addressId}`, data), deleteAddress: (id: string, addressId: string) => api.delete(`/api/users/${id}/addresses/${addressId}`), }; // ── Brands ──────────────────────────────────────────────────────────────────── export const brandsApi = { list: () => api.get<{ items: import('@/types').Brand[] }>('/api/brands'), create: (data: unknown) => api.post('/api/brands', data), update: (id: string, data: unknown) => api.patch(`/api/brands/${id}`, data), delete: (id: string) => api.delete(`/api/brands/${id}`), }; // ── Categories ──────────────────────────────────────────────────────────────── export const categoriesApi = { list: () => api.get<{ items: import('@/types').Category[] }>('/api/categories/tree'), create: (data: unknown) => api.post('/api/categories', data), update: (id: string, data: unknown) => api.patch(`/api/categories/${id}`, data), generateSeo: (id: string) => api.post(`/api/categories/${id}/generate-seo`), delete: (id: string) => api.delete(`/api/categories/${id}`), }; // ── Inventory ───────────────────────────────────────────────────────────────── export const inventoryApi = { getAvailability: (variantId: string) => api.get(`/api/inventory/${variantId}/availability`), setStock: (id: string, quantity: number) => api.put(`/api/inventory/${id}/stock`, { quantity }), }; // ── Pricing ─────────────────────────────────────────────────────────────────── export const pricingApi = { getVariantPrice: (id: string) => api.get(`/api/pricing/variants/${id}`), setVariantPrice: ( id: string, netUnitAmountCents: number, vatRate: 'general' | 'reduced' | 'super-reduced', offerCents?: number | null, costCents?: number | null, ) => api.put(`/api/pricing/variants/${id}`, { netUnitAmountCents, vatRate, offerCents: offerCents ?? null, costCents: costCents ?? null, }), }; // ── Promotions ──────────────────────────────────────────────────────────────── export const promotionsApi = { list: () => api.get<{ items: unknown[] }>('/api/promotions'), create: (data: unknown) => api.post('/api/promotions', data), update: (code: string, data: unknown) => api.patch(`/api/promotions/${code}`, data), delete: (code: string) => api.delete(`/api/promotions/${code}`), }; // ── Reviews ─────────────────────────────────────────────────────────────────── export const reviewsApi = { listAdmin: (params?: { status?: string; limit?: number; offset?: number }) => { const sp = new URLSearchParams(); if (params?.status) sp.set('status', params.status); if (params?.limit) sp.set('limit', String(params.limit)); if (params?.offset) sp.set('offset', String(params.offset)); const qs = sp.toString(); return api.get<{ items: unknown[]; total: number }>(`/api/reviews/admin${qs ? `?${qs}` : ''}`); }, moderate: (id: string, status: 'published' | 'rejected') => api.patch(`/api/reviews/${id}/moderate`, { status }), }; // ── CMS ─────────────────────────────────────────────────────────────────────── export const cmsApi = { list: () => api.get<{ items: unknown[] }>('/api/cms/pages'), get: (slug: string) => api.get(`/api/cms/pages/${slug}`), create: (data: unknown) => api.post('/api/cms/pages', data), update: (id: string, data: unknown) => api.patch(`/api/cms/pages/${id}`, data), publish: (id: string) => api.post(`/api/cms/pages/${id}/publish`, {}), unpublish: (id: string) => api.post(`/api/cms/pages/${id}/unpublish`, {}), }; // ── Admin Users ───────────────────────────────────────────────────────────────── export const adminUsersApi = { list: (params?: { limit?: number; offset?: number; role?: string; q?: string }) => { const sp = new URLSearchParams(); if (params?.limit) sp.set('limit', String(params.limit)); if (params?.offset) sp.set('offset', String(params.offset)); if (params?.role) sp.set('role', params.role); if (params?.q) sp.set('q', params.q); const qs = sp.toString(); return api.get<{ items: { id: string; email: string; role: string; createdAt: string }[]; total: number }>( `/api/admin/users${qs ? `?${qs}` : ''}`, ); }, create: (data: { email: string; password: string; role: string }) => api.post<{ id: string; email: string; role: string; createdAt: string }>('/api/admin/users', data), update: (id: string, data: { role?: string; password?: string }) => api.patch<{ id: string; email: string; role: string; createdAt: string }>(`/api/admin/users/${id}`, data), delete: (id: string) => api.delete(`/api/admin/users/${id}`), }; // ── Tax Rates ──────────────────────────────────────────────────────────────────── export interface TaxRate { id: string; name: string; ratePercent: number; country: string; appliesTo: string; active: boolean; } export const taxApi = { list: () => api.get<{ items: TaxRate[] }>('/api/admin/tax-rates'), update: (id: string, data: Partial<{ name: string; ratePercent: number; active: boolean; appliesTo: 'general' | 'reduced' | 'super-reduced' }>) => api.patch('/api/admin/tax-rates/' + id, data), }; // ── Payments ────────────────────────────────────────────────────────────────────── export interface PaymentTransaction { id: string; provider: string; providerPaymentId: string | null; orderId: string | null; amountCents: number; currency: string; status: string; raw: unknown; createdAt: string; } export const paymentsApi = { list: (params?: { limit?: number; offset?: number; status?: string; q?: string }) => { const sp = new URLSearchParams(); if (params?.limit) sp.set('limit', String(params.limit)); if (params?.offset) sp.set('offset', String(params.offset)); if (params?.status) sp.set('status', params.status); if (params?.q) sp.set('q', params.q); const qs = sp.toString(); return api.get<{ items: PaymentTransaction[]; total: number }>(`/api/admin/payments${qs ? `?${qs}` : ''}`); }, refund: (id: string) => api.post<{ ok: boolean }>(`/api/admin/payments/${id}/refund`, {}), }; // ── Shipping ─────────────────────────────────────────────────────────────────────── export interface ShippingZone { id: string; name: string; country: string; postalCodePrefix: string | null; active: boolean; } export interface ShippingMethod { id: string; zoneId: string; zoneName: string; name: string; baseCostCents: number; freeShippingThresholdCents: number | null; description: string | null; maxWeightKg: number | null; freeShippingMaxWeightKg: number | null; active: boolean; } export const shippingApi = { listZones: () => api.get<{ items: ShippingZone[] }>('/api/admin/shipping/zones'), createZone: (data: { name: string; country: string; postalCodePrefix?: string | null; active?: boolean }) => api.post<{ id: string }>('/api/admin/shipping/zones', data), updateZone: (id: string, data: Partial<{ name: string; country: string; postalCodePrefix?: string | null; active: boolean }>) => api.patch('/api/admin/shipping/zones/' + id, data), deleteZone: (id: string) => api.delete('/api/admin/shipping/zones/' + id), listMethods: () => api.get<{ items: ShippingMethod[] }>('/api/admin/shipping/methods'), createMethod: (data: { zoneId: string; name: string; baseCostCents: number; freeShippingThresholdCents?: number | null; description?: string | null; maxWeightKg?: number | null; freeShippingMaxWeightKg?: number | null; active?: boolean }) => api.post<{ id: string }>('/api/admin/shipping/methods', data), updateMethod: (id: string, data: Partial<{ name: string; baseCostCents: number; freeShippingThresholdCents?: number | null; description?: string | null; maxWeightKg?: number | null; freeShippingMaxWeightKg?: number | null; active: boolean }>) => api.patch('/api/admin/shipping/methods/' + id, data), deleteMethod: (id: string) => api.delete('/api/admin/shipping/methods/' + id), }; // ── Store Settings ───────────────────────────────────────────────────────────────── export const auditApi = { list: (params?: { actorId?: string; action?: string; limit?: number; offset?: number }) => { const sp = new URLSearchParams(); if (params?.action) sp.set('action', params.action); if (params?.limit) sp.set('limit', String(params.limit)); if (params?.offset) sp.set('offset', String(params.offset)); const qs = sp.toString(); return api.get<{ items: AuditEntry[]; total: number }>(`/api/admin/audit${qs ? `?${qs}` : ''}`); }, }; export interface AuditEntry { id: string; actorId: string | null; action: string; target: string; metadata: Record; createdAt: string; } export interface StoreSettings { storeName: string; storeTagline: string; contactEmail: string; contactPhone: string; contactAddress: string; footerText: string; facebookUrl: string; instagramUrl: string; aiProvider: string; aiBaseUrl: string; aiModel: string; aiApiKey: string; aiApiKeyConfigured?: boolean; aiSeoTitlePrompt: string; aiSeoDescriptionPrompt: string; aiProductDescriptionPrompt: string; aiCategoryDescriptionPrompt: string; aiCategorySeoTitlePrompt: string; aiCategorySeoDescriptionPrompt: string; smtpHost: string; smtpPort: string; smtpSecure: boolean; smtpUser: string; smtpPass: string; smtpPassConfigured?: boolean; smtpFrom: string; couriers?: string[]; } export const settingsApi = { get: () => api.get('/api/admin/settings'), update: (data: Partial) => api.patch('/api/admin/settings', data), };