Add complete end-to-end AI-powered quote extraction workflow: - Disable PB SDK auto-cancellation to support parallel batch requests - Add required runtime dependencies for AI processing and document parsing - Implement AI settings backend, operator UI, and REST API routes - Add quote ingestion, insight population, and manual re-run AI endpoints - Build service layers for quote items, AI settings, and structured item insights - Fix attachment upload handling and quote item history blob parsing - Add supporting migrations, smoke tests, and shared utility functions - Export previously private helper functions for cross-service reuse
94 lines
3.8 KiB
TypeScript
94 lines
3.8 KiB
TypeScript
/**
|
|
* 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: `<smoke-minimax-real-direct-${Date.now()}@wfdash.local>`,
|
|
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);
|
|
});
|