Comprehensive Loan Module Analysis - FastAPI Backend
Document Purpose: Complete reference for writing comprehensive pytest tests for the loan module
Generated: March 31, 2026
Table of Contents
- Route Paths & HTTP Methods
- Pydantic Schema Classes
- SQLAlchemy Models
- Controller Functions
- Key Operations Summary
ROUTE PATHS & HTTP METHODS
Loan Products (/product/)
| Method |
Endpoint |
Status |
Response Model |
Request Payload |
Purpose |
| POST |
/product/add |
201 |
LoanProductFetchResponse |
LoanProductCreate |
Create new loan product |
| POST |
/product/get |
200 |
LoanProductGetResponse |
LoanProductID |
Get product details by ID |
| POST |
/product/fetch |
200 |
LoanProductFetchAllResponse |
FetchFilters |
List products with pagination & filters |
| POST |
/product/edit |
201 |
LoanProductGetResponse |
LoanProductCreate |
Update loan product |
| POST |
/product/lock |
200 |
LoanProductFetchResponse |
LoanProductID |
Toggle product active/inactive status |
LoanProductCreate Fields:
product_id: Optional[str]
name: str (max 100, required)
code: str (max 20, required)
description: Optional[str]
interest_type: InterestType (flat, reducing)
interest_rate: Decimal (0-100)
currency: str (default "GHS", max 3)
min_amount: Decimal (gt 0, required)
max_amount: Decimal (gt 0, required)
tenure_days: int (gt 0, required)
installment_type: InstallmentType (none, daily, weekly, monthly)
grace_period_days: int (default 0, ge 0)
penalty_rate: Decimal (default 0.0, le 100)
max_dti_ratio: Optional[Decimal] (0-1)
requires_guarantors: bool (default False)
requires_collateral: bool (default False)
requires_manual_approval: bool (default False)
requires_manual_disbursement: bool (default True)
requires_score_check: bool (default True)
requires_external_score: bool (default False)
min_required_score: Decimal (default 500.0, 0-1000)
min_required_external_score: Decimal (default 550.0, 300-850)
preferred_score_model: str (default "internal", max 20)
risk_rules: Optional[List[RiskRuleCreate]]
scoring_config: Optional[List[ProductScoringConfigCreate]]
RiskRuleCreate Fields:
min_credit_score: Optional[Decimal] (0-1000)
max_exposure: Optional[Decimal] (ge 0)
min_income: Optional[Decimal] (ge 0)
max_dti_ratio: Optional[Decimal] (0-1)
max_active_loans: Optional[int] (ge 0)
is_enabled: bool (default True)
ProductScoringConfigCreate Fields:
repayment_history: Optional[Decimal] (0-1000)
exposure: Optional[Decimal] (0-1000)
income_stability: Optional[Decimal] (0-1000)
behavioral: Optional[Decimal] (0-1000)
Loan Applications (/application/)
| Method |
Endpoint |
Status |
Response Model |
Request Payload |
Purpose |
| POST |
/application/add |
201 |
LoanApplicationResponse |
LoanApplicationCreate |
Submit new loan application (async) |
| POST |
/application/get |
200 |
LoanApplicationResponse |
LoanApplicationID |
Get application details (async) |
| POST |
/application/fetch |
200 |
LoanApplicationFetchResponse |
FetchFilters |
List applications (paginated, filtered) |
| POST |
/application/approve |
200 |
LoanApplicationResponse |
LoanApplicationApprove |
Approve application |
| POST |
/application/reject |
200 |
LoanApplicationResponse |
LoanApplicationReject |
Reject application with reason |
LoanApplicationCreate Fields:
applicant_id: int (required)
loan_product_id: int (required)
requested_amount: Decimal (gt 0, required)
purpose: LoanPurpose (required)
borrower_income: Optional[Decimal] (gt 0)
device_info: Optional[DeviceInfo]
ip_address: Optional[str]
user_agent: Optional[str]
geolocation: Optional[Geolocation]
location: Optional[Location]
DeviceInfo Fields:
ip_address: Optional[str] (valid IP format)
user_agent: Optional[str] (max 2000 chars)
device_id: Optional[str]
- At least one field required
Geolocation Fields:
country: Optional[str] (ISO 2-3 char code)
city: Optional[str]
latitude: Optional[float] (-90 to 90)
longitude: Optional[float] (-180 to 180)
- Requires country OR both lat/lon
Location Fields:
address: Optional[str]
city: Optional[str]
region: Optional[str]
postal_code: Optional[str]
latitude: Optional[float] (-90 to 90)
longitude: Optional[float] (-180 to 180)
- Requires address OR both lat/lon
LoanApplicationApprove Fields:
LoanApplicationReject Fields:
application_id: str
reason: str (required)
LoanApplicationStatus Enum:
- pending
- under_review
- approved
- rejected
- conditionally_approved
- cancelled
- expired
- blocked
LoanPurpose Enum:
- debt_consolidation
- business
- education
- medical
- home_renovation
- car
- vacation
- emergency
- mortgage
- other
Loans (/loan/)
| Method |
Endpoint |
Status |
Response Model |
Request Payload |
Purpose |
| POST |
/loan/add |
201 |
LoanResponse |
LoanCreate |
Create new loan |
| POST |
/loan/get |
200 |
LoanResponse |
LoanID |
Get loan details (async) |
| POST |
/loan/fetch |
200 |
LoanFetchResponse |
FetchFilters |
List loans (paginated, filtered) |
| POST |
/loan/disburse |
200 |
LoanResponse |
LoanID |
Disburse loan to borrower (async) |
| POST |
/loan/mark-overdue |
200 |
LoanResponse |
LoanID |
Mark loan as overdue |
| POST |
/loan/write-off |
200 |
LoanResponse |
LoanID |
Write off defaulted loan |
| POST |
/status/update |
200 |
LoanResponse |
LoanStatusUpdate |
Update loan status (manual override) |
LoanCreate Fields:
borrower_id: int (required)
loan_product_id: int (required)
application_id: Optional[int]
principal_amount: Decimal (gt 0, required)
channel: Optional[str] (max 50)
LoanID Fields:
LoanStatusUpdate Fields:
loan_id: int
status: LoanStatus
reason: Optional[str]
LoanStatus Enum:
- pending
- approved
- disbursed
- active
- overdue
- repaid
- defaulted
- rejected
- cancelled
- written_off (custom)
Valid Status Transitions:
- pending → approved, rejected
- approved → rejected, cancelled
- active → defaulted, repaid
- defaulted → written_off
- disbursed → active, defaulted
Loan Repayment (/repayment/)
| Method |
Endpoint |
Status |
Response Model |
Request Payload |
Purpose |
| POST |
/repayment/add |
200 |
List[RepaymentResponse] |
RepaymentCreate |
Record loan repayment (async) |
| POST |
/repayment/get |
200 |
RepaymentResponse |
RepaymentID |
Get repayment details |
| POST |
/repayment/list |
200 |
List[RepaymentResponse] |
LoanID |
List all repayments for loan |
| POST |
/repayment/reverse |
200 |
RepaymentResponse |
RepaymentReverse |
Reverse a repayment (async) |
RepaymentCreate Fields:
loan_id: int (required)
amount: Decimal (gt 0, required)
method: RepaymentMethod (default "cbs")
reference: Optional[str] (max 50)
RepaymentMethod Enum:
- wallet
- cash
- commission
- cbs
- manual
RepaymentID Fields:
RepaymentReverse Fields:
repayment_id: str (required)
Loan Guarantor (/guarantor/)
| Method |
Endpoint |
Status |
Response Model |
Request Payload |
Purpose |
| POST |
/guarantor/add |
201 |
GuarantorResponse |
ApplicationGuarantorCreate |
Add guarantor to application |
| POST |
/guarantor/accept |
200 |
GuarantorResponse |
GuarantorID |
Accept/validate guarantor |
| POST |
/guarantor/reject |
200 |
GuarantorResponse |
GuarantorReject |
Reject guarantor |
| POST |
/guarantor/list |
200 |
List[GuarantorResponse] |
LoanID |
List all guarantors for loan |
ApplicationGuarantorCreate Fields:
application_id: int
guarantor_id: int
guarantee_amount: Optional[Decimal] (gt 0)
relation_type: Optional[str] (max 50)
GuarantorID Fields:
GuarantorReject Fields:
guarantor_id: str
reason: Optional[str]
GuaranteeStatus Enum:
- pending
- validated
- accepted
- rejected
Loan Collateral (/collateral/)
| Method |
Endpoint |
Status |
Response Model |
Request Payload |
Purpose |
| POST |
/collateral/add |
201 |
ApplicationCollateralResponse |
ApplicationCollateralCreate |
Add collateral to application (async) |
| POST |
/collateral/accept |
200 |
ApplicationCollateralResponse |
CollateralUpdate |
Accept collateral |
| POST |
/collateral/reject |
200 |
ApplicationCollateralResponse |
ApplicationCollateralReject |
Reject collateral |
| POST |
/collateral/get |
200 |
CollateralResponse |
LoanApplicationID |
List collateral for application |
ApplicationCollateralCreate Fields:
application_id: int
type: CollateralType (required)
description: Optional[str]
value: Optional[Decimal] (gt 0)
file: Optional[BaseUploadRequest]
CollateralType Enum:
- vehicle
- real_estate
- cash_deposit
- guarantor
- equipment
- other
ApplicationCollateralReject Fields:
collateral_id: str
reject_reason: Optional[str]
Loan Blacklist (/blacklist/)
| Method |
Endpoint |
Status |
Response Model |
Request Payload |
Purpose |
| POST |
/blacklist/add |
201 |
BlacklistResponse |
BlacklistCreate |
Add user to blacklist |
| POST |
/blacklist/edit |
201 |
BlacklistResponse |
BlacklistUpdate |
Edit blacklist entry |
| POST |
/blacklist/fetch |
200 |
LoanBlacklistFetchResponse |
FetchFilters |
List blacklist entries |
| POST |
/blacklist/get |
200 |
BlacklistResponse |
LoanBlacklistID |
Get blacklist entry |
| POST |
/blacklist/remove |
200 |
BlacklistResponse |
LoanBlacklistID |
Remove from blacklist |
| POST |
/blacklist/reactivate |
200 |
BlacklistResponse |
LoanBlacklistID |
Reactivate blacklist entry |
| POST |
/blacklist/user |
200 |
BlacklistResponse |
BlackListUserID |
Check if user is blacklisted |
BlacklistCreate Fields:
user_id: int
reason: Optional[str]
expires_at: Optional[datetime]
BlacklistUpdate Fields:
blacklist_id: int
reason: Optional[str]
expires_at: Optional[datetime]
active: Optional[bool]
Loan Scoring (/scoring/)
| Method |
Endpoint |
Status |
Response Model |
Request Payload |
Purpose |
| POST |
/scoring/run-internal |
200 |
ScoreLogResponse |
Scoring |
Run internal credit score |
| POST |
/scoring/run-external |
200 |
ScoreLogResponse |
ScoringUser |
Run external credit score |
| POST |
/scoring/fetch |
200 |
LoanScoreLogsFetchResponse |
LoanScoreFilters |
Get score history (paginated) |
| POST |
/scoring/get |
200 |
ScoreLogGetResponse |
LoanScoreID |
Get specific score log (async) |
| POST |
/scoring/loan |
200 |
List[ScoreLogResponse] |
LoanID |
Get all scores for loan |
Scoring Fields:
user_id: str
loan_amount: Decimal
product_id: str
ScoringUser Fields:
LoanScoreFilters Fields:
- Standard pagination (page_number, page_size)
- Date range filters (date_from, date_to)
- Loan/applicant/application IDs
LoanScoreID Fields:
Loan Collection (/case/)
| Method |
Endpoint |
Status |
Response Model |
Request Payload |
Purpose |
| POST |
/case/add |
201 |
CollectionCaseResponse |
LoanID |
Create collection case |
| POST |
/case/fetch |
200 |
CollectionCaseFetchResponse |
FetchCollectionCaseFilters |
List collection cases |
| POST |
/case/get |
200 |
CollectionCaseResponse |
CollectionCaseID |
Get case details (async) |
| POST |
/case/actions |
201 |
CollectionActionResponse |
CollectionActionCreate |
Record collection action |
| POST |
/case/status |
200 |
CollectionCaseResponse |
CollectionCaseStatusUpdate |
Update case status |
| POST |
/case/note |
201 |
CollectionNoteResponse |
CollectionNoteAdd |
Add note to case |
| POST |
/case/assign |
200 |
CollectionCaseResponse |
CaseAssignPayload |
Assign case to agent |
| POST |
/cases/auto-assign |
200 |
CollectionCaseResponse |
(no body) |
Auto-assign cases to agents |
CollectionActionCreate Fields:
case_id: Optional[str]
action_type: CollectionActionType
outcome: CollectionActionOutcome
amount_recovered: Optional[float] (default 0.0)
notes: Optional[str]
next_followup_date: Optional[datetime]
CollectionActionType Enum:
- call
- sms
- email
- visit
- legal_notice
CollectionActionOutcome Enum:
- contacted
- promise_to_pay
- no_response
- disputed
- payment_received
CollectionCaseStatusUpdate Fields:
CollectionNoteAdd Fields:
case_id: int
content: str
FetchCollectionCaseFilters Fields:
assigned_to: Optional[int]
status: Optional[CollectionStatus]
case_type: Optional[CaseType]
current_stage: Optional[CaseStage]
days_overdue_min: Optional[int]
days_overdue_max: Optional[int]
- Pagination and sorting
CollectionStatus Enum:
- open
- work_in_progress
- resolved
- written_off
CaseType Enum:
Loan Reports
| Method |
Endpoint |
Status |
Response |
Purpose |
| POST |
/portfolio-summary |
200 |
JSON |
Portfolio overview |
| POST |
/report/exposure |
200 |
List[ExposureLogResponse] |
User exposure history |
| POST |
/report/snapshot-exposures |
200 |
JSON |
Snapshot current exposures |
| POST |
/report/delinquency |
200 |
JSON |
Delinquent loans report |
Loan Operations (Admin) (/operations/)
| Method |
Endpoint |
Status |
Purpose |
| POST |
/operations/mark-overdue-loans |
200 |
Mark all overdue loans |
| POST |
/operations/mark-defaulted-loans |
200 |
Mark loans as defaulted |
| POST |
/operations/run-end-of-day |
200 |
Run EOD processes |
Fraud Rules (/fraudrule/)
| Method |
Endpoint |
Status |
Response |
Request |
Purpose |
| POST |
/fraudrule/add |
201 |
FraudRuleResponse |
FraudRuleCreate |
Create fraud rule (async) |
| POST |
/fraudrule/fetch |
200 |
LoanFraudRulesResponse |
FraudRuleFilters |
List fraud rules (async) |
| POST |
/fraudrule/get |
200 |
FraudRuleResponse |
FraudRuleID |
Get fraud rule (async) |
| POST |
/fraudrule/edit |
201 |
FraudRuleResponse |
FraudRuleCreate |
Edit fraud rule |
| POST |
/fraudrule/toggle |
200 |
FraudRuleResponse |
FraudRuleToggleStatus |
Toggle rule active/inactive (async) |
| POST |
/fraudrule/delete |
204 |
- |
FraudRuleID |
Delete fraud rule (async) |
FraudRuleCreate Fields:
id: Optional[int]
name: str (max 100, min 3 chars)
description: Optional[str]
risk_score: int (0-100, required)
action: FraudAction
action_params: Optional[Dict[str, Any]]
group_operator: LogicalOperator (default AND)
category: FraudCategory
severity: RuleSeverity
priority: int (ge 1, required)
is_active: bool (default True)
valid_from: Optional[datetime]
valid_to: Optional[datetime]
conditions: List[FraudRuleConditionCreate] (min 1, required)
FraudAction Enum:
FraudContext Enum:
- loan_application
- loan_disbursement
- repayment
FraudCategory Enum:
- identity
- behavior
- document
- application
- financial
RuleSeverity Enum:
LogicalOperator Enum:
FraudRuleConditionCreate Fields:
condition_type: str (max 50)
field_name: str (max 100, required)
operator: ConditionOperator
comparison_value: Union[str, int, float, List, Dict]
score_contribution: int (0-100, required)
logical_operator: LogicalOperator (default AND)
condition_group: int (default 1, ge 1)
is_negative: bool (default False)
ConditionOperator Enum:
- equals
- not_equals
- greater_than
- less_than
- greater_than_equal
- less_than_equal
- contains
- starts_with
- ends_with
- in
- not_in
- regex
Fraud Rule Groups (/fraudrulegroup/)
| Method |
Endpoint |
Status |
Response |
Request |
Purpose |
| POST |
/fraudrulegroup/add |
201 |
FraudRuleGroupResponse |
FraudRuleGroupCreate |
Create rule group (async) |
| POST |
/fraudrulegroup/fetch |
200 |
LoanFraudRuleGroupsResponse |
FraudRuleGroupFilters |
List groups (async) |
| POST |
/fraudrulegroup/get |
200 |
FraudRuleGroupResponse |
FraudRuleGroupID |
Get group details (async) |
| POST |
/fraudrulegroup/edit |
201 |
FraudRuleGroupResponse |
FraudRuleGroupUpdate |
Edit group |
| POST |
/fraudrulegroup/toggle |
200 |
FraudRuleGroupResponse |
FraudRuleGroupToggleStatus |
Toggle group active/inactive (async) |
| POST |
/fraudrulegroup/delete |
204 |
- |
FraudRuleGroupID |
Delete group (async) |
Fraud Flags & Cases (/fraud*)
| Method |
Endpoint |
Status |
Response |
Request |
Purpose |
| POST |
/fraud/check |
200 |
JSON |
LoanApplicationID |
Check application fraud |
| POST |
/fraudflag/fetch |
200 |
LoanFraudFlagsFetchResponse |
FraudFlagFetchFilters |
List fraud flags |
| POST |
/fraudflag/get |
200 |
LoanFraudFlagResponse |
FraudFlagID |
Get flag details (async) |
| POST |
/fraudflag/false |
200 |
LoanFraudFlagResponse |
FraudFlagReview |
Mark as false positive |
| POST |
/fraudflag/confirm |
200 |
LoanFraudFlagResponse |
FraudFlagReview |
Confirm fraud |
| POST |
/fraudflag/escalate |
200 |
LoanFraudFlagResponse |
FraudFlagEscalate |
Escalate flag |
| POST |
/fraudcase/add |
201 |
FraudCaseResponse |
FraudCaseCreate |
Create fraud case (async) |
| POST |
/fraudcase/fetch |
200 |
- |
- |
List fraud cases (async) |
| POST |
/fraudcase/get |
200 |
- |
- |
Get fraud case (async) |
| POST |
/fraudcase/assign |
200 |
- |
- |
Assign case (async) |
| POST |
/fraudcase/status |
200 |
- |
- |
Update case status (async) |
| POST |
/fraudcase/close |
200 |
- |
- |
Close fraud case (async) |
| POST |
/fraudcase/decision |
200 |
- |
- |
Record decision (async) |
| POST |
/fraudcase/note |
201 |
- |
- |
Add note to case (async) |
| POST |
/fraudcase/action |
201 |
- |
- |
Record action (async) |
PYDANTIC SCHEMA CLASSES
LoanProductFetchResponse
id: int
is_active: bool
created_at: datetime
updated_at: Optional[datetime]
[+ all LoanProductCreate fields]
LoanProductGetResponse
Extends LoanProductGetResponseMin with:
risk_rules: Optional[List[RiskRuleCreate]]
scoring_config: Optional[List[ProductScoringConfigCreate]]
LoanApplicationResponse
id: int
applicant_id: int
loan_product_id: int
requested_amount: Decimal
purpose: Optional[str]
device_info: Optional[DeviceInfo]
ip_address: Optional[str]
user_agent: Optional[str]
geolocation: Optional[Geolocation]
location: Optional[Location]
risk_level: Optional[str]
status: LoanApplicationStatus
score: Optional[Decimal]
reviewer_id: Optional[int]
reviewed_at: Optional[datetime]
rejection_reason: Optional[str]
created_at: datetime
updated_at: Optional[datetime]
loan_product: LoanProductGetResponseMin
applicant: ApplicantResponse
score_logs: Optional[List[ScoreLogResponse]]
fraud_flags: Any
guarantor_drafts: Optional[List[GuarantorResponse]]
collateral_drafts: Optional[List[CollateralResponse]]
loan: Optional[LoanFetch]
borrower: CustomerResponseSchema
ApplicantResponse
identity_id: int
firstname: str
lastname: str
middlename: Optional[str]
date_of_birth: Optional[str]
place_of_birth: Optional[str]
age: Optional[str]
national_id: Optional[str]
profile_staging_id: Optional[int]
nationality: Optional[str]
gender: Optional[str]
marital_status_id: Optional[int]
income_range_id: Optional[int]
employment_id: Optional[int]
employer_name: Optional[str]
education_id: Optional[int]
employer_category_id: Optional[int]
income: Optional[Decimal]
updated_on: Optional[datetime]
created_on: Optional[datetime]
LoanFetch
id: int
borrower_id: int
loan_product_id: int
application_id: Optional[int]
principal_amount: Decimal
interest_rate: Decimal
interest_type: InterestType
total_payable: Decimal
amount_paid: Decimal
outstanding_balance: Decimal
status: LoanStatus
cbs_reference: Optional[str]
disbursed_at: Optional[datetime]
first_due_date: Optional[date]
final_due_date: Optional[date]
channel: Optional[str]
created_at: datetime
updated_at: Optional[datetime]
borrower: ApplicantResponse
LoanResponse (extends LoanFetch)
application: Optional[LoanApplicationResponse]
installments: List[InstallmentResponse]
repayments: List[RepaymentResponse]
guarantors: List[GuarantorResponse]
collaterals: List[CollateralResponse]
score_logs: List[ScoreLogResponse]
cases: List[CollectionCaseResponse]
applicant: CustomerResponseSchema
udf: Optional[Dict[str, Any]]
InstallmentResponse
id: int
loan_id: int
installment_number: int
due_date: date
amount: Decimal
principal: Decimal
interest: Decimal
remaining_balance: Decimal
status: InstallmentStatus
amount_paid: Decimal
paid_at: Optional[datetime]
InstallmentStatus Enum
- pending
- partially_paid
- paid
- overdue
RepaymentResponse
id: int
loan_id: int
installment_id: Optional[int]
amount: Decimal
repayment_date: datetime
method: RepaymentMethod
external_reference: Optional[str]
cbs_reference: Optional[str]
recorded_by: Optional[int]
reversal_reference: Optional[str]
is_reversed: bool
GuarantorResponse
id: int
loan_id: Optional[int]
application_id: Optional[int]
guarantor_id: int
relation_type: Optional[str]
guarantee_amount: Optional[Decimal]
status: GuaranteeStatus
accepted_at: Optional[datetime]
rejected_at: Optional[datetime]
guarantor: Optional[ApplicantResponse]
ApplicationCollateralResponse
id: int
application_id: int
type: str
description: Optional[str]
document_url: Optional[str]
value: Optional[Decimal]
file_id: Optional[int]
accepted: bool
accepted_at: Optional[datetime]
rejected_at: Optional[datetime]
file: Optional[FilesResponse]
CollateralResponse
id: int
loan_id: Optional[int]
application_id: Optional[int]
type: str
description: Optional[str]
document_url: Optional[str]
value: Optional[Decimal]
file_id: Optional[int]
accepted: bool
accepted_at: Optional[datetime]
rejected_at: Optional[datetime]
file: Optional[FilesResponse]
BlacklistResponse
id: int
user_id: int
reason: Optional[str]
active: bool
blocked_by: int
blocked_at: datetime
expires_at: Optional[datetime]
unblocked_at: Optional[datetime]
unblocked_by: Optional[int]
profile: Optional[ApplicantResponse]
ScoreLogResponse
id: int
loan_id: Optional[int]
loan_product_id: Optional[int]
applicant_id: int
score: Decimal
scoring_model: str
result: str
risk_level: Optional[str]
raw_data: Optional[dict]
evaluated_at: datetime
loan: Optional[LoanFetch]
application: Optional[LoanApplicationMinResponse]
borrower: CustomerProfileResponseMinSchema
loan_product: Optional[LoanProductGetResponseMin]
ExposureLogResponse
id: int
user_id: int
current_exposure: Decimal
date_logged: datetime
CollectionCaseResponse
id: int
loan_id: int
case_type: CaseType
status: CollectionStatus
days_overdue: int
assigned_agent: Optional[int]
current_stage: int
outstanding_amount: Decimal
fees_accrued: Decimal
opened_at: datetime
resolved_at: Optional[datetime]
created_by: Optional[int]
updated_by: Optional[int]
updated_at: datetime
CollectionActionResponse
id: str
case_id: str
action_type: CollectionActionType
outcome: CollectionActionOutcome
amount_recovered: float
next_followup_date: Optional[datetime]
performed_at: datetime
creator: UserProfileMin
CollectionNoteResponse
id: int
case_id: int
content: str
created_at: datetime
creator: UserProfileMin
FraudRuleResponse
id: int
name: str
description: Optional[str]
risk_score: int
action: FraudAction
action_params: Optional[Dict[str, Any]]
group_operator: LogicalOperator
category: FraudCategory
severity: RuleSeverity
priority: int
is_active: bool
valid_from: Optional[datetime]
valid_to: Optional[datetime]
conditions: List[FraudRuleConditionCreate]
created_at: datetime
updated_at: Optional[datetime]
FraudRuleGroupResponse
id: int
name: str
description: Optional[str]
context: str
logical_operator: str
min_rules_required: int
execution_mode: str
risk_score: int
action: Optional[str]
is_active: bool
created_at: datetime
rules: List[FraudRuleGroupMemberResponse]
LoanFraudFlagResponse
id: int
application_id: int
loan_id: Optional[int]
user_id: int
risk_score: float
risk_level: str
flag_reason: str
rule_violations: JSONB
triggered_rules: List[int]
status: str
review_notes: Optional[str]
created_at: datetime
updated_at: datetime
FraudCaseResponse
id: int
flag_id: int
application_id: Optional[int]
loan_id: Optional[int]
user_id: int
status: str
decision: Optional[str]
priority: str
assigned_to: Optional[int]
sla_due_at: Optional[datetime]
opened_at: datetime
closed_at: Optional[datetime]
actions: List[FraudCaseActionResponse]
notes: List[FraudCaseNoteResponse]
outcomes: List[FraudOutcomeResponse]
FetchFilters (Comprehensive)
# --- Common filters ---
search_string: Optional[str]
sort_by: Optional[str]
sort_order: Optional[str] (ASC/DESC, default DESC)
active: Optional[bool]
deleted: Optional[bool] (default False)
status_id: Optional[str]
# --- Pagination ---
page_number: Optional[int] (ge 1, default 1)
page_size: Optional[int] (ge 1, default 10)
# --- Date range ---
date_from: Optional[datetime]
date_to: Optional[datetime]
# --- Loan Product-specific ---
interest_type: Optional[str]
installment_type: Optional[str]
currency: Optional[str]
is_active: Optional[bool]
requires_manual_approval: Optional[bool]
requires_guarantors: Optional[bool]
# --- Loan Application-specific ---
applicant_id: Optional[int]
product_id: Optional[int]
status: Optional[str]
risk_level: Optional[str]
min_score: Optional[Decimal]
max_score: Optional[Decimal]
min_amount: Optional[Decimal]
max_amount: Optional[Decimal]
entity_ids: Optional[List[int]]
SQLALCHEMY MODELS
LoanProduct
Schema: "loan"
Table: "loan_products"
Columns:
- id: Integer (PK, auto)
- name: String(100) (NOT NULL, UNIQUE)
- code: String(20) (NOT NULL, UNIQUE)
- description: Text
- interest_type: Enum(flat, reducing, compound)
- interest_rate: Numeric(5,2) (NOT NULL)
- currency: String(3) FK to core.currency.code
- min_amount: Numeric(12,2) (NOT NULL)
- max_amount: Numeric(12,2) (NOT NULL)
- tenure_days: Integer (NOT NULL)
- installment_type: Enum(none, daily, weekly, monthly, quarterly)
- grace_period_days: Integer (default 0)
- penalty_rate: Numeric(5,2) (default 0.0)
- max_dti_ratio: Numeric(5,2)
- compounding_frequency: Enum(daily, weekly, monthly, quarterly, annually)
- requires_guarantors: Boolean (default False)
- requires_collateral: Boolean (default False)
- requires_manual_approval: Boolean (default False)
- requires_manual_disbursement: Boolean (default True)
- requires_score_check: Boolean (default True)
- requires_external_score: Boolean (default False)
- min_required_score: Numeric(5,2) (default 500.0)
- min_required_external_score: Numeric(5,2) (default 550.0)
- preferred_score_model: String(20) (default "internal")
- is_active: Boolean (default True)
- created_at: DateTime(tz) (default UTC now)
- updated_at: DateTime(tz) (onupdate UTC now)
Relationships:
- risk_rules: 1-to-Many LoanProductRiskRule (cascade delete)
- scoring_config: 1-to-Many LoanProductScoringConfig (cascade delete)
- applications: 1-to-Many LoanApplication
LoanProductRiskRule
Schema: "loan"
Table: "loan_product_risk_rules"
Columns:
- id: Integer (PK, auto)
- product_id: Integer FK to loan.loan_products.id (NOT NULL)
- min_credit_score: Numeric(5,2)
- max_exposure: Numeric(12,2)
- min_income: Numeric(12,2)
- max_dti_ratio: Numeric(5,2)
- max_active_loans: Integer
- is_enabled: Boolean (default True)
- created_at: DateTime(tz)
Relationships:
- product: Many-to-1 LoanProduct
LoanProductScoringConfig
Schema: "loan"
Table: "loan_products_score_config"
Columns:
- id: Integer (PK, auto)
- product_id: Integer FK to loan.loan_products.id (NOT NULL)
- repayment_history: Numeric(5,2) (NOT NULL)
- exposure: Numeric(5,2) (NOT NULL)
- income_stability: Numeric(5,2) (NOT NULL)
- behavioral: Numeric(5,2) (NOT NULL)
Relationships:
- product: Many-to-1 LoanProduct
LoanApplication
Schema: "loan"
Table: "loan_applications"
Columns:
- id: Integer (PK, auto)
- applicant_id: Integer FK to core.identity.id (NOT NULL)
- loan_product_id: Integer FK to loan.loan_products.id (NOT NULL)
- requested_amount: Numeric(12,2) (NOT NULL)
- purpose: Text
- device_info: JSONB
- ip_address: String(100)
- user_agent: Text
- geolocation: JSONB
- location: JSONB
- risk_level: Enum(very_low, low, medium, high)
- status: Enum(pending, under_review, approved, rejected, conditionally_approved, cancelled, expired, blocked)
- score: Numeric(10,2)
- reviewer_id: Integer FK to core.identity.id
- reviewed_at: DateTime(tz)
- rejection_reason: Text
- created_at: DateTime(tz) (default UTC now)
- updated_at: DateTime(tz) (onupdate UTC now)
Relationships:
- loan_product: Many-to-1 LoanProduct (uselist=False)
- loan: 1-to-1 Loan
- score_logs: 1-to-Many LoanScoreLog (cascade delete)
- fraud_flags: 1-to-Many LoanFraudFlag (cascade delete)
- applicant: Many-to-1 Profile
- guarantor_drafts: 1-to-Many LoanApplicationGuarantor (cascade delete)
- collateral_drafts: 1-to-Many LoanApplicationCollateral (cascade delete)
- borrower: Many-to-1 Customer (view-only)
LoanApplicationGuarantor
Schema: "loan"
Table: "loan_application_guarantors"
Columns:
- id: Integer (PK, auto)
- application_id: Integer FK to loan.loan_applications.id (NOT NULL)
- guarantor_id: Integer FK to core.identity.id (NOT NULL)
- guarantee_amount: Numeric(12,2)
- relation_type: String(50)
- status: Enum(pending, validated, rejected)
- validated_at: DateTime(tz)
- rejected_at: DateTime(tz)
- updated_by: Integer FK to core.identity.id
- reject_reason: Text
Relationships:
- application: Many-to-1 LoanApplication
- guarantor: Many-to-1 Profile
LoanApplicationCollateral
Schema: "loan"
Table: "loan_application_collateral"
Columns:
- id: Integer (PK, auto)
- application_id: Integer FK to loan.loan_applications.id (NOT NULL)
- type: Enum(vehicle, real_estate, cash_deposit, guarantor, equipment, other)
- description: Text
- value: Numeric(12,2)
- document_url: String(255)
- file_id: Integer FK to document.file.id
- accepted: Boolean (default False)
- accepted_at: DateTime(tz)
- rejected_at: DateTime(tz)
- updated_by: Integer FK to core.identity.id
- reject_reason: Text
Relationships:
- application: Many-to-1 LoanApplication
- file: 1-to-1 Files (view-only)
Loan
Schema: "loan"
Table: "loans"
Columns:
- id: Integer PK (also FK to core.identity.id)
- borrower_id: Integer FK to core.identity.id (NOT NULL)
- loan_product_id: Integer FK to loan.loan_products.id (NOT NULL)
- application_id: Integer FK to loan.loan_applications.id
- benificiary_account_id: Integer FK to customer.account.id
- principal_amount: Numeric(12,2) (NOT NULL)
- interest_rate: Numeric(5,2) (NOT NULL)
- interest_type: Enum(flat, reducing, compound) (NOT NULL)
- total_payable: Numeric(12,2) (NOT NULL)
- amount_paid: Numeric(12,2) (default 0.0)
- outstanding_balance: Numeric(12,2) (default 0.0, NOT NULL)
- status: Enum(pending, approved, disbursed, active, overdue, repaid, defaulted, rejected, cancelled)
- cbs_reference: String(50)
- disbursed_at: DateTime(tz)
- first_due_date: DateTime(tz)
- final_due_date: DateTime(tz)
- channel: String(50)
- amortization_schedule: JSONB
- created_at: DateTime(tz)
- updated_at: DateTime(tz)
Relationships:
- application: 1-to-1 LoanApplication
- cases: 1-to-Many CollectionCase (cascade delete, single parent)
- installments: 1-to-Many LoanInstallment (cascade delete)
- repayments: 1-to-Many LoanRepayment (cascade delete)
- guarantors: 1-to-Many LoanGuarantor (cascade delete)
- collaterals: 1-to-Many LoanCollateral (cascade delete)
- score_logs: Many-to-Many through LoanApplication (view-only)
- borrower: Many-to-1 Profile (view-only)
- applicant: Many-to-1 Customer (view-only)
- status_history: 1-to-Many LoanStatusHistory (cascade delete)
Methods:
- update_loan_status(new_status, user_id)
LoanStatusHistory
Schema: "loan"
Table: "loan_status_history"
Columns:
- id: Integer (PK, auto)
- loan_id: Integer FK to loan.loans.id (NOT NULL, CASCADE delete, indexed)
- old_status: String(50)
- new_status: String(50) (NOT NULL)
- changed_at: DateTime(tz) (server default now, NOT NULL)
- changed_by: Integer
Relationships:
- loan: Many-to1 Loan
LoanInstallment
Schema: "loan"
Table: "loan_installments"
Columns:
- id: Integer (PK, auto)
- loan_id: Integer FK to loan.loans.id (NOT NULL)
- installment_number: Integer (NOT NULL)
- due_date: Date (NOT NULL)
- is_grace_period: Boolean (default False)
- amount: Numeric(12,2) (NOT NULL)
- principal: Numeric(12,2) (NOT NULL)
- interest: Numeric(12,2) (NOT NULL)
- remaining_balance: Numeric(12,2) (NOT NULL)
- status: Enum(pending, partially_paid, paid, overdue)
- amount_paid: Numeric(12,2) (default 0.0)
- paid_at: DateTime(tz)
- last_notified_at: DateTime(tz)
- updated_at: DateTime(tz)
Relationships:
- loan: Many-to-1 Loan
LoanRepayment
Schema: "loan"
Table: "loan_repayments"
Columns:
- id: Integer (PK, auto)
- loan_id: Integer FK to loan.loans.id (NOT NULL)
- installment_id: Integer FK to loan.loan_installments.id
- amount: Numeric(12,2) (NOT NULL)
- repayment_date: DateTime(tz) (default UTC now)
- method: Enum(wallet, cash, commission, cbs, manual)
- external_reference: String(50)
- cbs_reference: String(50)
- recorded_by: Integer FK to core.identity.id
- reversal_reference: String(50)
- reversed_at: DateTime(tz)
- is_reversed: Boolean (default False)
- is_penalty: Boolean (default False)
Relationships:
- loan: Many-to-1 Loan
- installment: Many-to-1 LoanInstallment
LoanGuarantor
Schema: "loan"
Table: "loan_guarantors"
Columns:
- id: Integer (PK, auto)
- loan_id: Integer FK to loan.loans.id (NOT NULL)
- guarantor_id: Integer FK to core.identity.id (NOT NULL)
- guarantee_amount: Numeric(12,2)
- relation_type: String(50)
- status: Enum(pending, accepted, rejected)
- accepted_at: DateTime(tz)
- rejected_at: DateTime(tz)
- updated_by: Integer FK to core.identity.id
Relationships:
- loan: Many-to-1 Loan
LoanCollateral
Schema: "loan"
Table: "loan_collateral"
Columns:
- id: Integer (PK, auto)
- loan_id: Integer FK to loan.loans.id (NOT NULL)
- type: Enum(vehicle, real_estate, cash_deposit, guarantor, equipment,other)
- description: Text
- value: Numeric(12,2)
- document_url: String(255)
- file_id: Integer FK to document.file.id
- accepted: Boolean (default False)
- accepted_at: DateTime(tz)
- rejected_at: DateTime(tz)
- updated_by: Integer FK to core.identity.id
Relationships:
- loan: Many-to-1 Loan
- file: 1-to-1 Files
CollectionCase
Schema: "loan"
Table: "collection_cases"
Columns:
- id: Integer (PK, auto)
- loan_id: Integer FK to loan.loans.id (NOT NULL)
- case_type: Enum(soft, hard, legal)
- status: Enum(open, work_in_progress, resolved, written_off)
- days_overdue: Integer (NOT NULL)
- assigned_agent: Integer FK to core.identity.id
- current_stage: Integer (default 1)
- outstanding_amount: Numeric(12,2) (NOT NULL)
- fees_accrued: Numeric(12,2) (default 0.0)
- opened_at: DateTime (default UTC now)
- resolved_at: DateTime
- created_by: Integer FK to users.users.id
- updated_by: Integer FK to users.users.id
- updated_at: DateTime(tz)
Relationships:
- loan: Many-to-1 Loan (cascade delete, single parent)
- actions: 1-to-Many CollectionAction (cascade delete)
- notes: 1-to-Many CollectionNote (cascade delete)
- creator: Many-to-1 Profile (view-only)
- assigned: Many-to-1 Profile (view-only)
CollectionAction
Schema: "loan"
Table: "collection_actions"
Columns:
- id: Integer (PK, auto)
- case_id: Integer FK to loan.collection_cases.id
- action_type: Enum(call, sms, email, visit, legal_notice)
- outcome: Enum(contacted, promise_to_pay, no_response, disputed)
- amount_recovered: Numeric(12,2) (default 0.0)
- next_followup_date: DateTime
- performed_by: Integer FK to users.users.id
- performed_at: DateTime (default UTC now)
- notes: Text
Relationships:
- case: Many-to-1 CollectionCase
- creator: Many-to-1 Profile (view-only)
CollectionNote
Schema: "loan"
Table: "collection_notes"
Columns:
- id: Integer (PK, auto)
- case_id: Integer FK to loan.collection_cases.id
- content: Text (NOT NULL)
- created_by: Integer FK to users.users.id
- created_at: DateTime (default UTC now)
Relationships:
- case: Many-to-1 CollectionCase
- creator: Many-to-1 Profile
LoanScoreLog
Schema: "loan"
Table: "loan_score_logs"
Columns:
- id: Integer (PK, auto)
- loan_id: Integer FK to loan.loans.id
- application_id: Integer FK to loan.loan_applications.id
- loan_product_id: Integer FK to loan.loan_products.id
- applicant_id: Integer FK to core.identity.id (NOT NULL)
- score: Numeric(10,2) (NOT NULL)
- scoring_model: String(50) (NOT NULL)
- result: Enum(passed, failed)
- risk_level: Enum(very_low, low, medium, high)
- raw_data: JSONB
- evaluated_at: DateTime(tz) (default UTC now)
Relationships:
- loan: Many-to-1 Loan
- application: Many-to-1 LoanApplication
- borrower: Many-to-1 Customer
- loan_product: Many-to-1 LoanProduct
LoanBlacklist
Schema: "loan"
Table: "loan_blacklist"
Columns:
- id: Integer (PK, auto)
- user_id: Integer FK to core.identity.id (NOT NULL)
- reason: Text
- active: Boolean (default True)
- blocked_by: Integer FK to core.identity.id (NOT NULL)
- blocked_at: DateTime(tz) (default UTC now)
- expires_at: DateTime(tz)
- unblocked_at: DateTime(tz)
- unblocked_by: Integer FK to core.identity.id
- created_at: DateTime(tz)
- updated_at: DateTime(tz)
Relationships:
- profile: Many-to-1 Profile (view-only)
LoanExposureLog
Schema: "loan"
Table: "loan_exposure_logs"
Columns:
- id: Integer (PK, auto)
- user_id: Integer FK to core.identity.id (NOT NULL)
- current_exposure: Numeric(12,2) (NOT NULL)
- date_logged: DateTime(tz) (default UTC now)
LoanAuditLog
Schema: "loan"
Table: "loan_audit_logs"
Columns:
- id: Integer (PK, auto)
- loan_id: Integer FK to loan.loans.id
- user_id: Integer FK to core.identity.id
- action: String(50) (NOT NULL)
- entity_type: String(50) (NOT NULL)
- entity_id: Integer
- old_values: JSONB
- new_values: JSONB
- ip_address: String(50)
- user_agent: String(255)
- created_at: DateTime(tz)
LoanFraudFlag
Schema: "loan"
Table: "fraud_flags"
Columns:
- id: Integer (PK, auto)
- application_id: Integer FK to loan.loan_applications.id (NOT NULL)
- loan_id: Integer FK to loan.loans.id
- user_id: Integer FK to core.identity.id (NOT NULL)
- risk_score: Float (NOT NULL)
- risk_level: Enum(very_low, low, medium, high, critical)
- flag_reason: Text (NOT NULL)
- rule_violations: JSONB
- triggered_rules: ARRAY(Integer)
- status: Enum(open, escalated, auto_blocked, under_review, false_positive, resolved)
- review_notes: Text
- created_at: DateTime(tz)
- updated_at: DateTime(tz)
- Indexes: ix_fraud_flag_application, ix_fraud_flag_user
Relationships:
- application: Many-to-1 LoanApplication
- loan: Many-to-1 Loan
- fraudster: Many-to-1 Customer (view-only)
- case: 1-to-1 LoanFraudCase (cascade delete)
LoanFraudCase
Schema: "loan"
Table: "fraud_cases"
Columns:
- id: Integer (PK, auto)
- flag_id: Integer FK to loan.fraud_flags.id (NOT NULL, UNIQUE)
- application_id: Integer FK to loan.loan_applications.id
- loan_id: Integer FK to loan.loans.id
- user_id: Integer FK to core.identity.id (NOT NULL)
- status: Enum(open, in_review, awaiting_info, resolved, closed)
- decision: Enum(confirmed_fraud, false_positive, inconclusive)
- priority: Enum(low, medium, high, urgent)
- assigned_to: Integer FK to users.users.id
- sla_due_at: DateTime(tz)
- opened_at: DateTime(tz)
- closed_at: DateTime(tz)
- created_at: DateTime(tz)
- updated_at: DateTime(tz)
- Indexes: ix_fraud_case_status, ix_fraud_case_assigned
Relationships:
- flag: Many-to-1 LoanFraudFlag
- application: Many-to-1 LoanApplication
- loan: Many-to-1 Loan
- user: Many-to-1 Profile
- reviewer: Many-to-1 Profile
- actions: 1-to-Many LoanFraudCaseAction (cascade delete)
- notes: 1-to-Many LoanFraudCaseNote (cascade delete)
- outcomes: 1-to-Many LoanFraudOutcome (cascade delete)
LoanFraudCaseAction
Schema: "loan"
Table: "fraud_case_actions"
Columns:
- id: Integer (PK, auto)
- case_id: Integer FK to loan.fraud_cases.id (NOT NULL)
- action: Enum(assigned, status_changed, decision_recorded, requested_documents, escalated, note_added)
- old_value: JSONB
- new_value: JSONB
- performed_by: Integer FK to users.users.id
- performed_at: DateTime(tz)
- Index: ix_fraud_case_action_case
Relationships:
- case: Many-to-1 LoanFraudCase
- actor: Many-to-1 User
LoanFraudCaseNote
Schema: "loan"
Table: "fraud_case_notes"
Columns:
- id: Integer (PK, auto)
- case_id: Integer FK to loan.fraud_cases.id (NOT NULL)
- content: Text (NOT NULL)
- is_internal: Boolean (default True)
- created_by: Integer FK to users.users.id
- created_at: DateTime(tz)
- Index: ix_fraud_case_note_case
Relationships:
- case: Many-to-1 LoanFraudCase
- author: Many-to-1 User
LoanFraudRule
Schema: "loan"
Table: "fraud_rules"
Columns:
- id: Integer (PK, auto)
- name: String(100) (NOT NULL, UNIQUE)
- description: Text
- risk_score: Integer (NOT NULL)
- action: Enum(flag, block, review, notify)
- action_params: JSONB
- group_operator: Enum(and, or)
- category: Enum(identity, behavior, document, application, financial)
- severity: Enum(low, medium, high, critical)
- priority: Integer (NOT NULL)
- is_active: Boolean (default True)
- created_by: Integer FK to users.users.id
- updated_by: Integer FK to users.users.id
- valid_from: DateTime(tz)
- valid_to: DateTime(tz)
- created_at: DateTime(tz)
- updated_at: DateTime(tz)
Relationships:
- conditions: 1-to-Many LoanFraudRuleCondition (cascade delete)
- groups: Many-to-Many through LoanFraudRuleGroupMember
LoanFraudRuleCondition
Schema: "loan"
Table: "fraud_rule_conditions"
Columns:
- id: Integer (PK, auto)
- rule_id: Integer FK to loan.fraud_rules.id (NOT NULL)
- condition_type: String(50)
- field_name: String(100) (NOT NULL)
- operator: Enum(equals, not_equals, greater_than, less_than, greater_than_equal, less_than_equal, contains, starts_with, ends_with, in, not_in, regex)
- comparison_value: JSONB
- score_contribution: Integer (0-100)
- logical_operator: Enum(and, or)
- condition_group: Integer (default 1)
- is_negative: Boolean (default False)
Relationships:
- rule: Many-to-1 LoanFraudRule
LoanFraudRuleGroup
Schema: "loan"
Table: "fraud_rule_groups"
Columns:
- id: Integer (PK, auto)
- name: String(100) (NOT NULL, UNIQUE)
- description: Text
- context: Enum(loan_application, loan_disbursement, repayment)
- logical_operator: Enum(and, or)
- min_rules_required: Integer (default 1)
- execution_mode: Enum(all, first_match)
- risk_score: Integer (default 0)
- action: Enum(flag, block, review, notify)
- is_active: Boolean (default True)
- created_by: Integer FK to users.users.id
- updated_by: Integer FK to users.users.id
- created_at: DateTime(tz)
- updated_at: DateTime(tz)
Relationships:
- members: 1-to-Many LoanFraudRuleGroupMember (cascade delete)
LoanFraudRuleGroupMember
Schema: "loan"
Table: "fraud_rule_group_members"
Columns:
- id: Integer (PK, auto)
- group_id: Integer FK to loan.fraud_rule_groups.id (NOT NULL)
- rule_id: Integer FK to loan.fraud_rules.id (NOT NULL)
- priority: Integer (default 1)
Relationships:
- group: Many-to-1 LoanFraudRuleGroup
- rule: Many-to-1 LoanFraudRule
CONTROLLER FUNCTIONS
LoanProductService
create_product(db, payload, current_user) → LoanProduct
- Creates a new loan product
- Validates unique constraints on name & code
- Creates associated risk rules and scoring configs
- Returns created LoanProduct or raises ValidationException/LoanException
update_product(db, product_id, payload) → LoanProduct
- Updates existing product
- Validates constraints (no name/code conflicts)
- Clears and recreates risk rules/scoring configs
- Returns updated LoanProduct or raises NotFoundException/ValidationException
get_product(db, product_id) → LoanProduct
- Fetches product by ID
- Raises NotFoundException if not found
list_products(db, filters) → dict
- Lists products with advanced filtering:
- is_active filter
- search_string (name, code, description)
- date_from/date_to
- interest_type, installment_type
- requires_manual_approval
- min_amount, max_amount
- requires_guarantors
- min_score
- currency
- Supports sorting (name, code, interest_rate, etc.) ASC/DESC
- Returns paginated results with pagination info
toggle_product_status(db, product_id) → LoanProduct
- Toggles product is_active flag
- Raises NotFoundException if not found
LoanApplicationService
apply_for_loan(db, payload, trace_id) → LoanApplication (async)
- Creates new loan application
- Validates product (active & amount within bounds)
- Checks blacklist
- Runs scoring if required
- Checks DTI ratio
- Creates application with device/geolocation metadata
- Runs fraud check
- Auto-approves if conditions met + not requiring manual approval
- Auto-disburses if product doesn't require manual disbursement
- Returns application or raises ValidationException/LoanException
approve_application(db, application_id, reviewer_id) → LoanApplication
- Approves loan application
- Validates all requirements (guarantors/collateral)
- Creates Loan from application
- Moves validated guarantors to loan
- Moves accepted collateral to loan
- Sends notification
- Raises NotFoundException/ValidationException
reject_application(db, application_id, reviewer_id, reason) → LoanApplication
- Rejects application with reason
- Sends rejection notification
- Raises NotFoundException/ValidationException
get_application(db, application_id) → LoanApplication
- Fetches application by ID
- Raises NotFoundException if not found
list_applications(db, current_user, filters) → dict
- Lists applications visible to user
- Filters by applicant_id, entity_ids, status, product_id, risk_level
- Date range, amount range filters
- Search, sort, pagination
- Returns paginated results
add_application_guarantor(db, payload, updated_by) → LoanApplicationGuarantor
- Adds guarantor to application (application must be under_review or conditionally_approved)
- Validates product requires guarantors
- Raises NotFoundException/ValidationException
validate_application_guarantor(db, guarantor_id, validated_by) → LoanApplicationGuarantor
- Marks guarantor as validated
reject_application_guarantor(db, guarantor_id, rejected_by, reason) → LoanApplicationGuarantor
- Marks guarantor as rejected
add_application_collateral(db, payload, current_user) → LoanApplicationCollateral (async)
- Adds collateral to application
- Uploads file if provided
- Validates product requires collateral
- Raises NotFoundException/ValidationException
validate_application_collateral(db, collateral_id, validated_by) → LoanApplicationCollateral
- Marks collateral as accepted
reject_application_collateral(db, collateral_id, rejected_by, reason) → LoanApplicationCollateral
- Marks collateral as rejected
total_guarantee_amount(db, application_id) → Decimal
- Sums all validated guarantor amounts for application
application_has_all_requirements(db, application) → bool
- Validates application meets all security requirements
- Checks if total guarantees + collateral ≥ requested amount
- Raises ValidationException if insufficient
LoanService
create_loan(db, payload) → Loan
- Creates new loan from LoanCreate payload
- Validates product
- Validates application if provided
- Calculates amortization schedule (flat/reducing/compound interest)
- Creates loan with first/final due dates
- Creates installments
- Raises ValidationException/LoanException
calculate_amortization(principal, annual_rate, tenure_days, interest_type, installment_type, compounding_frequency, grace_period_days) → dict
- Delegates to specific calculation method based on interest_type
- Returns: {total_payable, final_due_date, schedule:[]}
_calculate_flat_interest() → dict
- Flat interest calculation
- Supports all installment types
- Handles grace period
_calculate_reducing_balance() → dict
- Reducing balance amortization
- Periodic rate calculation
- Handles grace period
_calculate_compound_interest() → dict
- Compound interest calculation
- Supports all compounding frequencies
- Handles grace period
_get_installment_periods(installment_type, tenure_days) → tuple(periods, period_days)
- Calculates number of periods based on installment type
disburse_loan(db, current_user, loan_id, trace_id) → Loan (async)
- Disburses loan to borrower
- Updates loan status to "disbursed"
- Integrates with core banking
- Raises NotFoundException/ValidationException/CoreBankingException
get_loan(db, loan_id) → Loan
- Fetches loan by ID
- Raises NotFoundException if not found
list_loans(db, current_user, filters) → dict
- Lists loans visible to user
- Supports all FetchFilters
- Paginated results
mark_loan_overdue(db, loan_id) → Loan
- Marks active loan as overdue
record _repayment(db, payload, current_user, trace_id) → list[LoanRepayment] (async)
- Records repayment
- Updates installment status
- Updates loan amounts
- Creates repayment record
- Integrates with core banking if needed
- Returns list of repayments
reverse_repayment(db, repayment_id, current_user, trace_id) → LoanRepayment (async)
- Reverses a repayment
- Updates installments
- Marks as reversed with reference
- Updates loan amounts
is_blacklisted(db, user_id) → bool
- Checks if user is on active blacklist
check_dti_ratio(db, user_id, requested_amount, product) → bool
- Calculates debt-to-income ratio for user
- Compares against product max_dti_ratio
mark_overdue_loans(db) → int
- Admin endpoint to mark all overdue loans
- Returns count of marked loans
mark_defaulted_loans(db, days_overdue) → int
- Admin endpoint to mark loans as defaulted
- Returns count
snapshot_exposures(db) → int
- Takes snapshot of all users' current loan exposure
- Returns count of users snapshotted
add_to_blacklist(db, payload, current_user_id) → LoanBlacklist
- Adds user to blacklist with reason & expiration
edit_loan_blacklist(db, payload, current_user_id) → LoanBlacklist
- Edits existing blacklist entry
get_loan_blacklist(db, blacklist_id) → LoanBlacklist
list_loan_blacklist(db, current_user, filters) → LoanBlacklistFetchResponse
remove_from_blacklist(db, blacklist_id, current_user_id) → LoanBlacklist
- Removes user from blacklist
reactivate_blacklist(db, blacklist_id, current_user_id) → LoanBlacklist
- Reactivates blacklist entry
LoanScoringService
run_comprehensive_score(db, user_id, loan_amount, product_id) → LoanScoreLog
- Runs internal credit scoring
- Evaluates: repayment history, exposure, income stability, behavioral
- Returns LoanScoreLog with score, result (passed/failed), risk_level
run_external_score(db, user_id) → LoanScoreLog
- Calls external scoring service
- Returns LoanScoreLog with external score
list_score_history(db, current_user, filters) → LoanScoreLogsFetchResponse
- Lists score logs with pagination
get_loan_score(db, score_log_id) → LoanScoreLog
- Fetches specific score log
CollectionService
create_case(db, loan_id, days_overdue, created_by) → CollectionCase
- Creates collection case for overdue loan
- Sets case_type based on days_overdue
- Raises NotFoundException
get_case(db, case_id) → CollectionCase
list_cases(db, filters) → CollectionCaseFetchResponse
- Lists collection cases with filters
- Paginated
- Records action taken on case
- Validates case exists
- Raises NotFoundException/ValidationException
update_case_status(db, payload, updated_by) → CollectionCaseResponse
- Updates case status
- Validates transitions
add_collection_note(db, payload, created_by) → CollectionNoteResponse
- Adds note to case
- Raises NotFoundException/ValidationException
assign_case(db, payload, assigned_by) → CollectionCaseResponse
- Assigns case to agent
- Raises NotFoundException/ValidationException
auto_assign_cases(db) → dict
- Distributes open cases to available agents
FraudDetectionService
create_fraud_rule(db, payload, current_user_id) → FraudRuleResponse
- Creates fraud detection rule with conditions
- Validates name uniqueness
- Raises Exception if failed
list_fraudrules(db, filters) → LoanFraudRulesResponse
- Lists fraud rules with filtering & pagination
- Filters by name, category, severity, action, is_active
edit_fraud_rule(db, payload, current_user_id) → FraudRuleResponse
- Edits fraud rule
- Updates conditions
toggle_fraud_rule_status(db, rule_id, is_active, current_user_id) → FraudRuleResponse
- Toggles rule active/inactive status (async)
create_fraud_rule_group() → FraudRuleGroupResponse
- Creates rule group with member rules
list_fraud_rule_groups(db, filters) → LoanFraudRuleGroupsResponse
- Lists rule groups (async)
get_fraud_rule_group(db, group_id) → FraudRuleGroupResponse
edit_fraud_rule_group(db, payload, current_user_id) → FraudRuleGroupResponse
toggle_fraud_rule_group_status(db, group_id, is_active, current_user_id) → FraudRuleGroupResponse
check_application_fraud(db, application_id, data) → dict
- Runs fraud detection on application
- Returns: {risk_level, final_immediate_action, triggered_rules}
list_fraud_flags(db, filters) → LoanFraudFlagsFetchResponse
- Lists fraud flags (paginated)
get_fraud_flag(db, flag_id) → LoanFraudFlagResponse
- Gets fraud flag by ID (async)
mark_fraud_false_positive(db, payload) → LoanFraudFlagResponse
- Marks flag as false positive
confirm_fraud_flag(db, payload) → LoanFraudFlagResponse
escalate_fraud_flag(db, payload) → LoanFraudFlagResponse
create_fraud_case(db, flag_id, priority, assigned_to, sla_due_at) → FraudCaseResponse (async)
- Creates fraud investigation case from flag
KEY OPERATIONS SUMMARY
Loan Creation Flow
- Application Submission → LoanApplicationCreate
- Validation → Product active, amount within bounds, not blacklisted
- Scoring → Run internal/external credit score
- Fraud Check → Run fraud rules on application
- Auto-Decision → Approve/Conditional/Manual review based on product config
- Loan Creation → If approved, create Loan from approval
Loan Disbursement Flow
- Loan must be in "approved" status
- Call disburse_loan()
- Update loan status to "disbursed" then "active"
- Post to core banking
- Create amortization schedule
- Create installments
Repayment Flow
- Record repayment via record_repayment()
- Update installment status (pending → partially_paid/paid)
- Update loan outstanding_balance
- Check if all installments paid → Mark loan as "repaid"
- Create LoanRepayment record with method (wallet/cash/cbs/etc)
Collection Flow
When loan becomes due:
1. Mark loan "overdue"
2. Create Collection Case
3. Assign to agent
4. Record collection actions (call/sms/email/visit)
5. Track outcomes (contacted/promise_to_pay/no_response/disputed)
6. Escalate if needed (soft → hard → legal)
Fraud Detection Flow
- Create fraud rules with conditions
- Group rules for combined evaluation
- Trigger on application/disbursement/repayment
- Generate fraud flags (open/escalated/auto_blocked/resolved)
- Create fraud cases for manual review
- Record decisions (confirmed fraud / false positive / inconclusive)
Additional Important Details
Dependencies
- All endpoints require
permission_required("") dependency
- Database session via
request.state.db_session
- Current user via
request.state.current_user
- Trace ID for async operations via
request.state.trace_id
Error Handling
- NotFoundException → HTTP 404
- ValidationException → HTTP 400
- CoreBankingException → HTTP 502
- LoanException → HTTP 500
- CustomException → Custom status code
Async Operations
apply_for_loan
get_loan_application
disburse_loan
record_repayment
reverse_repayment
add_collateral
get_loan_score
get_collection_case
- Fraud module operations
Database Relationships
- Loans relate to LoanApplications (1-to-1)
- Loans have many Installments & Repayments
- Loans can have Guarantors & Collaterals
- Applications have draft Guarantors & Collaterals
- Collection Cases track defaults (1 case per loan typically)
- Score Logs track all scoring events
- Fraud Flags & Cases track fraud detection
Key Enums
- LoanStatus: pending, approved, disbursed, active, overdue, repaid, defaulted, rejected, cancelled
- LoanApplicationStatus: pending, under_review, approved, rejected, conditionally_approved, cancelled, expired, blocked
- InterestType: flat, reducing, compound
- RepaymentMethod: wallet, cash, commission, cbs, manual
- CollateralType: vehicle, real_estate, cash_deposit, guarantor, equipment, other
- CollectionStatus: open, work_in_progress, resolved, written_off
- FraudAction: flag, block, review, notify
- RuleSeverity: low, medium, high, critical
Test Coverage Recommendations
Unit Tests
- LoanProduct CRUD operations
- LoanApplication validation & approval/rejection
- Loan creation & disbursement
- Amortization calculations (flat/reducing/compound)
- Repayment processing
- Guarantor & Collateral validation
- Blacklist operations
- Scoring service calls
- Collection case management
- Fraud rule creation & execution
Integration Tests
- Full application-to-loan workflow
- Fraud detection integration
- Collection workflow
- Repayment vs. installment reconciliation
- Status transition validations
- Pagination & filtering
Edge Cases
- Grace period handling
- Rounding in amortization
- Concurrent repayments
- Multiple guarantors/collaterals
- DTI ratio boundary conditions
- Blacklist expiration logic
- Fraud false positives