backend unit test coverage to 80%
This commit is contained in:
@@ -2,6 +2,7 @@
|
||||
jest.mock('../../../models', () => ({
|
||||
Rental: {
|
||||
findAll: jest.fn(),
|
||||
findByPk: jest.fn(),
|
||||
update: jest.fn()
|
||||
},
|
||||
User: jest.fn(),
|
||||
@@ -12,6 +13,14 @@ jest.mock('../../../services/stripeService', () => ({
|
||||
createTransfer: jest.fn()
|
||||
}));
|
||||
|
||||
// Mock email services
|
||||
const mockSendPayoutReceivedEmail = jest.fn();
|
||||
jest.mock('../../../services/email', () => ({
|
||||
rentalFlow: {
|
||||
sendPayoutReceivedEmail: mockSendPayoutReceivedEmail
|
||||
}
|
||||
}));
|
||||
|
||||
jest.mock('sequelize', () => ({
|
||||
Op: {
|
||||
not: 'not'
|
||||
@@ -37,6 +46,7 @@ const StripeService = require('../../../services/stripeService');
|
||||
|
||||
// Get references to mocks after importing
|
||||
const mockRentalFindAll = Rental.findAll;
|
||||
const mockRentalFindByPk = Rental.findByPk;
|
||||
const mockRentalUpdate = Rental.update;
|
||||
const mockUserModel = User;
|
||||
const mockItemModel = Item;
|
||||
@@ -755,4 +765,284 @@ describe('PayoutService', () => {
|
||||
expect(result.amount).toBe(999999999);
|
||||
});
|
||||
});
|
||||
|
||||
describe('triggerPayoutOnCompletion', () => {
|
||||
beforeEach(() => {
|
||||
mockRentalFindByPk.mockReset();
|
||||
mockSendPayoutReceivedEmail.mockReset();
|
||||
});
|
||||
|
||||
it('should return rental_not_found when rental does not exist', async () => {
|
||||
mockRentalFindByPk.mockResolvedValue(null);
|
||||
|
||||
const result = await PayoutService.triggerPayoutOnCompletion('nonexistent-rental-id');
|
||||
|
||||
expect(result).toEqual({
|
||||
attempted: false,
|
||||
success: false,
|
||||
reason: 'rental_not_found'
|
||||
});
|
||||
});
|
||||
|
||||
it('should return payment_not_paid when paymentStatus is not paid', async () => {
|
||||
const mockRental = {
|
||||
id: 'rental-123',
|
||||
paymentStatus: 'pending',
|
||||
payoutStatus: 'pending',
|
||||
owner: {
|
||||
stripeConnectedAccountId: 'acct_123',
|
||||
stripePayoutsEnabled: true
|
||||
}
|
||||
};
|
||||
mockRentalFindByPk.mockResolvedValue(mockRental);
|
||||
|
||||
const result = await PayoutService.triggerPayoutOnCompletion('rental-123');
|
||||
|
||||
expect(result).toEqual({
|
||||
attempted: false,
|
||||
success: false,
|
||||
reason: 'payment_not_paid'
|
||||
});
|
||||
});
|
||||
|
||||
it('should return payout_not_pending when payoutStatus is not pending', async () => {
|
||||
const mockRental = {
|
||||
id: 'rental-123',
|
||||
paymentStatus: 'paid',
|
||||
payoutStatus: 'completed',
|
||||
owner: {
|
||||
stripeConnectedAccountId: 'acct_123',
|
||||
stripePayoutsEnabled: true
|
||||
}
|
||||
};
|
||||
mockRentalFindByPk.mockResolvedValue(mockRental);
|
||||
|
||||
const result = await PayoutService.triggerPayoutOnCompletion('rental-123');
|
||||
|
||||
expect(result).toEqual({
|
||||
attempted: false,
|
||||
success: false,
|
||||
reason: 'payout_not_pending'
|
||||
});
|
||||
});
|
||||
|
||||
it('should return no_stripe_account when owner has no stripeConnectedAccountId', async () => {
|
||||
const mockRental = {
|
||||
id: 'rental-123',
|
||||
ownerId: 1,
|
||||
paymentStatus: 'paid',
|
||||
payoutStatus: 'pending',
|
||||
owner: {
|
||||
stripeConnectedAccountId: null,
|
||||
stripePayoutsEnabled: false
|
||||
}
|
||||
};
|
||||
mockRentalFindByPk.mockResolvedValue(mockRental);
|
||||
|
||||
const result = await PayoutService.triggerPayoutOnCompletion('rental-123');
|
||||
|
||||
expect(result).toEqual({
|
||||
attempted: false,
|
||||
success: false,
|
||||
reason: 'no_stripe_account'
|
||||
});
|
||||
});
|
||||
|
||||
it('should return payouts_not_enabled when owner stripePayoutsEnabled is false', async () => {
|
||||
const mockRental = {
|
||||
id: 'rental-123',
|
||||
ownerId: 1,
|
||||
paymentStatus: 'paid',
|
||||
payoutStatus: 'pending',
|
||||
owner: {
|
||||
stripeConnectedAccountId: 'acct_123',
|
||||
stripePayoutsEnabled: false
|
||||
}
|
||||
};
|
||||
mockRentalFindByPk.mockResolvedValue(mockRental);
|
||||
|
||||
const result = await PayoutService.triggerPayoutOnCompletion('rental-123');
|
||||
|
||||
expect(result).toEqual({
|
||||
attempted: false,
|
||||
success: false,
|
||||
reason: 'payouts_not_enabled'
|
||||
});
|
||||
});
|
||||
|
||||
it('should successfully process payout when all conditions are met', async () => {
|
||||
const mockRental = {
|
||||
id: 'rental-123',
|
||||
ownerId: 2,
|
||||
paymentStatus: 'paid',
|
||||
payoutStatus: 'pending',
|
||||
payoutAmount: 9500,
|
||||
totalAmount: 10000,
|
||||
platformFee: 500,
|
||||
startDateTime: new Date('2023-01-01T10:00:00Z'),
|
||||
endDateTime: new Date('2023-01-02T10:00:00Z'),
|
||||
owner: {
|
||||
id: 2,
|
||||
email: 'owner@example.com',
|
||||
firstName: 'John',
|
||||
stripeConnectedAccountId: 'acct_123',
|
||||
stripePayoutsEnabled: true
|
||||
},
|
||||
update: jest.fn().mockResolvedValue(true)
|
||||
};
|
||||
mockRentalFindByPk.mockResolvedValue(mockRental);
|
||||
mockCreateTransfer.mockResolvedValue({
|
||||
id: 'tr_success_123',
|
||||
amount: 9500
|
||||
});
|
||||
mockSendPayoutReceivedEmail.mockResolvedValue(true);
|
||||
|
||||
const result = await PayoutService.triggerPayoutOnCompletion('rental-123');
|
||||
|
||||
expect(result).toEqual({
|
||||
attempted: true,
|
||||
success: true,
|
||||
transferId: 'tr_success_123',
|
||||
amount: 9500
|
||||
});
|
||||
expect(mockCreateTransfer).toHaveBeenCalled();
|
||||
expect(mockRental.update).toHaveBeenCalledWith({
|
||||
payoutStatus: 'completed',
|
||||
payoutProcessedAt: expect.any(Date),
|
||||
stripeTransferId: 'tr_success_123'
|
||||
});
|
||||
});
|
||||
|
||||
it('should return payout_failed on processRentalPayout error', async () => {
|
||||
const mockRental = {
|
||||
id: 'rental-123',
|
||||
ownerId: 2,
|
||||
paymentStatus: 'paid',
|
||||
payoutStatus: 'pending',
|
||||
payoutAmount: 9500,
|
||||
totalAmount: 10000,
|
||||
platformFee: 500,
|
||||
startDateTime: new Date('2023-01-01T10:00:00Z'),
|
||||
endDateTime: new Date('2023-01-02T10:00:00Z'),
|
||||
owner: {
|
||||
id: 2,
|
||||
email: 'owner@example.com',
|
||||
firstName: 'John',
|
||||
stripeConnectedAccountId: 'acct_123',
|
||||
stripePayoutsEnabled: true
|
||||
},
|
||||
update: jest.fn().mockResolvedValue(true)
|
||||
};
|
||||
mockRentalFindByPk.mockResolvedValue(mockRental);
|
||||
mockCreateTransfer.mockRejectedValue(new Error('Stripe transfer failed'));
|
||||
|
||||
const result = await PayoutService.triggerPayoutOnCompletion('rental-123');
|
||||
|
||||
expect(result).toEqual({
|
||||
attempted: true,
|
||||
success: false,
|
||||
reason: 'payout_failed',
|
||||
error: 'Stripe transfer failed'
|
||||
});
|
||||
});
|
||||
|
||||
it('should include Item model in findByPk query', async () => {
|
||||
mockRentalFindByPk.mockResolvedValue(null);
|
||||
|
||||
await PayoutService.triggerPayoutOnCompletion('rental-123');
|
||||
|
||||
expect(mockRentalFindByPk).toHaveBeenCalledWith('rental-123', {
|
||||
include: [
|
||||
{
|
||||
model: mockUserModel,
|
||||
as: 'owner',
|
||||
attributes: ['id', 'email', 'firstName', 'lastName', 'stripeConnectedAccountId', 'stripePayoutsEnabled']
|
||||
},
|
||||
{ model: mockItemModel, as: 'item' }
|
||||
]
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('processRentalPayout - email notifications', () => {
|
||||
let mockRental;
|
||||
|
||||
beforeEach(() => {
|
||||
mockSendPayoutReceivedEmail.mockReset();
|
||||
mockRental = {
|
||||
id: 1,
|
||||
ownerId: 2,
|
||||
payoutStatus: 'pending',
|
||||
payoutAmount: 9500,
|
||||
totalAmount: 10000,
|
||||
platformFee: 500,
|
||||
startDateTime: new Date('2023-01-01T10:00:00Z'),
|
||||
endDateTime: new Date('2023-01-02T10:00:00Z'),
|
||||
owner: {
|
||||
id: 2,
|
||||
email: 'owner@example.com',
|
||||
firstName: 'John',
|
||||
stripeConnectedAccountId: 'acct_123'
|
||||
},
|
||||
update: jest.fn().mockResolvedValue(true)
|
||||
};
|
||||
mockCreateTransfer.mockResolvedValue({
|
||||
id: 'tr_123456789',
|
||||
amount: 9500
|
||||
});
|
||||
});
|
||||
|
||||
it('should send payout notification email on successful payout', async () => {
|
||||
mockSendPayoutReceivedEmail.mockResolvedValue(true);
|
||||
|
||||
await PayoutService.processRentalPayout(mockRental);
|
||||
|
||||
expect(mockSendPayoutReceivedEmail).toHaveBeenCalledWith(
|
||||
mockRental.owner,
|
||||
mockRental
|
||||
);
|
||||
expect(mockLoggerInfo).toHaveBeenCalledWith(
|
||||
'Payout notification email sent to owner',
|
||||
expect.objectContaining({
|
||||
rentalId: 1,
|
||||
ownerId: 2
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it('should continue successfully even if email sending fails', async () => {
|
||||
mockSendPayoutReceivedEmail.mockRejectedValue(new Error('Email service unavailable'));
|
||||
|
||||
const result = await PayoutService.processRentalPayout(mockRental);
|
||||
|
||||
// Payout should still succeed
|
||||
expect(result).toEqual({
|
||||
success: true,
|
||||
transferId: 'tr_123456789',
|
||||
amount: 9500
|
||||
});
|
||||
|
||||
// Error should be logged
|
||||
expect(mockLoggerError).toHaveBeenCalledWith(
|
||||
'Failed to send payout notification email',
|
||||
expect.objectContaining({
|
||||
error: 'Email service unavailable',
|
||||
rentalId: 1,
|
||||
ownerId: 2
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it('should still update rental status even if email fails', async () => {
|
||||
mockSendPayoutReceivedEmail.mockRejectedValue(new Error('Email error'));
|
||||
|
||||
await PayoutService.processRentalPayout(mockRental);
|
||||
|
||||
expect(mockRental.update).toHaveBeenCalledWith({
|
||||
payoutStatus: 'completed',
|
||||
payoutProcessedAt: expect.any(Date),
|
||||
stripeTransferId: 'tr_123456789'
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user