feat: client Shopify sync (fetch all products + CRUD)
This commit is contained in:
@@ -0,0 +1,171 @@
|
|||||||
|
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[]
|
||||||
|
}
|
||||||
|
|
||||||
|
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',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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.
|
||||||
|
* Limite : 250 par page via cursor-based pagination.
|
||||||
|
*/
|
||||||
|
export async function fetchAllShopifyProducts(): Promise<ShopifyProductFull[]> {
|
||||||
|
const { baseUrl, headers } = getConfig()
|
||||||
|
const all: ShopifyProductFull[] = []
|
||||||
|
|
||||||
|
let url: string | null =
|
||||||
|
`${baseUrl}/products.json?limit=250&fields=id,title,handle,status,body_html,variants&status=any`
|
||||||
|
|
||||||
|
while (url) {
|
||||||
|
const res = await fetch(url, { headers })
|
||||||
|
if (!res.ok) throw new Error(`Shopify fetch error: ${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<string> {
|
||||||
|
const { baseUrl, headers } = getConfig()
|
||||||
|
|
||||||
|
const body = {
|
||||||
|
product: {
|
||||||
|
title: product.title,
|
||||||
|
body_html: product.description,
|
||||||
|
handle: product.handle,
|
||||||
|
status: product.status,
|
||||||
|
variants: [{ price: product.price, sku: product.ref }],
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
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()
|
||||||
|
return String(data.product.id)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Met à jour un produit Shopify existant (titre, description, prix, statut).
|
||||||
|
*/
|
||||||
|
export async function updateShopifyProduct(
|
||||||
|
shopifyId: string,
|
||||||
|
product: SyncProduct,
|
||||||
|
variantId: string,
|
||||||
|
): Promise<void> {
|
||||||
|
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<void> {
|
||||||
|
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}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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<string> {
|
||||||
|
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)
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user