diff --git a/backend/src/disputes/disputes.service.spec.ts b/backend/src/disputes/disputes.service.spec.ts index 47dd8600..b49cf016 100644 --- a/backend/src/disputes/disputes.service.spec.ts +++ b/backend/src/disputes/disputes.service.spec.ts @@ -37,7 +37,7 @@ describe('DisputesService', () => { id: 'market-123', on_chain_market_id: 'chain-market-123', is_resolved: true, - resolution_time: new Date(Date.now() - 5 * 24 * 60 * 60 * 1000), // 5 days ago + resolved_at: new Date(Date.now() - 5 * 24 * 60 * 60 * 1000), // resolved 5 days ago } as Market; const mockDispute: Dispute = { @@ -153,7 +153,7 @@ describe('DisputesService', () => { it('should throw BadRequestException if dispute window has passed', async () => { const oldMarket = { ...mockMarket, - resolution_time: new Date(Date.now() - 10 * 24 * 60 * 60 * 1000), // 10 days ago + resolved_at: new Date(Date.now() - 10 * 24 * 60 * 60 * 1000), // resolved 10 days ago }; jest.spyOn(marketsRepository, 'findOne').mockResolvedValue(oldMarket); @@ -162,6 +162,38 @@ describe('DisputesService', () => { ); }); + it('should succeed when market was resolved 1 day ago (within 7-day window)', async () => { + const recentMarket = { + ...mockMarket, + resolved_at: new Date(Date.now() - 1 * 24 * 60 * 60 * 1000), // resolved 1 day ago + }; + jest.spyOn(marketsRepository, 'findOne').mockResolvedValue(recentMarket); + jest.spyOn(disputesRepository, 'findOne').mockResolvedValue(null); + jest.spyOn(disputesRepository, 'create').mockReturnValue(mockDispute); + jest.spyOn(disputesRepository, 'save').mockResolvedValue(mockDispute); + jest.spyOn(service, 'findOne').mockResolvedValue(mockDispute); + jest.spyOn(sorobanService, 'raiseDispute').mockResolvedValue({ + dispute_id: 'chain-dispute-123', + tx_hash: 'tx-hash-123', + }); + + const result = await service.create(createDisputeDto, mockUser); + + expect(result).toEqual(mockDispute); + }); + + it('should throw BadRequestException when market was resolved 8 days ago', async () => { + const staleMarket = { + ...mockMarket, + resolved_at: new Date(Date.now() - 8 * 24 * 60 * 60 * 1000), // resolved 8 days ago + }; + jest.spyOn(marketsRepository, 'findOne').mockResolvedValue(staleMarket); + + await expect(service.create(createDisputeDto, mockUser)).rejects.toThrow( + new BadRequestException('Dispute window has passed'), + ); + }); + it('should throw ConflictException if dispute already exists regardless of status', async () => { jest.spyOn(marketsRepository, 'findOne').mockResolvedValue(mockMarket); @@ -380,7 +412,7 @@ describe('DisputesService', () => { it('should return false if dispute window has passed', async () => { const oldMarket = { ...mockMarket, - resolution_time: new Date(Date.now() - 10 * 24 * 60 * 60 * 1000), // 10 days ago + resolved_at: new Date(Date.now() - 10 * 24 * 60 * 60 * 1000), // resolved 10 days ago }; jest.spyOn(marketsRepository, 'findOne').mockResolvedValue(oldMarket); diff --git a/backend/src/disputes/disputes.service.ts b/backend/src/disputes/disputes.service.ts index a7fa6957..bb2a926e 100644 --- a/backend/src/disputes/disputes.service.ts +++ b/backend/src/disputes/disputes.service.ts @@ -55,8 +55,8 @@ export class DisputesService { ); } - // Check if dispute window has passed (7 days after resolution) - const disputeWindowEnd = new Date(market.resolution_time); + // Check if dispute window has passed (7 days after actual resolution) + const disputeWindowEnd = new Date(market.resolved_at!); disputeWindowEnd.setDate(disputeWindowEnd.getDate() + 7); if (new Date() > disputeWindowEnd) { @@ -235,7 +235,7 @@ export class DisputesService { return false; } - const disputeWindowEnd = new Date(market.resolution_time); + const disputeWindowEnd = new Date(market.resolved_at!); disputeWindowEnd.setDate(disputeWindowEnd.getDate() + 7); return new Date() <= disputeWindowEnd; diff --git a/backend/src/markets/entities/market.entity.ts b/backend/src/markets/entities/market.entity.ts index baa451b0..9c7cc67a 100644 --- a/backend/src/markets/entities/market.entity.ts +++ b/backend/src/markets/entities/market.entity.ts @@ -63,6 +63,10 @@ export class Market { @IsString() resolved_outcome: string; + @Column({ type: 'timestamptz', nullable: true }) + @IsOptional() + resolved_at: Date | null; + @Column({ default: true }) @IsBoolean() is_public: boolean; diff --git a/backend/src/markets/markets.service.ts b/backend/src/markets/markets.service.ts index 964819ce..448c0c90 100644 --- a/backend/src/markets/markets.service.ts +++ b/backend/src/markets/markets.service.ts @@ -342,6 +342,7 @@ export class MarketsService { market.is_resolved = true; market.resolved_outcome = outcome; + market.resolved_at = new Date(); const saved = await this.marketsRepository.save(market); await this.webhookDispatcher.emit('market.resolved', { diff --git a/backend/src/notifications/notification-generator.service.spec.ts b/backend/src/notifications/notification-generator.service.spec.ts index 3f84fa97..50ce2d53 100644 --- a/backend/src/notifications/notification-generator.service.spec.ts +++ b/backend/src/notifications/notification-generator.service.spec.ts @@ -380,6 +380,58 @@ describe('NotificationGeneratorService', () => { }); }); + describe('queue flushing', () => { + it('should call save once with all 30 items when fewer than BATCH_SIZE are queued', async () => { + const saveSpy = jest + .spyOn(notificationsRepository, 'save') + .mockResolvedValue({} as any); + jest.spyOn(notificationsRepository, 'create').mockReturnValue({} as any); + + const notifications = Array.from({ length: 30 }, (_, i) => ({ + userAddress: `GUSER${i}`, + type: NotificationType.EventCreated, + title: 'Test', + message: 'Test message', + })); + + await service['queueBatchNotifications'](notifications); + await service.flushQueue(); + + expect(saveSpy).toHaveBeenCalledTimes(1); + expect(saveSpy.mock.calls[0][0]).toHaveLength(30); + }); + + it('should call save three times with batch sizes of 50, 50, and 10 for 110 items', async () => { + const saveSpy = jest + .spyOn(notificationsRepository, 'save') + .mockResolvedValue({} as any); + jest.spyOn(notificationsRepository, 'create').mockReturnValue({} as any); + + const notifications = Array.from({ length: 110 }, (_, i) => ({ + userAddress: `GUSER${i}`, + type: NotificationType.EventCreated, + title: 'Test', + message: 'Test message', + })); + + await service['queueBatchNotifications'](notifications); + await service.flushQueue(); + + expect(saveSpy).toHaveBeenCalledTimes(3); + expect(saveSpy.mock.calls[0][0]).toHaveLength(50); + expect(saveSpy.mock.calls[1][0]).toHaveLength(50); + expect(saveSpy.mock.calls[2][0]).toHaveLength(10); + }); + + it('should not call save when the queue is empty', async () => { + const saveSpy = jest.spyOn(notificationsRepository, 'save'); + + await service.flushQueue(); + + expect(saveSpy).not.toHaveBeenCalled(); + }); + }); + describe('flushQueue', () => { it('should flush all queued notifications', async () => { // Queue some notifications first diff --git a/backend/test/markets.e2e-spec.ts b/backend/test/markets.e2e-spec.ts index 63de877b..770dcb87 100644 --- a/backend/test/markets.e2e-spec.ts +++ b/backend/test/markets.e2e-spec.ts @@ -59,7 +59,10 @@ describe('Markets (e2e)', () => { is_public: true, is_resolved: false, resolved_outcome: null as any, // eslint-disable-line @typescript-eslint/no-unsafe-assignment + resolved_at: null, is_cancelled: false, + is_featured: false, + featured_at: null, total_pool_stroops: '0', participant_count: 0, created_at: new Date('2024-01-01'),