import type { ShopifyProductFull, SyncProduct } from '@/types/sync' const API_VERSION = '2024-01' export 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 compare_at_price: string | null 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', compareAtPrice: p.variants[0]?.compare_at_price ?? undefined, 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, compare_at_price: product.compareAtPrice ?? null, 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 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 const inventoryItemId = data.product.variants[0].inventory_item_id if (locationId && inventoryItemId) { // Connect l'inventory item à la location avant de setter la quantité await fetch(`${baseUrl}/inventory_levels/connect.json`, { method: 'POST', headers, body: JSON.stringify({ location_id: locationId, inventory_item_id: inventoryItemId }), }) const setRes = await fetch(`${baseUrl}/inventory_levels/set.json`, { method: 'POST', headers, body: JSON.stringify({ location_id: locationId, inventory_item_id: inventoryItemId, available: product.stock ?? 0, }), }) if (!setRes.ok) { const errText = await setRes.text() console.error(`[shopify] inventory_levels/set failed: ${setRes.status} — ${errText}`) } } } } catch (err) { console.error('[shopify] stock set error:', err) } 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, compare_at_price: product.compareAtPrice ?? null }], }, } 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) }