From 40d537efb6abab6bd2e29c16ec8d865d6fe3003a Mon Sep 17 00:00:00 2001 From: Mathieu Date: Tue, 9 Jun 2026 10:09:27 +0200 Subject: [PATCH] feat(plan5): API POST /api/creation/execute streaming NDJSON --- src/app/api/creation/execute/route.ts | 107 ++++++++++++++++++++++++++ 1 file changed, 107 insertions(+) create mode 100644 src/app/api/creation/execute/route.ts diff --git a/src/app/api/creation/execute/route.ts b/src/app/api/creation/execute/route.ts new file mode 100644 index 0000000..f4e7022 --- /dev/null +++ b/src/app/api/creation/execute/route.ts @@ -0,0 +1,107 @@ +import { NextRequest } from 'next/server' +import { getServerSession } from 'next-auth' +import { authOptions } from '@/lib/auth' +import { fetchPendingProducts } from '@/lib/creationSheets' +import { generateDescriptions } from '@/lib/openaiClient' +import { ensureMetafieldDefinition, setProductMetafield } from '@/lib/shopifyMetafields' +import { addProductToCollection } from '@/lib/shopifyCollections' +import { createShopifyProduct } from '@/lib/shopifySync' +import { prisma } from '@/lib/prisma' +import type { MetafieldResolution, CreationStreamEvent } from '@/types/creation' + +function encode(event: CreationStreamEvent): string { + return JSON.stringify(event) + '\n' +} + +export async function POST(req: NextRequest) { + const session = await getServerSession(authOptions) + if (!session) { + return new Response(JSON.stringify({ error: 'Non authentifié' }), { status: 401 }) + } + + const userId = (session.user as { id: string }).id + const body = await req.json().catch(() => ({})) as { resolutions?: MetafieldResolution[] } + const resolutions: MetafieldResolution[] = body.resolutions ?? [] + const resolutionMap = new Map(resolutions.map(r => [r.columnHeader, r])) + + const stream = new ReadableStream({ + async start(controller) { + const send = (event: CreationStreamEvent) => controller.enqueue(encode(event)) + try { + const existing = await prisma.productCreation.findMany({ select: { sheetTab: true, sheetRow: true } }) + const alreadyCreated = new Set(existing.map(e => `${e.sheetTab}:${e.sheetRow}`)) + const pending = await fetchPendingProducts(alreadyCreated) + const total = pending.length + + if (total === 0) { + send({ type: 'done', summary: { created: 0, errors: 0, status: 'success' } }) + controller.close() + return + } + + // Crée les définitions manquantes en amont + for (const resolution of resolutions) { + if (resolution.action === 'create') await ensureMetafieldDefinition(resolution) + } + + let created = 0 + let errors = 0 + + for (let i = 0; i < pending.length; i++) { + const product = pending[i] + send({ type: 'progress', current: i + 1, total, tab: product.tab, title: product.title, step: 'Génération descriptions…' }) + try { + const { longDesc } = await generateDescriptions({ + title: product.title, + rawDescription: '', + comment: product.comment, + vendor: product.vendor, + famille: product.famille, + }) + send({ type: 'progress', current: i + 1, total, tab: product.tab, title: product.title, step: 'Création produit Shopify…' }) + const shopifyId = await createShopifyProduct({ + ref: product.sku, + handle: product.sku.toLowerCase().replace(/[^a-z0-9]+/g, '-'), + title: product.title, + description: longDesc, + price: product.priceActual || '0.00', + stock: product.stock, + status: 'active', + }) + if (product.collectionId) await addProductToCollection(shopifyId, product.collectionId) + if (product.subCollectionId) await addProductToCollection(shopifyId, product.subCollectionId) + send({ type: 'progress', current: i + 1, total, tab: product.tab, title: product.title, step: 'Métachamps…' }) + for (const [colHeader, value] of Object.entries(product.specificFields)) { + const resolution = resolutionMap.get(colHeader) + if (!resolution || resolution.action === 'skip') continue + await setProductMetafield(shopifyId, resolution, value) + } + await prisma.productCreation.create({ + data: { userId, sheetTab: product.tab, sheetRow: product.rowNumber, shopifyId, sku: product.sku, title: product.title, status: 'success' }, + }) + created++ + } catch (err) { + const message = err instanceof Error ? err.message : 'Erreur inconnue' + send({ type: 'error', title: product.title, message }) + await prisma.productCreation.create({ + data: { userId, sheetTab: product.tab, sheetRow: product.rowNumber, shopifyId: '', sku: product.sku, title: product.title, status: 'error', errorMessage: message }, + }) + errors++ + } + } + const status = errors === 0 ? 'success' : created === 0 ? 'error' : 'partial' + send({ type: 'done', summary: { created, errors, status } }) + } catch (err) { + const message = err instanceof Error ? err.message : 'Erreur inconnue' + send({ type: 'done', summary: { created: 0, errors: 1, status: 'error' } }) + console.error('[creation/execute]', message) + } finally { + controller.close() + } + }, + }) + + return new Response(stream, { + headers: { 'Content-Type': 'application/x-ndjson', 'Transfer-Encoding': 'chunked' }, + }) +}