// 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', ADMIN_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.00, 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.00, 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.00, ownerPayoutAmount: 45.00, 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'); }); }); });