diff --git a/src/app/api/webhooks/saleor/route.ts b/src/app/api/webhooks/saleor/route.ts deleted file mode 100644 index f49c978..0000000 --- a/src/app/api/webhooks/saleor/route.ts +++ /dev/null @@ -1,295 +0,0 @@ -import { NextRequest, NextResponse } from "next/server"; -import { orderNotificationService } from "@/lib/services/OrderNotificationService"; -import { analyticsService } from "@/lib/services/AnalyticsService"; - -// Saleor webhook payload interfaces (snake_case) -interface SaleorLineItemPayload { - id: string; - product_name: string; - variant_name?: string; - quantity: number; - total_price_gross_amount: string; - currency: string; -} - -interface SaleorAddressPayload { - first_name?: string; - last_name?: string; - street_address_1?: string; - street_address_2?: string; - city?: string; - postal_code?: string; - country?: string; - phone?: string; -} - -interface SaleorOrderPayload { - id: string; - number: number; - user_email: string; - first_name?: string; - last_name?: string; - billing_address?: SaleorAddressPayload; - shipping_address?: SaleorAddressPayload; - lines: SaleorLineItemPayload[]; - total_gross_amount: string; - shipping_price_gross_amount?: string; - channel: { currency_code: string }; - currency?: string; - language_code?: string; - metadata?: Record; -} - -// Internal camelCase interfaces -interface OrderItem { - id: string; - productName: string; - variantName?: string; - quantity: number; - totalPrice: { - gross: { amount: number; currency: string }; - }; -} - -interface OrderAddress { - firstName?: string; - lastName?: string; - streetAddress1?: string; - streetAddress2?: string; - city?: string; - postalCode?: string; - country?: string; - phone?: string; -} - -interface Order { - id: string; - number: string; - userEmail: string; - user?: { firstName?: string; lastName?: string }; - billingAddress?: OrderAddress; - shippingAddress?: OrderAddress; - lines: OrderItem[]; - total: { gross: { amount: number; currency: string } }; - languageCode?: string; - metadata?: Array<{ key: string; value: string }>; -} - -const SUPPORTED_EVENTS = [ - "ORDER_CREATED", - "ORDER_CONFIRMED", - "ORDER_FULLY_PAID", - "ORDER_CANCELLED", - "ORDER_FULFILLED", -]; - -// Convert Saleor payload to internal format -function convertPayloadToOrder(payload: SaleorOrderPayload): Order { - return { - id: payload.id, - number: String(payload.number), - userEmail: payload.user_email, - user: payload.first_name || payload.last_name ? { - firstName: payload.first_name, - lastName: payload.last_name, - } : undefined, - billingAddress: payload.billing_address ? { - firstName: payload.billing_address.first_name, - lastName: payload.billing_address.last_name, - streetAddress1: payload.billing_address.street_address_1, - streetAddress2: payload.billing_address.street_address_2, - city: payload.billing_address.city, - postalCode: payload.billing_address.postal_code, - country: payload.billing_address.country, - phone: payload.billing_address.phone, - } : undefined, - shippingAddress: payload.shipping_address ? { - firstName: payload.shipping_address.first_name, - lastName: payload.shipping_address.last_name, - streetAddress1: payload.shipping_address.street_address_1, - streetAddress2: payload.shipping_address.street_address_2, - city: payload.shipping_address.city, - postalCode: payload.shipping_address.postal_code, - country: payload.shipping_address.country, - phone: payload.shipping_address.phone, - } : undefined, - lines: payload.lines.map((line) => ({ - id: line.id, - productName: line.product_name, - variantName: line.variant_name, - quantity: line.quantity, - totalPrice: { - gross: { - amount: parseInt(line.total_price_gross_amount), - currency: line.currency || payload.channel.currency_code, - }, - }, - })), - total: { - gross: { - amount: parseInt(payload.total_gross_amount), - currency: payload.channel.currency_code, - }, - }, - languageCode: payload.language_code?.toUpperCase(), - metadata: payload.metadata ? Object.entries(payload.metadata).map(([key, value]) => ({ key, value })) : undefined, - }; -} - -// Extract tracking number from metadata -function getTrackingInfo(order: Order): { trackingNumber?: string; trackingUrl?: string } { - if (!order.metadata) return {}; - - const trackingMeta = order.metadata.find((m) => m.key === "trackingNumber"); - const trackingUrlMeta = order.metadata.find((m) => m.key === "trackingUrl"); - - return { - trackingNumber: trackingMeta?.value, - trackingUrl: trackingUrlMeta?.value, - }; -} - -// Extract cancellation reason from metadata -function getCancellationReason(order: Order): string | undefined { - if (!order.metadata) return undefined; - const reasonMeta = order.metadata.find((m) => m.key === "cancellationReason"); - return reasonMeta?.value; -} - -// Webhook handlers -async function handleOrderConfirmed(order: Order, eventType: string) { - const itemCount = order.lines.reduce((sum, line) => sum + line.quantity, 0); - - // Send customer email only for ORDER_CONFIRMED (not ORDER_CREATED) - if (eventType === "ORDER_CONFIRMED") { - await orderNotificationService.sendOrderConfirmation(order); - - // Track revenue and order analytics only on ORDER_CONFIRMED (not ORDER_CREATED) - // This prevents duplicate tracking when both events fire for the same order - analyticsService.trackOrderReceived({ - orderId: order.id, - orderNumber: order.number, - total: order.total.gross.amount, - currency: order.total.gross.currency, - itemCount, - customerEmail: order.userEmail, - eventType, - }); - - analyticsService.trackRevenue({ - amount: order.total.gross.amount, - currency: order.total.gross.currency, - orderId: order.id, - orderNumber: order.number, - }); - } - - // Send admin notification for both events - await orderNotificationService.sendOrderConfirmationToAdmin(order); -} - -async function handleOrderFulfilled(order: Order) { - const { trackingNumber, trackingUrl } = getTrackingInfo(order); - - await orderNotificationService.sendOrderShipped(order, trackingNumber, trackingUrl); - await orderNotificationService.sendOrderShippedToAdmin(order, trackingNumber, trackingUrl); -} - -async function handleOrderCancelled(order: Order) { - const reason = getCancellationReason(order); - - await orderNotificationService.sendOrderCancelled(order, reason); - await orderNotificationService.sendOrderCancelledToAdmin(order, reason); -} - -async function handleOrderFullyPaid(order: Order) { - await orderNotificationService.sendOrderPaid(order); - await orderNotificationService.sendOrderPaidToAdmin(order); -} - -// Main webhook processor -async function processWebhook(event: string, order: Order) { - console.log(`Processing webhook event: ${event} for order ${order.id}`); - - switch (event) { - case "ORDER_CREATED": - case "ORDER_CONFIRMED": - await handleOrderConfirmed(order, event); - break; - case "ORDER_FULFILLED": - await handleOrderFulfilled(order); - break; - case "ORDER_CANCELLED": - await handleOrderCancelled(order); - break; - case "ORDER_FULLY_PAID": - await handleOrderFullyPaid(order); - break; - default: - console.log(`Unsupported event: ${event}`); - } -} - -export async function POST(request: NextRequest) { - try { - console.log("=== WEBHOOK RECEIVED ==="); - console.log("Timestamp:", new Date().toISOString()); - - const body = await request.json(); - const headers = request.headers; - - const event = headers.get("saleor-event") as string; - const domain = headers.get("saleor-domain"); - - console.log(`Received webhook: ${event} from ${domain}`); - - // Parse payload - let orderPayload: SaleorOrderPayload | null = null; - if (Array.isArray(body) && body.length > 0) { - orderPayload = body[0] as SaleorOrderPayload; - } else if (body.data && Array.isArray(body.data)) { - orderPayload = body.data[0] as SaleorOrderPayload; - } - - if (!orderPayload) { - console.error("No order found in webhook payload"); - return NextResponse.json({ error: "No order in payload" }, { status: 400 }); - } - - console.log("Order:", { - id: orderPayload.id, - number: orderPayload.number, - email: orderPayload.user_email, - }); - - if (!event) { - return NextResponse.json({ error: "Missing saleor-event header" }, { status: 400 }); - } - - const normalizedEvent = event.toUpperCase(); - - if (!SUPPORTED_EVENTS.includes(normalizedEvent)) { - console.log(`Event ${event} not supported, skipping`); - return NextResponse.json({ success: true, message: "Event not supported" }); - } - - const order = convertPayloadToOrder(orderPayload); - await processWebhook(normalizedEvent, order); - - return NextResponse.json({ success: true }); - } catch (error) { - console.error("Webhook processing error:", error); - return NextResponse.json( - { error: "Internal server error", details: String(error) }, - { status: 500 } - ); - } -} - -export async function GET() { - return NextResponse.json({ - status: "ok", - message: "Saleor webhook endpoint is active", - supportedEvents: SUPPORTED_EVENTS, - }); -} diff --git a/src/app/api/webhooks/saleor/utils.ts b/src/app/api/webhooks/saleor/utils.ts deleted file mode 100644 index 527a4bd..0000000 --- a/src/app/api/webhooks/saleor/utils.ts +++ /dev/null @@ -1,6 +0,0 @@ -export function formatPrice(amount: number, currency: string): string { - return new Intl.NumberFormat("sr-RS", { - style: "currency", - currency: currency, - }).format(amount); -} diff --git a/src/emails/BaseLayout.tsx b/src/emails/BaseLayout.tsx deleted file mode 100644 index 3bc4a78..0000000 --- a/src/emails/BaseLayout.tsx +++ /dev/null @@ -1,98 +0,0 @@ -import { - Body, - Button, - Container, - Head, - Hr, - Html, - Img, - Link, - Preview, - Section, - Text, -} from "@react-email/components"; - -interface BaseLayoutProps { - children: React.ReactNode; - previewText: string; - language: string; - siteUrl: string; -} - -const translations: Record = { - sr: { - footer: "ManoonOils - Prirodna kozmetika | www.manoonoils.com", - company: "ManoonOils", - }, - en: { - footer: "ManoonOils - Natural Cosmetics | www.manoonoils.com", - company: "ManoonOils", - }, - de: { - footer: "ManoonOils - Natürliche Kosmetik | www.manoonoils.com", - company: "ManoonOils", - }, - fr: { - footer: "ManoonOils - Cosmétiques Naturels | www.manoonoils.com", - company: "ManoonOils", - }, -}; - -export function BaseLayout({ children, previewText, language, siteUrl }: BaseLayoutProps) { - const t = translations[language] || translations.en; - - return ( - - - {previewText} - - -
- ManoonOils -
- {children} -
- {t.footer} -
-
- - - ); -} - -const styles = { - body: { - backgroundColor: "#f6f6f6", - fontFamily: - '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif', - }, - container: { - backgroundColor: "#ffffff", - margin: "0 auto", - padding: "40px 20px", - maxWidth: "600px", - }, - logoSection: { - textAlign: "center" as const, - marginBottom: "30px", - }, - logo: { - margin: "0 auto", - }, - footer: { - marginTop: "40px", - paddingTop: "20px", - borderTop: "1px solid #e0e0e0", - }, - footerText: { - color: "#666666", - fontSize: "12px", - textAlign: "center" as const, - }, -}; diff --git a/src/emails/OrderCancelled.tsx b/src/emails/OrderCancelled.tsx deleted file mode 100644 index e508fbc..0000000 --- a/src/emails/OrderCancelled.tsx +++ /dev/null @@ -1,237 +0,0 @@ -import { Button, Hr, Section, Text } from "@react-email/components"; -import { BaseLayout } from "./BaseLayout"; - -interface OrderItem { - id: string; - name: string; - quantity: number; - price: string; -} - -interface OrderCancelledProps { - language: string; - orderId: string; - orderNumber: string; - customerName: string; - items: OrderItem[]; - total: string; - reason?: string; - siteUrl: string; -} - -const translations: Record< - string, - { - title: string; - preview: string; - greeting: string; - orderCancelled: string; - items: string; - total: string; - reason: string; - questions: string; - } -> = { - sr: { - title: "Vaša narudžbina je otkazana", - preview: "Vaša narudžbina je otkazana", - greeting: "Poštovani {name},", - orderCancelled: - "Vaša narudžbina je otkazana. Ako niste zatražili otkazivanje, molimo kontaktirajte nas što pre.", - items: "Artikli", - total: "Ukupno", - reason: "Razlog", - questions: "Imate pitanja? Pišite nam na support@manoonoils.com", - }, - en: { - title: "Your Order Has Been Cancelled", - preview: "Your order has been cancelled", - greeting: "Dear {name},", - orderCancelled: - "Your order has been cancelled. If you did not request this cancellation, please contact us as soon as possible.", - items: "Items", - total: "Total", - reason: "Reason", - questions: "Questions? Email us at support@manoonoils.com", - }, - de: { - title: "Ihre Bestellung wurde storniert", - preview: "Ihre Bestellung wurde storniert", - greeting: "Sehr geehrte/r {name},", - orderCancelled: - "Ihre Bestellung wurde storniert. Wenn Sie diese Stornierung nicht angefordert haben, kontaktieren Sie uns bitte so schnell wie möglich.", - items: "Artikel", - total: "Gesamt", - reason: "Grund", - questions: "Fragen? Schreiben Sie uns an support@manoonoils.com", - }, - fr: { - title: "Votre commande a été annulée", - preview: "Votre commande a été annulée", - greeting: "Cher(e) {name},", - orderCancelled: - "Votre commande a été annulée. Si vous n'avez pas demandé cette annulation, veuillez nous contacter dès que possible.", - items: "Articles", - total: "Total", - reason: "Raison", - questions: "Questions? Écrivez-nous à support@manoonoils.com", - }, -}; - -export function OrderCancelled({ - language = "en", - orderId, - orderNumber, - customerName, - items, - total, - reason, - siteUrl, -}: OrderCancelledProps) { - const t = translations[language] || translations.en; - - return ( - - {t.title} - {t.greeting.replace("{name}", customerName)} - {t.orderCancelled} - -
- - Order Number: {orderNumber} - - {reason && ( - - {t.reason}: {reason} - - )} -
- -
- {t.items} -
- {items.map((item) => ( -
- - {item.quantity}x {item.name} - - {item.price} -
- ))} -
-
- {t.total}: - {total} -
-
- -
- -
- - {t.questions} -
- ); -} - -const styles = { - title: { - fontSize: "24px", - fontWeight: "bold" as const, - color: "#dc2626", - marginBottom: "20px", - }, - greeting: { - fontSize: "16px", - color: "#333333", - marginBottom: "10px", - }, - text: { - fontSize: "14px", - color: "#666666", - marginBottom: "20px", - }, - orderInfo: { - backgroundColor: "#fef2f2", - padding: "15px", - borderRadius: "8px", - marginBottom: "20px", - }, - orderNumber: { - fontSize: "14px", - color: "#333333", - margin: "0 0 5px 0", - }, - reason: { - fontSize: "14px", - color: "#991b1b", - margin: "0", - }, - itemsSection: { - marginBottom: "20px", - }, - sectionTitle: { - fontSize: "16px", - fontWeight: "bold" as const, - color: "#1a1a1a", - marginBottom: "10px", - }, - hr: { - borderColor: "#e0e0e0", - margin: "10px 0", - }, - itemRow: { - display: "flex" as const, - justifyContent: "space-between" as const, - padding: "8px 0", - }, - itemName: { - fontSize: "14px", - color: "#666666", - margin: "0", - textDecoration: "line-through", - }, - itemPrice: { - fontSize: "14px", - color: "#666666", - margin: "0", - textDecoration: "line-through", - }, - totalRow: { - display: "flex" as const, - justifyContent: "space-between" as const, - padding: "8px 0", - }, - totalLabel: { - fontSize: "16px", - fontWeight: "bold" as const, - color: "#666666", - margin: "0", - }, - totalValue: { - fontSize: "16px", - fontWeight: "bold" as const, - color: "#666666", - margin: "0", - textDecoration: "line-through", - }, - buttonSection: { - textAlign: "center" as const, - marginBottom: "20px", - }, - button: { - backgroundColor: "#000000", - color: "#ffffff", - padding: "12px 30px", - borderRadius: "4px", - fontSize: "14px", - fontWeight: "bold" as const, - textDecoration: "none", - }, - questions: { - fontSize: "14px", - color: "#666666", - }, -}; diff --git a/src/emails/OrderConfirmation.tsx b/src/emails/OrderConfirmation.tsx deleted file mode 100644 index 8cce158..0000000 --- a/src/emails/OrderConfirmation.tsx +++ /dev/null @@ -1,394 +0,0 @@ -import { Button, Hr, Section, Text } from "@react-email/components"; -import { BaseLayout } from "./BaseLayout"; - -interface OrderItem { - id: string; - name: string; - quantity: number; - price: string; -} - -interface OrderConfirmationProps { - language: string; - orderId: string; - orderNumber: string; - customerEmail: string; - customerName: string; - items: OrderItem[]; - total: string; - shippingAddress?: string; - billingAddress?: string; - phone?: string; - siteUrl: string; - dashboardUrl?: string; - isAdmin?: boolean; -} - -const translations: Record< - string, - { - title: string; - preview: string; - greeting: string; - orderReceived: string; - orderNumber: string; - items: string; - quantity: string; - total: string; - shippingTo: string; - questions: string; - thankYou: string; - adminTitle: string; - adminPreview: string; - adminGreeting: string; - adminMessage: string; - customerLabel: string; - customerEmailLabel: string; - billingAddressLabel: string; - phoneLabel: string; - viewDashboard: string; - } -> = { - sr: { - title: "Potvrda narudžbine", - preview: "Vaša narudžbina je potvrđena", - greeting: "Poštovani {name},", - orderReceived: "Zahvaljujemo se na Vašoj narudžbini! Primili smo je i sada je u pripremi.", - orderNumber: "Broj narudžbine", - items: "Artikli", - quantity: "Količina", - total: "Ukupno", - shippingTo: "Adresa za dostavu", - questions: "Imate pitanja? Pišite nam na support@manoonoils.com", - thankYou: "Hvala Vam što kupujete kod nas!", - adminTitle: "Nova narudžbina!", - adminPreview: "Nova narudžbina je primljena", - adminGreeting: "Čestitamo na prodaji!", - adminMessage: "Nova narudžbina je upravo primljena. Detalji su ispod:", - customerLabel: "Kupac", - customerEmailLabel: "Email kupca", - billingAddressLabel: "Adresa za naplatu", - phoneLabel: "Telefon", - viewDashboard: "Pogledaj u Dashboardu", - }, - en: { - title: "Order Confirmation", - preview: "Your order has been confirmed", - greeting: "Dear {name},", - orderReceived: - "Thank you for your order! We have received it and it is now being processed.", - orderNumber: "Order number", - items: "Items", - quantity: "Quantity", - total: "Total", - shippingTo: "Shipping address", - questions: "Questions? Email us at support@manoonoils.com", - thankYou: "Thank you for shopping with us!", - adminTitle: "New Order! 🎉", - adminPreview: "A new order has been received", - adminGreeting: "Congratulations on the sale!", - adminMessage: "A new order has just been placed. Details below:", - customerLabel: "Customer", - customerEmailLabel: "Customer Email", - billingAddressLabel: "Billing Address", - phoneLabel: "Phone", - viewDashboard: "View in Dashboard", - }, - de: { - title: "Bestellungsbestätigung", - preview: "Ihre Bestellung wurde bestätigt", - greeting: "Sehr geehrte/r {name},", - orderReceived: - "Vielen Dank für Ihre Bestellung! Wir haben sie erhalten und sie wird nun bearbeitet.", - orderNumber: "Bestellnummer", - items: "Artikel", - quantity: "Menge", - total: "Gesamt", - shippingTo: "Lieferadresse", - questions: "Fragen? Schreiben Sie uns an support@manoonoils.com", - thankYou: "Vielen Dank für Ihren Einkauf!", - adminTitle: "Neue Bestellung! 🎉", - adminPreview: "Eine neue Bestellung wurde erhalten", - adminGreeting: "Glückwunsch zum Verkauf!", - adminMessage: "Eine neue Bestellung wurde soeben aufgegeben. Details unten:", - customerLabel: "Kunde", - customerEmailLabel: "Kunden-E-Mail", - billingAddressLabel: "Rechnungsadresse", - phoneLabel: "Telefon", - viewDashboard: "Im Dashboard anzeigen", - }, - fr: { - title: "Confirmation de commande", - preview: "Votre commande a été confirmée", - greeting: "Cher(e) {name},", - orderReceived: - "Merci pour votre commande! Nous l'avons reçue et elle est en cours de traitement.", - orderNumber: "Numéro de commande", - items: "Articles", - quantity: "Quantité", - total: "Total", - shippingTo: "Adresse de livraison", - questions: "Questions? Écrivez-nous à support@manoonoils.com", - thankYou: "Merci d'avoir Magasiné avec nous!", - adminTitle: "Nouvelle commande! 🎉", - adminPreview: "Une nouvelle commande a été reçue", - adminGreeting: "Félicitations pour la vente!", - adminMessage: "Une nouvelle commande vient d'être passée. Détails ci-dessous:", - customerLabel: "Client", - customerEmailLabel: "Email du client", - billingAddressLabel: "Adresse de facturation", - phoneLabel: "Téléphone", - viewDashboard: "Voir dans le Dashboard", - }, -}; - -export function OrderConfirmation({ - language = "en", - orderId, - orderNumber, - customerEmail, - customerName, - items, - total, - shippingAddress, - billingAddress, - phone, - siteUrl, - dashboardUrl, - isAdmin = false, -}: OrderConfirmationProps) { - const t = translations[language] || translations.en; - - // For admin emails, always use English - const adminT = translations["en"]; - - if (isAdmin) { - return ( - - {adminT.adminTitle} - {adminT.adminGreeting} - {adminT.adminMessage} - -
- - {adminT.orderNumber}: {orderNumber} - - - {adminT.customerLabel}: {customerName} - - - {adminT.customerEmailLabel}: {customerEmail} - - {phone && ( - - {adminT.phoneLabel}: {phone} - - )} -
- -
- {adminT.items} -
- {items.map((item) => ( -
- - {item.quantity}x {item.name} - - {item.price} -
- ))} -
-
- {adminT.total}: - {total} -
-
- - {shippingAddress && ( -
- {adminT.shippingTo} - {shippingAddress} -
- )} - - {billingAddress && ( -
- {adminT.billingAddressLabel} - {billingAddress} -
- )} - -
- -
-
- ); - } - - return ( - - {t.title} - {t.greeting.replace("{name}", customerName)} - {t.orderReceived} - -
- - {t.orderNumber}: {orderNumber} - -
- -
- {t.items} -
- {items.map((item) => ( -
- - {item.quantity}x {item.name} - - {item.price} -
- ))} -
-
- {t.total}: - {total} -
-
- - {shippingAddress && ( -
- {t.shippingTo} - {shippingAddress} -
- )} - -
- -
- - {t.questions} - {t.thankYou} -
- ); -} - -const styles = { - title: { - fontSize: "24px", - fontWeight: "bold" as const, - color: "#1a1a1a", - marginBottom: "20px", - }, - greeting: { - fontSize: "16px", - color: "#333333", - marginBottom: "10px", - }, - text: { - fontSize: "14px", - color: "#666666", - marginBottom: "20px", - }, - orderInfo: { - backgroundColor: "#f9f9f9", - padding: "15px", - borderRadius: "8px", - marginBottom: "20px", - }, - orderNumber: { - fontSize: "14px", - color: "#333333", - margin: "0 0 8px 0", - }, - customerInfo: { - fontSize: "14px", - color: "#333333", - margin: "0 0 4px 0", - }, - itemsSection: { - marginBottom: "20px", - }, - sectionTitle: { - fontSize: "16px", - fontWeight: "bold" as const, - color: "#1a1a1a", - marginBottom: "10px", - }, - hr: { - borderColor: "#e0e0e0", - margin: "10px 0", - }, - itemRow: { - display: "flex" as const, - justifyContent: "space-between" as const, - padding: "8px 0", - }, - itemName: { - fontSize: "14px", - color: "#333333", - margin: "0", - }, - itemPrice: { - fontSize: "14px", - color: "#333333", - margin: "0", - }, - totalRow: { - display: "flex" as const, - justifyContent: "space-between" as const, - padding: "8px 0", - }, - totalLabel: { - fontSize: "16px", - fontWeight: "bold" as const, - color: "#1a1a1a", - margin: "0", - }, - totalValue: { - fontSize: "16px", - fontWeight: "bold" as const, - color: "#1a1a1a", - margin: "0", - }, - shippingSection: { - marginBottom: "20px", - }, - shippingAddress: { - fontSize: "14px", - color: "#666666", - margin: "0", - }, - buttonSection: { - textAlign: "center" as const, - marginBottom: "20px", - }, - button: { - backgroundColor: "#000000", - color: "#ffffff", - padding: "12px 30px", - borderRadius: "4px", - fontSize: "14px", - fontWeight: "bold" as const, - textDecoration: "none", - }, - questions: { - fontSize: "14px", - color: "#666666", - marginBottom: "10px", - }, - thankYou: { - fontSize: "14px", - fontWeight: "bold" as const, - color: "#1a1a1a", - }, -}; diff --git a/src/emails/OrderPaid.tsx b/src/emails/OrderPaid.tsx deleted file mode 100644 index a276e37..0000000 --- a/src/emails/OrderPaid.tsx +++ /dev/null @@ -1,253 +0,0 @@ -import { Button, Hr, Section, Text } from "@react-email/components"; -import { BaseLayout } from "./BaseLayout"; - -interface OrderItem { - id: string; - name: string; - quantity: number; - price: string; -} - -interface OrderPaidProps { - language: string; - orderId: string; - orderNumber: string; - customerName: string; - items: OrderItem[]; - total: string; - siteUrl: string; -} - -const translations: Record< - string, - { - title: string; - preview: string; - greeting: string; - orderPaid: string; - items: string; - total: string; - nextSteps: string; - nextStepsText: string; - questions: string; - } -> = { - sr: { - title: "Plaćanje je primljeno!", - preview: "Vaša uplata je zabeležena", - greeting: "Poštovani {name},", - orderPaid: - "Plaćanje za vašu narudžbinu je primljeno. Hvala vam! Narudžbina će uskoro biti spremna za slanje.", - items: "Artikli", - total: "Ukupno", - nextSteps: "Šta dalje?", - nextStepsText: - "Primićete još jedan email kada vaša narudžbina bude poslata. Možete očekivati dostavu u roku od 3-5 radnih dana.", - questions: "Imate pitanja? Pišite nam na support@manoonoils.com", - }, - en: { - title: "Payment Received!", - preview: "Your payment has been recorded", - greeting: "Dear {name},", - orderPaid: - "Payment for your order has been received. Thank you! Your order will be prepared for shipping soon.", - items: "Items", - total: "Total", - nextSteps: "What's next?", - nextStepsText: - "You will receive another email when your order ships. You can expect delivery within 3-5 business days.", - questions: "Questions? Email us at support@manoonoils.com", - }, - de: { - title: "Zahlung erhalten!", - preview: "Ihre Zahlung wurde verbucht", - greeting: "Sehr geehrte/r {name},", - orderPaid: - "Zahlung für Ihre Bestellung ist eingegangen. Vielen Dank! Ihre Bestellung wird bald für den Versand vorbereitet.", - items: "Artikel", - total: "Gesamt", - nextSteps: "Was kommt als nächstes?", - nextStepsText: - "Sie erhalten eine weitere E-Mail, wenn Ihre Bestellung versandt wird. Die Lieferung erfolgt innerhalb von 3-5 Werktagen.", - questions: "Fragen? Schreiben Sie uns an support@manoonoils.com", - }, - fr: { - title: "Paiement reçu!", - preview: "Votre paiement a été enregistré", - greeting: "Cher(e) {name},", - orderPaid: - "Le paiement de votre commande a été reçu. Merci! Votre commande sera bientôt prête à être expédiée.", - items: "Articles", - total: "Total", - nextSteps: "Et ensuite?", - nextStepsText: - "Vous recevrez un autre email lorsque votre commande sera expédiée. Vous pouvez vous attendre à une livraison dans 3-5 jours ouvrables.", - questions: "Questions? Écrivez-nous à support@manoonoils.com", - }, -}; - -export function OrderPaid({ - language = "en", - orderId, - orderNumber, - customerName, - items, - total, - siteUrl, -}: OrderPaidProps) { - const t = translations[language] || translations.en; - - return ( - - {t.title} - {t.greeting.replace("{name}", customerName)} - {t.orderPaid} - -
- - Order Number: {orderNumber} - -
- -
- {t.items} -
- {items.map((item) => ( -
- - {item.quantity}x {item.name} - - {item.price} -
- ))} -
-
- {t.total}: - {total} -
-
- -
- {t.nextSteps} - {t.nextStepsText} -
- -
- -
- - {t.questions} -
- ); -} - -const styles = { - title: { - fontSize: "24px", - fontWeight: "bold" as const, - color: "#16a34a", - marginBottom: "20px", - }, - greeting: { - fontSize: "16px", - color: "#333333", - marginBottom: "10px", - }, - text: { - fontSize: "14px", - color: "#666666", - marginBottom: "20px", - }, - orderInfo: { - backgroundColor: "#f0fdf4", - padding: "15px", - borderRadius: "8px", - marginBottom: "20px", - }, - orderNumber: { - fontSize: "14px", - color: "#333333", - margin: "0", - }, - itemsSection: { - marginBottom: "20px", - }, - sectionTitle: { - fontSize: "16px", - fontWeight: "bold" as const, - color: "#1a1a1a", - marginBottom: "10px", - }, - hr: { - borderColor: "#e0e0e0", - margin: "10px 0", - }, - itemRow: { - display: "flex" as const, - justifyContent: "space-between" as const, - padding: "8px 0", - }, - itemName: { - fontSize: "14px", - color: "#333333", - margin: "0", - }, - itemPrice: { - fontSize: "14px", - color: "#333333", - margin: "0", - }, - totalRow: { - display: "flex" as const, - justifyContent: "space-between" as const, - padding: "8px 0", - }, - totalLabel: { - fontSize: "16px", - fontWeight: "bold" as const, - color: "#1a1a1a", - margin: "0", - }, - totalValue: { - fontSize: "16px", - fontWeight: "bold" as const, - color: "#1a1a1a", - margin: "0", - }, - nextSteps: { - backgroundColor: "#f9f9f9", - padding: "15px", - borderRadius: "8px", - marginBottom: "20px", - }, - nextStepsTitle: { - fontSize: "14px", - fontWeight: "bold" as const, - color: "#1a1a1a", - marginBottom: "5px", - }, - nextStepsText: { - fontSize: "14px", - color: "#666666", - margin: "0", - }, - buttonSection: { - textAlign: "center" as const, - marginBottom: "20px", - }, - button: { - backgroundColor: "#000000", - color: "#ffffff", - padding: "12px 30px", - borderRadius: "4px", - fontSize: "14px", - fontWeight: "bold" as const, - textDecoration: "none", - }, - questions: { - fontSize: "14px", - color: "#666666", - }, -}; diff --git a/src/emails/OrderShipped.tsx b/src/emails/OrderShipped.tsx deleted file mode 100644 index 51f3f02..0000000 --- a/src/emails/OrderShipped.tsx +++ /dev/null @@ -1,193 +0,0 @@ -import { Button, Hr, Section, Text } from "@react-email/components"; -import { BaseLayout } from "./BaseLayout"; - -interface OrderItem { - id: string; - name: string; - quantity: number; - price: string; -} - -interface OrderShippedProps { - language: string; - orderId: string; - orderNumber: string; - customerName: string; - items: OrderItem[]; - trackingNumber?: string; - trackingUrl?: string; - siteUrl: string; -} - -const translations: Record< - string, - { - title: string; - preview: string; - greeting: string; - orderShipped: string; - tracking: string; - items: string; - questions: string; - } -> = { - sr: { - title: "Vaša narudžbina je poslata!", - preview: "Vaša narudžbina je na putu", - greeting: "Poštovani {name},", - orderShipped: - "Odlične vesti! Vaša narudžbina je poslata i uskoro će stići na vašu adresu.", - tracking: "Praćenje pošiljke", - items: "Artikli", - questions: "Imate pitanja? Pišite nam na support@manoonoils.com", - }, - en: { - title: "Your Order Has Shipped!", - preview: "Your order is on its way", - greeting: "Dear {name},", - orderShipped: - "Great news! Your order has been shipped and will arrive at your address soon.", - tracking: "Track your shipment", - items: "Items", - questions: "Questions? Email us at support@manoonoils.com", - }, - de: { - title: "Ihre Bestellung wurde versendet!", - preview: "Ihre Bestellung ist unterwegs", - greeting: "Sehr geehrte/r {name},", - orderShipped: - "Großartige Neuigkeiten! Ihre Bestellung wurde versandt und wird in Kürze bei Ihnen eintreffen.", - tracking: "Sendung verfolgen", - items: "Artikel", - questions: "Fragen? Schreiben Sie uns an support@manoonoils.com", - }, - fr: { - title: "Votre commande a été expédiée!", - preview: "Votre commande est en route", - greeting: "Cher(e) {name},", - orderShipped: - "Bonne nouvelle! Votre commande a été expédiée et arrivera bientôt à votre adresse.", - tracking: "Suivre votre envoi", - items: "Articles", - questions: "Questions? Écrivez-nous à support@manoonoils.com", - }, -}; - -export function OrderShipped({ - language = "en", - orderId, - orderNumber, - customerName, - items, - trackingNumber, - trackingUrl, - siteUrl, -}: OrderShippedProps) { - const t = translations[language] || translations.en; - - return ( - - {t.title} - {t.greeting.replace("{name}", customerName)} - {t.orderShipped} - - {trackingNumber && ( -
- {t.tracking} - {trackingUrl ? ( - - ) : ( - {trackingNumber} - )} -
- )} - -
- {t.items} -
- {items.map((item) => ( -
- - {item.quantity}x {item.name} - - {item.price} -
- ))} -
- - {t.questions} -
- ); -} - -const styles = { - title: { - fontSize: "24px", - fontWeight: "bold" as const, - color: "#1a1a1a", - marginBottom: "20px", - }, - greeting: { - fontSize: "16px", - color: "#333333", - marginBottom: "10px", - }, - text: { - fontSize: "14px", - color: "#666666", - marginBottom: "20px", - }, - trackingSection: { - backgroundColor: "#f9f9f9", - padding: "15px", - borderRadius: "8px", - marginBottom: "20px", - }, - sectionTitle: { - fontSize: "16px", - fontWeight: "bold" as const, - color: "#1a1a1a", - marginBottom: "10px", - }, - trackingNumber: { - fontSize: "14px", - color: "#333333", - margin: "0", - }, - trackingButton: { - backgroundColor: "#000000", - color: "#ffffff", - padding: "10px 20px", - borderRadius: "4px", - fontSize: "14px", - textDecoration: "none", - }, - itemsSection: { - marginBottom: "20px", - }, - hr: { - borderColor: "#e0e0e0", - margin: "10px 0", - }, - itemRow: { - display: "flex" as const, - justifyContent: "space-between" as const, - padding: "8px 0", - }, - itemName: { - fontSize: "14px", - color: "#333333", - margin: "0", - }, - itemPrice: { - fontSize: "14px", - color: "#333333", - margin: "0", - }, - questions: { - fontSize: "14px", - color: "#666666", - }, -}; diff --git a/src/emails/index.ts b/src/emails/index.ts deleted file mode 100644 index ea354ff..0000000 --- a/src/emails/index.ts +++ /dev/null @@ -1,5 +0,0 @@ -export { BaseLayout } from "./BaseLayout"; -export { OrderConfirmation } from "./OrderConfirmation"; -export { OrderShipped } from "./OrderShipped"; -export { OrderCancelled } from "./OrderCancelled"; -export { OrderPaid } from "./OrderPaid"; diff --git a/src/lib/services/OrderNotificationService.ts b/src/lib/services/OrderNotificationService.ts deleted file mode 100644 index 0b6a032..0000000 --- a/src/lib/services/OrderNotificationService.ts +++ /dev/null @@ -1,357 +0,0 @@ -import { sendEmailToCustomer, sendEmailToAdmin } from "@/lib/resend"; -import { OrderConfirmation } from "@/emails/OrderConfirmation"; -import { OrderShipped } from "@/emails/OrderShipped"; -import { OrderCancelled } from "@/emails/OrderCancelled"; -import { OrderPaid } from "@/emails/OrderPaid"; -import { formatPrice } from "@/app/api/webhooks/saleor/utils"; - -const SITE_URL = process.env.NEXT_PUBLIC_SITE_URL || "https://dev.manoonoils.com"; -const DASHBOARD_URL = process.env.DASHBOARD_URL || "https://dashboard.manoonoils.com"; - -// Translation helper for email subjects -function getOrderConfirmationSubject(language: string, orderNumber: string): string { - const subjects: Record = { - sr: `Potvrda narudžbine #${orderNumber}`, - de: `Bestellbestätigung #${orderNumber}`, - fr: `Confirmation de commande #${orderNumber}`, - en: `Order Confirmation #${orderNumber}`, - }; - return subjects[language] || subjects.en; -} - -function getOrderShippedSubject(language: string, orderNumber: string): string { - const subjects: Record = { - sr: `Vaša narudžbina #${orderNumber} je poslata!`, - de: `Ihre Bestellung #${orderNumber} wurde versendet!`, - fr: `Votre commande #${orderNumber} a été expédiée!`, - en: `Your Order #${orderNumber} Has Shipped!`, - }; - return subjects[language] || subjects.en; -} - -function getOrderCancelledSubject(language: string, orderNumber: string): string { - const subjects: Record = { - sr: `Vaša narudžbina #${orderNumber} je otkazana`, - de: `Ihre Bestellung #${orderNumber} wurde storniert`, - fr: `Votre commande #${orderNumber} a été annulée`, - en: `Your Order #${orderNumber} Has Been Cancelled`, - }; - return subjects[language] || subjects.en; -} - -function getOrderPaidSubject(language: string, orderNumber: string): string { - const subjects: Record = { - sr: `Plaćanje za narudžbinu #${orderNumber} je primljeno!`, - de: `Zahlung für Bestellung #${orderNumber} erhalten!`, - fr: `Paiement reçu pour la commande #${orderNumber}!`, - en: `Payment Received for Order #${orderNumber}!`, - }; - return subjects[language] || subjects.en; -} - -// Interfaces -interface OrderItem { - id: string; - productName: string; - variantName?: string; - quantity: number; - totalPrice: { - gross: { - amount: number; - currency: string; - }; - }; -} - -interface OrderAddress { - firstName?: string; - lastName?: string; - streetAddress1?: string; - streetAddress2?: string; - city?: string; - postalCode?: string; - country?: string; - phone?: string; -} - -interface Order { - id: string; - number: string; - userEmail: string; - user?: { - firstName?: string; - lastName?: string; - }; - billingAddress?: OrderAddress; - shippingAddress?: OrderAddress; - lines: OrderItem[]; - total: { - gross: { - amount: number; - currency: string; - }; - }; - languageCode?: string; - metadata?: Array<{ key: string; value: string }>; -} - -interface OrderEmailItem { - id: string; - name: string; - quantity: number; - price: string; -} - -class OrderNotificationService { - private static instance: OrderNotificationService; - - static getInstance(): OrderNotificationService { - if (!OrderNotificationService.instance) { - OrderNotificationService.instance = new OrderNotificationService(); - } - return OrderNotificationService.instance; - } - - private parseOrderItems(lines: OrderItem[], currency: string): OrderEmailItem[] { - return lines.map((line) => ({ - id: line.id, - name: line.variantName ? `${line.productName} (${line.variantName})` : line.productName, - quantity: line.quantity, - price: formatPrice(line.totalPrice.gross.amount, currency), - })); - } - - private formatAddress(address?: OrderAddress): string { - if (!address) return ""; - const parts = [ - address.firstName, - address.lastName, - address.streetAddress1, - address.streetAddress2, - address.city, - address.postalCode, - address.country, - ].filter(Boolean); - return parts.join(", "); - } - - private getCustomerName(order: Order): string { - if (order.user?.firstName || order.user?.lastName) { - return `${order.user.firstName || ""} ${order.user.lastName || ""}`.trim(); - } - if (order.shippingAddress?.firstName || order.shippingAddress?.lastName) { - return `${order.shippingAddress.firstName || ""} ${order.shippingAddress.lastName || ""}`.trim(); - } - return "Customer"; - } - - private getCustomerLanguage(order: Order): string { - const LANGUAGE_CODE_MAP: Record = { - SR: "sr", - EN: "en", - DE: "de", - FR: "fr", - }; - - if (order.languageCode && LANGUAGE_CODE_MAP[order.languageCode]) { - return LANGUAGE_CODE_MAP[order.languageCode]; - } - if (order.metadata) { - const langMeta = order.metadata.find((m) => m.key === "language"); - if (langMeta && LANGUAGE_CODE_MAP[langMeta.value.toUpperCase()]) { - return LANGUAGE_CODE_MAP[langMeta.value.toUpperCase()]; - } - } - return "en"; - } - - async sendOrderConfirmation(order: Order): Promise { - const language = this.getCustomerLanguage(order); - const currency = order.total.gross.currency; - const customerName = this.getCustomerName(order); - const customerEmail = order.userEmail; - const phone = order.shippingAddress?.phone || order.billingAddress?.phone; - - await sendEmailToCustomer({ - to: customerEmail, - subject: getOrderConfirmationSubject(language, order.number), - react: OrderConfirmation({ - language, - orderId: order.id, - orderNumber: order.number, - customerEmail, - customerName, - items: this.parseOrderItems(order.lines, currency), - total: formatPrice(order.total.gross.amount, currency), - shippingAddress: this.formatAddress(order.shippingAddress), - siteUrl: SITE_URL, - }), - language, - idempotencyKey: `order-confirmed/${order.id}`, - }); - } - - async sendOrderConfirmationToAdmin(order: Order): Promise { - const currency = order.total.gross.currency; - const customerName = this.getCustomerName(order); - const customerEmail = order.userEmail; - const phone = order.shippingAddress?.phone || order.billingAddress?.phone; - - await sendEmailToAdmin({ - subject: `🎉 New Order #${order.number} - ${formatPrice(order.total.gross.amount, currency)}`, - react: OrderConfirmation({ - language: "en", - orderId: order.id, - orderNumber: order.number, - customerEmail, - customerName, - items: this.parseOrderItems(order.lines, currency), - total: formatPrice(order.total.gross.amount, currency), - shippingAddress: this.formatAddress(order.shippingAddress), - billingAddress: this.formatAddress(order.billingAddress), - phone, - siteUrl: SITE_URL, - dashboardUrl: DASHBOARD_URL, - isAdmin: true, - }), - eventType: "ORDER_CONFIRMED", - orderId: order.id, - }); - } - - async sendOrderShipped(order: Order, trackingNumber?: string, trackingUrl?: string): Promise { - const language = this.getCustomerLanguage(order); - const currency = order.total.gross.currency; - const customerName = this.getCustomerName(order); - const customerEmail = order.userEmail; - - await sendEmailToCustomer({ - to: customerEmail, - subject: getOrderShippedSubject(language, order.number), - react: OrderShipped({ - language, - orderId: order.id, - orderNumber: order.number, - customerName, - items: this.parseOrderItems(order.lines, currency), - trackingNumber, - trackingUrl, - siteUrl: SITE_URL, - }), - language, - idempotencyKey: `order-fulfilled/${order.id}`, - }); - } - - async sendOrderShippedToAdmin(order: Order, trackingNumber?: string, trackingUrl?: string): Promise { - const currency = order.total.gross.currency; - const customerName = this.getCustomerName(order); - - await sendEmailToAdmin({ - subject: `Order Shipped #${order.number} - ${customerName}`, - react: OrderShipped({ - language: "en", - orderId: order.id, - orderNumber: order.number, - customerName, - items: this.parseOrderItems(order.lines, currency), - trackingNumber, - trackingUrl, - siteUrl: SITE_URL, - }), - eventType: "ORDER_FULFILLED", - orderId: order.id, - }); - } - - async sendOrderCancelled(order: Order, reason?: string): Promise { - const language = this.getCustomerLanguage(order); - const currency = order.total.gross.currency; - const customerName = this.getCustomerName(order); - const customerEmail = order.userEmail; - - await sendEmailToCustomer({ - to: customerEmail, - subject: getOrderCancelledSubject(language, order.number), - react: OrderCancelled({ - language, - orderId: order.id, - orderNumber: order.number, - customerName, - items: this.parseOrderItems(order.lines, currency), - total: formatPrice(order.total.gross.amount, currency), - reason, - siteUrl: SITE_URL, - }), - language, - idempotencyKey: `order-cancelled/${order.id}`, - }); - } - - async sendOrderCancelledToAdmin(order: Order, reason?: string): Promise { - const currency = order.total.gross.currency; - const customerName = this.getCustomerName(order); - - await sendEmailToAdmin({ - subject: `Order Cancelled #${order.number} - ${customerName}`, - react: OrderCancelled({ - language: "en", - orderId: order.id, - orderNumber: order.number, - customerName, - items: this.parseOrderItems(order.lines, currency), - total: formatPrice(order.total.gross.amount, currency), - reason, - siteUrl: SITE_URL, - }), - eventType: "ORDER_CANCELLED", - orderId: order.id, - }); - } - - async sendOrderPaid(order: Order): Promise { - const language = this.getCustomerLanguage(order); - const currency = order.total.gross.currency; - const customerName = this.getCustomerName(order); - const customerEmail = order.userEmail; - - await sendEmailToCustomer({ - to: customerEmail, - subject: getOrderPaidSubject(language, order.number), - react: OrderPaid({ - language, - orderId: order.id, - orderNumber: order.number, - customerName, - items: this.parseOrderItems(order.lines, currency), - total: formatPrice(order.total.gross.amount, currency), - siteUrl: SITE_URL, - }), - language, - idempotencyKey: `order-paid/${order.id}`, - }); - } - - async sendOrderPaidToAdmin(order: Order): Promise { - const currency = order.total.gross.currency; - const customerName = this.getCustomerName(order); - - await sendEmailToAdmin({ - subject: `Payment Received #${order.number} - ${customerName} - ${formatPrice(order.total.gross.amount, currency)}`, - react: OrderPaid({ - language: "en", - orderId: order.id, - orderNumber: order.number, - customerName, - items: this.parseOrderItems(order.lines, currency), - total: formatPrice(order.total.gross.amount, currency), - siteUrl: SITE_URL, - }), - eventType: "ORDER_FULLY_PAID", - orderId: order.id, - }); - } -} - -export const orderNotificationService = OrderNotificationService.getInstance(); -export default orderNotificationService;