Skip to content

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

  1. Route Paths & HTTP Methods
  2. Pydantic Schema Classes
  3. SQLAlchemy Models
  4. Controller Functions
  5. 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:

  • application_id: str

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:

  • loan_id: int

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:

  • repayment_id: int

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:

  • guarantor_id: str

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:

  • user_id: str

LoanScoreFilters Fields:

  • Standard pagination (page_number, page_size)
  • Date range filters (date_from, date_to)
  • Loan/applicant/application IDs

LoanScoreID Fields:

  • score_log_id: int

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:

  • Status update payload

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:

  • soft
  • hard
  • legal

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:

  • flag
  • block
  • review
  • notify

FraudContext Enum:

  • loan_application
  • loan_disbursement
  • repayment

FraudCategory Enum:

  • identity
  • behavior
  • document
  • application
  • financial

RuleSeverity Enum:

  • low
  • medium
  • high
  • critical

LogicalOperator Enum:

  • and
  • or

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

  • Fetches blacklist entry

list_loan_blacklist(db, current_user, filters) → LoanBlacklistFetchResponse

  • Lists blacklist entries

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

  • Fetches case by ID

list_cases(db, filters) → CollectionCaseFetchResponse

  • Lists collection cases with filters
  • Paginated

record_action(db, payload, performed_by) → CollectionActionResponse

  • 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

  • Gets group by ID (async)

edit_fraud_rule_group(db, payload, current_user_id) → FraudRuleGroupResponse

  • Edits rule group

toggle_fraud_rule_group_status(db, group_id, is_active, current_user_id) → FraudRuleGroupResponse

  • Toggles group status

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

  • Confirms fraud flag

escalate_fraud_flag(db, payload) → LoanFraudFlagResponse

  • Escalates fraud flag

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

  1. Application Submission → LoanApplicationCreate
  2. Validation → Product active, amount within bounds, not blacklisted
  3. Scoring → Run internal/external credit score
  4. Fraud Check → Run fraud rules on application
  5. Auto-Decision → Approve/Conditional/Manual review based on product config
  6. Loan Creation → If approved, create Loan from approval

Loan Disbursement Flow

  1. Loan must be in "approved" status
  2. Call disburse_loan()
  3. Update loan status to "disbursed" then "active"
  4. Post to core banking
  5. Create amortization schedule
  6. Create installments

Repayment Flow

  1. Record repayment via record_repayment()
  2. Update installment status (pending → partially_paid/paid)
  3. Update loan outstanding_balance
  4. Check if all installments paid → Mark loan as "repaid"
  5. 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

  1. Create fraud rules with conditions
  2. Group rules for combined evaluation
  3. Trigger on application/disbursement/repayment
  4. Generate fraud flags (open/escalated/auto_blocked/resolved)
  5. Create fraud cases for manual review
  6. 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