20 Commits

Author SHA1 Message Date
ameye b0c44db524 Merge branch 'penal_charges' of DigiFi/FirstCore into master 2026-03-16 10:46:25 +00:00
VivianDee 8638510458 [add]: Penal Charges 2026-03-16 11:29:25 +01:00
CHIEFSOFT\ameye 7f6a6350eb added counts 2025-11-03 14:21:26 -05:00
CHIEFSOFT\ameye dbe46a67ff get recent loans 2025-11-03 11:41:13 -05:00
CHIEFSOFT\ameye bddce977a1 Merge branch 'master' of https://gitlab.chiefsoft.net/DigiFi/FirstCore
# Conflicts:
#	app/config.py
2025-10-30 20:11:33 -04:00
CHIEFSOFT\ameye 59fc8439b1 database connect 2025-10-30 19:43:31 -04:00
CHIEFSOFT\ameye e2960d5ba1 fix cnclic 2025-10-30 19:41:38 -04:00
CHIEFSOFT\ameye ac4f06436b Database options 2025-10-30 19:36:35 -04:00
ameye 52fd523739 Merge branch 'add_health' of DigiFi/FirstCore into master 2025-10-15 10:46:34 +00:00
VivianDee f4bc554396 [add]: health endpoint 2025-10-15 11:21:53 +01:00
ameye 4a697f14c8 Merge branch 'oracle_migration' of DigiFi/FirstCore into master 2025-07-11 10:19:17 +00:00
ameye e5df8d92d3 Merge branch 'task_3' of DigiFi/FirstCore into master 2025-07-07 23:17:56 +00:00
Chinenye Nmoh 04861d2c52 added repayment data 2025-07-07 20:25:03 +01:00
VivianDee 3f59ed7da3 [add]: Oracle database configuration 2025-07-07 13:27:16 +01:00
ameye 0957c2ea37 Merge branch 'task_3' of DigiFi/FirstCore into master 2025-07-05 20:05:34 +00:00
Chinenye Nmoh 2d6ff1adc2 updated loan and repayment endpoint 2025-07-05 20:49:57 +01:00
ameye 6bed4d2dfa Merge branch 'task_3' of DigiFi/FirstCore into master 2025-05-28 12:40:51 +00:00
Chinenye Nmoh bbb919d933 completed the task 2025-05-28 13:14:09 +01:00
CHIEFSOFT\ameye 2c46a4390c Sample env 2025-05-26 06:54:20 -04:00
ameye fac6a80284 Merge branch 'testing' of DigiFi/FirstCore into master 2025-05-19 15:04:49 +00:00
22 changed files with 781 additions and 104 deletions
+32 -17
View File
@@ -1,26 +1,41 @@
VALID_APP_ID=**********
VALID_API_KEY=*************
BASIC_AUTH_USERNAME=******
BASIC_AUTH_PASSWORD=******
SIMBRELLA_BASE_URL=***************
# Environment Variables
BASIC_AUTH_USERNAME=user
BASIC_AUTH_PASSWORD=password
SWAGGER_URL="/documentation"
API_URL="/swagger.json"
JWT_SECRET_KEY=******
JWT_ACCESS_TOKEN_EXPIRES=******
JWT_REFRESH_TOKEN_EXPIRES=******
DATABASE_USER=*****
DATABASE_PASSWORD=*****
DATABASE_HOST=******
DATABASE_PORT=******
DATABASE_NAME=*****
# Flask Configuration
FLASK_APP=wsgi.py
FLASK_ENV=development
APP_PORT=4500
APP_PORT=4700
#Database Configuration
#DATABASE_USER=firstadvance
#DATABASE_PASSWORD=FirstAdvance!
#DATABASE_HOST=10.20.30.60
#DATABASE_PORT=5432
#DATABASE_NAME=firstadvancedev
DATABASE_USER=system
DATABASE_PASSWORD=FIRSTADV_PASS
DATABASE_HOST=10.10.33.65
DATABASE_PORT=1521
DATABASE_SID=FREE
# DATABASE_USER=firstadvance
# DATABASE_PASSWORD=FirstAdvance!
# DATABASE_HOST=dev-data.simbrellang.net
# DATABASE_PORT=10532
# DATABASE_NAME=firstadvancedev
#Events if Needed
KAFKA_TIMEOUT=45000.0
KAFKA_BROKER="10.20.30.50:9092"
SIMBRELLA_BASE_URL=***************
+6 -3
View File
@@ -18,7 +18,10 @@ ENV FLASK_APP=app.py
ENV FLASK_RUN_HOST=0.0.0.0
#COPY scripts/enterypointone.sh scripts/enterypointone.sh
#RUN chmod +x scripts/entry.sh
#ENTRYPOINT ["scripts/entry.sh"]
RUN chmod +x scripts/enterypointone.sh
ENTRYPOINT ["scripts/enterypointone.sh"]
# Run the application
CMD [ "sh", "-c", \
"echo 'Starting Gunicorn server...' && \
exec gunicorn -w 4 -b 0.0.0.0:5000 wsgi:wsgi_app"]
+94 -3
View File
@@ -11,6 +11,7 @@ from app.api.services.auth_service import AuthService
from app.api.services.dashboard_service import DashboardService
from app.api.services.offer_service import OfferService
from app.api.services.charge_service import ChargeService
from app.api.services.repayment_data_service import RepaymentDataService
from functools import wraps
from app.utils.logger import logger
from app.api.middlewares import enforce_json, require_auth
@@ -22,6 +23,9 @@ from flask_jwt_extended import (
get_jwt_identity,
create_refresh_token,
)
from sqlalchemy import text
from app.extensions import db
from app.config import settings
api = Blueprint('api', __name__)
@@ -107,8 +111,35 @@ def get_dashboard():
def get_loans():
# Extract query parameters for filtering
filters = {
'id': request.args.get('id'),
'customer_id': request.args.get('customer_id'),
'username': request.args.get('id'),
'email': request.args.get('customer_id'),
'account_id': request.args.get('account_id'),
'status': request.args.get('status'),
'tenor': request.args.get('tenor'),
'offer_id': request.args.get('offer_id'),
'product_id': request.args.get('product_id'),
'transaction_id': request.args.get('transaction_id'),
'original_transaction': request.args.get('original_transaction'),
'start_date': request.args.get('start_date'),
'end_date': request.args.get('end_date'),
'due_before': request.args.get('due_before'),
'due_after': request.args.get('due_after'),
'page': request.args.get('page', 1),
'limit': request.args.get('limit', 20)
}
#logger.info(f"Get loans request received with filters: {filters}")
response = LoanService.process_request(filters)
return response
@api.route('/recent-loans', methods=['GET'])
# @token_required
def get_recent_loans():
# Extract query parameters for filtering
filters = {
'username': request.args.get('id'),
'email': request.args.get('customer_id'),
'account_id': request.args.get('account_id'),
'status': request.args.get('status'),
'tenor': request.args.get('tenor'),
@@ -124,9 +155,12 @@ def get_loans():
'limit': request.args.get('limit', 20)
}
# logger.info(f"Get loans request received with filters: {filters}")
response = LoanService.process_request(filters)
response = LoanService.process_request(filters, True)
return response
@api.route('/transactions', methods=['GET'])
# @token_required
def get_transactions():
@@ -179,6 +213,22 @@ def get_all_repayments():
# logger.info(f"Get repayments request received with filters: {filters}")
response = RepaymentService.get_all_repayments(filters)
return response
@api.route('/repayment-data', methods=['GET'])
# @token_required
def get_all_repayments_data():
# Extract query parameters for filtering
filters = {
'customer_id': request.args.get('customer_id'),
'account_id': request.args.get('account_id'),
'added_date': request.args.get('added_date'),
'transaction_id': request.args.get('transaction_id'),
'fbn_transaction_id': request.args.get('fbn_transaction_id'),
'page': request.args.get('page', 1),
'limit': request.args.get('limit', 20)
}
# logger.info(f"Get repayments request received with filters: {filters}")
response = RepaymentDataService.get_all_repayments_data(filters)
return response
@api.route('/loan-charges', methods=['GET'])
# @token_required
@@ -249,3 +299,44 @@ def get_all_offers():
# # logger.info(f"Get charges request received with filters: {filters}")
# response = ChargeService.get_all_charges(filters)
# return jsonify(response)
# Health Check Endpoint
@api.route("/health", methods=["GET"])
def health_check():
SQLALCHEMY_DATABASE_URI = settings.SQLALCHEMY_DATABASE_URI
response = {}
db_status = "Connection Successful"
errors = []
status = "ok"
# Extract the database URI
try:
db_uri = db.engine.url.render_as_string(hide_password=False)
db_uri = db_uri
except Exception as e:
db_uri = "Unavailable"
errors.append(f"Database URI Error: {str(e)}")
# Check database connection
try:
logger.info(f"Database Health == : {SQLALCHEMY_DATABASE_URI}")
db.session.execute(text("SELECT 1"))
except Exception as e:
db_status = "Connection Failed"
errors.append(f"Database Error: {str(e)}")
status = "failed"
response = {
"status": status,
"db_status": db_status,
"db_uri": db_uri,
"errors": errors or None
}
return jsonify(response), 200 if status == "ok" else 500
+1
View File
@@ -9,3 +9,4 @@ from app.api.services.repayment_service import RepaymentService
from app.api.services.loan_charge_service import LoanChargeService
from app.api.services.loan_repayment_schedule_service import LoanRepaymentScheduleService
from app.api.services.transaction_offers_service import TransactionOfferService
from app.api.services.repayment_data_service import RepaymentDataService
+30 -34
View File
@@ -4,34 +4,30 @@ from app.api.services.base_service import BaseService
from app.models.transaction import Transaction
from app.models.loan import Loan
from sqlalchemy import func, desc
from datetime import datetime, timedelta
from datetime import datetime, timedelta, timezone
from app.extensions import db
from app.api.enums.transaction_type import TransactionType
class DashboardService(BaseService):
@staticmethod
def get_dashboard_data():
"""
Get dashboard summary data.
Returns:
dict: A standardized response with dashboard data.
"""
try:
# Get current date and start of the week
now = datetime.now()
start_of_week = now - timedelta(days=now.weekday())
start_of_week = start_of_week.replace(hour=0, minute=0, second=0, microsecond=0)
# Get loans data for the current week
# Calculate 24 hours ago
last_24_hours = datetime.now(timezone.utc) - timedelta(hours=24)
# Loans this week
loans_this_week = db.session.query(
func.sum(Loan.initial_loan_amount)
).filter(
Loan.created_at >= start_of_week
).scalar() or 0
# Get payments data for the current week
# Assuming payments are transactions with type 'PAYMENT'
# Payments this week
payments_this_week = db.session.query(
func.count(Transaction.id)
).filter(
@@ -39,51 +35,53 @@ class DashboardService(BaseService):
Transaction.type == 'PAYMENT'
).scalar() or 0
# Get request summary counts
# These are placeholders - needed to adjust based on your actual data model
# Request summary for the last 24 hours
eligibility_check_count = db.session.query(
func.count(Transaction.id)
).filter(
Transaction.type == 'ELIGIBILITY_CHECK'
Transaction.type == TransactionType.ELIGIBILITY_CHECK.value,
Transaction.created_at >= last_24_hours
).scalar() or 0
select_offer_count = db.session.query(
func.count(Transaction.id)
).filter(
Transaction.type == 'SELECT_OFFER'
Transaction.type == TransactionType.SELECT_OFFER.value,
Transaction.created_at >= last_24_hours
).scalar() or 0
provide_loan_count = db.session.query(
func.count(Transaction.id)
).filter(
Transaction.type == 'PROVIDE_LOAN'
Transaction.type == TransactionType.PROVIDE_LOAN.value,
Transaction.created_at >= last_24_hours
).scalar() or 0
repayment_count = db.session.query(
func.count(Transaction.id)
).filter(
Transaction.type == 'REPAYMENT'
Transaction.type == TransactionType.REPAYMENT.value,
Transaction.created_at >= last_24_hours
).scalar() or 0
# Get recent transactions
# Recent transactions (not limited to 24 hrs, just latest 15)
recent_transactions = Transaction.query.order_by(
Transaction.id.desc()
).limit(15).all()
# Format recent transactions
recent_transactions_data = []
for transaction in recent_transactions:
recent_transactions_data.append({
'id': transaction.id,
'transaction_id': transaction.transaction_id,
'account_id': transaction.account_id,
'type': transaction.type,
'channel': transaction.channel,
'created_at': transaction.created_at.isoformat() if transaction.created_at else None,
'updated_at': transaction.updated_at.isoformat() if transaction.updated_at else None
})
recent_transactions_data = [{
'id': t.id,
'transaction_id': t.transaction_id,
'account_id': t.account_id,
'type': t.type,
'channel': t.channel,
'created_at': t.created_at.isoformat() if t.created_at else None,
'updated_at': t.updated_at.isoformat() if t.updated_at else None
} for t in recent_transactions]
# Prepare response data
# Final response
dashboard_data = {
"loans": {
"value": float(loans_this_week),
@@ -110,6 +108,4 @@ class DashboardService(BaseService):
except Exception as e:
logger.error(f"An error occurred while getting dashboard data: {str(e)}", exc_info=True)
return jsonify({
"message": "Internal Server Error"
}), 500
return jsonify({"message": "Internal Server Error"}), 500
@@ -82,6 +82,9 @@ class LoanRepaymentScheduleService:
'total_repayment_amount': schedule.total_repayment_amount,
'paid': schedule.paid,
'paid_at': schedule.paid_at.isoformat() if schedule.paid_at else None,
'penal_charge': schedule.penal_charge,
'penal_count': schedule.penal_count,
'last_penal_date': schedule.last_penal_date.isoformat() if schedule.last_penal_date else None,
'created_at': schedule.created_at.isoformat() if schedule.created_at else None,
'updated_at': schedule.updated_at.isoformat() if schedule.updated_at else None
})
+15 -4
View File
@@ -13,7 +13,7 @@ class LoanService:
"""
@staticmethod
def process_request(filters=None):
def process_request(filters=None, recent_only=False):
"""
Process the get loans request.
@@ -78,10 +78,11 @@ class LoanService:
due_before=due_before,
due_after=due_after,
page=page,
limit=limit
limit=limit,
recent_only=recent_only,
)
logger.info(f"Result from loans model cme back")
logger.info(f"Result from loans model cme back ")
# Convert loans to dictionary format
loans_data = []
@@ -98,6 +99,8 @@ class LoanService:
'current_loan_amount': loan.current_loan_amount,
'status': loan.status,
'tenor': loan.tenor,
'balance': loan.balance,
'reference': loan.reference,
'product_id': loan.product_id,
'default_penalty_fee': loan.default_penalty_fee,
'continuous_fee': loan.continuous_fee,
@@ -106,7 +109,15 @@ class LoanService:
'installment_amount': loan.installment_amount,
'due_date': loan.due_date.isoformat() if loan.due_date else None,
'created_at': loan.created_at.isoformat() if loan.created_at else None,
'updated_at': loan.updated_at.isoformat() if loan.updated_at else None
'updated_at': loan.updated_at.isoformat() if loan.updated_at else None,
'disburseResult': loan.disburse_result,
'disburseDescription': loan.disburse_description,
'verifyResult': loan.verify_result,
'verifyDescription': loan.verify_description,
'disburseDate': loan.disburse_date.isoformat() if loan.disburse_date else None,
'disburseVerify': loan.disburse_verify.isoformat() if loan.disburse_verify else None,
'totalPenalCharge': loan.total_penal_charge,
'lastPenalDate': loan.last_penal_date.isoformat() if loan.last_penal_date else None,
})
# Calculate total pages
@@ -0,0 +1,82 @@
import logging
from datetime import datetime
from app.models.repayment_data import RepaymentsData
from dateutil.parser import parse as parse_date
logger = logging.getLogger(__name__)
class RepaymentDataService:
"""
Service class for handling repayment-data operations.
"""
@staticmethod
def get_all_repayments_data(filters=None):
try:
if filters is None:
filters = {}
customer_id = filters.get('customer_id')
account_id = filters.get('account_id')
added_date = filters.get('added_date')
transaction_id = filters.get('transaction_id')
fbn_transaction_id = filters.get('fbn_transaction_id')
page = int(filters.get('page', 1))
limit = int(filters.get('limit', 20))
if page < 1:
page = 1
if limit < 1 or limit > 100:
limit = 20
if added_date and isinstance(added_date, str):
try:
added_date = parse_date(added_date)
except Exception as parse_err:
logger.error(f"Invalid date format for 'added_date': {added_date}")
return {"message": "Invalid date format for 'added_date'"}, 400
repayments_data, total_count = RepaymentsData.get_all_repayment_data(
customer_id=customer_id,
account_id=account_id,
added_date=added_date,
transaction_id=transaction_id,
fbn_transaction_id=fbn_transaction_id,
page=page,
limit=limit
)
repayments_list = []
for repayment in repayments_data:
repayments_list.append({
'customer_id': repayment.customer_id,
'account_id': repayment.account_id,
'transaction_id': repayment.transaction_id,
'fbn_transaction_id': repayment.fbn_transaction_id,
'added_date': repayment.added_date.isoformat() if repayment.added_date else None,
'response_code': repayment.response_code,
'response_descr': repayment.response_descr,
'repayment_amount': repayment.repayment_amount,
'amount_collected': repayment.amount_collected,
'balance': repayment.balance
})
total_pages = (total_count + limit - 1) // limit
return {
'repayment_data': repayments_list,
'count': len(repayments_list),
'pagination': {
'total_count': total_count,
'total_pages': total_pages,
'current_page': page,
'limit': limit,
'has_next': page < total_pages,
'has_prev': page > 1
}
}
except Exception as e:
logger.error(f"An error occurred: {str(e)}", exc_info=True)
return {"message": "Internal Server Error"}, 500
+9 -1
View File
@@ -69,7 +69,15 @@ class RepaymentService:
'product_id': repayment.product_id,
'transaction_id': repayment.transaction_id,
'created_at': repayment.created_at.isoformat() if repayment.created_at else None,
'updated_at': repayment.updated_at.isoformat() if repayment.updated_at else None
'updated_at': repayment.updated_at.isoformat() if repayment.updated_at else None,
'repay_date': repayment.repay_date.isoformat() if repayment.repay_date else None,
'initiated_by': repayment.initiated_by,
'salary_amount': repayment.salary_amount,
'verify_date': repayment.verify_date.isoformat() if repayment.verify_date else None,
'repay_result': repayment.repay_result,
'repay_description': repayment.repay_description,
'verify_result': repayment.verify_result,
'verify_description': repayment.verify_description
})
# Calculate total pages
+1
View File
@@ -72,6 +72,7 @@ class TransactionService:
'transaction_id': transaction.transaction_id,
'account_id': transaction.account_id,
'type': transaction.type,
'customer_id': transaction.customer_id,
'channel': transaction.channel,
'created_at': transaction.created_at.isoformat(),
'updated_at': transaction.updated_at.isoformat()
+8 -1
View File
@@ -19,8 +19,15 @@ class Config:
DATABASE_HOST = os.environ.get("DATABASE_HOST")
DATABASE_PORT = os.environ.get("DATABASE_PORT", 10532)
DATABASE_NAME = os.environ.get("DATABASE_NAME")
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_INTERNAL = (f"oracle+oracledb://{DATABASE_USER}:{DATABASE_PASSWORD}@{DNS}")
SQLALCHEMY_DATABASE_URI = os.getenv("SQLALCHEMY_DATABASE_URI_FULL", SQLALCHEMY_DATABASE_URI_INTERNAL)
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")
+63 -36
View File
@@ -36,6 +36,18 @@ class Loan(db.Model):
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))
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)
balance = db.Column(db.Float, nullable=True, default=0.0)
total_penal_charge = db.Column(db.Float, default=0.0)
last_penal_date = db.Column(db.DateTime, nullable=True)
customer = relationship(
"Customer",
@@ -47,7 +59,7 @@ class Loan(db.Model):
@classmethod
def get_all_loans(cls, id=None, customer_id=None, account_id=None, status=None, tenor=None, offer_id=None,
product_id=None, start_date=None, end_date=None, due_before=None, due_after=None,
transaction_id=None, original_transaction=None, page=1, limit=20):
transaction_id=None, original_transaction=None, page=1, limit=20, recent_only= False):
"""
Get all loans with optional filtering
@@ -71,55 +83,60 @@ class Loan(db.Model):
"""
query = cls.query
logger.info(f"Get all loan models from loans model cme back")
# Apply filters if provided
if id:
query = query.filter(cls.id == id)
if recent_only:
query = query.order_by(cls.created_at.desc()).limit(10)
# Get total count before pagination
total_count = query.count()
else:
# Apply filters if provided
if id:
query = query.filter(cls.id == id)
if customer_id:
query = query.filter(cls.customer_id == customer_id)
if customer_id:
query = query.filter(cls.customer_id == customer_id)
if account_id:
query = query.filter(cls.account_id == account_id)
if account_id:
query = query.filter(cls.account_id == account_id)
if status:
query = query.filter(cls.status == status)
if status:
query = query.filter(cls.status == status)
if tenor:
query = query.filter(cls.tenor == tenor)
if tenor:
query = query.filter(cls.tenor == tenor)
if offer_id:
query = query.filter(cls.offer_id == offer_id)
if offer_id:
query = query.filter(cls.offer_id == offer_id)
if product_id:
query = query.filter(cls.product_id == product_id)
if product_id:
query = query.filter(cls.product_id == product_id)
if transaction_id:
query = query.filter(cls.transaction_id == transaction_id)
if transaction_id:
query = query.filter(cls.transaction_id == transaction_id)
if original_transaction:
query = query.filter(cls.original_transaction == original_transaction)
if original_transaction:
query = query.filter(cls.original_transaction == original_transaction)
if start_date:
query = query.filter(cls.created_at >= start_date)
if start_date:
query = query.filter(cls.created_at >= start_date)
if end_date:
query = query.filter(cls.created_at <= end_date)
if end_date:
query = query.filter(cls.created_at <= end_date)
if due_before:
query = query.filter(cls.due_date <= due_before)
if due_before:
query = query.filter(cls.due_date <= due_before)
if due_after:
query = query.filter(cls.due_date >= due_after)
if due_after:
query = query.filter(cls.due_date >= due_after)
# Order by created_at descending (newest first)
query = query.order_by(cls.created_at.desc())
# Order by created_at descending (newest first)
query = query.order_by(cls.created_at.desc())
# Get total count before pagination
total_count = query.count()
# Get total count before pagination
total_count = query.count()
# Apply pagination
offset = (page - 1) * limit
query = query.limit(limit).offset(offset)
# Apply pagination
offset = (page - 1) * limit
query = query.limit(limit).offset(offset)
return query.all(), total_count
@@ -147,7 +164,17 @@ class Loan(db.Model):
'installment_amount': self.installment_amount,
'due_date': self.due_date.isoformat() if self.due_date else None,
'created_at': self.created_at.isoformat() if self.created_at else None,
'updated_at': self.updated_at.isoformat() if self.updated_at else None
'updated_at': self.updated_at.isoformat() if self.updated_at else None,
'disburseResult': self.disburse_result,
'disburseDescription': self.disburse_description,
'verifyResult': self.verify_result,
'verifyDescription': self.verify_description,
'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,
'totalPenalCharge': self.total_penal_charge,
'lastPenalDate': self.last_penal_date.isoformat() if self.last_penal_date else None,
}
def __repr__(self):
+9
View File
@@ -20,6 +20,12 @@ class LoanRepaymentSchedule(db.Model):
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))
penal_charge = db.Column(db.Float, default=0.0)
penal_count = db.Column(db.Integer, default=0)
last_penal_date = db.Column(db.DateTime, nullable=True)
# loan = relationship(
# "Loan",
# primaryjoin="LoanRepaymentSchedule.loan_id == Loan.id",
@@ -96,6 +102,9 @@ class LoanRepaymentSchedule(db.Model):
'total_repayment_amount': self.total_repayment_amount,
'paid': self.paid,
'paid_at': self.paid_at.isoformat() if self.paid_at else None,
'penal_charge': self.penal_charge,
'penal_count': self.penal_count,
'last_penal_date': self.last_penal_date.isoformat() if self.last_penal_date else None,
'created_at': self.created_at.isoformat() if self.created_at else None,
'updated_at': self.updated_at.isoformat() if self.updated_at else None
}
+18 -1
View File
@@ -12,6 +12,14 @@ class Repayment(db.Model):
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))
transaction_id = db.Column(db.String(50))
repay_date = db.Column(db.DateTime, nullable=True)
initiated_by = db.Column(db.String(50), nullable=True)
salary_amount = db.Column(db.Float, nullable=True, default=0.0)
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)
@classmethod
def get_all_repayments(cls, loan_id=None, customer_id=None, product_id=None,
@@ -71,7 +79,16 @@ class Repayment(db.Model):
'product_id': self.product_id,
'transaction_id': self.transaction_id,
'created_at': self.created_at.isoformat() if self.created_at else None,
'updated_at': self.updated_at.isoformat() if self.updated_at else None
'updated_at': self.updated_at.isoformat() if self.updated_at else None,
'repay_date': self.repay_date.isoformat() if self.repay_date else None,
'initiated_by': self.initiated_by,
'salary_amount': self.salary_amount,
'verify_date': self.verify_date.isoformat() if self.verify_date else None,
'repay_result': self.repay_result,
'repay_description': self.repay_description,
'verify_result': self.verify_result,
'verify_description': self.verify_description
}
def __repr__(self):
+80
View File
@@ -0,0 +1,80 @@
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.account_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 get_all_repayment_data(cls, customer_id=None, account_id=None, transaction_id=None, fbn_transaction_id=None,
added_date=None, page=1, limit=20):
"""
Get all repayment data with optional filtering
Args:
customer_id (str, optional): Filter by customer ID
account_id (str, optional): Filter by account ID
added_date (datetime, optional): Filter by added date
transaction_id (str, optional): Filter by transaction ID
fbn_transaction_id (str, optional): Filter by FBN transaction ID
page (int, optional): Page number for pagination
limit (int, optional): Number of items per page
Returns:
tuple: (list of Repayment objects, total count)
"""
query = cls.query
if customer_id:
query = query.filter(cls.customer_id == customer_id)
if account_id:
query = query.filter(cls.account_id == account_id)
if transaction_id:
query = query.filter(cls.transaction_id == transaction_id)
if fbn_transaction_id:
query = query.filter(cls.fbn_transaction_id == fbn_transaction_id)
if added_date:
query = query.filter(cls.added_date >= added_date)
# Get total count before pagination
total_count = query.count()
#order
query = query.order_by(cls.added_date.desc())
# Apply pagination
offset = (page - 1) * limit
query = query.limit(limit).offset(offset)
return query.all(), total_count
+61 -3
View File
@@ -33,14 +33,17 @@
"url": "http://www.simbrellang.net:14700"
},
{
"url" : "http://10.10.11.17:4300"
"url": "http://10.10.11.17:4300"
},
{
"url" : "http://10.10.11.17:4700"
"url": "http://10.10.11.17:4700"
},
{
"url": "http://10.2.249.133:4700"
}
],
"tags": [
{
{
"name": "Authorize",
"description": "This feature will be used for authorizing customers.",
"externalDocs": {
@@ -80,6 +83,14 @@
"url": "https://www.simbrellang.net"
}
},
{
"name": "Repayment Data",
"description": "Get all repayment data with optional filtering.",
"externalDocs": {
"description": "Find out more",
"url": "https://www.simbrellang.net"
}
},
{
"name": "Offers",
"description": "Get all offers with optional filtering.",
@@ -95,6 +106,10 @@
"description": "Find out more",
"url": "https://www.simbrellang.net"
}
},
{
"name": "Health",
"description": "System health check including DB status."
}
],
"paths": {
@@ -119,6 +134,9 @@
"/loan-charges": {
"$ref": "../swagger/paths/LoanCharges.json"
},
"/repayment-data": {
"$ref": "../swagger/paths/RepaymentData.json"
},
"/repayment-schedules": {
"$ref": "../swagger/paths/RepaymentSchedules.json"
},
@@ -127,6 +145,43 @@
},
"/transaction-offers": {
"$ref": "../swagger/paths/TransactionOffers.json"
},
"/health": {
"get": {
"tags": [
"Health"
],
"summary": "Health Check",
"description": "Returns service health information including DB connection status.",
"responses": {
"200": {
"description": "Health check successful",
"content": {
"application/json": {
"example": {
"status": "ok",
"db_status": "Connection Successful",
"error": []
}
}
}
},
"500": {
"description": "Health check failed",
"content": {
"application/json": {
"example": {
"status": "ok",
"db_status": "Connection Failed",
"error": [
"could not connect to server: Connection refused"
]
}
}
}
}
}
}
}
},
"components": {
@@ -158,6 +213,9 @@
"RepaymentsResponse": {
"$ref": "../swagger/schemas/RepaymentsResponse.json"
},
"RepaymentDataResponse": {
"$ref": "../swagger/schemas/RepaymentDataResponse.json"
},
"LoanChargesResponse": {
"$ref": "../swagger/schemas/LoanChargesResponse.json"
},
+104
View File
@@ -0,0 +1,104 @@
{
"get": {
"tags": ["RepaymentData"],
"summary": "Get all repayment data with optional filtering",
"description": "Retrieve repayment data records with optional filtering by transaction ID, customer ID, account ID, FBN transaction ID, and added date. Supports pagination.",
"operationId": "getRepaymentData",
"parameters": [
{
"name": "transaction_id",
"in": "query",
"description": "Filter by transaction ID",
"required": false,
"schema": {
"type": "string"
},
"example": "TRX123456789"
},
{
"name": "customer_id",
"in": "query",
"description": "Filter by customer ID",
"required": false,
"schema": {
"type": "string"
},
"example": "CID0000025585"
},
{
"name": "account_id",
"in": "query",
"description": "Filter by account ID",
"required": false,
"schema": {
"type": "string"
},
"example": "ACCT000000123"
},
{
"name": "fbn_transaction_id",
"in": "query",
"description": "Filter by FBN transaction ID",
"required": false,
"schema": {
"type": "string"
},
"example": "FBNTRX7890"
},
{
"name": "added_date",
"in": "query",
"description": "Filter by added date (ISO format)",
"required": false,
"schema": {
"type": "string",
"format": "date-time"
},
"example": "2024-01-01T00:00:00Z"
},
{
"name": "page",
"in": "query",
"description": "Page number for pagination",
"required": false,
"schema": {
"type": "integer",
"default": 1,
"minimum": 1
},
"example": 1
},
{
"name": "limit",
"in": "query",
"description": "Number of items per page (max 100)",
"required": false,
"schema": {
"type": "integer",
"default": 20,
"minimum": 1,
"maximum": 100
},
"example": 20
}
],
"responses": {
"200": {
"description": "Successful operation",
"content": {
"application/json": {
"schema": {
"$ref": "../schemas/RepaymentDataResponse.json"
}
}
}
},
"400": {
"description": "Invalid request"
},
"500": {
"description": "Internal server error"
}
}
}
}
+11
View File
@@ -76,6 +76,17 @@
"example": 10500.0,
"nullable": true
},
"balance": {
"type": "number",
"format": "float",
"example": 5000.0,
"nullable": true
},
"reference": {
"type": "string",
"example": "REF12345",
"nullable": true
},
"installment_amount": {
"type": "number",
"format": "float",
@@ -0,0 +1,106 @@
{
"type": "object",
"properties": {
"repayment_data": {
"type": "array",
"items": {
"type": "object",
"properties": {
"id": {
"type": "integer",
"example": 1
},
"transaction_id": {
"type": "string",
"example": "TRX123456",
"nullable": false
},
"added_date": {
"type": "string",
"format": "date-time",
"example": "2025-04-10T16:45:47.879Z"
},
"response_code": {
"type": "string",
"example": "00",
"nullable": true
},
"response_descr": {
"type": "string",
"example": "Repayment successful",
"nullable": true
},
"fbn_transaction_id": {
"type": "string",
"example": "FBN123456",
"nullable": true
},
"customer_id": {
"type": "string",
"example": "CID0000025585",
"nullable": true
},
"account_id": {
"type": "string",
"example": "ACCT000000123",
"nullable": true
},
"repayment_amount": {
"type": "number",
"format": "float",
"example": 15000.0,
"nullable": true
},
"amount_collected": {
"type": "number",
"format": "float",
"example": 14500.0,
"nullable": true
},
"balance": {
"type": "number",
"format": "float",
"example": 500.0,
"nullable": true
}
}
}
},
"count": {
"type": "integer",
"example": 1
},
"pagination": {
"type": "object",
"properties": {
"total_count": {
"type": "integer",
"example": 100
},
"total_pages": {
"type": "integer",
"example": 5
},
"current_page": {
"type": "integer",
"example": 1
},
"limit": {
"type": "integer",
"example": 20
},
"has_next": {
"type": "boolean",
"example": true
},
"has_prev": {
"type": "boolean",
"example": false
}
}
}
},
"xml": {
"name": "RepaymentDataResponse"
}
}
@@ -23,6 +23,50 @@
"example": "TRX123456",
"nullable": true
},
"initiated_by": {
"type": "string",
"example": "system",
"nullable": true
},
"salary_amount": {
"type": "number",
"format": "float",
"example": 1000.0,
"nullable": true
},
"repay_date": {
"type": "string",
"format": "date-time",
"example": "2025-04-10T16:45:47.879552Z",
"nullable": true
},
"verify_date": {
"type": "string",
"format": "date-time",
"example": "2025-04-10T16:45:47.879552Z",
"nullable": true
},
"repay_result": {
"type": "string",
"example": "success",
"nullable": true
},
"repay_description": {
"type": "string",
"example": "Repayment processed successfully",
"nullable": true
},
"verify_result": {
"type": "string",
"example": "verified",
"nullable": true
},
"verify_description": {
"type": "string",
"example": "Verification completed successfully",
"nullable": true
},
"created_at": {
"type": "string",
"format": "date-time",
+3
View File
@@ -6,11 +6,14 @@ flask-sqlalchemy
flask-migrate
psycopg2-binary
alembic
oracledb
# Schema for validations
Flask-Marshmallow==0.15.0
marshmallow==3.19.0
python-dateutil==2.9.0
# CORS
Flask-Cors==3.0.10