// Mock dependencies jest.mock("../../../../../services/email/core/EmailClient", () => { return jest.fn().mockImplementation(() => ({ initialize: jest.fn().mockResolvedValue(), sendEmail: jest .fn() .mockResolvedValue({ success: true, messageId: "msg-123" }), })); }); jest.mock("../../../../../services/email/core/TemplateManager", () => { return jest.fn().mockImplementation(() => ({ initialize: jest.fn().mockResolvedValue(), renderTemplate: jest.fn().mockResolvedValue("Test"), })); }); jest.mock("../../../../../utils/logger", () => ({ info: jest.fn(), error: jest.fn(), warn: jest.fn(), })); const PaymentEmailService = require("../../../../../services/email/domain/PaymentEmailService"); describe("PaymentEmailService", () => { let service; const originalEnv = process.env; beforeEach(() => { jest.clearAllMocks(); process.env = { ...originalEnv, FRONTEND_URL: "http://localhost:3000", CUSTOMER_SUPPORT_EMAIL: "admin@example.com", }; service = new PaymentEmailService(); }); afterEach(() => { process.env = originalEnv; }); describe("initialize", () => { it("should initialize only once", async () => { await service.initialize(); await service.initialize(); expect(service.emailClient.initialize).toHaveBeenCalledTimes(1); }); }); describe("sendPaymentDeclinedNotification", () => { it("should send payment declined notification to renter", async () => { const result = await service.sendPaymentDeclinedNotification( "renter@example.com", { renterFirstName: "John", itemName: "Test Item", declineReason: "Card declined", updatePaymentUrl: "http://localhost:3000/update-payment", }, ); expect(result.success).toBe(true); expect(service.templateManager.renderTemplate).toHaveBeenCalledWith( "paymentDeclinedToRenter", expect.objectContaining({ renterFirstName: "John", itemName: "Test Item", declineReason: "Card declined", }), ); }); it("should use default values for missing params", async () => { await service.sendPaymentDeclinedNotification("renter@example.com", {}); expect(service.templateManager.renderTemplate).toHaveBeenCalledWith( "paymentDeclinedToRenter", expect.objectContaining({ renterFirstName: "there", itemName: "the item", }), ); }); it("should handle errors gracefully", async () => { service.templateManager.renderTemplate.mockRejectedValue( new Error("Template error"), ); const result = await service.sendPaymentDeclinedNotification( "test@example.com", {}, ); expect(result.success).toBe(false); expect(result.error).toContain("Template error"); }); }); describe("sendPaymentMethodUpdatedNotification", () => { it("should send payment method updated notification to owner", async () => { const result = await service.sendPaymentMethodUpdatedNotification( "owner@example.com", { ownerFirstName: "Jane", itemName: "Test Item", approvalUrl: "http://localhost:3000/approve", }, ); expect(result.success).toBe(true); expect(service.emailClient.sendEmail).toHaveBeenCalledWith( "owner@example.com", "Payment Method Updated - Test Item", expect.any(String), ); }); }); describe("sendPayoutFailedNotification", () => { it("should send payout failed notification to owner", async () => { const result = await service.sendPayoutFailedNotification( "owner@example.com", { ownerName: "John", payoutAmount: 50.0, failureMessage: "Bank account closed", actionRequired: "Please update your bank account", failureCode: "account_closed", requiresBankUpdate: true, }, ); expect(result.success).toBe(true); expect(service.templateManager.renderTemplate).toHaveBeenCalledWith( "payoutFailedToOwner", expect.objectContaining({ ownerName: "John", payoutAmount: "50.00", failureCode: "account_closed", requiresBankUpdate: true, }), ); }); }); describe("sendAccountDisconnectedEmail", () => { it("should send account disconnected notification", async () => { const result = await service.sendAccountDisconnectedEmail( "owner@example.com", { ownerName: "John", hasPendingPayouts: true, pendingPayoutCount: 3, }, ); expect(result.success).toBe(true); expect(service.templateManager.renderTemplate).toHaveBeenCalledWith( "accountDisconnectedToOwner", expect.objectContaining({ hasPendingPayouts: true, pendingPayoutCount: 3, }), ); }); it("should use default values for missing params", async () => { await service.sendAccountDisconnectedEmail("owner@example.com", {}); expect(service.templateManager.renderTemplate).toHaveBeenCalledWith( "accountDisconnectedToOwner", expect.objectContaining({ ownerName: "there", hasPendingPayouts: false, pendingPayoutCount: 0, }), ); }); }); describe("sendPayoutsDisabledEmail", () => { it("should send payouts disabled notification", async () => { const result = await service.sendPayoutsDisabledEmail( "owner@example.com", { ownerName: "John", disabledReason: "Verification required", }, ); expect(result.success).toBe(true); expect(service.emailClient.sendEmail).toHaveBeenCalledWith( "owner@example.com", "Action Required: Your payouts have been paused - Village Share", expect.any(String), ); }); }); describe("sendDisputeAlertEmail", () => { it("should send dispute alert to admin", async () => { const result = await service.sendDisputeAlertEmail({ rentalId: "rental-123", amount: 50.0, reason: "fraudulent", evidenceDueBy: new Date(), renterName: "Renter Name", renterEmail: "renter@example.com", ownerName: "Owner Name", ownerEmail: "owner@example.com", itemName: "Test Item", }); expect(result.success).toBe(true); expect(service.emailClient.sendEmail).toHaveBeenCalledWith( "admin@example.com", "URGENT: Payment Dispute - Rental #rental-123", expect.any(String), ); }); }); describe("sendDisputeLostAlertEmail", () => { it("should send dispute lost alert to admin", async () => { const result = await service.sendDisputeLostAlertEmail({ rentalId: "rental-123", amount: 50.0, ownerPayoutAmount: 45.0, ownerName: "Owner Name", ownerEmail: "owner@example.com", }); expect(result.success).toBe(true); expect(service.templateManager.renderTemplate).toHaveBeenCalledWith( "disputeLostAlertToAdmin", expect.objectContaining({ rentalId: "rental-123", amount: "50.00", ownerPayoutAmount: "45.00", }), ); }); }); describe("formatDisputeReason", () => { it("should format known dispute reasons", () => { expect(service.formatDisputeReason("fraudulent")).toBe( "Fraudulent transaction", ); expect(service.formatDisputeReason("product_not_received")).toBe( "Product not received", ); expect(service.formatDisputeReason("duplicate")).toBe("Duplicate charge"); }); it("should return original reason for unknown reasons", () => { expect(service.formatDisputeReason("unknown_reason")).toBe( "unknown_reason", ); }); it('should return "Unknown reason" for null/undefined', () => { expect(service.formatDisputeReason(null)).toBe("Unknown reason"); expect(service.formatDisputeReason(undefined)).toBe("Unknown reason"); }); }); });