more unit tests

This commit is contained in:
jackiettran
2026-01-19 00:29:28 -05:00
parent 75ddb2908f
commit d4362074f5
6 changed files with 1877 additions and 0 deletions

View File

@@ -0,0 +1,986 @@
// 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('<html>Test</html>'),
}));
});
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');
});
});
});