import type { PendingProduct } from '@/types/creation' const COMMON_HEADERS = new Set([ 'ID', 'Type', 'UGS', 'Nom', 'Publié', 'Mis en avant ?', 'Description courte', 'Description longue', 'Date de début de promo', 'Date de fin de promo', 'État de la TVA', 'Classe de TVA', 'En stock ?', 'Stock (en pièce)', 'Vendre individuellement ?', 'Prix marché', 'Prix remisé', 'Code ecopart', 'Ecopart montant', 'Famille', 'Sous famille', "Classe d'expédition (en retrait, livraison)", 'Conditionnement', 'Surface (m2)', 'Poids (kg) fonction unité', 'Images', 'Unité', 'Marque', 'Neuf ?', 'PHOTO ?', 'ID FAMILLE', 'ID SOUS FAMILLE', ]) export function findColIndex(headers: string[], search: string): number { const exact = headers.findIndex(h => h === search) if (exact !== -1) return exact return headers.findIndex(h => h.startsWith(search)) } export function extractSpecificFields(headers: string[], row: string[]): Record { const result: Record = {} headers.forEach((header, i) => { if (!header) return const isCommon = COMMON_HEADERS.has(header) || header.startsWith('Commentaire') if (isCommon) return const val = (row[i] ?? '').trim() if (val) result[header] = val }) return result } export function parseTabRows(tab: string, headers: string[], rows: string[][], dataStartRow = 2): PendingProduct[] { const idx = { id: findColIndex(headers, 'ID'), sku: findColIndex(headers, 'UGS'), nom: findColIndex(headers, 'Nom'), publie: findColIndex(headers, 'Publié'), stock: findColIndex(headers, 'Stock (en pièce)'), prixMarche: findColIndex(headers, 'Prix marché'), prixRemise: findColIndex(headers, 'Prix remisé'), poids: findColIndex(headers, 'Poids (kg) fonction unité'), marque: findColIndex(headers, 'Marque'), famille: findColIndex(headers, 'Famille'), sousFamille: findColIndex(headers, 'Sous famille'), commentaire: findColIndex(headers, 'Commentaire'), photo: findColIndex(headers, 'PHOTO ?'), collectionId: findColIndex(headers, 'ID FAMILLE'), subCollectionId: findColIndex(headers, 'ID SOUS FAMILLE'), } const get = (row: string[], i: number) => (i >= 0 ? (row[i] ?? '').trim() : '') return rows .map((row, rowIdx): PendingProduct | null => { const id = get(row, idx.id) const publie = get(row, idx.publie) if (id !== '' || publie !== '1') return null return { tab, rowNumber: dataStartRow + rowIdx, sku: get(row, idx.sku), title: get(row, idx.nom), priceMarket: get(row, idx.prixMarche).replace(',', '.'), priceActual: get(row, idx.prixRemise).replace(',', '.'), stock: parseInt(get(row, idx.stock) || '0', 10) || 0, weightKg: parseFloat(get(row, idx.poids).replace(',', '.') || '0') || 0, vendor: get(row, idx.marque), famille: get(row, idx.famille), sousFamille: get(row, idx.sousFamille), comment: get(row, idx.commentaire), collectionId: get(row, idx.collectionId), subCollectionId: get(row, idx.subCollectionId), hasPhoto: get(row, idx.photo) !== '', specificFields: extractSpecificFields(headers, row), } }) .filter((p): p is PendingProduct => p !== null) } /** * Écrit l'ID Shopify en colonne A de la ligne correspondante dans Google Sheets. * Utilise un compte de service avec accès éditeur sur la feuille. */ export async function writeShopifyIdToSheet(tab: string, rowNumber: number, shopifyId: string): Promise { const sheetId = process.env.GOOGLE_SHEETS_ID const email = process.env.GOOGLE_SERVICE_ACCOUNT_EMAIL const rawKey = process.env.GOOGLE_SERVICE_ACCOUNT_KEY if (!sheetId || !email || !rawKey) return // Génère un JWT pour l'authentification Google const privateKey = rawKey.replace(/\\n/g, '\n') const now = Math.floor(Date.now() / 1000) const header = Buffer.from(JSON.stringify({ alg: 'RS256', typ: 'JWT' })).toString('base64url') const payload = Buffer.from(JSON.stringify({ iss: email, scope: 'https://www.googleapis.com/auth/spreadsheets', aud: 'https://oauth2.googleapis.com/token', iat: now, exp: now + 3600, })).toString('base64url') const { createSign } = await import('crypto') const sign = createSign('RSA-SHA256') sign.update(`${header}.${payload}`) const signature = sign.sign(privateKey, 'base64url') const jwt = `${header}.${payload}.${signature}` const tokenRes = await fetch('https://oauth2.googleapis.com/token', { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, body: new URLSearchParams({ grant_type: 'urn:ietf:params:oauth:grant-type:jwt-bearer', assertion: jwt }), }) if (!tokenRes.ok) return const { access_token } = await tokenRes.json() as { access_token: string } const range = encodeURIComponent(`${tab}!A${rowNumber}`) await fetch( `https://sheets.googleapis.com/v4/spreadsheets/${sheetId}/values/${range}?valueInputOption=RAW`, { method: 'PUT', headers: { Authorization: `Bearer ${access_token}`, 'Content-Type': 'application/json' }, body: JSON.stringify({ range: `${tab}!A${rowNumber}`, majorDimension: 'ROWS', values: [[shopifyId]] }), }, ) } export async function fetchPendingProducts( alreadyCreatedRows: Set, tabFilter?: string, ): Promise { const apiKey = process.env.GOOGLE_API_KEY const sheetId = process.env.GOOGLE_SHEETS_ID if (!apiKey || !sheetId) throw new Error('GOOGLE_API_KEY et GOOGLE_SHEETS_ID requis') let tabs: string[] if (tabFilter) { tabs = [tabFilter] } else { const metaRes = await fetch( `https://sheets.googleapis.com/v4/spreadsheets/${sheetId}?key=${apiKey}&fields=sheets.properties.title`, ) if (!metaRes.ok) throw new Error(`Sheets metadata error: ${metaRes.status}`) const meta = await metaRes.json() tabs = (meta.sheets as Array<{ properties: { title: string } }>).map( s => s.properties.title, ) } const all: PendingProduct[] = [] for (const tab of tabs) { const range = encodeURIComponent(`${tab}!A1:BZ`) const res = await fetch( `https://sheets.googleapis.com/v4/spreadsheets/${sheetId}/values/${range}?key=${apiKey}`, ) if (!res.ok) continue const data = await res.json() const rows = (data.values ?? []) as string[][] if (rows.length < 2) continue // La ligne d'en-têtes est la première ligne qui contient "ID" en col A // (certains onglets ont une ligne de section en row 1, les vrais en-têtes sont en row 2) const headerRowIdx = rows.findIndex(r => (r[0] ?? '').trim() === 'ID') if (headerRowIdx === -1) continue // pas un onglet produit const headers = rows[headerRowIdx] const dataRows = rows.slice(headerRowIdx + 1) // Ignorer les onglets sans colonne "Publié" (COLLECTIONS, LIVRAISONS, etc.) if (findColIndex(headers, 'Publié') === -1) continue const pending = parseTabRows(tab, headers, dataRows, headerRowIdx + 2).filter( p => !alreadyCreatedRows.has(`${p.tab}:${p.rowNumber}`), ) all.push(...pending) } return all }