diff --git a/src/__tests__/lib/googleSheets.test.ts b/src/__tests__/lib/googleSheets.test.ts new file mode 100644 index 0000000..fbcaa42 --- /dev/null +++ b/src/__tests__/lib/googleSheets.test.ts @@ -0,0 +1,62 @@ +import { parseSheetRows } from '@/lib/googleSheets' + +describe('parseSheetRows', () => { + it('parse une ligne complète', () => { + const rows = [['REF-1042', 'Carrelage sol', 'Beau carrelage', '29.99', '15', 'actif']] + const result = parseSheetRows(rows) + expect(result).toHaveLength(1) + expect(result[0]).toEqual({ + ref: 'REF-1042', + handle: 'ref-1042', + title: 'Carrelage sol', + description: 'Beau carrelage', + price: '29.99', + stock: 15, + status: 'active', + }) + }) + + it('normalise le handle en minuscules avec tirets', () => { + const rows = [['ART 001/B', 'Produit', '', '10.00', '0', 'actif']] + const [p] = parseSheetRows(rows) + expect(p.handle).toBe('art-001-b') + }) + + it('ignore les lignes sans référence', () => { + const rows = [['', 'Titre vide', '', '0', '0', ''], ['REF-1', 'OK', '', '5.00', '1', 'actif']] + expect(parseSheetRows(rows)).toHaveLength(1) + }) + + it('gère un prix avec virgule (format français)', () => { + const rows = [['REF-2', 'Test', '', '19,99', '0', 'actif']] + expect(parseSheetRows(rows)[0].price).toBe('19.99') + }) + + it('marque comme draft si la colonne statut contient "inac"', () => { + const rows = [ + ['REF-3', 'A', '', '1.00', '0', 'Inactif'], + ['REF-4', 'B', '', '1.00', '0', 'inactif'], + ['REF-5', 'C', '', '1.00', '0', 'actif'], + ['REF-6', 'D', '', '1.00', '0', ''], + ] + const results = parseSheetRows(rows) + expect(results[0].status).toBe('draft') + expect(results[1].status).toBe('draft') + expect(results[2].status).toBe('active') + expect(results[3].status).toBe('active') + }) + + it('utilise la référence comme titre si le titre est vide', () => { + const rows = [['REF-7', '', '', '5.00', '0', 'actif']] + expect(parseSheetRows(rows)[0].title).toBe('REF-7') + }) + + it('accepte des lignes avec colonnes manquantes', () => { + const rows = [['REF-8']] + const [p] = parseSheetRows(rows) + expect(p.ref).toBe('REF-8') + expect(p.price).toBe('0.00') + expect(p.stock).toBe(0) + expect(p.status).toBe('active') + }) +}) diff --git a/src/lib/googleSheets.ts b/src/lib/googleSheets.ts new file mode 100644 index 0000000..b958a56 --- /dev/null +++ b/src/lib/googleSheets.ts @@ -0,0 +1,69 @@ +import type { SyncProduct } from '@/types/sync' + +const API_VERSION = 'v4' + +function normalizeHandle(ref: string): string { + return ref + .toLowerCase() + .replace(/[^a-z0-9]+/g, '-') + .replace(/^-+|-+$/g, '') +} + +/** + * Parse des lignes brutes Google Sheets en SyncProduct[]. + * Colonnes : A=Référence, B=Nom, C=Description, D=Prix, E=Stock, F=Statut + * Exporté pour les tests unitaires. + */ +export function parseSheetRows(rows: string[][]): SyncProduct[] { + return rows + .map((row): SyncProduct | null => { + const ref = row[0]?.trim() + if (!ref) return null + + const rawPrice = (row[3] ?? '').replace(',', '.').trim() + const price = parseFloat(rawPrice || '0') + + return { + ref, + handle: normalizeHandle(ref), + title: row[1]?.trim() || ref, + description: row[2]?.trim() || '', + price: isNaN(price) ? '0.00' : price.toFixed(2), + stock: parseInt(row[4] ?? '0', 10) || 0, + status: (row[5] ?? '').toLowerCase().includes('inac') ? 'draft' : 'active', + } + }) + .filter((p): p is SyncProduct => p !== null) +} + +/** + * Lit tous les produits depuis Google Sheets via l'API v4. + * Utilise un service account (GOOGLE_SERVICE_ACCOUNT_EMAIL + GOOGLE_PRIVATE_KEY). + */ +export async function readSheetProducts(): Promise { + const email = process.env.GOOGLE_SERVICE_ACCOUNT_EMAIL + const key = process.env.GOOGLE_PRIVATE_KEY?.replace(/\\n/g, '\n') + const sheetId = process.env.GOOGLE_SHEETS_ID + const sheetName = process.env.GOOGLE_SHEETS_TAB ?? 'Produits' + + if (!email || !key || !sheetId) { + throw new Error('Variables GOOGLE_SERVICE_ACCOUNT_EMAIL, GOOGLE_PRIVATE_KEY et GOOGLE_SHEETS_ID requises') + } + + const { google } = await import('googleapis') + + const auth = new google.auth.GoogleAuth({ + credentials: { client_email: email, private_key: key }, + scopes: ['https://www.googleapis.com/auth/spreadsheets.readonly'], + }) + + const sheets = google.sheets({ version: API_VERSION, auth }) + + const response = await sheets.spreadsheets.values.get({ + spreadsheetId: sheetId, + range: `${sheetName}!A2:F`, // A2 = skip header row + }) + + const rows = (response.data.values ?? []) as string[][] + return parseSheetRows(rows) +}