Files
rentall-app/backend/tests/unit/services/disputeService.test.js
2026-01-18 19:18:35 -05:00

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);
});
});
});