more unit tests
This commit is contained in:
@@ -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');
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user