254 lines
7.4 KiB
TypeScript
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(/ /g, ' ')
|
|
.replace(/&/g, '&')
|
|
.replace(/</g, '<')
|
|
.replace(/>/g, '>')
|
|
.replace(/"/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();
|