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