Domain Specification: Customer Risk Rating¶
Validated against PRD v1.0 — All FR-RR-* requirements implemented here. See Traceability Matrix.
1. Context Overview¶
Bounded Context: riskrating
Responsibility: Calculate customer risk scores using configurable rules. Produce explainable risk bands.
Owns: RiskAssessment aggregate.
Depends On: Configuration Engine (thresholds, risk factors, weights).
Events Published: RiskAssessmentRequested, RiskAssessmentCompleted.
2. Scoring Model¶
2.1 Risk Factors (Configurable)¶
{
"methodologyVersion": "1.0.0",
"factors": [
{
"id": "GEOGRAPHY",
"name": "Geographic Risk",
"weight": 0.25,
"options": {
"LOW": { "score": 0, "countries": ["NLD","DEU","GBR","FRA","USA","CAN","AUS","JPN","CHE","SGP"] },
"MEDIUM": { "score": 30, "countries": ["BRA","IND","MEX","ZAF","TUR","ARE","MYS"] },
"HIGH": { "score": 60, "countries": ["IRN","PRK","SYR","VEN","MMR","default"] }
}
},
{
"id": "CUSTOMER_TYPE",
"name": "Customer Type Risk",
"weight": 0.15,
"options": {
"LOW": { "score": 0, "types": ["RETAIL_INDIVIDUAL"] },
"MEDIUM": { "score": 25, "types": ["SME","LEASING"] },
"HIGH": { "score": 50, "types": ["CORPORATE","PRIVATE_BANKING"] },
"CRITICAL": { "score": 80, "types": ["CORRESPONDENT_BANKING"] }
}
},
{
"id": "OWNERSHIP_COMPLEXITY",
"name": "Ownership Complexity",
"weight": 0.20,
"options": {
"LOW": { "score": 0, "condition": "ownershipLevels <= 1 && uboCount <= 2" },
"MEDIUM": { "score": 40, "condition": "ownershipLevels <= 3 && uboCount <= 5" },
"HIGH": { "score": 75, "condition": "ownershipLevels > 3 || uboCount > 5" }
}
},
{
"id": "PEP_EXPOSURE",
"name": "PEP Exposure",
"weight": 0.20,
"options": {
"LOW": { "score": 0, "condition": "pepFlag == false" },
"MEDIUM": { "score": 35, "condition": "pepLevel == 'NATIONAL'" },
"HIGH": { "score": 65, "condition": "pepLevel == 'INTERNATIONAL' || pepLevel == 'CLOSE_ASSOCIATE'" }
}
},
{
"id": "PRODUCT_RISK",
"name": "Product Risk",
"weight": 0.10,
"options": {
"LOW": { "score": 0, "products": ["SAVINGS","CURRENT_ACCOUNT"] },
"MEDIUM": { "score": 30, "products": ["TERM_DEPOSIT","MORTGAGE"] },
"HIGH": { "score": 60, "products": ["COMMERCIAL_LENDING","TRADE_FINANCE","CORRESPONDENT_BANKING"] }
}
},
{
"id": "INDUSTRY_RISK",
"name": "Industry Risk",
"weight": 0.10,
"options": {
"LOW": { "score": 0, "industries": ["TECHNOLOGY","HEALTHCARE","EDUCATION","RETAIL"] },
"MEDIUM": { "score": 30, "industries": ["MANUFACTURING","CONSTRUCTION","TRANSPORT"] },
"HIGH": { "score": 60, "industries": ["GAMBLING","CRYPTO","ARMS","PRECIOUS_METALS","default"] }
}
}
],
"bandThresholds": {
"LOW": { "maxScore": 29 },
"MEDIUM": { "minScore": 30, "maxScore": 59 },
"HIGH": { "minScore": 60 }
},
"routingRules": {
"LOW": { "action": "FAST_TRACK", "skipAnalystReview": true },
"MEDIUM": { "action": "STANDARD_REVIEW" },
"HIGH": { "action": "EDD_REQUIRED", "branchState": "EDD_REVIEW" }
}
}
2.2 Scoring Formula¶
TotalScore = Σ (factor.weight × factor.selectedOption.score) for each factor
Example: Corporate company in Brazil, 3 ownership levels, no PEP, lending product, construction industry
GEOGRAPHY: 0.25 × 30 = 7.5 (Brazil = MEDIUM)
CUSTOMER_TYPE: 0.15 × 50 = 7.5 (Corporate = HIGH)
OWNERSHIP: 0.20 × 40 = 8.0 (3 levels = MEDIUM)
PEP_EXPOSURE: 0.20 × 0 = 0.0 (no PEP = LOW)
PRODUCT_RISK: 0.10 × 60 = 6.0 (Commercial lending = HIGH)
INDUSTRY_RISK: 0.10 × 30 = 3.0 (Construction = MEDIUM)
-----
TotalScore: 32.0 -> MEDIUM band (30-59)
2.3 Versioning¶
Each RiskAssessment stores the methodologyVersion used. When the configuration engine promotes a new version, all NEW assessments use the new version. Existing assessments are not retroactively changed — a new assessment must be requested.
3. Data Model¶
data class RiskAssessment(
val assessmentId: UUID,
val customerId: UUID,
val workflowInstanceId: UUID,
val methodologyVersion: String, // "1.0.0"
val totalScore: Double, // 32.0
val riskBand: RiskBand, // LOW, MEDIUM, HIGH
val factorResults: List<FactorResult>,
val routingAction: RoutingAction, // FAST_TRACK, STANDARD_REVIEW, EDD_REQUIRED
val createdAt: Instant,
val validUntil: Instant? // null = valid until re-assessed
)
enum class RiskBand { LOW, MEDIUM, HIGH }
data class FactorResult(
val factorId: String, // "GEOGRAPHY"
val factorName: String, // "Geographic Risk"
val weight: Double, // 0.25
val selectedOption: String, // "MEDIUM"
val optionScore: Int, // 30
val weightedScore: Double, // 7.5
val rationale: String // "Customer resides in Brazil (MEDIUM risk jurisdiction)"
)
enum class RoutingAction { FAST_TRACK, STANDARD_REVIEW, EDD_REQUIRED }
4. API Contracts¶
4.1 Assess Customer Risk¶
POST /api/v1/risk-rating/assess
Authorization: System
Request:
{
"customerId": "uuid",
"workflowInstanceId": "uuid",
"customerContext": {
"customerType": "LEGAL_ENTITY",
"incorporationCountry": "BRA",
"residenceCountries": ["BRA"],
"nationalities": ["BRA"],
"pepFlag": false,
"pepLevel": null,
"ownershipLevels": 3,
"uboCount": 4,
"productInterest": "COMMERCIAL_LENDING",
"industryCode": "CONSTRUCTION"
}
}
Response 200:
{
"assessmentId": "uuid",
"totalScore": 32.0,
"riskBand": "MEDIUM",
"routingAction": "STANDARD_REVIEW",
"factorResults": [
{
"factorId": "GEOGRAPHY",
"factorName": "Geographic Risk",
"weight": 0.25,
"selectedOption": "MEDIUM",
"optionScore": 30,
"weightedScore": 7.5,
"rationale": "Customer incorporation country Brazil is rated MEDIUM risk jurisdiction"
}
// ... all 6 factors
],
"methodologyVersion": "1.0.0",
"createdAt": "2025-06-01T12:30:00Z"
}
4.2 Get Risk History¶
GET /api/v1/risk-rating/customers/{customerId}/history
Authorization: Bearer <jwt>
Response 200:
{
"customerId": "uuid",
"current": { /* RiskAssessment */ },
"history": [
{ "assessmentId": "uuid-1", "methodologyVersion": "0.9.0", "riskBand": "LOW", "createdAt": "2024-01-15" },
{ "assessmentId": "uuid-2", "methodologyVersion": "1.0.0", "riskBand": "MEDIUM", "createdAt": "2025-06-01" }
]
}
5. Decision Module Integration¶
Risk Rating implements the standard DecisionModule contract:
class RiskRatingEngine(
private val configEngine: ConfigurationEngine
) : DecisionModule<RiskRatingRequest, RiskAssessment> {
override fun execute(request: ModuleRequest<RiskRatingRequest>): ModuleResponse<RiskAssessment> {
val config = configEngine.getActiveConfig("RISK_RATING", request.configVersion)
val factors = config.getFactors()
val thresholds = config.getBandThresholds()
val factorResults = factors.map { factor ->
val option = factor.evaluate(request.payload.customerContext)
FactorResult(
factorId = factor.id,
factorName = factor.name,
weight = factor.weight,
selectedOption = option.label,
optionScore = option.score,
weightedScore = factor.weight * option.score,
rationale = option.rationale
)
}
val totalScore = factorResults.sumOf { it.weightedScore }
val riskBand = thresholds.determineBand(totalScore)
val routingAction = config.getRoutingRule(riskBand).action
val assessment = RiskAssessment(
assessmentId = UUID.randomUUID(),
customerId = request.payload.customerId,
methodologyVersion = config.version,
totalScore = totalScore,
riskBand = riskBand,
factorResults = factorResults,
routingAction = routingAction
)
return ModuleResponse(
requestId = request.requestId,
status = ModuleStatus.SUCCESS,
decision = assessment,
score = assessment.totalScore,
rationale = "Risk score $totalScore -> $riskBand band. Factors: ${factorResults.map { "${it.factorName}=${it.selectedOption}" }.joinToString()}",
executionMetadata = ExecutionMetadata(/* ... */)
)
}
}
6. Error Handling¶
| Scenario | Behavior |
|---|---|
| Missing required customer context field | 400 Bad Request. "Required context field 'incorporationCountry' is missing." |
| Unknown country in geography factor | Falls to "default" option (HIGH). Rationale: "Country XX not classified — rated as default HIGH risk." |
| Config version not found | 500 Internal Server Error. "Configuration version {version} not found. Contact administrator." |
| Methodology version mismatch | Assessment uses the requested version. If config engine promotes new version during assessment, in-flight assessment completes with old version. |
| All risk factors disabled (empty config) | Returns score 0, risk band LOW. Logs warning. |
Spec validated against PRD v1.0 requirements FR-RR-01 through FR-RR-03.