747 lines
22 KiB
JavaScript
747 lines
22 KiB
JavaScript
// Mock dependencies - define mocks inline to avoid hoisting issues
|
|
jest.mock('../../../models', () => ({
|
|
Rental: {
|
|
findAll: jest.fn(),
|
|
update: jest.fn()
|
|
},
|
|
User: jest.fn(),
|
|
Item: jest.fn()
|
|
}));
|
|
|
|
jest.mock('../../../services/stripeService', () => ({
|
|
createTransfer: jest.fn()
|
|
}));
|
|
|
|
jest.mock('sequelize', () => ({
|
|
Op: {
|
|
not: 'not'
|
|
}
|
|
}));
|
|
|
|
const PayoutService = require('../../../services/payoutService');
|
|
const { Rental, User, Item } = require('../../../models');
|
|
const StripeService = require('../../../services/stripeService');
|
|
|
|
// Get references to mocks after importing
|
|
const mockRentalFindAll = Rental.findAll;
|
|
const mockRentalUpdate = Rental.update;
|
|
const mockUserModel = User;
|
|
const mockItemModel = Item;
|
|
const mockCreateTransfer = StripeService.createTransfer;
|
|
|
|
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
|
|
}
|
|
}
|
|
},
|
|
{
|
|
model: mockItemModel,
|
|
as: 'item'
|
|
}
|
|
]
|
|
});
|
|
|
|
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 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).toHaveBeenCalledWith({
|
|
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 failure status was set
|
|
expect(mockRental.update).toHaveBeenCalledWith({
|
|
payoutStatus: 'failed'
|
|
});
|
|
|
|
expect(consoleErrorSpy).toHaveBeenCalledWith(
|
|
'Error processing payout for rental 1:',
|
|
stripeError
|
|
);
|
|
});
|
|
|
|
it('should handle database update errors during processing', async () => {
|
|
// Stripe succeeds but database update fails
|
|
mockCreateTransfer.mockResolvedValue({
|
|
id: 'tr_123456789',
|
|
amount: 9500
|
|
});
|
|
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.mockRejectedValueOnce(dbError);
|
|
|
|
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.mockRejectedValueOnce(updateError);
|
|
|
|
// 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).toHaveBeenCalledWith({
|
|
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
|
|
}
|
|
}
|
|
},
|
|
{
|
|
model: mockItemModel,
|
|
as: 'item'
|
|
}
|
|
]
|
|
});
|
|
|
|
// 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);
|
|
});
|
|
});
|
|
}); |