Compare commits

...

24 Commits

Author SHA1 Message Date
VivianDee ec5db19e20 [add]: loan max amount 2025-05-28 20:19:50 +01:00
VivianDee 729cc26698 [add]: loan ref to repayment 2025-05-28 20:05:38 +01:00
VivianDee 65472d3f07 [update]: loan status response 2025-05-27 02:02:22 +01:00
VivianDee 7a2ff6586f Update provide_loan.py 2025-05-26 13:01:27 +01:00
VivianDee 066ced55b0 Update provide_loan.py 2025-05-26 12:56:49 +01:00
CHIEFSOFT\ameye f6c98d9bfd repayment status 2025-05-25 06:33:46 -04:00
CHIEFSOFT\ameye 9e22e0fcf3 customer with loan listing 2025-05-24 07:41:20 -04:00
CHIEFSOFT\ameye c9aba07e9c Funct log 2025-05-24 07:10:44 -04:00
CHIEFSOFT\ameye 20c9a5c713 Remove logger 2025-05-24 07:08:03 -04:00
CHIEFSOFT\ameye 1cb0d88cc2 No need for new logger 2025-05-24 07:05:18 -04:00
CHIEFSOFT\ameye 3e9d5d4089 Looking for customer 2025-05-24 07:02:50 -04:00
CHIEFSOFT\ameye 2ae49ace86 fix call 2025-05-24 06:04:19 -04:00
CHIEFSOFT\ameye e9de001340 fix string 2025-05-24 05:50:17 -04:00
CHIEFSOFT\ameye 0bdc11423f Rack analysis 2025-05-24 05:37:21 -04:00
CHIEFSOFT\ameye a4ed936392 racResponse 2025-05-24 05:01:20 -04:00
CHIEFSOFT\ameye 1a315b1d80 SIMBRELLA_API_KEY 2025-05-24 04:52:58 -04:00
CHIEFSOFT\ameye 4c30f81bfd Updated racc check payload 2025-05-24 04:42:58 -04:00
CHIEFSOFT\ameye 916261fa94 Fix url 2025-05-24 04:34:11 -04:00
CHIEFSOFT\ameye 081b73a932 Added logger 2025-05-24 04:31:25 -04:00
CHIEFSOFT\ameye 6852986ce5 end poit config 2025-05-24 04:27:07 -04:00
CHIEFSOFT\ameye 0038c22577 SIMBRELLA_ENDPOINT_RAC_CHECKS 2025-05-24 04:22:40 -04:00
ameye 326ee87b13 Merge branch 'loan_min_amount' of DigiFi/digifi-BankToProductCore into master 2025-05-21 14:58:49 +00:00
VivianDee ca22ee86f7 [add]: Loan min amount 2025-05-21 15:49:11 +01:00
ameye aa033a50a3 Merge branch 'Offers_update' of DigiFi/digifi-BankToProductCore into master 2025-05-19 15:15:13 +00:00
15 changed files with 162 additions and 63 deletions
+1
View File
@@ -3,4 +3,5 @@ from enum import Enum
class LoanStatus(str, Enum):
PENDING = "pending"
ACTIVE = "active"
START_REPAY = "start_repay"
REPAID = "repaid"
+21 -2
View File
@@ -7,15 +7,26 @@ import logging
class SimbrellaIntegration:
BASE_URL = settings.SIMBRELLA_BASE_URL
ENDPOINT_RAC_CHECKS = settings.SIMBRELLA_ENDPOINT_RAC_CHECKS
@staticmethod
def rac_check(customer_id, account_id, transaction_id):
"""
Calls the RACCheck endpoit
"""
url = f"{SimbrellaIntegration.BASE_URL}/RACCheck"
url = f"{SimbrellaIntegration.BASE_URL}/{SimbrellaIntegration.ENDPOINT_RAC_CHECKS}"
logger.info(f"Contacting Rack Checks EndPoint: {str(url)}", exc_info=True)
payload = {
# {
# "transactionId": "T001",
# "fbnTransactionId": "Tr201712RK9232P115",
# "customerId": "CN621868",
# "accountId": "2017821799",
# "channel": "USSD",
# "countryCode": "NG"
# }
#
payload_old = {
"customerId": customer_id,
"accountId": account_id,
"transactionId": str(transaction_id),
@@ -35,6 +46,14 @@ class SimbrellaIntegration:
],
}
payload = {
"customerId": customer_id,
"accountId": account_id,
"transactionId": str(transaction_id),
"fbnTransactionId": f"FBN{transaction_id}",
"countryCode": "NG",
"channel": "USSD"
}
# logger.info(f"This is PayLoad: {str(payload)}", exc_info=True)
headers = {
+2 -2
View File
@@ -5,8 +5,8 @@ class RepaymentSchema(Schema):
type = fields.Str(required=False)
msisdn = fields.Str(required=False) #optional
debtId = fields.Str(required=True)
productId = fields.Str(required=True)
transactionId = fields.Str(required=True)
accountId = fields.Str(required=True)
customerId = fields.Str(required=True)
channel = fields.Str(required=True)
loanRef = fields.Str(required=True)
initiatedBy = fields.Str(required=False)
+5 -2
View File
@@ -75,12 +75,14 @@ class EligibilityCheckService(BaseService):
customer_id = customer_id,
account_id = account_id,
transaction_id = transaction.transaction_id,
data = response['RACResponse']
data = response['racResponse']
)
if not rac_check:
logger.error(f"Failed to save RACCheck")
return ResponseHelper.error(result_description="Failed to save RACCheck.")
rack_checks_response = response['racResponse']
# -----------------TIME FOR ANALYSIS TO REGISTER OFFER ----------------------
# eligible_offers = []
try:
@@ -88,7 +90,8 @@ class EligibilityCheckService(BaseService):
transaction_id=transactionId,
rac_check=rac_check,
validated_data=validated_data,
customer_id=customer_id
customer_id=customer_id,
rack_checks_response =rack_checks_response
)
except ValueError as ve:
logger.error(str(ve))
+4 -19
View File
@@ -29,38 +29,23 @@ class LoanStatusService(BaseService):
# Validate data
validated_data = LoanStatusService.validate_data(data, LoanStatusSchema())
customer_id = validated_data.get('customerId')
customer = Customer.get_customer(customer_id)
logger.info(f"Looking for customer *** {customer_id}")
customer = Customer.get_customer_with_loan_list(customer_id)
transactionId = validated_data.get('transactionId')
account_id = validated_data.get('accountId')
if(LoanStatusService.validate_account_ownership(account_id = account_id, customer_id = customer_id)):
# Get loans
loans = [loan.to_dict() for loan in customer.loans if loan.status == LoanStatus.ACTIVE]
transaction = LoanStatusService.log_transaction(validated_data = validated_data)
if not transaction:
logger.error(f"Failed to log transaction")
return ResponseHelper.error(result_description="Failed to log transaction.")
else:
return ResponseHelper.error(result_description="Invalid Customer or Account")
# loans = [
# {
# "debtId": "123456789",
# "loanDate": "2019-10-18 14:26:21.063",
# "dueDate": "2019-11-20 14:26:21.063",
# "currentLoanAmount": 8500,
# "initialLoanAmount": 10000,
# "defaultPenaltyFee": 0,
# "continuousFee": 0,
# "productId": "101"
# }
# ]
total_debt_amount = sum(
loan.get("currentLoanAmount") or 0
+26 -2
View File
@@ -32,12 +32,32 @@ class OfferAnalysis:
original_transaction = transaction_id
return transaction_offer, offer, eligible_amount, original_transaction
@staticmethod
def _analyze_rack_checks(rack_response):
# "racResponse": {
# "accountStatus": true,
# "bvnValidated": true,
# "creditBureauCheck": false,
# "crmsCheck": true,
# "hasLien": false,
# "hasPastDueLoan": false,
# "hasSalaryAccount": true,
# "isWhitelisted": true,
# "noBouncedCheck": true
# },
#
return 0
@staticmethod
def decide_offer(transaction_id, rac_check, validated_data, customer_id):
def decide_offer(transaction_id, rac_check, validated_data, customer_id, rack_checks_response):
eligible_offers = []
# if we have active offers - we have to feed off it
logger.info(f"LOOOOOOOOOOOOOOOOOO** {customer_id}")
logger.info(f"**RACK ANALYSIS** {customer_id}")
# Analyze Rack Checks
# _analyze_rack_checks(rack_checks_response) --> We need detail analysis
# we can now find the origin transactions
# Find the last loan - it will have original_transaction
@@ -63,6 +83,10 @@ class OfferAnalysis:
logger.info(f"sum_active_loans === > {sum_active_loans}")
real_eligible_amount = original_loan.eligible_amount - sum_active_loans
if real_eligible_amount < original_transaction_offer.min_amount:
logger.error(f"Max eligible amount ({real_eligible_amount}) is less than the minimum offer amount ({original_transaction_offer.min_amount}).")
raise ValueError("You are not eligible for a loan at this time.")
transaction_offer = TransactionOffer.create_transaction_offer(
customer_id=customer_id,
transaction_id=transaction_id,
+13
View File
@@ -60,6 +60,13 @@ class ProvideLoanService(BaseService):
except ValueError as ve:
logger.error(str(ve))
return ResponseHelper.error(result_description=str(ve))
if(amount < transaction_offer.min_amount):
return ResponseHelper.error(result_description="The amount is less than the minimum allowed transaction amount.")
elif amount > transaction_offer.max_amount:
return ResponseHelper.error(result_description="The amount is greater than the maximum allowed transaction amount.")
# transaction_offer_id = int(offer_id[5:]) # The last part is int
@@ -105,6 +112,9 @@ class ProvideLoanService(BaseService):
insurance = charges["insurance"]
vat = charges["vat"]
padded_id = str(transaction_id).zfill(12)
loan_ref = f"{padded_id}{channel}{offer.product_id}"
# Save the loan details
loan = Loan.create_loan(
@@ -122,6 +132,7 @@ class ProvideLoanService(BaseService):
eligible_amount=eligible_amount,
status = LoanStatus.ACTIVE,
tenor = offer.tenor,
reference = loan_ref
)
if not loan:
@@ -151,10 +162,12 @@ class ProvideLoanService(BaseService):
else:
return ResponseHelper.error(result_description="Invalid Customer or Account")
response_data = {
"requestId": request_id,
"transactionId": transaction_id,
"loanRef": loan_ref,
"customerId": customer_id,
"accountId": account_id,
"msisdn": customer.msisdn
+12 -13
View File
@@ -32,30 +32,30 @@ class RepaymentService(BaseService):
customer_id = validated_data.get('customerId')
request_id = validated_data.get('requestId')
loan_id = validated_data.get('debtId')
product_id = validated_data.get('productId')
account_id = validated_data.get('accountId')
customer = Customer.get_customer(customer_id)
loan_ref = validated_data.get('loanRef')
# customer = Customer.get_customer_with_loan_list(customer_id)
transaction_id = validated_data.get('transactionId')
initiated_by = validated_data.get('initiatedBy')
if(RepaymentService.validate_account_ownership(account_id = account_id, customer_id = customer_id)):
# Check loan exists
loan = Loan.get_customer_loan(loan_id = loan_id, customer_id = customer_id)
# Save the repayment details
repayment = Repayment.create_repayment(
customer_id = customer_id,
loan_id = loan_id,
product_id = product_id,
transaction_id=transaction_id
loan = loan,
transaction_id = transaction_id
)
if not repayment:
logger.error(f"Failed to save repayment details")
return ResponseHelper.error(result_description="Failed to save repayment details.")
#Update Loan status
Loan.update_status(loan_id = loan_id, status = LoanStatus.REPAID)
Loan.update_status(loan_id = loan_id, status = LoanStatus.START_REPAY) # repay started bu user
transaction = RepaymentService.log_transaction(validated_data = validated_data)
if not transaction:
@@ -63,14 +63,13 @@ class RepaymentService(BaseService):
return ResponseHelper.error(result_description="Failed to log transaction.")
else:
return ResponseHelper.error(result_description="Invalid Customer or Account")
# Simulated processing logic
response_data = {
"transactionId": transaction_id,
"customerId": customer_id,
"productId": product_id,
"productId": loan.product_id,
"loanRef": loan_ref,
"debtId": loan_id
}
+7
View File
@@ -59,6 +59,13 @@ class SelectOfferService(BaseService):
db.session.flush()
if amount < offer.min_amount:
return ResponseHelper.error(result_description="The amount is less than the minimum allowed offer amount.")
elif amount > offer.max_amount:
return ResponseHelper.error(result_description="The amount is greater than the maximum allowed offer amount.")
charges = SelectOfferService.calculate_charges(offer, amount)
upfront_payment = charges["upfront_payment"]
total_amount = charges["total_amount"]
+17 -4
View File
@@ -1,7 +1,6 @@
import os
from datetime import timedelta
class Config:
"""Base configuration for Flask app"""
@@ -9,20 +8,19 @@ class Config:
API_URL = os.getenv("API_URL", "/swagger.json")
DEBUG = True
VALID_APP_ID = os.getenv("VALID_APP_ID", "app1")
VALID_API_KEY = os.getenv("VALID_API_KEY", "test-api-key-12345")
BASIC_AUTH_USERNAME = os.environ.get("BASIC_AUTH_USERNAME", "user")
BASIC_AUTH_PASSWORD = os.environ.get("BASIC_AUTH_PASSWORD", "password")
# Database Configuration
DATABASE_USER = os.environ.get("DATABASE_USER")
DATABASE_PASSWORD = os.environ.get("DATABASE_PASSWORD")
DATABASE_HOST = os.environ.get("DATABASE_HOST")
DATABASE_PORT = os.environ.get("DATABASE_PORT", 10532)
DATABASE_NAME = os.environ.get("DATABASE_NAME")
# Database Connection
SQLALCHEMY_DATABASE_URI = f"postgresql+psycopg2://{DATABASE_USER}:{DATABASE_PASSWORD}@{DATABASE_HOST}:{DATABASE_PORT}/{DATABASE_NAME}"
SQLALCHEMY_TRACK_MODIFICATIONS = False
SIMBRELLA_BASE_URL = os.getenv("SIMBRELLA_BASE_URL", "http://127.0.0.1:6337")
JWT_SECRET_KEY = os.getenv("JWT_SECRET_KEY", "secret-key")
@@ -34,5 +32,20 @@ class Config:
# KAFKA_BROKER = 'dev-events.simbrellang.net:9085'
KAFKA_BROKER = os.getenv("KAFKA_BROKER", "dev-events.simbrellang.net:9085")
# SIMBRELLA_ENDPOINT_RAC_CHECKS = os.getenv("SIMBRELLA_ENDPOINT_RAC_CHECKS", "RACCheck")
VALID_APP_ID = os.getenv("SIMBRELLA_APP_ID", "app1")
VALID_API_KEY = os.getenv("SIMBRELLA_API_KEY", "testtest-api-key-12345")
SIMBRELLA_BASE_URL = os.getenv("SIMBRELLA_BASE_URL", "http://127.0.0.1:6337")
SIMBRELLA_ENDPOINT_RAC_CHECKS = os.getenv("SIMBRELLA_ENDPOINT_RAC_CHECKS","api/rac-check")
RAC_RESULT_accountStatus = os.environ.get("RAC_RESULT_accountStatus", "true")
RAC_RESULT_bvnValidated = os.environ.get("RAC_RESULT_bvnValidated", "true")
RAC_RESULT_creditBureauCheck = os.environ.get("RAC_RESULT_creditBureauCheck", "false")
RAC_RESULT_crmsCheck = os.environ.get("RAC_RESULT_crmsCheck", "true")
RAC_RESULT_hasLien = os.environ.get("RAC_RESULT_hasLien", "false")
RAC_RESULT_hasPastDueLoan = os.environ.get("RAC_RESULT_hasPastDueLoan", "false")
RAC_RESULT_hasSalaryAccount = os.environ.get("RAC_RESULT_hasSalaryAccount", "true")
RAC_RESULT_isWhitelisted = os.environ.get("RAC_RESULT_isWhitelisted", "true")
RAC_RESULT_noBouncedCheck = os.environ.get("RAC_RESULT_noBouncedCheck", "true")
settings = Config()
+8 -1
View File
@@ -1,9 +1,12 @@
from datetime import datetime, timezone
from sqlalchemy.orm import relationship
#
# from app.api.services.offer_analysis import logger
from app.extensions import db
from app.models.account import Account
from sqlalchemy.exc import IntegrityError
from sqlalchemy.sql import func
# from app.utils.logger import logger
class Customer(db.Model):
__tablename__ = 'customers'
@@ -45,6 +48,10 @@ class Customer(db.Model):
def create_customer(cls, id, msisdn, country_code, account_id, account_type='savings'):
if cls.query.filter_by(id=id).first():
raise ValueError("Customer already exists")
elif Account.query.filter_by(id=account_id).first():
raise ValueError("Account already exists")
elif cls.query.filter_by(msisdn=msisdn).first():
raise ValueError("msisdn already exists")
# Create the customer
customer = cls(
@@ -69,7 +76,7 @@ class Customer(db.Model):
return customer
@classmethod
def get_customer(cls, customer_id):
def get_customer_with_loan_list(cls, customer_id):
"""
Get customer by ID.
"""
+8
View File
@@ -1,4 +1,5 @@
from datetime import datetime, timezone
from itertools import product
from app.extensions import db
from app.models.customer import Customer
from app.models.account import Account
@@ -43,6 +44,7 @@ class Loan(db.Model):
eligible_amount = db.Column(db.Float, nullable=True, default=0.0)
disburse_date = db.Column(db.DateTime, nullable=True)
disburse_verify = db.Column(db.DateTime, nullable=True)
reference = db.Column(db.String(50), nullable=True)
customer = relationship(
"Customer",
@@ -82,6 +84,7 @@ class Loan(db.Model):
installment_amount,
tenor,
eligible_amount,
reference,
status = "pending",
):
# Check if customer exists
@@ -110,6 +113,7 @@ class Loan(db.Model):
tenor = tenor,
status = status,
eligible_amount =eligible_amount,
reference = reference,
created_at=datetime.now(timezone.utc),
updated_at=datetime.now(timezone.utc)
)
@@ -213,6 +217,9 @@ class Loan(db.Model):
"""
return {
'debtId': self.id,
'transactionId': self.transaction_id,
'loanRef': self.reference,
'productId': self.product_id,
'initialLoanAmount': self.initial_loan_amount,
'currentLoanAmount': self.current_loan_amount,
'defaultPenaltyFee': self.default_penalty_fee,
@@ -222,6 +229,7 @@ class Loan(db.Model):
'repaymentAmount': self.repayment_amount,
'installmentAmount': self.installment_amount,
'status': self.status,
'tenor': self.tenor,
'dueDate': self.due_date.isoformat() if self.due_date else None,
'loanDate': self.created_at.isoformat() if self.created_at else None,
}
+4 -12
View File
@@ -23,25 +23,17 @@ class Repayment(db.Model):
transaction_id = db.Column(db.String(50), nullable=True)
@classmethod
def create_repayment(cls, customer_id, loan_id, product_id, transaction_id):
# Check customer exists
if not Customer.is_valid_customer(customer_id):
raise ValueError("Invalid customer")
# Check loan exists
loan = Loan.get_customer_loan(loan_id = loan_id, customer_id = customer_id)
def create_repayment(cls, customer_id, loan, transaction_id):
# Check that the loan is active
if loan.status != LoanStatus.ACTIVE:
if loan.status not in [LoanStatus.ACTIVE, LoanStatus.START_REPAY]:
raise ValueError(f"Repayment cannot be processed. Loan status: ({loan.status})")
repayment = cls(
customer_id=customer_id,
loan_id=loan_id,
product_id=product_id,
loan_id=loan.id,
product_id=loan.product_id,
transaction_id = transaction_id,
created_at=datetime.now(timezone.utc),
updated_at=datetime.now(timezone.utc)
+2 -6
View File
@@ -9,10 +9,6 @@
"type": "string",
"example": "10"
},
"productId": {
"type": "string",
"example": "101"
},
"transactionId": {
"type": "string",
"example": "20171209232115"
@@ -21,9 +17,9 @@
"type": "string",
"example": "CID0000025585"
},
"channel": {
"loanRef": {
"type": "string",
"example": "USSD"
"example": "Trx5847365252USSD3MPC"
},
"accountId": {
"type": "string",
+32
View File
@@ -0,0 +1,32 @@
"""empty message
Revision ID: b3a5e10bc77e
Revises: e8dd9b841ad7
Create Date: 2025-05-27 01:52:48.538333
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = 'b3a5e10bc77e'
down_revision = 'e8dd9b841ad7'
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
with op.batch_alter_table('loans', schema=None) as batch_op:
batch_op.add_column(sa.Column('reference', sa.String(length=50), nullable=True))
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
with op.batch_alter_table('loans', schema=None) as batch_op:
batch_op.drop_column('reference')
# ### end Alembic commands ###