Skip to content
Merged
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
26 changes: 26 additions & 0 deletions src/modules/loans/dto/loan-stats-response.dto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { ApiProperty } from '@nestjs/swagger';

export class LoanStatsResponseDto {
@ApiProperty({ description: 'Total number of loans across all statuses', example: 120 })
totalLoans: number;

@ApiProperty({ description: 'Number of currently active loans', example: 45 })
activeLoans: number;

@ApiProperty({ description: 'Number of fully repaid loans', example: 70 })
repaidLoans: number;

@ApiProperty({ description: 'Number of defaulted loans', example: 5 })
defaultedLoans: number;

@ApiProperty({ description: 'Total loan volume disbursed in USD', example: 98500.0 })
totalVolume: number;

@ApiProperty({
description: 'Percentage of loans repaid on time (0-100)',
example: 93.5,
minimum: 0,
maximum: 100,
})
onTimeRepaymentRate: number;
}
17 changes: 17 additions & 0 deletions src/modules/loans/loans.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import { LoanPaymentResponseDto } from './dto/loan-payment-response.dto';
import { AvailableCreditResponseDto } from './dto/available-credit-response.dto';
import { LoanListQueryDto, LoanListStatusFilter } from './dto/loan-list-query.dto';
import { LoanListResponseDto } from './dto/loan-list-response.dto';
import { LoanStatsResponseDto } from './dto/loan-stats-response.dto';
import { JwtAuthGuard } from '../../common/guards/jwt-auth.guard';
import { CurrentUser } from '../../common/decorators/current-user.decorator';

Expand Down Expand Up @@ -61,6 +62,22 @@ export class LoansController {
return { success: true, data, message: 'Loan quote calculated successfully' };
}

@Get('stats')
@ApiOperation({
summary: 'Get protocol-wide loan statistics',
description:
'Returns aggregated loan counts and volume across all users. No authentication required.',
})
@ApiResponse({
status: 200,
description: 'Loan statistics retrieved successfully',
type: LoanStatsResponseDto,
})
async getStats() {
const data = await this.loansService.getStats();
return { success: true, data, message: 'Loan statistics retrieved successfully' };
}

@Get('my-loans')
@UseGuards(JwtAuthGuard)
@ApiBearerAuth()
Expand Down
39 changes: 39 additions & 0 deletions src/modules/loans/loans.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import {
LoanListVendorDto,
LoanListResponseDto,
} from './dto/loan-list-response.dto';
import { LoanStatsResponseDto } from './dto/loan-stats-response.dto';
import { ReputationTier } from '../reputation/dto/reputation-response.dto';

const GUARANTEE_PERCENT = 0.2;
Expand Down Expand Up @@ -326,6 +327,44 @@ export class LoansService {
};
}

async getStats(): Promise<LoanStatsResponseDto> {
const client = this.supabaseService.getServiceRoleClient();
const { data, error } = await client
.from('loans')
.select('status, loan_amount, completed_at, next_payment_due');

if (error) {
this.logger.error(`Failed to fetch loan stats: ${error.message}`);
throw new InternalServerErrorException({
code: 'LOAN_STATS_QUERY_FAILED',
message: 'Failed to retrieve loan statistics. Please try again later.',
});
}

const loans = data ?? [];
const totalLoans = loans.length;
const activeLoans = loans.filter((l) => l.status === 'active').length;
const repaidLoans = loans.filter((l) => l.status === 'completed').length;
const defaultedLoans = loans.filter((l) => l.status === 'defaulted').length;
const totalVolume = this.roundCurrency(
loans.reduce((sum, l) => sum + Number(l.loan_amount ?? 0), 0),
);

const repaidOnTime = loans.filter(
(l) =>
l.status === 'completed' &&
l.completed_at != null &&
l.next_payment_due != null &&
new Date(l.completed_at) <= new Date(l.next_payment_due),
).length;
const onTimeRepaymentRate =
repaidLoans > 0
? Math.round((repaidOnTime / repaidLoans) * 10_000) / 100
: 0;

return { totalLoans, activeLoans, repaidLoans, defaultedLoans, totalVolume, onTimeRepaymentRate };
}

private async prepareLoanPreview(
wallet: string,
dto: LoanQuoteRequestDto,
Expand Down
Loading