From 204a2b890fd7edac9ba501b06f7b8229a43b4c69 Mon Sep 17 00:00:00 2001 From: David Ojo Date: Sun, 29 Mar 2026 21:16:00 +0100 Subject: [PATCH 1/2] feat(admin): add system config endpoints GET/PATCH /admin/config - Add SystemConfig entity with key/value jsonb storage - Add UpdateSystemConfigDto with validation for all config fields - Add getConfig() with in-memory cache and updateConfig() to AdminService - Register GET /admin/config and PATCH /admin/config (admin only) - Add migration for system_config table - Add tests for config defaults, merging, caching, and partial updates Closes #409 --- backend/src/admin/admin.controller.ts | 14 +++ backend/src/admin/admin.module.ts | 2 + backend/src/admin/admin.service.ts | 41 +++++++ backend/src/admin/dto/system-config.dto.ts | 55 ++++++++++ .../admin/entities/system-config.entity.ts | 13 +++ .../src/admin/system-config.service.spec.ts | 103 ++++++++++++++++++ .../1774600000000-CreateSystemConfigEntity.ts | 15 +++ 7 files changed, 243 insertions(+) create mode 100644 backend/src/admin/dto/system-config.dto.ts create mode 100644 backend/src/admin/entities/system-config.entity.ts create mode 100644 backend/src/admin/system-config.service.spec.ts create mode 100644 backend/src/migrations/1774600000000-CreateSystemConfigEntity.ts diff --git a/backend/src/admin/admin.controller.ts b/backend/src/admin/admin.controller.ts index 469da1cd..9c74d4b0 100644 --- a/backend/src/admin/admin.controller.ts +++ b/backend/src/admin/admin.controller.ts @@ -19,6 +19,7 @@ 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 { UpdateSystemConfigDto } from './dto/system-config.dto'; @Controller('admin') @UseGuards(JwtAuthGuard, RolesGuard) @@ -77,4 +78,17 @@ export class AdminController { (req as { user: { id: string } }).user.id, ); } + + @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, + ); + } } diff --git a/backend/src/admin/admin.module.ts b/backend/src/admin/admin.module.ts index f4269615..7eb29c22 100644 --- a/backend/src/admin/admin.module.ts +++ b/backend/src/admin/admin.module.ts @@ -5,6 +5,7 @@ import { Market } from '../markets/entities/market.entity'; import { Prediction } from '../predictions/entities/prediction.entity'; import { Competition } from '../competitions/entities/competition.entity'; import { ActivityLog } from '../analytics/entities/activity-log.entity'; +import { SystemConfig } from './entities/system-config.entity'; import { NotificationsModule } from '../notifications/notifications.module'; import { AdminController } from './admin.controller'; import { AdminService } from './admin.service'; @@ -17,6 +18,7 @@ import { AdminService } from './admin.service'; Prediction, Competition, ActivityLog, + SystemConfig, ]), NotificationsModule, ], diff --git a/backend/src/admin/admin.service.ts b/backend/src/admin/admin.service.ts index 7cdd00c9..4d2acaa3 100644 --- a/backend/src/admin/admin.service.ts +++ b/backend/src/admin/admin.service.ts @@ -21,10 +21,17 @@ 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 { SystemConfig } from './entities/system-config.entity'; +import { + UpdateSystemConfigDto, + SystemConfigValues, + DEFAULT_CONFIG, +} from './dto/system-config.dto'; @Injectable() export class AdminService { private readonly logger = new Logger(AdminService.name); + private configCache: SystemConfigValues | null = null; constructor( @InjectRepository(User) @@ -37,11 +44,45 @@ export class AdminService { private readonly competitionsRepository: Repository, @InjectRepository(ActivityLog) private readonly activityLogsRepository: Repository, + @InjectRepository(SystemConfig) + private readonly systemConfigRepository: Repository, private readonly analyticsService: AnalyticsService, private readonly notificationsService: NotificationsService, private readonly sorobanService: SorobanService, ) {} + async getConfig(): Promise { + 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)[row.key] = row.value; + } + } + + this.configCache = config; + return config; + } + + async updateConfig(dto: UpdateSystemConfigDto, adminId: string): Promise { + const updates = Object.entries(dto).filter(([, v]) => v !== undefined); + + for (const [key, value] of updates) { + await this.systemConfigRepository.save({ key, value }); + } + + this.configCache = null; + + await this.analyticsService.logActivity(adminId, 'SYSTEM_CONFIG_UPDATED', { + updated_keys: updates.map(([k]) => k), + }); + + return this.getConfig(); + } + async getStats(): Promise { const now = new Date(); const twentyFourHoursAgo = new Date(now.getTime() - 24 * 60 * 60 * 1000); diff --git a/backend/src/admin/dto/system-config.dto.ts b/backend/src/admin/dto/system-config.dto.ts new file mode 100644 index 00000000..81aeba85 --- /dev/null +++ b/backend/src/admin/dto/system-config.dto.ts @@ -0,0 +1,55 @@ +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, +}; diff --git a/backend/src/admin/entities/system-config.entity.ts b/backend/src/admin/entities/system-config.entity.ts new file mode 100644 index 00000000..a67dfad9 --- /dev/null +++ b/backend/src/admin/entities/system-config.entity.ts @@ -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; +} diff --git a/backend/src/admin/system-config.service.spec.ts b/backend/src/admin/system-config.service.spec.ts new file mode 100644 index 00000000..fafa121d --- /dev/null +++ b/backend/src/admin/system-config.service.spec.ts @@ -0,0 +1,103 @@ +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 { 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; + let analyticsService: jest.Mocked>; + + 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(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); + }); + + 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 }); + }); + }); +}); diff --git a/backend/src/migrations/1774600000000-CreateSystemConfigEntity.ts b/backend/src/migrations/1774600000000-CreateSystemConfigEntity.ts new file mode 100644 index 00000000..4d7012b7 --- /dev/null +++ b/backend/src/migrations/1774600000000-CreateSystemConfigEntity.ts @@ -0,0 +1,15 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class CreateSystemConfigEntity1774600000000 implements MigrationInterface { + name = 'CreateSystemConfigEntity1774600000000'; + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `CREATE TABLE "system_config" ("key" character varying NOT NULL, "value" jsonb NOT NULL, "updated_at" TIMESTAMP NOT NULL DEFAULT now(), CONSTRAINT "PK_system_config" PRIMARY KEY ("key"))`, + ); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(`DROP TABLE "system_config"`); + } +} From fc4e9db2a2f7cf4b34e3eace2e67a367438a9630 Mon Sep 17 00:00:00 2001 From: David Ojo Date: Wed, 1 Apr 2026 21:04:38 +0100 Subject: [PATCH 2/2] fix: resolve admin module imports and test mocks --- backend/lint.txt | Bin 0 -> 4588 bytes backend/lint2.txt | 23 ++++++++++++++++++ backend/lint_final.json | Bin 0 -> 168058 bytes backend/src/admin/admin.controller.ts | 14 +++++++++++ backend/src/admin/admin.service.spec.ts | 5 ++++ backend/src/admin/admin.service.ts | 12 +++++++-- backend/src/admin/dto/system-config.dto.ts | 8 +----- .../src/admin/system-config.service.spec.ts | 12 +++++++-- backend/test_out.txt | Bin 0 -> 239778 bytes 9 files changed, 63 insertions(+), 11 deletions(-) create mode 100644 backend/lint.txt create mode 100644 backend/lint2.txt create mode 100644 backend/lint_final.json create mode 100644 backend/test_out.txt diff --git a/backend/lint.txt b/backend/lint.txt new file mode 100644 index 0000000000000000000000000000000000000000..2e5c14b8bc95e25f0bc13a8e1ea2a296b18aed1a GIT binary patch literal 4588 zcmeI0|4JJ{5XT4nuh4fm{KvLbZR#(yBHCXdSW2-LiV=*zgGnyj88uj+!bj-C^kMo4 zg|?sXtmj>fl^W4nkmYi7yR)-1-Wi;Nf zx+7SmhZq-z`;4qZ1%0JmL>sX&kbZ8X{w2NyR1^Qw_<${18ws+!j#pyvm5tl>iS=sM zazqB;til!1*YH`nB#rGMqY0;|9eCtnMS+kLrQZRc+vXjlZ19fJI!LNwn|!6Q8ck!8 zrO{lz#uY(fouE;R@2cya^U1RAHjBidXNqnKvzqh_`np++3?nV3PZ76)^@?HHEWT{^ zAhU&&P^rajEjIa6B(}l|3iTWknnB*G?tAhkFq#g@2vCRp$2E zu7bMoPFYX54D#;8C5!2CD}io_CY_4d1Rc+#n`3ky;SkNMtkl5%Riy1hhp6w>-rJpb z9dx^K$q^oPgvb60VWCzE>WS^oVSvlb{Wy9TOrmi(o}-mMzxZ2ikke9g-E{ zeMyP&hq8pC`(L!)5ia7QXwGrTaaML7C11$0?nwUVJ5V@f`8Did<)1n`yv0aBMyI3J zi))&bjH2HZvAN&w<2l>sm&Nlqr{Bf#d=;FFnfQOq z9XA`#ljMz%H7?2ccj7uBxBX(_Zi9+%uNRoeRG*jebMd?T?v(mHX1(hcRq}h(1h_>YDH+$P=Zh6*ad-As!!NR@YdpGd zeel~zf>&QcRq5BgDzRyMSFrqMJS%LpV@8O)d!#PA#b^(l^nJA-AQtX1O?(tn_sWlT zA~mu{j$vLC>Ys|p@9~}Sm)hgA{~}KMzS(#GA7$R&@_uEXc^06;i2ip`<)~etn{C8R ctKv}-BJTiNHM!1|L#I_c#vVh`bjVlw4d+$FivR!s literal 0 HcmV?d00001 diff --git a/backend/lint2.txt b/backend/lint2.txt new file mode 100644 index 00000000..30d41e28 --- /dev/null +++ b/backend/lint2.txt @@ -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` @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` @typescript-eslint/no-unsafe-argument + 363:7 warning Unsafe argument of type `any` assigned to a parameter of type `SelectQueryBuilder` @typescript-eslint/no-unsafe-argument + +??? 11 problems (7 errors, 4 warnings) + +???ELIFECYCLE??? Command failed with exit code 1. diff --git a/backend/lint_final.json b/backend/lint_final.json new file mode 100644 index 0000000000000000000000000000000000000000..58d5cb918010b3630aad6b5a11af66c25d9c04f0 GIT binary patch literal 168058 zcmeI5{c{z^)#vBut=j+LhRU;&pN)iHV#je3Cm3+TF4%;ZxKfxBAPFo35|M<7){p=7 z-94W^_oz?zbkCi85g2Q_RFdwU>FJl#-}Bn1d;Z`5z1RKNUFaTnd)>2cx4YRr?2fzd z&+-+wv@>cS7kSd3HN|;?8&S+-~=S{5}t5j_2*y?rq952U6Fu)N&xBVLXT3 zrhGq=vHV7Uspm+39!h`s#3=7|_rtxT?xl?2Md&?ccBM}@2Fg=6?Hme~4&+(drM~w= zZI1&TA4(st4wic*Mv#{=$^Ko90aq7soL%$jO zEg2IOj8ytbMs|L%@?2(ZXFkS;kvjgSj1iT3KL}TC3ny#}FT9Zd+fAkn$g75zh?k~$ zYFh35qhv?yfr9~CW<&Ho+VzE$!bU>A=rc_lO<5aZ4Ihhq`~*Jn-q|$$q#f~qk*z9I>|67jr|LHw0&+Yw-AALHJczTT%8dJX>D4F(3?+~+2`}S$~(-}v?KM14!WkkY{gdQ)2^2GUc zKK5kZ@XeWxnZ&)nl~3OWB+R-mWd3)$zslcZDNhXlMB@Da4o`#QJ$Y_lM&EztmX!Zj zK+chrTl5UGv@aM0UE3qdUz5_`OAC~>6@#=F*QLfMVfEuM>)%&$=eYY){=#oNgMIr+ zDDqh7aUe9Bb!**U4mnL{VSUJY3~it|+)eD{P^dZ+ zR1UXga)k2F7qs?;v~Vo*wJSf&&rDW1#zzY2YOt;w;Tte)mcqd`K^?!QBYT4)3Rmi zl}au{x!Xay9>|(9QfHieLPH|)^_h!;djEYns@$DRu^J^Ta#bF^-CbtHWl8w=po!5l zR|LBUMQmS`+2dYa@@>pDI`vo#e^~xum!U0<4oBT`J;5H}d2D0V2S@T*?TWFz@V1F* zL2Ml?05mC_pV|(-RNmiu$ve_lq#*Xf!|*AV&2>9^(5MaHnEkcv*i^5B&}YFT@Dg>* zOw)FJdLvN7r2V)VzAe;)f=9u^@T)vy+GlR{>G~?sFv9K%?RG*PyQe6-73i@$3hU&; z*z*x(^ah&7wW{G>+!l^tEM)MY^RZ>|+)8|3(xFH0`3=!Vv4-)y?{QA%(wE{j-4#pd zPPZj}`nvl{u3QsaKB96a{j2r-A3{_0{Eo6GJjH8I>)gkdhVVZ$cI6RI`@bZXwAuda z-Fv6D_pbOS#!oY$;5EH z_NB}#`S1OX^=gZoQeI`md(LZMKJvCb7B0&(WQ*;Jk3DYYX5D{uuLoi3m7T6eo9^X$WJ;CCNaH7vwq5tac=DCe3X3(M|oeWoAH;f|M9fV&ok7H z?X#DltMuDjqbd_~Pp{s0-}b8gNBZEAs>iU(2tYurVc9XH?XNnDn}O$23vrdPfV12x zU3NsJzN|VfrB3eQtuiv5rM#wP$5!sos-r90o~w*5_q|oBE<3t%e^wn`P1mh5KCknb z1G5)OGOaEzYCS5AaIwVg$7ND2Nk10Fe4@r5FX`(ZXutihgjQwWJ1x6PKTFcVm`p+n%MsfZ}tlQOiB_I7FBx=ik?)q(}NUfJG_rmj_ z_x~;nMNAU+Enf9S;LcS%AfpR@gyyEO#zk#vYuoI2YMr$#P&z4J`f(d~el0OCTca?) z6WkCZSeM@73%n=11J<6&H{$c_@^des>IYd>1FL@$M6q%>>wYDlL6NSr5E)^V*khh^ z{fMDbnZNGMIRyMhG=fuIyG+MZ=b5vRK&;WS{<kSvObQq z)|#z0`_EM`QM8uZGNT&Vs_S$sfz{WKoR#&t=V6uLDlaYma3!<~HcR;PPwV<1d9^3= z)zHK4Z-N5!|B=k#6Zu@LTU*|oTQC;+q|KScXJ?X)@LT!&pYr>O>=VmBeIVEPS=2*p zANXuv5XH*#>-Oq*^PVi`hQ6(*T*7!|pKg)iMogsXrE#Y9lE~0iC z`9049e=`eOj&);vxm~%#YC&FdRn+F~Zv_o#D>K8=C8gow8G3!nXslRQ%op+KSVWd1 zcSDFMKe2fHUJDKN?g!C_nn|`hU+cma!;{6v#Zn?aD)(&{&qm%R5)2&x26P1?u0ehw z@)g^gtU7G(wFDPyqEXAWAy2?3@qC`TEq2-s`E*(IJeJ&gQqQK8iaHJb#K^Y<`?}+t z>^(5Zv$NnyUzSyw8}j|S^x(a@)wkwqsrO8+rCWh|Ukfj(<$}CdPZpgLdz1T1`TE9? z-(Hh_eqQ@yWNv*4@Wc*!X8zAIPqATgOwP#)r^%rGz zniv!-XUB7YjQlqcsrDEdpQ?my%|`Ko+ZPs~%qQ_f}-i8+(<;@4>@a>tczpGJ8#W0WZYfZYA@a*Gb+7{eZvWpiTKDZu0)z8^P91YyL3s z!c1tHBY=LSWz{0?Qvq+A9^y?{1ShrZ^SYU%7{e>ec0cB5i}ocq{jA$hSkd1VY_j2} zu)4}E`Cg?`vs^yy*_bDwP0V({Ivw+r?^jvJl>6tO^L&saYw^=HE-yd1EVuM)@lur8 zEJrQ8&ue))cZI&{|E#+#9i;Z+OrUbi#>;^+nqLS;(ITpc$;Hljf-%rT;%=s|;ka^1 zMnKLZEA8bHL`9KW<-59$a4Z~7JQw}3mS}VHiinMrdTP&qEqzcuZ?%xsaa(kD>B*nw zZN=QEKlEz%wB#%s_9B?|5~+y){(;CY_1YudVtsk9eZ8^$)K<}UVyq>~otmzKTNm3u zRu9s+-a}gj@pD_o8(680VN)zTvM=aaS-bG`+G!ME^YoKtdyofyU4H+!%;Ze`V&9*c z8cDULPD$yCF6`Mr75BE4BUpVVG>7-FgGPUiW(4{`6k)4q`P zO2h{Q=*LEEvt$dpM63gH2*?WqC?24DmV8 zT*rRY*5z72q{+y7i6|}WWnxR5K?bEX<_)!wkNAGOg8pZTb&4J7wOA?YO&?3Vx4b^r z+~>Q}Gbp<*y(z6#sYievz`9xPn}MHn^&l@%Uf;4-7stPiZ~Dj_uV-{kYDZ+R`kwfk zQOYZ3kVV7_(XnMYV6y60r~HGoq8G>%@&YYB|CM|n-hG63VKmVACpHVKV!Dnttm%v< zo6=_@Svr%nZqZJQ)VN=`{%kQe#2V0^wE=ex$ zSMqImea+8QSsHn4fRvNK!zpF^Po^A@(>s;kHj+3$Dx>Tv`_EPHPk{8XGAQkDlQA>F z#?LKdVyrx!dFI(Lu{odNl9cJMbJt{)Nd|PMNtk?%e9^N}s`T6S2vwApqP#Yya)u(7 znq>@F$#+Md+!efxl_`-gjqdN3)QEdI?#^wOTfz%i@O^)+Y}4fPUC#Q+Yri7sJ1E$n z!?G!rt*cl=W_PH4Lbi?C=ssto?l+a}lP`kJc1tLx);&IGfA=;v%0Gie!df_*j&)i% zkf;gYH6n8o${fmd%{7Z2Rcgf;ht|D8A7d~cYIYxybp7%4(I_SRu(27DNy-CCFQeXO zuub8OAsKBxu|BKZPD!5B$F(S(rJ?;QQYMYFr!nd^v3>N+3ga74qg47V zK~ju)rX`1^fY~25em~n~Od$vAeaCXhls{GPQkH&RIpiD%|yr{JknxysjwZ{DlnoN_dOADW4gIO8SR+>W>tOqS}PK z4_%!h`kc2}i%wE&+J|zDX_;DWDK@E_pZW~eahJ#BBj5C?u2fr-@!M+Ln~KFN@_#vz z_}JePqFNk&`8a&EkMPKdy7td`=-XOYMSaU4pA)rtC|9rhig5Ou@oQP_uzbeFk$#`2 zp)$YhU-)u0AEO`f&nbfxN!I{)cT8Lbz+&M$+HPxZv9hxz%_m8%eB{#oLWCfdVR=tX z=lm+kRHnu9e0YU%>yqKap#qH#s~4^)ff5rhfz+TCJW!$Y(s%lxNbG`&9j2 z`UYh|J<^KV)Leqn=)6yBqpc%cvqSI5{E$b9w@Q2u-%z7{L;*Ru0jrQQj{=qKL@;C~ zC)^Mvq=$Kbcm_WFv3G}_m8Cv%gE)b%7~Kv%vQr3i9C;bg*PJ%`oZ5@!d_R6-b&W+s zxrXxGDF4D&?C@C+BR*Wf56pzWq^Bs680M9zA{@edrz~%k z*^AMi$ldUeq64hvcrYr0viu}ZyH}zS_ez>VrNE*SP2rm_laXk=DE6A{<5Xg*J&>Oh zu&f+Sk*90;X{+YH*VpmWI`XG;IjZ*5Sd=b`!{|Df^|8#azJE0ci6hXT`lmAF3Vosb+NY*3Tku%HkI($q?BZ%o4G{f2Wao!8C zo7$gPo1PA61+z%`?C)vTfX2{D*+A3|CN!sBIZrc??g{EG`m?rzwd!a4rnC`Hu-Kf> zL|Mv|>E?I+z6i37o#iE38Lz8^Q#%y|r006(r)8#bxN^^^nV%OTIgK)wl~L;nX^BuCiutC zST0i;-`KGY&o-^uD(aTtkd{-KL|fn!TYxjAnUlOfi`#f5&uR|eqPox*y3TxdBh)il zO16Kr4z{F!tTFFaWq_aSKMPfmka4C;HI?V7RMN*4eLMMQYUJ#nTFlbQ*CaFdYtNRh zFV=|Hg=%o1x4wKtJeOl$ieo{kR7GR1md%#e&j@2*t_hmU=tRz4lGqfv@|CDCY z$IhQ>$daBKq8_7+`R-=-?qpdRWq;h~OzOLg@RM|1hL~79$FV;P8j~R^gDngnf+O?V z^{l9zcaa4*q+Z>Zr0wW)kL0T6Cm<>MxMGx+N&M#pEtNB&((x>g8K>hLiW;XSYaWSN z>|e|H$R{gHmA>5$(x7bG3iL*&^eHtqBAnK@Tr5oO(=TPlG^f0j;q@Tw*oX-7oN`TM zZsd=&CydN&Mb+3odwtoMaauR_8CmT)*3I86PzZbkI}Z;@o)of__~rE!eXS{}P|sO}vrxEP%3H$TfH_Im#S-Lt6YL zuK6%JZz)9HRU32cI#8*O`>!DZe0^_%|NVSLZuhO8Ew2x)ouqjE`PPT>ew;;oFRxQ| z)!6u6{`B&@ZIC_tLGz>kf9?q0dGS}i6fcEy@a_iBDx&#BZdOEAvY$Rk`s{=&~?-D9Vv1;iLF6L?Eh;BODEU!1C zv-5R-wbN6?zCEoS{f<I^W_l(U(BJ^3!y*&w$;o*I?7k?Wtp7B*9_0M=v%o>BWr@e< za^5|fM_?_Nr9Y~=Z%alUD_-$!Kv4Qlk1m||uX!o#anF0861Fsg8Xc5MZ{qx8>&v4M zm*FIrNIAR5|M4ULM(DFE^>7lw+Zg-mr!4#N@%ij%K+Kvb_jhlN3LnY&VtRts@~2HN zO9!Iy*dKZ-KNbFR0FEI-4SSN<(WT&;X9)=J%I zEhP7ky_V@|Ki2P0ZT0#(KmEEdE6LJSse-gCY3<>40i~Mv31wJ*Rn6~ge&Z^ z8qR(47VD9#C!QixrfK?|X*mB@IPw&Jd33!w&z916Q*ah4V~Zz{>iNIhGPakZk>NvI z8)Mf1IWiXWAXfuT&y3&?4I$c}8cBP0?yRD^ojAPOiNld=$@27wO*6ubf8YKtyM8py zG(x&BPCWr+WBCarMvgCPGP~t%pStXk{|~{xou;jLuAzP0Da%G7uMvEEIlp4x_SeLz zB|jn9*W!E4sV={S6MCt2AS4k5k%<}b_eYGXoXVHSAM!VxJwQ`@lbY|T@wXdivEfFmq5KyiYy0BlF z6Ya~Wq~7wKi1l*a=^S~9mY@2pZA44JOO{&Q%bkLrw~$J}ej9#wYAywAW!CH;tQ#<` zd7#v6h$pASq7R#5Io_6Ry&?ZSt~`>HObid2Se||QMlyRt)Vh?2EgFxp3p?HSRF3u{ z<2}E|w~3Wo_4i-DWqKa7y&i<(y&%zrd$MZ)-{nX7M!poWp_`%Xcj5j8K?MHkx2Gpu z;)i@IHD8sy&g=8f)b>>BXRjc4FNHSAKjqssp$#6z6Y&R+vwQ3S+YYDS;^R>INS@se zpJIHN>zdQYC(DJknB9i-nvB^EcTUCTygX8nkwO(BZ@gFB&TzHya}RCwVooc{AdPMe zln3+3$U~uzc}LX8St?KxY9q0(4wmC&I^Nxh$MK|6(_VP*!lN)V!?iJgyrV(OezUM{ zmFnjeCAzn8<5YJwL`)qKob`;|6)|ztWJZ%2PP3$0rMqF@v(L$HGGyv9#9L*}-e~ql zv`Rrn++nzp__C-@eL`tn`DBU6C4` zwE{Bm=AJZc$iDPNKJxj-hJQbE^jwf4x_hrlk7*)AcY(5cvKRD;#?I<;B$huBsnRsf zlr+tde!&7c4Ec|oEvIXw#ETn7>QUMAqH*#QA~d?5U)2nteUTZeb66udY)BC*Oh@zN zm{<-eP1~GzZ8H`h8et{h4qel5(QvUXo-bBMYK7=2Myx1vx@faXny$$uOdm7E)SBfw zoKG+{%QcrPP2)^auCCB z>K<@hzl-aAwM=;46GFV6)xoyfH5XztVvEMrlGUm;^R}>Ey?>xIG2fiwEDSO0L=mzkBVjwoOgSmgoWMi zL(z^c4`WIeLJ0@WN}QIJSi(SVGq${&>39ue@X#!T=~xJH6`b?5Ie*&TW@qn==Y7uH z;eN}Rn4+YJ7>U^6EEYZcZ$olvI4g-Ww%RJ^H0{fX5KidjUQ_aGE)hfI{gp+_Z zb1t6GL!={rqM@dtMp5%ZP($9qUN~o^A!T}}R*#i0cFLDV#+pQ#+Ue#wPS7*Fu}bBL zp+ocix!!cl)O3u#wSluE%_iY33&)b#y&>2BEgFV*K-e4Qc$+bAdC>9=EmK3w6OkUe zlUXH*z9nH(R_E+`!^G4vp^-9nGHaYQMMul_n93S%ZhJ%-p|WEbCoNhwt!P<}4zo@1 zT;jc!K{}lk{zdYmhL9#XrX@MBx6~goUnRy@YdhRo*@H@(;eY?>X2nj;iXB2x|BV~v z(hW7!Kn>?a>ggD~)qVeTEr<2L{=K$8ZtDBv)RO5VBd!wn@6}M#teYuVH+?(|W2Nn@ zno=IpYVxG*yV7@5Pj$6;S(%dKrD>k&YMz)uza6rb^lk9^uDQO=+xBTqE5=`zEDhrTXJmo<(&2Qrrh6j&EoCH z=``0mee-2mFKq;Qa~NWMUi!52ai?|aSs5Lg#V{UC4K>q4jlGAP=p9J;K~@}p5dDOG z!`ne(US;#H&%bx=(ZI>-*fgwFlXs1bvHNWu@w~4iekU`J6tJ^2qp!?v9?ml%%X&Aw zG02x~e)3fOWIyX@5C7eyMSG+2RQ(%|hOy{q7RLFuForSFR*0vxLOhIxwz4-ZPZZ=o z3?~X4x_42cH=^BHF0;}7OLpBBb7_Ddz6(LMNW*8 zwKqeZ|2IRG5dnV6cbY#l^$giEE|y13^Ks6%kFz{Zn%@J>rV=$<9v?>%dBKnk^RNt&b$G`YdFrraLOqXIjj4Dwb$E@92w&l@uYGjre>XUu_6!2;r=8&Zw|m zSsqIb9}OSG-S@;NIhH*U9vf{J&2%DT>Aa`<#oyCiGcQ73YI`eE!v{#|FW{*rYgW2npMLX%DuiNs7rW8vb zhY??DLnO>@IEC-o+#7DYE~l02>d{b_E51}iNJ9vjs_BUU@jQt~;^`bnq~0*o@G^D0 z7!ug2iD#pGck|tNZ5{Xguj3wyTu>C`aT0LRau26%t>$tf&-Ac0;FfVX<><&Fl#I_t z{ZY<)It+OQyWKU3v9PP)LiZQZQhL?|xr>Kl=kPu^_U9ahbs=n@ixQ1FkZUjH|B-l3 zkLB)${67ruUNQgsLdS_xp!f&*r6y{ol@G!l${mI0pU5X|{jRHV)~j(Qtv>E%r#+l? z52Vb#)cQcKJ`A<(Ngwv5zMW9+AY7lx80g(Yk+H~MdQ+eiyDnKkBHW^va1Ok9#)@(!GI9yE@2z1T;+cRB0nN8S6~Ptx0l zlN@triIYzRE1>6H#)(#hav#oX+7q6#_qh(&#ycxx*~R$)N1^A3GJ__?_JsFr zO>`#wGwZ&Pws*Q)QbKpgvbqkJ+dF8K$A2Rwp#vxTAWe`%{ZcdG%9)gaZqMb@n#gSA z4t0Jne=i0s*}LV}x<5!H{ZM|PPu{OP^XF*UK5MM~hSYAm92U2TOr?E&cDPNX{c)fL zG(x@|%YSx$-jV;v)qUyBqA{A5(i4$}Tf8FF2S>N1e%_!C)fqAKk@unB@-i*a0`sD8 zx1%S?B+N{7<$)md>8WIzb$=3G{$A!C>AHD(AHDj=Q@K8q zGB<<1cq&&*QZ2Vyk$!6lFRaOx{|Z;J=&{)26*MBNuwTpH()G3O^V7b3?(~g~4@=j) ziAVBzUFsg9>spY`>)m_OpFc_6=uoSRv0|ei%m0+JpUIV>TB9EHb)>UZqMg>8&P%zj zRFbFikXpvfV>*{Z`lLUHe)MtnkazU#S(w$dOq@5`^>gQGq-Rg2`g3SJC4KAb_H&73 z#EWD;iGYEA<9tPbI`{O8(=cg&?Nozoh`$ADe9e@kUVJ(v=3%!be|O|ZDRMNgiVb9@OGdU2>VK;Ub!~3{EpPZTu>j+BOhPMf8_uqoqgJrdk1n)rP}|L zGM~u2Y{@S_?}=<@%-5vlujQB30kx$m-=|*WX-1?YSiL78fAzj>2YzBUnfoolz;2-K zBcZPH7zo6p%je^H7~@vpQso%^j0Mfy7>DZf?}s_**YQyNsPT0uy>l)H9ppT3Nvki# zUQq9g8D%Co(msF4z?u@kY__WX{3yi6CHGYOL!udg1x(sHn?pQjw?Q zTqqu+#(6BQm$Afirt=x`*k@9!V3jAwKJ!zB*4lILJ*7H#ETz2c9or|`j$DYnv`Q^Q zBP(g$u_KDn%UgopefhyRE00ZAp3Lezmhy*E{vU!_;+Dv;z6NIW{HWHWc48If`cuxa zC;5+dMlTt1o}^xw;g+@tK%g(vtKp5wF+ zP-0A!Z9VBrX;!Rt{;NM7$==DSA|Ixi*fH^<1x|hN1gKO9gLTZOs>AOMq)Ut3RAd>73?ioKkIgq`g@# zn{#cQMrm^lelD$e>6_p8Jy4$USA3tae~7_)$>3{N50$q~VA;G9i)!o&W{d!lae*Z^ z6Y8XK5{>ST_lVWU(7JRL$0%5fvZb>DE%?!AH+%ij+Qvq1Hl(KUk()Xuyd}CT5e(k$ zg1%o5K0#bHOs&&&mUKkKfz5U>`SqUQaWhyBc=OcDN(2#_JEya{eu`J45dX)nRC_$} zoX48gmyfINsn=LYkL2lcG@obcnBcCLHeNpNNHovZ z%TJ^n-elRjp#NL0|62)i&T8D?4Ae7blWh3CdbO|VqJQmWNL@a8Iv5v>rvaG_DTdN# z`~8FVHAL*ljMh-F?6^GE${uUl?-cLG>Cp>bmzj>ygLIT7nK-6>qj%bNjn}Mnz3rUy z{oK-b#~+UVuNZsrbSXrvHqW0p_hYp7|xE{ z6Tcb#{A|vbJF@bU$Le>M*1luVk^1Z7xVNlBYLc+L$_=H*Yn|ovt7#O~t+#@-u-U@? z@pLJzs%hWOa^000b*(PNj#?At^*Y6(ttWewc^RF{NiPXg?#Sh0+7n+t822x=oJuW9{=p$k9x=)B!}Tkd*)q(0U+ZL8N?J`0bPh<|+BYMw2ythMgvyk=*d ze|f=vj&#=EMVUdK`7_aI`6~arDK&0LzS2L%|Gg>_WOd2$bNTrx8oZ=@Q_{odI9c|g zu8y&qtI;W2?enWy%JC{$N?C}Y!>ky*PqK^HC6vYlG=s#qlIl!i2I`3)OJ8{UQLtCg zW9d_5P$HMfz{JiTA3>w1-saa#q*~moq#yDcQt75O-7ChEc(QEcaEG7eZ5?|u`nzhK zTo?L~ePuZW(EO8SY%N+BtJ`ryGTDzt zYk=PSf^p6DgpZ*3*gAA~AZXf_U%nCPA%?>&@R{1!Nt9cNz9;ygx?71XGBVhn&GXV~ z`E2!EG+(dxImtI9lY{=~4AuKoThpNK)j0CQfV5#6r2fL+gq}}CHom(&c{|J%IY`P! zL!%qVldOuf({v_!+Bh>_`{J!}C=t+{FC(v>M-KhCm}TmDX*`com;YHT@zKnhWpx%@ zdOz?C)`7Q?*f;ryQz=x!YO0xUNuTl^Zkwlh1>+(VX^q}GD7EkH3@9=_wm8NnBL_=& zb5j~A$X;@GgrJ|0Ju0cuNcGs0AtLp9eQ!5KgD{s5C5layowG@ZL|G(^)u(mgFHa4t zmH%UEz9;y)FZE;pr1jmw3OSFWKEk*>3i_kU}Y+n55tNf8nL`y$Y{%yyC-wQ z_=Z|7Yp+$t8admKq&7#x*eF}|wznA@oLD(_!V{T2axt+Th^wi*f7dMYOVVTVYk8Zh z?Hg83^4NxRK9*nX2C~$*!v8%%9`=20CyJrotoR36wCmTxv3pYIg8}O)wc{To5|14$ zi)%2u+|H|r`NM0%T}YwrFgGQM#Ry){ULR){{4?~!EP}DZHTGO0N!5- zpJ8Fab>!yX>8`J&O;c<2EbL!wZco{g4)H5R`P$H%wQEiP^8e@SMTsVw1#fE{L^*wo z6l>{Oh|Lj`RdiS$m#$_K%^|miJxD6MBZgEqms(9N?`6a35*hgi4OF4vD zNw1{eYy@VbV82+5`i9JB+*^^a!Wmt^6zX7q>OKKZ+wo7SwNZ~@WB*~xrkKSNOzTNo zj~*!FvpN>jntJ2z!W$yF*i)rv8I(9R&4>Lw*`IEdqX*vSq!-jVL>G-$uS@%SG8t6Y z8K8$Z0|l{3Sry9rpwE)st$VmVozzbDJ)?KEeM3VQGqoZO$>G#YLw2q(k9A9WYzDxC z;Hjh;cG%dfw3%A&Ydxx`II6`^)JA0Autzd;f$!MgeG66j$)E7C(R?3v&05p8$L;#H zc3;P))y%j)^7>XQ2rF5Gk^4N-kQ(pCzT>#}G(9;l<^KA9x@tSO1#6(RbXwb4 zw^iQNw934d)_2hrWaq*=zwA9phr(@^WsP5$>-cmn-+PW~2NLPj9WOk~?yO_^eki(N z+1PBZU9eSay3MS@l0?Klek{HCUot!REtlkXNA`y3j!5?Alr>|zI%71s6MSU+kD5jE zN=h)|+D^+=>s34Sn>SQDoQMs6DA5c(4JdjFW-F}P^COu#*7eVp&a4ihSI@Rq)#KXtBs!R8CX6jVbF_Xw52GG-}JUsm*&z>)K9`dp&8HjvAfz;eu$Ery(+A z{QB*viY_|IZoxqf_8qw}$f^EI-57N3qU z=JCC$xl65&v!A(K8)>AfG#a>SO->Eu48qGrUOfw}nnkqSNK=WuD*v0^v|QVqP`G&H zUSnDPe%IQMr@2$sd;VLl50+d$Q@I8$$=kTPM-8<+QCqd63_7q>Q(TSHno6tL=?Vh< zPp|lTEt7>E(s&*CB3M7#7rc>}yOR4)tj>T?waCVuUg)idMK+J}c6mmc&$J)%uFz+7 zPcM~PAg1pdq?Y4&FVZB$+16^jf3~$YpGk8@#`sJo_ZD&IS$QP-rjJ?3b@cMvODXy^ zw01q#vq179TT@Mux3sLiYMfyDWN3Xj`VM(7jPA>}ZXEOat*2Td{ibgh(oBsVu|CKe z);JpaffZWL5moKMyNZ}q@&tJ|kVe^d!@4PJmijKE%TkKB+{IWf->_@0%a49W7d@5w zIqR1zmqMH9B)(k}?Zn$b*gtTb-LvyJHAjxpNAm1;c<#A8A6GB9mtM(FUV*Yc#+s59K>)D>?sX&Y+DijT2L_+Ak?4b&)Z zJ`uEhD&yuSMUD3y8gkGbP2Q-y$@doJ$RRfzIfG`^%ygAn4Mhz_--@mphoYN7gY5(i znciy%Y6vPJ$o4j*8Vub|J^?H8M3R~mTIRiw)3Ab~E+azQw}P$*D_U3f9|@lD&FsuD zPD5xCsY#@gMB?qkil$3~mmP^C@cz<&g=}EtlD;zxpN=T@vBVVc*6KJjAC9~mqGi<3 zd^l(~Wgjj|uiu{Ht>pRVIhRKgowv6MHxx}BMVzXU{-k!)ZP9b&qLW9gvJ21dNz;8H zqTHea>d!5MB6cWiWWL!?&3=mZ(}ZZ)=$%64LKe;x%}CXH8(78Z!v!vtZnv1 z?sfksNFa>~pw)O=;PYUYH;brQM8E6FA<(36eEl-`V76yj-%Qe0V5jVd*$P_JU8x1F zkw{kO@%5YLYMLu*u1{s!V^{d#`BC%8$ZCXq}c}}m~46=L#prNUusf4D|s+np# zc2to|tLO7KOQ~5((NZGwI+aYR-*2n-Uy2msbU=L@aa((xPA=`f%y}snRx|iD9;CTx z?A&0tN9_rH^Rs5Mwfz0_uf=LO7)A;9&9-2UI3xRL^i0x2xq1{bA-7xB(7Cs!&l)*x zr_lNKzE;NzyKp(-7@M%ge9pU7NhQqAgA5jKxaRHs@R05Q)WN=S`NWpZp}`CvY<1Gx2+Tx#s0e zMSl2M2Oa)nq-)QWYDvS@+hi*n~hl$c4|?J znNA~Ss?ws2kGPVTmTI=bFca(y;Ac?TD)tzGGrV=nq@jQ z%QVMD^INCqw|ZM6#{=KJ@7i=o(;<3FZ+|_lX^-=+J$V1nfn*KxMl#}DWzQs@ib{mT zPPA-Z%GA7+94+XXh#%gWtXa#T2rbk0;!S7&Z;AztnJ+>|(=SugFDXj&ou_SI-E=Ha zFA+*IqIoC!p3dpLzb2IoW3bV*$NAPC`d+3(@i;(3+~;Ig=7#KdQhBi<9?XIKJeA!~ ztT;ZE>ur5)Dq6)*Fa{@_7}9@JT0_fp{FG(TV$y`O8yiL%MoOBemT%s2f2NW9Q4 zn1+~doo|~Wqh%IO(=v%D=u4HBGd8W9G1DfgZmFLE)nvvrWk$qFZLd>X?Vr+WzsEz{ z!7`;CEOF+^`iS59yAztOXu3ipUq6IZw)SR*^XiTA=1lgTAQ9N9!pU7H@}0F+Y{^}D z3nyn7H;Hh*B|?gW7c&1F-48PJBDe`ay<1=p4tu9FLZb1Nvm~MtaT=>Kkm4< zFMl6||0nW)Cas`ZUrJq@^R~6mTK6aN4p90-4z0caoKb%qt}><_dGgMD{7R3TLW6@q zkHdN2sUhumq#ivw=`Mval_&vKcDn0>EpeQ znS2`Rv{~fcEUcw&OP}`w?R6%;5B^B$Etfjl3*ArM{qV~u(Myc}FuXBd-_lIE_d}l^ z3$58n`>eYThS-2&l!wMwh$|%i(n-2Pk5%1OInK zYQRaya>6aVw~Dm5BwX+7n1$Lv=1fp^B!881vp}V7K`Ut7m2b1|m6V`uWC;H$|CxOI zL3*<%C6Fk{&HN?CeD zkF{5i3reHhk#VA7A_^YLNVmf^uKK!vBj2Hc_8uBNl5(nL6hm>2 zV$Wj@DRN5s=hN<^qW<|nyg%e^Sp(gWUOW$dITT6Dd})@9c?`-m%t+LvQ6r`N_DvWe z-1bsNdS{OLOVV;2q52_OtImVhS2hN{)7MwOZ~96o>$RlUZPU?LNH#1&wNo~P=RmD; z(A&^a_X7@_1#>gd{JVh23!*nZ3o)p>GBeOdS2eGQT)8ekM5nF0~cMB8_&@}Xg1iWu-~V{bm*F ({ @@ -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 }, @@ -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() } }, @@ -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() } }, @@ -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, diff --git a/backend/src/admin/admin.service.ts b/backend/src/admin/admin.service.ts index 6c08aa0d..46506a3f 100644 --- a/backend/src/admin/admin.service.ts +++ b/backend/src/admin/admin.service.ts @@ -32,7 +32,12 @@ 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); @@ -77,7 +82,10 @@ export class AdminService { return config; } - async updateConfig(dto: UpdateSystemConfigDto, adminId: string): Promise { + async updateConfig( + dto: UpdateSystemConfigDto, + adminId: string, + ): Promise { const updates = Object.entries(dto).filter(([, v]) => v !== undefined); for (const [key, value] of updates) { diff --git a/backend/src/admin/dto/system-config.dto.ts b/backend/src/admin/dto/system-config.dto.ts index 81aeba85..f1f5bb51 100644 --- a/backend/src/admin/dto/system-config.dto.ts +++ b/backend/src/admin/dto/system-config.dto.ts @@ -1,10 +1,4 @@ -import { - IsNumber, - IsBoolean, - IsOptional, - Min, - Max, -} from 'class-validator'; +import { IsNumber, IsBoolean, IsOptional, Min, Max } from 'class-validator'; export class UpdateSystemConfigDto { @IsOptional() diff --git a/backend/src/admin/system-config.service.spec.ts b/backend/src/admin/system-config.service.spec.ts index fafa121d..0a71bb57 100644 --- a/backend/src/admin/system-config.service.spec.ts +++ b/backend/src/admin/system-config.service.spec.ts @@ -3,6 +3,7 @@ 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'; @@ -36,6 +37,7 @@ describe('AdminService - system config', () => { 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() }, @@ -82,7 +84,10 @@ describe('AdminService - system config', () => { await service.updateConfig({ platform_fee_percent: 3 }, adminId); - expect(configRepo.save).toHaveBeenCalledWith({ key: 'platform_fee_percent', value: 3 }); + expect(configRepo.save).toHaveBeenCalledWith({ + key: 'platform_fee_percent', + value: 3, + }); expect(analyticsService.logActivity).toHaveBeenCalledWith( adminId, 'SYSTEM_CONFIG_UPDATED', @@ -97,7 +102,10 @@ describe('AdminService - system config', () => { await service.updateConfig({ maintenance_mode: true }, adminId); expect(configRepo.save).toHaveBeenCalledTimes(1); - expect(configRepo.save).toHaveBeenCalledWith({ key: 'maintenance_mode', value: true }); + expect(configRepo.save).toHaveBeenCalledWith({ + key: 'maintenance_mode', + value: true, + }); }); }); }); diff --git a/backend/test_out.txt b/backend/test_out.txt new file mode 100644 index 0000000000000000000000000000000000000000..e8bf651f1cbec20b7faace835fa6cf6c8d6f77f4 GIT binary patch literal 239778 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