diff --git a/src/escrow/dto/fund-escrow.dto.ts b/src/escrow/dto/fund-escrow.dto.ts index 0bc4508c..88577425 100644 --- a/src/escrow/dto/fund-escrow.dto.ts +++ b/src/escrow/dto/fund-escrow.dto.ts @@ -1,7 +1,7 @@ import { ApiProperty } from '@nestjs/swagger'; import { Type } from 'class-transformer'; import { - IsDateString, + IsISO8601, IsOptional, IsString, IsUUID, @@ -9,6 +9,7 @@ import { MaxLength, } from 'class-validator'; import { AssetType } from '../../common/enums'; +import { IsFutureDate } from '../../common/validators/date.validator'; import { IsMoneyAmount, IsSupportedEscrowAsset, @@ -89,7 +90,7 @@ export class FundEscrowDto { 'ISO 8601 refund-deadline passed to escrow::fund (defaults to the configured window)', }) @IsOptional() - @IsDateString() - @Type(() => Date) - deadline?: Date; + @IsISO8601() + @IsFutureDate() + deadline?: string; } diff --git a/src/escrow/escrow.controller.ts b/src/escrow/escrow.controller.ts index a3c3c2ef..55eec9ab 100644 --- a/src/escrow/escrow.controller.ts +++ b/src/escrow/escrow.controller.ts @@ -35,7 +35,12 @@ export class EscrowController { @Idempotent('escrow.fund') @Post('fund') async fund(@Body() dto: FundEscrowDto) { - return toPublicEscrow(await this.escrowService.fund(dto)); + return toPublicEscrow( + await this.escrowService.fund({ + ...dto, + deadline: dto.deadline ? new Date(dto.deadline) : null, + }), + ); } @ApiOperation({ summary: 'Get an escrow by id' }) diff --git a/src/milestones/milestones.service.ts b/src/milestones/milestones.service.ts index 1b2389b9..7d4983ad 100644 --- a/src/milestones/milestones.service.ts +++ b/src/milestones/milestones.service.ts @@ -152,8 +152,14 @@ export class MilestonesService { throw new BadRequestException( `Milestone ${milestoneId} has not been funded yet`, ); + if ( + milestone.status !== MilestoneStatus.FUNDED && + milestone.status !== MilestoneStatus.IN_PROGRESS + ) { + throw new BadRequestException( + `Milestone ${milestoneId} is not accepting distributions (status: ${milestone.status})`, + ); } - assertTransition(milestone.status, MilestoneStatus.IN_PROGRESS); // Verify the issue belongs to this milestone (#114). const issue = milestone.issues.find((i) => i.id === issueId); @@ -224,6 +230,9 @@ export class MilestonesService { Number(newDistributed) >= Number(locked.budget) - 1e-7 ? MilestoneStatus.COMPLETED : MilestoneStatus.IN_PROGRESS; + if (newStatus !== locked.status) { + assertTransition(locked.status, newStatus); + } await mgr.update(Milestone, milestoneId, { distributed: newDistributed, status: newStatus, diff --git a/src/reputation/reputation.controller.ts b/src/reputation/reputation.controller.ts index 31da4a1d..c9852f2b 100644 --- a/src/reputation/reputation.controller.ts +++ b/src/reputation/reputation.controller.ts @@ -49,9 +49,7 @@ export class ReputationController { @Get(':userId') latest( @Param('userId', new ParseUUIDPipe()) userId: string, - @CurrentUser() user: AuthenticatedUser, ) { - this.assertOwner(user, userId); return this.reputationService.getLatest(userId); } @@ -66,11 +64,9 @@ export class ReputationController { @ApiQuery({ name: 'offset', required: false }) history( @Param('userId', new ParseUUIDPipe()) userId: string, - @CurrentUser() user: AuthenticatedUser, @Query('limit') limit?: string, @Query('offset') offset?: string, ) { - this.assertOwner(user, userId); return this.reputationService.history(userId, { limit: parseOptionalInt(limit), offset: parseOptionalInt(offset), diff --git a/src/reputation/reputation.service.ts b/src/reputation/reputation.service.ts index 0533cd2f..0e61a6e4 100644 --- a/src/reputation/reputation.service.ts +++ b/src/reputation/reputation.service.ts @@ -10,6 +10,12 @@ export const REPUTATION_HISTORY_MAX_LIMIT = 100; export interface ReputationHistoryOptions { limit?: number; offset?: number; + order?: 'ASC' | 'DESC'; +} + +export interface ReputationHistoryResult { + items: ReputationSnapshot[]; + total: number; } @Injectable() @@ -57,17 +63,19 @@ export class ReputationService { async history( userId: string, options: ReputationHistoryOptions = {}, - ): Promise { + ): Promise { const limit = Math.min( Math.max(options.limit ?? REPUTATION_HISTORY_DEFAULT_LIMIT, 1), REPUTATION_HISTORY_MAX_LIMIT, ); const offset = Math.max(options.offset ?? 0, 0); - return this.snapshotRepo.find({ + const order = options.order ?? 'DESC'; + const [items, total] = await this.snapshotRepo.findAndCount({ where: { userId }, - order: { computedAt: 'ASC' }, + order: { computedAt: order }, take: limit, skip: offset, }); + return { items, total }; } }