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
4 changes: 3 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,9 @@
"rootDir": "test",
"testRegex": ".*\\.spec\\.ts$",
"transform": {
"^.+\\.(t|j)s$": "ts-jest"
"^.+\\.ts$": ["ts-jest", {
"diagnostics": false
}]
},
"collectCoverageFrom": [
"../src/**/*.(t|j)s"
Expand Down
2 changes: 2 additions & 0 deletions src/app.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import { NonceCleanupModule } from './jobs/nonce-cleanup/nonce-cleanup.module';
import { StellarModule } from './stellar/stellar.module';
import { LoggerModule } from './common/logger/logger.module';
import { MetricsModule } from './modules/metrics/metrics.module';
import { CreditScoringModule } from './modules/credit-scoring/credit-scoring.module';
import { CorrelationIdMiddleware } from './common/logger/correlation-id.middleware';

@Module({
Expand Down Expand Up @@ -60,6 +61,7 @@ import { CorrelationIdMiddleware } from './common/logger/correlation-id.middlewa
LoanPaymentReminderModule,
TransactionStatusCheckerModule,
NonceCleanupModule,
CreditScoringModule,
StellarModule,
],
controllers: [],
Expand Down
8 changes: 8 additions & 0 deletions src/modules/credit-scoring/credit-scoring.module.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { Module } from '@nestjs/common';
import { CreditScoringService } from './credit-scoring.service';

@Module({
providers: [CreditScoringService],
exports: [CreditScoringService],
})
export class CreditScoringModule {}
61 changes: 61 additions & 0 deletions src/modules/credit-scoring/credit-scoring.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import { Injectable } from '@nestjs/common';
import { CreditAssessmentResultDto } from './dto/credit-scoring-response.dto';

export interface AssessParams {
amount: number;
reputationScore: number;
maxCredit: number;
creditUtilization: number;
}

@Injectable()
export class CreditScoringService {
assess(params: AssessParams): CreditAssessmentResultDto {
const { amount, reputationScore, maxCredit, creditUtilization } = params;
const reasons: string[] = [];

if (reputationScore < 60) {
reasons.push(
`Reputation score ${reputationScore} is below the minimum threshold of 60`,
);
return { decision: 'rejected', score: reputationScore, reasons };
}

if (amount > maxCredit) {
reasons.push(
`Loan amount $${amount} exceeds maximum credit limit of $${maxCredit}`,
);
return { decision: 'rejected', score: reputationScore, reasons };
}

if (
reputationScore >= 75 &&
amount <= maxCredit * 0.8 &&
creditUtilization < 0.7
) {
reasons.push(
`Strong reputation score of ${reputationScore} with sufficient available credit`,
);
return { decision: 'approved', score: reputationScore, reasons };
}

if (reputationScore >= 75) {
if (amount > maxCredit * 0.8) {
reasons.push(
`Loan amount ($${amount}) exceeds 80% of credit limit ($${maxCredit})`,
);
}
if (creditUtilization >= 0.7) {
reasons.push(
`Credit utilization at ${Math.round(creditUtilization * 100)}% exceeds 70% threshold`,
);
}
return { decision: 'manual_review', score: reputationScore, reasons };
}

reasons.push(
`Bronze tier reputation score (${reputationScore}) requires manual review`,
);
return { decision: 'manual_review', score: reputationScore, reasons };
}
}
23 changes: 23 additions & 0 deletions src/modules/credit-scoring/dto/credit-scoring-response.dto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { ApiProperty } from '@nestjs/swagger';

export type AssessmentDecision = 'approved' | 'rejected' | 'manual_review';

export class CreditAssessmentResultDto {
@ApiProperty({
description: 'Assessment decision',
enum: ['approved', 'rejected', 'manual_review'],
})
decision: AssessmentDecision;

@ApiProperty({
description: 'Reputation score used for assessment',
example: 75,
})
score: number;

@ApiProperty({
description: 'Reasons for the assessment decision',
example: ['Strong reputation score of 75 with sufficient available credit'],
})
reasons: string[];
}
11 changes: 10 additions & 1 deletion src/modules/loans/dto/create-loan-response.dto.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { ApiProperty } from '@nestjs/swagger';
import { LoanQuoteResponseDto } from './loan-quote-response.dto';
import { CreditAssessmentResultDto } from '../../credit-scoring/dto/credit-scoring-response.dto';

export class CreateLoanResponseDto {
@ApiProperty({
Expand All @@ -11,8 +12,9 @@ export class CreateLoanResponseDto {
@ApiProperty({
description: 'Unsigned Soroban XDR transaction to be signed by the user',
example: 'AAAAAgAAAAC...',
nullable: true,
})
xdr: string;
xdr: string | null;

@ApiProperty({
description: 'Human-readable transaction description',
Expand All @@ -25,4 +27,11 @@ export class CreateLoanResponseDto {
type: LoanQuoteResponseDto,
})
terms: LoanQuoteResponseDto;

@ApiProperty({
description: 'Credit assessment result',
type: CreditAssessmentResultDto,
nullable: true,
})
assessment: CreditAssessmentResultDto | null;
}
4 changes: 3 additions & 1 deletion src/modules/loans/dto/loan-list-query.dto.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ export enum LoanListStatusFilter {
ACTIVE = 'active',
COMPLETED = 'completed',
DEFAULTED = 'defaulted',
UNDER_REVIEW = 'under_review',
REJECTED = 'rejected',
}

export class LoanListQueryDto {
Expand All @@ -16,7 +18,7 @@ export class LoanListQueryDto {
})
@IsOptional()
@IsEnum(LoanListStatusFilter, {
message: 'status must be one of: active, completed, defaulted',
message: 'status must be one of: active, completed, defaulted, under_review, rejected',
})
status?: LoanListStatusFilter;

Expand Down
29 changes: 29 additions & 0 deletions src/modules/loans/loans.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -185,4 +185,33 @@ export class LoansController {
const data = await this.loansService.repayLoan(user.wallet, loanId, dto);
return { success: true, data, message: 'Repayment transaction constructed successfully' };
}

@Post(':loanId/assess')
@HttpCode(HttpStatus.OK)
@UseGuards(JwtAuthGuard)
@ApiBearerAuth()
@ApiParam({
name: 'loanId',
description: 'UUID of the loan to assess',
example: 'a1b2c3d4-e5f6-7890-abcd-ef1234567890',
})
@ApiOperation({
summary: 'Run credit assessment on a loan',
description:
'Runs the credit scoring pipeline on a pending or under_review loan and updates its status based on the assessment result. Auto-approved loans stay pending, auto-rejected loans are marked rejected, and edge cases are flagged for manual review.',
})
@ApiResponse({
status: 200,
description: 'Loan assessed successfully',
})
@ApiResponse({ status: 400, description: 'Loan cannot be assessed in its current status' })
@ApiResponse({ status: 401, description: 'Unauthorized - missing or invalid JWT' })
@ApiResponse({ status: 404, description: 'Loan not found or does not belong to user' })
async assessLoan(
@CurrentUser() user: { wallet: string },
@Param('loanId', ParseUUIDPipe) loanId: string,
) {
const data = await this.loansService.assessLoan(user.wallet, loanId);
return { success: true, data, message: 'Loan assessment completed successfully' };
}
}
3 changes: 2 additions & 1 deletion src/modules/loans/loans.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,10 @@ import { AuthModule } from '../auth/auth.module';
import { ReputationModule } from '../reputation/reputation.module';
import { SupabaseService } from '../../database/supabase.client';
import { StellarModule } from '../../stellar/stellar.module';
import { CreditScoringModule } from '../credit-scoring/credit-scoring.module';

@Module({
imports: [ConfigModule, AuthModule, ReputationModule, StellarModule],
imports: [ConfigModule, AuthModule, ReputationModule, StellarModule, CreditScoringModule],
controllers: [LoansController],
providers: [
LoansService,
Expand Down
Loading
Loading