From 16fece8ca9a61dc7f4efacade9641a13e2b75c00 Mon Sep 17 00:00:00 2001 From: Mathieu Date: Mon, 8 Jun 2026 21:04:45 +0200 Subject: [PATCH] feat: API route POST /api/sync/execute (NDJSON streaming) --- src/app/api/sync/execute/route.ts | 113 ++++++++++++++++++++++++++++++ 1 file changed, 113 insertions(+) create mode 100644 src/app/api/sync/execute/route.ts diff --git a/src/app/api/sync/execute/route.ts b/src/app/api/sync/execute/route.ts new file mode 100644 index 0000000..98cdfa6 --- /dev/null +++ b/src/app/api/sync/execute/route.ts @@ -0,0 +1,113 @@ +import { NextRequest, NextResponse } from 'next/server' +import { getServerSession } from 'next-auth' +import { authOptions } from '@/lib/auth' +import { prisma } from '@/lib/prisma' +import { + createShopifyProduct, + updateShopifyProduct, + deactivateShopifyProduct, + getFirstVariantId, +} from '@/lib/shopifySync' +import type { SyncChange, SyncLogEntry, SyncStreamEvent } from '@/types/sync' + +export async function POST(req: NextRequest) { + const session = await getServerSession(authOptions) + if (!session?.user) { + return NextResponse.json({ error: 'Non authentifié' }, { status: 401 }) + } + + let changes: SyncChange[] + try { + const body = await req.json() + changes = body.changes + } catch { + return NextResponse.json({ error: 'Corps JSON invalide' }, { status: 400 }) + } + + if (!Array.isArray(changes) || changes.length === 0) { + return NextResponse.json({ error: 'Aucun changement à appliquer' }, { status: 400 }) + } + + const userId = (session.user as { id: string }).id + const encoder = new TextEncoder() + + const stream = new ReadableStream({ + async start(controller) { + const send = (event: SyncStreamEvent) => { + controller.enqueue(encoder.encode(JSON.stringify(event) + '\n')) + } + + const log: SyncLogEntry[] = [] + let created = 0, updated = 0, deactivated = 0, errors = 0 + const total = changes.filter((c) => c.type !== 'unchanged').length + let current = 0 + + for (const change of changes) { + if (change.type === 'unchanged') continue + current++ + + try { + if (change.type === 'create') { + await createShopifyProduct(change.sheetProduct!) + created++ + const msg = `✓ ${change.ref} créé` + log.push({ ref: change.ref, action: 'create', status: 'success', message: msg }) + send({ type: 'progress', current, total, message: msg }) + + } else if (change.type === 'update') { + const variantId = await getFirstVariantId(change.shopifyProduct!.shopifyId) + await updateShopifyProduct( + change.shopifyProduct!.shopifyId, + change.sheetProduct!, + variantId, + ) + updated++ + const fields = change.changedFields?.map((f) => f.field).join(', ') ?? '' + const msg = `✓ ${change.ref} mis à jour (${fields})` + log.push({ ref: change.ref, action: 'update', status: 'success', message: msg }) + send({ type: 'progress', current, total, message: msg }) + + } else if (change.type === 'deactivate') { + await deactivateShopifyProduct(change.shopifyProduct!.shopifyId) + deactivated++ + const msg = `✓ ${change.ref} désactivé` + log.push({ ref: change.ref, action: 'deactivate', status: 'success', message: msg }) + send({ type: 'progress', current, total, message: msg }) + } + } catch (err) { + errors++ + const msg = `✗ ${change.ref} : ${(err as Error).message}` + log.push({ ref: change.ref, action: change.type as SyncLogEntry['action'], status: 'error', message: msg }) + send({ type: 'progress', current, total, message: msg }) + } + } + + const syncStatus = errors === 0 ? 'success' : errors === total ? 'error' : 'partial' + try { + await prisma.syncRun.create({ + data: { + userId, + status: syncStatus, + created, + updated, + deactivated, + errors, + log: JSON.stringify(log), + }, + }) + } catch (dbErr) { + console.error('Erreur sauvegarde SyncRun:', dbErr) + } + + send({ + type: 'done', + summary: { created, updated, deactivated, errors, status: syncStatus }, + }) + controller.close() + }, + }) + + return new Response(stream, { + headers: { 'Content-Type': 'application/x-ndjson; charset=utf-8' }, + }) +}