100 Commits

Author SHA1 Message Date
VivianDee 38499d67ac Update .env.remote.example 2025-07-09 12:13:39 +01:00
VivianDee 3f40385128 Update .env.local.example 2025-07-06 10:03:36 +01:00
VivianDee 4aaf07748e [add]: oracle migration 2025-07-06 09:53:52 +01:00
ameye 39d9abf5ec Merge branch 'test' of DigiFi/digifi-EventManager into master 2025-07-04 10:35:05 +00:00
Chinenye Nmoh 0000907698 fixed restart error 2025-07-03 15:37:53 +01:00
ameye 501d0f2703 Merge branch 'test' of DigiFi/digifi-EventManager into master 2025-06-30 00:30:14 +00:00
Chinenye Nmoh 1e338c1a50 added catch for 404 2025-06-29 20:12:23 +01:00
ameye d79b71da58 Merge branch 'test' of DigiFi/digifi-EventManager into master 2025-06-26 20:11:57 +00:00
Chinenye Nmoh 1bf4d61554 debugging 2025-06-26 18:26:57 +01:00
Chinenye Nmoh 79109af695 balance 2025-06-26 13:36:49 +01:00
Chinenye Nmoh 9feab46016 balance 2025-06-26 13:31:51 +01:00
ameye d863285e9a Merge branch 'test' of DigiFi/digifi-EventManager into master 2025-06-25 14:59:07 +00:00
Chinenye Nmoh 4a949d8b7e corrected update loan 2025-06-25 14:10:30 +01:00
ameye c4a89a0bd0 Merge branch 'calculat_loan_balance' of DigiFi/digifi-EventManager into master 2025-06-25 08:39:01 +00:00
ameye bb21a33014 Merge branch 'test' of DigiFi/digifi-EventManager into master 2025-06-25 08:38:38 +00:00
VivianDee 9831395e2a [add]: balance when calculating loan total 2025-06-25 08:23:11 +01:00
Chinenye Nmoh 340132d8ad worked on balance 2025-06-24 22:21:58 +01:00
ameye aa408cc720 Merge branch 'test' of DigiFi/digifi-EventManager into master 2025-06-23 10:42:04 +00:00
Chinenye Nmoh 35e4580313 changed balance to 2 decimal place 2025-06-23 11:09:28 +01:00
CHIEFSOFT\ameye 3d62cbc157 Loan status 2025-06-22 21:50:18 -04:00
CHIEFSOFT\ameye 3785196fb7 salary process apart 2025-06-21 11:55:54 -04:00
CHIEFSOFT\ameye 8249a1b6d8 kafka collect error 2025-06-21 11:11:45 -04:00
CHIEFSOFT\ameye caa77191c7 balace onm payment 2025-06-21 10:43:31 -04:00
CHIEFSOFT\ameye 3f1d87b88a Loan status fix 2025-06-20 22:31:15 -04:00
CHIEFSOFT\ameye 42a3b0fd73 except Exception as e: 2025-06-20 21:55:07 -04:00
CHIEFSOFT\ameye 43055ad010 LoanStatus 2025-06-20 21:51:43 -04:00
CHIEFSOFT\ameye 114a089bdb ["LoanStatus"] 2025-06-20 21:37:20 -04:00
CHIEFSOFT\ameye 48fb58c6ab repayment_data 2025-06-20 21:35:05 -04:00
CHIEFSOFT\ameye 306221f502 repayment_data.LoanStatus 2025-06-20 21:27:53 -04:00
CHIEFSOFT\ameye a310709c5e repayment bug 2025-06-20 21:24:14 -04:00
CHIEFSOFT\ameye a1b1caf5d7 variable bug 2025-06-20 21:22:06 -04:00
CHIEFSOFT\ameye b88a09376b loid 2025-06-20 21:19:33 -04:00
CHIEFSOFT\ameye ee077fb380 loan id 2025-06-20 20:52:08 -04:00
CHIEFSOFT\ameye 4e23ea4e91 customerId 2025-06-20 20:48:44 -04:00
CHIEFSOFT\ameye cb4383c10b loan_dict 2025-06-20 20:46:49 -04:00
CHIEFSOFT\ameye e9c29a8743 "customerId": loan_dict["customer_id"], 2025-06-20 20:45:08 -04:00
CHIEFSOFT\ameye 08a0c8a933 Repayment data 2025-06-20 20:39:37 -04:00
CHIEFSOFT\ameye 6200fc4dba fix ID 2025-06-20 20:23:05 -04:00
CHIEFSOFT\ameye 0d2ce16ff5 enums 2025-06-20 15:48:29 -04:00
CHIEFSOFT\ameye 7b21140b39 Repayment updates 2025-06-20 15:18:04 -04:00
ameye 190cd6611a Merge branch 'add_total_amount' of DigiFi/digifi-EventManager into master 2025-06-20 16:28:39 +00:00
VivianDee c550c8c356 [add]: loan total amount 2025-06-20 15:58:53 +01:00
ameye 3cf6c94786 Merge branch 'test' of DigiFi/digifi-EventManager into master 2025-06-20 13:30:08 +00:00
Chinenye Nmoh 79b22e6d4f changed amount to salaryAmount 2025-06-20 13:49:10 +01:00
ameye 1d0409d072 Merge branch 'test' of DigiFi/digifi-EventManager into master 2025-06-20 10:52:31 +00:00
Chinenye Nmoh bcd9513a10 added loop 2025-06-20 11:47:21 +01:00
Chinenye Nmoh 684833bd66 added loop 2025-06-19 23:23:06 +01:00
Chinenye Nmoh 7fbb659fc6 added salary table 2025-06-19 21:30:21 +01:00
vivian.d.simbrellang.com 3be765bf41 Merge branch 'pending_salaries' of DigiFi/digifi-EventManager into master 2025-06-19 12:25:26 +00:00
VivianDee 31e38da473 [add]: pending salaries 2025-06-19 12:49:15 +01:00
CHIEFSOFT\ameye a105cf2a99 collect_loan_user_salary_detect 2025-06-19 06:54:54 -04:00
CHIEFSOFT\ameye cc826a0469 added return 2025-06-19 06:52:07 -04:00
CHIEFSOFT\ameye 45678d8288 start added 2025-06-19 06:46:49 -04:00
CHIEFSOFT\ameye 11aab30a19 salaryAmount 2025-06-19 06:09:52 -04:00
CHIEFSOFT\ameye 500e749acb Reafctor import 2025-06-18 23:29:09 -04:00
ameye 405c837499 Merge branch 'test' of DigiFi/digifi-EventManager into master 2025-06-18 22:05:28 +00:00
Chinenye Nmoh ad25be1856 added salary table 2025-06-18 22:25:45 +01:00
CHIEFSOFT\ameye 8755ed0825 added URL 2025-06-18 14:03:41 -04:00
CHIEFSOFT\ameye e8e52480b1 swagger update 2025-06-18 12:21:56 -04:00
ameye 36cddd9327 Merge branch 'test' of DigiFi/digifi-EventManager into master 2025-06-17 16:17:25 +00:00
Chinenye Nmoh 9158a8c3cd added endpoint to swagger 2025-06-17 15:35:07 +01:00
CHIEFSOFT\ameye b574c985db collect_loan_user_initiated 2025-06-17 07:18:11 -04:00
CHIEFSOFT\ameye e87ceabe47 salary detect 2025-06-17 07:11:11 -04:00
CHIEFSOFT\ameye 68669784de Added Call Paths 2025-06-17 06:58:24 -04:00
ameye 257c84e4c1 Merge branch 'test' of DigiFi/digifi-EventManager into master 2025-06-16 20:32:56 +00:00
Chinenye Nmoh 87a876252d added repayment data 2025-06-16 19:50:24 +01:00
ameye e0ca6f0e1c Merge branch 'test' of DigiFi/digifi-EventManager into master 2025-06-10 18:03:30 +00:00
Chinenye Nmoh fc19f39378 added repayment data to db 2025-06-10 18:22:50 +01:00
ameye 217b63efcf Merge branch 'test' of DigiFi/digifi-EventManager into master 2025-06-06 23:05:47 +00:00
Chinenye Nmoh b2816b947e added collect loan 2025-06-06 14:10:58 -04:00
Chinenye Nmoh 321de4bd6f added collect loan 2025-06-06 14:02:35 -04:00
Chinenye Nmoh c0d5893a9b added collect loan 2025-06-06 13:56:50 -04:00
ameye 08e53f4834 Merge branch 'test' of DigiFi/digifi-EventManager into master 2025-06-05 19:54:24 +00:00
Chinenye Nmoh b8a40bb638 added verify date 2025-06-05 20:03:18 +01:00
CHIEFSOFT\ameye 8c5be70a02 added 2 endpoint 2025-06-05 10:50:19 -04:00
CHIEFSOFT\ameye 50769f7faf ref 2025-06-05 07:13:35 -04:00
ameye 67363ff6e5 Merge branch 'test' of DigiFi/digifi-EventManager into master 2025-06-04 22:58:22 +00:00
Chinenye Nmoh 680ec9e9e0 made loan data dynamic and populated result fields 2025-06-04 18:55:53 +01:00
ameye 5bbcaefe7f Merge branch 'test' of DigiFi/digifi-EventManager into master 2025-06-03 19:13:31 +00:00
Chinenye Nmoh 15e012c071 commited loan date 2025-06-03 17:14:02 +01:00
Chinenye Nmoh 2872f6c75c returned disbured_date 2025-06-03 11:16:26 +01:00
CHIEFSOFT\ameye 4619f83d46 Fix default topics 2025-06-02 12:08:19 -04:00
CHIEFSOFT\ameye 9fc3104c79 Verify transation s 2025-05-31 21:44:52 -04:00
CHIEFSOFT\ameye 1a4f0c1332 python-dateutil 2025-05-30 20:50:20 -04:00
CHIEFSOFT\ameye ef2dbc3800 Include 2025-05-30 20:47:53 -04:00
CHIEFSOFT\ameye cf2e1fc2a7 get loan 2025-05-30 20:36:44 -04:00
CHIEFSOFT\ameye 8c50e5a98f loand ids 2025-05-30 20:27:05 -04:00
CHIEFSOFT\ameye 03a30577d7 collect call 2025-05-30 18:15:31 -04:00
CHIEFSOFT\ameye c8685b7b8c Repay data 2025-05-30 18:07:15 -04:00
CHIEFSOFT\ameye fb28fd9bbf Loan data logs 2025-05-30 17:58:18 -04:00
CHIEFSOFT\ameye 240f9c3d32 Url config 2025-05-30 17:50:08 -04:00
CHIEFSOFT\ameye 0591cd6073 Api url 2025-05-30 17:37:57 -04:00
CHIEFSOFT\ameye fad8315d5e Updated the loan service end 2025-05-30 17:26:00 -04:00
ameye 38c7bf16d4 Merge branch 'EVN-Dsibursement-data-clean-up-' of DigiFi/digifi-EventManager into master 2025-04-26 23:48:59 +00:00
ameye 8760b9ba93 Started cleaning disbursement data 2025-04-26 19:47:22 -04:00
ameye a6054e5a26 Merge branch 'oluyemi' of DigiFi/digifi-EventManager into master 2025-04-17 21:07:23 +00:00
ameye 6a68041b08 Merge branch 'oluyemi' of DigiFi/digifi-EventManager into master 2025-04-16 20:05:34 +00:00
ameye eca34b77ab Merge branch 'oluyemi' of DigiFi/digifi-EventManager into master 2025-04-16 09:42:30 +00:00
ameye c39d68a28f Merge branch 'oluyemi' of DigiFi/digifi-EventManager into master 2025-04-15 20:44:47 +00:00
ameye 5a5b418354 Merge branch 'oluyemi' of DigiFi/digifi-EventManager into master 2025-04-15 16:06:06 +00:00
24 changed files with 1480 additions and 107 deletions
+11 -5
View File
@@ -3,8 +3,14 @@ KAFKA_TIMEOUT=1000.0
KAFKA_BROKER="10.20.30.50:9092" KAFKA_BROKER="10.20.30.50:9092"
KAFKA_TOPICS=PROCESS_PAYMENT,LOAN_REPAYMENT KAFKA_TOPICS=PROCESS_PAYMENT,LOAN_REPAYMENT
DATABASE_USER=firstadvance # DATABASE_USER=firstadvance
DATABASE_PASSWORD=FirstAdvance! # DATABASE_PASSWORD=FirstAdvance!
DATABASE_HOST=10.20.30.60 # DATABASE_HOST=10.20.30.60
DATABASE_PORT=5432 # DATABASE_PORT=5432
DATABASE_NAME=firstadvancedev # DATABASE_NAME=firstadvancedev
DATABASE_USER=system
DATABASE_PASSWORD=FIRSTADV_PASS
DATABASE_HOST=10.10.33.65
DATABASE_PORT=1521
DATABASE_SID=FREE
+11 -5
View File
@@ -3,8 +3,14 @@ KAFKA_TIMEOUT=1000.0
KAFKA_BROKER="dev-events.simbrellang.net:9085" KAFKA_BROKER="dev-events.simbrellang.net:9085"
KAFKA_TOPICS=PROCESS_PAYMENT,LOAN_REPAYMENT KAFKA_TOPICS=PROCESS_PAYMENT,LOAN_REPAYMENT
DATABASE_USER=firstadvance # DATABASE_USER=firstadvance
DATABASE_PASSWORD=FirstAdvance! # DATABASE_PASSWORD=FirstAdvance!
DATABASE_HOST=dev-data.simbrellang.net # DATABASE_HOST=dev-data.simbrellang.net
DATABASE_PORT=10532 # DATABASE_PORT=10532
DATABASE_NAME=firstadvancedev # DATABASE_NAME=firstadvancedev
DATABASE_USER=system
DATABASE_PASSWORD=FIRSTADV_PASS
DATABASE_HOST=10.10.33.65
DATABASE_PORT=1521
DATABASE_SID=FREE
+1 -1
View File
@@ -18,4 +18,4 @@ ENV FLASK_APP=app.py
ENV FLASK_RUN_HOST=0.0.0.0 ENV FLASK_RUN_HOST=0.0.0.0
# Run the application # Run the application
CMD ["gunicorn", "-w", "4", "-b", "0.0.0.0:5000", "wsgi:wsgi_app"] CMD ["gunicorn", "-w", "4", "-b", "0.0.0.0:5000", "--timeout", "120", "wsgi:wsgi_app"]
+15 -4
View File
@@ -6,12 +6,11 @@ class Config:
"""Base configuration for Flask app""" """Base configuration for Flask app"""
SECRET_KEY = os.getenv("SECRET_KEY", "supersecretkey") SECRET_KEY = os.getenv("SECRET_KEY", "supersecretkey")
BANK_CALL_BASE_URL = "https://bank-emulator.dev.simbrellang.net"
JWT_SECRET_KEY = os.getenv("JWT_SECRET_KEY", "your_jwt_secret") JWT_SECRET_KEY = os.getenv("JWT_SECRET_KEY", "your_jwt_secret")
DEBUG = True DEBUG = True
KAFKA_BROKER = os.getenv("KAFKA_BROKER", "dev-events.simbrellang.net:9085") KAFKA_BROKER = os.getenv("KAFKA_BROKER", "dev-events.simbrellang.net:9085")
KAFKA_TOPICS = [topic.strip() for topic in os.getenv("KAFKA_TOPICS", "").split(",") if topic.strip()] KAFKA_TOPICS = [topic.strip() for topic in os.getenv("KAFKA_TOPICS", "PROCESS_PAYMENT,LOAN_REPAYMENT").split(",") if topic.strip()]
KAFKA_TIMEOUT = float( os.getenv("KAFKA_TIMEOUT", 1000.0) ) KAFKA_TIMEOUT = float( os.getenv("KAFKA_TIMEOUT", 1000.0) )
JWT_ACCESS_TOKEN_EXPIRES = os.getenv("JWT_ACCESS_TOKEN_EXPIRES", timedelta(hours=1)) JWT_ACCESS_TOKEN_EXPIRES = os.getenv("JWT_ACCESS_TOKEN_EXPIRES", timedelta(hours=1))
@@ -20,7 +19,7 @@ class Config:
) )
BANK_CALL_APP_ID = os.getenv("BANK_CALL_APP_ID", "app1") BANK_CALL_APP_ID = os.getenv("BANK_CALL_APP_ID", "app1")
BANK_CALL_API_KEY = os.getenv("BANK_CALL_API_KEY", "test-api-key-12345") BANK_CALL_API_KEY = os.getenv("BANK_CALL_API_KEY", "testtest-api-key-12345")
BANK_CALL_BASIC_AUTH_USERNAME = os.environ.get( BANK_CALL_BASIC_AUTH_USERNAME = os.environ.get(
"BANK_CALL_BASIC_AUTH_USERNAME", "user" "BANK_CALL_BASIC_AUTH_USERNAME", "user"
) )
@@ -33,10 +32,22 @@ class Config:
DATABASE_HOST = os.getenv("DATABASE_HOST") DATABASE_HOST = os.getenv("DATABASE_HOST")
DATABASE_NAME = os.getenv("DATABASE_NAME") DATABASE_NAME = os.getenv("DATABASE_NAME")
DATABASE_PORT = os.getenv("DATABASE_PORT", 10532) DATABASE_PORT = os.getenv("DATABASE_PORT", 10532)
DATABASE_SID = os.environ.get("DATABASE_SID", "FREE")
DNS = f"(DESCRIPTION=(ADDRESS=(PROTOCOL=TCP)(HOST={DATABASE_HOST})(PORT={DATABASE_PORT}))(CONNECT_DATA=(SID={DATABASE_SID})))"
SQLALCHEMY_DATABASE_URI = f"postgresql+psycopg2://{DATABASE_USER}:{DATABASE_PASSWORD}@{DATABASE_HOST}:{DATABASE_PORT}/{DATABASE_NAME}"
SQLALCHEMY_DATABASE_URI = (f"oracle+oracledb://{DATABASE_USER}:{DATABASE_PASSWORD}@{DNS}")
# SQLALCHEMY_DATABASE_URI = f"postgresql+psycopg2://{DATABASE_USER}:{DATABASE_PASSWORD}@{DATABASE_HOST}:{DATABASE_PORT}/{DATABASE_NAME}"
SQLALCHEMY_TRACK_MODIFICATIONS = False SQLALCHEMY_TRACK_MODIFICATIONS = False
# SQLALCHEMY_ECHO = True
BANK_CALL_BASE_URL = os.getenv("BANK_CALL_BASE_URL", "https://bank-emulator.dev.simbrellang.net/api")
BANK_CALL_SMS_BASE_URL= os.getenv("BANK_CALL_SMS_BASE_URL","https://first-advance-middleware-develop.fbn-devops-dev-asenv.appserviceenvironment.net/SMS")
BANK_CALL_DISBURSE_LOAN_ENDPOINT = os.getenv("BANK_CALL_DISBURSE_LOAN_ENDPOINT","/DisburseLoan")
BANK_CALL_COLLECT_LOAN_ENDPOINT = os.getenv("BANK_CALL_COLLECT_LOAN_ENDPOINT","/CollectLoan")
BANK_CALL_TRANSACTION_VERIFY = os.getenv("BANK_CALL_TRANSACTION_VERIFY", "/TransactionVerify")
TEST_NO = os.getenv("TEST_NO", "2347038224367")
settings = Config() settings = Config()
+2
View File
@@ -0,0 +1,2 @@
from .transaction_type import TransactionType
from .loan_status import LoanStatus
+8
View File
@@ -0,0 +1,8 @@
from enum import Enum
class LoanStatus(str, Enum):
PENDING = "pending"
ACTIVE = "active"
ACTIVE_PARTIAL = "active_partial"
START_REPAY = "start_repay"
REPAID = "repaid"
+10
View File
@@ -0,0 +1,10 @@
from enum import Enum
class TransactionType(str, Enum):
ELIGIBILITY_CHECK = "eligibility_check"
CUSTOMER_CONSENT = "customer_consent"
LOAN_STATUS = "loan_status"
NOTIFICATION_CALLBACK = "notification_callback"
PROVIDE_LOAN = "provide_loan"
REPAYMENT = "repayment"
SELECT_OFFER = "select_offer"
+9 -3
View File
@@ -127,10 +127,15 @@ class KafkaIntegration:
logger.info(f"Calling disbursement service with message: {message}") logger.info(f"Calling disbursement service with message: {message}")
try: try:
response = SimbrellaClient.disbursement(message) response = SimbrellaClient.disburse_loan(message)
logger.info( logger.info(
f"Successfully sent message to disbursement service: {response}" f"Successfully sent message to disbursement service: {response}"
) )
# LoanService.set_disbursement_date(loan_id=loan_data['debtId'],
# customer_id=customerId) # must mark it on way out
#
except Exception as e: except Exception as e:
logger.info(f"Failed to call disbursement service: {e}") logger.info(f"Failed to call disbursement service: {e}")
#raise #raise
@@ -141,10 +146,11 @@ class KafkaIntegration:
logger.info(f"Calling collect_loan service with message: {message}") logger.info(f"Calling collect_loan service with message: {message}")
try: try:
response = SimbrellaClient.collect_loan(message) #Calling CollectLoan endpoint with data: {'transactionId': 'TRCVIC85641527829', 'customerId': 'ZX48440946', 'productId': 'AMPC', 'loanRef': 'TRCVIC85641527829USSDAMPC', 'debtId': '014231'}
response = SimbrellaClient.collect_loan_user_initiated(message)
logger.info( logger.info(
f"Successfully sent message to collect_loan service: {response}" f"Successfully sent message to collect_loan service: {response}"
) )
except Exception as e: except Exception as e:
logger.info(f"Failed to call collect_loan service: {e}") logger.error(f"Failed to call collect_loan service: {e}")
# raise # raise
+284 -72
View File
@@ -4,25 +4,40 @@ from app.helpers.response_helper import ResponseHelper
from app.services.loan import LoanService from app.services.loan import LoanService
from app.utils.auth import get_headers from app.utils.auth import get_headers
from app.utils.extras import preprocess_loan_charges_data from app.utils.extras import preprocess_loan_charges_data
import random
import random
import string
from app.utils.logger import logger from app.utils.logger import logger
from flask import jsonify, current_app from flask import jsonify, current_app
from app.services.transactions import TransactionService from app.services.transactions import TransactionService
from app.services.repayment import RepaymentService from app.services.repayment import RepaymentService
from app.extensions import db
from app.services.repayments_data import RepaymentsData
from app.services.salary import SalaryService
from app.enums.loan_status import LoanStatus
from decimal import Decimal, ROUND_HALF_UP
from requests.exceptions import SSLError, RequestException,Timeout
import sys
class SimbrellaClient: class SimbrellaClient:
BANK_CALL_BASE_URL = settings.BANK_CALL_BASE_URL BANK_CALL_BASE_URL = settings.BANK_CALL_BASE_URL
BANK_CALL_SMS_BASE_URL = settings.BANK_CALL_SMS_BASE_URL
BANK_CALL_DISBURSE_LOAN_ENDPOINT = settings.BANK_CALL_DISBURSE_LOAN_ENDPOINT
BANK_CALL_COLLECT_LOAN_ENDPOINT = settings.BANK_CALL_COLLECT_LOAN_ENDPOINT
BANK_CALL_TRANSACTION_VERIFY = settings.BANK_CALL_TRANSACTION_VERIFY
@staticmethod @staticmethod
def disbursement(data): def disburse_loan(data):
api_url = f"{SimbrellaClient.BANK_CALL_BASE_URL}/Disbursement" api_url = f"{SimbrellaClient.BANK_CALL_BASE_URL}/{SimbrellaClient.BANK_CALL_DISBURSE_LOAN_ENDPOINT}"
logger.info(f"Calling Disbursement endpoint with data: {data}") logger.info(f"Calling DisburseLoan api_url==> : {api_url}")
logger.info(f"Calling DisburseLoan endpoint with data: {data}")
# Check if the transaction exists # Check if the transaction exists
logger.info(f"Checking if transaction exists") logger.info(f"Checking if transaction exists")
transaction = TransactionService.get_transaction_by_transaction_id(transaction_id=data['transactionId']) transaction = TransactionService.get_transaction_by_transaction_id(transaction_id=data['transactionId'])
logger.info(f"Response from database: {transaction}") logger.info(f"Loan Response From Database ** : {transaction}")
# If transaction is not found # If transaction is not found
if not transaction: if not transaction:
@@ -42,24 +57,36 @@ class SimbrellaClient:
loan_data = loan.to_dict() loan_data = loan.to_dict()
logger.info(f"Here is your loan data: {loan_data}") logger.info(f"Here is your loan data: {loan_data}")
if loan_data['disburseDate'] is not None:
logger.info(
f"Please call verify loan : {data['transactionId']} loan send for processing at {loan_data['disburseDate']}")
return 0
# let us set disbursement date
LoanService.set_disbursement_date(loan_data['debtId'],loan_data['customerId']) # toda this must return something
logger.info(f"Here is your loan data after setting disbursement date: {loan_data}")
loan_charges = preprocess_loan_charges_data([loan_charge.to_dict() for loan_charge in loan.loan_charges]) loan_charges = preprocess_loan_charges_data([loan_charge.to_dict() for loan_charge in loan.loan_charges])
logger.info(f"Here are your loan_charges: {loan_charges}") logger.info(f"Here are your loan_charges: {loan_charges}")
mgt_fee = loan_charges.get("MGTFEE")['amount'] mgt_fee = loan_charges.get("MGTFEE")['amount']
vat_fee = loan_charges.get("VAT")['amount'] vat_fee = loan_charges.get("VAT")['amount']
interest_fee = loan_charges.get("INTEREST")['amount']
insurance_fee = loan_charges.get("INSURANCE")['amount']
disbursement_data ={ debtId = str(loan_data.get('debtId', "")).strip().zfill(6)
"requestId": data['requestId'],
"transactionId": data['transactionId'], disbursement_data = {
"debtId": loan_data['debtId'], "transactionId": loan_data.get('transactionId'),
"customerId": data['customerId'], "FbnTransactionId": loan_data.get('transactionId'),
"accountId": data['accountId'], "debtId": debtId,
"productId": loan_data['productId'], "customerId": loan_data.get('customerId'),
"provideAmount": loan_data['currentLoanAmount'], "accountId": loan_data.get('accountId'),
"collectAmountInterest": 5000, "productId": str(loan_data.get('productId', "")),
"provideAmount": loan_data.get('currentLoanAmount'),
"collectAmountInterest": interest_fee,
"collectAmountMgtFee": mgt_fee, "collectAmountMgtFee": mgt_fee,
"collectAmountInsurance": 1000, "collectAmountInsurance": insurance_fee,
"collectAmountVAT": vat_fee, "collectAmountVAT": vat_fee,
"countryId": "01", "countryId": "01",
"comment": "Loan Disbursement", "comment": "Loan Disbursement",
@@ -68,8 +95,13 @@ class SimbrellaClient:
try: try:
logger.info(f"Here is your Disbursement Request data ****** : {disbursement_data}") logger.info(f"Here is your Disbursement Request data ****** : {disbursement_data}")
response = requests.post(api_url, json=disbursement_data, timeout=10, headers=get_headers()) response = requests.post(api_url, json=disbursement_data, timeout=10, headers=get_headers())
if response.status_code == 404:
logger.error("Received 404 from external service")
return ResponseHelper.error("Disbursement Service url not found (404)", status_code=404)
logger.info(f"Disbursement response: {response.json()}") logger.info(f"Disbursement response: {response.json()}")
result = response.json()
LoanService.set_disbursement_result(loan_data['debtId'],result.get('responseCode', ''), result.get('responseMessage', ''))
return ResponseHelper.success(response.json(), "Successful")
except Exception as e: except Exception as e:
logger.info(f"Failed to call Disbursement endpoint: {e}") logger.info(f"Failed to call Disbursement endpoint: {e}")
return 0 return 0
@@ -77,83 +109,263 @@ class SimbrellaClient:
return 1 return 1
@staticmethod @staticmethod
def collect_loan(data): def verify_transaction(data):
api_url = f"{SimbrellaClient.BANK_CALL_BASE_URL}/CollectLoan" api_url = f"{SimbrellaClient.BANK_CALL_BASE_URL}{SimbrellaClient.BANK_CALL_TRANSACTION_VERIFY}"
logger.info(f"Calling CollectLoan endpoint with data: {data}") sms_url = f"{SimbrellaClient.BANK_CALL_SMS_BASE_URL}/singleSMS"
logger.info(f"Calling TransactionVerify api_url==> : {api_url}")
# Check if the repayment exists # Check if the transaction exists
logger.info(f"Checking if repayment exists") logger.info(f"Checking if transaction exists")
repayment = RepaymentService.get_repayment_by_transaction_id(transaction_id=data['transactionId']) transaction = TransactionService.get_transaction_by_transaction_id(transaction_id=data['transactionId'])
logger.info(f"Response from database: {repayment}") transaction_data = transaction.to_dict()
logger.info(f"Loan Response From Database ** : {transaction}")
# If repayment is not found # If transaction is not found
if not repayment: if not transaction:
logger.info(f"Repayment id: {data['transactionId']}, was not found") logger.info(f"Transaction id: {data['transactionId']}, was not found")
return 0 return 0
collect_loan_data = { # Fetch the loan based on the transaction_id
"transactionId": "T002", logger.info(f"Fetching the loan with transaction ID: {data['transactionId']}")
"fbnTransactionId": "FBN20231123", loan = LoanService.get_loan_by_transaction_id(transaction_id=data['transactionId'])
"debtId": data['debtId'], logger.info(f"Response from database: {loan}")
"customerId": data['customerId'],
"accountId": "2017821799", # If loan is not found
"productId": data['productId'], if not loan:
"collectAmount": 80000, logger.info(f"Could not find loan with transaction id: {data['transactionId']}")
"penalCharge": 0, return 0
"collectionMethod": 1,
"lienAmount": 80000, loan_data = loan.to_dict()
"countryId": "01", logger.info(f"Here is your loan data: {loan_data}")
"comment": "Testing CollectionLoanRequest"
if loan_data['disburseDate'] is not None and loan_data['disburseVerify'] is None :
LoanService.set_disburse_verify_date(loan_data['debtId'],loan_data['customerId'])
loan_data = loan.to_dict()
logger.info(f"Here is your loan data after setting verify date: {loan_data}")
logger.info(f"Good to Verify transaction id: {data['transactionId']}")
else:
logger.info(
f"Please call disburse loan : {data['transactionId']} loan send for processing first")
return 0
verify_data = {
"customerId": loan_data.get('customerId'),
"accountId": loan_data.get('accountId'),
"transactionId": loan_data.get('transactionId'),
"transactionType": "provide",
"fbnTransactionId": loan_data.get('transactionId'),
"countryId": "NG",
"requestId": loan_data.get('transactionId')
} }
try: try:
logger.info(f"Here is your CollectLoan Request data ***** : {collect_loan_data}") logger.info(f"Here is your TransactionVerify Request data ****** : {verify_data}")
response = requests.post(api_url, json=collect_loan_data, headers=get_headers()) response = requests.post(api_url, json=verify_data, timeout=10, headers=get_headers())
logger.info(f"CollectLoan response: {response.json()}") if response.status_code == 404:
logger.error("Received 404 from external service")
except Exception as e: return ResponseHelper.error("Verify Service url not found (404)", status_code=404)
logger.info(f"Failed to call CollectLoan endpoint: {e}") result = response.json()
return 0 logger.info(f"this is verify result, {result}")
LoanService.set_disburse_verify_result(loan_data['debtId'],result.get('responseCode', ''), result.get('responseMessage', ''))
return 1 sms_data = {
"dest": transaction_data.get('phone_number') or settings.TEST_NO,
@staticmethod "text": f"Transaction {loan_data.get('transactionId')} verified successfully",
def verify_transaction(): "unicode": True
try:
data = {
"status": "00",
"message": "Transaction verified"
} }
return ResponseHelper.success(data, "Successful") try:
sms_response = requests.post(sms_url, json=sms_data, timeout=10, headers=get_headers())
sms_response.raise_for_status() # Raise an exception for 4xx or 5xx status codes
result = sms_response.json()
logger.info(f"SMS Response JSON: {result}")
if result.get('isSuccess'):
logger.info(f"sms sent successfully")
return ResponseHelper.success(response.json(), "Successful")
logger.info(f"sms failed!")
return 1
except requests.RequestException as e:
# Handle the exception
logger.error(f"Failed to send SMS: {e}")
return 0
except Exception as e: except Exception as e:
logger.info(f"Failed to call TransactionVerify endpoint: {e}") logger.info(f"Failed to call TransactionVerify endpoint: {e}")
raise return 0
@staticmethod @staticmethod
def refresh_disbursement(data): def collect_loan_user_initiated(data):
# InitiatedBy = USER_INITIATED
try: try:
logger.info(f"Here is your Disbursement Request data ***** : {data}") logger.info(f"Calling CollectLoan collect_loan_user_initiated ******* endpoint with data: {data}")
return SimbrellaClient._collect_loan(data, "1")
return ResponseHelper.success(data, "Successful")
except Exception as e: except Exception as e:
logger.info(f"Failed to call Disbursement endpoint: {e}") logger.error(f"Error in collect_loan_user_initiated: {e}")
raise # return ResponseHelper.error(
# message="Failed to collect loan for user initiated ",
# status_code=500,
# error=str(e)
# )
@staticmethod @staticmethod
def payment_callback(data): def collect_loan_user_salary_detect(data):
try:
return SimbrellaClient._collect_loan(data, "2")
except Exception as e:
logger.error(f"Error in collect_loan_user_salary_detect: {e}")
return ResponseHelper.error(
message="Failed to collect loan for salary detection",
status_code=500,
error=str(e)
)
@staticmethod
def collect_loan_user_due_payment(data):
# InitiatedBy = REPAYMENT_DUE
return SimbrellaClient._collect_loan(data,"3")
@staticmethod
def _collect_loan(data, collectionMethod: str):
api_url = f"{SimbrellaClient.BANK_CALL_BASE_URL}{SimbrellaClient.BANK_CALL_COLLECT_LOAN_ENDPOINT}"
logger.info(f"Calling CollectLoan api_url==> : {api_url}")
logger.info(f"Calling CollectLoan endpoint with data: {data}")
repayment = RepaymentService.get_repayment_by_id(id=data['Id'])
if not repayment:
logger.info(f"Repayment with id: {data['Id']} not found")
return ResponseHelper.error("Repayment not found")
repayment_data = repayment.to_dict()
loan = LoanService.get_loan_by_loan_id(loan_id=int(repayment_data['loanId']))
if not loan:
logger.info(f"Loan with debtId: {repayment_data['loanId']} not found")
return ResponseHelper.error("Loan not found")
loan_data = loan.to_dict()
logger.info(f"Loan data: {loan_data}")
if repayment_data['repayDate'] is not None:
logger.info(f"Repayment already processed at {repayment_data['repayDate']}")
return ResponseHelper.error("Repayment already processed")
RepaymentService.set_repay_date(repayment_data['Id'], repayment_data['customerId'])
repayment = RepaymentService.get_repayment_by_transaction_id(transaction_id=data['transactionId'])
repayment_data = repayment.to_dict()
debtId = str(loan_data.get('debtId', "")).strip().zfill(6)
t_id = ''.join(random.choices(string.ascii_uppercase, k=22))
collect_loan_data = {
"transactionId": t_id,
"fbnTransactionId": loan_data['transactionId'],
"debtId": debtId,
"customerId": repayment_data['customerId'],
"accountId": loan_data['accountId'],
"productId": repayment_data['productId'],
"collectAmount": loan_data['balance'] or 0,
"penalCharge": 0,
"channel": "USSD",
"collectionMethod": collectionMethod,
"lienAmount": 0,
"countryId": "NG",
"comment": "COLLECT LOAN"
}
try: try:
logger.info(f"Here is your Payment Callback Request data ***** : {data}") logger.info(f"Sending CollectLoan request............ {collect_loan_data}")
response = requests.post(api_url, json=collect_loan_data, timeout=30, headers=get_headers())
return ResponseHelper.success(data, "Successful") logger.info(f"HTTP response object: {response}")
if response.status_code == 404:
RepaymentService.set_repay_result(
repayment_data['Id'],
'404',
'Collection Service url not found'
)
logger.error("Received 404 from external service")
return ResponseHelper.error("Collection Service URL not found", status_code=404)
result = response.json()
logger.info(f"CollectLoan response: {result}")
RepaymentService.set_repay_result(
repayment_data['Id'],
result.get('responseCode', ''),
result.get('responseMessage', '')
)
data_to_add = {
"transactionId": result.get('transactionId') or collect_loan_data.get('transactionId'),
"fbnTransactionId": loan_data['transactionId'],
"accountId": result.get('accountId') or collect_loan_data.get('accountId'),
"customerId": result.get('customerId') or collect_loan_data.get('customerId'),
"amountCollected": float(result.get('amountCollected', 0)),
"repaymentAmount": collect_loan_data.get('collectAmount'),
"responseCode": result.get('responseCode'),
"responseDescr": result.get('responseMessage'),
"balance": round(float(result.get('lienAmount', 0)), 2)
}
new_repayment_data = RepaymentsData.add_repayment_data(data_to_add)
if new_repayment_data:
logger.info(f"Repayment data added: {new_repayment_data.to_dict()}")
else:
logger.warning("Failed to add repayment data")
if result.get('responseCode') == '00':
amount_collected = Decimal(str(result.get('amountCollected', 0))).quantize(Decimal('0.01'), rounding=ROUND_HALF_UP)
logger.info(f"Amount collected: {amount_collected}")
if loan.balance is None or loan.balance <= 0:
logger.warning(f"Loan ID {loan.id} has no balance. Skipping loan update.")
return ResponseHelper.error("Loan has no balance. Skipping.")
try:
logger.info(f"Updating loan balance for loan ID {loan_data['debtId']} with amount collected: {amount_collected}")
updated_loan = LoanService.update_loan_balance(int(loan_data['debtId']), amount_collected)
logger.info(f"Updated loan: {updated_loan}")
except Exception as ex:
logger.error(f"Error updating loan balance for loan ID {loan.id}: {ex}")
return ResponseHelper.error("Error updating loan balance")
try:
updated_balance = Decimal(str(updated_loan['balance'])).quantize(Decimal('0.01'))
logger.info(f"Updated balance: {updated_balance}")
if updated_balance <= Decimal('0.00'):
logger.info('Loan fully repaid')
repaid = LoanService.update_status(updated_loan['debtId'], LoanStatus.REPAID)
logger.info(f'Updated loan with repaid status: {repaid}')
else:
logger.info('Loan partially repaid')
partial = LoanService.update_status(updated_loan['debtId'], LoanStatus.ACTIVE_PARTIAL)
logger.info(f'Updated loan with partial status: {partial}')
except Exception as e:
logger.error(f"Error while updating loan status for debtId {updated_loan['debtId']}: {e}")
return ResponseHelper.success(result, "Successful")
except SSLError as ssl_err:
logger.exception(f"SSL error while calling Simbrella endpoint: {ssl_err}")
return ResponseHelper.error("SSL handshake failed with Simbrella", status_code=502, error=str(ssl_err))
except Timeout as timeout_err:
logger.exception(f"Timeout while calling Simbrella: {timeout_err}")
return ResponseHelper.error("Connection to Simbrella timed out", status_code=504, error=str(timeout_err))
except RequestException as req_err:
logger.exception(f"RequestException while calling Simbrella: {req_err}")
return ResponseHelper.error("Connection to Simbrella failed", status_code=503, error=str(req_err))
except SystemExit as sys_exit:
logger.error(f"SystemExit was triggered: {sys_exit}")
return ResponseHelper.error("Unexpected shutdown detected", status_code=500, error=str(sys_exit))
except Exception as e: except Exception as e:
logger.info(f"Failed to call Payment Callback endpoint: {e}") logger.exception(f"Unexpected error occurred while calling CollectLoan: {e}")
raise return ResponseHelper.error("Unexpected error while processing loan collection", status_code=500, error=str(e))
@staticmethod @staticmethod
def penal_charge(data): def penal_charge(data):
+3 -1
View File
@@ -4,5 +4,7 @@ from .loan import Loan
from .loan_charge import LoanCharge from .loan_charge import LoanCharge
from .customer import Customer from .customer import Customer
from .account import Account from .account import Account
from .repayments_data import RepaymentsData
from .salary import Salary
__all__ = ['Transaction', 'Repayment', 'Loan', 'LoanCharge', 'Customer', 'Account'] __all__ = ['Transaction', 'Repayment', 'Loan', 'LoanCharge', 'Customer', 'Account', 'RepaymentsData','Salary']
+280 -4
View File
@@ -1,6 +1,15 @@
from datetime import datetime, timezone from datetime import datetime, timezone
from app.extensions import db from app.extensions import db
from sqlalchemy.orm import relationship from sqlalchemy.orm import relationship
from dateutil.relativedelta import relativedelta
from datetime import timedelta
import logging
from sqlalchemy import and_, or_, not_
from sqlalchemy.sql import func
from app.utils.logger import logger
from app.extensions import db
from decimal import Decimal, ROUND_HALF_UP
from datetime import datetime, timezone
class Loan(db.Model): class Loan(db.Model):
__tablename__ = "loans" __tablename__ = "loans"
@@ -12,6 +21,7 @@ class Loan(db.Model):
) )
customer_id = db.Column(db.String(50), nullable=False) customer_id = db.Column(db.String(50), nullable=False)
transaction_id = db.Column(db.String(50), nullable=True) transaction_id = db.Column(db.String(50), nullable=True)
original_transaction = db.Column(db.String(50), nullable=True)
account_id = db.Column(db.String(50), nullable=False) account_id = db.Column(db.String(50), nullable=False)
offer_id = db.Column(db.String(20), nullable=False) offer_id = db.Column(db.String(20), nullable=False)
product_id = db.Column(db.String(20), nullable=True) product_id = db.Column(db.String(20), nullable=True)
@@ -20,10 +30,23 @@ class Loan(db.Model):
initial_loan_amount = db.Column(db.Float, nullable=False) initial_loan_amount = db.Column(db.Float, nullable=False)
default_penalty_fee = db.Column(db.Float, default=0) default_penalty_fee = db.Column(db.Float, default=0)
continuous_fee = db.Column(db.Float, default=0) continuous_fee = db.Column(db.Float, default=0)
upfront_fee = db.Column(db.Float, nullable=True, default=0.0)
repayment_amount = db.Column(db.Float, nullable=True, default=0.0)
balance = db.Column(db.Float, nullable=True, default=0.0)
installment_amount = db.Column(db.Float, nullable=True, default=0.0)
status = db.Column(db.String(20), default='pending') status = db.Column(db.String(20), default='pending')
tenor = db.Column(db.Integer, nullable=True)
due_date = db.Column(db.DateTime, nullable=True) due_date = db.Column(db.DateTime, nullable=True)
created_at = db.Column(db.DateTime, default=datetime.now(timezone.utc)) created_at = db.Column(db.DateTime(timezone=True), server_default=func.now())
updated_at = db.Column(db.DateTime, default=datetime.now(timezone.utc), onupdate=datetime.now(timezone.utc)) updated_at = db.Column(db.DateTime(timezone=True), server_default=func.now(), onupdate=func.now())
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)
disburse_result = db.Column(db.String(10), nullable=True)
disburse_description = db.Column(db.String(100), nullable=True)
verify_result = db.Column(db.String(10), nullable=True)
verify_description = db.Column(db.String(100), nullable=True)
reference = db.Column(db.String(50), nullable=True)
customer = relationship( customer = relationship(
"Customer", "Customer",
@@ -48,17 +71,270 @@ class Loan(db.Model):
""" """
return { return {
'debtId': self.id, 'debtId': self.id,
"customerId": self.customer_id,
'initialLoanAmount': self.initial_loan_amount, 'initialLoanAmount': self.initial_loan_amount,
'currentLoanAmount': self.current_loan_amount, 'currentLoanAmount': self.current_loan_amount,
'defaultPenaltyFee': self.default_penalty_fee, 'defaultPenaltyFee': self.default_penalty_fee,
'continuousFee': self.continuous_fee, 'continuousFee': self.continuous_fee,
'collectionType': self.collection_type, 'collectionType': self.collection_type,
'repaymentAmount':self.repayment_amount,
'status': self.status, 'status': self.status,
'productId': self.product_id, 'productId': self.product_id,
'disburseResult': self.disburse_result,
'disburseDescription': self.disburse_description,
'verifyResult': self.verify_result,
'verifyDescription': self.verify_description,
'transactionId': self.transaction_id,
'accountId':self.account_id,
'dueDate': self.due_date.isoformat() if self.due_date else None, 'dueDate': self.due_date.isoformat() if self.due_date else None,
'loanDate': self.created_at.isoformat if self.created_at else None 'loanDate': self.created_at.isoformat() if self.created_at else None,
'disburseDate': self.disburse_date.isoformat() if self.disburse_date else None,
'disburseVerify': self.disburse_verify.isoformat() if self.disburse_verify else None,
'reference': self.reference,
'balance': self.balance,
'tenor': self.tenor,
} }
@classmethod @classmethod
def get_loan_by_transaction_id(cls, transaction_id): def get_loan_by_transaction_id(cls, transaction_id):
return cls.query.filter_by(transaction_id=transaction_id).first() return cls.query.filter_by(transaction_id=transaction_id).first()
@classmethod
def get_loan_by_loan_id(cls, loan_id):
return cls.query.filter_by(id=loan_id).first()
@classmethod
def set_disbursement_date(cls, loan_id, customer_id):
"""
Update the disburse date of the loan with the given loan_id.
"""
# Retrieve loan
loan = cls.query.get(loan_id)
if not loan:
raise ValueError(f"Loan with ID {loan_id} does not exist.")
# Check if customer_id matches
if loan.customer_id != customer_id:
raise ValueError(f"Customer ID {customer_id} does not match the loan's customer ID.")
current_time = datetime.now()
logger.info(f"What is now ======= ==== ==> : {current_time}")
# Update loan disburse_date
loan.disburse_date = current_time
# Commit changes to database
try:
logger.info(f"Updating disburse date for loan ID {loan_id} to {current_time}")
db.session.commit()
except Exception as e:
db.session.rollback()
logger.error(f"Failed to update disburse date: {e}")
raise
@classmethod
def set_disburse_verify_date(cls, loan_id, customer_id):
"""
Update the disburse verify date of the loan with the given loan_id.
"""
# Retrieve loan
loan = cls.query.get(loan_id)
if not loan:
raise ValueError(f"Loan with ID {loan_id} does not exist.")
# Check if customer_id matches
if loan.customer_id != customer_id:
raise ValueError(f"Customer ID {customer_id} does not match the loan's customer ID.")
current_time = datetime.now()
logger.info(f"What is now ======= ==== ==> : {current_time}")
# Update loan verify_date
loan.disburse_verify = current_time
# Commit changes to database
try:
logger.info(f"Updating disburse verify date for loan ID {loan_id} to {current_time}")
db.session.commit()
except Exception as e:
db.session.rollback()
logger.error(f"Failed to update disburse verify date: {e}")
raise
@classmethod
def set_disbursement_result(cls, loan_id, result, description):
"""
Update the disburse result and description of the loan with the given loan_id.
"""
# Retrieve loan
loan = cls.query.get(loan_id)
if not loan:
raise ValueError(f"Loan with ID {loan_id} does not exist.")
# Update disburse result and description
loan.disburse_result = result
loan.disburse_description = description
# Commit changes to database
try:
logger.info(f"Updating disburse result for loan ID {loan_id} to {result} with description {description}")
db.session.commit()
except Exception as e:
db.session.rollback()
logger.error(f"Failed to update disbursement result: {e}")
raise
@classmethod
def set_disburse_verify_result(cls, loan_id, result, description):
"""
Update the verify result and description of the loan with the given loan_id.
"""
# Retrieve loan
loan = cls.query.get(loan_id)
if not loan:
raise ValueError(f"Loan with ID {loan_id} does not exist.")
# Update disburse result and description
loan.verify_result = result
loan.verify_description = description
# Commit changes to database
try:
logger.info(f"Updating verify result for loan ID {loan_id} to {result} with description {description}")
db.session.commit()
except Exception as e:
db.session.rollback()
logger.error(f"Failed to update verify result: {e}")
raise
@classmethod
def get_latest_loan_without_disburse_date(cls):
"""
Get the latest loan without a disbursement date.
"""
return cls.query.filter(
cls.disburse_date.is_(None)
).order_by(cls.created_at.desc()).first()
@classmethod
def get_latest_loan_with_disburse_date(cls):
"""
Get the latest loan with a disbursement date and no verification date.
"""
return cls.query.filter(
cls.disburse_date.isnot(None),
cls.disburse_verify.is_(None)
).order_by(cls.created_at.desc()).first()
@classmethod
def get_customer_loans(cls, customer_id):
"""
Get customer's active loans and sum by customer_id.
"""
customer_loans = cls.query.filter_by( customer_id = customer_id).all()
if not customer_loans:
raise ValueError(f"Customer with Id {customer_id} does not have any loan.")
total_amount = (
cls.query.with_entities(func.coalesce(func.sum(cls.balance), 0.0))
.filter_by(customer_id=customer_id)
.scalar()
)
logger.info(f"Found {len(customer_loans)} loans for customer ID: {customer_id} with total amount: {total_amount}")
return customer_loans, total_amount
@classmethod
def get_customer_active_loans(cls, customer_id):
"""
Get customer's active loans and sum by customer_id.
"""
customer_loans = cls.query.filter(
cls.customer_id == customer_id,
cls.status != 'repaid'
).all()
if not customer_loans:
raise ValueError(f"Customer with Id {customer_id} does not have any active loan.")
total_amount = (
cls.query
.with_entities(func.coalesce(func.sum(cls.balance), 0.0))
.filter(
cls.customer_id == customer_id,
cls.status != 'repaid'
)
.scalar()
)
logger.info(f"Found {len(customer_loans)} active loans for customer ID: {customer_id} with total amount: {total_amount}")
return customer_loans, total_amount
@classmethod
def update_status(cls, loan_id, status):
"""
Update the status of the loan record with the given loan_id.
"""
try:
# Retrieve loan record
loan = cls.query.get(loan_id)
if not loan:
raise ValueError(f"Loan with ID {loan_id} does not exist.")
if loan.status == status:
return loan.to_dict() # Still return the current state if no change
# Update status and timestamp
loan.status = status
loan.updated_at = datetime.now(timezone.utc)
db.session.commit()
logger.info("Loan status updated and committed.")
return loan.to_dict()
except Exception as e:
db.session.rollback()
logger.error(f"Error updating loan status: {e}")
raise Exception(f"Error updating loan status: {str(e)}")
@classmethod
def update_loan_balance(cls, loan_id, amount_collected):
"""
Update the balance of a loan after successful repayment.
"""
try:
# Fetch the loan record
loan = cls.query.get(loan_id)
if not loan:
raise ValueError(f"Loan with ID {loan_id} does not exist.")
# Convert to Decimal and round to 2 decimal places
amount_collected = Decimal(str(amount_collected)).quantize(Decimal("0.01"), rounding=ROUND_HALF_UP)
balance = Decimal(str(loan.balance or 0)).quantize(Decimal("0.01"), rounding=ROUND_HALF_UP)
# Ensure valid repayment amount
if amount_collected <= Decimal("0.00"):
raise ValueError("Repayment amount must be greater than zero.")
if balance <= Decimal("0.00"):
raise ValueError("There is no balance for this loan.")
if amount_collected > balance:
raise ValueError("Repayment amount exceeds current loan balance.")
# Deduct the amount from the current balance
new_balance = balance - amount_collected
loan.balance = float(new_balance)
loan.updated_at = datetime.now(timezone.utc)
db.session.commit()
logger.info(f"Loan balance updated for loan ID {loan_id}. New balance: {loan.balance}")
return loan.to_dict()
except Exception as e:
db.session.rollback()
logger.error(f"Error updating loan balance: {e}")
raise Exception(f"Error updating loan balance: {str(e)}")
+236 -2
View File
@@ -1,5 +1,9 @@
from app.extensions import db from app.extensions import db
from datetime import datetime, timezone from datetime import datetime, timezone
from app.utils.logger import logger
from app.enums.loan_status import LoanStatus
from sqlalchemy.exc import IntegrityError
class Repayment(db.Model): class Repayment(db.Model):
__tablename__ = "repayments" __tablename__ = "repayments"
@@ -13,12 +17,242 @@ class Repayment(db.Model):
customer_id = db.Column(db.String(50), nullable=False) customer_id = db.Column(db.String(50), nullable=False)
product_id = db.Column(db.String(20), nullable=True) product_id = db.Column(db.String(20), nullable=True)
transaction_id = db.Column(db.String(50), nullable=False) transaction_id = db.Column(db.String(50), nullable=False)
initiated_by = db.Column(db.String(50), nullable=True)
salary_amount = db.Column(db.Float, nullable=True, default=0.0)
created_at = db.Column(db.DateTime, default=datetime.now(timezone.utc)) created_at = db.Column(db.DateTime, default=datetime.now(timezone.utc))
updated_at = db.Column(db.DateTime, default=datetime.now(timezone.utc), onupdate=datetime.now(timezone.utc)) updated_at = db.Column(db.DateTime, default=datetime.now(timezone.utc), onupdate=datetime.now(timezone.utc))
repay_date = db.Column(db.DateTime, nullable=True)
verify_date = db.Column(db.DateTime, nullable=True)
repay_result = db.Column(db.String(10), nullable=True)
repay_description = db.Column(db.String(100), nullable=True)
verify_result = db.Column(db.String(10), nullable=True)
verify_description = db.Column(db.String(100), nullable=True)
def __repr__(self): def __repr__(self):
return f'<Repayment {self.id}>' return f'<Repayment {self.id}>'
def to_dict(self):
"""
Convert the Repayment object to a dictionary format for JSON serialization.
"""
return {
'Id': self.id,
"customerId": self.customer_id,
'loanId': self.loan_id,
'productId': self.product_id,
'repayResult': self.repay_result,
'repayDescription': self.repay_description,
'verifyResult': self.verify_result,
'verifyDescription': self.verify_description,
'transactionId': self.transaction_id,
'initiatedBy':self.initiated_by,
'salaryAmount':self.salary_amount,
'repayDate': self.repay_date.isoformat() if self.repay_date else None,
'VerifyDate': self.verify_date.isoformat() if self.verify_date else None,
}
@classmethod
def create_repayment(cls, repayment_data):
if repayment_data["LoanStatus"] not in [LoanStatus.ACTIVE, LoanStatus.START_REPAY,LoanStatus.ACTIVE_PARTIAL]:
raise ValueError(f"Repayment cannot be processed. Loan status: ({repayment_data['LoanStatus']})")
repayment = cls(
customer_id=repayment_data["customerId"],
loan_id=repayment_data["loanId"],
product_id=repayment_data["productId"],
transaction_id=repayment_data["transactionId"],
created_at=datetime.now(timezone.utc),
updated_at=datetime.now(timezone.utc),
initiated_by= repayment_data["initiatedBy"],
salary_amount=repayment_data["salaryAmount"]
)
try:
db.session.add(repayment)
db.session.commit()
logger.info("Repayment record committed.")
return repayment
except IntegrityError as err:
logger.error(f"Database integrity error: {err}")
return {"error": "Integrity error", "details": str(err)}
@classmethod
def add_repayment(cls, data: dict):
"""
Create and persist a new repayment record.
"""
logger.info(f"Received repayment data: {data}")
try:
new_repayment = cls(
loan_id=data["loanId"],
customer_id=data["customerId"],
product_id=data.get("productId"),
transaction_id=data["transactionId"],
initiated_by=data.get("initiatedBy"),
salary_amount=float(data.get("salaryAmount", 0.0)),
repay_date=(
datetime.strptime(data["repayDate"], "%Y-%m-%d")
.replace(tzinfo=timezone.utc)
if data.get("repayDate")
else None
),
repay_result=data.get("repayResult"),
repay_description=data.get("repayDescription"),
verify_result=data.get("verifyResult"),
verify_description=data.get("verifyDescription"),
verify_date=(
datetime.strptime(data["verifyDate"], "%Y-%m-%d")
.replace(tzinfo=timezone.utc)
if data.get("verifyDate")
else None
),
)
db.session.add(new_repayment)
db.session.commit()
logger.info("Repayment record committed.")
return new_repayment
except Exception as e:
db.session.rollback()
logger.error(f"Error adding repayment data: {e}")
raise
@classmethod @classmethod
def get_repayment_by_transaction_id(cls, transaction_id): def get_repayment_by_transaction_id(cls, transaction_id):
return cls.query.filter_by(transaction_id=transaction_id).first() return cls.query.filter_by(transaction_id=transaction_id).first()
@classmethod
def get_repayment_by_id(cls, id):
return cls.query.filter_by(id=id).first()
@classmethod
def set_repay_date(cls, repayment_id, customer_id):
"""
Update the repay date of the loan with the given loan_id.
"""
# Retrieve repayment
repayment = cls.query.get(repayment_id)
if not repayment:
raise ValueError(f"repayment with ID {repayment_id} does not exist.")
# Check if customer_id matches
if repayment.customer_id != customer_id:
raise ValueError(f"Customer ID {customer_id} does not match the repayment's customer ID.")
current_time = datetime.now()
logger.info(f"What is now ======= ==== ==> : {current_time}")
# Update repayment date
repayment.repay_date = current_time
# Commit changes to database
try:
logger.info(f"Updating repay date for repayment ID {repayment_id} to {current_time}")
db.session.commit()
except Exception as e:
db.session.rollback()
logger.error(f"Failed to update repay date: {e}")
raise
@classmethod
def set_repay_verify_date(cls, repayment_id, customer_id):
"""
Update the repayment verify date of the loan with the given repayment_id.
"""
# Retrieve repayment
repayment = cls.query.get(repayment_id)
if not repayment:
raise ValueError(f"repayment with ID {repayment_id} does not exist.")
# Check if customer_id matches
if repayment.customer_id != customer_id:
raise ValueError(f"Customer ID {customer_id} does not match the repayment's customer ID.")
current_time = datetime.now()
logger.info(f"What is now ======= ==== ==> : {current_time}")
# Update repayment verify_date
repayment.verify_date = current_time
# Commit changes to database
try:
logger.info(f"Updating repay verify date for repayment ID {repayment_id} to {current_time}")
db.session.commit()
except Exception as e:
db.session.rollback()
logger.error(f"Failed to update repay verify date: {e}")
raise
@classmethod
def set_repay_result(cls, repayment_id, result, description):
"""
Update the repayment result and description of the repayment with the given repayment_id.
"""
# Retrieve loan
repayment = cls.query.get(repayment_id)
if not repayment:
raise ValueError(f"repayment with ID {repayment_id} does not exist.")
# Update repayment result and description
repayment.repay_result = result
repayment.repay_description = description
# Commit changes to database
try:
logger.info(f"Updating repayment result for repayment ID {repayment_id} to {result} with description {description}")
db.session.commit()
except Exception as e:
db.session.rollback()
logger.error(f"Failed to update repayment result: {e}")
raise
@classmethod
def set_verify_date_result(cls, repayment_id, result, description):
"""
Update the verify result and description of the repayment with the given repayment_id.
"""
# Retrieve repayment
repayment = cls.query.get(repayment_id)
if not repayment:
raise ValueError(f"repayment with ID {repayment_id} does not exist.")
# Update disburse result and description
repayment.verify_result = result
repayment.verify_description = description
# Commit changes to database
try:
logger.info(f"Updating verify result for repayment ID {repayment_id} to {result} with description {description}")
db.session.commit()
except Exception as e:
db.session.rollback()
logger.error(f"Failed to update verify result: {e}")
raise
@classmethod
def get_latest_repayment_without_repay_date(cls):
"""
Get the latest repayment without a repay date.
"""
return cls.query.filter(
cls.repay_date.is_(None)
).order_by(cls.created_at.desc()).first()
@classmethod
def get_latest_repayment_with_loanId(cls, loan_id):
"""
Get the latest repayment with loan Id.
"""
return cls.query.filter(
cls.loan_id == loan_id
).order_by(cls.created_at.desc()).first()
@classmethod
def get_latest_loan_with_repay_date(cls):
"""
Get the latest repayment with a repay date and no verification date.
"""
return cls.query.filter(
cls.repay_date.isnot(None),
cls.verify_date.is_(None)
).order_by(cls.created_at.desc()).first()
+75
View File
@@ -0,0 +1,75 @@
from datetime import datetime, timezone
from app.extensions import db
from app.utils.logger import logger
class RepaymentsData(db.Model):
__tablename__ = 'repayments_data'
id = db.Column(db.Integer, primary_key=True, autoincrement=True)
transaction_id = db.Column(db.String(50), nullable=False)
added_date = db.Column(db.DateTime(timezone=True), default=datetime.now(timezone.utc), nullable=False)
response_code = db.Column(db.String(10), nullable=True)
response_descr = db.Column(db.String(255), nullable=True)
fbn_transaction_id = db.Column(db.String(255),nullable=True)
account_id = db.Column(db.String(50), nullable=True)
customer_id = db.Column(db.String(50), nullable=True)
repayment_amount = db.Column(db.Float, nullable=True)
amount_collected = db.Column(db.Float, nullable=True)
balance = db.Column(db.Float, nullable=True, default=0.0)
def to_dict(self):
return {
"id": self.id,
"transaction_id": self.transaction_id,
"added_date": self.added_date.isoformat() if self.added_date else None,
"response_code": self.response_code,
"response_descr": self.response_descr,
"customerId": self.customer_id,
"accountId": self.customer_id,
"fbnTransactionId": self.fbn_transaction_id,
"repaymentAmount": self.repayment_amount,
"amountCollected": self.amount_collected,
"balance": self.balance
}
def __repr__(self):
return f"<RepaymentsData id={self.id}, transaction_id={self.transaction_id}>"
@classmethod
def add_repayment_data(cls, data):
"""
Add a new repayment data entry.
"""
try:
repayment_amount = float(data.get('repaymentAmount', 0.0))
amount_collected = float(data.get('amountCollected', 0.0))
if amount_collected < 0 or repayment_amount < 0:
raise ValueError("Amounts cannot be negative.")
account_balance = round(repayment_amount - amount_collected, 2)
new_data = cls(
transaction_id=data.get('transactionId'),
response_code=data.get('responseCode'),
response_descr=data.get('responseDescr'),
fbn_transaction_id=data.get('fbnTransactionId'),
account_id=data.get('accountId'),
customer_id=data.get('customerId'),
amount_collected=amount_collected,
repayment_amount=repayment_amount,
balance=account_balance,
)
db.session.add(new_data)
db.session.commit()
logger.info("Repayment data committed successfully")
return new_data
except Exception as e:
db.session.rollback()
logger.error(f"Error adding repayment data: {e}")
raise Exception(f"Error adding repayment data: {str(e)}")
+98
View File
@@ -0,0 +1,98 @@
from app.extensions import db
from datetime import datetime, timezone
from app.utils.logger import logger
class Salary(db.Model):
__tablename__ = "salaries"
id = db.Column(
db.Integer,
primary_key=True,
autoincrement=True,
)
customer_id = db.Column(db.String(50), nullable=False)
account_id = db.Column(db.String(50), nullable=False)
amount = db.Column(db.Float, nullable=True, default=0.0)
status = db.Column(db.String(20), nullable=True)
created_at = db.Column(db.DateTime, default=datetime.now)
updated_at = db.Column(db.DateTime, default=datetime.now, onupdate=datetime.now)
salary_date = db.Column(db.DateTime, nullable=True)
def __repr__(self):
return f'<Salary {self.id}>'
def to_dict(self):
"""
Convert the Salary object to a dictionary format for JSON serialization.
"""
return {
'id': self.id,
'customerId': self.customer_id,
'accountId' : self.account_id,
'salaryAmount': self.amount,
'status': self.status,
'createdAt': self.created_at.isoformat() if self.created_at else None,
'updatedAt': self.updated_at.isoformat() if self.updated_at else None,
'salaryDate': self.salary_date.isoformat() if self.salary_date else None,
}
@classmethod
def add_salary_data(cls, data):
"""
Add a new salary data entry.
"""
logger.info(f"Received data:{data}")
try:
new_data = cls(
customer_id=data.get('customerId'),
amount=data.get('salaryAmount', 0.0),
status='START',
salary_date = datetime.strptime(data.get('salaryDate'), "%Y-%m-%d").date() if data.get('salaryDate') else None,
account_id=data.get('accountId')
)
db.session.add(new_data)
db.session.commit()
logger.info("Salary data has been committed.")
return new_data
except Exception as e:
db.session.rollback()
logger.info(f"error : {str(e)}")
raise Exception(f"Error adding salary data: {str(e)}")
@classmethod
def get_pending_salaries(cls):
"""
Retrieve all salary entries with status 'START', ordered by ID ascending.
"""
try:
return cls.query.filter_by(status='START').order_by(cls.id.asc()).all()
except Exception as e:
logger.error(f"Error fetching pending salaries: {str(e)}")
return []
@classmethod
def update_status(cls, salary_id, status):
"""
Update the status of the salary record with the given salary_id.
"""
try:
# Retrieve salary record
salary = cls.query.get(salary_id)
if not salary:
raise ValueError(f"Salary with ID {salary_id} does not exist.")
if salary.status == status:
return salary.to_dict() # Still return the current state if no change
# Update status and timestamp
salary.status = status
salary.updated_at = datetime.now(timezone.utc) # Manually update timestamp if not auto-updating
db.session.commit()
logger.info("Salary status updated and committed.")
return salary.to_dict()
except Exception as e:
db.session.rollback()
logger.error(f"Error updating salary status: {e}")
raise Exception(f"Error updating salary status: {str(e)}")
+2
View File
@@ -14,6 +14,7 @@ class Transaction(db.Model):
customer_id = db.Column(db.String(50), nullable=True) customer_id = db.Column(db.String(50), nullable=True)
type = db.Column(db.String(50), nullable=False) type = db.Column(db.String(50), nullable=False)
channel = db.Column(db.String(50), nullable=False) channel = db.Column(db.String(50), nullable=False)
phone_number = db.Column(db.String(50), nullable=True)
created_at = db.Column(db.DateTime, default=datetime.now(timezone.utc)) created_at = db.Column(db.DateTime, default=datetime.now(timezone.utc))
updated_at = db.Column(db.DateTime, default=datetime.now(timezone.utc), onupdate=datetime.now(timezone.utc)) updated_at = db.Column(db.DateTime, default=datetime.now(timezone.utc), onupdate=datetime.now(timezone.utc))
@@ -29,6 +30,7 @@ class Transaction(db.Model):
'transaction_id': self.transaction_id, 'transaction_id': self.transaction_id,
'account_id': self.account_id, 'account_id': self.account_id,
'customer_id': self.customer_id, 'customer_id': self.customer_id,
'phone_number':self.phone_number,
'type': self.type, 'type': self.type,
'channel': self.channel, 'channel': self.channel,
} }
+2
View File
@@ -2,6 +2,7 @@ from flask import Blueprint, request, jsonify, current_app
import requests import requests
from app.utils.auth import get_headers from app.utils.auth import get_headers
from app.config import settings from app.config import settings
from app.utils.logger import logger
auth_bp = Blueprint("auth", __name__) auth_bp = Blueprint("auth", __name__)
@@ -10,6 +11,7 @@ BASE_URL = settings.BANK_CALL_BASE_URL
@auth_bp.route("/health", methods=["GET"]) @auth_bp.route("/health", methods=["GET"])
def health(): def health():
logger.info("Health check endpoint called")
return jsonify({"status": "Up"}) return jsonify({"status": "Up"})
+196 -6
View File
@@ -5,27 +5,95 @@ from app.helpers.response_helper import ResponseHelper
from app.utils.auth import get_headers from app.utils.auth import get_headers
from app.utils.logger import logger from app.utils.logger import logger
from app.integrations.simbrella import SimbrellaClient from app.integrations.simbrella import SimbrellaClient
from app.services.loan import LoanService
from app.services.repayment import RepaymentService
from app.services.salary import SalaryService
from app.enums.loan_status import LoanStatus
autocall_bp = Blueprint("autocall", __name__) autocall_bp = Blueprint("autocall", __name__)
@autocall_bp.route("/refresh-verify-disbursement", methods=["GET"]) @autocall_bp.route("/refresh-verify-disbursement", methods=["GET"])
def verify_transaction(): def verify_transaction():
# data = request.json()
logger.info(f"Calling VerifyTransaction Components") logger.info(f"Calling VerifyTransaction Components")
response = SimbrellaClient.verify_transaction() loan = LoanService.get_latest_loan_with_disburse_date()
if not loan:
logger.info(f"No loan found without disbursement date")
return 0
logger.info(f"Calling VerifyTransaction endpoint with data: {loan}")
loan_data = loan.to_dict()
data = {
"transactionId": loan_data.get('transactionId'),
"FbnTransactionId": loan_data.get('transactionId'),
"debtId": str(loan_data.get('debtId')),
"customerId": loan_data.get('customerId'),
"accountId": loan_data.get('accountId'),
"productId": str(loan_data.get('productId', "")),
"provideAmount": loan_data.get('currentLoanAmount'),
}
response = SimbrellaClient.verify_transaction(data)
return response return response
@autocall_bp.route("/refresh-disbursement", methods=["GET"]) @autocall_bp.route("/refresh-disbursement", methods=["GET"])
def disbursement(): def disbursement():
# data = request.json() # data = request.json()
logger.info(f"Calling Disbursement Components") logger.info(f"Calling Disbursement Components")
loan = LoanService.get_latest_loan_without_disburse_date()
if not loan:
logger.info(f"No loan found without disbursement date")
return 0
logger.info(f"Calling DisburseLoan endpoint with data: {loan}")
loan_data = loan.to_dict()
response = SimbrellaClient.verify_transaction() data = {
"transactionId": loan_data.get('transactionId'),
"FbnTransactionId": loan_data.get('transactionId'),
"debtId": str(loan_data.get('debtId')),
"customerId": loan_data.get('customerId'),
"accountId": loan_data.get('accountId'),
"productId": str(loan_data.get('productId', "")),
"provideAmount": loan_data.get('currentLoanAmount'),
}
response = SimbrellaClient.disburse_loan(data)
return response
@autocall_bp.route("/refresh-verify-collection", methods=["GET"])
def refresh_verify_collection():
data = request.get_json()
logger.info(f"Calling Verify Collection")
response = SimbrellaClient.collect_loan(data)
return response return response
@autocall_bp.route("/refresh-collection", methods=["GET"])
def refresh_collection():
#data = request.get_json()
logger.info(f"Calling Collection ")
#grab the last repayments with repay date is none
repayment = RepaymentService.get_latest_repayment_without_repay_date()
#repayment = RepaymentService.get_latest_repayment_with_loanId(13735)
if not repayment:
logger.info(f"No repayment found without repay date")
return 0
logger.info(f"Calling repay loan endpoint with data: {repayment}")
repayment_data = repayment.to_dict()
logger.info(f"here is the dict form of repayment {repayment_data}")
data = {
"transactionId": repayment_data['transactionId'],
"debtId": repayment_data['loanId'],
"customerId": repayment_data['customerId'],
"productId": repayment_data['productId'],
"Id":repayment_data['Id']
}
logger.info(f"Data being sent to Simbrella: {data}")
logger.info(f"calling simbrella")
response = SimbrellaClient.collect_loan_user_initiated(data)
return response
@autocall_bp.route("/payment-callback", methods=["POST"]) @autocall_bp.route("/payment-callback", methods=["POST"])
def payment_callback(): def payment_callback():
@@ -39,8 +107,130 @@ def payment_callback():
@autocall_bp.route("/penal-charge", methods=["POST"]) @autocall_bp.route("/penal-charge", methods=["POST"])
def penal_charge(): def penal_charge():
data = request.get_json() data = request.get_json()
logger.info(f"Calling Penal Charge Endpoints") logger.info(f"Calling Penal Charge Endpoint")
response = SimbrellaClient.penal_charge(data[0]) try:
response = SimbrellaClient.penal_charge(data[0])
return response
except Exception as e:
logger.error(f"Error in Penal Charge: {e}")
return ResponseHelper.error("Penal charge failed")
return response
@autocall_bp.route("/analytic-salary-detect", methods=["POST"])
def salary_detect():
payload = request.get_json()
logger.info("Calling Salary Detect endpoint")
if payload is None:
logger.warning("No payload received in request")
return ResponseHelper.error("Missing request payload", status_code=400)
# Step 1: Try to add new salary data
try:
new_salary = SalaryService.add_salary_data(payload) # TODO - This will come as array of salaries - not just one
if new_salary:
logger.info(f"Salary added: {new_salary.id}")
except Exception as e:
logger.error(f"Failed to save salary: {e}")
# Step 2: Try processing salary list
try:
process_salary_list()
except Exception as e:
logger.exception("Unhandled error occurred while processing salary list {e}")
return ResponseHelper.error("Failed to process salary list", status_code=500, error=str(e))
logger.info("Finished processing List")
return ResponseHelper.success([], "AutoCall Add Salary Successful")
@autocall_bp.route("/analytic-salary-process", methods=["POST"])
def salary_process():
response = process_salary_list()
return ResponseHelper.success([], "AutoCall Successful")
def process_salary_list():
# Step 1: Get all pending salaries
pending_salaries = SalaryService.get_pending_salaries()
if not pending_salaries:
logger.info("No pending salaries found")
return ResponseHelper.success([], "No pending salaries")
logger.info(f"Found {len(pending_salaries)} pending salaries to process")
for pending_salary in pending_salaries:
logger.info(f"Processing salary ID: {pending_salary.id}")
# Step 2: Update salary status to PROCESSING
try:
SalaryService.update_status(pending_salary.id, "PROCESSING")
except Exception as e:
logger.warning(f"Could not update status for salary ID {pending_salary.id}: {e}")
continue
# Step 3: Get customer's active loans
try:
loans, total_amount = LoanService.get_customer_active_loans(pending_salary.customer_id)
if not loans:
logger.warning(f"No loans found for customer ID: {pending_salary.customer_id}")
continue
except Exception as e:
logger.error(f"Error fetching loans for customer ID {pending_salary.customer_id}: {e}")
continue
# Step 4: Create repayments for each loan
for loan in loans:
logger.info(f"Processing Loan ID: {loan.id}")
try:
repayment_data = {
"customerId": loan.customer_id,
"loanId": loan.id,
"productId": loan.product_id,
"transactionId": loan.transaction_id,
"initiatedBy": "SALARY_DETECT",
"salaryAmount": pending_salary.amount,
"LoanStatus": loan.status,
}
logger.info(f"Creating repayment with data: {repayment_data}")
repayment = RepaymentService.create_repayment(repayment_data)
if not repayment:
logger.error(f"Repayment creation failed for loan ID {loan.id}")
continue
# Update loan status to START_REPAY
try:
LoanService.update_status(loan_id=loan.id, status=LoanStatus.START_REPAY)
except Exception as e:
logger.error(f"Failed to update loan status for loan ID {loan.id}: {e}")
logger.info(f"Created repayment ID: {repayment.id}")
except Exception as e:
logger.error(f"Error creating repayment for loan ID {loan.id}: {e}")
continue
# Step 5: Call Simbrella to collect loan
try:
simbrella_response = SimbrellaClient.collect_loan_user_salary_detect(repayment.to_dict())
if isinstance(simbrella_response, tuple):
simbrella_response, status_code = simbrella_response
logger.warning(f"Simbrella returned tuple: status={status_code}, response={simbrella_response}")
if isinstance(simbrella_response, dict):
status = simbrella_response.get("status")
if status != "success":
logger.warning(f"Simbrella call failed for repayment ID {repayment.id}: {simbrella_response}")
else:
logger.warning(f"Unexpected Simbrella response type: {type(simbrella_response)}")
except Exception as e:
logger.error(f"Failed to call Simbrella for repayment ID {repayment.id}: {e}")
logger.info(f"Finished processing salary ID: {pending_salary.id}")
return ResponseHelper.success([], "Processed all pending salaries")
+85 -1
View File
@@ -8,10 +8,94 @@ class LoanService:
Get the loan by transaction ID Get the loan by transaction ID
""" """
return Loan.get_loan_by_transaction_id(transaction_id) return Loan.get_loan_by_transaction_id(transaction_id)
@classmethod
def get_loan_by_loan_id(cls, loan_id):
"""
Get the loan by ID
"""
return Loan.get_loan_by_loan_id(loan_id)
@classmethod
def get_loan_by_debt_id(cls, debt_id):
"""
Get the loan by transaction ID
"""
return Loan.get_loan_by_debt_id(debt_id)
@classmethod @classmethod
def get_loan_charge_by_debt_id(cls, debt_id): def get_loan_charge_by_debt_id(cls, debt_id):
""" """
Get the loan charge by debt ID Get the loan charge by debt ID
""" """
return LoanCharge.get_loan_charge_by_debt_id(debt_id) return LoanCharge.get_loan_charge_by_debt_id(debt_id)
@classmethod
def set_disbursement_date(cls, loan_id, customer_id):
"""
Update the disbursement status of the loan with the given loan_id.
"""
return Loan.set_disbursement_date(loan_id, customer_id)
@classmethod
def set_disburse_verify_date(cls, loan_id, customer_id):
"""
Update the disburse verify date of the loan with the given loan_id.
"""
return Loan.set_disburse_verify_date(loan_id, customer_id)
@classmethod
def set_disbursement_result(cls, loan_id, result, description):
"""
Update the disbursement result of the loan with the given loan_id.
"""
return Loan.set_disbursement_result(loan_id, result, description)
@classmethod
def set_disburse_verify_result(cls, loan_id, result, description):
"""
Update the disburse verify result of the loan with the given loan_id.
"""
return Loan.set_disburse_verify_result(loan_id, result, description)
@classmethod
def get_latest_loan_without_disburse_date(cls):
"""
Get the latest loan without a disbursement date.
"""
return Loan.get_latest_loan_without_disburse_date()
@classmethod
def get_latest_loan_with_disburse_date(cls):
"""
Get the latest loan without a disbursement date.
"""
return Loan.get_latest_loan_with_disburse_date()
@classmethod
def get_customer_loans(cls, customer_id):
"""
Get customer's active loans by customer_id.
"""
return Loan.get_customer_loans(customer_id=customer_id)
@classmethod
def get_customer_active_loans(cls, customer_id):
"""
Get customer's active loans by customer_id.
"""
return Loan.get_customer_active_loans(customer_id=customer_id)
@classmethod
def update_status(cls, loan_id, status):
"""
Update the status of the loan with the given loan_id.
"""
# Retrieve loan
return Loan.update_status(loan_id, status)
@classmethod
def update_loan_balance(cls,loan_id,amount_collected):
"""
update the loan balance after successful repayment
"""
return Loan.update_loan_balance(loan_id,amount_collected)
+69 -1
View File
@@ -7,4 +7,72 @@ class RepaymentService:
""" """
Get the repayment by transaction ID Get the repayment by transaction ID
""" """
return Repayment.get_repayment_by_transaction_id(transaction_id) return Repayment.get_repayment_by_transaction_id(transaction_id)
@staticmethod
def get_repayment_by_id(id):
"""
Get the repayment by ID
"""
return Repayment.get_repayment_by_id(id)
@classmethod
def set_repay_date(cls, repayment_id, customer_id):
"""
Update the repay status of the repayment with the given repayment_id.
"""
return Repayment.set_repay_date(repayment_id, customer_id)
@classmethod
def set_repay_verify_date(cls, repayment_id, customer_id):
"""
Update the verify date of the repayment with the given repayment_id.
"""
return Repayment.set_repay_verify_date(repayment_id, customer_id)
@classmethod
def set_repay_result(cls, repayment_id, result, description):
"""
Update the repay result of the repayment with the given repayment_id.
"""
return Repayment.set_repay_result(repayment_id, result, description)
@classmethod
def set_verify_date_result(cls, repayment_id, result, description):
"""
Update the verify result of the repayment with the given repayment_id.
"""
return Repayment.set_verify_date_result(repayment_id, result, description)
@classmethod
def get_latest_repayment_without_repay_date(cls):
"""
Get the latest repayment without a repay date.
"""
return Repayment.get_latest_repayment_without_repay_date()
@classmethod
def get_latest_repayment_with_loanId(cls,loan_id):
"""
Get the latest repayment with loan id.
"""
return Repayment.get_latest_repayment_with_loanId(loan_id)
@classmethod
def get_latest_loan_with_repay_date(cls):
"""
Get the latest repayment with a repay date and no verification date.
"""
return Repayment.get_latest_loan_with_repay_date()
@classmethod
def add_repayment(cls, data):
"""
Add a new repayment entry.
"""
return Repayment.add_repayment(data)
@classmethod
def create_repayment(cls, repayment_data):
"""
Add a new repayment entry.
"""
return Repayment.create_repayment(repayment_data)
+11
View File
@@ -0,0 +1,11 @@
from app.models import RepaymentsData
class RepaymentService:
@classmethod
def add_repayment_data(cls,data):
"""
Add a new repayment data entry.
"""
return RepaymentsData.add_repayment_data(data)
+24
View File
@@ -0,0 +1,24 @@
from app.models import Salary
class SalaryService:
@classmethod
def add_salary_data(cls,data):
"""
Add a new salary data entry.
"""
return Salary.add_salary_data(data)
@classmethod
def get_pending_salaries(cls):
"""
Get the pending salary for a given customer.
"""
return Salary.get_pending_salaries()
@classmethod
def update_status(cls, salary_id, status):
"""
Update the status of the salary with the given salary_id.
"""
return Salary.update_status(salary_id, status)
+2 -1
View File
@@ -7,4 +7,5 @@ class TransactionService:
""" """
Get the transaction by ID Get the transaction by ID
""" """
return Transaction.get_transaction_by_transaction_id(transaction_id) return Transaction.get_transaction_by_transaction_id(transaction_id)
+43
View File
@@ -13,6 +13,10 @@ info:
servers: servers:
- url: http://localhost:5000 - url: http://localhost:5000
description: Local development server description: Local development server
- url: http://www.simbrellang.net:5000
description: Remote Temporary development server
- url: https://event-core.simbrellang.net
description: Remote development server
paths: paths:
/health: /health:
@@ -110,6 +114,18 @@ paths:
responses: responses:
200: 200:
description: A successful response description: A successful response
/autocall/refresh-verify-collection:
get:
summary: Refresh the disbursement to verify
responses:
200:
description: A successful response
/autocall/refresh-collection:
get:
summary: Refresh the disbursement
responses:
200:
description: A successful response
/autocall/payment-callback: /autocall/payment-callback:
get: get:
summary: The Payment callback summary: The Payment callback
@@ -155,6 +171,33 @@ paths:
comment: comment:
type: string type: string
example: "Testing PenalCharge" example: "Testing PenalCharge"
responses:
200:
description: A successful response
/autocall/analytic-salary-detect:
post:
summary: Salary Detect Endpoint
requestBody:
required: true
content:
application/json:
schema:
type: object
properties:
customerId:
type: string
example: "CN621868"
accountId:
type: string
example: "OP621868"
status:
type: string
salaryAmount:
type: number
example: 200000
salaryDate:
type: string
example: "2025-01-01"
responses: responses:
200: 200:
description: A successful response description: A successful response
+3 -1
View File
@@ -8,4 +8,6 @@ requests
confluent-kafka==1.9.2 confluent-kafka==1.9.2
flask-sqlalchemy flask-sqlalchemy
psycopg2-binary psycopg2-binary
alembic alembic
python-dateutil
oracledb