backend unit tests
This commit is contained in:
743
backend/tests/unit/services/payoutService.test.js
Normal file
743
backend/tests/unit/services/payoutService.test.js
Normal file
@@ -0,0 +1,743 @@
|
||||
// Mock dependencies
|
||||
const mockRentalFindAll = jest.fn();
|
||||
const mockRentalUpdate = jest.fn();
|
||||
const mockUserModel = jest.fn();
|
||||
const mockCreateTransfer = jest.fn();
|
||||
|
||||
jest.mock('../../../models', () => ({
|
||||
Rental: {
|
||||
findAll: mockRentalFindAll,
|
||||
update: mockRentalUpdate
|
||||
},
|
||||
User: mockUserModel
|
||||
}));
|
||||
|
||||
jest.mock('../../../services/stripeService', () => ({
|
||||
createTransfer: mockCreateTransfer
|
||||
}));
|
||||
|
||||
jest.mock('sequelize', () => ({
|
||||
Op: {
|
||||
not: 'not'
|
||||
}
|
||||
}));
|
||||
|
||||
const PayoutService = require('../../../services/payoutService');
|
||||
|
||||
describe('PayoutService', () => {
|
||||
let consoleSpy, consoleErrorSpy;
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
|
||||
// Set up console spies
|
||||
consoleSpy = jest.spyOn(console, 'log').mockImplementation();
|
||||
consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
consoleSpy.mockRestore();
|
||||
consoleErrorSpy.mockRestore();
|
||||
});
|
||||
|
||||
describe('getEligiblePayouts', () => {
|
||||
it('should return eligible rentals for payout', async () => {
|
||||
const mockRentals = [
|
||||
{
|
||||
id: 1,
|
||||
status: 'completed',
|
||||
paymentStatus: 'paid',
|
||||
payoutStatus: 'pending',
|
||||
owner: {
|
||||
id: 1,
|
||||
stripeConnectedAccountId: 'acct_123'
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
status: 'completed',
|
||||
paymentStatus: 'paid',
|
||||
payoutStatus: 'pending',
|
||||
owner: {
|
||||
id: 2,
|
||||
stripeConnectedAccountId: 'acct_456'
|
||||
}
|
||||
}
|
||||
];
|
||||
|
||||
mockRentalFindAll.mockResolvedValue(mockRentals);
|
||||
|
||||
const result = await PayoutService.getEligiblePayouts();
|
||||
|
||||
expect(mockRentalFindAll).toHaveBeenCalledWith({
|
||||
where: {
|
||||
status: 'completed',
|
||||
paymentStatus: 'paid',
|
||||
payoutStatus: 'pending'
|
||||
},
|
||||
include: [
|
||||
{
|
||||
model: mockUserModel,
|
||||
as: 'owner',
|
||||
where: {
|
||||
stripeConnectedAccountId: {
|
||||
'not': null
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
expect(result).toEqual(mockRentals);
|
||||
});
|
||||
|
||||
it('should handle database errors', async () => {
|
||||
const dbError = new Error('Database connection failed');
|
||||
mockRentalFindAll.mockRejectedValue(dbError);
|
||||
|
||||
await expect(PayoutService.getEligiblePayouts()).rejects.toThrow('Database connection failed');
|
||||
|
||||
expect(consoleErrorSpy).toHaveBeenCalledWith('Error getting eligible payouts:', dbError);
|
||||
});
|
||||
|
||||
it('should return empty array when no eligible rentals found', async () => {
|
||||
mockRentalFindAll.mockResolvedValue([]);
|
||||
|
||||
const result = await PayoutService.getEligiblePayouts();
|
||||
|
||||
expect(result).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('processRentalPayout', () => {
|
||||
let mockRental;
|
||||
|
||||
beforeEach(() => {
|
||||
mockRental = {
|
||||
id: 1,
|
||||
ownerId: 2,
|
||||
payoutStatus: 'pending',
|
||||
payoutAmount: 9500, // $95.00
|
||||
totalAmount: 10000, // $100.00
|
||||
platformFee: 500, // $5.00
|
||||
startDateTime: new Date('2023-01-01T10:00:00Z'),
|
||||
endDateTime: new Date('2023-01-02T10:00:00Z'),
|
||||
owner: {
|
||||
id: 2,
|
||||
stripeConnectedAccountId: 'acct_123'
|
||||
},
|
||||
update: jest.fn().mockResolvedValue(true)
|
||||
};
|
||||
});
|
||||
|
||||
describe('Validation', () => {
|
||||
it('should throw error when owner has no connected Stripe account', async () => {
|
||||
mockRental.owner.stripeConnectedAccountId = null;
|
||||
|
||||
await expect(PayoutService.processRentalPayout(mockRental))
|
||||
.rejects.toThrow('Owner does not have a connected Stripe account');
|
||||
});
|
||||
|
||||
it('should throw error when owner is missing', async () => {
|
||||
mockRental.owner = null;
|
||||
|
||||
await expect(PayoutService.processRentalPayout(mockRental))
|
||||
.rejects.toThrow('Owner does not have a connected Stripe account');
|
||||
});
|
||||
|
||||
it('should throw error when payout already processed', async () => {
|
||||
mockRental.payoutStatus = 'completed';
|
||||
|
||||
await expect(PayoutService.processRentalPayout(mockRental))
|
||||
.rejects.toThrow('Rental payout has already been processed');
|
||||
});
|
||||
|
||||
it('should throw error when payout amount is invalid', async () => {
|
||||
mockRental.payoutAmount = 0;
|
||||
|
||||
await expect(PayoutService.processRentalPayout(mockRental))
|
||||
.rejects.toThrow('Invalid payout amount');
|
||||
});
|
||||
|
||||
it('should throw error when payout amount is negative', async () => {
|
||||
mockRental.payoutAmount = -100;
|
||||
|
||||
await expect(PayoutService.processRentalPayout(mockRental))
|
||||
.rejects.toThrow('Invalid payout amount');
|
||||
});
|
||||
|
||||
it('should throw error when payout amount is null', async () => {
|
||||
mockRental.payoutAmount = null;
|
||||
|
||||
await expect(PayoutService.processRentalPayout(mockRental))
|
||||
.rejects.toThrow('Invalid payout amount');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Successful processing', () => {
|
||||
beforeEach(() => {
|
||||
mockCreateTransfer.mockResolvedValue({
|
||||
id: 'tr_123456789',
|
||||
amount: 9500,
|
||||
destination: 'acct_123'
|
||||
});
|
||||
});
|
||||
|
||||
it('should successfully process a rental payout', async () => {
|
||||
const result = await PayoutService.processRentalPayout(mockRental);
|
||||
|
||||
// Verify status update to processing
|
||||
expect(mockRental.update).toHaveBeenNthCalledWith(1, {
|
||||
payoutStatus: 'processing'
|
||||
});
|
||||
|
||||
// Verify Stripe transfer creation
|
||||
expect(mockCreateTransfer).toHaveBeenCalledWith({
|
||||
amount: 9500,
|
||||
destination: 'acct_123',
|
||||
metadata: {
|
||||
rentalId: 1,
|
||||
ownerId: 2,
|
||||
totalAmount: '10000',
|
||||
platformFee: '500',
|
||||
startDateTime: '2023-01-01T10:00:00.000Z',
|
||||
endDateTime: '2023-01-02T10:00:00.000Z'
|
||||
}
|
||||
});
|
||||
|
||||
// Verify status update to completed
|
||||
expect(mockRental.update).toHaveBeenNthCalledWith(2, {
|
||||
payoutStatus: 'completed',
|
||||
payoutProcessedAt: expect.any(Date),
|
||||
stripeTransferId: 'tr_123456789'
|
||||
});
|
||||
|
||||
// Verify success log
|
||||
expect(consoleSpy).toHaveBeenCalledWith(
|
||||
'Payout completed for rental 1: $9500 to acct_123'
|
||||
);
|
||||
|
||||
// Verify return value
|
||||
expect(result).toEqual({
|
||||
success: true,
|
||||
transferId: 'tr_123456789',
|
||||
amount: 9500
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle successful payout with different amounts', async () => {
|
||||
mockRental.payoutAmount = 15000;
|
||||
mockRental.totalAmount = 16000;
|
||||
mockRental.platformFee = 1000;
|
||||
|
||||
mockCreateTransfer.mockResolvedValue({
|
||||
id: 'tr_987654321',
|
||||
amount: 15000,
|
||||
destination: 'acct_123'
|
||||
});
|
||||
|
||||
const result = await PayoutService.processRentalPayout(mockRental);
|
||||
|
||||
expect(mockCreateTransfer).toHaveBeenCalledWith({
|
||||
amount: 15000,
|
||||
destination: 'acct_123',
|
||||
metadata: expect.objectContaining({
|
||||
totalAmount: '16000',
|
||||
platformFee: '1000'
|
||||
})
|
||||
});
|
||||
|
||||
expect(result.amount).toBe(15000);
|
||||
expect(result.transferId).toBe('tr_987654321');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Error handling', () => {
|
||||
it('should handle Stripe transfer creation errors', async () => {
|
||||
const stripeError = new Error('Stripe transfer failed');
|
||||
mockCreateTransfer.mockRejectedValue(stripeError);
|
||||
|
||||
await expect(PayoutService.processRentalPayout(mockRental))
|
||||
.rejects.toThrow('Stripe transfer failed');
|
||||
|
||||
// Verify processing status was set
|
||||
expect(mockRental.update).toHaveBeenNthCalledWith(1, {
|
||||
payoutStatus: 'processing'
|
||||
});
|
||||
|
||||
// Verify failure status was set
|
||||
expect(mockRental.update).toHaveBeenNthCalledWith(2, {
|
||||
payoutStatus: 'failed'
|
||||
});
|
||||
|
||||
expect(consoleErrorSpy).toHaveBeenCalledWith(
|
||||
'Error processing payout for rental 1:',
|
||||
stripeError
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle database update errors during processing', async () => {
|
||||
const dbError = new Error('Database update failed');
|
||||
mockRental.update.mockRejectedValueOnce(dbError);
|
||||
|
||||
await expect(PayoutService.processRentalPayout(mockRental))
|
||||
.rejects.toThrow('Database update failed');
|
||||
|
||||
expect(consoleErrorSpy).toHaveBeenCalledWith(
|
||||
'Error processing payout for rental 1:',
|
||||
dbError
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle database update errors during completion', async () => {
|
||||
mockCreateTransfer.mockResolvedValue({
|
||||
id: 'tr_123456789',
|
||||
amount: 9500
|
||||
});
|
||||
|
||||
const dbError = new Error('Database completion update failed');
|
||||
mockRental.update
|
||||
.mockResolvedValueOnce(true) // processing update succeeds
|
||||
.mockRejectedValueOnce(dbError); // completion update fails
|
||||
|
||||
await expect(PayoutService.processRentalPayout(mockRental))
|
||||
.rejects.toThrow('Database completion update failed');
|
||||
|
||||
expect(mockCreateTransfer).toHaveBeenCalled();
|
||||
expect(consoleErrorSpy).toHaveBeenCalledWith(
|
||||
'Error processing payout for rental 1:',
|
||||
dbError
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle failure status update errors gracefully', async () => {
|
||||
const stripeError = new Error('Stripe transfer failed');
|
||||
const updateError = new Error('Update failed status failed');
|
||||
|
||||
mockCreateTransfer.mockRejectedValue(stripeError);
|
||||
mockRental.update
|
||||
.mockResolvedValueOnce(true) // processing update succeeds
|
||||
.mockRejectedValueOnce(updateError); // failed status update fails
|
||||
|
||||
// The service will throw the update error since it happens in the catch block
|
||||
await expect(PayoutService.processRentalPayout(mockRental))
|
||||
.rejects.toThrow('Update failed status failed');
|
||||
|
||||
// Should still attempt to update to failed status
|
||||
expect(mockRental.update).toHaveBeenNthCalledWith(2, {
|
||||
payoutStatus: 'failed'
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('processAllEligiblePayouts', () => {
|
||||
beforeEach(() => {
|
||||
jest.spyOn(PayoutService, 'getEligiblePayouts');
|
||||
jest.spyOn(PayoutService, 'processRentalPayout');
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
PayoutService.getEligiblePayouts.mockRestore();
|
||||
PayoutService.processRentalPayout.mockRestore();
|
||||
});
|
||||
|
||||
it('should process all eligible payouts successfully', async () => {
|
||||
const mockRentals = [
|
||||
{ id: 1, payoutAmount: 9500 },
|
||||
{ id: 2, payoutAmount: 7500 }
|
||||
];
|
||||
|
||||
PayoutService.getEligiblePayouts.mockResolvedValue(mockRentals);
|
||||
PayoutService.processRentalPayout
|
||||
.mockResolvedValueOnce({
|
||||
success: true,
|
||||
transferId: 'tr_123',
|
||||
amount: 9500
|
||||
})
|
||||
.mockResolvedValueOnce({
|
||||
success: true,
|
||||
transferId: 'tr_456',
|
||||
amount: 7500
|
||||
});
|
||||
|
||||
const result = await PayoutService.processAllEligiblePayouts();
|
||||
|
||||
expect(consoleSpy).toHaveBeenCalledWith('Found 2 eligible rentals for payout');
|
||||
expect(consoleSpy).toHaveBeenCalledWith('Payout processing complete: 2 successful, 0 failed');
|
||||
|
||||
expect(result).toEqual({
|
||||
successful: [
|
||||
{ rentalId: 1, amount: 9500, transferId: 'tr_123' },
|
||||
{ rentalId: 2, amount: 7500, transferId: 'tr_456' }
|
||||
],
|
||||
failed: [],
|
||||
totalProcessed: 2
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle mixed success and failure results', async () => {
|
||||
const mockRentals = [
|
||||
{ id: 1, payoutAmount: 9500 },
|
||||
{ id: 2, payoutAmount: 7500 },
|
||||
{ id: 3, payoutAmount: 12000 }
|
||||
];
|
||||
|
||||
PayoutService.getEligiblePayouts.mockResolvedValue(mockRentals);
|
||||
PayoutService.processRentalPayout
|
||||
.mockResolvedValueOnce({
|
||||
success: true,
|
||||
transferId: 'tr_123',
|
||||
amount: 9500
|
||||
})
|
||||
.mockRejectedValueOnce(new Error('Stripe account suspended'))
|
||||
.mockResolvedValueOnce({
|
||||
success: true,
|
||||
transferId: 'tr_789',
|
||||
amount: 12000
|
||||
});
|
||||
|
||||
const result = await PayoutService.processAllEligiblePayouts();
|
||||
|
||||
expect(consoleSpy).toHaveBeenCalledWith('Found 3 eligible rentals for payout');
|
||||
expect(consoleSpy).toHaveBeenCalledWith('Payout processing complete: 2 successful, 1 failed');
|
||||
|
||||
expect(result).toEqual({
|
||||
successful: [
|
||||
{ rentalId: 1, amount: 9500, transferId: 'tr_123' },
|
||||
{ rentalId: 3, amount: 12000, transferId: 'tr_789' }
|
||||
],
|
||||
failed: [
|
||||
{ rentalId: 2, error: 'Stripe account suspended' }
|
||||
],
|
||||
totalProcessed: 3
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle no eligible payouts', async () => {
|
||||
PayoutService.getEligiblePayouts.mockResolvedValue([]);
|
||||
|
||||
const result = await PayoutService.processAllEligiblePayouts();
|
||||
|
||||
expect(consoleSpy).toHaveBeenCalledWith('Found 0 eligible rentals for payout');
|
||||
expect(consoleSpy).toHaveBeenCalledWith('Payout processing complete: 0 successful, 0 failed');
|
||||
|
||||
expect(result).toEqual({
|
||||
successful: [],
|
||||
failed: [],
|
||||
totalProcessed: 0
|
||||
});
|
||||
|
||||
expect(PayoutService.processRentalPayout).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should handle errors in getEligiblePayouts', async () => {
|
||||
const dbError = new Error('Database connection failed');
|
||||
PayoutService.getEligiblePayouts.mockRejectedValue(dbError);
|
||||
|
||||
await expect(PayoutService.processAllEligiblePayouts())
|
||||
.rejects.toThrow('Database connection failed');
|
||||
|
||||
expect(consoleErrorSpy).toHaveBeenCalledWith(
|
||||
'Error processing all eligible payouts:',
|
||||
dbError
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle all payouts failing', async () => {
|
||||
const mockRentals = [
|
||||
{ id: 1, payoutAmount: 9500 },
|
||||
{ id: 2, payoutAmount: 7500 }
|
||||
];
|
||||
|
||||
PayoutService.getEligiblePayouts.mockResolvedValue(mockRentals);
|
||||
PayoutService.processRentalPayout
|
||||
.mockRejectedValueOnce(new Error('Transfer failed'))
|
||||
.mockRejectedValueOnce(new Error('Account not found'));
|
||||
|
||||
const result = await PayoutService.processAllEligiblePayouts();
|
||||
|
||||
expect(result).toEqual({
|
||||
successful: [],
|
||||
failed: [
|
||||
{ rentalId: 1, error: 'Transfer failed' },
|
||||
{ rentalId: 2, error: 'Account not found' }
|
||||
],
|
||||
totalProcessed: 2
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('retryFailedPayouts', () => {
|
||||
beforeEach(() => {
|
||||
jest.spyOn(PayoutService, 'processRentalPayout');
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
PayoutService.processRentalPayout.mockRestore();
|
||||
});
|
||||
|
||||
it('should retry failed payouts successfully', async () => {
|
||||
const mockFailedRentals = [
|
||||
{
|
||||
id: 1,
|
||||
payoutAmount: 9500,
|
||||
update: jest.fn().mockResolvedValue(true)
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
payoutAmount: 7500,
|
||||
update: jest.fn().mockResolvedValue(true)
|
||||
}
|
||||
];
|
||||
|
||||
mockRentalFindAll.mockResolvedValue(mockFailedRentals);
|
||||
PayoutService.processRentalPayout
|
||||
.mockResolvedValueOnce({
|
||||
success: true,
|
||||
transferId: 'tr_retry_123',
|
||||
amount: 9500
|
||||
})
|
||||
.mockResolvedValueOnce({
|
||||
success: true,
|
||||
transferId: 'tr_retry_456',
|
||||
amount: 7500
|
||||
});
|
||||
|
||||
const result = await PayoutService.retryFailedPayouts();
|
||||
|
||||
// Verify query for failed rentals
|
||||
expect(mockRentalFindAll).toHaveBeenCalledWith({
|
||||
where: {
|
||||
status: 'completed',
|
||||
paymentStatus: 'paid',
|
||||
payoutStatus: 'failed'
|
||||
},
|
||||
include: [
|
||||
{
|
||||
model: mockUserModel,
|
||||
as: 'owner',
|
||||
where: {
|
||||
stripeConnectedAccountId: {
|
||||
'not': null
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
// Verify status reset to pending
|
||||
expect(mockFailedRentals[0].update).toHaveBeenCalledWith({ payoutStatus: 'pending' });
|
||||
expect(mockFailedRentals[1].update).toHaveBeenCalledWith({ payoutStatus: 'pending' });
|
||||
|
||||
// Verify processing attempts
|
||||
expect(PayoutService.processRentalPayout).toHaveBeenCalledWith(mockFailedRentals[0]);
|
||||
expect(PayoutService.processRentalPayout).toHaveBeenCalledWith(mockFailedRentals[1]);
|
||||
|
||||
// Verify logs
|
||||
expect(consoleSpy).toHaveBeenCalledWith('Found 2 failed payouts to retry');
|
||||
expect(consoleSpy).toHaveBeenCalledWith('Retry processing complete: 2 successful, 0 failed');
|
||||
|
||||
// Verify result
|
||||
expect(result).toEqual({
|
||||
successful: [
|
||||
{ rentalId: 1, amount: 9500, transferId: 'tr_retry_123' },
|
||||
{ rentalId: 2, amount: 7500, transferId: 'tr_retry_456' }
|
||||
],
|
||||
failed: [],
|
||||
totalProcessed: 2
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle mixed retry results', async () => {
|
||||
const mockFailedRentals = [
|
||||
{
|
||||
id: 1,
|
||||
payoutAmount: 9500,
|
||||
update: jest.fn().mockResolvedValue(true)
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
payoutAmount: 7500,
|
||||
update: jest.fn().mockResolvedValue(true)
|
||||
}
|
||||
];
|
||||
|
||||
mockRentalFindAll.mockResolvedValue(mockFailedRentals);
|
||||
PayoutService.processRentalPayout
|
||||
.mockResolvedValueOnce({
|
||||
success: true,
|
||||
transferId: 'tr_retry_123',
|
||||
amount: 9500
|
||||
})
|
||||
.mockRejectedValueOnce(new Error('Still failing'));
|
||||
|
||||
const result = await PayoutService.retryFailedPayouts();
|
||||
|
||||
expect(result).toEqual({
|
||||
successful: [
|
||||
{ rentalId: 1, amount: 9500, transferId: 'tr_retry_123' }
|
||||
],
|
||||
failed: [
|
||||
{ rentalId: 2, error: 'Still failing' }
|
||||
],
|
||||
totalProcessed: 2
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle no failed payouts to retry', async () => {
|
||||
mockRentalFindAll.mockResolvedValue([]);
|
||||
|
||||
const result = await PayoutService.retryFailedPayouts();
|
||||
|
||||
expect(consoleSpy).toHaveBeenCalledWith('Found 0 failed payouts to retry');
|
||||
expect(consoleSpy).toHaveBeenCalledWith('Retry processing complete: 0 successful, 0 failed');
|
||||
|
||||
expect(result).toEqual({
|
||||
successful: [],
|
||||
failed: [],
|
||||
totalProcessed: 0
|
||||
});
|
||||
|
||||
expect(PayoutService.processRentalPayout).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should handle errors in finding failed rentals', async () => {
|
||||
const dbError = new Error('Database query failed');
|
||||
mockRentalFindAll.mockRejectedValue(dbError);
|
||||
|
||||
await expect(PayoutService.retryFailedPayouts())
|
||||
.rejects.toThrow('Database query failed');
|
||||
|
||||
expect(consoleErrorSpy).toHaveBeenCalledWith(
|
||||
'Error retrying failed payouts:',
|
||||
dbError
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle status reset errors', async () => {
|
||||
const mockFailedRentals = [
|
||||
{
|
||||
id: 1,
|
||||
payoutAmount: 9500,
|
||||
update: jest.fn().mockRejectedValue(new Error('Status reset failed'))
|
||||
}
|
||||
];
|
||||
|
||||
mockRentalFindAll.mockResolvedValue(mockFailedRentals);
|
||||
|
||||
const result = await PayoutService.retryFailedPayouts();
|
||||
|
||||
expect(result.failed).toEqual([
|
||||
{ rentalId: 1, error: 'Status reset failed' }
|
||||
]);
|
||||
|
||||
expect(PayoutService.processRentalPayout).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Error logging', () => {
|
||||
it('should log errors with rental context in processRentalPayout', async () => {
|
||||
const mockRental = {
|
||||
id: 123,
|
||||
payoutStatus: 'pending',
|
||||
payoutAmount: 9500,
|
||||
owner: {
|
||||
stripeConnectedAccountId: 'acct_123'
|
||||
},
|
||||
update: jest.fn().mockRejectedValue(new Error('Update failed'))
|
||||
};
|
||||
|
||||
await expect(PayoutService.processRentalPayout(mockRental))
|
||||
.rejects.toThrow('Update failed');
|
||||
|
||||
expect(consoleErrorSpy).toHaveBeenCalledWith(
|
||||
'Error processing payout for rental 123:',
|
||||
expect.any(Error)
|
||||
);
|
||||
});
|
||||
|
||||
it('should log aggregate results in processAllEligiblePayouts', async () => {
|
||||
jest.spyOn(PayoutService, 'getEligiblePayouts').mockResolvedValue([
|
||||
{ id: 1 }, { id: 2 }, { id: 3 }
|
||||
]);
|
||||
jest.spyOn(PayoutService, 'processRentalPayout')
|
||||
.mockResolvedValueOnce({ amount: 100, transferId: 'tr_1' })
|
||||
.mockRejectedValueOnce(new Error('Failed'))
|
||||
.mockResolvedValueOnce({ amount: 300, transferId: 'tr_3' });
|
||||
|
||||
await PayoutService.processAllEligiblePayouts();
|
||||
|
||||
expect(consoleSpy).toHaveBeenCalledWith('Found 3 eligible rentals for payout');
|
||||
expect(consoleSpy).toHaveBeenCalledWith('Payout processing complete: 2 successful, 1 failed');
|
||||
|
||||
PayoutService.getEligiblePayouts.mockRestore();
|
||||
PayoutService.processRentalPayout.mockRestore();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Edge cases', () => {
|
||||
it('should handle rental with undefined owner', async () => {
|
||||
const mockRental = {
|
||||
id: 1,
|
||||
payoutStatus: 'pending',
|
||||
payoutAmount: 9500,
|
||||
owner: undefined,
|
||||
update: jest.fn()
|
||||
};
|
||||
|
||||
await expect(PayoutService.processRentalPayout(mockRental))
|
||||
.rejects.toThrow('Owner does not have a connected Stripe account');
|
||||
});
|
||||
|
||||
it('should handle rental with empty string Stripe account ID', async () => {
|
||||
const mockRental = {
|
||||
id: 1,
|
||||
payoutStatus: 'pending',
|
||||
payoutAmount: 9500,
|
||||
owner: {
|
||||
stripeConnectedAccountId: ''
|
||||
},
|
||||
update: jest.fn()
|
||||
};
|
||||
|
||||
await expect(PayoutService.processRentalPayout(mockRental))
|
||||
.rejects.toThrow('Owner does not have a connected Stripe account');
|
||||
});
|
||||
|
||||
it('should handle very large payout amounts', async () => {
|
||||
const mockRental = {
|
||||
id: 1,
|
||||
ownerId: 2,
|
||||
payoutStatus: 'pending',
|
||||
payoutAmount: 999999999, // Very large amount
|
||||
totalAmount: 1000000000,
|
||||
platformFee: 1,
|
||||
startDateTime: new Date('2023-01-01T10:00:00Z'),
|
||||
endDateTime: new Date('2023-01-02T10:00:00Z'),
|
||||
owner: {
|
||||
stripeConnectedAccountId: 'acct_123'
|
||||
},
|
||||
update: jest.fn().mockResolvedValue(true)
|
||||
};
|
||||
|
||||
mockCreateTransfer.mockResolvedValue({
|
||||
id: 'tr_large_amount',
|
||||
amount: 999999999
|
||||
});
|
||||
|
||||
const result = await PayoutService.processRentalPayout(mockRental);
|
||||
|
||||
expect(mockCreateTransfer).toHaveBeenCalledWith({
|
||||
amount: 999999999,
|
||||
destination: 'acct_123',
|
||||
metadata: expect.objectContaining({
|
||||
totalAmount: '1000000000',
|
||||
platformFee: '1'
|
||||
})
|
||||
});
|
||||
|
||||
expect(result.amount).toBe(999999999);
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user