import type { ShopifyProductFull, SyncProduct } from '@/types/sync' const API_VERSION = '2024-01' function getConfig() { const domain = process.env.SHOPIFY_STORE_DOMAIN const token = process.env.SHOPIFY_ADMIN_API_TOKEN if (!domain || !token) { throw new Error('SHOPIFY_STORE_DOMAIN et SHOPIFY_ADMIN_API_TOKEN requis') } return { baseUrl: `https://${domain}/admin/api/${API_VERSION}`, headers: { 'X-Shopify-Access-Token': token, 'Content-Type': 'application/json', }, } } interface ShopifyVariant { id: number price: string sku: string inventory_quantity: number } interface ShopifyAPIProduct { id: number title: string handle: string body_html: string status: string variants: ShopifyVariant[] images: Array<{ src: string }> } function toSyncProduct(p: ShopifyAPIProduct): ShopifyProductFull { return { shopifyId: String(p.id), ref: p.variants[0]?.sku || p.handle, handle: p.handle, title: p.title, description: p.body_html ?? '', price: p.variants[0]?.price ?? '0.00', stock: p.variants[0]?.inventory_quantity ?? 0, status: p.status === 'active' ? 'active' : 'draft', images: (p.images ?? []).map(i => i.src), } } function extractNextUrl(linkHeader: string | null): string | null { if (!linkHeader) return null const match = linkHeader.match(/<([^>]+)>;\s*rel="next"/) return match ? match[1] : null } /** * Récupère tous les produits Shopify (actifs + drafts), en paginant. * L'API REST ne supporte pas status=any — on fait deux passes (active + draft). */ export async function fetchAllShopifyProducts(): Promise { const [active, draft] = await Promise.all([ fetchByStatus('active'), fetchByStatus('draft'), ]) return [...active, ...draft] } async function fetchByStatus(status: 'active' | 'draft'): Promise { const { baseUrl, headers } = getConfig() const all: ShopifyProductFull[] = [] let url: string | null = `${baseUrl}/products.json?limit=250&fields=id,title,handle,status,body_html,variants,images&status=${status}` while (url) { const res = await fetch(url, { headers }) if (!res.ok) throw new Error(`Shopify fetch error (${status}): ${res.status}`) const data = await res.json() all.push(...(data.products as ShopifyAPIProduct[]).map(toSyncProduct)) url = extractNextUrl(res.headers.get('Link')) } return all } /** * Crée un nouveau produit Shopify depuis un SyncProduct. * Retourne l'ID Shopify créé. */ export async function createShopifyProduct(product: SyncProduct): Promise { const { baseUrl, headers } = getConfig() const variant: Record = { price: product.price, sku: product.ref, inventory_management: 'shopify', } if (product.weightKg != null && product.weightKg > 0) { variant.weight = product.weightKg variant.weight_unit = 'kg' } const body = { product: { title: product.title, body_html: product.description, handle: product.handle, status: product.status, variants: [variant], }, } const res = await fetch(`${baseUrl}/products.json`, { method: 'POST', headers, body: JSON.stringify(body), }) if (!res.ok) { const err = await res.text() throw new Error(`Shopify createProduct error: ${res.status} — ${err}`) } const data = await res.json() const shopifyId = String(data.product.id) const variantId = String(data.product.variants[0].id) // Récupérer le location_id par défaut pour setter le stock if (product.stock > 0) { try { const locRes = await fetch(`${baseUrl}/locations.json`, { headers }) if (locRes.ok) { const locData = await locRes.json() as { locations: Array<{ id: number }> } const locationId = locData.locations[0]?.id if (locationId) { await fetch(`${baseUrl}/inventory_levels/set.json`, { method: 'POST', headers, body: JSON.stringify({ location_id: locationId, inventory_item_id: data.product.variants[0].inventory_item_id, available: product.stock, }), }) } } } catch { // Non bloquant : le produit est créé, le stock peut être corrigé manuellement } } void variantId return shopifyId } /** * Met à jour un produit Shopify existant (titre, description, prix, statut). */ export async function updateShopifyProduct( shopifyId: string, product: SyncProduct, variantId: string, ): Promise { const { baseUrl, headers } = getConfig() const body = { product: { id: shopifyId, title: product.title, body_html: product.description, status: product.status, variants: [{ id: variantId, price: product.price }], }, } const res = await fetch(`${baseUrl}/products/${shopifyId}.json`, { method: 'PUT', headers, body: JSON.stringify(body), }) if (!res.ok) { const err = await res.text() throw new Error(`Shopify updateProduct error: ${res.status} — ${err}`) } } /** * Passe un produit Shopify en draft (désactivation — jamais de suppression). */ export async function deactivateShopifyProduct(shopifyId: string): Promise { const { baseUrl, headers } = getConfig() const res = await fetch(`${baseUrl}/products/${shopifyId}.json`, { method: 'PUT', headers, body: JSON.stringify({ product: { id: shopifyId, status: 'draft' } }), }) if (!res.ok) { const err = await res.text() throw new Error(`Shopify deactivateProduct error: ${res.status} — ${err}`) } } /** * Supprime définitivement un produit Shopify. */ export async function deleteShopifyProduct(shopifyId: string): Promise { const { baseUrl, headers } = getConfig() const res = await fetch(`${baseUrl}/products/${shopifyId}.json`, { method: 'DELETE', headers, }) if (!res.ok) { const err = await res.text() throw new Error(`Shopify deleteProduct error: ${res.status} — ${err}`) } } /** * Récupère l'ID de la première variante d'un produit Shopify. * Nécessaire pour mettre à jour le prix (update variant). */ export async function getFirstVariantId(shopifyId: string): Promise { const { baseUrl, headers } = getConfig() const res = await fetch(`${baseUrl}/products/${shopifyId}/variants.json?limit=1`, { headers }) if (!res.ok) throw new Error(`Shopify getVariants error: ${res.status}`) const data = await res.json() const variant = data.variants?.[0] if (!variant) throw new Error(`Aucune variante pour le produit ${shopifyId}`) return String(variant.id) }