// 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 RentalFlowEmailService = require('../../../../../services/email/domain/RentalFlowEmailService'); describe('RentalFlowEmailService', () => { let service; const originalEnv = process.env; beforeEach(() => { jest.clearAllMocks(); process.env = { ...originalEnv, FRONTEND_URL: 'http://localhost:3000' }; service = new RentalFlowEmailService(); }); afterEach(() => { process.env = originalEnv; }); describe('initialize', () => { it('should initialize only once', async () => { await service.initialize(); await service.initialize(); expect(service.emailClient.initialize).toHaveBeenCalledTimes(1); expect(service.templateManager.initialize).toHaveBeenCalledTimes(1); }); }); describe('sendRentalRequestEmail', () => { const owner = { firstName: 'John', email: 'john@example.com' }; const renter = { firstName: 'Jane', lastName: 'Smith' }; const rental = { id: 123, item: { name: 'Power Drill' }, startDateTime: '2024-01-15T10:00:00Z', endDateTime: '2024-01-16T10:00:00Z', totalAmount: '50.00', payoutAmount: '45.00', deliveryMethod: 'pickup', intendedUse: 'Home project', }; it('should send rental request email with correct variables', async () => { const result = await service.sendRentalRequestEmail(owner, renter, rental); expect(result.success).toBe(true); expect(service.templateManager.renderTemplate).toHaveBeenCalledWith( 'rentalRequestToOwner', expect.objectContaining({ ownerName: 'John', renterName: 'Jane Smith', itemName: 'Power Drill', totalAmount: '50.00', payoutAmount: '45.00', deliveryMethod: 'pickup', intendedUse: 'Home project', approveUrl: 'http://localhost:3000/owning?rentalId=123', }) ); expect(service.emailClient.sendEmail).toHaveBeenCalledWith( 'john@example.com', 'Rental Request for Power Drill', expect.any(String) ); }); it('should use default values when data is missing', async () => { const minimalRental = { id: 123, item: null }; const minimalRenter = {}; await service.sendRentalRequestEmail(owner, minimalRenter, minimalRental); expect(service.templateManager.renderTemplate).toHaveBeenCalledWith( 'rentalRequestToOwner', expect.objectContaining({ itemName: 'your item', startDate: 'Not specified', endDate: 'Not specified', totalAmount: '0.00', payoutAmount: '0.00', deliveryMethod: 'Not specified', intendedUse: 'Not specified', }) ); }); it('should use A renter fallback when names produce empty string', async () => { const minimalRental = { id: 123, item: null }; const renterWithEmptyNames = { firstName: '', lastName: '' }; await service.sendRentalRequestEmail(owner, renterWithEmptyNames, minimalRental); expect(service.templateManager.renderTemplate).toHaveBeenCalledWith( 'rentalRequestToOwner', expect.objectContaining({ renterName: 'A renter', }) ); }); it('should handle errors gracefully', async () => { service.templateManager.renderTemplate.mockRejectedValueOnce(new Error('Template error')); const result = await service.sendRentalRequestEmail(owner, renter, rental); expect(result.success).toBe(false); expect(result.error).toBe('Template error'); }); }); describe('sendRentalRequestConfirmationEmail', () => { const renter = { firstName: 'Jane', email: 'jane@example.com' }; const rental = { item: { name: 'Power Drill' }, startDateTime: '2024-01-15T10:00:00Z', endDateTime: '2024-01-16T10:00:00Z', totalAmount: '50.00', deliveryMethod: 'pickup', }; it('should send rental request confirmation with correct variables', async () => { const result = await service.sendRentalRequestConfirmationEmail(renter, rental); expect(result.success).toBe(true); expect(service.templateManager.renderTemplate).toHaveBeenCalledWith( 'rentalRequestConfirmationToRenter', expect.objectContaining({ renterName: 'Jane', itemName: 'Power Drill', totalAmount: '50.00', deliveryMethod: 'pickup', viewRentalsUrl: 'http://localhost:3000/renting', }) ); expect(service.emailClient.sendEmail).toHaveBeenCalledWith( 'jane@example.com', 'Rental Request Submitted - Power Drill', expect.any(String) ); }); it('should use default name when firstName is missing', async () => { const renterNoName = { email: 'jane@example.com' }; await service.sendRentalRequestConfirmationEmail(renterNoName, rental); expect(service.templateManager.renderTemplate).toHaveBeenCalledWith( 'rentalRequestConfirmationToRenter', expect.objectContaining({ renterName: 'there' }) ); }); it('should show different payment message for free rentals', async () => { const freeRental = { ...rental, totalAmount: '0' }; await service.sendRentalRequestConfirmationEmail(renter, freeRental); expect(service.templateManager.renderTemplate).toHaveBeenCalledWith( 'rentalRequestConfirmationToRenter', expect.objectContaining({ paymentMessage: 'The owner will review your request and respond soon.', }) ); }); it('should show payment pending message for paid rentals', async () => { await service.sendRentalRequestConfirmationEmail(renter, rental); expect(service.templateManager.renderTemplate).toHaveBeenCalledWith( 'rentalRequestConfirmationToRenter', expect.objectContaining({ paymentMessage: "The owner will review your request. You'll only be charged if they approve it.", }) ); }); it('should handle errors gracefully', async () => { service.emailClient.sendEmail.mockRejectedValueOnce(new Error('Send error')); const result = await service.sendRentalRequestConfirmationEmail(renter, rental); expect(result.success).toBe(false); expect(result.error).toBe('Send error'); }); }); describe('sendRentalApprovalConfirmationEmail', () => { const owner = { firstName: 'John', email: 'john@example.com', stripeConnectedAccountId: 'acct_123', }; const renter = { firstName: 'Jane', lastName: 'Smith' }; const rental = { id: 123, item: { name: 'Power Drill' }, startDateTime: '2024-01-15T10:00:00Z', endDateTime: '2024-01-16T10:00:00Z', deliveryMethod: 'delivery', totalAmount: '50.00', payoutAmount: '45.00', platformFee: '5.00', }; it('should send approval confirmation with correct variables', async () => { const result = await service.sendRentalApprovalConfirmationEmail(owner, renter, rental); expect(result.success).toBe(true); expect(service.templateManager.renderTemplate).toHaveBeenCalledWith( 'rentalApprovalConfirmationToOwner', expect.objectContaining({ ownerName: 'John', itemName: 'Power Drill', renterName: 'Jane Smith', deliveryMethod: 'Delivery', paymentMessage: 'their payment has been processed successfully.', rentalDetailsUrl: 'http://localhost:3000/owning?rentalId=123', }) ); expect(service.emailClient.sendEmail).toHaveBeenCalledWith( 'john@example.com', 'Rental Approved - Power Drill', expect.any(String) ); }); it('should show stripe setup reminder when no stripe account for paid rental', async () => { const ownerNoStripe = { firstName: 'John', email: 'john@example.com' }; await service.sendRentalApprovalConfirmationEmail(ownerNoStripe, renter, rental); expect(service.templateManager.renderTemplate).toHaveBeenCalledWith( 'rentalApprovalConfirmationToOwner', expect.objectContaining({ stripeSection: expect.stringContaining('Action Required'), stripeSection: expect.stringContaining('Set Up Your Earnings Account'), }) ); }); it('should show earnings active message when stripe account exists for paid rental', async () => { await service.sendRentalApprovalConfirmationEmail(owner, renter, rental); expect(service.templateManager.renderTemplate).toHaveBeenCalledWith( 'rentalApprovalConfirmationToOwner', expect.objectContaining({ stripeSection: expect.stringContaining('Earnings Account Active'), }) ); }); it('should handle free rentals correctly', async () => { const freeRental = { ...rental, totalAmount: '0', payoutAmount: '0', platformFee: '0' }; await service.sendRentalApprovalConfirmationEmail(owner, renter, freeRental); expect(service.templateManager.renderTemplate).toHaveBeenCalledWith( 'rentalApprovalConfirmationToOwner', expect.objectContaining({ paymentMessage: 'this is a free rental (no payment required).', earningsSection: '', stripeSection: '', }) ); }); it('should use default name when firstName is missing', async () => { const ownerNoName = { email: 'john@example.com', stripeConnectedAccountId: 'acct_123' }; await service.sendRentalApprovalConfirmationEmail(ownerNoName, renter, rental); expect(service.templateManager.renderTemplate).toHaveBeenCalledWith( 'rentalApprovalConfirmationToOwner', expect.objectContaining({ ownerName: 'there' }) ); }); it('should handle errors gracefully', async () => { service.templateManager.renderTemplate.mockRejectedValueOnce(new Error('Template error')); const result = await service.sendRentalApprovalConfirmationEmail(owner, renter, rental); expect(result.success).toBe(false); expect(result.error).toBe('Template error'); }); }); describe('sendRentalDeclinedEmail', () => { const renter = { firstName: 'Jane', email: 'jane@example.com' }; const rental = { item: { name: 'Power Drill' }, startDateTime: '2024-01-15T10:00:00Z', endDateTime: '2024-01-16T10:00:00Z', totalAmount: '50.00', payoutAmount: '45.00', deliveryMethod: 'pickup', }; it('should send declined email with correct variables', async () => { const result = await service.sendRentalDeclinedEmail(renter, rental, 'Item unavailable'); expect(result.success).toBe(true); expect(service.templateManager.renderTemplate).toHaveBeenCalledWith( 'rentalDeclinedToRenter', expect.objectContaining({ renterName: 'Jane', itemName: 'Power Drill', deliveryMethod: 'pickup', browseItemsUrl: 'http://localhost:3000/', }) ); expect(service.emailClient.sendEmail).toHaveBeenCalledWith( 'jane@example.com', 'Rental Request Declined - Power Drill', expect.any(String) ); }); it('should include owner message when decline reason is provided', async () => { await service.sendRentalDeclinedEmail(renter, rental, 'Item is broken'); expect(service.templateManager.renderTemplate).toHaveBeenCalledWith( 'rentalDeclinedToRenter', expect.objectContaining({ ownerMessage: expect.stringContaining('Item is broken'), }) ); }); it('should not include owner message when no decline reason', async () => { await service.sendRentalDeclinedEmail(renter, rental, null); expect(service.templateManager.renderTemplate).toHaveBeenCalledWith( 'rentalDeclinedToRenter', expect.objectContaining({ ownerMessage: '', }) ); }); it('should show no charge message for paid rentals', async () => { await service.sendRentalDeclinedEmail(renter, rental, null); expect(service.templateManager.renderTemplate).toHaveBeenCalledWith( 'rentalDeclinedToRenter', expect.objectContaining({ paymentMessage: 'Since your request was declined before payment was processed, you will not be charged.', }) ); }); it('should show no payment message for free rentals', async () => { const freeRental = { ...rental, totalAmount: '0' }; await service.sendRentalDeclinedEmail(renter, freeRental, null); expect(service.templateManager.renderTemplate).toHaveBeenCalledWith( 'rentalDeclinedToRenter', expect.objectContaining({ paymentMessage: 'No payment was required for this rental request.', }) ); }); it('should use default name when firstName is missing', async () => { const renterNoName = { email: 'jane@example.com' }; await service.sendRentalDeclinedEmail(renterNoName, rental, null); expect(service.templateManager.renderTemplate).toHaveBeenCalledWith( 'rentalDeclinedToRenter', expect.objectContaining({ renterName: 'there' }) ); }); it('should handle errors gracefully', async () => { service.emailClient.sendEmail.mockRejectedValueOnce(new Error('Send error')); const result = await service.sendRentalDeclinedEmail(renter, rental, null); expect(result.success).toBe(false); expect(result.error).toBe('Send error'); }); }); describe('sendRentalConfirmation', () => { const userEmail = 'jane@example.com'; const notification = { title: 'Rental Confirmed', message: 'Your rental has been confirmed.', }; const rental = { item: { name: 'Power Drill' }, startDateTime: '2024-01-15T10:00:00Z', endDateTime: '2024-01-16T10:00:00Z', totalAmount: '50.00', paymentStatus: 'paid', paymentMethodBrand: 'visa', paymentMethodLast4: '4242', stripePaymentIntentId: 'pi_123', chargedAt: '2024-01-15T09:00:00Z', }; it('should send rental confirmation with correct variables', async () => { const result = await service.sendRentalConfirmation( userEmail, notification, rental, 'Jane', true ); expect(result.success).toBe(true); expect(service.templateManager.renderTemplate).toHaveBeenCalledWith( 'rentalConfirmationToUser', expect.objectContaining({ recipientName: 'Jane', title: 'Rental Confirmed', message: 'Your rental has been confirmed.', itemName: 'Power Drill', isRenter: true, }) ); expect(service.emailClient.sendEmail).toHaveBeenCalledWith( 'jane@example.com', 'Rental Confirmation - Power Drill', expect.any(String) ); }); it('should include payment receipt for renter with paid rental', async () => { await service.sendRentalConfirmation(userEmail, notification, rental, 'Jane', true); expect(service.templateManager.renderTemplate).toHaveBeenCalledWith( 'rentalConfirmationToUser', expect.objectContaining({ paymentSection: expect.stringContaining('Payment Receipt'), paymentSection: expect.stringContaining('Visa ending in 4242'), }) ); }); it('should show free rental message for zero amount', async () => { const freeRental = { ...rental, totalAmount: '0' }; await service.sendRentalConfirmation(userEmail, notification, freeRental, 'Jane', true); expect(service.templateManager.renderTemplate).toHaveBeenCalledWith( 'rentalConfirmationToUser', expect.objectContaining({ paymentSection: expect.stringContaining('No Payment Required'), }) ); }); it('should not include payment section for owner', async () => { await service.sendRentalConfirmation(userEmail, notification, rental, 'John', false); expect(service.templateManager.renderTemplate).toHaveBeenCalledWith( 'rentalConfirmationToUser', expect.objectContaining({ paymentSection: '', isRenter: false, }) ); }); it('should use default name when recipientName is null', async () => { await service.sendRentalConfirmation(userEmail, notification, rental, null, true); expect(service.templateManager.renderTemplate).toHaveBeenCalledWith( 'rentalConfirmationToUser', expect.objectContaining({ recipientName: 'there' }) ); }); it('should handle missing rental data', async () => { const minimalRental = {}; await service.sendRentalConfirmation(userEmail, notification, minimalRental, 'Jane', true); expect(service.templateManager.renderTemplate).toHaveBeenCalledWith( 'rentalConfirmationToUser', expect.objectContaining({ itemName: 'Unknown Item', startDate: 'Not specified', endDate: 'Not specified', }) ); }); it('should handle errors gracefully', async () => { service.templateManager.renderTemplate.mockRejectedValueOnce(new Error('Template error')); const result = await service.sendRentalConfirmation( userEmail, notification, rental, 'Jane', true ); expect(result.success).toBe(false); expect(result.error).toBe('Template error'); }); }); describe('sendRentalConfirmationEmails', () => { const owner = { firstName: 'John', email: 'john@example.com' }; const renter = { firstName: 'Jane', email: 'jane@example.com' }; const rental = { id: 123, ownerId: 1, renterId: 2, item: { name: 'Power Drill' }, startDateTime: '2024-01-15T10:00:00Z', totalAmount: '50.00', paymentStatus: 'paid', }; it('should send confirmation emails to both owner and renter', async () => { const result = await service.sendRentalConfirmationEmails(owner, renter, rental); expect(result.ownerEmailSent).toBe(true); expect(result.renterEmailSent).toBe(true); expect(service.emailClient.sendEmail).toHaveBeenCalledTimes(2); }); it('should send owner email with isRenter false', async () => { await service.sendRentalConfirmationEmails(owner, renter, rental); // First call is for owner const firstCall = service.templateManager.renderTemplate.mock.calls[0]; expect(firstCall[1]).toMatchObject({ isRenter: false }); }); it('should send renter email with isRenter true', async () => { await service.sendRentalConfirmationEmails(owner, renter, rental); // Second call is for renter const secondCall = service.templateManager.renderTemplate.mock.calls[1]; expect(secondCall[1]).toMatchObject({ isRenter: true }); }); it('should handle missing owner email', async () => { const ownerNoEmail = { firstName: 'John' }; const result = await service.sendRentalConfirmationEmails(ownerNoEmail, renter, rental); expect(result.ownerEmailSent).toBe(false); expect(result.renterEmailSent).toBe(true); }); it('should handle missing renter email', async () => { const renterNoEmail = { firstName: 'Jane' }; const result = await service.sendRentalConfirmationEmails(owner, renterNoEmail, rental); expect(result.ownerEmailSent).toBe(true); expect(result.renterEmailSent).toBe(false); }); it('should continue sending renter email if owner email fails', async () => { service.emailClient.sendEmail .mockResolvedValueOnce({ success: false, error: 'Owner send failed' }) .mockResolvedValueOnce({ success: true, messageId: 'msg-456' }); const result = await service.sendRentalConfirmationEmails(owner, renter, rental); expect(result.ownerEmailSent).toBe(false); expect(result.renterEmailSent).toBe(true); }); it('should handle errors in owner email without crashing', async () => { service.emailClient.sendEmail .mockRejectedValueOnce(new Error('Network error')) .mockResolvedValueOnce({ success: true, messageId: 'msg-456' }); const result = await service.sendRentalConfirmationEmails(owner, renter, rental); expect(result.ownerEmailSent).toBe(false); expect(result.renterEmailSent).toBe(true); }); }); describe('sendRentalCancellationEmails', () => { const owner = { firstName: 'John', email: 'john@example.com' }; const renter = { firstName: 'Jane', email: 'jane@example.com' }; const rental = { item: { name: 'Power Drill' }, startDateTime: '2024-01-15T10:00:00Z', endDateTime: '2024-01-16T10:00:00Z', cancelledBy: 'renter', cancelledAt: '2024-01-14T10:00:00Z', totalAmount: '50.00', }; const refundInfo = { amount: 50, percentage: 1, reason: 'Full refund - cancelled before rental start', }; it('should send cancellation emails when renter cancels', async () => { const result = await service.sendRentalCancellationEmails(owner, renter, rental, refundInfo); expect(result.confirmationEmailSent).toBe(true); expect(result.notificationEmailSent).toBe(true); expect(service.emailClient.sendEmail).toHaveBeenCalledTimes(2); }); it('should send confirmation to renter and notification to owner when renter cancels', async () => { await service.sendRentalCancellationEmails(owner, renter, rental, refundInfo); // Confirmation to renter (canceller) expect(service.templateManager.renderTemplate).toHaveBeenCalledWith( 'rentalCancellationConfirmationToUser', expect.objectContaining({ recipientName: 'Jane' }) ); // Notification to owner expect(service.templateManager.renderTemplate).toHaveBeenCalledWith( 'rentalCancellationNotificationToUser', expect.objectContaining({ recipientName: 'John' }) ); }); it('should send confirmation to owner and notification to renter when owner cancels', async () => { const ownerCancelledRental = { ...rental, cancelledBy: 'owner' }; await service.sendRentalCancellationEmails(owner, renter, ownerCancelledRental, refundInfo); // Confirmation to owner (canceller) expect(service.templateManager.renderTemplate).toHaveBeenCalledWith( 'rentalCancellationConfirmationToUser', expect.objectContaining({ recipientName: 'John' }) ); // Notification to renter expect(service.templateManager.renderTemplate).toHaveBeenCalledWith( 'rentalCancellationNotificationToUser', expect.objectContaining({ recipientName: 'Jane' }) ); }); it('should include refund section for paid rentals', async () => { await service.sendRentalCancellationEmails(owner, renter, rental, refundInfo); expect(service.templateManager.renderTemplate).toHaveBeenCalledWith( 'rentalCancellationConfirmationToUser', expect.objectContaining({ refundSection: expect.stringContaining('Refund Information'), }) ); }); it('should show no refund message when refund amount is zero', async () => { const noRefundInfo = { amount: 0, percentage: 0, reason: 'Cancelled after rental period' }; await service.sendRentalCancellationEmails(owner, renter, rental, noRefundInfo); expect(service.templateManager.renderTemplate).toHaveBeenCalledWith( 'rentalCancellationConfirmationToUser', expect.objectContaining({ refundSection: expect.stringContaining('No Refund Available'), }) ); }); it('should not include refund section for free rentals', async () => { const freeRental = { ...rental, totalAmount: '0' }; await service.sendRentalCancellationEmails(owner, renter, freeRental, refundInfo); expect(service.templateManager.renderTemplate).toHaveBeenCalledWith( 'rentalCancellationConfirmationToUser', expect.objectContaining({ refundSection: '', }) ); }); it('should use default name when firstName is missing', async () => { const ownerNoName = { email: 'john@example.com' }; const renterNoName = { email: 'jane@example.com' }; await service.sendRentalCancellationEmails(ownerNoName, renterNoName, rental, refundInfo); expect(service.templateManager.renderTemplate).toHaveBeenCalledWith( 'rentalCancellationConfirmationToUser', expect.objectContaining({ recipientName: 'there' }) ); }); it('should continue if confirmation email fails', async () => { service.emailClient.sendEmail .mockResolvedValueOnce({ success: false, error: 'Send failed' }) .mockResolvedValueOnce({ success: true, messageId: 'msg-456' }); const result = await service.sendRentalCancellationEmails(owner, renter, rental, refundInfo); expect(result.confirmationEmailSent).toBe(false); expect(result.notificationEmailSent).toBe(true); }); }); describe('sendRentalCompletionEmails', () => { const owner = { firstName: 'John', lastName: 'Doe', email: 'john@example.com', stripeConnectedAccountId: 'acct_123', }; const renter = { firstName: 'Jane', lastName: 'Smith', email: 'jane@example.com' }; const rental = { id: 123, item: { name: 'Power Drill' }, startDateTime: '2024-01-15T10:00:00Z', endDateTime: '2024-01-16T10:00:00Z', actualReturnDateTime: '2024-01-16T09:30:00Z', itemReviewSubmittedAt: null, totalAmount: '50.00', payoutAmount: '45.00', platformFee: '5.00', }; it('should send completion emails to both owner and renter', async () => { const result = await service.sendRentalCompletionEmails(owner, renter, rental); expect(result.ownerEmailSent).toBe(true); expect(result.renterEmailSent).toBe(true); expect(service.emailClient.sendEmail).toHaveBeenCalledTimes(2); }); it('should send renter email with review prompt when not yet reviewed', async () => { await service.sendRentalCompletionEmails(owner, renter, rental); expect(service.templateManager.renderTemplate).toHaveBeenCalledWith( 'rentalCompletionThankYouToRenter', expect.objectContaining({ reviewSection: expect.stringContaining('Share Your Experience'), reviewSection: expect.stringContaining('Leave a Review'), }) ); }); it('should show thank you message when review already submitted', async () => { const reviewedRental = { ...rental, itemReviewSubmittedAt: '2024-01-16T12:00:00Z' }; await service.sendRentalCompletionEmails(owner, renter, reviewedRental); expect(service.templateManager.renderTemplate).toHaveBeenCalledWith( 'rentalCompletionThankYouToRenter', expect.objectContaining({ reviewSection: expect.stringContaining('Thank You for Your Review'), }) ); }); it('should send owner email with earnings for paid rental', async () => { await service.sendRentalCompletionEmails(owner, renter, rental); expect(service.templateManager.renderTemplate).toHaveBeenCalledWith( 'rentalCompletionCongratsToOwner', expect.objectContaining({ earningsSection: expect.stringContaining('Your Earnings'), earningsSection: expect.stringContaining('$45.00'), }) ); }); it('should show payout initiated message when owner has stripe account', async () => { await service.sendRentalCompletionEmails(owner, renter, rental); expect(service.templateManager.renderTemplate).toHaveBeenCalledWith( 'rentalCompletionCongratsToOwner', expect.objectContaining({ stripeSection: expect.stringContaining('Payout Initiated'), }) ); }); it('should show stripe setup reminder when owner has no stripe account for paid rental', async () => { const ownerNoStripe = { firstName: 'John', lastName: 'Doe', email: 'john@example.com' }; await service.sendRentalCompletionEmails(ownerNoStripe, renter, rental); expect(service.templateManager.renderTemplate).toHaveBeenCalledWith( 'rentalCompletionCongratsToOwner', expect.objectContaining({ stripeSection: expect.stringContaining('Action Required'), }) ); }); it('should not show earnings section for free rentals', async () => { const freeRental = { ...rental, totalAmount: '0', payoutAmount: '0', platformFee: '0' }; await service.sendRentalCompletionEmails(owner, renter, freeRental); expect(service.templateManager.renderTemplate).toHaveBeenCalledWith( 'rentalCompletionCongratsToOwner', expect.objectContaining({ earningsSection: '', stripeSection: '', }) ); }); it('should use default name when firstName is missing', async () => { const ownerNoName = { email: 'john@example.com', stripeConnectedAccountId: 'acct_123' }; const renterNoName = { email: 'jane@example.com' }; await service.sendRentalCompletionEmails(ownerNoName, renterNoName, rental); expect(service.templateManager.renderTemplate).toHaveBeenCalledWith( 'rentalCompletionThankYouToRenter', expect.objectContaining({ renterName: 'there' }) ); expect(service.templateManager.renderTemplate).toHaveBeenCalledWith( 'rentalCompletionCongratsToOwner', expect.objectContaining({ ownerName: 'there' }) ); }); it('should continue if renter email fails', async () => { service.emailClient.sendEmail .mockResolvedValueOnce({ success: false, error: 'Send failed' }) .mockResolvedValueOnce({ success: true, messageId: 'msg-456' }); const result = await service.sendRentalCompletionEmails(owner, renter, rental); expect(result.renterEmailSent).toBe(false); expect(result.ownerEmailSent).toBe(true); }); it('should handle errors in renter email without crashing', async () => { service.emailClient.sendEmail .mockRejectedValueOnce(new Error('Network error')) .mockResolvedValueOnce({ success: true, messageId: 'msg-456' }); const result = await service.sendRentalCompletionEmails(owner, renter, rental); expect(result.renterEmailSent).toBe(false); expect(result.ownerEmailSent).toBe(true); }); }); describe('sendPayoutReceivedEmail', () => { const owner = { firstName: 'John', email: 'john@example.com' }; const rental = { item: { name: 'Power Drill' }, startDateTime: '2024-01-15T10:00:00Z', endDateTime: '2024-01-16T10:00:00Z', totalAmount: '50.00', platformFee: '5.00', payoutAmount: '45.00', stripeTransferId: 'tr_123', }; it('should send payout email with correct variables', async () => { const result = await service.sendPayoutReceivedEmail(owner, rental); expect(result.success).toBe(true); expect(service.templateManager.renderTemplate).toHaveBeenCalledWith( 'payoutReceivedToOwner', expect.objectContaining({ ownerName: 'John', itemName: 'Power Drill', totalAmount: '50.00', platformFee: '5.00', payoutAmount: '45.00', stripeTransferId: 'tr_123', earningsDashboardUrl: 'http://localhost:3000/earnings', }) ); expect(service.emailClient.sendEmail).toHaveBeenCalledWith( 'john@example.com', 'Earnings Received - $45.00 for Power Drill', expect.any(String) ); }); it('should use default name when firstName is missing', async () => { const ownerNoName = { email: 'john@example.com' }; await service.sendPayoutReceivedEmail(ownerNoName, rental); expect(service.templateManager.renderTemplate).toHaveBeenCalledWith( 'payoutReceivedToOwner', expect.objectContaining({ ownerName: 'there' }) ); }); it('should handle missing item name', async () => { const rentalNoItem = { ...rental, item: null }; await service.sendPayoutReceivedEmail(owner, rentalNoItem); expect(service.templateManager.renderTemplate).toHaveBeenCalledWith( 'payoutReceivedToOwner', expect.objectContaining({ itemName: 'your item' }) ); }); it('should handle errors gracefully', async () => { service.templateManager.renderTemplate.mockRejectedValueOnce(new Error('Template error')); const result = await service.sendPayoutReceivedEmail(owner, rental); expect(result.success).toBe(false); expect(result.error).toBe('Template error'); }); }); describe('sendAuthenticationRequiredEmail', () => { const email = 'jane@example.com'; const data = { renterName: 'Jane', itemName: 'Power Drill', ownerName: 'John', amount: 50, }; it('should send authentication required email with correct variables', async () => { const result = await service.sendAuthenticationRequiredEmail(email, data); expect(result.success).toBe(true); expect(service.templateManager.renderTemplate).toHaveBeenCalledWith( 'authenticationRequiredToRenter', expect.objectContaining({ renterName: 'Jane', itemName: 'Power Drill', ownerName: 'John', amount: '50.00', }) ); expect(service.emailClient.sendEmail).toHaveBeenCalledWith( 'jane@example.com', 'Action Required: Complete payment for Power Drill', expect.any(String) ); }); it('should use default values when data is missing', async () => { const minimalData = {}; await service.sendAuthenticationRequiredEmail(email, minimalData); expect(service.templateManager.renderTemplate).toHaveBeenCalledWith( 'authenticationRequiredToRenter', expect.objectContaining({ renterName: 'there', itemName: 'the item', ownerName: 'The owner', amount: '0.00', }) ); }); it('should handle errors gracefully', async () => { service.emailClient.sendEmail.mockRejectedValueOnce(new Error('Send error')); const result = await service.sendAuthenticationRequiredEmail(email, data); expect(result.success).toBe(false); expect(result.error).toBe('Send error'); }); }); });