Files
Atlas/lib/email-parser.ts
2025-11-12 20:21:32 +01:00

254 lines
7.4 KiB
TypeScript

export interface ParsedBookingData {
locationSlug: string;
// Event Details
eventDate: string;
eventType?: string;
guestCount?: string;
eventLocation?: string;
contactPerson?: string;
contactPhone?: string;
// Model & Options
model: string;
printOption?: string;
accessories?: string;
taskGame?: string;
audioGuestbook?: string;
delivery?: string;
// Price
calculatedPrice?: number;
// Customer Data
salutation?: string;
companyName?: string;
firstName?: string;
lastName?: string;
email: string;
phone: string;
// Address
street?: string;
zip?: string;
city?: string;
// Message
message?: string;
// Raw Email Data
rawSubject: string;
rawBody: string;
}
export class NinjaFormsEmailParser {
private fieldMappings = {
// Date fields
date: ['Datum:', 'Datum'],
// Event fields
eventType: ['Art der Veranstaltung:', 'Art der Veranstaltung'],
guestCount: ['Gästeanzahl:', 'Gästeanzahl'],
eventLocation: ['Location:', 'Location'],
contactPerson: ['Ansprechpartner:', 'Ansprechpartner'],
contactPhone: ['Durchwahl:', 'Durchwahl'],
// Model fields
model: ['Modell:', 'Modell'],
printOption: ['Vor Ort Druck?:', 'Vor Ort Druck'],
accessories: ['Inklusive Accessiores?:', 'Inklusive Accessiores'],
taskGame: ['Fotobox - Aufgabenspiel:', 'Aufgabenspiel'],
audioGuestbook: ['Audio-Gästebuch:', 'Audio'],
delivery: ['Lieferung (inkl. Auf-/Abbau):', 'Lieferung'],
// Price
price: ['Preis:', 'Preis'],
// Customer fields
salutation: ['Anrede:', 'Anrede'],
company: ['Firma:', 'Firma'],
firstName: ['Vorname:', 'Vorname'],
lastName: ['Nachname:', 'Nachname'],
email: ['E-Mail:', 'E-Mail', 'Email:'],
phone: ['Telefon:', 'Telefon', 'Phone:'],
// Address
street: ['Straße:', 'Strasse:', 'Straße'],
zip: ['PLZ:', 'PLZ'],
city: ['Ort:', 'Ort'],
// Message
message: ['Nachricht:', 'Nachricht'],
};
parseEmailBody(body: string, subject: string): ParsedBookingData | null {
try {
const lines = body.split('\n').map(line => line.trim()).filter(Boolean);
const data: any = {
rawSubject: subject,
rawBody: body,
};
// Detect location from subject
data.locationSlug = this.detectLocation(subject, body);
// Parse each line (check current and next line for key-value pairs)
for (let i = 0; i < lines.length; i++) {
const line = lines[i];
const nextLine = lines[i + 1];
this.parseLine(line, nextLine, data);
}
// Extract email and phone if not found
if (!data.email) {
data.email = this.extractEmail(body);
}
if (!data.phone) {
data.phone = this.extractPhone(body);
}
// Map model names
data.model = this.mapModelName(data.model);
console.log('Parsed data before validation:', {
email: data.email,
phone: data.phone,
eventDate: data.eventDate,
date: data.date,
firstName: data.firstName,
lastName: data.lastName,
});
// Validate required fields
if (!data.email || (!data.eventDate && !data.date)) {
console.log('Validation failed: missing email or date');
return null;
}
// Normalize date field
if (data.date && !data.eventDate) {
data.eventDate = data.date;
}
return data as ParsedBookingData;
} catch (error) {
console.error('Email parsing error:', error);
return null;
}
}
private parseLine(line: string, nextLine: string | undefined, data: any) {
for (const [key, patterns] of Object.entries(this.fieldMappings)) {
for (const pattern of patterns) {
if (line.includes(pattern)) {
// Try to get value from same line first
let value = line.split(pattern)[1]?.trim();
// If no value on same line, check next line
if (!value && nextLine) {
value = nextLine.trim();
}
if (value) {
// Special handling for street - keep full address
if (key === 'street' && value) {
// Don't split on numbers, keep the full street name
data[key] = value;
} else if (key === 'price') {
data.calculatedPrice = this.extractPrice(value);
} else {
data[key] = value;
}
return;
}
}
}
}
}
private detectLocation(subject: string, body: string): string {
const text = (subject + ' ' + body).toLowerCase();
if (text.includes('lübeck') || text.includes('luebeck')) return 'luebeck';
if (text.includes('hamburg')) return 'hamburg';
if (text.includes('kiel')) return 'kiel';
if (text.includes('berlin') || text.includes('potsdam')) return 'berlin';
if (text.includes('rostock')) return 'rostock';
return 'luebeck'; // Default
}
private mapModelName(modelString?: string): string {
if (!modelString) return 'VINTAGE_SMILE';
const model = modelString.toLowerCase();
if (model.includes('smile')) return 'VINTAGE_SMILE';
if (model.includes('foto') || model.includes('photo')) return 'VINTAGE_PHOTOS';
if (model.includes('nostalgie')) return 'NOSTALGIE';
if (model.includes('magic') || model.includes('spiegel') || model.includes('mirror')) return 'MAGIC_MIRROR';
return 'VINTAGE_SMILE';
}
private extractPrice(value: string): number {
const match = value.match(/(\d+)/);
return match ? parseInt(match[1]) : 0;
}
private extractEmail(text: string): string | null {
const emailRegex = /[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}/;
const match = text.match(emailRegex);
return match ? match[0] : null;
}
private extractPhone(text: string): string | null {
const phoneRegex = /(\+?49|0)[\s\-]?(\d{2,5})[\s\-]?(\d{3,})[\s\-]?(\d{3,})/;
const match = text.match(phoneRegex);
return match ? match[0].replace(/\s+/g, '') : null;
}
parseFromNinjaFormsEmail(emailText: string, subject: string): ParsedBookingData | null {
// Clean HTML if present
let cleanText = emailText;
// Convert HTML table rows to newlines for better parsing
cleanText = cleanText.replace(/<\/tr>/gi, '\n');
cleanText = cleanText.replace(/<\/td>/gi, ' ');
cleanText = cleanText.replace(/<br\s*\/?>/gi, '\n');
cleanText = cleanText.replace(/<\/p>/gi, '\n');
cleanText = cleanText.replace(/<\/div>/gi, '\n');
// Remove all other HTML tags
cleanText = cleanText.replace(/<[^>]*>/g, '');
// Decode HTML entities
cleanText = cleanText
.replace(/&nbsp;/g, ' ')
.replace(/&amp;/g, '&')
.replace(/&lt;/g, '<')
.replace(/&gt;/g, '>')
.replace(/&quot;/g, '"')
.replace(/&#(\d+);/g, (match, dec) => String.fromCharCode(dec));
// Clean up whitespace
cleanText = cleanText
.split('\n')
.map(line => line.trim())
.filter(line => line.length > 0)
.join('\n');
console.log('=== CLEANED TEXT FOR PARSING ===');
console.log(cleanText.substring(0, 800));
console.log('=== END CLEANED TEXT ===');
const result = this.parseEmailBody(cleanText, subject);
console.log('Parse result from parseEmailBody:', result ? 'SUCCESS' : 'FAILED');
return result;
}
}
export const emailParser = new NinjaFormsEmailParser();