/** * One-shot fake-email processor test. * * Runs the actual AI extraction against MiniMax using the real key * the operator just configured in /settings/ai. Bypasses the Next.js * HTTP layer (which is broken in dev due to a pdfjs-dist/webpack * bundling issue — see https://github.com/mozilla/pdfjs-dist/issues/) * so we can prove the wiring works end-to-end without touching the * dev server. Same logic the route handler runs. */ import "./_env"; import { ingestQuoteFromEmail } from "../../src/lib/server/quotes-from-email-service"; import { processOneQuote } from "../../src/lib/server/quotes-ai-processor"; import { getAdminPocketBase } from "../../src/lib/server/pocketbase-admin"; import { getAiSettings } from "../../src/lib/server/ai-settings-service"; async function main() { const settings = await getAiSettings({ includeSecret: true }); console.log("=== AI settings (with secret) ==="); console.log(` provider: ${settings.provider}`); console.log(` model: ${settings.model_id}`); console.log(` base_url: ${settings.base_url || "(SDK default)"}`); console.log(` max_tokens: ${settings.max_tokens}`); console.log(` enabled: ${settings.enabled}`); console.log(` api_key_set: ${settings.api_key_set}\n`); if (!settings.enabled) { throw new Error("AI is not enabled in settings — flip the toggle first"); } console.log("=== Ingesting smoke email ==="); const ingest = await ingestQuoteFromEmail({ from_email: "smoke-minimax-real@example.invalid", from_name: "Smoke MiniMax Real", subject: "[SMOKE] URGENT QUOTE — M16 fasteners, JHB", body_plain: "Please quote:\n\n" + "- 10 x M16 x 50mm hex bolts grade 8.8 zinc plated\n" + "- 20 x M16 wing nuts zinc plated\n" + "- 4 x M16 flat washers zinc plated\n\n" + "Need delivery to Johannesburg by Friday. Please confirm availability and lead time.\n\n" + "Thanks,\nSmoke Tester", message_id: ``, received_at: new Date().toISOString(), company_hint: "Smoke MiniMax Direct Co", }); console.log(` quote: ${ingest.quote.quote_number} (${ingest.quote.id})`); console.log(` status: ai_status=${ingest.quote.ai_status}\n`); console.log("=== Running processOneQuote() ==="); const t0 = Date.now(); const result = await processOneQuote({ quoteId: ingest.quote.id }); const elapsed = ((Date.now() - t0) / 1000).toFixed(1); console.log(` outcome: ${result.outcome}`); console.log(` itemsWritten: ${result.itemsWritten}`); console.log(` error: ${result.error.slice(0, 200)}`); console.log(` elapsed: ${elapsed}s\n`); const pb = await getAdminPocketBase(); const after = await pb.collection("quotes").getOne(ingest.quote.id); console.log("=== Quote row after processing ==="); console.log(` ai_status: ${after.ai_status}`); console.log(` ai_error: ${(after.ai_error || "").slice(0, 200)}`); console.log(` ai_processed_at: ${after.ai_processed_at}`); if (result.outcome === "processed" && result.itemsWritten > 0) { const items = await pb.collection("quote_items").getFullList({ filter: `quote = "${ingest.quote.id}"`, }); console.log(`\n=== ${items.length} items written ===`); for (const it of items) { console.log( ` ${it.description} (qty=${it.quantity} ${it.unit || "ea"})`, ); } } // Cleanup console.log("\n=== Cleanup ==="); await pb.collection("quotes").delete(ingest.quote.id); try { await pb.collection("customers").delete(ingest.customer.id); } catch { /* cascade */ } if (ingest.company) { try { await pb.collection("companies").delete(ingest.company.id); } catch { /* cascade */ } } console.log(" done"); console.log("\n=== DONE ==="); } main().catch((e) => { console.error("FAILED:", e.message); console.error(e.stack); process.exit(1); });