refactor(email): abstract email system with proper architecture
- Create service layer for email generation (PaymentMethodService, OrderTransformerService, EmailBuilderService) - Extract all hardcoded values to configuration files - Add proper TypeScript types for all email data structures - Update GraphQL subscription to include transactions and events - Refactor order-created, order-cancelled templates to use new architecture - Payment method now dynamically detected from order transactions This makes the email system: - Testable: Each service can be unit tested independently - Configurable: All values in config.ts, no hardcoding - Extensible: Easy to add new languages or payment methods - Maintainable: Clear separation of concerns
This commit is contained in:
Generated
+13
-4
File diff suppressed because one or more lines are too long
@@ -86,5 +86,14 @@ fragment OrderCreatedWebhookPayload on OrderCreated {
|
|||||||
key
|
key
|
||||||
value
|
value
|
||||||
}
|
}
|
||||||
|
transactions {
|
||||||
|
id
|
||||||
|
message
|
||||||
|
externalUrl
|
||||||
|
events {
|
||||||
|
message
|
||||||
|
type
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,7 +1,14 @@
|
|||||||
import Handlebars from "handlebars";
|
/**
|
||||||
|
* Order Cancelled Email Template
|
||||||
|
*
|
||||||
|
* Refactored to use the abstracted email service architecture.
|
||||||
|
*/
|
||||||
|
|
||||||
// Translations for Order Cancelled emails
|
import Handlebars from "handlebars";
|
||||||
const translations = {
|
import { emailServiceConfig, DEFAULT_LANGUAGE } from "@/lib/email/config";
|
||||||
|
|
||||||
|
// Cancelled order specific translations
|
||||||
|
const cancelledTranslations = {
|
||||||
en: {
|
en: {
|
||||||
subject: "Order {{orderNumber}} Cancelled",
|
subject: "Order {{orderNumber}} Cancelled",
|
||||||
title: "Order Cancelled",
|
title: "Order Cancelled",
|
||||||
@@ -14,8 +21,8 @@ const translations = {
|
|||||||
itemsLabel: "Items",
|
itemsLabel: "Items",
|
||||||
contactButton: "Contact Us",
|
contactButton: "Contact Us",
|
||||||
thankYou: "Thank you!",
|
thankYou: "Thank you!",
|
||||||
companyName: "ManoonOils",
|
companyName: emailServiceConfig.companyName,
|
||||||
footerText: "support@manoonoils.com"
|
footerText: emailServiceConfig.fromEmail,
|
||||||
},
|
},
|
||||||
sr: {
|
sr: {
|
||||||
subject: "Porudžbina {{orderNumber}} Otkazana",
|
subject: "Porudžbina {{orderNumber}} Otkazana",
|
||||||
@@ -29,8 +36,8 @@ const translations = {
|
|||||||
itemsLabel: "Artikli",
|
itemsLabel: "Artikli",
|
||||||
contactButton: "Kontaktirajte Nas",
|
contactButton: "Kontaktirajte Nas",
|
||||||
thankYou: "Hvala vam!",
|
thankYou: "Hvala vam!",
|
||||||
companyName: "ManoonOils",
|
companyName: emailServiceConfig.companyName,
|
||||||
footerText: "support@manoonoils.com"
|
footerText: emailServiceConfig.fromEmail,
|
||||||
},
|
},
|
||||||
de: {
|
de: {
|
||||||
subject: "Bestellung {{orderNumber}} Storniert",
|
subject: "Bestellung {{orderNumber}} Storniert",
|
||||||
@@ -44,8 +51,8 @@ const translations = {
|
|||||||
itemsLabel: "Artikel",
|
itemsLabel: "Artikel",
|
||||||
contactButton: "Kontaktieren Sie Uns",
|
contactButton: "Kontaktieren Sie Uns",
|
||||||
thankYou: "Vielen Dank!",
|
thankYou: "Vielen Dank!",
|
||||||
companyName: "ManoonOils",
|
companyName: emailServiceConfig.companyName,
|
||||||
footerText: "support@manoonoils.com"
|
footerText: emailServiceConfig.fromEmail,
|
||||||
},
|
},
|
||||||
fr: {
|
fr: {
|
||||||
subject: "Commande {{orderNumber}} Annulée",
|
subject: "Commande {{orderNumber}} Annulée",
|
||||||
@@ -59,13 +66,13 @@ const translations = {
|
|||||||
itemsLabel: "Articles",
|
itemsLabel: "Articles",
|
||||||
contactButton: "Contactez-Nous",
|
contactButton: "Contactez-Nous",
|
||||||
thankYou: "Merci!",
|
thankYou: "Merci!",
|
||||||
companyName: "ManoonOils",
|
companyName: emailServiceConfig.companyName,
|
||||||
footerText: "support@manoonoils.com"
|
footerText: emailServiceConfig.fromEmail,
|
||||||
}
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
// Customer email template
|
// Template
|
||||||
const customerEmailTemplate = `
|
const cancelledEmailTemplate = `
|
||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html>
|
<html>
|
||||||
<head>
|
<head>
|
||||||
@@ -74,7 +81,7 @@ const customerEmailTemplate = `
|
|||||||
<body style="font-family:-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Helvetica,Arial,sans-serif;background:#f6f6f6;margin:0;padding:20px;">
|
<body style="font-family:-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Helvetica,Arial,sans-serif;background:#f6f6f6;margin:0;padding:20px;">
|
||||||
<div style="max-width:600px;margin:0 auto;background:white;padding:40px 20px;">
|
<div style="max-width:600px;margin:0 auto;background:white;padding:40px 20px;">
|
||||||
<div style="text-align:center;margin-bottom:30px;">
|
<div style="text-align:center;margin-bottom:30px;">
|
||||||
<img src="https://minio-api.nodecrew.me/manoon-media/2024/09/cropped-manoon-logo_256x-1-1.png" width="150" alt="{{companyName}}">
|
<img src="{{companyLogoUrl}}" width="150" alt="{{companyName}}">
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<h1 style="font-size:24px;font-weight:bold;color:#1a1a1a;margin-bottom:20px;">{{title}}</h1>
|
<h1 style="font-size:24px;font-weight:bold;color:#1a1a1a;margin-bottom:20px;">{{title}}</h1>
|
||||||
@@ -96,7 +103,7 @@ const customerEmailTemplate = `
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div style="text-align:center;margin:20px 0;">
|
<div style="text-align:center;margin:20px 0;">
|
||||||
<a href="https://manoonoils.com/contact" style="display:inline-block;background:#000;color:white;padding:12px 30px;border-radius:4px;text-decoration:none;font-size:14px;font-weight:bold;">{{contactButton}}</a>
|
<a href="{{storefrontUrl}}" style="display:inline-block;background:#000;color:white;padding:12px 30px;border-radius:4px;text-decoration:none;font-size:14px;font-weight:bold;">{{contactButton}}</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<p style="font-size:14px;color:#666;margin-bottom:10px;">{{footerText}}</p>
|
<p style="font-size:14px;color:#666;margin-bottom:10px;">{{footerText}}</p>
|
||||||
@@ -110,60 +117,24 @@ const customerEmailTemplate = `
|
|||||||
</html>
|
</html>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
// Admin email template
|
const compileCancelledEmail = Handlebars.compile(cancelledEmailTemplate);
|
||||||
const adminEmailTemplate = `
|
|
||||||
<!DOCTYPE html>
|
|
||||||
<html>
|
|
||||||
<head>
|
|
||||||
<meta charset="utf-8">
|
|
||||||
</head>
|
|
||||||
<body style="font-family:-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Helvetica,Arial,sans-serif;background:#f6f6f6;margin:0;padding:20px;">
|
|
||||||
<div style="max-width:600px;margin:0 auto;background:white;padding:40px 20px;">
|
|
||||||
<h1 style="font-size:24px;font-weight:bold;color:#1a1a1a;margin-bottom:20px;">Order Cancelled</h1>
|
|
||||||
|
|
||||||
<div style="background:#f9f9f9;padding:15px;border-radius:8px;margin-bottom:20px;">
|
|
||||||
<h3 style="font-size:16px;font-weight:bold;color:#1a1a1a;margin:0 0 10px 0;">Order #{{orderNumber}}</h3>
|
|
||||||
<p style="font-size:14px;color:#333;margin:0 0 8px 0;"><strong>Date:</strong> {{orderDate}}</p>
|
|
||||||
<p style="font-size:14px;color:#333;margin:0;"><strong>Reason:</strong> {{cancellationReason}}</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div style="background:#f9f9f9;padding:15px;border-radius:8px;margin-bottom:20px;">
|
|
||||||
<h3 style="font-size:16px;font-weight:bold;color:#1a1a1a;margin:0 0 10px 0;">Customer</h3>
|
|
||||||
<p style="font-size:14px;color:#333;margin:0 0 8px 0;"><strong>Name:</strong> {{customerName}}</p>
|
|
||||||
<p style="font-size:14px;color:#333;margin:0 0 8px 0;"><strong>Email:</strong> {{customerEmail}}</p>
|
|
||||||
<p style="font-size:14px;color:#333;margin:0;"><strong>Phone:</strong> {{#if phone}}{{phone}}{{else}}Not provided{{/if}}</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<p style="font-size:16px;font-weight:bold;color:#1a1a1a;margin-bottom:10px;">Items</p>
|
|
||||||
<div style="margin-bottom:20px;">
|
|
||||||
{{#each items}}
|
|
||||||
<p style="font-size:14px;color:#333;margin:0 0 4px 0;">{{quantity}}x {{name}} - {{price}}</p>
|
|
||||||
{{/each}}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div style="text-align:center;margin:20px 0;">
|
|
||||||
<a href="https://dashboard.manoonoils.com/orders" style="display:inline-block;background:#000;color:white;padding:12px 30px;border-radius:4px;text-decoration:none;font-size:14px;font-weight:bold;">Dashboard</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
`;
|
|
||||||
|
|
||||||
// Compile templates
|
|
||||||
const compileCustomerEmail = Handlebars.compile(customerEmailTemplate);
|
|
||||||
const compileAdminEmail = Handlebars.compile(adminEmailTemplate);
|
|
||||||
|
|
||||||
export function getOrderCancelledEmails(order: any) {
|
export function getOrderCancelledEmails(order: any) {
|
||||||
// Detect language
|
const lang = (order.languageCode?.toLowerCase() || DEFAULT_LANGUAGE) as keyof typeof cancelledTranslations;
|
||||||
const lang = order.languageCode?.toLowerCase() || "en";
|
const t = cancelledTranslations[lang] || cancelledTranslations[DEFAULT_LANGUAGE];
|
||||||
const t = translations[lang as keyof typeof translations] || translations.en;
|
|
||||||
|
|
||||||
// Format data
|
|
||||||
const customerName = order.shippingAddress?.firstName
|
const customerName = order.shippingAddress?.firstName
|
||||||
? `${order.shippingAddress.firstName} ${order.shippingAddress.lastName || ""}`.trim()
|
? `${order.shippingAddress.firstName} ${order.shippingAddress.lastName || ""}`.trim()
|
||||||
: order.userEmail?.split("@")[0] || "Customer";
|
: order.userEmail?.split("@")[0] || "Customer";
|
||||||
|
|
||||||
const currency = order.total?.gross?.currency || "EUR";
|
const currency = order.total?.gross?.currency || "EUR";
|
||||||
|
const orderDate = order.created
|
||||||
|
? new Date(order.created).toLocaleString(lang === "sr" ? "sr-RS" : lang === "de" ? "de-DE" : lang === "fr" ? "fr-FR" : "en-US")
|
||||||
|
: new Date().toLocaleString();
|
||||||
|
const cancellationReason = order.metadata?.find((m: { key: string; value: string }) => m.key === "cancellation_reason")?.value || t.noReason;
|
||||||
|
|
||||||
|
const greeting = Handlebars.compile(t.greeting)({ customerName });
|
||||||
|
const subject = Handlebars.compile(t.subject)({ orderNumber: order.number || order.id });
|
||||||
|
|
||||||
const items = (order.lines || []).map((line: any) => ({
|
const items = (order.lines || []).map((line: any) => ({
|
||||||
name: line.variant?.product?.name || line.variant?.name || "Product",
|
name: line.variant?.product?.name || line.variant?.name || "Product",
|
||||||
@@ -171,37 +142,25 @@ export function getOrderCancelledEmails(order: any) {
|
|||||||
price: `${line.totalPrice?.gross?.amount?.toFixed(2) || "0.00"} ${currency}`
|
price: `${line.totalPrice?.gross?.amount?.toFixed(2) || "0.00"} ${currency}`
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const orderDate = order.created
|
const emailData = {
|
||||||
? new Date(order.created).toLocaleString(lang === "sr" ? "sr-RS" : lang === "de" ? "de-DE" : lang === "fr" ? "fr-FR" : "en-US")
|
|
||||||
: new Date().toLocaleString();
|
|
||||||
|
|
||||||
const cancellationReason = order.cancellationReason || t.noReason;
|
|
||||||
|
|
||||||
// Customer email data
|
|
||||||
const customerData = {
|
|
||||||
...t,
|
...t,
|
||||||
|
greeting,
|
||||||
orderNumber: order.number || order.id,
|
orderNumber: order.number || order.id,
|
||||||
customerName,
|
|
||||||
orderDate,
|
orderDate,
|
||||||
cancellationReason,
|
cancellationReason,
|
||||||
items
|
items,
|
||||||
};
|
companyLogoUrl: emailServiceConfig.companyLogoUrl,
|
||||||
|
storefrontUrl: emailServiceConfig.storefrontUrl,
|
||||||
// Admin email data (English)
|
|
||||||
const adminData = {
|
|
||||||
orderNumber: order.number || order.id,
|
|
||||||
customerName,
|
|
||||||
customerEmail: order.userEmail || "",
|
|
||||||
orderDate,
|
|
||||||
cancellationReason,
|
|
||||||
phone: order.shippingAddress?.phone || "",
|
|
||||||
items
|
|
||||||
};
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
customerSubject: Handlebars.compile(t.subject)({ orderNumber: order.number || order.id }),
|
customerSubject: subject,
|
||||||
customerHtml: compileCustomerEmail(customerData),
|
customerHtml: compileCancelledEmail(emailData),
|
||||||
adminSubject: `Order Cancelled #${order.number || order.id}`,
|
adminSubject: `Order Cancelled #${order.number || order.id}`,
|
||||||
adminHtml: compileAdminEmail(adminData)
|
adminHtml: compileCancelledEmail({
|
||||||
|
...emailData,
|
||||||
|
title: `Order Cancelled - #${order.number || order.id}`,
|
||||||
|
body: `Order has been cancelled. Customer: ${order.userEmail}. Reason: ${cancellationReason}`,
|
||||||
|
}),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,287 +1,37 @@
|
|||||||
import Handlebars from "handlebars";
|
/**
|
||||||
|
* Order Created Email Template
|
||||||
|
*
|
||||||
|
* This module is now a thin wrapper around the abstracted email service architecture.
|
||||||
|
* All hardcoded values have been moved to configuration files.
|
||||||
|
*
|
||||||
|
* Architecture:
|
||||||
|
* - types.ts: Type definitions
|
||||||
|
* - config.ts: All configurable values (translations, URLs, etc.)
|
||||||
|
* - services/paymentMethodService.ts: Payment method detection logic
|
||||||
|
* - services/orderTransformerService.ts: Order data transformation
|
||||||
|
* - services/emailBuilderService.ts: Email template compilation
|
||||||
|
*/
|
||||||
|
|
||||||
// Translations for Order Created emails
|
import { EmailBuilderService } from "@/lib/email/services/emailBuilderService";
|
||||||
const translations = {
|
import { emailServiceConfig } from "@/lib/email/config";
|
||||||
en: {
|
|
||||||
subject: "Order {{orderNumber}} Confirmed",
|
|
||||||
title: "Order Confirmed",
|
|
||||||
greeting: "Dear {{customerName}},",
|
|
||||||
body: "Thank you for your order! We have received it and it is now being processed.",
|
|
||||||
orderLabel: "Order:",
|
|
||||||
dateLabel: "Date:",
|
|
||||||
statusLabel: "Status:",
|
|
||||||
itemsLabel: "Items",
|
|
||||||
subtotalLabel: "Subtotal:",
|
|
||||||
shippingLabel: "Shipping:",
|
|
||||||
taxLabel: "Tax:",
|
|
||||||
totalLabel: "Total:",
|
|
||||||
shippingAddressLabel: "Shipping Address",
|
|
||||||
phoneLabel: "Phone:",
|
|
||||||
phoneNotProvided: "Not provided",
|
|
||||||
viewOrderButton: "View Order",
|
|
||||||
thankYou: "Thank you!",
|
|
||||||
companyName: "ManoonOils",
|
|
||||||
footerText: "support@manoonoils.com"
|
|
||||||
},
|
|
||||||
sr: {
|
|
||||||
subject: "Porudžbina {{orderNumber}} Potvrđena",
|
|
||||||
title: "Porudžbina Potvrđena",
|
|
||||||
greeting: "Poštovani {{customerName}},",
|
|
||||||
body: "Hvala vam na porudžbini! Primili smo je i sada je obrađujemo.",
|
|
||||||
orderLabel: "Porudžbina:",
|
|
||||||
dateLabel: "Datum:",
|
|
||||||
statusLabel: "Status:",
|
|
||||||
itemsLabel: "Artikli",
|
|
||||||
subtotalLabel: "Međuzbir:",
|
|
||||||
shippingLabel: "Dostava:",
|
|
||||||
taxLabel: "Porez:",
|
|
||||||
totalLabel: "Ukupno:",
|
|
||||||
shippingAddressLabel: "Adresa za Dostavu",
|
|
||||||
phoneLabel: "Telefon:",
|
|
||||||
phoneNotProvided: "Nije navedeno",
|
|
||||||
viewOrderButton: "Pogledaj Porudžbinu",
|
|
||||||
thankYou: "Hvala vam!",
|
|
||||||
companyName: "ManoonOils",
|
|
||||||
footerText: "support@manoonoils.com"
|
|
||||||
},
|
|
||||||
de: {
|
|
||||||
subject: "Bestellung {{orderNumber}} Bestätigt",
|
|
||||||
title: "Bestellung Bestätigt",
|
|
||||||
greeting: "Sehr geehrte/r {{customerName}},",
|
|
||||||
body: "Vielen Dank für Ihre Bestellung! Wir haben sie erhalten und bearbeiten sie jetzt.",
|
|
||||||
orderLabel: "Bestellung:",
|
|
||||||
dateLabel: "Datum:",
|
|
||||||
statusLabel: "Status:",
|
|
||||||
itemsLabel: "Artikel",
|
|
||||||
subtotalLabel: "Zwischensumme:",
|
|
||||||
shippingLabel: "Versand:",
|
|
||||||
taxLabel: "Steuer:",
|
|
||||||
totalLabel: "Gesamt:",
|
|
||||||
shippingAddressLabel: "Lieferadresse",
|
|
||||||
phoneLabel: "Telefon:",
|
|
||||||
phoneNotProvided: "Nicht angegeben",
|
|
||||||
viewOrderButton: "Bestellung Ansehen",
|
|
||||||
thankYou: "Vielen Dank!",
|
|
||||||
companyName: "ManoonOils",
|
|
||||||
footerText: "support@manoonoils.com"
|
|
||||||
},
|
|
||||||
fr: {
|
|
||||||
subject: "Commande {{orderNumber}} Confirmée",
|
|
||||||
title: "Commande Confirmée",
|
|
||||||
greeting: "Cher/Chère {{customerName}},",
|
|
||||||
body: "Merci pour votre commande! Nous l'avons reçue et elle est en cours de traitement.",
|
|
||||||
orderLabel: "Commande:",
|
|
||||||
dateLabel: "Date:",
|
|
||||||
statusLabel: "Statut:",
|
|
||||||
itemsLabel: "Articles",
|
|
||||||
subtotalLabel: "Sous-total:",
|
|
||||||
shippingLabel: "Livraison:",
|
|
||||||
taxLabel: "Taxe:",
|
|
||||||
totalLabel: "Total:",
|
|
||||||
shippingAddressLabel: "Adresse de Livraison",
|
|
||||||
phoneLabel: "Téléphone:",
|
|
||||||
phoneNotProvided: "Non fourni",
|
|
||||||
viewOrderButton: "Voir la Commande",
|
|
||||||
thankYou: "Merci!",
|
|
||||||
companyName: "ManoonOils",
|
|
||||||
footerText: "support@manoonoils.com"
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Customer email template
|
|
||||||
const customerEmailTemplate = `
|
|
||||||
<!DOCTYPE html>
|
|
||||||
<html>
|
|
||||||
<head>
|
|
||||||
<meta charset="utf-8">
|
|
||||||
</head>
|
|
||||||
<body style="font-family:-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Helvetica,Arial,sans-serif;background:#f6f6f6;margin:0;padding:20px;">
|
|
||||||
<div style="max-width:600px;margin:0 auto;background:white;padding:40px 20px;">
|
|
||||||
<div style="text-align:center;margin-bottom:30px;">
|
|
||||||
<img src="https://minio-api.nodecrew.me/manoon-media/2024/09/cropped-manoon-logo_256x-1-1.png" width="150" alt="{{companyName}}">
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<h1 style="font-size:24px;font-weight:bold;color:#1a1a1a;margin-bottom:20px;">{{title}}</h1>
|
|
||||||
|
|
||||||
<p style="font-size:16px;color:#333;margin-bottom:10px;">{{greeting}}</p>
|
|
||||||
<p style="font-size:14px;color:#666;margin-bottom:20px;">{{body}}</p>
|
|
||||||
|
|
||||||
<div style="background:#f9f9f9;padding:15px;border-radius:8px;margin-bottom:20px;">
|
|
||||||
<p style="font-size:14px;color:#333;margin:0 0 8px 0;"><strong>{{orderLabel}}</strong> {{orderNumber}}</p>
|
|
||||||
<p style="font-size:14px;color:#333;margin:0 0 8px 0;"><strong>{{dateLabel}}</strong> {{orderDate}}</p>
|
|
||||||
<p style="font-size:14px;color:#333;margin:0;"><strong>{{statusLabel}}</strong> {{orderStatus}}</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<p style="font-size:16px;font-weight:bold;color:#1a1a1a;margin-bottom:10px;">{{itemsLabel}}</p>
|
|
||||||
<div style="margin-bottom:20px;">
|
|
||||||
{{#each items}}
|
|
||||||
<p style="font-size:14px;color:#333;margin:0 0 4px 0;">{{quantity}}x {{name}} - {{price}}</p>
|
|
||||||
{{/each}}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div style="background:#f9f9f9;padding:15px;border-radius:8px;margin-bottom:20px;">
|
|
||||||
<p style="font-size:14px;color:#333;margin:0 0 8px 0;"><strong>{{subtotalLabel}}</strong> {{subtotal}}</p>
|
|
||||||
<p style="font-size:14px;color:#333;margin:0 0 8px 0;"><strong>{{shippingLabel}}</strong> {{shipping}}</p>
|
|
||||||
<p style="font-size:14px;color:#333;margin:0 0 8px 0;"><strong>{{taxLabel}}</strong> {{tax}}</p>
|
|
||||||
<hr style="border-color:#e0e0e0;margin:10px 0;">
|
|
||||||
<p style="font-size:16px;font-weight:bold;color:#1a1a1a;margin:0;"><strong>{{totalLabel}}</strong> {{total}}</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{{#if shippingAddress}}
|
|
||||||
<div style="margin-bottom:20px;">
|
|
||||||
<p style="font-size:16px;font-weight:bold;color:#1a1a1a;margin-bottom:10px;">{{shippingAddressLabel}}</p>
|
|
||||||
<p style="font-size:14px;color:#666;margin:0;white-space:pre-line;">{{shippingAddress}}</p>
|
|
||||||
<p style="font-size:14px;color:#666;margin:8px 0 0 0;"><strong>{{phoneLabel}}</strong> {{#if phone}}{{phone}}{{else}}{{phoneNotProvided}}{{/if}}</p>
|
|
||||||
</div>
|
|
||||||
{{/if}}
|
|
||||||
|
|
||||||
<div style="text-align:center;margin:20px 0;">
|
|
||||||
<a href="https://manoonoils.com" style="display:inline-block;background:#000;color:white;padding:12px 30px;border-radius:4px;text-decoration:none;font-size:14px;font-weight:bold;">{{viewOrderButton}}</a>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<p style="font-size:14px;color:#666;margin-bottom:10px;">{{footerText}}</p>
|
|
||||||
<p style="font-size:14px;font-weight:bold;color:#1a1a1a;">{{thankYou}}</p>
|
|
||||||
|
|
||||||
<div style="margin-top:40px;padding-top:20px;border-top:1px solid #e0e0e0;text-align:center;color:#666;font-size:12px;">
|
|
||||||
<p>{{companyName}}</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
`;
|
|
||||||
|
|
||||||
// Admin email template
|
|
||||||
const adminEmailTemplate = `
|
|
||||||
<!DOCTYPE html>
|
|
||||||
<html>
|
|
||||||
<head>
|
|
||||||
<meta charset="utf-8">
|
|
||||||
</head>
|
|
||||||
<body style="font-family:-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Helvetica,Arial,sans-serif;background:#f6f6f6;margin:0;padding:20px;">
|
|
||||||
<div style="max-width:600px;margin:0 auto;background:white;padding:40px 20px;">
|
|
||||||
<h1 style="font-size:24px;font-weight:bold;color:#1a1a1a;margin-bottom:20px;">{{adminTitle}}</h1>
|
|
||||||
|
|
||||||
<div style="background:#f9f9f9;padding:15px;border-radius:8px;margin-bottom:20px;">
|
|
||||||
<h3 style="font-size:16px;font-weight:bold;color:#1a1a1a;margin:0 0 10px 0;">{{adminOrderLabel}} #{{orderNumber}}</h3>
|
|
||||||
<p style="font-size:14px;color:#333;margin:0 0 8px 0;"><strong>{{dateLabel}}</strong> {{orderDate}}</p>
|
|
||||||
<p style="font-size:14px;color:#333;margin:0 0 8px 0;"><strong>{{statusLabel}}</strong> {{orderStatus}}</p>
|
|
||||||
<p style="font-size:14px;color:#333;margin:0;"><strong>{{paymentLabel}}</strong> {{paymentMethod}}</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div style="background:#f9f9f9;padding:15px;border-radius:8px;margin-bottom:20px;">
|
|
||||||
<h3 style="font-size:16px;font-weight:bold;color:#1a1a1a;margin:0 0 10px 0;">{{customerLabel}}</h3>
|
|
||||||
<p style="font-size:14px;color:#333;margin:0 0 8px 0;"><strong>{{nameLabel}}</strong> {{customerName}}</p>
|
|
||||||
<p style="font-size:14px;color:#333;margin:0 0 8px 0;"><strong>{{emailLabel}}</strong> {{customerEmail}}</p>
|
|
||||||
<p style="font-size:14px;color:#333;margin:0;"><strong>{{phoneLabel}}</strong> {{#if phone}}{{phone}}{{else}}{{phoneNotProvided}}{{/if}}</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<p style="font-size:16px;font-weight:bold;color:#1a1a1a;margin-bottom:10px;">{{itemsLabel}}</p>
|
|
||||||
<div style="margin-bottom:20px;">
|
|
||||||
{{#each items}}
|
|
||||||
<p style="font-size:14px;color:#333;margin:0 0 4px 0;">{{quantity}}x {{name}} - {{price}}</p>
|
|
||||||
{{/each}}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div style="background:#f9f9f9;padding:15px;border-radius:8px;margin-bottom:20px;">
|
|
||||||
<p style="font-size:14px;color:#333;margin:0 0 8px 0;"><strong>{{subtotalLabel}}</strong> {{subtotal}}</p>
|
|
||||||
<p style="font-size:14px;color:#333;margin:0 0 8px 0;"><strong>{{shippingLabel}}</strong> {{shipping}}</p>
|
|
||||||
<p style="font-size:14px;color:#333;margin:0 0 8px 0;"><strong>{{taxLabel}}</strong> {{tax}}</p>
|
|
||||||
<hr style="border-color:#e0e0e0;margin:10px 0;">
|
|
||||||
<p style="font-size:16px;font-weight:bold;color:#1a1a1a;margin:0;"><strong>{{totalLabel}}</strong> {{total}}</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{{#if shippingAddress}}
|
|
||||||
<div style="margin-bottom:20px;">
|
|
||||||
<p style="font-size:16px;font-weight:bold;color:#1a1a1a;margin-bottom:10px;">{{shippingAddressLabel}}</p>
|
|
||||||
<p style="font-size:14px;color:#666;margin:0;white-space:pre-line;">{{shippingAddress}}</p>
|
|
||||||
</div>
|
|
||||||
{{/if}}
|
|
||||||
|
|
||||||
<div style="text-align:center;margin:20px 0;">
|
|
||||||
<a href="https://dashboard.manoonoils.com/orders" style="display:inline-block;background:#000;color:white;padding:12px 30px;border-radius:4px;text-decoration:none;font-size:14px;font-weight:bold;">Dashboard</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
`;
|
|
||||||
|
|
||||||
// Compile templates
|
|
||||||
const compileCustomerEmail = Handlebars.compile(customerEmailTemplate);
|
|
||||||
const compileAdminEmail = Handlebars.compile(adminEmailTemplate);
|
|
||||||
|
|
||||||
// Admin translations (always in English)
|
|
||||||
const adminTranslations = {
|
|
||||||
adminTitle: "New Order! 🎉",
|
|
||||||
adminOrderLabel: "Order",
|
|
||||||
customerLabel: "Customer",
|
|
||||||
paymentLabel: "Payment:",
|
|
||||||
paymentMethod: "Card"
|
|
||||||
};
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate order created emails (customer + admin)
|
||||||
|
*
|
||||||
|
* @param order - Saleor order object
|
||||||
|
* @returns EmailResult with customer and admin email content
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* const { customerSubject, customerHtml, adminSubject, adminHtml } = getOrderCreatedEmails(order);
|
||||||
|
*/
|
||||||
export function getOrderCreatedEmails(order: any) {
|
export function getOrderCreatedEmails(order: any) {
|
||||||
// Detect language
|
return EmailBuilderService.buildOrderCreatedEmails(order, {
|
||||||
const lang = order.languageCode?.toLowerCase() || "en";
|
companyLogoUrl: emailServiceConfig.companyLogoUrl,
|
||||||
const t = translations[lang as keyof typeof translations] || translations.en;
|
companyName: emailServiceConfig.companyName,
|
||||||
|
storefrontUrl: emailServiceConfig.storefrontUrl,
|
||||||
// Format data
|
dashboardUrl: emailServiceConfig.dashboardUrl,
|
||||||
const customerName = order.shippingAddress?.firstName
|
});
|
||||||
? `${order.shippingAddress.firstName} ${order.shippingAddress.lastName || ""}`.trim()
|
|
||||||
: order.userEmail?.split("@")[0] || "Customer";
|
|
||||||
|
|
||||||
const currency = order.total?.gross?.currency || "EUR";
|
|
||||||
|
|
||||||
const items = (order.lines || []).map((line: any) => ({
|
|
||||||
name: line.variant?.product?.name || line.variant?.name || "Product",
|
|
||||||
quantity: line.quantity || 0,
|
|
||||||
price: `${line.totalPrice?.gross?.amount?.toFixed(2) || "0.00"} ${currency}`
|
|
||||||
}));
|
|
||||||
|
|
||||||
const shippingAddress = order.shippingAddress
|
|
||||||
? `${order.shippingAddress.firstName || ""} ${order.shippingAddress.lastName || ""}\n${order.shippingAddress.streetAddress1 || ""}\n${order.shippingAddress.city || ""}, ${order.shippingAddress.postalCode || ""}\n${order.shippingAddress.country?.country || ""}`
|
|
||||||
: "";
|
|
||||||
|
|
||||||
const orderDate = order.created
|
|
||||||
? new Date(order.created).toLocaleString(lang === "sr" ? "sr-RS" : lang === "de" ? "de-DE" : lang === "fr" ? "fr-FR" : "en-US")
|
|
||||||
: new Date().toLocaleString();
|
|
||||||
|
|
||||||
// Customer email data
|
|
||||||
const customerData = {
|
|
||||||
...t,
|
|
||||||
orderNumber: order.number || order.id,
|
|
||||||
customerName,
|
|
||||||
orderDate,
|
|
||||||
orderStatus: order.status || "unfulfilled",
|
|
||||||
items,
|
|
||||||
subtotal: `${order.subtotal?.gross?.amount?.toFixed(2) || "0.00"} ${currency}`,
|
|
||||||
shipping: `${order.shippingPrice?.gross?.amount?.toFixed(2) || "0.00"} ${currency}`,
|
|
||||||
tax: `${order.total?.tax?.amount?.toFixed(2) || "0.00"} ${currency}`,
|
|
||||||
total: `${order.total?.gross?.amount?.toFixed(2) || "0.00"} ${currency}`,
|
|
||||||
shippingAddress,
|
|
||||||
phone: order.shippingAddress?.phone || ""
|
|
||||||
};
|
|
||||||
|
|
||||||
// Admin email data (English)
|
|
||||||
const adminData = {
|
|
||||||
...t,
|
|
||||||
...adminTranslations,
|
|
||||||
orderNumber: order.number || order.id,
|
|
||||||
customerName,
|
|
||||||
customerEmail: order.userEmail || "",
|
|
||||||
orderDate,
|
|
||||||
orderStatus: order.status || "unfulfilled",
|
|
||||||
items,
|
|
||||||
subtotal: `${order.subtotal?.gross?.amount?.toFixed(2) || "0.00"} ${currency}`,
|
|
||||||
shipping: `${order.shippingPrice?.gross?.amount?.toFixed(2) || "0.00"} ${currency}`,
|
|
||||||
tax: `${order.total?.tax?.amount?.toFixed(2) || "0.00"} ${currency}`,
|
|
||||||
total: `${order.total?.gross?.amount?.toFixed(2) || "0.00"} ${currency}`,
|
|
||||||
shippingAddress,
|
|
||||||
phone: order.shippingAddress?.phone || ""
|
|
||||||
};
|
|
||||||
|
|
||||||
return {
|
|
||||||
customerSubject: Handlebars.compile(t.subject)({ orderNumber: order.number || order.id }),
|
|
||||||
customerHtml: compileCustomerEmail(customerData),
|
|
||||||
adminSubject: `New Order! 🎉 #${order.number || order.id}`,
|
|
||||||
adminHtml: compileAdminEmail(adminData)
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Re-export types for consumers
|
||||||
|
export type { EmailResult } from "@/lib/email/types";
|
||||||
|
|||||||
@@ -183,9 +183,13 @@ export function getOrderShippedEmails(order: any, fulfillment?: any) {
|
|||||||
? `https://track.manoonoils.com/${trackingNumber}`
|
? `https://track.manoonoils.com/${trackingNumber}`
|
||||||
: "https://manoonoils.com";
|
: "https://manoonoils.com";
|
||||||
|
|
||||||
|
// Compile greeting with customer name
|
||||||
|
const greeting = Handlebars.compile(t.greeting)({ customerName });
|
||||||
|
|
||||||
// Customer email data
|
// Customer email data
|
||||||
const customerData = {
|
const customerData = {
|
||||||
...t,
|
...t,
|
||||||
|
greeting,
|
||||||
orderNumber: order.number || order.id,
|
orderNumber: order.number || order.id,
|
||||||
customerName,
|
customerName,
|
||||||
orderDate,
|
orderDate,
|
||||||
|
|||||||
@@ -0,0 +1,170 @@
|
|||||||
|
/**
|
||||||
|
* Email Configuration - All configurable values extracted from hardcoded locations
|
||||||
|
* This makes the system configurable without changing code
|
||||||
|
*/
|
||||||
|
|
||||||
|
import type {
|
||||||
|
LanguageCode,
|
||||||
|
EmailTemplateConfig,
|
||||||
|
AdminEmailTemplateConfig,
|
||||||
|
PaymentMethodConfig,
|
||||||
|
EmailServiceConfig
|
||||||
|
} from "./types";
|
||||||
|
|
||||||
|
// Payment method configurations - easily extendable
|
||||||
|
export const paymentMethodConfigs: Record<string, PaymentMethodConfig> = {
|
||||||
|
card: {
|
||||||
|
id: "card",
|
||||||
|
label: "Card",
|
||||||
|
},
|
||||||
|
cod: {
|
||||||
|
id: "cod",
|
||||||
|
label: "Cash on Delivery",
|
||||||
|
},
|
||||||
|
paypal: {
|
||||||
|
id: "paypal",
|
||||||
|
label: "PayPal",
|
||||||
|
},
|
||||||
|
bank_transfer: {
|
||||||
|
id: "bank_transfer",
|
||||||
|
label: "Bank Transfer",
|
||||||
|
},
|
||||||
|
unknown: {
|
||||||
|
id: "unknown",
|
||||||
|
label: "Unknown",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
// Customer email translations - configuration driven
|
||||||
|
export const emailTranslations: Record<LanguageCode, EmailTemplateConfig> = {
|
||||||
|
en: {
|
||||||
|
subject: "Order {{orderNumber}} Confirmed",
|
||||||
|
title: "Order Confirmed",
|
||||||
|
greeting: "Dear {{customerName}},",
|
||||||
|
body: "Thank you for your order! We have received it and it is now being processed.",
|
||||||
|
orderLabel: "Order:",
|
||||||
|
dateLabel: "Date:",
|
||||||
|
statusLabel: "Status:",
|
||||||
|
itemsLabel: "Items",
|
||||||
|
subtotalLabel: "Subtotal:",
|
||||||
|
shippingLabel: "Shipping:",
|
||||||
|
taxLabel: "Tax:",
|
||||||
|
totalLabel: "Total:",
|
||||||
|
shippingAddressLabel: "Shipping Address",
|
||||||
|
phoneLabel: "Phone:",
|
||||||
|
phoneNotProvided: "Not provided",
|
||||||
|
viewOrderButton: "View Order",
|
||||||
|
thankYou: "Thank you!",
|
||||||
|
companyName: "ManoonOils",
|
||||||
|
footerText: "support@manoonoils.com",
|
||||||
|
},
|
||||||
|
sr: {
|
||||||
|
subject: "Porudžbina {{orderNumber}} Potvrđena",
|
||||||
|
title: "Porudžbina Potvrđena",
|
||||||
|
greeting: "Poštovani {{customerName}},",
|
||||||
|
body: "Hvala vam na porudžbini! Primili smo je i sada je obrađujemo.",
|
||||||
|
orderLabel: "Porudžbina:",
|
||||||
|
dateLabel: "Datum:",
|
||||||
|
statusLabel: "Status:",
|
||||||
|
itemsLabel: "Artikli",
|
||||||
|
subtotalLabel: "Međuzbir:",
|
||||||
|
shippingLabel: "Dostava:",
|
||||||
|
taxLabel: "Porez:",
|
||||||
|
totalLabel: "Ukupno:",
|
||||||
|
shippingAddressLabel: "Adresa za Dostavu",
|
||||||
|
phoneLabel: "Telefon:",
|
||||||
|
phoneNotProvided: "Nije navedeno",
|
||||||
|
viewOrderButton: "Pogledaj Porudžbinu",
|
||||||
|
thankYou: "Hvala vam!",
|
||||||
|
companyName: "ManoonOils",
|
||||||
|
footerText: "support@manoonoils.com",
|
||||||
|
},
|
||||||
|
de: {
|
||||||
|
subject: "Bestellung {{orderNumber}} Bestätigt",
|
||||||
|
title: "Bestellung Bestätigt",
|
||||||
|
greeting: "Sehr geehrte/r {{customerName}},",
|
||||||
|
body: "Vielen Dank für Ihre Bestellung! Wir haben sie erhalten und bearbeiten sie jetzt.",
|
||||||
|
orderLabel: "Bestellung:",
|
||||||
|
dateLabel: "Datum:",
|
||||||
|
statusLabel: "Status:",
|
||||||
|
itemsLabel: "Artikel",
|
||||||
|
subtotalLabel: "Zwischensumme:",
|
||||||
|
shippingLabel: "Versand:",
|
||||||
|
taxLabel: "Steuer:",
|
||||||
|
totalLabel: "Gesamt:",
|
||||||
|
shippingAddressLabel: "Lieferadresse",
|
||||||
|
phoneLabel: "Telefon:",
|
||||||
|
phoneNotProvided: "Nicht angegeben",
|
||||||
|
viewOrderButton: "Bestellung Ansehen",
|
||||||
|
thankYou: "Vielen Dank!",
|
||||||
|
companyName: "ManoonOils",
|
||||||
|
footerText: "support@manoonoils.com",
|
||||||
|
},
|
||||||
|
fr: {
|
||||||
|
subject: "Commande {{orderNumber}} Confirmée",
|
||||||
|
title: "Commande Confirmée",
|
||||||
|
greeting: "Cher/Chère {{customerName}},",
|
||||||
|
body: "Merci pour votre commande! Nous l'avons reçue et elle est en cours de traitement.",
|
||||||
|
orderLabel: "Commande:",
|
||||||
|
dateLabel: "Date:",
|
||||||
|
statusLabel: "Statut:",
|
||||||
|
itemsLabel: "Articles",
|
||||||
|
subtotalLabel: "Sous-total:",
|
||||||
|
shippingLabel: "Livraison:",
|
||||||
|
taxLabel: "Taxe:",
|
||||||
|
totalLabel: "Total:",
|
||||||
|
shippingAddressLabel: "Adresse de Livraison",
|
||||||
|
phoneLabel: "Téléphone:",
|
||||||
|
phoneNotProvided: "Non fourni",
|
||||||
|
viewOrderButton: "Voir la Commande",
|
||||||
|
thankYou: "Merci!",
|
||||||
|
companyName: "ManoonOils",
|
||||||
|
footerText: "support@manoonoils.com",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
// Admin email translations - always in English but configurable
|
||||||
|
export const adminEmailTranslations: AdminEmailTemplateConfig = {
|
||||||
|
adminTitle: "New Order! 🎉",
|
||||||
|
adminOrderLabel: "Order",
|
||||||
|
customerLabel: "Customer",
|
||||||
|
nameLabel: "Name:",
|
||||||
|
emailLabel: "Email:",
|
||||||
|
dashboardButton: "Dashboard",
|
||||||
|
};
|
||||||
|
|
||||||
|
// Default payment label for admin emails
|
||||||
|
export const adminPaymentConfig = {
|
||||||
|
paymentLabel: "Payment:",
|
||||||
|
};
|
||||||
|
|
||||||
|
// Email service configuration - loaded from environment or defaults
|
||||||
|
export const emailServiceConfig: EmailServiceConfig = {
|
||||||
|
fromEmail: process.env.EMAIL_FROM_ADDRESS || "support@mail.manoonoils.com",
|
||||||
|
fromName: process.env.EMAIL_FROM_NAME || "Manoon Oils",
|
||||||
|
adminEmails: process.env.ADMIN_EMAILS?.split(",") || [
|
||||||
|
"me@hytham.me",
|
||||||
|
"tamara@hytham.me",
|
||||||
|
],
|
||||||
|
companyName: process.env.COMPANY_NAME || "ManoonOils",
|
||||||
|
companyLogoUrl:
|
||||||
|
process.env.COMPANY_LOGO_URL ||
|
||||||
|
"https://minio-api.nodecrew.me/manoon-media/2024/09/cropped-manoon-logo_256x-1-1.png",
|
||||||
|
dashboardUrl: process.env.DASHBOARD_URL || "https://dashboard.manoonoils.com",
|
||||||
|
storefrontUrl: process.env.STOREFRONT_URL || "https://manoonoils.com",
|
||||||
|
trackingUrl: process.env.TRACKING_URL || "https://track.manoonoils.com",
|
||||||
|
};
|
||||||
|
|
||||||
|
// Locale configurations for date formatting
|
||||||
|
export const localeConfigs: Record<LanguageCode, string> = {
|
||||||
|
en: "en-US",
|
||||||
|
sr: "sr-RS",
|
||||||
|
de: "de-DE",
|
||||||
|
fr: "fr-FR",
|
||||||
|
};
|
||||||
|
|
||||||
|
// Default fallback language
|
||||||
|
export const DEFAULT_LANGUAGE: LanguageCode = "en";
|
||||||
|
|
||||||
|
// Default currency
|
||||||
|
export const DEFAULT_CURRENCY = "EUR";
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
/**
|
||||||
|
* Email Module - Centralized exports for the email system
|
||||||
|
*/
|
||||||
|
|
||||||
|
// Types
|
||||||
|
export * from "./types";
|
||||||
|
|
||||||
|
// Configuration
|
||||||
|
export * from "./config";
|
||||||
|
|
||||||
|
// Services
|
||||||
|
export { PaymentMethodService } from "./services/paymentMethodService";
|
||||||
|
export { OrderTransformerService } from "./services/orderTransformerService";
|
||||||
|
export { EmailBuilderService } from "./services/emailBuilderService";
|
||||||
@@ -0,0 +1,197 @@
|
|||||||
|
/**
|
||||||
|
* Email Builder Service - Compiles email templates with data
|
||||||
|
* Handles template compilation and rendering
|
||||||
|
*/
|
||||||
|
|
||||||
|
import Handlebars from "handlebars";
|
||||||
|
import type { SaleorOrder, EmailResult } from "../types";
|
||||||
|
import { OrderTransformerService } from "./orderTransformerService";
|
||||||
|
import {
|
||||||
|
emailTranslations,
|
||||||
|
adminEmailTranslations,
|
||||||
|
adminPaymentConfig,
|
||||||
|
DEFAULT_LANGUAGE,
|
||||||
|
} from "../config";
|
||||||
|
|
||||||
|
// Customer email template
|
||||||
|
const customerEmailTemplate = `
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
</head>
|
||||||
|
<body style="font-family:-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Helvetica,Arial,sans-serif;background:#f6f6f6;margin:0;padding:20px;">
|
||||||
|
<div style="max-width:600px;margin:0 auto;background:white;padding:40px 20px;">
|
||||||
|
<div style="text-align:center;margin-bottom:30px;">
|
||||||
|
<img src="{{companyLogoUrl}}" width="150" alt="{{companyName}}">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h1 style="font-size:24px;font-weight:bold;color:#1a1a1a;margin-bottom:20px;">{{title}}</h1>
|
||||||
|
|
||||||
|
<p style="font-size:16px;color:#333;margin-bottom:10px;">{{greeting}}</p>
|
||||||
|
<p style="font-size:14px;color:#666;margin-bottom:20px;">{{body}}</p>
|
||||||
|
|
||||||
|
<div style="background:#f9f9f9;padding:15px;border-radius:8px;margin-bottom:20px;">
|
||||||
|
<p style="font-size:14px;color:#333;margin:0 0 8px 0;"><strong>{{orderLabel}}</strong> {{orderNumber}}</p>
|
||||||
|
<p style="font-size:14px;color:#333;margin:0 0 8px 0;"><strong>{{dateLabel}}</strong> {{orderDate}}</p>
|
||||||
|
<p style="font-size:14px;color:#333;margin:0;"><strong>{{statusLabel}}</strong> {{orderStatus}}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p style="font-size:16px;font-weight:bold;color:#1a1a1a;margin-bottom:10px;">{{itemsLabel}}</p>
|
||||||
|
<div style="margin-bottom:20px;">
|
||||||
|
{{#each items}}
|
||||||
|
<p style="font-size:14px;color:#333;margin:0 0 4px 0;">{{quantity}}x {{name}} - {{price}}</p>
|
||||||
|
{{/each}}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style="background:#f9f9f9;padding:15px;border-radius:8px;margin-bottom:20px;">
|
||||||
|
<p style="font-size:14px;color:#333;margin:0 0 8px 0;"><strong>{{subtotalLabel}}</strong> {{subtotal}}</p>
|
||||||
|
<p style="font-size:14px;color:#333;margin:0 0 8px 0;"><strong>{{shippingLabel}}</strong> {{shipping}}</p>
|
||||||
|
<p style="font-size:14px;color:#333;margin:0 0 8px 0;"><strong>{{taxLabel}}</strong> {{tax}}</p>
|
||||||
|
<hr style="border-color:#e0e0e0;margin:10px 0;">
|
||||||
|
<p style="font-size:16px;font-weight:bold;color:#1a1a1a;margin:0;"><strong>{{totalLabel}}</strong> {{total}}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{{#if shippingAddress}}
|
||||||
|
<div style="margin-bottom:20px;">
|
||||||
|
<p style="font-size:16px;font-weight:bold;color:#1a1a1a;margin-bottom:10px;">{{shippingAddressLabel}}</p>
|
||||||
|
<p style="font-size:14px;color:#666;margin:0;white-space:pre-line;">{{shippingAddress}}</p>
|
||||||
|
<p style="font-size:14px;color:#666;margin:8px 0 0 0;"><strong>{{phoneLabel}}</strong> {{#if phone}}{{phone}}{{else}}{{phoneNotProvided}}{{/if}}</p>
|
||||||
|
</div>
|
||||||
|
{{/if}}
|
||||||
|
|
||||||
|
<div style="text-align:center;margin:20px 0;">
|
||||||
|
<a href="{{storefrontUrl}}" style="display:inline-block;background:#000;color:white;padding:12px 30px;border-radius:4px;text-decoration:none;font-size:14px;font-weight:bold;">{{viewOrderButton}}</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p style="font-size:14px;color:#666;margin-bottom:10px;">{{footerText}}</p>
|
||||||
|
<p style="font-size:14px;font-weight:bold;color:#1a1a1a;">{{thankYou}}</p>
|
||||||
|
|
||||||
|
<div style="margin-top:40px;padding-top:20px;border-top:1px solid #e0e0e0;text-align:center;color:#666;font-size:12px;">
|
||||||
|
<p>{{companyName}}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
`;
|
||||||
|
|
||||||
|
// Admin email template
|
||||||
|
const adminEmailTemplate = `
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
</head>
|
||||||
|
<body style="font-family:-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Helvetica,Arial,sans-serif;background:#f6f6f6;margin:0;padding:20px;">
|
||||||
|
<div style="max-width:600px;margin:0 auto;background:white;padding:40px 20px;">
|
||||||
|
<h1 style="font-size:24px;font-weight:bold;color:#1a1a1a;margin-bottom:20px;">{{adminTitle}}</h1>
|
||||||
|
|
||||||
|
<div style="background:#f9f9f9;padding:15px;border-radius:8px;margin-bottom:20px;">
|
||||||
|
<h3 style="font-size:16px;font-weight:bold;color:#1a1a1a;margin:0 0 10px 0;">{{adminOrderLabel}} #{{orderNumber}}</h3>
|
||||||
|
<p style="font-size:14px;color:#333;margin:0 0 8px 0;"><strong>{{dateLabel}}</strong> {{orderDate}}</p>
|
||||||
|
<p style="font-size:14px;color:#333;margin:0 0 8px 0;"><strong>{{statusLabel}}</strong> {{orderStatus}}</p>
|
||||||
|
<p style="font-size:14px;color:#333;margin:0;"><strong>{{paymentLabel}}</strong> {{paymentMethod}}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style="background:#f9f9f9;padding:15px;border-radius:8px;margin-bottom:20px;">
|
||||||
|
<h3 style="font-size:16px;font-weight:bold;color:#1a1a1a;margin:0 0 10px 0;">{{customerLabel}}</h3>
|
||||||
|
<p style="font-size:14px;color:#333;margin:0 0 8px 0;"><strong>{{nameLabel}}</strong> {{customerName}}</p>
|
||||||
|
<p style="font-size:14px;color:#333;margin:0 0 8px 0;"><strong>{{emailLabel}}</strong> {{customerEmail}}</p>
|
||||||
|
<p style="font-size:14px;color:#333;margin:0;"><strong>{{phoneLabel}}</strong> {{#if phone}}{{phone}}{{else}}{{phoneNotProvided}}{{/if}}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p style="font-size:16px;font-weight:bold;color:#1a1a1a;margin-bottom:10px;">{{itemsLabel}}</p>
|
||||||
|
<div style="margin-bottom:20px;">
|
||||||
|
{{#each items}}
|
||||||
|
<p style="font-size:14px;color:#333;margin:0 0 4px 0;">{{quantity}}x {{name}} - {{price}}</p>
|
||||||
|
{{/each}}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style="background:#f9f9f9;padding:15px;border-radius:8px;margin-bottom:20px;">
|
||||||
|
<p style="font-size:14px;color:#333;margin:0 0 8px 0;"><strong>{{subtotalLabel}}</strong> {{subtotal}}</p>
|
||||||
|
<p style="font-size:14px;color:#333;margin:0 0 8px 0;"><strong>{{shippingLabel}}</strong> {{shipping}}</p>
|
||||||
|
<p style="font-size:14px;color:#333;margin:0 0 8px 0;"><strong>{{taxLabel}}</strong> {{tax}}</p>
|
||||||
|
<hr style="border-color:#e0e0e0;margin:10px 0;">
|
||||||
|
<p style="font-size:16px;font-weight:bold;color:#1a1a1a;margin:0;"><strong>{{totalLabel}}</strong> {{total}}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{{#if shippingAddress}}
|
||||||
|
<div style="margin-bottom:20px;">
|
||||||
|
<p style="font-size:16px;font-weight:bold;color:#1a1a1a;margin-bottom:10px;">{{shippingAddressLabel}}</p>
|
||||||
|
<p style="font-size:14px;color:#666;margin:0;white-space:pre-line;">{{shippingAddress}}</p>
|
||||||
|
</div>
|
||||||
|
{{/if}}
|
||||||
|
|
||||||
|
<div style="text-align:center;margin:20px 0;">
|
||||||
|
<a href="{{dashboardUrl}}" style="display:inline-block;background:#000;color:white;padding:12px 30px;border-radius:4px;text-decoration:none;font-size:14px;font-weight:bold;">{{dashboardButton}}</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
`;
|
||||||
|
|
||||||
|
export class EmailBuilderService {
|
||||||
|
private static compileCustomerEmail = Handlebars.compile(customerEmailTemplate);
|
||||||
|
private static compileAdminEmail = Handlebars.compile(adminEmailTemplate);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Build order created emails (customer + admin)
|
||||||
|
*/
|
||||||
|
static buildOrderCreatedEmails(
|
||||||
|
order: SaleorOrder,
|
||||||
|
config: {
|
||||||
|
companyLogoUrl: string;
|
||||||
|
companyName: string;
|
||||||
|
storefrontUrl: string;
|
||||||
|
dashboardUrl: string;
|
||||||
|
}
|
||||||
|
): EmailResult {
|
||||||
|
const languageCode = OrderTransformerService.getLanguageCode(order);
|
||||||
|
const translations = emailTranslations[languageCode] || emailTranslations[DEFAULT_LANGUAGE];
|
||||||
|
|
||||||
|
// Compile greeting and subject with Handlebars
|
||||||
|
const greeting = Handlebars.compile(translations.greeting)({
|
||||||
|
customerName: OrderTransformerService.extractCustomerName(order),
|
||||||
|
});
|
||||||
|
const subject = Handlebars.compile(translations.subject)({
|
||||||
|
orderNumber: order.number || order.id,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Build customer email data
|
||||||
|
const customerData = {
|
||||||
|
...OrderTransformerService.transformToEmailData(order, translations),
|
||||||
|
greeting,
|
||||||
|
subject,
|
||||||
|
companyLogoUrl: config.companyLogoUrl,
|
||||||
|
storefrontUrl: config.storefrontUrl,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Build admin email data
|
||||||
|
const adminTranslationsRecord: Record<string, string> = {
|
||||||
|
adminTitle: adminEmailTranslations.adminTitle,
|
||||||
|
adminOrderLabel: adminEmailTranslations.adminOrderLabel,
|
||||||
|
customerLabel: adminEmailTranslations.customerLabel,
|
||||||
|
nameLabel: adminEmailTranslations.nameLabel,
|
||||||
|
emailLabel: adminEmailTranslations.emailLabel,
|
||||||
|
dashboardButton: adminEmailTranslations.dashboardButton,
|
||||||
|
};
|
||||||
|
|
||||||
|
const adminData = {
|
||||||
|
...OrderTransformerService.transformToAdminEmailData(
|
||||||
|
order,
|
||||||
|
translations,
|
||||||
|
adminTranslationsRecord
|
||||||
|
),
|
||||||
|
...adminPaymentConfig,
|
||||||
|
dashboardUrl: config.dashboardUrl,
|
||||||
|
companyLogoUrl: config.companyLogoUrl,
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
customerSubject: subject,
|
||||||
|
customerHtml: this.compileCustomerEmail(customerData),
|
||||||
|
adminSubject: `${adminEmailTranslations.adminTitle} #${order.number || order.id}`,
|
||||||
|
adminHtml: this.compileAdminEmail(adminData),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,167 @@
|
|||||||
|
/**
|
||||||
|
* Order Transformer Service - Transforms Saleor order data into normalized email data
|
||||||
|
* All transformation logic abstracted here instead of inline in templates
|
||||||
|
*/
|
||||||
|
|
||||||
|
import type {
|
||||||
|
SaleorOrder,
|
||||||
|
EmailData,
|
||||||
|
AdminEmailData,
|
||||||
|
LanguageCode,
|
||||||
|
OrderLine
|
||||||
|
} from "../types";
|
||||||
|
import {
|
||||||
|
localeConfigs,
|
||||||
|
DEFAULT_LANGUAGE,
|
||||||
|
DEFAULT_CURRENCY
|
||||||
|
} from "../config";
|
||||||
|
import { PaymentMethodService } from "./paymentMethodService";
|
||||||
|
|
||||||
|
export class OrderTransformerService {
|
||||||
|
/**
|
||||||
|
* Extract and format customer name from order
|
||||||
|
*/
|
||||||
|
static extractCustomerName(order: SaleorOrder): string {
|
||||||
|
if (order.shippingAddress?.firstName) {
|
||||||
|
return `${order.shippingAddress.firstName} ${order.shippingAddress.lastName || ""}`.trim();
|
||||||
|
}
|
||||||
|
if (order.billingAddress?.firstName) {
|
||||||
|
return `${order.billingAddress.firstName} ${order.billingAddress.lastName || ""}`.trim();
|
||||||
|
}
|
||||||
|
if (order.userEmail) {
|
||||||
|
return order.userEmail.split("@")[0];
|
||||||
|
}
|
||||||
|
return "Customer";
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Format shipping address for display
|
||||||
|
*/
|
||||||
|
static formatShippingAddress(order: SaleorOrder): string {
|
||||||
|
const address = order.shippingAddress;
|
||||||
|
if (!address) return "";
|
||||||
|
|
||||||
|
const parts = [
|
||||||
|
`${address.firstName || ""} ${address.lastName || ""}`.trim(),
|
||||||
|
address.streetAddress1,
|
||||||
|
address.streetAddress2,
|
||||||
|
`${address.city || ""}${address.postalCode ? `, ${address.postalCode}` : ""}`,
|
||||||
|
address.country?.country || address.country?.code,
|
||||||
|
].filter(Boolean);
|
||||||
|
|
||||||
|
return parts.join("\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extract phone number with fallback
|
||||||
|
*/
|
||||||
|
static extractPhone(order: SaleorOrder): string {
|
||||||
|
return (
|
||||||
|
order.shippingAddress?.phone ||
|
||||||
|
order.billingAddress?.phone ||
|
||||||
|
""
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Format order items for email
|
||||||
|
*/
|
||||||
|
static formatItems(order: SaleorOrder): Array<{
|
||||||
|
name: string;
|
||||||
|
quantity: number;
|
||||||
|
price: string;
|
||||||
|
}> {
|
||||||
|
const currency = order.total?.gross?.currency || DEFAULT_CURRENCY;
|
||||||
|
|
||||||
|
return (order.lines || []).map((line: OrderLine) => {
|
||||||
|
const productName = line.variant?.product?.name || line.variant?.name || "Product";
|
||||||
|
const amount = line.totalPrice?.gross?.amount?.toFixed(2) || "0.00";
|
||||||
|
|
||||||
|
return {
|
||||||
|
name: productName,
|
||||||
|
quantity: line.quantity || 0,
|
||||||
|
price: `${amount} ${currency}`,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Format money amount with currency
|
||||||
|
*/
|
||||||
|
static formatMoney(amount: number | undefined, currency: string): string {
|
||||||
|
const value = amount?.toFixed(2) || "0.00";
|
||||||
|
return `${value} ${currency}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Format order date for display
|
||||||
|
*/
|
||||||
|
static formatOrderDate(order: SaleorOrder, languageCode: LanguageCode): string {
|
||||||
|
const date = order.created ? new Date(order.created) : new Date();
|
||||||
|
const locale = localeConfigs[languageCode] || localeConfigs[DEFAULT_LANGUAGE];
|
||||||
|
return date.toLocaleString(locale);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get language code with fallback
|
||||||
|
*/
|
||||||
|
static getLanguageCode(order: SaleorOrder): LanguageCode {
|
||||||
|
const lang = order.languageCode?.toLowerCase() as LanguageCode;
|
||||||
|
if (["en", "sr", "de", "fr"].includes(lang)) {
|
||||||
|
return lang;
|
||||||
|
}
|
||||||
|
return DEFAULT_LANGUAGE;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Transform order into email data structure
|
||||||
|
*/
|
||||||
|
static transformToEmailData(
|
||||||
|
order: SaleorOrder,
|
||||||
|
translations: Record<string, any>
|
||||||
|
): EmailData {
|
||||||
|
const languageCode = this.getLanguageCode(order);
|
||||||
|
const currency = order.total?.gross?.currency || DEFAULT_CURRENCY;
|
||||||
|
const customerName = this.extractCustomerName(order);
|
||||||
|
const paymentMethod = PaymentMethodService.detectPaymentMethod(order);
|
||||||
|
|
||||||
|
return {
|
||||||
|
...translations,
|
||||||
|
orderNumber: order.number || order.id,
|
||||||
|
customerName,
|
||||||
|
greeting: "", // Will be compiled by email builder
|
||||||
|
subject: "", // Will be compiled by email builder
|
||||||
|
orderDate: this.formatOrderDate(order, languageCode),
|
||||||
|
orderStatus: order.status || "unfulfilled",
|
||||||
|
items: this.formatItems(order),
|
||||||
|
subtotal: this.formatMoney(order.subtotal?.gross?.amount, currency),
|
||||||
|
shipping: this.formatMoney(order.shippingPrice?.gross?.amount, currency),
|
||||||
|
tax: this.formatMoney(order.total?.tax?.amount, currency),
|
||||||
|
total: this.formatMoney(order.total?.gross?.amount, currency),
|
||||||
|
shippingAddress: this.formatShippingAddress(order),
|
||||||
|
phone: this.extractPhone(order),
|
||||||
|
paymentMethod: PaymentMethodService.getPaymentLabel(paymentMethod),
|
||||||
|
paymentLabel: "Payment:", // Can be moved to config if needed per language
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Transform order into admin email data structure
|
||||||
|
*/
|
||||||
|
static transformToAdminEmailData(
|
||||||
|
order: SaleorOrder,
|
||||||
|
translations: Record<string, any>,
|
||||||
|
adminTranslations: Record<string, string>
|
||||||
|
): Record<string, any> {
|
||||||
|
const baseData = this.transformToEmailData(order, translations);
|
||||||
|
const paymentMethod = PaymentMethodService.detectPaymentMethod(order);
|
||||||
|
|
||||||
|
return {
|
||||||
|
...baseData,
|
||||||
|
...adminTranslations,
|
||||||
|
customerEmail: order.userEmail || "",
|
||||||
|
paymentMethod: PaymentMethodService.getPaymentLabel(paymentMethod),
|
||||||
|
paymentLabel: "Payment:",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,71 @@
|
|||||||
|
/**
|
||||||
|
* Payment Method Service - Detects payment method from order data
|
||||||
|
* Abstracted logic for determining how an order was paid
|
||||||
|
*/
|
||||||
|
|
||||||
|
import type { SaleorOrder, PaymentMethod } from "../types";
|
||||||
|
import { paymentMethodConfigs } from "../config";
|
||||||
|
|
||||||
|
export class PaymentMethodService {
|
||||||
|
/**
|
||||||
|
* Detect payment method from order transactions and metadata
|
||||||
|
*/
|
||||||
|
static detectPaymentMethod(order: SaleorOrder): PaymentMethod {
|
||||||
|
// Check transactions first
|
||||||
|
if (order.transactions && order.transactions.length > 0) {
|
||||||
|
const transaction = order.transactions[0];
|
||||||
|
const message = transaction.message?.toLowerCase() || "";
|
||||||
|
|
||||||
|
// Check transaction events for payment type
|
||||||
|
const event = transaction.events?.[0];
|
||||||
|
const eventMessage = event?.message?.toLowerCase() || "";
|
||||||
|
const eventType = event?.type?.toLowerCase() || "";
|
||||||
|
|
||||||
|
const combinedText = `${message} ${eventMessage} ${eventType}`;
|
||||||
|
|
||||||
|
if (combinedText.includes("cash") || combinedText.includes("cod")) {
|
||||||
|
return "cod";
|
||||||
|
}
|
||||||
|
if (combinedText.includes("card") || combinedText.includes("stripe")) {
|
||||||
|
return "card";
|
||||||
|
}
|
||||||
|
if (combinedText.includes("paypal")) {
|
||||||
|
return "paypal";
|
||||||
|
}
|
||||||
|
if (combinedText.includes("bank") || combinedText.includes("transfer")) {
|
||||||
|
return "bank_transfer";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check metadata for payment method
|
||||||
|
if (order.metadata) {
|
||||||
|
const paymentMeta = order.metadata.find(
|
||||||
|
(m) => m.key === "payment_method" || m.key === "paymentMethod"
|
||||||
|
);
|
||||||
|
if (paymentMeta) {
|
||||||
|
const value = paymentMeta.value.toLowerCase();
|
||||||
|
if (value.includes("cash") || value.includes("cod")) return "cod";
|
||||||
|
if (value.includes("card")) return "card";
|
||||||
|
if (value.includes("paypal")) return "paypal";
|
||||||
|
if (value.includes("bank")) return "bank_transfer";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Default to unknown if no payment method detected
|
||||||
|
return "unknown";
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get display label for a payment method
|
||||||
|
*/
|
||||||
|
static getPaymentLabel(method: PaymentMethod): string {
|
||||||
|
return paymentMethodConfigs[method]?.label || paymentMethodConfigs.unknown.label;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get full payment method configuration
|
||||||
|
*/
|
||||||
|
static getPaymentConfig(method: PaymentMethod) {
|
||||||
|
return paymentMethodConfigs[method] || paymentMethodConfigs.unknown;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,169 @@
|
|||||||
|
/**
|
||||||
|
* Email Types - Type definitions for the email system
|
||||||
|
* All data structures are strictly typed with no hardcoded values
|
||||||
|
*/
|
||||||
|
|
||||||
|
export type LanguageCode = "en" | "sr" | "de" | "fr";
|
||||||
|
export type PaymentMethod = "card" | "cod" | "paypal" | "bank_transfer" | "unknown";
|
||||||
|
export type OrderStatus = "draft" | "unconfirmed" | "unfulfilled" | "partially_fulfilled" | "fulfilled" | "canceled";
|
||||||
|
|
||||||
|
export interface Money {
|
||||||
|
amount: number;
|
||||||
|
currency: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Address {
|
||||||
|
firstName?: string | null;
|
||||||
|
lastName?: string | null;
|
||||||
|
streetAddress1?: string | null;
|
||||||
|
streetAddress2?: string | null;
|
||||||
|
city?: string | null;
|
||||||
|
postalCode?: string | null;
|
||||||
|
country?: {
|
||||||
|
country?: string | null;
|
||||||
|
code?: string | null;
|
||||||
|
} | null;
|
||||||
|
phone?: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface OrderLine {
|
||||||
|
quantity: number;
|
||||||
|
variant?: {
|
||||||
|
name?: string;
|
||||||
|
product?: {
|
||||||
|
name?: string;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
totalPrice?: {
|
||||||
|
gross?: Money;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Transaction {
|
||||||
|
id?: string;
|
||||||
|
message?: string;
|
||||||
|
externalUrl?: string;
|
||||||
|
events?: TransactionEvent[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TransactionEvent {
|
||||||
|
id?: string;
|
||||||
|
message?: string;
|
||||||
|
type?: string;
|
||||||
|
amount?: Money;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SaleorOrder {
|
||||||
|
id: string;
|
||||||
|
number?: string | null;
|
||||||
|
created?: string | null;
|
||||||
|
status?: string | null;
|
||||||
|
languageCode?: string | null;
|
||||||
|
userEmail?: string | null;
|
||||||
|
shippingAddress?: Address | null;
|
||||||
|
billingAddress?: Address | null;
|
||||||
|
lines?: OrderLine[] | null;
|
||||||
|
subtotal?: {
|
||||||
|
gross?: Money | null;
|
||||||
|
} | null;
|
||||||
|
shippingPrice?: {
|
||||||
|
gross?: Money | null;
|
||||||
|
} | null;
|
||||||
|
total?: {
|
||||||
|
gross?: Money | null;
|
||||||
|
tax?: Money | null;
|
||||||
|
} | null;
|
||||||
|
transactions?: Transaction[] | null;
|
||||||
|
metadata?: Array<{
|
||||||
|
key: string;
|
||||||
|
value: string;
|
||||||
|
}> | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PaymentMethodConfig {
|
||||||
|
id: PaymentMethod;
|
||||||
|
label: string;
|
||||||
|
icon?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface EmailTemplateConfig {
|
||||||
|
subject: string;
|
||||||
|
title: string;
|
||||||
|
greeting: string;
|
||||||
|
body: string;
|
||||||
|
orderLabel: string;
|
||||||
|
dateLabel: string;
|
||||||
|
statusLabel: string;
|
||||||
|
itemsLabel: string;
|
||||||
|
subtotalLabel: string;
|
||||||
|
shippingLabel: string;
|
||||||
|
taxLabel: string;
|
||||||
|
totalLabel: string;
|
||||||
|
shippingAddressLabel: string;
|
||||||
|
phoneLabel: string;
|
||||||
|
phoneNotProvided: string;
|
||||||
|
viewOrderButton: string;
|
||||||
|
thankYou: string;
|
||||||
|
companyName: string;
|
||||||
|
footerText: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AdminEmailTemplateConfig {
|
||||||
|
adminTitle: string;
|
||||||
|
adminOrderLabel: string;
|
||||||
|
customerLabel: string;
|
||||||
|
nameLabel: string;
|
||||||
|
emailLabel: string;
|
||||||
|
dashboardButton: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface EmailData {
|
||||||
|
orderNumber: string;
|
||||||
|
customerName: string;
|
||||||
|
greeting: string;
|
||||||
|
subject: string;
|
||||||
|
orderDate: string;
|
||||||
|
orderStatus: string;
|
||||||
|
items: Array<{
|
||||||
|
name: string;
|
||||||
|
quantity: number;
|
||||||
|
price: string;
|
||||||
|
}>;
|
||||||
|
subtotal: string;
|
||||||
|
shipping: string;
|
||||||
|
tax: string;
|
||||||
|
total: string;
|
||||||
|
shippingAddress: string;
|
||||||
|
phone: string;
|
||||||
|
paymentMethod?: string;
|
||||||
|
paymentLabel?: string;
|
||||||
|
[key: string]: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AdminEmailData extends EmailData {
|
||||||
|
customerEmail: string;
|
||||||
|
adminTitle: string;
|
||||||
|
adminOrderLabel: string;
|
||||||
|
customerLabel: string;
|
||||||
|
nameLabel: string;
|
||||||
|
emailLabel: string;
|
||||||
|
dashboardButton: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface EmailResult {
|
||||||
|
customerSubject: string;
|
||||||
|
customerHtml: string;
|
||||||
|
adminSubject: string;
|
||||||
|
adminHtml: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface EmailServiceConfig {
|
||||||
|
fromEmail: string;
|
||||||
|
fromName: string;
|
||||||
|
adminEmails: string[];
|
||||||
|
companyName: string;
|
||||||
|
companyLogoUrl: string;
|
||||||
|
dashboardUrl: string;
|
||||||
|
storefrontUrl: string;
|
||||||
|
trackingUrl?: string;
|
||||||
|
}
|
||||||
@@ -4,7 +4,7 @@ import {
|
|||||||
OrderCancelledWebhookPayloadFragment,
|
OrderCancelledWebhookPayloadFragment,
|
||||||
} from "@/generated/graphql";
|
} from "@/generated/graphql";
|
||||||
import { saleorApp } from "@/saleor-app";
|
import { saleorApp } from "@/saleor-app";
|
||||||
import { getOrderCancelledEmails } from "@/lib/email-templates";
|
import { getOrderCancelledEmails } from "@/lib/email-templates/order-cancelled";
|
||||||
import { Resend } from "resend";
|
import { Resend } from "resend";
|
||||||
|
|
||||||
const resend = new Resend(process.env.RESEND_API_KEY);
|
const resend = new Resend(process.env.RESEND_API_KEY);
|
||||||
|
|||||||
Reference in New Issue
Block a user