diff --git a/backend/src/disputes/disputes.controller.ts b/backend/src/disputes/disputes.controller.ts index e14aa1422..48e7630de 100644 --- a/backend/src/disputes/disputes.controller.ts +++ b/backend/src/disputes/disputes.controller.ts @@ -55,6 +55,40 @@ export class DisputesController { return this.disputesService.create(createDisputeDto, user); } + @Get('my') + @ApiOperation({ summary: 'Get disputes filed by the current user' }) + @ApiQuery({ + name: 'page', + required: false, + type: Number, + description: 'Page number', + }) + @ApiQuery({ + name: 'limit', + required: false, + type: Number, + description: 'Items per page', + }) + @ApiResponse({ + status: HttpStatus.OK, + description: 'Disputes retrieved successfully', + }) + async findMyDisputes( + @CurrentUser() user: User, + @Query('page') page?: string, + @Query('limit') limit?: string, + ): Promise<{ + disputes: Dispute[]; + total: number; + page: number; + limit: number; + }> { + const pageNum = page ? parseInt(page, 10) : 1; + const limitNum = limit ? parseInt(limit, 10) : 20; + + return this.disputesService.findMyDisputes(user.id, pageNum, limitNum); + } + @Get(':id') @ApiOperation({ summary: 'Get a dispute by ID' }) @ApiParam({ name: 'id', description: 'Dispute ID' }) diff --git a/backend/src/disputes/disputes.service.spec.ts b/backend/src/disputes/disputes.service.spec.ts index b6bd645b4..47dd86008 100644 --- a/backend/src/disputes/disputes.service.spec.ts +++ b/backend/src/disputes/disputes.service.spec.ts @@ -162,13 +162,19 @@ describe('DisputesService', () => { ); }); - it('should throw ConflictException if dispute already exists', async () => { + it('should throw ConflictException if dispute already exists regardless of status', async () => { jest.spyOn(marketsRepository, 'findOne').mockResolvedValue(mockMarket); - jest.spyOn(disputesRepository, 'findOne').mockResolvedValue(mockDispute); + + const resolvedDispute = { ...mockDispute, status: DisputeStatus.RESOLVED }; + jest.spyOn(disputesRepository, 'findOne').mockResolvedValue(resolvedDispute); await expect(service.create(createDisputeDto, mockUser)).rejects.toThrow( ConflictException, ); + + expect(disputesRepository.findOne).toHaveBeenCalledWith({ + where: { marketId: 'market-123' }, + }); }); }); @@ -275,6 +281,30 @@ describe('DisputesService', () => { }); }); + describe('findMyDisputes', () => { + it('should return paginated disputes for a user', async () => { + const disputes = [mockDispute]; + const mockFindAndCount: [Dispute[], number] = [disputes, 1]; + jest.spyOn(disputesRepository, 'findAndCount').mockResolvedValue(mockFindAndCount); + + const result = await service.findMyDisputes('user-123', 1, 20); + + expect(result).toEqual({ + disputes, + total: 1, + page: 1, + limit: 20, + }); + expect(disputesRepository.findAndCount).toHaveBeenCalledWith({ + where: { disputantId: 'user-123' }, + relations: ['market', 'resolvedBy'], + order: { createdAt: 'DESC' }, + skip: 0, + take: 20, + }); + }); + }); + describe('findAll', () => { it('should return paginated disputes', async () => { const disputes = [mockDispute]; diff --git a/backend/src/disputes/disputes.service.ts b/backend/src/disputes/disputes.service.ts index d324cb8f2..a7fa6957c 100644 --- a/backend/src/disputes/disputes.service.ts +++ b/backend/src/disputes/disputes.service.ts @@ -65,7 +65,7 @@ export class DisputesService { // Check if dispute already exists for this market const existingDispute = await this.disputesRepository.findOne({ - where: { marketId, status: DisputeStatus.PENDING }, + where: { marketId }, }); if (existingDispute) { @@ -163,6 +163,35 @@ export class DisputesService { }); } + /** + * Find disputes filed by a specific user with pagination + */ + async findMyDisputes( + userId: string, + page = 1, + limit = 20, + ): Promise<{ + disputes: Dispute[]; + total: number; + page: number; + limit: number; + }> { + const [disputes, total] = await this.disputesRepository.findAndCount({ + where: { disputantId: userId }, + relations: ['market', 'resolvedBy'], + order: { createdAt: 'DESC' }, + skip: (page - 1) * limit, + take: limit, + }); + + return { + disputes, + total, + page, + limit, + }; + } + /** * Find all disputes with pagination */ diff --git a/backend/src/search/dto/global-search.dto.ts b/backend/src/search/dto/global-search.dto.ts index fd78cc13d..661822b11 100644 --- a/backend/src/search/dto/global-search.dto.ts +++ b/backend/src/search/dto/global-search.dto.ts @@ -89,6 +89,9 @@ export class GlobalSearchResponseDto { competitions: CompetitionSearchResult[]; @ApiProperty() total: number; + @ApiPropertyOptional() total_markets?: number; + @ApiPropertyOptional() total_users?: number; + @ApiPropertyOptional() total_competitions?: number; @ApiProperty() page: number; @ApiProperty() limit: number; } diff --git a/backend/src/search/search.service.spec.ts b/backend/src/search/search.service.spec.ts index e15b40adb..ff3565028 100644 --- a/backend/src/search/search.service.spec.ts +++ b/backend/src/search/search.service.spec.ts @@ -23,6 +23,7 @@ type MockQb = jest.Mocked< | 'skip' | 'take' | 'getMany' + | 'getManyAndCount' > >; @@ -37,7 +38,8 @@ function makeQb(results: T[]): MockQb { skip: jest.fn().mockReturnThis(), take: jest.fn().mockReturnThis(), getMany: jest.fn().mockResolvedValue(results), - } as MockQb; + getManyAndCount: jest.fn().mockResolvedValue([results, results.length]), + } as unknown as MockQb; return qb; } diff --git a/backend/src/search/search.service.ts b/backend/src/search/search.service.ts index 66b9b9d96..a62db9252 100644 --- a/backend/src/search/search.service.ts +++ b/backend/src/search/search.service.ts @@ -31,28 +31,42 @@ export class SearchService { const searchType = dto.type ?? SearchType.All; const query = dto.query; - const [markets, users, competitions] = await Promise.all([ + const [ + [markets, total_markets], + [users, total_users], + [competitions, total_competitions], + ] = await Promise.all([ searchType === SearchType.All || searchType === SearchType.Markets ? this.searchMarkets(query, skip, limit) - : Promise.resolve([]), + : Promise.resolve([[], 0] as [Market[], number]), searchType === SearchType.All || searchType === SearchType.Users ? this.searchUsers(query, skip, limit) - : Promise.resolve([]), + : Promise.resolve([[], 0] as [User[], number]), searchType === SearchType.All || searchType === SearchType.Competitions ? this.searchCompetitions(query, skip, limit) - : Promise.resolve([]), + : Promise.resolve([[], 0] as [Competition[], number]), ]); - const total = markets.length + users.length + competitions.length; + const total = total_markets + total_users + total_competitions; - return { markets, users, competitions, total, page, limit }; + return { + markets, + users, + competitions, + total, + total_markets, + total_users, + total_competitions, + page, + limit, + }; } private async searchMarkets( query: string, skip: number, limit: number, - ): Promise { + ): Promise<[Market[], number]> { return this.marketsRepository .createQueryBuilder('market') .select([ @@ -75,14 +89,14 @@ export class SearchService { ) .skip(skip) .take(limit) - .getMany(); + .getManyAndCount(); } private async searchUsers( query: string, skip: number, limit: number, - ): Promise { + ): Promise<[User[], number]> { return this.usersRepository .createQueryBuilder('user') .select([ @@ -103,14 +117,14 @@ export class SearchService { ) .skip(skip) .take(limit) - .getMany(); + .getManyAndCount(); } private async searchCompetitions( query: string, skip: number, limit: number, - ): Promise { + ): Promise<[Competition[], number]> { return this.competitionsRepository .createQueryBuilder('competition') .select([ @@ -135,6 +149,6 @@ export class SearchService { ) .skip(skip) .take(limit) - .getMany(); + .getManyAndCount(); } }