Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file added backend/lint.txt
Binary file not shown.
23 changes: 23 additions & 0 deletions backend/lint2.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@

> backend@0.0.1 lint C:\Users\DELL\Desktop\Arena\InsightArena\backend
> eslint "{src,apps,libs,test}/**/*.ts" --fix


C:\Users\DELL\Desktop\Arena\InsightArena\backend\src\admin\admin.service.ts
35:24 error 'SystemConfigValues' is an 'error' type that acts as 'any' and overrides all other types in this union type @typescript-eslint/no-redundant-type-constituents
50:23 warning Unsafe argument of type error typed assigned to a parameter of type `EntityClassOrSchema` @typescript-eslint/no-unsafe-argument
61:11 error Unsafe assignment of an `any` value @typescript-eslint/no-unsafe-assignment
64:15 error Unsafe member access .key on a type that cannot be resolved @typescript-eslint/no-unsafe-member-access
65:45 error The type of computed name [row.key] cannot be resolved @typescript-eslint/no-unsafe-member-access
65:49 error Unsafe member access .key on a type that cannot be resolved @typescript-eslint/no-unsafe-member-access
65:60 error Unsafe member access .value on a type that cannot be resolved @typescript-eslint/no-unsafe-member-access
69:5 error Unsafe assignment of an `any` value @typescript-eslint/no-unsafe-assignment
77:36 warning Unsafe argument of type error typed assigned to a parameter of type `{ [s: string]: unknown; } | ArrayLike<unknown>` @typescript-eslint/no-unsafe-argument

C:\Users\DELL\Desktop\Arena\InsightArena\backend\src\markets\markets.service.spec.ts
315:7 warning Unsafe argument of type `any` assigned to a parameter of type `SelectQueryBuilder<Market>` @typescript-eslint/no-unsafe-argument
363:7 warning Unsafe argument of type `any` assigned to a parameter of type `SelectQueryBuilder<Market>` @typescript-eslint/no-unsafe-argument

??? 11 problems (7 errors, 4 warnings)

???ELIFECYCLE??? Command failed with exit code 1.
Binary file added backend/lint_final.json
Binary file not shown.
14 changes: 14 additions & 0 deletions backend/src/admin/admin.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import { ModerateCommentDto } from './dto/moderate-comment.dto';
import { ReportQueryDto } from './dto/report-query.dto';
import { ResolveMarketDto } from './dto/resolve-market.dto';
import { StatsResponseDto } from './dto/stats-response.dto';
import { UpdateSystemConfigDto } from './dto/system-config.dto';
import { UpdateUserRoleDto } from './dto/update-user-role.dto';

@Controller('admin')
Expand Down Expand Up @@ -161,4 +162,17 @@ export class AdminController {
async getActivityReport(@Query() query: ReportQueryDto) {
return this.adminService.getActivityReport(query);
}

@Get('config')
async getConfig() {
return this.adminService.getConfig();
}

@Patch('config')
async updateConfig(@Body() dto: UpdateSystemConfigDto, @Request() req: any) {
return this.adminService.updateConfig(
dto,
(req as { user: { id: string } }).user.id,
);
}
}
2 changes: 2 additions & 0 deletions backend/src/admin/admin.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { Prediction } from '../predictions/entities/prediction.entity';
import { User } from '../users/entities/user.entity';
import { AdminController } from './admin.controller';
import { AdminService } from './admin.service';
import { SystemConfig } from './entities/system-config.entity';

@Module({
imports: [
Expand All @@ -22,6 +23,7 @@ import { AdminService } from './admin.service';
Competition,
CompetitionParticipant,
ActivityLog,
SystemConfig,
]),
FlagsModule,
NotificationsModule,
Expand Down
6 changes: 6 additions & 0 deletions backend/src/admin/admin.service.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import { Prediction } from '../predictions/entities/prediction.entity';
import { SorobanService } from '../soroban/soroban.service';
import { User } from '../users/entities/user.entity';
import { AdminService } from './admin.service';
import { SystemConfig } from './entities/system-config.entity';
import { ResolveMarketDto } from './dto/resolve-market.dto';

const mockRepo = () => ({
Expand Down Expand Up @@ -84,6 +85,7 @@ describe('AdminService.adminResolveMarket', () => {
useValue: mockRepo(),
},
{ provide: getRepositoryToken(ActivityLog), useValue: mockRepo() },
{ provide: getRepositoryToken(SystemConfig), useValue: mockRepo() },
{ provide: AnalyticsService, useValue: analyticsService },
{ provide: NotificationsService, useValue: notificationsService },
{ provide: SorobanService, useValue: sorobanService },
Expand Down Expand Up @@ -262,6 +264,7 @@ describe('AdminService.featureMarket', () => {
useValue: mockRepo(),
},
{ provide: getRepositoryToken(ActivityLog), useValue: mockRepo() },
{ provide: getRepositoryToken(SystemConfig), useValue: mockRepo() },
{ provide: AnalyticsService, useValue: analyticsService },
{ provide: NotificationsService, useValue: { create: jest.fn() } },
{ provide: SorobanService, useValue: { resolveMarket: jest.fn() } },
Expand Down Expand Up @@ -361,6 +364,7 @@ describe('AdminService.unfeatureMarket', () => {
useValue: mockRepo(),
},
{ provide: getRepositoryToken(ActivityLog), useValue: mockRepo() },
{ provide: getRepositoryToken(SystemConfig), useValue: mockRepo() },
{ provide: AnalyticsService, useValue: analyticsService },
{ provide: NotificationsService, useValue: { create: jest.fn() } },
{ provide: SorobanService, useValue: { resolveMarket: jest.fn() } },
Expand Down Expand Up @@ -449,6 +453,7 @@ describe('AdminService.updateUserRole', () => {
useValue: mockRepo(),
},
{ provide: getRepositoryToken(ActivityLog), useValue: mockRepo() },
{ provide: getRepositoryToken(SystemConfig), useValue: mockRepo() },
{ provide: AnalyticsService, useValue: analyticsService },
{
provide: NotificationsService,
Expand Down Expand Up @@ -556,6 +561,7 @@ describe('AdminService.adminCancelCompetition', () => {
useValue: participantsRepo,
},
{ provide: getRepositoryToken(ActivityLog), useValue: mockRepo() },
{ provide: getRepositoryToken(SystemConfig), useValue: mockRepo() },
{ provide: AnalyticsService, useValue: analyticsService },
{ provide: NotificationsService, useValue: notificationsService },
{ provide: SorobanService, useValue: sorobanService },
Expand Down
48 changes: 47 additions & 1 deletion backend/src/admin/admin.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,10 +32,16 @@ import {
import { ResolveMarketDto } from './dto/resolve-market.dto';
import { StatsResponseDto } from './dto/stats-response.dto';
import { UpdateUserRoleDto } from './dto/update-user-role.dto';

import { SystemConfig } from './entities/system-config.entity';
import {
DEFAULT_CONFIG,
SystemConfigValues,
UpdateSystemConfigDto,
} from './dto/system-config.dto';
@Injectable()
export class AdminService {
private readonly logger = new Logger(AdminService.name);
private configCache: SystemConfigValues | null = null;

constructor(
@InjectRepository(User)
Expand All @@ -52,12 +58,52 @@ export class AdminService {
private readonly competitionParticipantsRepository: Repository<CompetitionParticipant>,
@InjectRepository(ActivityLog)
private readonly activityLogsRepository: Repository<ActivityLog>,
@InjectRepository(SystemConfig)
private readonly systemConfigRepository: Repository<SystemConfig>,
private readonly analyticsService: AnalyticsService,
private readonly notificationsService: NotificationsService,
private readonly sorobanService: SorobanService,
private readonly flagsService: FlagsService,
) {}

async getConfig(): Promise<SystemConfigValues> {
if (this.configCache) return this.configCache;

const rows = await this.systemConfigRepository.find();
const config = { ...DEFAULT_CONFIG };

for (const row of rows) {
if (row.key in config) {
(config as Record<string, unknown>)[row.key] = row.value;
}
}

this.configCache = config;
return config;
}

async updateConfig(
dto: UpdateSystemConfigDto,
adminId: string,
): Promise<SystemConfigValues> {
const updates = Object.entries(dto).filter(([, v]) => v !== undefined);

for (const [key, value] of updates) {
await this.systemConfigRepository.save({
key,
value: value as unknown,
});
}

this.configCache = null;

await this.analyticsService.logActivity(adminId, 'SYSTEM_CONFIG_UPDATED', {
updated_keys: updates.map(([k]) => k),
});

return this.getConfig();
}

async getStats(): Promise<StatsResponseDto> {
const now = new Date();
const twentyFourHoursAgo = new Date(now.getTime() - 24 * 60 * 60 * 1000);
Expand Down
49 changes: 49 additions & 0 deletions backend/src/admin/dto/system-config.dto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import { IsNumber, IsBoolean, IsOptional, Min, Max } from 'class-validator';

export class UpdateSystemConfigDto {
@IsOptional()
@IsNumber()
@Min(0)
@Max(100)
platform_fee_percent?: number;

@IsOptional()
@IsNumber()
@Min(0)
min_stake_stroops?: number;

@IsOptional()
@IsNumber()
@Min(1)
max_markets_per_user?: number;

@IsOptional()
@IsBoolean()
maintenance_mode?: boolean;

@IsOptional()
@IsBoolean()
feature_competitions?: boolean;

@IsOptional()
@IsBoolean()
feature_leaderboard?: boolean;
}

export interface SystemConfigValues {
platform_fee_percent: number;
min_stake_stroops: number;
max_markets_per_user: number;
maintenance_mode: boolean;
feature_competitions: boolean;
feature_leaderboard: boolean;
}

export const DEFAULT_CONFIG: SystemConfigValues = {
platform_fee_percent: 2,
min_stake_stroops: 1000000,
max_markets_per_user: 10,
maintenance_mode: false,
feature_competitions: true,
feature_leaderboard: true,
};
13 changes: 13 additions & 0 deletions backend/src/admin/entities/system-config.entity.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { Entity, PrimaryColumn, Column, UpdateDateColumn } from 'typeorm';

@Entity('system_config')
export class SystemConfig {
@PrimaryColumn()
key: string;

@Column('jsonb')
value: unknown;

@UpdateDateColumn()
updated_at: Date;
}
111 changes: 111 additions & 0 deletions backend/src/admin/system-config.service.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
import { Test, TestingModule } from '@nestjs/testing';
import { getRepositoryToken } from '@nestjs/typeorm';
import { AnalyticsService } from '../analytics/analytics.service';
import { Competition } from '../competitions/entities/competition.entity';
import { Market } from '../markets/entities/market.entity';
import { Comment } from '../markets/entities/comment.entity';
import { NotificationsService } from '../notifications/notifications.service';
import { Prediction } from '../predictions/entities/prediction.entity';
import { SorobanService } from '../soroban/soroban.service';
import { User } from '../users/entities/user.entity';
import { ActivityLog } from '../analytics/entities/activity-log.entity';
import { AdminService } from './admin.service';
import { SystemConfig } from './entities/system-config.entity';
import { DEFAULT_CONFIG } from './dto/system-config.dto';

const mockRepo = () => ({
findOne: jest.fn(),
save: jest.fn(),
find: jest.fn(),
count: jest.fn(),
createQueryBuilder: jest.fn(),
});

describe('AdminService - system config', () => {
let service: AdminService;
let configRepo: ReturnType<typeof mockRepo>;
let analyticsService: jest.Mocked<Pick<AnalyticsService, 'logActivity'>>;

const adminId = 'admin-1';

beforeEach(async () => {
configRepo = mockRepo();
analyticsService = { logActivity: jest.fn().mockResolvedValue({}) };

const module: TestingModule = await Test.createTestingModule({
providers: [
AdminService,
{ provide: getRepositoryToken(User), useValue: mockRepo() },
{ provide: getRepositoryToken(Market), useValue: mockRepo() },
{ provide: getRepositoryToken(Comment), useValue: mockRepo() },
{ provide: getRepositoryToken(Prediction), useValue: mockRepo() },
{ provide: getRepositoryToken(Competition), useValue: mockRepo() },
{ provide: getRepositoryToken(ActivityLog), useValue: mockRepo() },
{ provide: getRepositoryToken(SystemConfig), useValue: configRepo },
{ provide: AnalyticsService, useValue: analyticsService },
{ provide: NotificationsService, useValue: { create: jest.fn() } },
{ provide: SorobanService, useValue: { resolveMarket: jest.fn() } },
],
}).compile();

service = module.get<AdminService>(AdminService);
});

describe('getConfig', () => {
it('returns defaults when no rows exist', async () => {
configRepo.find.mockResolvedValue([]);
const config = await service.getConfig();
expect(config).toEqual(DEFAULT_CONFIG);
});

it('merges stored values over defaults', async () => {
configRepo.find.mockResolvedValue([
{ key: 'platform_fee_percent', value: 5 },
{ key: 'maintenance_mode', value: true },
]);
const config = await service.getConfig();
expect(config.platform_fee_percent).toBe(5);
expect(config.maintenance_mode).toBe(true);
expect(config.min_stake_stroops).toBe(DEFAULT_CONFIG.min_stake_stroops);
});

it('returns cached value on second call', async () => {
configRepo.find.mockResolvedValue([]);
await service.getConfig();
await service.getConfig();
expect(configRepo.find).toHaveBeenCalledTimes(1);
});
});

describe('updateConfig', () => {
it('saves each provided key and invalidates cache', async () => {
configRepo.find.mockResolvedValue([]);
configRepo.save.mockResolvedValue({});

await service.updateConfig({ platform_fee_percent: 3 }, adminId);

expect(configRepo.save).toHaveBeenCalledWith({
key: 'platform_fee_percent',
value: 3,
});
expect(analyticsService.logActivity).toHaveBeenCalledWith(
adminId,
'SYSTEM_CONFIG_UPDATED',
expect.objectContaining({ updated_keys: ['platform_fee_percent'] }),
);
});

it('does not save keys with undefined values', async () => {
configRepo.find.mockResolvedValue([]);
configRepo.save.mockResolvedValue({});

await service.updateConfig({ maintenance_mode: true }, adminId);

expect(configRepo.save).toHaveBeenCalledTimes(1);
expect(configRepo.save).toHaveBeenCalledWith({
key: 'maintenance_mode',
value: true,
});
});
});
});
9 changes: 6 additions & 3 deletions backend/src/flags/flags.service.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,14 +51,17 @@
end_time: new Date(),
resolution_time: new Date(),
is_resolved: false,
resolved_outcome: null,
resolved_outcome: null as any,
is_public: true,
is_cancelled: false,
total_pool_stroops: '1000',
participant_count: 0,
created_at: new Date(),
is_featured: false,
featured_at: null,
created_at: new Date('2026-04-03T23:00:00.000Z'),
};

const mockDate = new Date('2026-04-03T23:00:00.000Z');
const createMockFlag = (): Flag => ({
id: 'flag-1',
market: mockMarket,
Expand All @@ -73,7 +76,7 @@
resolved_by: null,
resolved_by_user: null,
resolved_at: null,
created_at: new Date(),
created_at: mockDate,
});

beforeEach(async () => {
Expand Down Expand Up @@ -225,7 +228,7 @@

jest
.spyOn(flagsRepository, 'createQueryBuilder')
.mockReturnValue(mockQueryBuilder as any);

Check warning on line 231 in backend/src/flags/flags.service.spec.ts

View workflow job for this annotation

GitHub Actions / Lint

Unsafe argument of type `any` assigned to a parameter of type `SelectQueryBuilder<Flag>`

const result = await service.listFlags(query);

Expand Down Expand Up @@ -290,7 +293,7 @@
jest
.spyOn(flagsRepository, 'findOne')
.mockResolvedValue(createMockFlag());
jest.spyOn(marketsRepository, 'update').mockResolvedValue({} as any);

Check warning on line 296 in backend/src/flags/flags.service.spec.ts

View workflow job for this annotation

GitHub Actions / Lint

Unsafe argument of type `any` assigned to a parameter of type `UpdateResult | Promise<UpdateResult>`
jest.spyOn(flagsRepository, 'save').mockResolvedValue({
...createMockFlag(),
status: FlagStatus.RESOLVED,
Expand Down Expand Up @@ -321,7 +324,7 @@
jest
.spyOn(flagsRepository, 'findOne')
.mockResolvedValue(createMockFlag());
jest.spyOn(usersRepository, 'update').mockResolvedValue({} as any);

Check warning on line 327 in backend/src/flags/flags.service.spec.ts

View workflow job for this annotation

GitHub Actions / Lint

Unsafe argument of type `any` assigned to a parameter of type `UpdateResult | Promise<UpdateResult>`
jest.spyOn(flagsRepository, 'save').mockResolvedValue({
...createMockFlag(),
status: FlagStatus.RESOLVED,
Expand Down
Loading
Loading