import { describe, it, expect, vi, beforeEach } from "vitest"; import { NextRequest } from "next/server"; import { POST, GET } from "@/app/api/webhooks/saleor/route"; import { orderNotificationService } from "@/lib/services/OrderNotificationService"; import { analyticsService } from "@/lib/services/AnalyticsService"; import { mockOrderPayload, mockOrderWithTracking, mockOrderCancelled } from "../../../fixtures/orders"; // Mock the services vi.mock("@/lib/services/OrderNotificationService", () => ({ orderNotificationService: { sendOrderConfirmation: vi.fn().mockResolvedValue(undefined), sendOrderConfirmationToAdmin: vi.fn().mockResolvedValue(undefined), sendOrderShipped: vi.fn().mockResolvedValue(undefined), sendOrderShippedToAdmin: vi.fn().mockResolvedValue(undefined), sendOrderCancelled: vi.fn().mockResolvedValue(undefined), sendOrderCancelledToAdmin: vi.fn().mockResolvedValue(undefined), sendOrderPaid: vi.fn().mockResolvedValue(undefined), sendOrderPaidToAdmin: vi.fn().mockResolvedValue(undefined), }, })); vi.mock("@/lib/services/AnalyticsService", () => ({ analyticsService: { trackOrderReceived: vi.fn().mockResolvedValue(undefined), trackRevenue: vi.fn().mockResolvedValue(undefined), track: vi.fn().mockResolvedValue(undefined), }, })); describe("Saleor Webhook Handler", () => { beforeEach(() => { vi.clearAllMocks(); }); describe("GET /api/webhooks/saleor", () => { it("should return health check response", async () => { const response = await GET(); const data = await response.json(); expect(response.status).toBe(200); expect(data.status).toBe("ok"); expect(data.supportedEvents).toContain("ORDER_CONFIRMED"); expect(data.supportedEvents).toContain("ORDER_CREATED"); }); }); describe("POST /api/webhooks/saleor - ORDER_CONFIRMED", () => { it("should process ORDER_CONFIRMED and send customer + admin emails", async () => { const request = new NextRequest("http://localhost:3000/api/webhooks/saleor", { method: "POST", headers: { "saleor-event": "ORDER_CONFIRMED", "saleor-domain": "api.manoonoils.com", }, body: JSON.stringify([mockOrderPayload]), }); const response = await POST(request); const data = await response.json(); expect(response.status).toBe(200); expect(data.success).toBe(true); // Should send customer email expect(orderNotificationService.sendOrderConfirmation).toHaveBeenCalledTimes(1); // Should send admin email expect(orderNotificationService.sendOrderConfirmationToAdmin).toHaveBeenCalledTimes(1); // Should track analytics expect(analyticsService.trackOrderReceived).toHaveBeenCalledTimes(1); expect(analyticsService.trackRevenue).toHaveBeenCalledTimes(1); // Verify revenue tracking has correct data expect(analyticsService.trackRevenue).toHaveBeenCalledWith({ amount: 10000, currency: "RSD", orderId: mockOrderPayload.id, orderNumber: String(mockOrderPayload.number), }); }); it("should NOT track analytics for ORDER_CREATED (prevents duplication)", async () => { const request = new NextRequest("http://localhost:3000/api/webhooks/saleor", { method: "POST", headers: { "saleor-event": "ORDER_CREATED", "saleor-domain": "api.manoonoils.com", }, body: JSON.stringify([mockOrderPayload]), }); const response = await POST(request); expect(response.status).toBe(200); // Should NOT send customer email expect(orderNotificationService.sendOrderConfirmation).not.toHaveBeenCalled(); // Should NOT track analytics expect(analyticsService.trackOrderReceived).not.toHaveBeenCalled(); expect(analyticsService.trackRevenue).not.toHaveBeenCalled(); // Should still send admin notification expect(orderNotificationService.sendOrderConfirmationToAdmin).toHaveBeenCalledTimes(1); }); }); describe("POST /api/webhooks/saleor - ORDER_FULFILLED", () => { it("should send shipping emails with tracking info", async () => { const request = new NextRequest("http://localhost:3000/api/webhooks/saleor", { method: "POST", headers: { "saleor-event": "ORDER_FULFILLED", "saleor-domain": "api.manoonoils.com", }, body: JSON.stringify([mockOrderWithTracking]), }); const response = await POST(request); expect(response.status).toBe(200); expect(orderNotificationService.sendOrderShipped).toHaveBeenCalledWith( expect.any(Object), "TRK123456789", "https://tracking.example.com/TRK123456789" ); expect(orderNotificationService.sendOrderShippedToAdmin).toHaveBeenCalledWith( expect.any(Object), "TRK123456789", "https://tracking.example.com/TRK123456789" ); }); }); describe("POST /api/webhooks/saleor - ORDER_CANCELLED", () => { it("should send cancellation emails with reason", async () => { const request = new NextRequest("http://localhost:3000/api/webhooks/saleor", { method: "POST", headers: { "saleor-event": "ORDER_CANCELLED", "saleor-domain": "api.manoonoils.com", }, body: JSON.stringify([mockOrderCancelled]), }); const response = await POST(request); expect(response.status).toBe(200); expect(orderNotificationService.sendOrderCancelled).toHaveBeenCalledWith( expect.any(Object), "Customer requested cancellation" ); expect(orderNotificationService.sendOrderCancelledToAdmin).toHaveBeenCalledWith( expect.any(Object), "Customer requested cancellation" ); }); }); describe("POST /api/webhooks/saleor - ORDER_FULLY_PAID", () => { it("should send payment confirmation emails", async () => { const request = new NextRequest("http://localhost:3000/api/webhooks/saleor", { method: "POST", headers: { "saleor-event": "ORDER_FULLY_PAID", "saleor-domain": "api.manoonoils.com", }, body: JSON.stringify([mockOrderPayload]), }); const response = await POST(request); expect(response.status).toBe(200); expect(orderNotificationService.sendOrderPaid).toHaveBeenCalledTimes(1); expect(orderNotificationService.sendOrderPaidToAdmin).toHaveBeenCalledTimes(1); }); }); describe("Error Handling", () => { it("should return 400 for missing order in payload", async () => { const request = new NextRequest("http://localhost:3000/api/webhooks/saleor", { method: "POST", headers: { "saleor-event": "ORDER_CONFIRMED", "saleor-domain": "api.manoonoils.com", }, body: JSON.stringify([]), // Empty array }); const response = await POST(request); const data = await response.json(); expect(response.status).toBe(400); expect(data.error).toBe("No order in payload"); }); it("should return 400 for missing saleor-event header", async () => { const request = new NextRequest("http://localhost:3000/api/webhooks/saleor", { method: "POST", headers: { "saleor-domain": "api.manoonoils.com", }, body: JSON.stringify([mockOrderPayload]), }); const response = await POST(request); const data = await response.json(); expect(response.status).toBe(400); expect(data.error).toBe("Missing saleor-event header"); }); it("should return 200 for unsupported events (graceful skip)", async () => { const request = new NextRequest("http://localhost:3000/api/webhooks/saleor", { method: "POST", headers: { "saleor-event": "UNSUPPORTED_EVENT", "saleor-domain": "api.manoonoils.com", }, body: JSON.stringify([mockOrderPayload]), }); const response = await POST(request); const data = await response.json(); expect(response.status).toBe(200); expect(data.message).toBe("Event not supported"); }); it("should handle server errors gracefully", async () => { // Simulate service throwing error vi.mocked(orderNotificationService.sendOrderConfirmationToAdmin).mockRejectedValueOnce( new Error("Email service down") ); const request = new NextRequest("http://localhost:3000/api/webhooks/saleor", { method: "POST", headers: { "saleor-event": "ORDER_CREATED", "saleor-domain": "api.manoonoils.com", }, body: JSON.stringify([mockOrderPayload]), }); const response = await POST(request); expect(response.status).toBe(500); }); }); describe("Currency Handling", () => { it("should preserve RSD currency from Saleor payload", async () => { const rsdOrder = { ...mockOrderPayload, total_gross_amount: "5479", channel: { currency_code: "RSD" }, }; const request = new NextRequest("http://localhost:3000/api/webhooks/saleor", { method: "POST", headers: { "saleor-event": "ORDER_CONFIRMED", "saleor-domain": "api.manoonoils.com", }, body: JSON.stringify([rsdOrder]), }); await POST(request); // Verify the order passed to analytics has correct currency expect(analyticsService.trackRevenue).toHaveBeenCalledWith( expect.objectContaining({ amount: 5479, currency: "RSD", }) ); }); }); });