import fs from 'fs';
import path from 'path';
import { HistoricalQuote, QuoteDocument, QuoteEvaluationResult } from './types';
const DATA_DIR = path.join(__dirname, '..', 'data');
const QUOTES_FILE = path.join(DATA_DIR, 'quotes.json');
const EVALUATIONS_FILE = path.join(DATA_DIR, 'evaluations.json');
/**
* Initialize data directory and files
*/
export function initializeStorage(): void {
if (!fs.existsSync(DATA_DIR)) {
fs.mkdirSync(DATA_DIR, { recursive: true });
}
if (!fs.existsSync(QUOTES_FILE)) {
fs.writeFileSync(QUOTES_FILE, JSON.stringify([], null, 2));
}
if (!fs.existsSync(EVALUATIONS_FILE)) {
fs.writeFileSync(EVALUATIONS_FILE, JSON.stringify([], null, 2));
}
}
/**
* Load historical quotes from storage
*/
export function loadHistoricalQuotes(): HistoricalQuote[] {
try {
const data = fs.readFileSync(QUOTES_FILE, 'utf-8');
return JSON.parse(data);
} catch (error) {
console.error('Error loading historical quotes:', error);
return [];
}
}
/**
* Save a new historical quote
*/
export function saveHistoricalQuote(quote: HistoricalQuote): void {
const quotes = loadHistoricalQuotes();
// Check if quote already exists (by ID)
const existingIndex = quotes.findIndex(q => q.id === quote.id);
if (existingIndex >= 0) {
// Update existing
quotes[existingIndex] = quote;
} else {
// Add new
quotes.push(quote);
}
fs.writeFileSync(QUOTES_FILE, JSON.stringify(quotes, null, 2));
}
/**
* Convert a quote document to historical quote
*/
export function convertToHistoricalQuote(
doc: QuoteDocument,
approved: boolean,
approvedBy?: string
): HistoricalQuote {
const totalCost = doc.lines.reduce((sum, line) => sum + line.total, 0);
const qty = doc.lines.find(l => l.qty > 0)?.qty || 1;
const costPerUnit = totalCost / qty;
// Extract lead days from terms
const leadMatch = /(\d+)\s*(?:business\s*)?days?/i.exec(doc.terms);
const leadDays = leadMatch ? parseInt(leadMatch[1]) : 14;
// Try to extract material and processes from description
const firstLine = doc.lines[0]?.desc || '';
const material = extractMaterial(firstLine);
const processes = extractProcesses(doc.lines);
return {
id: doc.quoteId,
quoteDate: doc.createdAt,
customerName: doc.customerName,
normalized: {
material: material.toLowerCase(),
processes: processes.map(p => p.toLowerCase()),
qtyRange: getQtyRange(qty),
tolerances: extractTolerances(doc.lines),
finish: extractFinish(firstLine),
},
costPerUnit,
totalCost,
leadDays,
notes: doc.confidence === 'low' ? 'Low confidence estimate' : undefined,
approved,
approvedBy,
sentDate: doc.status === 'sent' ? new Date().toISOString() : undefined,
};
}
/**
* Load past evaluations
*/
export function loadEvaluations(): QuoteEvaluationResult[] {
try {
const data = fs.readFileSync(EVALUATIONS_FILE, 'utf-8');
return JSON.parse(data);
} catch (error) {
console.error('Error loading evaluations:', error);
return [];
}
}
/**
* Save an evaluation result
*/
export function saveEvaluation(evaluation: QuoteEvaluationResult): void {
const evaluations = loadEvaluations();
evaluations.push(evaluation);
// Keep only last 100 evaluations
if (evaluations.length > 100) {
evaluations.shift();
}
fs.writeFileSync(EVALUATIONS_FILE, JSON.stringify(evaluations, null, 2));
}
/**
* Get evaluation by idempotency key
*/
export function getEvaluation(idempotencyKey: string): QuoteEvaluationResult | null {
const evaluations = loadEvaluations();
return evaluations.find(e => e.idempotencyKey === idempotencyKey) || null;
}
// Helper functions
function extractMaterial(text: string): string {
const materialPatterns = [
/6061[-\s]?T6/i,
/6061/i,
/304[-\s]?SS/i,
/304/i,
/316[-\s]?SS/i,
/stainless\s*steel/i,
/aluminum/i,
/steel/i,
/titanium/i,
/brass/i,
];
for (const pattern of materialPatterns) {
const match = pattern.exec(text);
if (match) return match[0];
}
return 'unknown';
}
function extractProcesses(lines: any[]): string[] {
const processes: string[] = [];
const processLine = lines.find(l => l.desc.includes('Processes:'));
if (processLine) {
const match = /Processes:\s*(.+)/.exec(processLine.desc);
if (match) {
return match[1].split(',').map(p => p.trim());
}
}
return processes;
}
function extractTolerances(lines: any[]): string {
const tolLine = lines.find(l => l.desc.includes('Tolerances:'));
if (tolLine) {
const match = /Tolerances:\s*(.+)/.exec(tolLine.desc);
if (match) return match[1];
}
return '';
}
function extractFinish(text: string): string {
const finishPatterns = [
/anodize/i,
/powder coat/i,
/paint/i,
/polish/i,
/passivate/i,
/plating/i,
];
for (const pattern of finishPatterns) {
const match = pattern.exec(text);
if (match) return match[0];
}
return '';
}
function getQtyRange(qty: number): [number, number] {
if (qty <= 10) return [1, 10];
if (qty <= 50) return [11, 50];
if (qty <= 100) return [51, 100];
if (qty <= 500) return [101, 500];
if (qty <= 1000) return [501, 1000];
return [1001, 10000];
}