Create service-oriented architecture for better maintainability: - AnalyticsService: Centralized analytics tracking with OpenPanel - trackOrderReceived(), trackRevenue(), track() - Error handling that doesn't break main flow - Singleton pattern for single instance - OrderNotificationService: Encapsulates all order email logic - sendOrderConfirmation() - customer + admin - sendOrderShipped() - with tracking info - sendOrderCancelled() - with reason - sendOrderPaid() - payment confirmation - Translation logic moved from webhook to service - Email formatting utilities encapsulated - Webhook route refactored: - Reduced from 605 lines to ~250 lines - No business logic - only HTTP handling - Delegates to services for emails and analytics - Cleaner separation of concerns - New utils file: formatPrice() shared between services This prevents future bugs by: 1. Centralizing email logic in one place 2. Making code testable (services can be unit tested) 3. Easier to add new webhook handlers 4. Translation logic not mixed with HTTP code 5. Analytics failures don't break order processing
79 lines
2.1 KiB
TypeScript
79 lines
2.1 KiB
TypeScript
import { OpenPanel } from "@openpanel/nextjs";
|
|
|
|
// Initialize OpenPanel for server-side tracking
|
|
const op = new OpenPanel({
|
|
clientId: process.env.NEXT_PUBLIC_OPENPANEL_CLIENT_ID || "",
|
|
clientSecret: process.env.OPENPANEL_CLIENT_SECRET || "",
|
|
apiUrl: process.env.OPENPANEL_API_URL || "https://op.nodecrew.me/api",
|
|
});
|
|
|
|
export interface OrderAnalyticsData {
|
|
orderId: string;
|
|
orderNumber: string;
|
|
total: number;
|
|
currency: string;
|
|
itemCount: number;
|
|
customerEmail: string;
|
|
eventType: string;
|
|
}
|
|
|
|
export interface RevenueData {
|
|
amount: number;
|
|
currency: string;
|
|
orderId: string;
|
|
orderNumber: string;
|
|
}
|
|
|
|
class AnalyticsService {
|
|
private static instance: AnalyticsService;
|
|
|
|
static getInstance(): AnalyticsService {
|
|
if (!AnalyticsService.instance) {
|
|
AnalyticsService.instance = new AnalyticsService();
|
|
}
|
|
return AnalyticsService.instance;
|
|
}
|
|
|
|
async trackOrderReceived(data: OrderAnalyticsData): Promise<void> {
|
|
try {
|
|
await op.track("order_received", {
|
|
order_id: data.orderId,
|
|
order_number: data.orderNumber,
|
|
total: data.total,
|
|
currency: data.currency,
|
|
item_count: data.itemCount,
|
|
customer_email: data.customerEmail,
|
|
event_type: data.eventType,
|
|
});
|
|
} catch (error) {
|
|
console.error("Failed to track order received:", error);
|
|
// Don't throw - analytics should not break the main flow
|
|
}
|
|
}
|
|
|
|
async trackRevenue(data: RevenueData): Promise<void> {
|
|
try {
|
|
await op.revenue(data.amount, {
|
|
currency: data.currency,
|
|
order_id: data.orderId,
|
|
order_number: data.orderNumber,
|
|
});
|
|
} catch (error) {
|
|
console.error("Failed to track revenue:", error);
|
|
// Don't throw - analytics should not break the main flow
|
|
}
|
|
}
|
|
|
|
async track(eventName: string, properties: Record<string, unknown>): Promise<void> {
|
|
try {
|
|
await op.track(eventName, properties);
|
|
} catch (error) {
|
|
console.error(`Failed to track event ${eventName}:`, error);
|
|
// Don't throw - analytics should not break the main flow
|
|
}
|
|
}
|
|
}
|
|
|
|
export const analyticsService = AnalyticsService.getInstance();
|
|
export default analyticsService;
|