Compare commits
22 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| ec5db19e20 | |||
| 729cc26698 | |||
| 65472d3f07 | |||
| 7a2ff6586f | |||
| 066ced55b0 | |||
| f6c98d9bfd | |||
| 9e22e0fcf3 | |||
| c9aba07e9c | |||
| 20c9a5c713 | |||
| 1cb0d88cc2 | |||
| 3e9d5d4089 | |||
| 2ae49ace86 | |||
| e9de001340 | |||
| 0bdc11423f | |||
| a4ed936392 | |||
| 1a315b1d80 | |||
| 4c30f81bfd | |||
| 916261fa94 | |||
| 081b73a932 | |||
| 6852986ce5 | |||
| 0038c22577 | |||
| 326ee87b13 |
@@ -3,4 +3,5 @@ from enum import Enum
|
||||
class LoanStatus(str, Enum):
|
||||
PENDING = "pending"
|
||||
ACTIVE = "active"
|
||||
START_REPAY = "start_repay"
|
||||
REPAID = "repaid"
|
||||
@@ -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 = {
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -64,6 +64,9 @@ class ProvideLoanService(BaseService):
|
||||
|
||||
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
|
||||
|
||||
@@ -109,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(
|
||||
@@ -126,6 +132,7 @@ class ProvideLoanService(BaseService):
|
||||
eligible_amount=eligible_amount,
|
||||
status = LoanStatus.ACTIVE,
|
||||
tenor = offer.tenor,
|
||||
reference = loan_ref
|
||||
)
|
||||
|
||||
if not loan:
|
||||
@@ -155,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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -59,8 +59,11 @@ class SelectOfferService(BaseService):
|
||||
|
||||
db.session.flush()
|
||||
|
||||
if(amount < offer.min_amount):
|
||||
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)
|
||||
|
||||
+17
-4
@@ -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()
|
||||
|
||||
@@ -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.
|
||||
"""
|
||||
|
||||
@@ -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
@@ -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)
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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 ###
|
||||
Reference in New Issue
Block a user