fix: vérifier l'existence Shopify avant création pour éviter les doublons
- Chargement du catalogue Shopify au début de l'exécution (fetchAllShopifyProducts) - Vérification par SKU (ref) : si déjà présent dans Shopify → skipped, pas de création - Nouveau type d'événement 'skipped' dans CreationStreamEvent - CreationProgress affiche les produits ignorés en amber avec le SKU concerné - Résumé final inclut le compte de produits ignorés Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -5,7 +5,7 @@ import { fetchPendingProducts } from '@/lib/creationSheets'
|
|||||||
import { generateDescriptions } from '@/lib/openaiClient'
|
import { generateDescriptions } from '@/lib/openaiClient'
|
||||||
import { ensureMetafieldDefinition, setProductMetafield } from '@/lib/shopifyMetafields'
|
import { ensureMetafieldDefinition, setProductMetafield } from '@/lib/shopifyMetafields'
|
||||||
import { addProductToCollection } from '@/lib/shopifyCollections'
|
import { addProductToCollection } from '@/lib/shopifyCollections'
|
||||||
import { createShopifyProduct } from '@/lib/shopifySync'
|
import { createShopifyProduct, fetchAllShopifyProducts } from '@/lib/shopifySync'
|
||||||
import { prisma } from '@/lib/prisma'
|
import { prisma } from '@/lib/prisma'
|
||||||
import type { MetafieldResolution, CreationStreamEvent } from '@/types/creation'
|
import type { MetafieldResolution, CreationStreamEvent } from '@/types/creation'
|
||||||
|
|
||||||
@@ -28,13 +28,17 @@ export async function POST(req: NextRequest) {
|
|||||||
async start(controller) {
|
async start(controller) {
|
||||||
const send = (event: CreationStreamEvent) => controller.enqueue(encode(event))
|
const send = (event: CreationStreamEvent) => controller.enqueue(encode(event))
|
||||||
try {
|
try {
|
||||||
const existing = await prisma.productCreation.findMany({ select: { sheetTab: true, sheetRow: true } })
|
const [existingRows, shopifyProducts] = await Promise.all([
|
||||||
const alreadyCreated = new Set(existing.map(e => `${e.sheetTab}:${e.sheetRow}`))
|
prisma.productCreation.findMany({ select: { sheetTab: true, sheetRow: true } }),
|
||||||
|
fetchAllShopifyProducts(),
|
||||||
|
])
|
||||||
|
const alreadyCreated = new Set(existingRows.map(e => `${e.sheetTab}:${e.sheetRow}`))
|
||||||
|
const shopifyRefs = new Set(shopifyProducts.map(p => p.ref.trim().toLowerCase()))
|
||||||
const pending = await fetchPendingProducts(alreadyCreated)
|
const pending = await fetchPendingProducts(alreadyCreated)
|
||||||
const total = pending.length
|
const total = pending.length
|
||||||
|
|
||||||
if (total === 0) {
|
if (total === 0) {
|
||||||
send({ type: 'done', summary: { created: 0, errors: 0, status: 'success' } })
|
send({ type: 'done', summary: { created: 0, skipped: 0, errors: 0, status: 'success' } })
|
||||||
controller.close()
|
controller.close()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -45,10 +49,19 @@ export async function POST(req: NextRequest) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let created = 0
|
let created = 0
|
||||||
|
let skipped = 0
|
||||||
let errors = 0
|
let errors = 0
|
||||||
|
|
||||||
for (let i = 0; i < pending.length; i++) {
|
for (let i = 0; i < pending.length; i++) {
|
||||||
const product = pending[i]
|
const product = pending[i]
|
||||||
|
|
||||||
|
// Vérifier si le produit existe déjà dans Shopify par SKU
|
||||||
|
if (shopifyRefs.has(product.sku.trim().toLowerCase())) {
|
||||||
|
send({ type: 'skipped', title: product.title, sku: product.sku })
|
||||||
|
skipped++
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
send({ type: 'progress', current: i + 1, total, tab: product.tab, title: product.title, step: 'Génération descriptions…' })
|
send({ type: 'progress', current: i + 1, total, tab: product.tab, title: product.title, step: 'Génération descriptions…' })
|
||||||
try {
|
try {
|
||||||
const { longDesc } = await generateDescriptions({
|
const { longDesc } = await generateDescriptions({
|
||||||
@@ -90,10 +103,10 @@ export async function POST(req: NextRequest) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
const status = errors === 0 ? 'success' : created === 0 ? 'error' : 'partial'
|
const status = errors === 0 ? 'success' : created === 0 ? 'error' : 'partial'
|
||||||
send({ type: 'done', summary: { created, errors, status } })
|
send({ type: 'done', summary: { created, skipped, errors, status } })
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
const message = err instanceof Error ? err.message : 'Erreur inconnue'
|
const message = err instanceof Error ? err.message : 'Erreur inconnue'
|
||||||
send({ type: 'done', summary: { created: 0, errors: 1, status: 'error' } })
|
send({ type: 'done', summary: { created: 0, skipped: 0, errors: 1, status: 'error' } })
|
||||||
console.error('[creation/execute]', message)
|
console.error('[creation/execute]', message)
|
||||||
} finally {
|
} finally {
|
||||||
controller.close()
|
controller.close()
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ export function CreationProgress({ events, done }: Props) {
|
|||||||
const lastProgress = [...events].reverse().find(e => e.type === 'progress') as Extract<CreationStreamEvent, { type: 'progress' }> | undefined
|
const lastProgress = [...events].reverse().find(e => e.type === 'progress') as Extract<CreationStreamEvent, { type: 'progress' }> | undefined
|
||||||
const doneEvent = events.find(e => e.type === 'done') as Extract<CreationStreamEvent, { type: 'done' }> | undefined
|
const doneEvent = events.find(e => e.type === 'done') as Extract<CreationStreamEvent, { type: 'done' }> | undefined
|
||||||
const errors = events.filter(e => e.type === 'error') as Array<Extract<CreationStreamEvent, { type: 'error' }>>
|
const errors = events.filter(e => e.type === 'error') as Array<Extract<CreationStreamEvent, { type: 'error' }>>
|
||||||
|
const skipped = events.filter(e => e.type === 'skipped') as Array<Extract<CreationStreamEvent, { type: 'skipped' }>>
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
@@ -26,6 +27,17 @@ export function CreationProgress({ events, done }: Props) {
|
|||||||
<p className="text-xs text-gray-400 mt-1 truncate">{lastProgress.tab} — {lastProgress.title}</p>
|
<p className="text-xs text-gray-400 mt-1 truncate">{lastProgress.tab} — {lastProgress.title}</p>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
{skipped.length > 0 && (
|
||||||
|
<div className="space-y-1">
|
||||||
|
{skipped.map((e, i) => (
|
||||||
|
<div key={i} className="flex gap-2 p-2 bg-amber-900/20 border border-amber-800/40 rounded text-xs text-amber-300">
|
||||||
|
<span>⚠️</span>
|
||||||
|
<span className="font-medium">{e.title}</span>
|
||||||
|
<span className="text-amber-500">— SKU {e.sku} déjà présent dans Shopify, ignoré</span>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
{errors.length > 0 && (
|
{errors.length > 0 && (
|
||||||
<div className="space-y-1">
|
<div className="space-y-1">
|
||||||
{errors.map((e, i) => (
|
{errors.map((e, i) => (
|
||||||
@@ -43,6 +55,7 @@ export function CreationProgress({ events, done }: Props) {
|
|||||||
</p>
|
</p>
|
||||||
<p className="text-sm text-gray-300 mt-1">
|
<p className="text-sm text-gray-300 mt-1">
|
||||||
{doneEvent.summary.created} créé{doneEvent.summary.created > 1 ? 's' : ''}
|
{doneEvent.summary.created} créé{doneEvent.summary.created > 1 ? 's' : ''}
|
||||||
|
{doneEvent.summary.skipped > 0 && ` — ${doneEvent.summary.skipped} déjà existant${doneEvent.summary.skipped > 1 ? 's' : ''} (ignorés)`}
|
||||||
{doneEvent.summary.errors > 0 && ` — ${doneEvent.summary.errors} erreur${doneEvent.summary.errors > 1 ? 's' : ''}`}
|
{doneEvent.summary.errors > 0 && ` — ${doneEvent.summary.errors} erreur${doneEvent.summary.errors > 1 ? 's' : ''}`}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -41,7 +41,8 @@ export interface MetafieldResolution {
|
|||||||
export type CreationStreamEvent =
|
export type CreationStreamEvent =
|
||||||
| { type: 'progress'; current: number; total: number; tab: string; title: string; step: string }
|
| { type: 'progress'; current: number; total: number; tab: string; title: string; step: string }
|
||||||
| { type: 'error'; title: string; message: string }
|
| { type: 'error'; title: string; message: string }
|
||||||
| { type: 'done'; summary: { created: number; errors: number; status: 'success' | 'partial' | 'error' } }
|
| { type: 'skipped'; title: string; sku: string }
|
||||||
|
| { type: 'done'; summary: { created: number; skipped: number; errors: number; status: 'success' | 'partial' | 'error' } }
|
||||||
|
|
||||||
/** Résumé d'une création (stocké en DB) */
|
/** Résumé d'une création (stocké en DB) */
|
||||||
export interface CreationRecord {
|
export interface CreationRecord {
|
||||||
|
|||||||
Reference in New Issue
Block a user