forked from StepFi-app/StepFi-API
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathcredit-scoring.service.ts
More file actions
100 lines (88 loc) · 3.29 KB
/
Copy pathcredit-scoring.service.ts
File metadata and controls
100 lines (88 loc) · 3.29 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
import { Injectable } from '@nestjs/common';
import { CreditAssessmentResultDto } from './dto/credit-scoring-response.dto';
export interface AssessParams {
amount: number;
reputationScore: number;
maxCredit: number;
creditUtilization: number;
}
export interface CreditTier {
minScore: number;
tier: 'gold' | 'silver' | 'bronze' | 'starter';
interestRate: number;
maxCredit: number;
}
/**
* Authoritative credit tier definitions matching the parameters-contract
* deployed on Stellar testnet. The contracts enforce these exact values —
* the API must match, not the reverse.
*
* Source: context/architecture-context.md → StepFi-Contracts → Reputation → Credit Tiers
*/
export const CREDIT_TIERS: readonly CreditTier[] = [
{ minScore: 90, tier: 'gold', interestRate: 4, maxCredit: 10_000 },
{ minScore: 75, tier: 'silver', interestRate: 6, maxCredit: 5_000 },
{ minScore: 60, tier: 'bronze', interestRate: 8, maxCredit: 2_500 },
{ minScore: 0, tier: 'starter', interestRate: 10, maxCredit: 1_000 },
] as const;
@Injectable()
export class CreditScoringService {
/**
* Resolves a reputation score to its canonical credit tier, interest rate,
* and credit limit. This is the single source of truth for tier resolution
* across the entire API.
*/
resolveTier(score: number): { tier: CreditTier['tier']; interestRate: number; maxCredit: number } {
const normalizedScore = Math.max(0, Math.min(100, score));
for (const tier of CREDIT_TIERS) {
if (normalizedScore >= tier.minScore) {
return { tier: tier.tier, interestRate: tier.interestRate, maxCredit: tier.maxCredit };
}
}
// Should never reach here, but return starter as the lowest tier
return { tier: 'starter', interestRate: 10, maxCredit: 1_000 };
}
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 };
}
}