284 lines
8.3 KiB
JavaScript
284 lines
8.3 KiB
JavaScript
// Mock dependencies before requiring the service
|
|
jest.mock('../../../models', () => ({
|
|
Rental: {
|
|
findOne: jest.fn(),
|
|
},
|
|
User: {},
|
|
Item: {},
|
|
}));
|
|
|
|
jest.mock('../../../services/email', () => ({
|
|
payment: {
|
|
sendDisputeAlertEmail: jest.fn(),
|
|
sendDisputeLostAlertEmail: jest.fn(),
|
|
},
|
|
}));
|
|
|
|
jest.mock('../../../utils/logger', () => ({
|
|
info: jest.fn(),
|
|
error: jest.fn(),
|
|
warn: jest.fn(),
|
|
}));
|
|
|
|
const { Rental } = require('../../../models');
|
|
const emailServices = require('../../../services/email');
|
|
const DisputeService = require('../../../services/disputeService');
|
|
|
|
describe('DisputeService', () => {
|
|
beforeEach(() => {
|
|
jest.clearAllMocks();
|
|
});
|
|
|
|
describe('handleDisputeCreated', () => {
|
|
const mockDispute = {
|
|
id: 'dp_123',
|
|
payment_intent: 'pi_456',
|
|
reason: 'fraudulent',
|
|
amount: 5000,
|
|
created: Math.floor(Date.now() / 1000),
|
|
evidence_details: {
|
|
due_by: Math.floor(Date.now() / 1000) + 86400 * 7,
|
|
},
|
|
};
|
|
|
|
it('should process dispute and update rental', async () => {
|
|
const mockRental = {
|
|
id: 'rental-123',
|
|
bankDepositStatus: 'pending',
|
|
owner: { email: 'owner@test.com', firstName: 'Owner' },
|
|
renter: { email: 'renter@test.com', firstName: 'Renter' },
|
|
item: { name: 'Test Item' },
|
|
update: jest.fn().mockResolvedValue(),
|
|
};
|
|
|
|
Rental.findOne.mockResolvedValue(mockRental);
|
|
emailServices.payment.sendDisputeAlertEmail.mockResolvedValue();
|
|
|
|
const result = await DisputeService.handleDisputeCreated(mockDispute);
|
|
|
|
expect(result.processed).toBe(true);
|
|
expect(result.rentalId).toBe('rental-123');
|
|
expect(mockRental.update).toHaveBeenCalledWith(
|
|
expect.objectContaining({
|
|
stripeDisputeId: 'dp_123',
|
|
stripeDisputeReason: 'fraudulent',
|
|
stripeDisputeAmount: 5000,
|
|
})
|
|
);
|
|
});
|
|
|
|
it('should put payout on hold if not yet deposited', async () => {
|
|
const mockRental = {
|
|
id: 'rental-123',
|
|
bankDepositStatus: 'pending',
|
|
owner: { email: 'owner@test.com' },
|
|
renter: { email: 'renter@test.com' },
|
|
item: { name: 'Test Item' },
|
|
update: jest.fn().mockResolvedValue(),
|
|
};
|
|
|
|
Rental.findOne.mockResolvedValue(mockRental);
|
|
emailServices.payment.sendDisputeAlertEmail.mockResolvedValue();
|
|
|
|
await DisputeService.handleDisputeCreated(mockDispute);
|
|
|
|
expect(mockRental.update).toHaveBeenCalledWith({ payoutStatus: 'on_hold' });
|
|
});
|
|
|
|
it('should not put payout on hold if already deposited', async () => {
|
|
const mockRental = {
|
|
id: 'rental-123',
|
|
bankDepositStatus: 'paid',
|
|
owner: { email: 'owner@test.com' },
|
|
renter: { email: 'renter@test.com' },
|
|
item: { name: 'Test Item' },
|
|
update: jest.fn().mockResolvedValue(),
|
|
};
|
|
|
|
Rental.findOne.mockResolvedValue(mockRental);
|
|
emailServices.payment.sendDisputeAlertEmail.mockResolvedValue();
|
|
|
|
await DisputeService.handleDisputeCreated(mockDispute);
|
|
|
|
// Should be called once for dispute info, not for on_hold
|
|
const updateCalls = mockRental.update.mock.calls;
|
|
const onHoldCall = updateCalls.find(call => call[0].payoutStatus === 'on_hold');
|
|
expect(onHoldCall).toBeUndefined();
|
|
});
|
|
|
|
it('should send dispute alert email', async () => {
|
|
const mockRental = {
|
|
id: 'rental-123',
|
|
bankDepositStatus: 'pending',
|
|
owner: { email: 'owner@test.com', firstName: 'Owner' },
|
|
renter: { email: 'renter@test.com', firstName: 'Renter' },
|
|
item: { name: 'Test Item' },
|
|
update: jest.fn().mockResolvedValue(),
|
|
};
|
|
|
|
Rental.findOne.mockResolvedValue(mockRental);
|
|
emailServices.payment.sendDisputeAlertEmail.mockResolvedValue();
|
|
|
|
await DisputeService.handleDisputeCreated(mockDispute);
|
|
|
|
expect(emailServices.payment.sendDisputeAlertEmail).toHaveBeenCalledWith(
|
|
expect.objectContaining({
|
|
rentalId: 'rental-123',
|
|
amount: 50, // Converted from cents
|
|
reason: 'fraudulent',
|
|
renterEmail: 'renter@test.com',
|
|
ownerEmail: 'owner@test.com',
|
|
})
|
|
);
|
|
});
|
|
|
|
it('should return not processed when rental not found', async () => {
|
|
Rental.findOne.mockResolvedValue(null);
|
|
|
|
const result = await DisputeService.handleDisputeCreated(mockDispute);
|
|
|
|
expect(result.processed).toBe(false);
|
|
expect(result.reason).toBe('rental_not_found');
|
|
});
|
|
});
|
|
|
|
describe('handleDisputeClosed', () => {
|
|
it('should process won dispute and resume payout', async () => {
|
|
const mockRental = {
|
|
id: 'rental-123',
|
|
payoutStatus: 'on_hold',
|
|
update: jest.fn().mockResolvedValue(),
|
|
};
|
|
|
|
Rental.findOne.mockResolvedValue(mockRental);
|
|
|
|
const mockDispute = {
|
|
id: 'dp_123',
|
|
status: 'won',
|
|
amount: 5000,
|
|
};
|
|
|
|
const result = await DisputeService.handleDisputeClosed(mockDispute);
|
|
|
|
expect(result.processed).toBe(true);
|
|
expect(result.won).toBe(true);
|
|
expect(mockRental.update).toHaveBeenCalledWith({ payoutStatus: 'pending' });
|
|
});
|
|
|
|
it('should process lost dispute and record loss', async () => {
|
|
const mockRental = {
|
|
id: 'rental-123',
|
|
payoutStatus: 'on_hold',
|
|
bankDepositStatus: 'pending',
|
|
owner: { email: 'owner@test.com' },
|
|
update: jest.fn().mockResolvedValue(),
|
|
};
|
|
|
|
Rental.findOne.mockResolvedValue(mockRental);
|
|
|
|
const mockDispute = {
|
|
id: 'dp_123',
|
|
status: 'lost',
|
|
amount: 5000,
|
|
};
|
|
|
|
const result = await DisputeService.handleDisputeClosed(mockDispute);
|
|
|
|
expect(result.processed).toBe(true);
|
|
expect(result.won).toBe(false);
|
|
expect(mockRental.update).toHaveBeenCalledWith({
|
|
stripeDisputeLost: true,
|
|
stripeDisputeLostAmount: 5000,
|
|
});
|
|
});
|
|
|
|
it('should send alert when dispute lost and owner already paid', async () => {
|
|
const mockRental = {
|
|
id: 'rental-123',
|
|
payoutStatus: 'on_hold',
|
|
bankDepositStatus: 'paid',
|
|
payoutAmount: 4500,
|
|
owner: { email: 'owner@test.com', firstName: 'Owner' },
|
|
update: jest.fn().mockResolvedValue(),
|
|
};
|
|
|
|
Rental.findOne.mockResolvedValue(mockRental);
|
|
emailServices.payment.sendDisputeLostAlertEmail.mockResolvedValue();
|
|
|
|
const mockDispute = {
|
|
id: 'dp_123',
|
|
status: 'lost',
|
|
amount: 5000,
|
|
};
|
|
|
|
await DisputeService.handleDisputeClosed(mockDispute);
|
|
|
|
expect(emailServices.payment.sendDisputeLostAlertEmail).toHaveBeenCalledWith(
|
|
expect.objectContaining({
|
|
rentalId: 'rental-123',
|
|
ownerAlreadyPaid: true,
|
|
ownerPayoutAmount: 4500,
|
|
})
|
|
);
|
|
});
|
|
|
|
it('should not send alert when dispute lost but owner not yet paid', async () => {
|
|
const mockRental = {
|
|
id: 'rental-123',
|
|
payoutStatus: 'on_hold',
|
|
bankDepositStatus: 'pending',
|
|
owner: { email: 'owner@test.com' },
|
|
update: jest.fn().mockResolvedValue(),
|
|
};
|
|
|
|
Rental.findOne.mockResolvedValue(mockRental);
|
|
|
|
const mockDispute = {
|
|
id: 'dp_123',
|
|
status: 'lost',
|
|
amount: 5000,
|
|
};
|
|
|
|
await DisputeService.handleDisputeClosed(mockDispute);
|
|
|
|
expect(emailServices.payment.sendDisputeLostAlertEmail).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('should return not processed when rental not found', async () => {
|
|
Rental.findOne.mockResolvedValue(null);
|
|
|
|
const mockDispute = {
|
|
id: 'dp_123',
|
|
status: 'won',
|
|
};
|
|
|
|
const result = await DisputeService.handleDisputeClosed(mockDispute);
|
|
|
|
expect(result.processed).toBe(false);
|
|
expect(result.reason).toBe('rental_not_found');
|
|
});
|
|
|
|
it('should handle warning_closed status as not won', async () => {
|
|
const mockRental = {
|
|
id: 'rental-123',
|
|
payoutStatus: 'pending',
|
|
bankDepositStatus: 'pending',
|
|
owner: { email: 'owner@test.com' },
|
|
update: jest.fn().mockResolvedValue(),
|
|
};
|
|
|
|
Rental.findOne.mockResolvedValue(mockRental);
|
|
|
|
const mockDispute = {
|
|
id: 'dp_123',
|
|
status: 'warning_closed',
|
|
amount: 5000,
|
|
};
|
|
|
|
const result = await DisputeService.handleDisputeClosed(mockDispute);
|
|
|
|
expect(result.won).toBe(false);
|
|
});
|
|
});
|
|
});
|