diff --git a/.gitignore b/.gitignore index f822be10..df253997 100644 --- a/.gitignore +++ b/.gitignore @@ -15,3 +15,5 @@ build/ /node_modules/ /dist/ /build/ + +issue.md \ No newline at end of file diff --git a/backend/lint_report.json b/backend/lint_report.json new file mode 100644 index 00000000..df82f3b7 --- /dev/null +++ b/backend/lint_report.json @@ -0,0 +1 @@ +[{"filePath":"/home/solodev/Documents/dripsNetwork/second/InsightArena/backend/src/admin/admin.controller.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/solodev/Documents/dripsNetwork/second/InsightArena/backend/src/admin/admin.module.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/solodev/Documents/dripsNetwork/second/InsightArena/backend/src/admin/admin.service.spec.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/solodev/Documents/dripsNetwork/second/InsightArena/backend/src/admin/admin.service.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/solodev/Documents/dripsNetwork/second/InsightArena/backend/src/admin/dto/activity-log-query.dto.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/solodev/Documents/dripsNetwork/second/InsightArena/backend/src/admin/dto/ban-user.dto.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/solodev/Documents/dripsNetwork/second/InsightArena/backend/src/admin/dto/list-users-query.dto.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/solodev/Documents/dripsNetwork/second/InsightArena/backend/src/admin/dto/moderate-comment.dto.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/solodev/Documents/dripsNetwork/second/InsightArena/backend/src/admin/dto/report-query.dto.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/solodev/Documents/dripsNetwork/second/InsightArena/backend/src/admin/dto/resolve-market.dto.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/solodev/Documents/dripsNetwork/second/InsightArena/backend/src/admin/dto/stats-response.dto.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/solodev/Documents/dripsNetwork/second/InsightArena/backend/src/analytics/analytics.controller.spec.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/solodev/Documents/dripsNetwork/second/InsightArena/backend/src/analytics/analytics.controller.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/solodev/Documents/dripsNetwork/second/InsightArena/backend/src/analytics/analytics.module.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/solodev/Documents/dripsNetwork/second/InsightArena/backend/src/analytics/analytics.service.spec.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/solodev/Documents/dripsNetwork/second/InsightArena/backend/src/analytics/analytics.service.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/solodev/Documents/dripsNetwork/second/InsightArena/backend/src/analytics/dto/dashboard-kpis.dto.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/solodev/Documents/dripsNetwork/second/InsightArena/backend/src/analytics/dto/market-analytics.dto.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/solodev/Documents/dripsNetwork/second/InsightArena/backend/src/analytics/entities/activity-log.entity.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/solodev/Documents/dripsNetwork/second/InsightArena/backend/src/app.controller.spec.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/solodev/Documents/dripsNetwork/second/InsightArena/backend/src/app.controller.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/solodev/Documents/dripsNetwork/second/InsightArena/backend/src/app.module.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/solodev/Documents/dripsNetwork/second/InsightArena/backend/src/app.service.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/solodev/Documents/dripsNetwork/second/InsightArena/backend/src/auth/auth.controller.spec.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/solodev/Documents/dripsNetwork/second/InsightArena/backend/src/auth/auth.controller.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/solodev/Documents/dripsNetwork/second/InsightArena/backend/src/auth/auth.e2e.spec.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/solodev/Documents/dripsNetwork/second/InsightArena/backend/src/auth/auth.module.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/solodev/Documents/dripsNetwork/second/InsightArena/backend/src/auth/auth.service.spec.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/solodev/Documents/dripsNetwork/second/InsightArena/backend/src/auth/auth.service.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/solodev/Documents/dripsNetwork/second/InsightArena/backend/src/auth/dto/generate-challenge.dto.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/solodev/Documents/dripsNetwork/second/InsightArena/backend/src/auth/dto/verify-challenge.dto.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/solodev/Documents/dripsNetwork/second/InsightArena/backend/src/auth/strategies/jwt.strategy.spec.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/solodev/Documents/dripsNetwork/second/InsightArena/backend/src/auth/strategies/jwt.strategy.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/solodev/Documents/dripsNetwork/second/InsightArena/backend/src/common/common.module.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/solodev/Documents/dripsNetwork/second/InsightArena/backend/src/common/decorators/current-user.decorator.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/solodev/Documents/dripsNetwork/second/InsightArena/backend/src/common/decorators/public.decorator.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/solodev/Documents/dripsNetwork/second/InsightArena/backend/src/common/decorators/roles.decorator.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/solodev/Documents/dripsNetwork/second/InsightArena/backend/src/common/enums/role.enum.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/solodev/Documents/dripsNetwork/second/InsightArena/backend/src/common/filters/http-exception.filter.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/solodev/Documents/dripsNetwork/second/InsightArena/backend/src/common/guards/ban.guard.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/solodev/Documents/dripsNetwork/second/InsightArena/backend/src/common/guards/jwt-auth.guard.spec.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/solodev/Documents/dripsNetwork/second/InsightArena/backend/src/common/guards/jwt-auth.guard.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/solodev/Documents/dripsNetwork/second/InsightArena/backend/src/common/guards/roles.guard.spec.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/solodev/Documents/dripsNetwork/second/InsightArena/backend/src/common/guards/roles.guard.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/solodev/Documents/dripsNetwork/second/InsightArena/backend/src/common/interceptors/activity-logging.interceptor.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/solodev/Documents/dripsNetwork/second/InsightArena/backend/src/common/interceptors/response.interceptor.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/solodev/Documents/dripsNetwork/second/InsightArena/backend/src/competitions/competitions.controller.spec.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/solodev/Documents/dripsNetwork/second/InsightArena/backend/src/competitions/competitions.controller.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/solodev/Documents/dripsNetwork/second/InsightArena/backend/src/competitions/competitions.module.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/solodev/Documents/dripsNetwork/second/InsightArena/backend/src/competitions/competitions.service.spec.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/solodev/Documents/dripsNetwork/second/InsightArena/backend/src/competitions/competitions.service.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/solodev/Documents/dripsNetwork/second/InsightArena/backend/src/competitions/dto/create-competition.dto.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/solodev/Documents/dripsNetwork/second/InsightArena/backend/src/competitions/dto/list-competitions.dto.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/solodev/Documents/dripsNetwork/second/InsightArena/backend/src/competitions/entities/competition-participant.entity.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/solodev/Documents/dripsNetwork/second/InsightArena/backend/src/competitions/entities/competition.entity.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/solodev/Documents/dripsNetwork/second/InsightArena/backend/src/config/env.validation.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/solodev/Documents/dripsNetwork/second/InsightArena/backend/src/config/typeorm.config.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/solodev/Documents/dripsNetwork/second/InsightArena/backend/src/health/health.controller.spec.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/solodev/Documents/dripsNetwork/second/InsightArena/backend/src/health/health.controller.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/solodev/Documents/dripsNetwork/second/InsightArena/backend/src/health/health.module.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/solodev/Documents/dripsNetwork/second/InsightArena/backend/src/health/health.service.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/solodev/Documents/dripsNetwork/second/InsightArena/backend/src/leaderboard/dto/leaderboard-query.dto.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/solodev/Documents/dripsNetwork/second/InsightArena/backend/src/leaderboard/entities/leaderboard-entry.entity.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/solodev/Documents/dripsNetwork/second/InsightArena/backend/src/leaderboard/leaderboard.controller.spec.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/solodev/Documents/dripsNetwork/second/InsightArena/backend/src/leaderboard/leaderboard.controller.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/solodev/Documents/dripsNetwork/second/InsightArena/backend/src/leaderboard/leaderboard.module.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/solodev/Documents/dripsNetwork/second/InsightArena/backend/src/leaderboard/leaderboard.scheduler.spec.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/solodev/Documents/dripsNetwork/second/InsightArena/backend/src/leaderboard/leaderboard.scheduler.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/solodev/Documents/dripsNetwork/second/InsightArena/backend/src/leaderboard/leaderboard.service.spec.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/solodev/Documents/dripsNetwork/second/InsightArena/backend/src/leaderboard/leaderboard.service.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/solodev/Documents/dripsNetwork/second/InsightArena/backend/src/main.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/solodev/Documents/dripsNetwork/second/InsightArena/backend/src/markets/dto/create-comment.dto.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/solodev/Documents/dripsNetwork/second/InsightArena/backend/src/markets/dto/create-market.dto.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/solodev/Documents/dripsNetwork/second/InsightArena/backend/src/markets/dto/list-markets.dto.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/solodev/Documents/dripsNetwork/second/InsightArena/backend/src/markets/dto/market-response.dto.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/solodev/Documents/dripsNetwork/second/InsightArena/backend/src/markets/dto/prediction-stats.dto.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/solodev/Documents/dripsNetwork/second/InsightArena/backend/src/markets/entities/comment.entity.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/solodev/Documents/dripsNetwork/second/InsightArena/backend/src/markets/entities/market-template.entity.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/solodev/Documents/dripsNetwork/second/InsightArena/backend/src/markets/entities/market.entity.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/solodev/Documents/dripsNetwork/second/InsightArena/backend/src/markets/markets.controller.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/solodev/Documents/dripsNetwork/second/InsightArena/backend/src/markets/markets.module.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/solodev/Documents/dripsNetwork/second/InsightArena/backend/src/markets/markets.service.spec.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/solodev/Documents/dripsNetwork/second/InsightArena/backend/src/markets/markets.service.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/solodev/Documents/dripsNetwork/second/InsightArena/backend/src/migrations/1774313247489-CreateUserEntity.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/solodev/Documents/dripsNetwork/second/InsightArena/backend/src/migrations/1774431698000-CreateMarketEntity.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/solodev/Documents/dripsNetwork/second/InsightArena/backend/src/migrations/1774500000000-CreateNotificationEntity.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/solodev/Documents/dripsNetwork/second/InsightArena/backend/src/migrations/1774500000000-CreatePredictionEntity.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/solodev/Documents/dripsNetwork/second/InsightArena/backend/src/migrations/1774500001000-CreateCompetitionEntity.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/solodev/Documents/dripsNetwork/second/InsightArena/backend/src/migrations/1774500002000-CreateLeaderboardEntryEntity.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/solodev/Documents/dripsNetwork/second/InsightArena/backend/src/migrations/1774500003000-AddUniqueConstraintLeaderboardEntryUserSeason.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/solodev/Documents/dripsNetwork/second/InsightArena/backend/src/migrations/1774600000000-CreateSystemStateEntity.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/solodev/Documents/dripsNetwork/second/InsightArena/backend/src/migrations/1774650000000-CreateSeasonsTable.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/solodev/Documents/dripsNetwork/second/InsightArena/backend/src/migrations/1774660000000-AddSeasonFinalizationColumns.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/solodev/Documents/dripsNetwork/second/InsightArena/backend/src/migrations/1774670000000-AdminFeatures.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/solodev/Documents/dripsNetwork/second/InsightArena/backend/src/migrations/1774800000000-CreateCommentsTable.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/solodev/Documents/dripsNetwork/second/InsightArena/backend/src/migrations/1774900000000-CreateMarketTemplatesTable.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/solodev/Documents/dripsNetwork/second/InsightArena/backend/src/notifications/entities/notification.entity.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/solodev/Documents/dripsNetwork/second/InsightArena/backend/src/notifications/notifications.controller.spec.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/solodev/Documents/dripsNetwork/second/InsightArena/backend/src/notifications/notifications.controller.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/solodev/Documents/dripsNetwork/second/InsightArena/backend/src/notifications/notifications.module.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/solodev/Documents/dripsNetwork/second/InsightArena/backend/src/notifications/notifications.service.spec.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/solodev/Documents/dripsNetwork/second/InsightArena/backend/src/notifications/notifications.service.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/solodev/Documents/dripsNetwork/second/InsightArena/backend/src/predictions/dto/list-my-predictions.dto.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/solodev/Documents/dripsNetwork/second/InsightArena/backend/src/predictions/dto/submit-prediction.dto.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/solodev/Documents/dripsNetwork/second/InsightArena/backend/src/predictions/entities/prediction.entity.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/solodev/Documents/dripsNetwork/second/InsightArena/backend/src/predictions/predictions.controller.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/solodev/Documents/dripsNetwork/second/InsightArena/backend/src/predictions/predictions.module.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/solodev/Documents/dripsNetwork/second/InsightArena/backend/src/predictions/predictions.service.spec.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/solodev/Documents/dripsNetwork/second/InsightArena/backend/src/predictions/predictions.service.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/solodev/Documents/dripsNetwork/second/InsightArena/backend/src/seasons/dto/create-season.dto.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/solodev/Documents/dripsNetwork/second/InsightArena/backend/src/seasons/dto/list-seasons.dto.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/solodev/Documents/dripsNetwork/second/InsightArena/backend/src/seasons/entities/season.entity.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/solodev/Documents/dripsNetwork/second/InsightArena/backend/src/seasons/seasons.controller.spec.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/solodev/Documents/dripsNetwork/second/InsightArena/backend/src/seasons/seasons.controller.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/solodev/Documents/dripsNetwork/second/InsightArena/backend/src/seasons/seasons.module.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/solodev/Documents/dripsNetwork/second/InsightArena/backend/src/seasons/seasons.service.spec.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/solodev/Documents/dripsNetwork/second/InsightArena/backend/src/seasons/seasons.service.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/solodev/Documents/dripsNetwork/second/InsightArena/backend/src/soroban/entities/system-state.entity.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/solodev/Documents/dripsNetwork/second/InsightArena/backend/src/soroban/soroban.listener.spec.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/solodev/Documents/dripsNetwork/second/InsightArena/backend/src/soroban/soroban.listener.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/solodev/Documents/dripsNetwork/second/InsightArena/backend/src/soroban/soroban.module.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/solodev/Documents/dripsNetwork/second/InsightArena/backend/src/soroban/soroban.service.spec.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/solodev/Documents/dripsNetwork/second/InsightArena/backend/src/soroban/soroban.service.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/solodev/Documents/dripsNetwork/second/InsightArena/backend/src/users/dto/list-user-competitions.dto.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/solodev/Documents/dripsNetwork/second/InsightArena/backend/src/users/dto/list-user-markets.dto.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/solodev/Documents/dripsNetwork/second/InsightArena/backend/src/users/dto/list-user-predictions.dto.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/solodev/Documents/dripsNetwork/second/InsightArena/backend/src/users/dto/public-user.dto.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/solodev/Documents/dripsNetwork/second/InsightArena/backend/src/users/dto/update-user.dto.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/solodev/Documents/dripsNetwork/second/InsightArena/backend/src/users/dto/user-response.dto.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/solodev/Documents/dripsNetwork/second/InsightArena/backend/src/users/entities/user.entity.spec.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/solodev/Documents/dripsNetwork/second/InsightArena/backend/src/users/entities/user.entity.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/solodev/Documents/dripsNetwork/second/InsightArena/backend/src/users/users.controller.spec.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/solodev/Documents/dripsNetwork/second/InsightArena/backend/src/users/users.controller.ts","messages":[{"ruleId":"@typescript-eslint/no-unsafe-return","severity":2,"message":"Unsafe return of a value of type error.","line":129,"column":5,"nodeType":"ReturnStatement","messageId":"unsafeReturn","endLine":129,"endColumn":60},{"ruleId":"@typescript-eslint/no-unsafe-call","severity":2,"message":"Unsafe call of a type that could not be resolved.","line":129,"column":18,"nodeType":"MemberExpression","messageId":"errorCall","endLine":129,"endColumn":50}],"suppressedMessages":[],"errorCount":2,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"source":"import {\n Controller,\n Get,\n Patch,\n Param,\n Body,\n Query,\n UsePipes,\n ValidationPipe,\n} from '@nestjs/common';\nimport { ApiOperation, ApiResponse } from '@nestjs/swagger';\nimport { plainToInstance } from 'class-transformer';\nimport { Public } from '../common/decorators/public.decorator';\nimport { CurrentUser } from '../common/decorators/current-user.decorator';\nimport { UsersService } from './users.service';\nimport { PublicUserDto } from './dto/public-user.dto';\nimport { UserResponseDto } from './dto/user-response.dto';\nimport { UpdateUserDto } from './dto/update-user.dto';\nimport { User } from './entities/user.entity';\nimport {\n ListUserPredictionsDto,\n PaginatedPublicUserPredictionsResponse,\n} from './dto/list-user-predictions.dto';\n\nimport { ListUserCompetitionsDto } from './dto/list-user-competitions.dto';\nimport {\n ListUserMarketsDto,\n PaginatedUserMarketsResponse,\n} from './dto/list-user-markets.dto';\n\n@Controller('users')\nexport class UsersController {\n constructor(private readonly usersService: UsersService) {}\n\n @Get('me')\n @ApiOperation({ summary: 'Fetch own profile' })\n @ApiResponse({\n status: 200,\n description: 'User profile retrieved successfully',\n type: UserResponseDto,\n })\n @ApiResponse({ status: 401, description: 'Unauthorized' })\n getOwnProfile(@CurrentUser() user: User) {\n return plainToInstance(UserResponseDto, user, {\n excludeExtraneousValues: true,\n });\n }\n\n @Patch('me')\n @UsePipes(\n new ValidationPipe({ whitelist: true, forbidNonWhitelisted: false }),\n )\n @ApiOperation({ summary: 'Update own profile (username, avatar_url)' })\n @ApiResponse({\n status: 200,\n description: 'Profile updated successfully',\n type: UserResponseDto,\n })\n @ApiResponse({ status: 400, description: 'Validation error' })\n @ApiResponse({ status: 401, description: 'Unauthorized' })\n async updateOwnProfile(\n @CurrentUser() user: User,\n @Body() dto: UpdateUserDto,\n ) {\n const updated = await this.usersService.updateProfile(user.id, dto);\n return plainToInstance(UserResponseDto, updated, {\n excludeExtraneousValues: true,\n });\n }\n\n @Get(':address')\n @Public()\n async getPublicProfile(@Param('address') address: string) {\n const user = await this.usersService.findByAddress(address);\n return plainToInstance(PublicUserDto, user, {\n excludeExtraneousValues: true,\n });\n }\n\n @Get(':address/predictions')\n @Public()\n @UsePipes(\n new ValidationPipe({ whitelist: true, forbidNonWhitelisted: false }),\n )\n @ApiOperation({\n summary: \"Get a user's predictions for resolved markets (public)\",\n })\n @ApiResponse({\n status: 200,\n description: 'Paginated predictions for resolved markets only',\n })\n async getPublicPredictions(\n @Param('address') address: string,\n @Query() query: ListUserPredictionsDto,\n ): Promise {\n return this.usersService.findPublicPredictionsByAddress(address, query);\n }\n\n @Get(':address/markets')\n @Public()\n @UsePipes(\n new ValidationPipe({ whitelist: true, forbidNonWhitelisted: false }),\n )\n @ApiOperation({ summary: 'List markets created by a user (public)' })\n @ApiResponse({ status: 200, description: 'Paginated markets list' })\n @ApiResponse({ status: 404, description: 'User not found' })\n async getUserMarkets(\n @Param('address') address: string,\n @Query() query: ListUserMarketsDto,\n ): Promise {\n return this.usersService.findMarketsByAddress(address, query);\n }\n\n @Get(':address/competitions')\n @Public()\n @ApiOperation({ summary: 'Get competitions a user has participated in' })\n @ApiResponse({ status: 200, description: 'List of competitions' })\n async getUserCompetitions(\n @Param('address') address: string,\n @Query() query: ListUserCompetitionsDto,\n ) {\n return this.usersService.findUserCompetitions(address, query);\n }\n\n @Get('me/export')\n @ApiOperation({ summary: 'Export all user data (GDPR)' })\n @ApiResponse({ status: 200, description: 'User data exported as JSON' })\n async exportData(@CurrentUser() user: User) {\n return await this.usersService.exportUserData(user.id);\n }\n}\n","usedDeprecatedRules":[]},{"filePath":"/home/solodev/Documents/dripsNetwork/second/InsightArena/backend/src/users/users.module.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/solodev/Documents/dripsNetwork/second/InsightArena/backend/src/users/users.service.spec.ts","messages":[{"ruleId":"@typescript-eslint/no-unsafe-argument","severity":1,"message":"Unsafe argument of type `any` assigned to a parameter of type `SelectQueryBuilder`.","line":164,"column":26,"nodeType":"TSAsExpression","messageId":"unsafeArgument","endLine":164,"endColumn":45},{"ruleId":"@typescript-eslint/no-unsafe-argument","severity":1,"message":"Unsafe argument of type `any` assigned to a parameter of type `SelectQueryBuilder`.","line":240,"column":26,"nodeType":"TSAsExpression","messageId":"unsafeArgument","endLine":240,"endColumn":45},{"ruleId":"@typescript-eslint/no-unsafe-argument","severity":1,"message":"Unsafe argument of type `any` assigned to a parameter of type `SelectQueryBuilder`.","line":278,"column":26,"nodeType":"TSAsExpression","messageId":"unsafeArgument","endLine":278,"endColumn":45},{"ruleId":"@typescript-eslint/no-unsafe-argument","severity":1,"message":"Unsafe argument of type `any` assigned to a parameter of type `SelectQueryBuilder`.","line":301,"column":26,"nodeType":"TSAsExpression","messageId":"unsafeArgument","endLine":301,"endColumn":45},{"ruleId":"@typescript-eslint/no-unsafe-argument","severity":1,"message":"Unsafe argument of type `any` assigned to a parameter of type `SelectQueryBuilder`.","line":317,"column":26,"nodeType":"TSAsExpression","messageId":"unsafeArgument","endLine":317,"endColumn":45},{"ruleId":"@typescript-eslint/no-unsafe-argument","severity":1,"message":"Unsafe argument of type `any` assigned to a parameter of type `SelectQueryBuilder`.","line":333,"column":26,"nodeType":"TSAsExpression","messageId":"unsafeArgument","endLine":333,"endColumn":45},{"ruleId":"@typescript-eslint/no-unsafe-argument","severity":1,"message":"Unsafe argument of type `any` assigned to a parameter of type `SelectQueryBuilder`.","line":349,"column":26,"nodeType":"TSAsExpression","messageId":"unsafeArgument","endLine":349,"endColumn":45}],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":7,"fixableErrorCount":0,"fixableWarningCount":0,"source":"import { Test, TestingModule } from '@nestjs/testing';\nimport { getRepositoryToken } from '@nestjs/typeorm';\nimport { NotFoundException } from '@nestjs/common';\nimport { Repository } from 'typeorm';\nimport { UsersService } from './users.service';\nimport { User } from './entities/user.entity';\nimport { Prediction } from '../predictions/entities/prediction.entity';\nimport { Market } from '../markets/entities/market.entity';\nimport { Notification } from '../notifications/entities/notification.entity';\nimport { ListUserPredictionsDto } from './dto/list-user-predictions.dto';\nimport { CompetitionParticipant } from '../competitions/entities/competition-participant.entity';\nimport { UserCompetitionFilterStatus } from './dto/list-user-competitions.dto';\nimport { Market } from '../markets/entities/market.entity';\nimport {\n ListUserMarketsDto,\n UserMarketFilterStatus,\n UserMarketsSortBy,\n UserMarketsSortOrder,\n} from './dto/list-user-markets.dto';\n\ndescribe('UsersService', () => {\n let service: UsersService;\n let repository: Repository;\n let predictionsRepository: Repository;\n let participantsRepository: Repository;\n let marketsRepository: Repository;\n\n const mockUser: User = {\n id: '123e4567-e89b-12d3-a456-426614174000',\n stellar_address: 'GBRPYHIL2CI3WHZDTOOQFC6EB4RRJC3XNRBF7XNZFXNRBF7XNRBF7XN',\n username: 'testuser',\n avatar_url: null,\n total_predictions: 10,\n correct_predictions: 7,\n total_staked_stroops: '1000000',\n total_winnings_stroops: '500000',\n reputation_score: 85,\n season_points: 100,\n role: 'user',\n is_banned: false,\n ban_reason: '',\n banned_at: null,\n banned_by: '',\n created_at: new Date('2024-01-01'),\n updated_at: new Date('2024-01-01'),\n } as User;\n\n beforeEach(async () => {\n const module: TestingModule = await Test.createTestingModule({\n providers: [\n UsersService,\n {\n provide: getRepositoryToken(User),\n useValue: {\n findOneBy: jest.fn(),\n save: jest.fn(),\n find: jest.fn(),\n },\n },\n {\n provide: getRepositoryToken(Prediction),\n useValue: {\n createQueryBuilder: jest.fn(),\n find: jest.fn(),\n },\n },\n {\n provide: getRepositoryToken(Market),\n useValue: {\n find: jest.fn(),\n },\n },\n {\n provide: getRepositoryToken(Notification),\n useValue: {\n find: jest.fn(),\n },\n },\n {\n provide: getRepositoryToken(CompetitionParticipant),\n useValue: {\n createQueryBuilder: jest.fn(),\n find: jest.fn(),\n },\n },\n {\n provide: getRepositoryToken(Market),\n useValue: {\n createQueryBuilder: jest.fn(),\n },\n },\n ],\n }).compile();\n\n service = module.get(UsersService);\n repository = module.get>(getRepositoryToken(User));\n predictionsRepository = module.get>(\n getRepositoryToken(Prediction),\n );\n participantsRepository = module.get>(\n getRepositoryToken(CompetitionParticipant),\n );\n marketsRepository = module.get>(\n getRepositoryToken(Market),\n );\n });\n\n it('should be defined', () => {\n expect(service).toBeDefined();\n });\n\n describe('findByAddress', () => {\n it('should return a user when found', async () => {\n const findOneByMock = jest\n .spyOn(repository, 'findOneBy')\n .mockResolvedValue(mockUser);\n\n const result = await service.findByAddress(mockUser.stellar_address);\n\n expect(result).toEqual(mockUser);\n expect(findOneByMock).toHaveBeenCalledWith({\n stellar_address: mockUser.stellar_address,\n });\n });\n\n it('should throw NotFoundException when user not found', async () => {\n jest.spyOn(repository, 'findOneBy').mockResolvedValue(null);\n\n await expect(\n service.findByAddress('NONEXISTENT_ADDRESS'),\n ).rejects.toThrow(NotFoundException);\n });\n });\n\n describe('findUserCompetitions', () => {\n it('should return paginated user competitions', async () => {\n jest.spyOn(repository, 'findOneBy').mockResolvedValue(mockUser);\n\n const queryBuilder = {\n leftJoinAndSelect: jest.fn().mockReturnThis(),\n where: jest.fn().mockReturnThis(),\n andWhere: jest.fn().mockReturnThis(),\n orderBy: jest.fn().mockReturnThis(),\n skip: jest.fn().mockReturnThis(),\n take: jest.fn().mockReturnThis(),\n getManyAndCount: jest.fn().mockResolvedValue([\n [\n {\n rank: 1,\n score: 100,\n competition: {\n id: 'comp-1',\n title: 'Test Competition',\n end_time: new Date(Date.now() + 10000),\n },\n },\n ],\n 1,\n ]),\n };\n\n jest\n .spyOn(participantsRepository, 'createQueryBuilder')\n .mockReturnValue(queryBuilder as any);\n\n const result = await service.findUserCompetitions(\n mockUser.stellar_address,\n {\n page: 1,\n limit: 10,\n status: UserCompetitionFilterStatus.Active,\n },\n );\n\n expect(result.data).toHaveLength(1);\n expect(result.total).toBe(1);\n expect(result.data[0].title).toBe('Test Competition');\n expect(queryBuilder.where).toHaveBeenCalledWith(\n 'participant.user_id = :userId',\n { userId: mockUser.id },\n );\n });\n });\n\n describe('findPublicPredictionsByAddress', () => {\n it('should return only resolved-market predictions with outcome mapping', async () => {\n jest.spyOn(repository, 'findOneBy').mockResolvedValue(mockUser);\n\n const now = new Date('2025-02-01T00:00:00.000Z');\n const queryBuilder = {\n leftJoinAndSelect: jest.fn().mockReturnThis(),\n where: jest.fn().mockReturnThis(),\n andWhere: jest.fn().mockReturnThis(),\n orderBy: jest.fn().mockReturnThis(),\n skip: jest.fn().mockReturnThis(),\n take: jest.fn().mockReturnThis(),\n getManyAndCount: jest.fn().mockResolvedValue([\n [\n {\n id: 'pred-1',\n chosen_outcome: 'YES',\n stake_amount_stroops: '100',\n payout_claimed: false,\n payout_amount_stroops: '0',\n tx_hash: null,\n submitted_at: now,\n market: {\n id: 'mkt-1',\n title: 'Resolved YES market',\n end_time: now,\n resolved_outcome: 'YES',\n is_resolved: true,\n is_cancelled: false,\n },\n },\n {\n id: 'pred-2',\n chosen_outcome: 'NO',\n stake_amount_stroops: '200',\n payout_claimed: false,\n payout_amount_stroops: '0',\n tx_hash: null,\n submitted_at: now,\n market: {\n id: 'mkt-1', // same market, different outcome to test 'incorrect'\n title: 'Resolved YES market',\n end_time: now,\n resolved_outcome: 'YES',\n is_resolved: true,\n is_cancelled: false,\n },\n },\n ],\n 2,\n ]),\n };\n\n jest\n .spyOn(predictionsRepository, 'createQueryBuilder')\n .mockReturnValue(queryBuilder as any);\n\n const result = await service.findPublicPredictionsByAddress(\n mockUser.stellar_address,\n new ListUserPredictionsDto(),\n );\n\n expect(result.data[0].outcome).toBe('correct');\n expect(result.data[1].outcome).toBe('incorrect');\n });\n });\n\n describe('findMarketsByAddress', () => {\n const queryBuilder = {\n leftJoinAndSelect: jest.fn().mockReturnThis(),\n where: jest.fn().mockReturnThis(),\n andWhere: jest.fn().mockReturnThis(),\n orderBy: jest.fn().mockReturnThis(),\n skip: jest.fn().mockReturnThis(),\n take: jest.fn().mockReturnThis(),\n getManyAndCount: jest.fn(),\n };\n\n beforeEach(() => {\n jest.clearAllMocks();\n queryBuilder.leftJoinAndSelect.mockReturnThis();\n queryBuilder.where.mockReturnThis();\n queryBuilder.andWhere.mockReturnThis();\n queryBuilder.orderBy.mockReturnThis();\n queryBuilder.skip.mockReturnThis();\n queryBuilder.take.mockReturnThis();\n });\n\n it('should scope markets to creator and return pagination', async () => {\n jest.spyOn(repository, 'findOneBy').mockResolvedValue(mockUser);\n queryBuilder.getManyAndCount.mockResolvedValue([[], 0]);\n jest\n .spyOn(marketsRepository, 'createQueryBuilder')\n .mockReturnValue(queryBuilder as any);\n\n const result = await service.findMarketsByAddress(\n mockUser.stellar_address,\n new ListUserMarketsDto(),\n );\n\n expect(queryBuilder.where).toHaveBeenCalledWith(\n 'market.creatorId = :userId',\n { userId: mockUser.id },\n );\n expect(queryBuilder.orderBy).toHaveBeenCalledWith(\n 'market.created_at',\n 'DESC',\n );\n expect(result).toEqual({ data: [], total: 0, page: 1, limit: 20 });\n });\n\n it('should filter active markets', async () => {\n jest.spyOn(repository, 'findOneBy').mockResolvedValue(mockUser);\n queryBuilder.getManyAndCount.mockResolvedValue([[], 0]);\n jest\n .spyOn(marketsRepository, 'createQueryBuilder')\n .mockReturnValue(queryBuilder as any);\n\n await service.findMarketsByAddress(mockUser.stellar_address, {\n status: UserMarketFilterStatus.Active,\n } as ListUserMarketsDto);\n\n expect(queryBuilder.andWhere).toHaveBeenCalledWith(\n 'market.is_resolved = false AND market.is_cancelled = false',\n );\n });\n\n it('should filter resolved markets', async () => {\n jest.spyOn(repository, 'findOneBy').mockResolvedValue(mockUser);\n queryBuilder.getManyAndCount.mockResolvedValue([[], 0]);\n jest\n .spyOn(marketsRepository, 'createQueryBuilder')\n .mockReturnValue(queryBuilder as any);\n\n await service.findMarketsByAddress(mockUser.stellar_address, {\n status: UserMarketFilterStatus.Resolved,\n } as ListUserMarketsDto);\n\n expect(queryBuilder.andWhere).toHaveBeenCalledWith(\n 'market.is_resolved = true',\n );\n });\n\n it('should filter cancelled markets', async () => {\n jest.spyOn(repository, 'findOneBy').mockResolvedValue(mockUser);\n queryBuilder.getManyAndCount.mockResolvedValue([[], 0]);\n jest\n .spyOn(marketsRepository, 'createQueryBuilder')\n .mockReturnValue(queryBuilder as any);\n\n await service.findMarketsByAddress(mockUser.stellar_address, {\n status: UserMarketFilterStatus.Cancelled,\n } as ListUserMarketsDto);\n\n expect(queryBuilder.andWhere).toHaveBeenCalledWith(\n 'market.is_cancelled = true',\n );\n });\n\n it('should sort by participant_count and order asc', async () => {\n jest.spyOn(repository, 'findOneBy').mockResolvedValue(mockUser);\n queryBuilder.getManyAndCount.mockResolvedValue([[], 0]);\n jest\n .spyOn(marketsRepository, 'createQueryBuilder')\n .mockReturnValue(queryBuilder as any);\n\n await service.findMarketsByAddress(mockUser.stellar_address, {\n sort_by: UserMarketsSortBy.ParticipantCount,\n order: UserMarketsSortOrder.Asc,\n } as ListUserMarketsDto);\n\n expect(queryBuilder.orderBy).toHaveBeenCalledWith(\n 'market.participant_count',\n 'ASC',\n );\n });\n });\n});\n","usedDeprecatedRules":[]},{"filePath":"/home/solodev/Documents/dripsNetwork/second/InsightArena/backend/src/users/users.service.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/solodev/Documents/dripsNetwork/second/InsightArena/backend/test/analytics.e2e-spec.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/solodev/Documents/dripsNetwork/second/InsightArena/backend/test/api-versioning.e2e-spec.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/solodev/Documents/dripsNetwork/second/InsightArena/backend/test/app.e2e-spec.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/solodev/Documents/dripsNetwork/second/InsightArena/backend/test/markets.e2e-spec.ts","messages":[],"suppressedMessages":[{"ruleId":"@typescript-eslint/no-unsafe-assignment","severity":2,"message":"Unsafe assignment of an `any` value.","line":61,"column":5,"nodeType":"Property","messageId":"anyAssignment","endLine":61,"endColumn":34,"suppressions":[{"kind":"directive","justification":""}]}],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/solodev/Documents/dripsNetwork/second/InsightArena/backend/test/seasons-create.e2e-spec.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/solodev/Documents/dripsNetwork/second/InsightArena/backend/test/seasons-list.e2e-spec.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/solodev/Documents/dripsNetwork/second/InsightArena/backend/test/seasons.e2e-spec.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/solodev/Documents/dripsNetwork/second/InsightArena/backend/test/users.e2e-spec.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]}] diff --git a/backend/package-lock.json b/backend/package-lock.json index c6b8af49..9f691ac3 100644 --- a/backend/package-lock.json +++ b/backend/package-lock.json @@ -750,7 +750,7 @@ "version": "0.8.1", "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "@jridgewell/trace-mapping": "0.3.9" @@ -763,7 +763,7 @@ "version": "0.3.9", "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "@jridgewell/resolve-uri": "^3.0.3", @@ -2014,7 +2014,7 @@ "version": "3.1.2", "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", - "dev": true, + "devOptional": true, "license": "MIT", "engines": { "node": ">=6.0.0" @@ -2035,7 +2035,7 @@ "version": "1.5.5", "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", - "dev": true, + "devOptional": true, "license": "MIT" }, "node_modules/@jridgewell/trace-mapping": { @@ -2937,28 +2937,28 @@ "version": "1.0.12", "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.12.tgz", "integrity": "sha512-UCYBaeFvM11aU2y3YPZ//O5Rhj+xKyzy7mvcIoAjASbigy8mHMryP5cK7dgjlz2hWxh1g5pLw084E0a/wlUSFQ==", - "dev": true, + "devOptional": true, "license": "MIT" }, "node_modules/@tsconfig/node12": { "version": "1.0.11", "resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz", "integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==", - "dev": true, + "devOptional": true, "license": "MIT" }, "node_modules/@tsconfig/node14": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz", "integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==", - "dev": true, + "devOptional": true, "license": "MIT" }, "node_modules/@tsconfig/node16": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.4.tgz", "integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==", - "dev": true, + "devOptional": true, "license": "MIT" }, "node_modules/@tybys/wasm-util": { @@ -4070,7 +4070,7 @@ "version": "8.16.0", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", - "dev": true, + "devOptional": true, "license": "MIT", "bin": { "acorn": "bin/acorn" @@ -4106,7 +4106,7 @@ "version": "8.3.5", "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.5.tgz", "integrity": "sha512-HEHNfbars9v4pgpW6SO1KSPkfoS0xVOM/9UzkJltjlsHZmJasxg8aXkuZa7SMf8vKGIBhpUsPluQSqhJFCqebw==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "acorn": "^8.11.0" @@ -4298,7 +4298,7 @@ "version": "4.1.3", "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==", - "dev": true, + "devOptional": true, "license": "MIT" }, "node_modules/argparse": { @@ -5324,7 +5324,7 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==", - "dev": true, + "devOptional": true, "license": "MIT" }, "node_modules/cron": { @@ -5495,7 +5495,7 @@ "version": "4.0.4", "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.4.tgz", "integrity": "sha512-X07nttJQkwkfKfvTPG/KSnE2OMdcUCao6+eXF3wmnIQRn2aPAHH3VxDbDOdegkd6JbPsXqShpvEOHfAT+nCNwQ==", - "dev": true, + "devOptional": true, "license": "BSD-3-Clause", "engines": { "node": ">=0.3.1" @@ -8317,7 +8317,7 @@ "version": "1.3.6", "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", - "dev": true, + "devOptional": true, "license": "ISC" }, "node_modules/makeerror": { @@ -10745,7 +10745,7 @@ "version": "10.9.2", "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz", "integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "@cspotcode/source-map-support": "^0.8.0", @@ -11087,7 +11087,7 @@ "version": "5.9.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", - "dev": true, + "devOptional": true, "license": "Apache-2.0", "bin": { "tsc": "bin/tsc", @@ -11298,7 +11298,7 @@ "version": "3.0.1", "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==", - "dev": true, + "devOptional": true, "license": "MIT" }, "node_modules/v8-to-istanbul": { @@ -11368,6 +11368,56 @@ "defaults": "^1.0.3" } }, + "node_modules/webpack": { + "version": "5.105.4", + "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.105.4.tgz", + "integrity": "sha512-jTywjboN9aHxFlToqb0K0Zs9SbBoW4zRUlGzI2tYNxVYcEi/IPpn+Xi4ye5jTLvX2YeLuic/IvxNot+Q1jMoOw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@types/eslint-scope": "^3.7.7", + "@types/estree": "^1.0.8", + "@types/json-schema": "^7.0.15", + "@webassemblyjs/ast": "^1.14.1", + "@webassemblyjs/wasm-edit": "^1.14.1", + "@webassemblyjs/wasm-parser": "^1.14.1", + "acorn": "^8.16.0", + "acorn-import-phases": "^1.0.3", + "browserslist": "^4.28.1", + "chrome-trace-event": "^1.0.2", + "enhanced-resolve": "^5.20.0", + "es-module-lexer": "^2.0.0", + "eslint-scope": "5.1.1", + "events": "^3.2.0", + "glob-to-regexp": "^0.4.1", + "graceful-fs": "^4.2.11", + "json-parse-even-better-errors": "^2.3.1", + "loader-runner": "^4.3.1", + "mime-types": "^2.1.27", + "neo-async": "^2.6.2", + "schema-utils": "^4.3.3", + "tapable": "^2.3.0", + "terser-webpack-plugin": "^5.3.17", + "watchpack": "^2.5.1", + "webpack-sources": "^3.3.4" + }, + "bin": { + "webpack": "bin/webpack.js" + }, + "engines": { + "node": ">=10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependenciesMeta": { + "webpack-cli": { + "optional": true + } + } + }, "node_modules/webpack-node-externals": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/webpack-node-externals/-/webpack-node-externals-3.0.0.tgz", @@ -11388,6 +11438,137 @@ "node": ">=10.13.0" } }, + "node_modules/webpack/node_modules/ajv": { + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.18.0.tgz", + "integrity": "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/webpack/node_modules/ajv-formats": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-2.1.1.tgz", + "integrity": "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "ajv": "^8.0.0" + }, + "peerDependencies": { + "ajv": "^8.0.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } + } + }, + "node_modules/webpack/node_modules/ajv-keywords": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-5.1.0.tgz", + "integrity": "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "fast-deep-equal": "^3.1.3" + }, + "peerDependencies": { + "ajv": "^8.8.2" + } + }, + "node_modules/webpack/node_modules/eslint-scope": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", + "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", + "dev": true, + "license": "BSD-2-Clause", + "peer": true, + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^4.1.1" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/webpack/node_modules/estraverse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", + "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", + "dev": true, + "license": "BSD-2-Clause", + "peer": true, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/webpack/node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/webpack/node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/webpack/node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/webpack/node_modules/schema-utils": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.3.3.tgz", + "integrity": "sha512-eflK8wEtyOE6+hsaRVPxvUKYCpRgzLqDTb8krvAsRIwOGlHoSgYLgBXoubGgLd2fT41/OUYdb48v4k4WWHQurA==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@types/json-schema": "^7.0.9", + "ajv": "^8.9.0", + "ajv-formats": "^2.1.1", + "ajv-keywords": "^5.1.0" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", @@ -11562,7 +11743,7 @@ "version": "3.1.1", "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", - "dev": true, + "devOptional": true, "license": "MIT", "engines": { "node": ">=6" diff --git a/backend/src/admin/admin.controller.ts b/backend/src/admin/admin.controller.ts index 469da1cd..48ec932b 100644 --- a/backend/src/admin/admin.controller.ts +++ b/backend/src/admin/admin.controller.ts @@ -19,6 +19,8 @@ import { BanUserDto } from './dto/ban-user.dto'; import { ActivityLogQueryDto } from './dto/activity-log-query.dto'; import { StatsResponseDto } from './dto/stats-response.dto'; import { ResolveMarketDto } from './dto/resolve-market.dto'; +import { ModerateCommentDto } from './dto/moderate-comment.dto'; +import { ReportQueryDto } from './dto/report-query.dto'; @Controller('admin') @UseGuards(JwtAuthGuard, RolesGuard) @@ -77,4 +79,17 @@ export class AdminController { (req as { user: { id: string } }).user.id, ); } + + @Patch('comments/:id/moderate') + async moderateComment( + @Param('id') id: string, + @Body() dto: ModerateCommentDto, + ) { + return this.adminService.moderateComment(id, dto.is_moderated, dto.reason); + } + + @Get('reports/activity') + async getActivityReport(@Query() query: ReportQueryDto) { + return this.adminService.getActivityReport(query); + } } diff --git a/backend/src/admin/admin.module.ts b/backend/src/admin/admin.module.ts index f4269615..493f5853 100644 --- a/backend/src/admin/admin.module.ts +++ b/backend/src/admin/admin.module.ts @@ -2,6 +2,7 @@ import { Module } from '@nestjs/common'; import { TypeOrmModule } from '@nestjs/typeorm'; import { User } from '../users/entities/user.entity'; import { Market } from '../markets/entities/market.entity'; +import { Comment } from '../markets/entities/comment.entity'; import { Prediction } from '../predictions/entities/prediction.entity'; import { Competition } from '../competitions/entities/competition.entity'; import { ActivityLog } from '../analytics/entities/activity-log.entity'; @@ -14,6 +15,7 @@ import { AdminService } from './admin.service'; TypeOrmModule.forFeature([ User, Market, + Comment, Prediction, Competition, ActivityLog, diff --git a/backend/src/admin/admin.service.spec.ts b/backend/src/admin/admin.service.spec.ts index b6b7ba7e..f475c542 100644 --- a/backend/src/admin/admin.service.spec.ts +++ b/backend/src/admin/admin.service.spec.ts @@ -6,6 +6,7 @@ import { } from '@nestjs/common'; import { Test, TestingModule } from '@nestjs/testing'; import { getRepositoryToken } from '@nestjs/typeorm'; +import { Comment } from '../markets/entities/comment.entity'; import { ActivityLog } from '../analytics/entities/activity-log.entity'; import { AnalyticsService } from '../analytics/analytics.service'; import { Competition } from '../competitions/entities/competition.entity'; @@ -46,7 +47,9 @@ describe('AdminService.adminResolveMarket', () => { ...overrides, }) as Market; - const makeDto = (overrides: Partial = {}): ResolveMarketDto => ({ + const makeDto = ( + overrides: Partial = {}, + ): ResolveMarketDto => ({ resolved_outcome: 'YES', ...overrides, }); @@ -63,6 +66,7 @@ describe('AdminService.adminResolveMarket', () => { AdminService, { provide: getRepositoryToken(User), useValue: mockRepo() }, { provide: getRepositoryToken(Market), useValue: marketsRepo }, + { provide: getRepositoryToken(Comment), useValue: mockRepo() }, { provide: getRepositoryToken(Prediction), useValue: predictionsRepo }, { provide: getRepositoryToken(Competition), useValue: mockRepo() }, { provide: getRepositoryToken(ActivityLog), useValue: mockRepo() }, @@ -78,32 +82,36 @@ describe('AdminService.adminResolveMarket', () => { it('throws NotFoundException when market does not exist', async () => { marketsRepo.findOne.mockResolvedValue(null); - await expect(service.adminResolveMarket('bad-id', makeDto(), adminId)).rejects.toThrow( - NotFoundException, - ); + await expect( + service.adminResolveMarket('bad-id', makeDto(), adminId), + ).rejects.toThrow(NotFoundException); }); it('throws ConflictException when market is already resolved', async () => { marketsRepo.findOne.mockResolvedValue(makeMarket({ is_resolved: true })); - await expect(service.adminResolveMarket('market-1', makeDto(), adminId)).rejects.toThrow( - ConflictException, - ); + await expect( + service.adminResolveMarket('market-1', makeDto(), adminId), + ).rejects.toThrow(ConflictException); }); it('throws BadRequestException when market is cancelled', async () => { marketsRepo.findOne.mockResolvedValue(makeMarket({ is_cancelled: true })); - await expect(service.adminResolveMarket('market-1', makeDto(), adminId)).rejects.toThrow( - BadRequestException, - ); + await expect( + service.adminResolveMarket('market-1', makeDto(), adminId), + ).rejects.toThrow(BadRequestException); }); it('throws BadRequestException for invalid outcome', async () => { marketsRepo.findOne.mockResolvedValue(makeMarket()); await expect( - service.adminResolveMarket('market-1', makeDto({ resolved_outcome: 'MAYBE' }), adminId), + service.adminResolveMarket( + 'market-1', + makeDto({ resolved_outcome: 'MAYBE' }), + adminId, + ), ).rejects.toThrow(BadRequestException); }); @@ -111,23 +119,38 @@ describe('AdminService.adminResolveMarket', () => { marketsRepo.findOne.mockResolvedValue(makeMarket()); sorobanService.resolveMarket.mockRejectedValue(new Error('Soroban down')); - await expect(service.adminResolveMarket('market-1', makeDto(), adminId)).rejects.toThrow( - BadGatewayException, - ); + await expect( + service.adminResolveMarket('market-1', makeDto(), adminId), + ).rejects.toThrow(BadGatewayException); }); it('resolves market, notifies participants, and logs admin action', async () => { const market = makeMarket(); const participant = { id: 'user-2' } as User; - const prediction = { user: participant, chosen_outcome: 'YES', market } as Prediction; + const prediction = { + user: participant, + chosen_outcome: 'YES', + market, + } as Prediction; marketsRepo.findOne.mockResolvedValue(market); - marketsRepo.save.mockResolvedValue({ ...market, is_resolved: true, resolved_outcome: 'YES' }); + marketsRepo.save.mockResolvedValue({ + ...market, + is_resolved: true, + resolved_outcome: 'YES', + }); predictionsRepo.find.mockResolvedValue([prediction]); - const result = await service.adminResolveMarket('market-1', makeDto(), adminId); + const result = await service.adminResolveMarket( + 'market-1', + makeDto(), + adminId, + ); - expect(sorobanService.resolveMarket).toHaveBeenCalledWith('on-chain-1', 'YES'); + expect(sorobanService.resolveMarket).toHaveBeenCalledWith( + 'on-chain-1', + 'YES', + ); expect(marketsRepo.save).toHaveBeenCalledWith( expect.objectContaining({ is_resolved: true, resolved_outcome: 'YES' }), ); @@ -141,17 +164,28 @@ describe('AdminService.adminResolveMarket', () => { expect(analyticsService.logActivity).toHaveBeenCalledWith( adminId, 'MARKET_RESOLVED_BY_ADMIN', - expect.objectContaining({ market_id: 'market-1', resolved_outcome: 'YES' }), + expect.objectContaining({ + market_id: 'market-1', + resolved_outcome: 'YES', + }), ); expect(result.is_resolved).toBe(true); }); it('includes resolution_note in notification metadata when provided', async () => { const market = makeMarket(); - const prediction = { user: { id: 'user-2' } as User, chosen_outcome: 'NO', market } as Prediction; + const prediction = { + user: { id: 'user-2' } as User, + chosen_outcome: 'NO', + market, + } as Prediction; marketsRepo.findOne.mockResolvedValue(market); - marketsRepo.save.mockResolvedValue({ ...market, is_resolved: true, resolved_outcome: 'YES' }); + marketsRepo.save.mockResolvedValue({ + ...market, + is_resolved: true, + resolved_outcome: 'YES', + }); predictionsRepo.find.mockResolvedValue([prediction]); await service.adminResolveMarket( @@ -165,7 +199,10 @@ describe('AdminService.adminResolveMarket', () => { expect.any(String), 'Market Resolved', expect.any(String), - expect.objectContaining({ resolution_note: 'Dispute resolved by admin', won: false }), + expect.objectContaining({ + resolution_note: 'Dispute resolved by admin', + won: false, + }), ); }); }); diff --git a/backend/src/admin/admin.service.ts b/backend/src/admin/admin.service.ts index 7cdd00c9..b5f8e227 100644 --- a/backend/src/admin/admin.service.ts +++ b/backend/src/admin/admin.service.ts @@ -10,6 +10,7 @@ import { InjectRepository } from '@nestjs/typeorm'; import { Repository, Between } from 'typeorm'; import { User } from '../users/entities/user.entity'; import { Market } from '../markets/entities/market.entity'; +import { Comment } from '../markets/entities/comment.entity'; import { Prediction } from '../predictions/entities/prediction.entity'; import { Competition } from '../competitions/entities/competition.entity'; import { ActivityLog } from '../analytics/entities/activity-log.entity'; @@ -21,6 +22,11 @@ import { ListUsersQueryDto } from './dto/list-users-query.dto'; import { ActivityLogQueryDto } from './dto/activity-log-query.dto'; import { StatsResponseDto } from './dto/stats-response.dto'; import { ResolveMarketDto } from './dto/resolve-market.dto'; +import { + ReportFormat, + ReportQueryDto, + ReportTimeframe, +} from './dto/report-query.dto'; @Injectable() export class AdminService { @@ -31,6 +37,8 @@ export class AdminService { private readonly usersRepository: Repository, @InjectRepository(Market) private readonly marketsRepository: Repository, + @InjectRepository(Comment) + private readonly commentsRepository: Repository, @InjectRepository(Prediction) private readonly predictionsRepository: Repository, @InjectRepository(Competition) @@ -239,7 +247,10 @@ export class AdminService { dto.resolved_outcome, ); } catch (err) { - this.logger.error('Soroban resolveMarket failed during admin resolution', err); + this.logger.error( + 'Soroban resolveMarket failed during admin resolution', + err, + ); throw new BadGatewayException('Failed to resolve market on Soroban'); } @@ -265,18 +276,24 @@ export class AdminService { resolved_outcome: dto.resolved_outcome, your_prediction: p.chosen_outcome, won: p.chosen_outcome === dto.resolved_outcome, - ...(dto.resolution_note ? { resolution_note: dto.resolution_note } : {}), + ...(dto.resolution_note + ? { resolution_note: dto.resolution_note } + : {}), }, ), ), ); // Log admin action - await this.analyticsService.logActivity(adminId, 'MARKET_RESOLVED_BY_ADMIN', { - market_id: market.id, - resolved_outcome: dto.resolved_outcome, - resolution_note: dto.resolution_note ?? null, - }); + await this.analyticsService.logActivity( + adminId, + 'MARKET_RESOLVED_BY_ADMIN', + { + market_id: market.id, + resolved_outcome: dto.resolved_outcome, + resolution_note: dto.resolution_note ?? null, + }, + ); this.logger.log( `Admin ${adminId} resolved market "${market.title}" (${market.id}) with outcome "${dto.resolved_outcome}"`, @@ -284,4 +301,93 @@ export class AdminService { return saved; } + + async moderateComment( + commentId: string, + isModerated: boolean, + reason?: string, + ): Promise { + const comment = await this.commentsRepository.findOne({ + where: { id: commentId }, + }); + + if (!comment) { + throw new NotFoundException(`Comment with ID "${commentId}" not found`); + } + + comment.is_moderated = isModerated; + comment.moderation_reason = reason ?? null; + + return await this.commentsRepository.save(comment); + } + + async getActivityReport(query: ReportQueryDto) { + const { timeframe, format } = query; + const now = new Date(); + let startDate: Date; + + switch (timeframe) { + case ReportTimeframe.Daily: + startDate = new Date(now.getTime() - 24 * 60 * 60 * 1000); + break; + case ReportTimeframe.Weekly: + startDate = new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000); + break; + case ReportTimeframe.Monthly: + startDate = new Date(now.getTime() - 30 * 24 * 60 * 60 * 1000); + break; + default: + startDate = new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000); + } + + // User Growth + const userGrowth = await this.usersRepository.count({ + where: { created_at: Between(startDate, now) }, + }); + + // Market Creation Trends + const marketsCreated = await this.marketsRepository.count({ + where: { created_at: Between(startDate, now) }, + }); + + // Platform Revenue (accumulated in this period) + const volumeResult = (await this.marketsRepository + .createQueryBuilder('market') + .select('SUM(CAST(market.total_pool_stroops AS DECIMAL))', 'total') + .where('market.created_at BETWEEN :startDate AND :endDate', { + startDate, + endDate: now, + }) + .getRawOne()) as { total: string | null }; + + const periodVolume = volumeResult?.total || '0'; + const periodRevenue = ( + (BigInt(periodVolume.split('.')[0]) * BigInt(2)) / + BigInt(100) + ).toString(); + + // Predictions activity + const predictionsCount = await this.predictionsRepository.count({ + where: { submitted_at: Between(startDate, now) }, + }); + + const reportData = { + timeframe, + period_start: startDate.toISOString(), + period_end: now.toISOString(), + user_growth: userGrowth, + markets_created: marketsCreated, + total_predictions: predictionsCount, + period_volume_stroops: periodVolume, + platform_revenue_stroops: periodRevenue, + }; + + if (format === ReportFormat.CSV) { + const headers = Object.keys(reportData).join(','); + const values = Object.values(reportData).join(','); + return `${headers}\n${values}`; + } + + return reportData; + } } diff --git a/backend/src/admin/dto/moderate-comment.dto.ts b/backend/src/admin/dto/moderate-comment.dto.ts new file mode 100644 index 00000000..52f19057 --- /dev/null +++ b/backend/src/admin/dto/moderate-comment.dto.ts @@ -0,0 +1,15 @@ +import { IsBoolean, IsString, IsOptional } from 'class-validator'; +import { ApiProperty } from '@nestjs/swagger'; + +export class ModerateCommentDto { + @ApiProperty({ + description: 'Whether the comment should be hidden/moderated', + }) + @IsBoolean() + is_moderated: boolean; + + @ApiProperty({ description: 'Reason for moderation', required: false }) + @IsOptional() + @IsString() + reason?: string; +} diff --git a/backend/src/admin/dto/report-query.dto.ts b/backend/src/admin/dto/report-query.dto.ts new file mode 100644 index 00000000..f404e1f3 --- /dev/null +++ b/backend/src/admin/dto/report-query.dto.ts @@ -0,0 +1,32 @@ +import { IsEnum, IsOptional } from 'class-validator'; +import { ApiProperty } from '@nestjs/swagger'; + +export enum ReportTimeframe { + Daily = 'daily', + Weekly = 'weekly', + Monthly = 'monthly', +} + +export enum ReportFormat { + JSON = 'json', + CSV = 'csv', +} + +export class ReportQueryDto { + @ApiProperty({ + enum: ReportTimeframe, + description: 'Timeframe for the report', + }) + @IsEnum(ReportTimeframe) + timeframe: ReportTimeframe; + + @ApiProperty({ + enum: ReportFormat, + description: 'Output format (json or csv)', + required: false, + default: ReportFormat.JSON, + }) + @IsOptional() + @IsEnum(ReportFormat) + format?: ReportFormat = ReportFormat.JSON; +} diff --git a/backend/src/admin/dto/resolve-market.dto.ts b/backend/src/admin/dto/resolve-market.dto.ts index 53ed7ff8..510e7433 100644 --- a/backend/src/admin/dto/resolve-market.dto.ts +++ b/backend/src/admin/dto/resolve-market.dto.ts @@ -7,7 +7,9 @@ export class ResolveMarketDto { @IsNotEmpty() resolved_outcome: string; - @ApiPropertyOptional({ description: 'Optional note explaining the resolution' }) + @ApiPropertyOptional({ + description: 'Optional note explaining the resolution', + }) @IsString() @IsOptional() @MaxLength(1000) diff --git a/backend/src/markets/dto/create-comment.dto.ts b/backend/src/markets/dto/create-comment.dto.ts new file mode 100644 index 00000000..905998ce --- /dev/null +++ b/backend/src/markets/dto/create-comment.dto.ts @@ -0,0 +1,17 @@ +import { IsString, IsNotEmpty, IsOptional, IsUUID } from 'class-validator'; +import { ApiProperty } from '@nestjs/swagger'; + +export class CreateCommentDto { + @ApiProperty({ description: 'The content of the comment' }) + @IsString() + @IsNotEmpty() + content: string; + + @ApiProperty({ + description: 'The ID of the parent comment for nested replies', + required: false, + }) + @IsOptional() + @IsUUID() + parentId?: string; +} diff --git a/backend/src/markets/entities/comment.entity.ts b/backend/src/markets/entities/comment.entity.ts new file mode 100644 index 00000000..4e60f4a4 --- /dev/null +++ b/backend/src/markets/entities/comment.entity.ts @@ -0,0 +1,51 @@ +import { + Entity, + PrimaryGeneratedColumn, + Column, + CreateDateColumn, + UpdateDateColumn, + ManyToOne, + OneToMany, + Index, +} from 'typeorm'; +import { User } from '../../users/entities/user.entity'; +import { Market } from './market.entity'; + +@Entity('comments') +export class Comment { + @PrimaryGeneratedColumn('uuid') + id: string; + + @Column('text') + content: string; + + @ManyToOne(() => User, { onDelete: 'CASCADE' }) + @Index() + author: User; + + @ManyToOne(() => Market, { onDelete: 'CASCADE' }) + @Index() + market: Market; + + @ManyToOne(() => Comment, (comment) => comment.replies, { + onDelete: 'CASCADE', + nullable: true, + }) + @Index() + parent: Comment; + + @OneToMany(() => Comment, (comment) => comment.parent) + replies: Comment[]; + + @Column({ default: false }) + is_moderated: boolean; + + @Column({ nullable: true }) + moderation_reason: string | null; + + @CreateDateColumn() + created_at: Date; + + @UpdateDateColumn() + updated_at: Date; +} diff --git a/backend/src/markets/entities/market-template.entity.ts b/backend/src/markets/entities/market-template.entity.ts new file mode 100644 index 00000000..e4113b8b --- /dev/null +++ b/backend/src/markets/entities/market-template.entity.ts @@ -0,0 +1,36 @@ +import { + Entity, + PrimaryGeneratedColumn, + Column, + CreateDateColumn, +} from 'typeorm'; +import { IsString, IsNumber, Min } from 'class-validator'; + +@Entity('market_templates') +export class MarketTemplate { + @PrimaryGeneratedColumn('uuid') + id: string; + + @Column() + @IsString() + title: string; + + @Column('text') + @IsString() + description: string; + + @Column() + @IsString() + category: string; + + @Column('simple-array') + outcome_options: string[]; + + @Column() + @IsNumber() + @Min(1) + suggested_duration_days: number; + + @CreateDateColumn() + created_at: Date; +} diff --git a/backend/src/markets/markets.controller.ts b/backend/src/markets/markets.controller.ts index 1d87d47a..f0048540 100644 --- a/backend/src/markets/markets.controller.ts +++ b/backend/src/markets/markets.controller.ts @@ -20,7 +20,10 @@ import { } from '@nestjs/swagger'; import { MarketsService } from './markets.service'; import { Market } from './entities/market.entity'; +import { Comment } from './entities/comment.entity'; +import { MarketTemplate } from './entities/market-template.entity'; import { CreateMarketDto } from './dto/create-market.dto'; +import { CreateCommentDto } from './dto/create-comment.dto'; import { ListMarketsDto, PaginatedMarketsResponse, @@ -36,6 +39,18 @@ import { User } from '../users/entities/user.entity'; export class MarketsController { constructor(private readonly marketsService: MarketsService) {} + @Get('templates') + @Public() + @ApiOperation({ summary: 'List predefined market templates' }) + @ApiResponse({ + status: 200, + description: 'List of market templates', + type: [MarketTemplate], + }) + async getTemplates(): Promise { + return this.marketsService.getTemplates(); + } + @Get(':id/predictions') @Public() @ApiOperation({ summary: 'Get prediction statistics for a market' }) @@ -106,4 +121,32 @@ export class MarketsController { async cancelMarket(@Param('id') id: string): Promise { return this.marketsService.cancelMarket(id); } + + @Post(':id/comments') + @UseGuards(BanGuard) + @HttpCode(HttpStatus.CREATED) + @ApiBearerAuth() + @ApiOperation({ summary: 'Post a comment on a market' }) + @ApiResponse({ status: 201, description: 'Comment posted', type: Comment }) + @ApiResponse({ status: 404, description: 'Market/Parent not found' }) + async postComment( + @Param('id') id: string, + @Body() dto: CreateCommentDto, + @CurrentUser() user: User, + ): Promise { + return this.marketsService.createComment(id, dto, user); + } + + @Get(':id/comments') + @Public() + @ApiOperation({ summary: 'Get comments for a market' }) + @ApiResponse({ + status: 200, + description: 'List of comments (nested structure)', + type: [Comment], + }) + @ApiResponse({ status: 404, description: 'Market not found' }) + async getComments(@Param('id') id: string): Promise { + return this.marketsService.getComments(id); + } } diff --git a/backend/src/markets/markets.module.ts b/backend/src/markets/markets.module.ts index 97f1ae5f..d19643a0 100644 --- a/backend/src/markets/markets.module.ts +++ b/backend/src/markets/markets.module.ts @@ -1,12 +1,17 @@ import { Module } from '@nestjs/common'; import { TypeOrmModule } from '@nestjs/typeorm'; import { Market } from './entities/market.entity'; +import { Comment } from './entities/comment.entity'; +import { MarketTemplate } from './entities/market-template.entity'; import { MarketsService } from './markets.service'; import { MarketsController } from './markets.controller'; import { UsersModule } from '../users/users.module'; @Module({ - imports: [TypeOrmModule.forFeature([Market]), UsersModule], + imports: [ + TypeOrmModule.forFeature([Market, Comment, MarketTemplate]), + UsersModule, + ], controllers: [MarketsController], providers: [MarketsService], exports: [MarketsService], diff --git a/backend/src/markets/markets.service.spec.ts b/backend/src/markets/markets.service.spec.ts index 96616c7c..c6136104 100644 --- a/backend/src/markets/markets.service.spec.ts +++ b/backend/src/markets/markets.service.spec.ts @@ -6,11 +6,13 @@ import { SorobanService } from '../soroban/soroban.service'; import { UsersService } from '../users/users.service'; import { User } from '../users/entities/user.entity'; import { Market } from './entities/market.entity'; +import { Comment } from './entities/comment.entity'; +import { MarketTemplate } from './entities/market-template.entity'; import { CreateMarketDto } from './dto/create-market.dto'; import { MarketsService } from './markets.service'; type MockRepo = jest.Mocked< - Pick, 'create' | 'save' | 'findOne'> + Pick, 'create' | 'save' | 'findOne' | 'find'> >; describe('MarketsService', () => { @@ -43,6 +45,7 @@ describe('MarketsService', () => { create: jest.fn(), save: jest.fn(), findOne: jest.fn(), + find: jest.fn(), }; sorobanService = { @@ -57,6 +60,14 @@ describe('MarketsService', () => { provide: getRepositoryToken(Market), useValue: marketsRepository, }, + { + provide: getRepositoryToken(Comment), + useValue: marketsRepository, // reuse marketsRepository mock structure + }, + { + provide: getRepositoryToken(MarketTemplate), + useValue: marketsRepository, + }, { provide: UsersService, useValue: {}, diff --git a/backend/src/markets/markets.service.ts b/backend/src/markets/markets.service.ts index 33020b1c..51f0d64e 100644 --- a/backend/src/markets/markets.service.ts +++ b/backend/src/markets/markets.service.ts @@ -10,7 +10,10 @@ import { PredictionStatsDto } from './dto/prediction-stats.dto'; import { InjectRepository } from '@nestjs/typeorm'; import { Repository } from 'typeorm'; import { Market } from './entities/market.entity'; +import { Comment } from './entities/comment.entity'; +import { MarketTemplate } from './entities/market-template.entity'; import { CreateMarketDto } from './dto/create-market.dto'; +import { CreateCommentDto } from './dto/create-comment.dto'; import { UsersService } from '../users/users.service'; import { User } from '../users/entities/user.entity'; import { @@ -27,6 +30,10 @@ export class MarketsService { constructor( @InjectRepository(Market) private readonly marketsRepository: Repository, + @InjectRepository(Comment) + private readonly commentsRepository: Repository, + @InjectRepository(MarketTemplate) + private readonly marketTemplatesRepository: Repository, private readonly usersService: UsersService, private readonly sorobanService: SorobanService, ) {} @@ -264,4 +271,85 @@ export class MarketsService { ); } } + + /** + * Create a comment for a market + */ + async createComment( + marketId: string, + dto: CreateCommentDto, + user: User, + ): Promise { + const market = await this.findByIdOrOnChainId(marketId); + + let parent: Comment | null = null; + if (dto.parentId) { + parent = await this.commentsRepository.findOne({ + where: { id: dto.parentId }, + }); + if (!parent) { + throw new NotFoundException( + `Parent comment with ID "${dto.parentId}" not found`, + ); + } + } + + const comment = this.commentsRepository.create({ + content: dto.content, + author: user, + market, + parent: parent || undefined, + }); + + return await this.commentsRepository.save(comment); + } + + /** + * Get all comments for a market, including nested replies + */ + async getComments(marketId: string): Promise { + const market = await this.findByIdOrOnChainId(marketId); + + // Fetch all comments for this market + const comments = await this.commentsRepository.find({ + where: { market: { id: market.id } }, + relations: ['author', 'parent'], + order: { created_at: 'ASC' }, + }); + + // Build nested structure + const commentMap = new Map(); + const roots: Comment[] = []; + + comments.forEach((c) => { + const commentWithReplies = { ...c, replies: [] }; + commentMap.set(c.id, commentWithReplies); + }); + + comments.forEach((c) => { + const commentWithReplies = commentMap.get(c.id)!; + if (c.parent) { + const parent = commentMap.get(c.parent.id); + if (parent) { + parent.replies.push(commentWithReplies); + } else { + // Parent might not be in this market, which shouldn't happen + roots.push(commentWithReplies); + } + } else { + roots.push(commentWithReplies); + } + }); + + return roots; + } + + /** + * Get all market templates + */ + async getTemplates(): Promise { + return this.marketTemplatesRepository.find({ + order: { category: 'ASC', title: 'ASC' }, + }); + } } diff --git a/backend/src/migrations/1774800000000-CreateCommentsTable.ts b/backend/src/migrations/1774800000000-CreateCommentsTable.ts new file mode 100644 index 00000000..4c603f2e --- /dev/null +++ b/backend/src/migrations/1774800000000-CreateCommentsTable.ts @@ -0,0 +1,104 @@ +import { + MigrationInterface, + QueryRunner, + Table, + TableForeignKey, + TableIndex, +} from 'typeorm'; + +export class CreateCommentsTable1774800000000 implements MigrationInterface { + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.createTable( + new Table({ + name: 'comments', + columns: [ + { + name: 'id', + type: 'uuid', + isPrimary: true, + isGenerated: true, + generationStrategy: 'uuid', + }, + { + name: 'content', + type: 'text', + }, + { + name: 'authorId', + type: 'uuid', + }, + { + name: 'marketId', + type: 'uuid', + }, + { + name: 'parentId', + type: 'uuid', + isNullable: true, + }, + { + name: 'is_moderated', + type: 'boolean', + default: false, + }, + { + name: 'moderation_reason', + type: 'varchar', + isNullable: true, + }, + { + name: 'created_at', + type: 'timestamp', + default: 'now()', + }, + { + name: 'updated_at', + type: 'timestamp', + default: 'now()', + }, + ], + }), + true, + ); + + await queryRunner.createForeignKeys('comments', [ + new TableForeignKey({ + columnNames: ['authorId'], + referencedColumnNames: ['id'], + referencedTableName: 'users', + onDelete: 'CASCADE', + }), + new TableForeignKey({ + columnNames: ['marketId'], + referencedColumnNames: ['id'], + referencedTableName: 'markets', + onDelete: 'CASCADE', + }), + new TableForeignKey({ + columnNames: ['parentId'], + referencedColumnNames: ['id'], + referencedTableName: 'comments', + onDelete: 'CASCADE', + }), + ]); + + await queryRunner.createIndices('comments', [ + new TableIndex({ + name: 'IDX_COMMENTS_AUTHOR', + columnNames: ['authorId'], + }), + new TableIndex({ + name: 'IDX_COMMENTS_MARKET', + columnNames: ['marketId'], + }), + new TableIndex({ + name: 'IDX_COMMENTS_PARENT', + columnNames: ['parentId'], + }), + ]); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.dropTable('comments'); + } +} diff --git a/backend/src/migrations/1774900000000-CreateMarketTemplatesTable.ts b/backend/src/migrations/1774900000000-CreateMarketTemplatesTable.ts new file mode 100644 index 00000000..f630ec0d --- /dev/null +++ b/backend/src/migrations/1774900000000-CreateMarketTemplatesTable.ts @@ -0,0 +1,95 @@ +import { MigrationInterface, QueryRunner, Table } from 'typeorm'; + +export class CreateMarketTemplatesTable1774900000000 implements MigrationInterface { + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.createTable( + new Table({ + name: 'market_templates', + columns: [ + { + name: 'id', + type: 'uuid', + isPrimary: true, + isGenerated: true, + generationStrategy: 'uuid', + }, + { + name: 'title', + type: 'varchar', + }, + { + name: 'description', + type: 'text', + }, + { + name: 'category', + type: 'varchar', + }, + { + name: 'outcome_options', + type: 'text', + }, + { + name: 'suggested_duration_days', + type: 'integer', + }, + { + name: 'created_at', + type: 'timestamp', + default: 'now()', + }, + ], + }), + true, + ); + + // Seed some initial template data + const templates = [ + { + title: 'Sports Match Result', + description: 'Predict the winner of an upcoming sports match.', + category: 'Sports', + outcome_options: 'Team A,Team B,Draw', + suggested_duration_days: 7, + }, + { + title: 'Election Outcome', + description: 'Predict the winner of a political election.', + category: 'Politics', + outcome_options: 'Candidate X,Candidate Y,Other', + suggested_duration_days: 30, + }, + { + title: 'Crypto Price Prediction', + description: 'Predict if a cryptocurrency will reach a certain price.', + category: 'Crypto', + outcome_options: 'Yes,No', + suggested_duration_days: 14, + }, + { + title: 'Entertainment Awards', + description: 'Predict the winner of an award show category.', + category: 'Entertainment', + outcome_options: 'Nominee 1,Nominee 2,Nominee 3,Other', + suggested_duration_days: 21, + }, + ]; + + for (const t of templates) { + await queryRunner.query( + `INSERT INTO market_templates (title, description, category, outcome_options, suggested_duration_days) VALUES ($1, $2, $3, $4, $5)`, + [ + t.title, + t.description, + t.category, + t.outcome_options, + t.suggested_duration_days, + ], + ); + } + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.dropTable('market_templates'); + } +} diff --git a/backend/src/users/dto/list-user-markets.dto.ts b/backend/src/users/dto/list-user-markets.dto.ts index a57ac6e5..6345d83c 100644 --- a/backend/src/users/dto/list-user-markets.dto.ts +++ b/backend/src/users/dto/list-user-markets.dto.ts @@ -1,11 +1,5 @@ import { ApiPropertyOptional } from '@nestjs/swagger'; -import { - IsOptional, - IsEnum, - IsInt, - Min, - Max, -} from 'class-validator'; +import { IsOptional, IsEnum, IsInt, Min, Max } from 'class-validator'; import { Type } from 'class-transformer'; export enum UserMarketFilterStatus { diff --git a/backend/src/users/users.controller.ts b/backend/src/users/users.controller.ts index 7aee4cc8..8e2fff01 100644 --- a/backend/src/users/users.controller.ts +++ b/backend/src/users/users.controller.ts @@ -101,7 +101,7 @@ export class UsersController { @UsePipes( new ValidationPipe({ whitelist: true, forbidNonWhitelisted: false }), ) - @ApiOperation({ summary: "List markets created by a user (public)" }) + @ApiOperation({ summary: 'List markets created by a user (public)' }) @ApiResponse({ status: 200, description: 'Paginated markets list' }) @ApiResponse({ status: 404, description: 'User not found' }) async getUserMarkets( @@ -121,4 +121,11 @@ export class UsersController { ) { return this.usersService.findUserCompetitions(address, query); } + + @Get('me/export') + @ApiOperation({ summary: 'Export all user data (GDPR)' }) + @ApiResponse({ status: 200, description: 'User data exported as JSON' }) + async exportData(@CurrentUser() user: User) { + return await this.usersService.exportUserData(user.id); + } } diff --git a/backend/src/users/users.module.ts b/backend/src/users/users.module.ts index a8b1b078..7a6b91c4 100644 --- a/backend/src/users/users.module.ts +++ b/backend/src/users/users.module.ts @@ -6,6 +6,7 @@ import { UsersController } from './users.controller'; import { Prediction } from '../predictions/entities/prediction.entity'; import { CompetitionParticipant } from '../competitions/entities/competition-participant.entity'; import { Market } from '../markets/entities/market.entity'; +import { Notification } from '../notifications/entities/notification.entity'; @Module({ imports: [ @@ -14,6 +15,7 @@ import { Market } from '../markets/entities/market.entity'; Prediction, CompetitionParticipant, Market, + Notification, ]), ], controllers: [UsersController], diff --git a/backend/src/users/users.service.spec.ts b/backend/src/users/users.service.spec.ts index 919d0239..2eccf302 100644 --- a/backend/src/users/users.service.spec.ts +++ b/backend/src/users/users.service.spec.ts @@ -5,10 +5,11 @@ import { Repository } from 'typeorm'; import { UsersService } from './users.service'; import { User } from './entities/user.entity'; import { Prediction } from '../predictions/entities/prediction.entity'; +import { Market } from '../markets/entities/market.entity'; +import { Notification } from '../notifications/entities/notification.entity'; import { ListUserPredictionsDto } from './dto/list-user-predictions.dto'; import { CompetitionParticipant } from '../competitions/entities/competition-participant.entity'; import { UserCompetitionFilterStatus } from './dto/list-user-competitions.dto'; -import { Market } from '../markets/entities/market.entity'; import { ListUserMarketsDto, UserMarketFilterStatus, @@ -59,18 +60,27 @@ describe('UsersService', () => { provide: getRepositoryToken(Prediction), useValue: { createQueryBuilder: jest.fn(), + find: jest.fn(), }, }, { - provide: getRepositoryToken(CompetitionParticipant), + provide: getRepositoryToken(Market), useValue: { + find: jest.fn(), createQueryBuilder: jest.fn(), }, }, { - provide: getRepositoryToken(Market), + provide: getRepositoryToken(Notification), + useValue: { + find: jest.fn(), + }, + }, + { + provide: getRepositoryToken(CompetitionParticipant), useValue: { createQueryBuilder: jest.fn(), + find: jest.fn(), }, }, ], diff --git a/backend/src/users/users.service.ts b/backend/src/users/users.service.ts index 9766a36f..f185288c 100644 --- a/backend/src/users/users.service.ts +++ b/backend/src/users/users.service.ts @@ -9,6 +9,8 @@ import { PublicUserPredictionItem, } from './dto/list-user-predictions.dto'; import { User } from './entities/user.entity'; +import { Market } from '../markets/entities/market.entity'; +import { Notification } from '../notifications/entities/notification.entity'; import { UpdateUserDto } from './dto/update-user.dto'; import { CompetitionParticipant } from '../competitions/entities/competition-participant.entity'; @@ -16,7 +18,6 @@ import { ListUserCompetitionsDto, UserCompetitionFilterStatus, } from './dto/list-user-competitions.dto'; -import { Market } from '../markets/entities/market.entity'; import { ListUserMarketsDto, PaginatedUserMarketsResponse, @@ -32,11 +33,12 @@ export class UsersService { private readonly usersRepository: Repository, @InjectRepository(Prediction) private readonly predictionsRepository: Repository, - - @InjectRepository(CompetitionParticipant) - private readonly participantsRepository: Repository, @InjectRepository(Market) private readonly marketsRepository: Repository, + @InjectRepository(Notification) + private readonly notificationsRepository: Repository, + @InjectRepository(CompetitionParticipant) + private readonly participantsRepository: Repository, ) {} async findAll(): Promise { @@ -224,4 +226,75 @@ export class UsersService { return { data, total, page, limit }; } + + async exportUserData(userId: string) { + const user = await this.findById(userId); + + const [predictions, markets, notifications, competitions] = + await Promise.all([ + this.predictionsRepository.find({ + where: { user: { id: user.id } }, + relations: ['market'], + }), + this.marketsRepository.find({ + where: { creator: { id: user.id } }, + }), + this.notificationsRepository.find({ + where: { user: { id: user.id } }, + order: { created_at: 'DESC' }, + }), + this.participantsRepository.find({ + where: { user_id: user.id }, + relations: ['competition'], + }), + ]); + + return { + profile: { + id: user.id, + stellar_address: user.stellar_address, + username: user.username, + avatar_url: user.avatar_url, + reputation_score: user.reputation_score, + season_points: user.season_points, + created_at: user.created_at, + }, + stats: { + total_predictions: user.total_predictions, + correct_predictions: user.correct_predictions, + total_staked_stroops: user.total_staked_stroops, + total_winnings_stroops: user.total_winnings_stroops, + }, + predictions: predictions.map((p) => ({ + id: p.id, + market_id: p.market.id, + market_title: p.market.title, + chosen_outcome: p.chosen_outcome, + stake_amount_stroops: p.stake_amount_stroops, + submitted_at: p.submitted_at, + })), + markets_created: markets.map((m) => ({ + id: m.id, + title: m.title, + category: m.category, + is_resolved: m.is_resolved, + created_at: m.created_at, + })), + notifications: notifications.map((n) => ({ + id: n.id, + type: n.type, + title: n.title, + message: n.message, + is_read: n.is_read, + created_at: n.created_at, + })), + competitions_joined: competitions.map((c) => ({ + id: c.competition.id, + title: c.competition.title, + rank: c.rank, + score: c.score, + })), + exported_at: new Date().toISOString(), + }; + } } diff --git a/backend/test/users.e2e-spec.ts b/backend/test/users.e2e-spec.ts index e2d8cf32..2bc1594e 100644 --- a/backend/test/users.e2e-spec.ts +++ b/backend/test/users.e2e-spec.ts @@ -243,14 +243,16 @@ describe('Users (e2e)', () => { return request(app.getHttpServer()) .get(`/users/${mockUser.stellar_address}/markets`) .expect(200) - .expect((res: { body: { data: { data: unknown[]; total: number } } }) => { - expect(queryBuilder.where).toHaveBeenCalledWith( - 'market.creatorId = :userId', - { userId: mockUser.id }, - ); - expect(res.body.data.data).toEqual([]); - expect(res.body.data.total).toBe(0); - }); + .expect( + (res: { body: { data: { data: unknown[]; total: number } } }) => { + expect(queryBuilder.where).toHaveBeenCalledWith( + 'market.creatorId = :userId', + { userId: mockUser.id }, + ); + expect(res.body.data.data).toEqual([]); + expect(res.body.data.total).toBe(0); + }, + ); }); it('should apply status=active filter query', () => {