Compare commits

..

4 Commits

Author SHA1 Message Date
VivianDee bcb4ae183d [add]: eco models 2025-07-30 09:19:43 +01:00
VivianDee cb0d9938c6 [add]: eco routes, services and enums 2025-07-25 15:14:30 +01:00
VivianDee 5c99bf96c2 [feat]: add models bind to eco database 2025-07-25 15:13:59 +01:00
VivianDee 8c215a7f35 [add]: Multi-database support using SQLAlchemy binds 2025-07-25 15:13:07 +01:00
75 changed files with 1866 additions and 1155 deletions
+18 -10
View File
@@ -7,18 +7,26 @@ SWAGGER_URL="/documentation"
API_URL="/swagger.json"
# Database Configuration
DATABASE_USER=firstadvance
DATABASE_PASSWORD=FirstAdvance!
DATABASE_HOST=dev-data.simbrellang.net
DATABASE_PORT=10532
DATABASE_NAME=firstadvancedev
#SQLALCHEMY_DATABASE_URI_FULL="oracle+oracledb://FIRSTADVSTG:Pchanged_56789@10.2.110.30:1521/?service_name=firstadv"
# DATABASE_HOST=10.20.30.60
# DATABASE_USER=firstadvance
# DATABASE_PASSWORD=firstadvance
# DATABASE_PASSWORD=FirstAdvance!
# DATABASE_HOST=dev-data.simbrellang.net
# DATABASE_PORT=10532
# DATABASE_NAME=firstadvancedev
# DATABASE_PORT=5432
# ECO_DATABASE_USER=username
# ECO_DATABASE_PASSWORD=password
# ECO_DATABASE_HOST=localhost
# ECO_DATABASE_PORT=5432
# ECO_DATABASE_NAME=eco_db
DATABASE_USER=system
DATABASE_PASSWORD=FIRSTADV_PASS
DATABASE_HOST=209.195.2.27
# DATABASE_HOST=10.10.33.65
DATABASE_PORT=1521
DATABASE_SID=FREE
# Flask Configuration
FLASK_APP=wsgi.py
+2
View File
@@ -70,6 +70,7 @@ You can check if the Flask application is running by accessing the `/health` end
```bash
curl http://localhost:4500/health
curl http://localhost:4500/eco/health
```
If the application is running properly, you should receive a response similar to this:
@@ -87,6 +88,7 @@ You can check the Swagger Doc by accessing the `/documentation` endpoint. Run th
```bash
curl http://localhost:4500/documentation
curl http://localhost:4500/eco/documentation
```
-40
View File
@@ -1,40 +0,0 @@
# Environment Variables ======================================================
BASIC_AUTH_USERNAME=user
BASIC_AUTH_PASSWORD=password
SWAGGER_URL="/documentation"
API_URL="/swagger.json"
# Flask Configuration =========================================================
FLASK_APP=wsgi.py
FLASK_ENV=development
APP_PORT=4500
#Database Configuration =======================================================
DATABASE_USER=FIRSTADVSTG
DATABASE_PASSWORD=Pchanged_56789
DATABASE_HOST=ig-x6-uat-scan
DATABASE_PORT=1521
DATABASE_NAME=FIRSTADVSTG
DATABASE_SID=firstadv
SQLALCHEMY_DATABASE_URI_FULL="oracle+oracledb://FIRSTADVSTG:Pchanged_56789@10.2.110.30:1521/?service_name=firstadv"
# Event Bus =====================================================================
KAFKA_BROKER="10.2.110.20:9082"
#Bank Calls =====================================================================
SIMBRELLA_BASE_URL="https://bank-emulator.dev.simbrellang.net"
SIMBRELLA_APP_ID="app1"
SIMBRELLA_API_KEY="testtest-api-key-12345"
#Events Direct Location =========================================================
EVENTS_SERVICE_BASE_URL="http://10.2.24.133:5000"
ENDPOINT_DIRECT_LOAN="/autocall/direct/loan"
ENDPOINT_DIRECT_REPAYMENT="/autocall/direct/repayment"
#EVENTS_SERVICE_BASE_URL2="https://event-core.simbrellang.net"
#EVENTS_SERVICE_BASE_URL="http://10.10.11.17:14700"
+29 -26
View File
@@ -1,3 +1,4 @@
from re import U
from flask import Flask
from flask_mail import Mail
import os
@@ -5,6 +6,7 @@ from flask_swagger_ui import get_swaggerui_blueprint
from flask_cors import CORS
from app.config import Config
from app.api.routes import api
from app.eco.routes import eco
from app.errors import register_error_handlers
from flask_sqlalchemy import SQLAlchemy
from flask_migrate import Migrate
@@ -20,47 +22,48 @@ from flask_jwt_extended import (
def create_app():
"""Factory function to create a Flask app instance"""
# import oracledb
# oracledb.init_oracle_client(lib_dir=None)
app = Flask(__name__)
# Load configuration
app.config.from_object(Config)
CORS(app)
JWTManager(app)
CORS(app, supports_credentials=True)
CORS(app, supports_credentials=True)
# Swagger Doc
SWAGGER_URL = app.config.get("SWAGGER_URL")
API_URL = app.config.get("API_URL")
try:
# Swagger Doc
SWAGGER_URL = app.config.get("SWAGGER_URL")
API_URL = app.config.get("API_URL")
# Register blueprints
app.register_blueprint(api)
app.register_blueprint(eco, url_prefix="/eco")
# Register blueprints
app.register_blueprint(api)
swagger_ui_blueprint = get_swaggerui_blueprint(SWAGGER_URL, API_URL)
app.register_blueprint(swagger_ui_blueprint, url_prefix=SWAGGER_URL)
swagger_ui_blueprint = get_swaggerui_blueprint(SWAGGER_URL, API_URL)
app.register_blueprint(swagger_ui_blueprint, url_prefix=SWAGGER_URL)
# Second UI (ECO)
eco_docs = "/eco" + SWAGGER_URL
eco_api = "/eco" + API_URL
swagger_ui_eco = get_swaggerui_blueprint(eco_docs, eco_api)
swagger_ui_eco.name = 'swagger_ui_eco' # Rename blueprint
app.register_blueprint(swagger_ui_eco, url_prefix=eco_docs)
except Exception as e:
print(f"Swagger Unexpected error occurred: {e}")
# Error Handlers
register_error_handlers(app)
from . import models
try:
# Error Handlers
register_error_handlers(app)
# Initialize Flask-Mail
mail.init_app(app)
from . import models
# Database and Migrations
db.init_app(app)
# Initialize Flask-Mail
mail.init_app(app)
migrate.init_app(app, db)
# Database and Migrations
db.init_app(app)
migrate.init_app(app, db)
return app
except Exception as e:
print(f"An unexpected error occurred: {e}")
return app
+1 -2
View File
@@ -1,3 +1,2 @@
from .simbrella import SimbrellaIntegration
from .kafka import KafkaIntegration
from .events_service import EventServiceIntegration
from .kafka import KafkaIntegration
-69
View File
@@ -1,69 +0,0 @@
import httpx
from app.utils.logger import logger
from app.config import settings
class EventServiceIntegration:
BASE_URL = settings.SIMBRELLA_BASE_URL
EVENTS_SERVICE_BASE_URL = settings.EVENTS_SERVICE_BASE_URL
ENDPOINT_DIRECT_LOAN = settings.ENDPOINT_DIRECT_LOAN
ENDPOINT_DIRECT_REPAYMENT = settings.ENDPOINT_DIRECT_REPAYMENT
@staticmethod
def direct_loan(transaction_id: str):
"""
Calls the Direct Loan endpoint
"""
url = f"{EventServiceIntegration.EVENTS_SERVICE_BASE_URL}{EventServiceIntegration.ENDPOINT_DIRECT_LOAN}"
logger.info(f"Direct Loan URL: {url}")
payload = {"transactionId": str(transaction_id)}
headers = {
"Content-Type": "application/json"
}
try:
response = httpx.post(url, json=payload, headers=headers, timeout=10.0)
logger.info(f"Loan Response: {response.text}")
return response
except Exception as e:
logger.error(f"Direct Loan API call failed: {str(e)}", exc_info=True)
raise
@staticmethod
def direct_repayment(transaction_id: str):
"""
Calls the Direct Repayment endpoint
"""
url = f"{EventServiceIntegration.EVENTS_SERVICE_BASE_URL}{EventServiceIntegration.ENDPOINT_DIRECT_REPAYMENT}"
logger.info(f"Direct Repayment URL: {url}")
payload = {"transactionId": str(transaction_id)}
headers = {
"Content-Type": "application/json",
}
try:
response = httpx.post(url, json=payload, headers=headers, timeout=10.0)
logger.info(f"Repayment Response: {response.text}")
return response
except Exception as e:
logger.error(f"Direct Repayment API call failed: {str(e)}", exc_info=True)
raise
@staticmethod
def health_check():
"""
Health check for Events Service
"""
url = f"{EventServiceIntegration.EVENTS_SERVICE_BASE_URL}/health"
logger.info(f"Health Check URL: {url}")
try:
response = httpx.get(url, timeout=5.0)
logger.info(f"Health Check Response: {response.text}")
return response
except Exception as e:
logger.error(f"Health Check API call failed: {str(e)}", exc_info=True)
raise
+8 -83
View File
@@ -1,64 +1,13 @@
from os import access
import httpx
import time
import json
from app.utils.logger import logger
from app.config import settings
import logging
class SimbrellaIntegration:
BASE_URL = settings.SIMBRELLA_BASE_URL
ENDPOINT_RAC_CHECKS = settings.SIMBRELLA_ENDPOINT_RAC_CHECKS
HEALTH_ENDPOINT = settings.SIMBRELLA_HEALTH
AUTH_ENDPOINT = settings.BANK_CALL_AUTH_ENDPOINT
_access_token = None # cache token in memory
_token_expiry = 0
@staticmethod
def generate_token():
"""
Generate a new access token using the username and password from settings.
"""
url = f"{SimbrellaIntegration.BASE_URL}{SimbrellaIntegration.AUTH_ENDPOINT}"
payload = {
"username": settings.BANK_CALL_USERNAME,
"password": settings.BANK_CALL_PASSWORD,
"grant_type": "password"
}
headers = {"Content-Type": "application/json"}
try:
logger.info(f"Requesting Bank token from {url}")
response = httpx.post(url, json=payload, headers=headers, timeout=10.0)
response.raise_for_status()
data = response.json()
expires_in = data.get("expires_in", 1800)
SimbrellaIntegration._access_token = data.get("access_token")
SimbrellaIntegration._token_expiry = time.time() + expires_in - 60
if not SimbrellaIntegration._access_token:
raise Exception("Access token not found in Bank Authorization response")
logger.info("Successfully retrieved Bank access token")
return SimbrellaIntegration._access_token
except Exception as e:
logger.error(f"Token generation failed: {str(e)}", exc_info=True)
raise Exception(f"Token generation failed: {str(e)}")
@staticmethod
def _get_token():
"""
Return a valid token, refreshing if expired or missing
"""
if not SimbrellaIntegration._access_token or time.time() >= SimbrellaIntegration._token_expiry:
return SimbrellaIntegration.generate_token()
return SimbrellaIntegration._access_token
@staticmethod
def rac_check(customer_id, account_id, transaction_id):
@@ -77,14 +26,13 @@ class SimbrellaIntegration:
"channel": "USSD"
}
try:
access_token = SimbrellaIntegration._get_token()
headers = {
"Content-Type": "application/json",
"Authorization": f"Bearer {access_token}"
}
headers = {
"Content-Type": "application/json",
"x-api-key": f"{settings.VALID_API_KEY}",
"App-Id": f"{settings.VALID_APP_ID}",
}
try:
response = httpx.post(url, json=payload, headers=headers, timeout=10.0)
logger.info(f"This is Response: {str(response)}", exc_info=True)
@@ -94,27 +42,4 @@ class SimbrellaIntegration:
except Exception as e:
logger.error(f"RACCheck API call failed: {str(e)}", exc_info=True)
raise Exception(f"RACCheck API call failed: {str(e)}")
@staticmethod
def health_check():
"""
Health check for Bank Service
"""
url = f"{SimbrellaIntegration.BASE_URL}/{SimbrellaIntegration.HEALTH_ENDPOINT}"
logger.info(f"Bank Health Check URL: {url}")
try:
access_token = SimbrellaIntegration._get_token()
headers = {
"Content-Type": "application/json",
"Authorization": f"Bearer {access_token}"
}
response = httpx.get(url, headers=headers, timeout=10.0)
logger.info(f"Bank Health Check Response: {response.text}")
return response
except Exception as e:
logger.error(f"Bank Health Check API call failed: {str(e)}", exc_info=True)
raise Exception(f"Bank Health Check API call failed: {str(e)}")
+2 -73
View File
@@ -1,5 +1,3 @@
from app.api.integrations.events_service import EventServiceIntegration
from app.api.integrations.simbrella import SimbrellaIntegration
from flask import Blueprint, request, jsonify, send_from_directory
from app.api.services import (
EligibilityCheckService,
@@ -21,9 +19,6 @@ 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__)
@@ -118,77 +113,11 @@ def notification_callback():
response = NotificationCallbackService.process_request(data)
return response
# Health Check Endpoint
@api.route("/health", methods=["GET"])
def health_check():
SQLALCHEMY_DATABASE_URI = settings.SQLALCHEMY_DATABASE_URI
response = {}
db_status = "Connection Successful"
events_service_status = "Connection Successful"
bank_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"
# Check database connection
try:
logger.info(f"Database Health == : {SQLALCHEMY_DATABASE_URI}")
db.session.execute(text("SELECT table_name FROM user_tables ORDER BY table_name"))
except Exception as e:
db_status = "Connection Failed"
errors.append(f"Database Error: {str(e)}")
status = "failed"
# Check Events Service health
try:
events_service_response = EventServiceIntegration.health_check()
if events_service_response.status_code != 200:
events_service_status = "Connection Failed"
status = "failed"
errors.append(f"Events Service response: {events_service_response.text}")
except Exception as e:
events_service_status = "Connection Failed"
status = "failed"
errors.append(f"Events Service connection failed: {str(e)}")
# Check Bank health
try:
emulator_response = SimbrellaIntegration.health_check()
if emulator_response.status_code != 200:
bank_status = "Connection Failed"
status = "failed"
errors.append(f"Bank Connection response: {emulator_response.text}")
except Exception as e:
bank_status = "Connection Failed"
status = "failed"
errors.append(f"Connection to Bank failed: {str(e)}")
response = {
"status": status,
"db_status": db_status,
"events_service_status": events_service_status,
"bank_status": bank_status,
"db_uri": db_uri,
"errors": errors or None
}
return jsonify(response), 200 if status == "ok" else 500
return {"status": "ok"}, 200
# Authorize endpoint
+1 -13
View File
@@ -51,13 +51,6 @@ class EligibilityCheckService(BaseService):
db.session.flush()
# Determine if there is any loan of 3MPC active
current_loan = EligibilityCheckService.get_current_active_loans_by_account_id(account_id = account_id)
if current_loan:
logger.info(f"Account {current_loan.account_id} has active loan {current_loan}")
if current_loan.product_id =='3MPC':
return ResponseHelper.error(result_description="Max loan count for 3MPC reached")
# Determine Loan count
is_eligible = EligibilityCheckService.check_loan_limits(customer_id)
@@ -174,12 +167,7 @@ class EligibilityCheckService(BaseService):
logger.error(f"An error occurred: {str(e)}", exc_info=True)
db.session.rollback()
return ResponseHelper.internal_server_error()
@staticmethod
def get_current_active_loans_by_account_id(account_id):
current_loan = Loan.get_current_active_loans_by_account_id(account_id)
return current_loan
@staticmethod
def check_loan_limits(customer_id):
+21 -54
View File
@@ -3,15 +3,15 @@ from marshmallow import ValidationError
from app.api.enums.loan_status import LoanStatus
from app.models import Customer
from app.utils.logger import logger
from app.api.schemas.loan_status import LoanStatusSchema
from app.api.schemas.loan_status import LoanStatusSchema
from app.api.services.base_service import BaseService
from app.api.enums import TransactionType
from app.api.enums import TransactionType
from app.extensions import db
from app.api.helpers.response_helper import ResponseHelper
class LoanStatusService(BaseService):
TRANSACTION_TYPE = TransactionType.LOAN_STATUS
TRANSACTION_TYPE = TransactionType.LOAN_STATUS
@staticmethod
def process_request(data):
@@ -27,56 +27,30 @@ class LoanStatusService(BaseService):
try:
with db.session.begin():
# Validate data
validated_data = LoanStatusService.validate_data(
data, LoanStatusSchema()
)
validated_data = LoanStatusService.validate_data(data, LoanStatusSchema())
customer_id = validated_data.get("customerId")
customer_id = validated_data.get('customerId')
logger.info(f"Looking for customer *** {customer_id}")
customer = Customer.get_customer_with_loan_list(customer_id)
transactionId = validated_data.get('transactionId')
account_id = validated_data.get('accountId')
transactionId = validated_data.get("transactionId")
account_id = validated_data.get("accountId")
if LoanStatusService.validate_account_ownership(
account_id=account_id, customer_id=customer_id
):
if(LoanStatusService.validate_account_ownership(account_id = account_id, customer_id = customer_id)):
# Get loans
customer_loans = customer.loans
loans = [
loan.to_dict()
for loan in customer_loans
if loan.status in [LoanStatus.ACTIVE, LoanStatus.START_REPAY, LoanStatus.ACTIVE_PARTIAL]
]
transaction = LoanStatusService.log_transaction(
validated_data=validated_data
)
loans = [loan.to_dict() for loan in customer.loans if loan.status == LoanStatus.ACTIVE]
transaction = LoanStatusService.log_transaction(validated_data = validated_data)
if not transaction:
logger.error(f"Failed to log transaction")
return ResponseHelper.error(
result_description="Failed to log transaction."
)
else:
return ResponseHelper.error(
result_description="Invalid Customer or Account"
)
return ResponseHelper.error(result_description="Failed to log transaction.")
else:
return ResponseHelper.error(result_description="Invalid Customer or Account")
# CONFIRM IF THE TOTAL DEBT IF FOR ONLY ACTIVE LOANS OR ALL LOANS
total_debt_amount = sum(
loan.get("currentLoanAmount") or 0 for loan in loans
)
total_outstanding_amount = sum(
loan.get("currentLoanAmount") or 0 for loan in loans
)
total_active_loan_amount = sum(
loan.get("repaymentAmount") or 0 for loan in loans
)
total_settled_amount = total_active_loan_amount - total_outstanding_amount
loan.get("currentLoanAmount") or 0
for loan in loans
)
# Simulated processing logic
response_data = {
@@ -85,11 +59,6 @@ class LoanStatusService(BaseService):
"transactionId": transactionId,
"loans": loans,
"totalDebtAmount": total_debt_amount,
"summary": {
"totalSettledAmount": total_settled_amount,
"totalOutstandingAmount": total_outstanding_amount,
"totalActiveLoanAmount": total_active_loan_amount,
}
}
db.session.commit()
@@ -99,11 +68,9 @@ class LoanStatusService(BaseService):
logger.error(f"Validation Error: {getattr(err, 'messages', str(err))}")
db.session.rollback()
return ResponseHelper.unprocessable_entity(
result_description="Validation exception"
)
except ValueError as err:
return ResponseHelper.unprocessable_entity(result_description="Validation exception")
except ValueError as err:
logger.error(f"{getattr(err, 'messages', str(err))}")
db.session.rollback()
return ResponseHelper.error(result_description=str(err))
@@ -111,4 +78,4 @@ class LoanStatusService(BaseService):
except Exception as e:
logger.error(f"An error occurred: {str(e)}", exc_info=True)
db.session.rollback()
return ResponseHelper.internal_server_error()
return ResponseHelper.internal_server_error()
+2 -14
View File
@@ -180,11 +180,7 @@ class OfferAnalysis:
if real_eligible_amount < original_transaction_offer.min_amount:
logger.error(f"Max eligible amount ({real_eligible_amount}) is less than the minimum offer amount ({original_transaction_offer.min_amount}).")
raise ValueError("You are not eligible for a loan at this time - Minimum amount not met.")
# if real_eligible_amount < 100:
# logger.error(f"Max eligible amount ({real_eligible_amount}) is less than the minimum offer amount ({original_transaction_offer.min_amount}).")
# raise ValueError("You are not eligible for a loan at this time.")
raise ValueError("You are not eligible for a loan at this time.")
transaction_offer = TransactionOffer.create_transaction_offer(
customer_id=customer_id,
@@ -225,12 +221,8 @@ class OfferAnalysis:
if approved_amount < offer.min_amount:
logger.error(f"Max eligible amount ({approved_amount}) is less than the minimum offer amount ({offer.min_amount}).")
continue
# raise ValueError("You are not eligible for a loan at this time.")
raise ValueError("You are not eligible for a loan at this time.")
# if approved_amount < 100:
# logger.error(f"Max eligible amount ({approved_amount}) is less than the minimum offer amount ({offer.min_amount}).")
# raise ValueError("You are not eligible for a loan at this time.")
transaction_offer = TransactionOffer.create_transaction_offer(
customer_id=customer_id,
@@ -256,8 +248,4 @@ class OfferAnalysis:
"tenor": offer.tenor
})
if not eligible_offers:
logger.error("No eligible offers found for customer: {customer_id} - Minimum amount not met")
raise ValueError("You are not eligible for a loan at this time - Minimum amount not met")
return eligible_offers
+10 -102
View File
@@ -1,4 +1,3 @@
from gettext import install
from flask import request, jsonify
from marshmallow import ValidationError
from app.api.integrations.kafka import KafkaIntegration
@@ -12,9 +11,8 @@ from threading import Thread
from app.models import Loan, Offer, Charge , TransactionOffer, RACCheck
from app.api.enums import LoanStatus
from app.extensions import db
from datetime import datetime, timezone, timedelta
from datetime import datetime, timezone
from dateutil.relativedelta import relativedelta
from app.api.integrations import EventServiceIntegration
from app.models import LoanRepaymentSchedule
from app.api.services.offer_analysis import OfferAnalysis
from app.api.helpers.response_helper import ResponseHelper
@@ -144,10 +142,10 @@ class ProvideLoanService(BaseService):
db.session.flush()
current_product_id = offer.product_id
schedules = LoanRepaymentSchedule.add_repayment_schedule(loan = loan, num_schedules = num_schedules, transaction_id = transaction_id)
schedule = LoanRepaymentSchedule.add_repayment_schedule(loan = loan, num_schedules = num_schedules, transaction_id = transaction_id)
if not schedules:
if not schedule:
logger.error(f"Failed to create repayment schedule for loan ID {loan.id}")
return ResponseHelper.error(result_description="Failed to generate loan repayment schedule.")
@@ -159,37 +157,26 @@ class ProvideLoanService(BaseService):
loan_charges = LoanCharge.create_charges_for_loan(loan_id = loan_id, transaction_id = transaction_id, referenced_amount = 800, charges = charges)
else:
return ResponseHelper.error(result_description="Invalid Customer or Account")
charge_schedule_items = ProvideLoanService.get_charge_schedule_items(
loan_charges=loan_charges,
offer=offer,
loan_ref=loan_ref,
amount=amount,
schedules=schedules
)
response_data = {
"requestId": request_id,
"transactionId": transaction_id,
"loanRef": loan_ref,
"customerId": customer_id,
"accountId": account_id,
"msisdn": customer.msisdn,
"schedule": charge_schedule_items,
"msisdn": customer.msisdn
}
event_thread = Thread(target=ProvideLoanService.trigger_loan_disbursement, args=(transaction_id,))
event_thread.start()
# KafkaIntegration.send_loan_request(loan_data = response_data, request_id = request_id)
# Call Kafka in a background thread
kafka_thread = Thread(target=ProvideLoanService.async_send_to_kafka, args=(response_data, request_id, "PROCESS_PAYMENT"))
kafka_thread.start()
thread = Thread(target=ProvideLoanService.async_send_to_kafka, args=(response_data, request_id, "PROCESS_PAYMENT"))
thread.start()
db.session.commit()
return ResponseHelper.success(data=response_data)
@@ -208,83 +195,4 @@ class ProvideLoanService(BaseService):
except Exception as e:
logger.error(f"An error occurred: {str(e)}", exc_info=True)
db.session.rollback()
return ResponseHelper.internal_server_error()
@classmethod
def trigger_loan_disbursement(cls, transaction_id: str):
response = EventServiceIntegration.direct_loan(transaction_id=transaction_id)
return response
@classmethod
def get_charge_schedule_items(cls, loan_charges, offer, loan_ref, amount, schedules):
now = datetime.now(timezone.utc)
due_date = now + timedelta(days=offer.tenor)
id_counter = 1
charge_schedule_items = []
charge_schedule_items.append({
"id": id_counter,
"dueDate": due_date.isoformat(),
"amountDue": amount,
"componentName": "PRINCIPAL",
"startDate": now.isoformat(),
})
interest_amount = 0.0
for charge in loan_charges:
code = charge.code.upper()
if code == "INTEREST":
interest_amount = float(charge.amount)
continue
item = {
"id": id_counter,
"dueDate": charge.due_date.isoformat(),
"amountDue": float(charge.amount),
"componentName": charge.code.upper(), # e.g. INTEREST, MGMT_FEE, VAT_FEE
"startDate": charge.created_at.isoformat(),
}
charge_schedule_items.append(item)
id_counter += 1
num_schedules = len(schedules)
if num_schedules > 0:
principal_per_schedule = amount / num_schedules
else:
principal_per_schedule = 0.0
for schedule in schedules:
default = {
"id": id_counter,
"installmentNo": schedule.installment_number,
"dueDate": schedule.due_date.isoformat(),
"amountDue": round(principal_per_schedule, 2),
"componentName": "DEFAULT",
"startDate": schedule.created_at.isoformat(),
}
charge_schedule_items.append(default)
id_counter += 1
interest = {
"id": id_counter,
"dueDate": schedule.due_date.isoformat(),
"amountDue": round(interest_amount, 2),
"componentName": "INTEREST",
"startDate": schedule.created_at.isoformat()
}
charge_schedule_items.append(interest)
id_counter += 1
return charge_schedule_items
return ResponseHelper.internal_server_error()
+3 -14
View File
@@ -11,7 +11,6 @@ from app.api.services.base_service import BaseService
from app.api.enums import TransactionType
from threading import Thread
from app.extensions import db
from app.api.integrations import EventServiceIntegration
class RepaymentService(BaseService):
TRANSACTION_TYPE = TransactionType.REPAYMENT
@@ -57,7 +56,7 @@ class RepaymentService(BaseService):
return ResponseHelper.error(result_description="Failed to save repayment details.")
#Update Loan status
Loan.update_status(loan_id = loan_id, status = LoanStatus.START_REPAY) # repay started by user
Loan.update_status(loan_id = loan_id, status = LoanStatus.START_REPAY) # repay started bu user
transaction = RepaymentService.log_transaction(validated_data = validated_data)
if not transaction:
@@ -80,12 +79,9 @@ class RepaymentService(BaseService):
"debtId": loan_id
}
event_thread = Thread(target=RepaymentService.trigger_loan_repayment, args=(transaction_id,))
event_thread.start()
# Call Kafka in a background thread
kafka_thread = Thread(target=RepaymentService.async_send_to_kafka, args=(response_data, request_id, "LOAN_REPAYMENT"))
kafka_thread.start()
thread = Thread(target=RepaymentService.async_send_to_kafka, args=(response_data, request_id, "LOAN_REPAYMENT"))
thread.start()
db.session.commit()
return ResponseHelper.success(data=response_data)
@@ -105,10 +101,3 @@ class RepaymentService(BaseService):
logger.error(f"An error occurred: {str(e)}", exc_info=True)
db.session.rollback()
return ResponseHelper.internal_server_error()
@classmethod
def trigger_loan_repayment(cls, transaction_id: str):
response = EventServiceIntegration.direct_repayment(transaction_id=transaction_id)
return response
-1
View File
@@ -38,7 +38,6 @@ class SelectOfferService(BaseService):
transaction_id = validated_data.get("transactionId")
request_id = validated_data.get("requestId")
offer_id = int(transaction_offer_id[5:]) # The last part is int
#"offerId": "SAL30001129",
+29 -29
View File
@@ -1,6 +1,7 @@
import os
from datetime import timedelta
class Config:
"""Base configuration for Flask app"""
@@ -21,17 +22,26 @@ class Config:
DNS = f"(DESCRIPTION=(ADDRESS=(PROTOCOL=TCP)(HOST={DATABASE_HOST})(PORT={DATABASE_PORT}))(CONNECT_DATA=(SID={DATABASE_SID})))"
# ECO Database Configuration
ECO_DATABASE_USER = os.environ.get("ECO_DATABASE_USER", "eco_user")
ECO_DATABASE_PASSWORD = os.environ.get("ECO_DATABASE_PASSWORD", "eco_pass")
ECO_DATABASE_HOST = os.environ.get("ECO_DATABASE_HOST", "localhost")
ECO_DATABASE_PORT = os.environ.get("ECO_DATABASE_PORT", 5432)
ECO_DATABASE_NAME = os.environ.get("ECO_DATABASE_NAME", "eco_db")
# Database Connection
# 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"oracle+oracledb://{DATABASE_USER}:{DATABASE_PASSWORD}@{DNS}"
)
#SQLALCHEMY_DATABASE_URI_FULL = 'oracle+oracledb://FIRSTADVSTG:Pchanged_56789@10.2.110.30:1521/?service_name=firstadv'
SQLALCHEMY_BINDS = {
"eco": f"postgresql+psycopg2://{ECO_DATABASE_USER}:{ECO_DATABASE_PASSWORD}@{ECO_DATABASE_HOST}:{ECO_DATABASE_PORT}/{ECO_DATABASE_NAME}"
}
SQLALCHEMY_TRACK_MODIFICATIONS = False
JWT_SECRET_KEY = os.getenv("JWT_SECRET_KEY", "secret-key")
JWT_ACCESS_TOKEN_EXPIRES = os.getenv("JWT_ACCESS_TOKEN_EXPIRES", timedelta(hours=1))
JWT_REFRESH_TOKEN_EXPIRES = os.getenv(
@@ -41,26 +51,19 @@ class Config:
# KAFKA_BROKER = 'dev-events.simbrellang.net:9085'
KAFKA_BROKER = os.getenv("KAFKA_BROKER", "dev-events.simbrellang.net:9085")
# SIMBRELLA_ENDPOINT_RAC_CHECKS = os.getenv("SIMBRELLA_ENDPOINT_RAC_CHECKS", "RACCheck")
# SIMBRELLA_ENDPOINT_RAC_CHECKS = os.getenv("SIMBRELLA_ENDPOINT_RAC_CHECKS", "RACCheck")
VALID_APP_ID = os.getenv("SIMBRELLA_APP_ID", "app1")
VALID_API_KEY = os.getenv("SIMBRELLA_API_KEY", "test-api-key-12345")
SIMBRELLA_BASE_URL = os.getenv("SIMBRELLA_BASE_URL", "http://127.0.0.1:6337")
SIMBRELLA_ENDPOINT_RAC_CHECKS = os.getenv("SIMBRELLA_ENDPOINT_RAC_CHECKS","api/rac-check")
SIMBRELLA_HEALTH = os.getenv("SIMBRELLA_ENDPOINT_RAC_CHECKS","api/system-health-check")
BANK_CALL_AUTH_ENDPOINT = os.getenv("BANK_CALL_AUTH_ENDPOINT", "/api/Auth/generate-token")
BANK_CALL_USERNAME = os.getenv("BANK_CALL_USERNAME", "simbrella")
BANK_CALL_PASSWORD = os.getenv("BANK_CALL_PASSWORD", "G7$k9@pL2!qR")
EVENTS_SERVICE_BASE_URL = os.getenv("EVENTS_SERVICE_BASE_URL","https://event-core.simbrellang.net")
ENDPOINT_DIRECT_LOAN = os.getenv("ENDPOINT_DIRECT_LOAN","/autocall/direct/loan")
ENDPOINT_DIRECT_REPAYMENT = os.getenv("ENDPOINT_DIRECT_REPAYMENT","/autocall/direct/repayment")
SIMBRELLA_ENDPOINT_RAC_CHECKS = os.getenv(
"SIMBRELLA_ENDPOINT_RAC_CHECKS", "api/rac-check"
)
RAC_RESULT_accountStatus = os.environ.get("RAC_RESULT_accountStatus", "true")
RAC_RESULT_bvnValidated = os.environ.get("RAC_RESULT_bvnValidated", "true")
RAC_RESULT_creditBureauCheck = os.environ.get("RAC_RESULT_creditBureauCheck", "false")
RAC_RESULT_creditBureauCheck = os.environ.get(
"RAC_RESULT_creditBureauCheck", "false"
)
RAC_RESULT_crmsCheck = os.environ.get("RAC_RESULT_crmsCheck", "true")
RAC_RESULT_hasLien = os.environ.get("RAC_RESULT_hasLien", "false")
RAC_RESULT_hasPastDueLoan = os.environ.get("RAC_RESULT_hasPastDueLoan", "false")
@@ -83,12 +86,10 @@ class Config:
"rule12_CRMS_no_delinquency",
"rule13_BVN_ignore",
"rule14_no_lien",
"rule15_null_ignore"
"rule15_null_ignore",
]
rac_false_rules = [
]
rac_false_rules = []
rac_salary_payments = [
"salarypaymenT_1",
@@ -96,18 +97,17 @@ class Config:
"salarypaymenT_3",
"salarypaymenT_4",
"salarypaymenT_5",
"salarypaymenT_6"
"salarypaymenT_6",
]
MAIL_SERVER = os.getenv('MAIL_SERVER','smtp.zoho.com')
MAIL_SERVER = os.getenv("MAIL_SERVER", "smtp.zoho.com")
MAIL_PORT = 587
MAIL_USERNAME = os.getenv('MAIL_USERNAME', 'firstadvance@dynamikservices.tech')
MAIL_PASSWORD = os.getenv('MAIL_PASSWORD')
MAIL_USERNAME = os.getenv("MAIL_USERNAME", "firstadvance@dynamikservices.tech")
MAIL_PASSWORD = os.getenv("MAIL_PASSWORD")
MAIL_USE_TLS = True
MAIL_USE_SSL = False
MAIL_DEFAULT_SENDER = ('FirstAdvance', 'firstadvance@dynamikservices.tech')
MAIL_RECEIVER= os.getenv('MAIL_RECEIVER', 'vdagbue@gmail.com')
MAIL_DEFAULT_SENDER = ("FirstAdvance", "firstadvance@dynamikservices.tech")
MAIL_RECEIVER = os.getenv("MAIL_RECEIVER", "vdagbue@gmail.com")
settings = Config()
+1
View File
@@ -0,0 +1 @@
from .transaction_type import TransactionType
+7
View File
@@ -0,0 +1,7 @@
from enum import Enum
class InstallmentStatus(str, Enum):
PENDING = 'PENDING'
PAID = 'PAID'
OVERDUE = 'OVERDUE'
PARTIALLY_PAID = 'PARTIALLY_PAID'
+8
View File
@@ -0,0 +1,8 @@
from enum import Enum
class LoanStatus(str, Enum):
PENDING = 'PENDING'
ACTIVE = 'ACTIVE'
PAID = 'PAID'
DEFAULTED = 'DEFAULTED'
CLOSED = 'CLOSED'
+10
View File
@@ -0,0 +1,10 @@
from enum import Enum
class TransactionType(str, Enum):
ELIGIBILITY_CHECK = "eligibility_check"
GET_OFFERS = "get_offers"
LOAN_INFORMATION = "loan_information"
INFLOW_NOTIFICATION = "inflow_notification"
LOW_BALANCE_NOTIFICATION = "low_balance_notification"
PROVIDE_LOAN = "provide_loan"
REPAYMENT = "repayment"
+112
View File
@@ -0,0 +1,112 @@
from flask import jsonify
from typing import Optional, Union, Dict, List, Any
class ResponseHelper:
"""
A helper class for building standardized JSON responses using resultCode and resultDescription.
"""
@staticmethod
def build_response(
result_code: str,
result_description: str,
data: Optional[Union[Dict, List, str]] = None
) -> Dict[str, Any]:
response = {
"resultCode": result_code,
"resultDescription": result_description
}
if isinstance(data, dict):
response.update(data)
return jsonify(response)
@staticmethod
def success(
result_description: str = "Successful",
result_code: str = "00",
data: Optional[Dict[str, Any]] = None
) -> Dict[str, Any]:
return ResponseHelper.build_response(result_code, result_description, data)
@staticmethod
def error(
result_description: str = "An error occurred",
result_code: str = "01",
data: Optional[Dict[str, Any]] = None
) -> Dict[str, Any]:
return ResponseHelper.build_response(result_code, result_description, data)
@staticmethod
def created(
result_description: str = "Resource created successfully",
result_code: str = "00",
data: Optional[Dict[str, Any]] = None
) -> Dict[str, Any]:
return ResponseHelper.build_response(result_code, result_description, data)
@staticmethod
def updated(
result_description: str = "Resource updated successfully",
result_code: str = "00",
data: Optional[Dict[str, Any]] = None
) -> Dict[str, Any]:
return ResponseHelper.build_response(result_code, result_description, data)
@staticmethod
def internal_server_error(
result_description: str = "Internal Server Error",
result_code: str = "500",
data: Optional[Dict[str, Any]] = None
) -> Dict[str, Any]:
return ResponseHelper.build_response(result_code, result_description, data)
@staticmethod
def unauthorized(
result_description: str = "Unauthorized",
result_code: str = "401",
data: Optional[Dict[str, Any]] = None
) -> Dict[str, Any]:
return ResponseHelper.build_response(result_code, result_description, data)
@staticmethod
def forbidden(
result_description: str = "Forbidden",
result_code: str = "403",
data: Optional[Dict[str, Any]] = None
) -> Dict[str, Any]:
return ResponseHelper.build_response(result_code, result_description, data)
@staticmethod
def not_found(
result_description: str = "Resource not found",
result_code: str = "404",
data: Optional[Dict[str, Any]] = None
) -> Dict[str, Any]:
return ResponseHelper.build_response(result_code, result_description, data)
@staticmethod
def unprocessable_entity(
result_description: str = "Unprocessable entity",
result_code: str = "422",
data: Optional[Dict[str, Any]] = None
) -> Dict[str, Any]:
return ResponseHelper.build_response(result_code, result_description, data)
@staticmethod
def method_not_allowed(
result_description: str = "Method Not Allowed",
result_code: str = "405",
data: Optional[Dict[str, Any]] = None
) -> Dict[str, Any]:
return ResponseHelper.build_response(result_code, result_description, data)
@staticmethod
def bad_request(
result_description: str = "Bad Request",
result_code: str = "400",
data: Optional[Dict[str, Any]] = None
) -> Dict[str, Any]:
return ResponseHelper.build_response(result_code, result_description, data)
+1
View File
@@ -0,0 +1 @@
from .routes import eco
+109
View File
@@ -0,0 +1,109 @@
from flask import Blueprint, request, jsonify, send_from_directory
from app.eco.services import (
AuthorizationService,
EligibilityCheckService,
GetOfferService,
ProvideLoanService,
LoanInformationService,
RepaymentService,
InflowNotificationService,
LowBalanceNotificationService
)
from app.utils.logger import logger
from flask_jwt_extended import jwt_required
import os
eco = Blueprint("eco", __name__)
# Swagger JSON file
@eco.route("/swagger.json", methods=["GET"])
def swagger_json():
swagger_dir = os.path.join("swagger")
return send_from_directory(swagger_dir, "digifi_swagger.json")
@eco.route("/swagger/<path:filename>")
def serve_paths(filename):
swagger_dir = os.path.join("swagger")
return send_from_directory(swagger_dir, filename)
# Simbrella Request API Endpoints
@eco.route("/EligibilityCheck", methods=["POST"])
@jwt_required()
def eligibility_check():
data = request.get_json()
response = EligibilityCheckService.process_request(data)
return jsonify(response)
@eco.route("/GetOffers", methods=["POST"])
@jwt_required()
def get_offers():
data = request.get_json()
response = GetOfferService.process_request(data)
return jsonify(response)
@eco.route("/ProvideLoan", methods=["POST"])
@jwt_required()
def provide_loan():
data = request.get_json()
response = ProvideLoanService.process_request(data)
return jsonify(response)
@eco.route("/LoanInformation", methods=["POST"])
@jwt_required()
def loan_information():
data = request.get_json()
response = LoanInformationService.process_request(data)
return jsonify(response)
@eco.route("/Repayment", methods=["POST"])
@jwt_required()
def repayment():
data = request.get_json()
response = RepaymentService.process_request(data)
return jsonify(response)
@eco.route("/InflowNotification", methods=["POST"])
@jwt_required()
def inflow_notification():
data = request.get_json()
response = InflowNotificationService.process_request(data)
return jsonify(response)
@eco.route("/LowBalanceNotification", methods=["POST"])
@jwt_required()
def low_balance_notification():
data = request.get_json()
response = LowBalanceNotificationService.process_request(data)
return jsonify(response)
# Health Check
@eco.route("/health", methods=["GET"])
def health_check():
return jsonify({"status": "ok"}), 200
# Authorize endpoint
@eco.route("/Authorize", methods=["POST"])
def authorize():
data = request.get_json()
response = AuthorizationService.process_request(data)
return response
# Authorize refresh endpoint
@eco.route("/AuthorizeRefresh", methods=["POST"])
@jwt_required(refresh=True)
def refresh():
response = AuthorizationService.process_refresh_request()
return response
+8
View File
@@ -0,0 +1,8 @@
from .authorization import AuthorizeRequestSchema
from .eligibility_check import EligibilityCheckSchema
from .get_offer import GetOfferSchema
from .provide_loan import ProvideLoanSchema
from .loan_information import LoanInformationSchema
from .repayment import RepaymentSchema
from .inflow_notification import InflowNotificationSchema
from .low_balance_notification import LowBalanceNotificationSchema
+6
View File
@@ -0,0 +1,6 @@
from marshmallow import Schema, fields
class AuthorizeRequestSchema(Schema):
username = fields.Str(required=True)
password = fields.Str(required=True)
+14
View File
@@ -0,0 +1,14 @@
from marshmallow import Schema, fields, EXCLUDE
class EligibilityCheckSchema(Schema):
requestId = fields.Str(required=True)
sessionId = fields.Str(required=True)
affiliateCode = fields.Str(required=True)
customerId = fields.Str(required=True)
accountId = fields.Str(required=True)
msisdn = fields.Str(required=True)
lienAmount = fields.Str(required=True)
class Meta:
unknown = EXCLUDE
+17
View File
@@ -0,0 +1,17 @@
from marshmallow import Schema, fields, validate, validates_schema, EXCLUDE
from marshmallow.validate import OneOf
from datetime import datetime
class GetOfferSchema(Schema):
requestId = fields.Str(required=True)
sessionId = fields.Str(required=True)
affiliateCode = fields.Str(required=True)
customerId = fields.Str(required=True)
accountId = fields.Str(required=True)
msisdn = fields.Str(required=True)
lienAmount = fields.Decimal(required=True)
requestedAmount = fields.Decimal(required=True)
channel = fields.Str(required=True)
class Meta:
unknown = EXCLUDE
+18
View File
@@ -0,0 +1,18 @@
from marshmallow import Schema, fields, EXCLUDE
from datetime import datetime
class InflowItemSchema(Schema):
customerId = fields.Str(required=True)
accountId = fields.Str(required=True)
amount = fields.Decimal(required=True, places=2)
transactionId = fields.Str(required=True)
transactionDate = fields.Str(required=True)
class InflowNotificationSchema(Schema):
batchTime = fields.Str(required=True)
affiliateCode = fields.Str(required=True)
inflows = fields.List(fields.Nested(InflowItemSchema), required=True)
class Meta:
unknown = EXCLUDE
+13
View File
@@ -0,0 +1,13 @@
from marshmallow import Schema, fields, validate, EXCLUDE
from marshmallow.validate import OneOf, Range
class LoanInformationSchema(Schema):
requestId = fields.Str(required=True)
sessionId = fields.Str(required=True)
affiliateCode = fields.Str(required=True)
customerId = fields.Str(required=True)
msisdn = fields.Str(required=True)
channel = fields.Str(required=True)
class Meta:
unknown = EXCLUDE
@@ -0,0 +1,16 @@
from marshmallow import Schema, fields, validate, EXCLUDE
class LowBalanceItemSchema(Schema):
customerId = fields.Str(required=True)
accountId = fields.Str(required=True)
balance = fields.Decimal(required=True, places=2)
transactionId = fields.Str(required=True)
transactionDate = fields.Str(required=True)
class LowBalanceNotificationSchema(Schema):
batchTime = fields.Str(required=True)
affiliateCode = fields.Str(required=True)
lbns = fields.List(fields.Nested(LowBalanceItemSchema), required=True)
class Meta:
unknown = EXCLUDE
+20
View File
@@ -0,0 +1,20 @@
from marshmallow import Schema, fields, validate, EXCLUDE
from marshmallow.validate import OneOf, Range
class ProvideLoanSchema(Schema):
requestId = fields.Str(required=True)
sessionId = fields.Str(required=True)
affiliateCode = fields.Str(required=True)
customerId = fields.Str(required=True)
accountId = fields.Str(required=True)
msisdn = fields.Str(required=True)
offerId = fields.Int(required=True, validate=Range(min=1))
upfrontPayment = fields.Decimal(required=True)
interestRate = fields.Decimal(required=True)
requestedAmount = fields.Decimal(required=True)
lienAmount = fields.Decimal(required=True)
productId = fields.Str( required=True)
channel = fields.Str(required=True)
class Meta:
unknown = EXCLUDE
+16
View File
@@ -0,0 +1,16 @@
from marshmallow import Schema, fields, validate, EXCLUDE
from marshmallow.validate import OneOf, Range
class RepaymentSchema(Schema):
requestId = fields.Str(required=True)
sessionId = fields.Str(required=True)
affiliateCode = fields.Str(required=True)
customerId = fields.Str(required=True)
msisdn = fields.Str(required=True)
debtId = fields.Int(required=True, validate=Range(min=1))
repaymentAmount = fields.Decimal(required=False) # Optional, required for "Manual" repaymentType
repaymentType = fields.Str(required=True) # "Full", "Due", "Manual", "Overdue"
channel = fields.Str(required=True)
class Meta:
unknown = EXCLUDE
+8
View File
@@ -0,0 +1,8 @@
from app.eco.services.authorization import AuthorizationService
from app.eco.services.eligibility_check import EligibilityCheckService
from app.eco.services.get_offer import GetOfferService
from app.eco.services.provide_loan import ProvideLoanService
from app.eco.services.loan_information import LoanInformationService
from app.eco.services.repayment import RepaymentService
from app.eco.services.inflow_notification import InflowNotificationService
from app.eco.services.low_balance_notification import LowBalanceNotificationService
+102
View File
@@ -0,0 +1,102 @@
from flask import request, jsonify
from marshmallow import ValidationError
from app.eco.services.base_service import BaseService
from app.utils.logger import logger
from app.eco.schemas.authorization import AuthorizeRequestSchema
from app.eco.helpers.response_helper import ResponseHelper
from flask_jwt_extended import (
JWTManager,
jwt_required,
create_access_token,
create_refresh_token,
get_jwt_identity,
)
from app.config import Config
USERNAME = Config.BASIC_AUTH_USERNAME
PASSWORD = Config.BASIC_AUTH_PASSWORD
class AuthorizationService(BaseService):
@staticmethod
def process_request(data):
"""
Process the Authorization request.
Args:
data (dict): The request data.
Returns:
dict: A standardized response.
"""
try:
logger.info("Processing Authorization request")
if not data:
return ResponseHelper.bad_request(result_description="Missing JSON in request")
# Validate input data using the Authorization schema
schema = AuthorizeRequestSchema()
validated_data = schema.load(data) # Raises ValidationError if invalid
if (
validated_data["username"] != USERNAME
or validated_data["password"] != PASSWORD
):
return ResponseHelper.unauthorized(result_description="Invalid credentials")
access_token = create_access_token(identity=validated_data["username"])
refresh_token = create_refresh_token(identity=validated_data["username"])
# Simulated processing logic
response_data = {
"access_token": access_token,
"refresh_token": refresh_token,
}
return ResponseHelper.success(
data={"data": response_data}, result_description="Authorization processed successfully"
)
except ValidationError as e:
logger.error(f"Validation error: {e}")
return ResponseHelper.bad_request(result_description=f"Validation error: {e}")
except Exception as e:
logger.error(f"Error processing Authorization request: {e}")
return ResponseHelper.internal_server_error(
result_description=f"Error processing Authorization request: {e}"
)
@staticmethod
def process_refresh_request():
"""
Process the RefreshToken request.
Args:
data (dict): The request data.
Returns:
dict: A standardized response.
"""
try:
logger.info("Processing RefreshToken request")
identity = get_jwt_identity()
access_token = create_access_token(identity=identity)
# Simulated processing logic
response_data = {
"access_token": access_token,
}
return ResponseHelper.success(
data={"data": response_data}, result_description="RefreshToken processed successfully"
)
except Exception as e:
logger.error(f"Error processing RefreshToken request: {e}")
return ResponseHelper.internal_server_error(
result_description=f"Error processing RefreshToken request: {e}"
)
+37
View File
@@ -0,0 +1,37 @@
from app.eco.enums import TransactionType
from flask import jsonify
from marshmallow import ValidationError
import logging
from app.utils.mail import send_report_email, get_report_data
logger = logging.getLogger(__name__)
class BaseService:
TRANSACTION_TYPE = None
@classmethod
def validate_data(cls, data, schema):
"""
Validate input data based on the provided schema.
"""
logger.info(f"Processing {cls.TRANSACTION_TYPE} request")
return schema.load(data)
@classmethod
def send_mail(cls, report_data, recipients):
send_report_email(report_data, recipients)
# @classmethod
# def log_session(cls, validated_data):
# """
# Create a new session.
# """
# channel = "USSD" if validated_data.get("channel") is None else validated_data.get("channel")
# return Session.create_session(
# session_id = validated_data.get("transactionId"),
# customer_id = validated_data.get('customerId', None),
# account_id = validated_data.get("accountId", None),
# type = cls.TRANSACTION_TYPE,
# channel = channel,
# )
+63
View File
@@ -0,0 +1,63 @@
from flask import session, jsonify
from app.utils.logger import logger
from app.eco.services.base_service import BaseService
from app.eco.schemas.eligibility_check import EligibilityCheckSchema
from marshmallow import ValidationError
from app.eco.enums import TransactionType
from app.eco.helpers.response_helper import ResponseHelper
import random
class EligibilityCheckService(BaseService):
TRANSACTION_TYPE = TransactionType.ELIGIBILITY_CHECK
@staticmethod
def process_request(data):
"""
Process the EligibilityCheck request.
Args:
data (dict): The request data.
Returns:
dict: A standardized response.
"""
try:
# with db.session.begin():
validated_data = EligibilityCheckService.validate_data(
data, EligibilityCheckSchema()
)
account_id = validated_data.get("accountId")
customer_id = validated_data.get("customerId")
sessionId = validated_data.get("sessionId")
msisdn = validated_data.get("msisdn")
# Simulate processing
response_data = {
"minEligibleAmount": 1000.0,
"maxEligibleAmount": 50000.0,
"outstandingDebtAmount": 1000.0,
}
return ResponseHelper.success(data=response_data)
except ValidationError as err:
logger.error(f"Validation Error: {getattr(err, 'messages', str(err))}")
# db.session.rollback()
return ResponseHelper.unprocessable_entity(
result_description="Validation exception"
)
except ValueError as err:
logger.error(f"{getattr(err, 'messages', str(err))}")
# db.session.rollback()
return ResponseHelper.error(result_description=str(err))
except Exception as e:
logger.error(f"An error occurred: {str(e)}", exc_info=True)
# db.session.rollback()
return ResponseHelper.internal_server_error()
+67
View File
@@ -0,0 +1,67 @@
from urllib import response
from app.utils.logger import logger
from app.eco.services.base_service import BaseService
from app.eco.schemas.get_offer import GetOfferSchema
from marshmallow import ValidationError
from app.eco.enums import TransactionType
from app.eco.helpers.response_helper import ResponseHelper
import random
class GetOfferService(BaseService):
TRANSACTION_TYPE = TransactionType.GET_OFFERS
@staticmethod
def process_request(data):
"""
Process the GetOffer request.
Args:
data (dict): The request data.
Returns:
dict: A standardized response.
"""
try:
validated_data = GetOfferService.validate_data(
data, GetOfferSchema()
)
# Simulate processing
offers = [
{
"offerId": 14451,
"productId": "2030",
"amount": 10000.0,
"upfrontPayment": 1000.0,
"interestRate": 10.0,
"recommendedRepaymentDates": ["2022-11-30"],
"installmentAmount": 11000.0,
"totalRepaymentAmount": 11000.0
},
{
"offerId": 16645,
"productId": "2060",
"amount": 10000.0,
"upfrontPayment": 0.0,
"interestRate": 10.0,
"recommendedRepaymentDates": ["2022-11-30", "2023-12-30"],
"installmentAmount": 5761.9,
"totalRepaymentAmount": 11523.8
}
]
response_data = {
"outstandingDebtAmount": 0,
"offers": offers
}
return ResponseHelper.success(data=response_data)
except ValidationError as err:
logger.error(f"Validation Error: {getattr(err, 'messages', str(err))}")
return ResponseHelper.unprocessable_entity(result_description="Validation exception")
except Exception as e:
logger.error(f"An error occurred: {str(e)}", exc_info=True)
return ResponseHelper.internal_server_error()
+38
View File
@@ -0,0 +1,38 @@
from urllib import response
from app.utils.logger import logger
from app.eco.services.base_service import BaseService
from app.eco.schemas.inflow_notification import InflowNotificationSchema
from marshmallow import ValidationError
from app.eco.enums import TransactionType
from app.eco.helpers.response_helper import ResponseHelper
import random
class InflowNotificationService(BaseService):
TRANSACTION_TYPE = TransactionType.INFLOW_NOTIFICATION
@staticmethod
def process_request(data):
"""
Process the InflowNotification request.
Args:
data (dict): The request data.
Returns:
dict: A standardized response.
"""
try:
validated_data = InflowNotificationService.validate_data(
data, InflowNotificationSchema()
)
return ResponseHelper.success()
except ValidationError as err:
logger.error(f"Validation Error: {getattr(err, 'messages', str(err))}")
return ResponseHelper.unprocessable_entity(result_description="Validation exception")
except Exception as e:
logger.error(f"An error occurred: {str(e)}", exc_info=True)
return ResponseHelper.internal_server_error()
+50
View File
@@ -0,0 +1,50 @@
from app.utils.logger import logger
from app.eco.services.base_service import BaseService
from app.eco.schemas.loan_information import LoanInformationSchema
from marshmallow import ValidationError
from app.eco.enums import TransactionType
from app.eco.helpers.response_helper import ResponseHelper
class LoanInformationService(BaseService):
TRANSACTION_TYPE = TransactionType.LOAN_INFORMATION
@staticmethod
def process_request(data):
"""
Process the LoanInformation request.
Args:
data (dict): The request data.
Returns:
dict: A standardized response.
"""
try:
# Validate the input data
validated_data = LoanInformationService.validate_data(
data, LoanInformationSchema()
)
logger.info(f"Fetching loan information for customer {validated_data['customerId']}")
# Simulate fetching loan information
debts = LoanInformationService._generate_sample_debts(validated_data['customerId'])
response_data = {
"debts": debts,
"totalCurrentPayment": sum(debt['currentPaymentAmount'] for debt in debts),
"totalDebtAmount": sum(debt['totalRepaymentAmount'] for debt in debts),
"resultCode": "00",
"resultDescription": "Successful"
}
return ResponseHelper.success(result=loan_data)
except ValidationError as err:
logger.error(f"Validation Error: {getattr(err, 'messages', str(err))}")
return ResponseHelper.unprocessable_entity(result_description="Validation exception")
except Exception as e:
logger.error(f"An error occurred: {str(e)}", exc_info=True)
return ResponseHelper.internal_server_error()
@@ -0,0 +1,38 @@
from urllib import response
from app.utils.logger import logger
from app.eco.services.base_service import BaseService
from app.eco.schemas.low_balance_notification import LowBalanceNotificationSchema
from marshmallow import ValidationError
from app.eco.enums import TransactionType
from app.eco.helpers.response_helper import ResponseHelper
import random
class LowBalanceNotificationService(BaseService):
TRANSACTION_TYPE = TransactionType.LOW_BALANCE_NOTIFICATION
@staticmethod
def process_request(data):
"""
Process the LowBalanceNotification request.
Args:
data (dict): The request data.
Returns:
dict: A standardized response.
"""
try:
validated_data = LowBalanceNotificationService.validate_data(
data, LowBalanceNotificationSchema()
)
return ResponseHelper.success()
except ValidationError as err:
logger.error(f"Validation Error: {getattr(err, 'messages', str(err))}")
return ResponseHelper.unprocessable_entity(result_description="Validation exception")
except Exception as e:
logger.error(f"An error occurred: {str(e)}", exc_info=True)
return ResponseHelper.internal_server_error()
+38
View File
@@ -0,0 +1,38 @@
from urllib import response
from app.utils.logger import logger
from app.eco.services.base_service import BaseService
from app.eco.schemas.provide_loan import ProvideLoanSchema
from marshmallow import ValidationError
from app.eco.enums import TransactionType
from app.eco.helpers.response_helper import ResponseHelper
import random
class ProvideLoanService(BaseService):
TRANSACTION_TYPE = TransactionType.PROVIDE_LOAN
@staticmethod
def process_request(data):
"""
Process the ProvideLoan request.
Args:
data (dict): The request data.
Returns:
dict: A standardized response.
"""
try:
validated_data = ProvideLoanService.validate_data(
data, ProvideLoanSchema()
)
return ResponseHelper.success()
except ValidationError as err:
logger.error(f"Validation Error: {getattr(err, 'messages', str(err))}")
return ResponseHelper.unprocessable_entity(result_description="Validation exception")
except Exception as e:
logger.error(f"An error occurred: {str(e)}", exc_info=True)
return ResponseHelper.internal_server_error()
+38
View File
@@ -0,0 +1,38 @@
from urllib import response
from app.utils.logger import logger
from app.eco.services.base_service import BaseService
from app.eco.schemas.repayment import RepaymentSchema
from marshmallow import ValidationError
from app.eco.enums import TransactionType
from app.eco.helpers.response_helper import ResponseHelper
import random
class RepaymentService(BaseService):
TRANSACTION_TYPE = TransactionType.REPAYMENT
@staticmethod
def process_request(data):
"""
Process the Repayment request.
Args:
data (dict): The request data.
Returns:
dict: A standardized response.
"""
try:
validated_data = RepaymentService.validate_data(
data, RepaymentSchema()
)
return ResponseHelper.success()
except ValidationError as err:
logger.error(f"Validation Error: {getattr(err, 'messages', str(err))}")
return ResponseHelper.unprocessable_entity(result_description="Validation exception")
except Exception as e:
logger.error(f"An error occurred: {str(e)}", exc_info=True)
return ResponseHelper.internal_server_error()
+7
View File
@@ -11,6 +11,13 @@ from .loan_repayment_schedule import LoanRepaymentSchedule
from .transaction_offers import TransactionOffer
from .repayments_data import RepaymentsData
from .salary import Salary
from .eco.account import EcoAccount
from .eco.customer import EcoCustomer
from .eco.loan import EcoLoan
from .eco.session import EcoSession
from .eco.installment import EcoInstallment
from .eco.offer import EcoOffer
__all__ = ['Customer', 'Account', 'Loan', 'Transaction', 'Repayment', 'LoanCharge', 'Offer', 'Charge', 'RACCheck', 'LoanRepaymentSchedule', 'TransactionOffer', 'RepaymentsData', 'Salary']
+3 -3
View File
@@ -47,11 +47,11 @@ class Customer(db.Model):
@classmethod
def create_customer(cls, id, msisdn, country_code, account_id, account_type='savings'):
if cls.query.filter_by(id=id).first():
raise ValueError("Customer ID '{id}' already exists.")
raise ValueError("Customer already exists")
elif Account.query.filter_by(id=account_id).first():
raise ValueError(f"Account ID '{account_id}' already exists.")
raise ValueError("Account already exists")
elif cls.query.filter_by(msisdn=msisdn).first():
raise ValueError("MSISDN '{msisdn}' already exists")
raise ValueError("msisdn already exists")
# Create the customer
customer = cls(
+17
View File
@@ -0,0 +1,17 @@
from sqlalchemy import Column, String, Date, Boolean, Integer
from app.extensions import db
class Account(db.Model):
__tablename__ = 'accounts'
__bind_key__ = 'eco'
id = Column(Integer, primary_key=True)
affiliate_code = Column(String(3), nullable=False)
customer_id = Column(String(9), nullable=False)
account_id = Column(String(10), unique=True, nullable=False)
registration_date = Column(Date, nullable=False)
account_currency_code = Column(String(3), nullable=False)
plastic_card_attached = Column(Boolean, default=False)
def __repr__(self):
return f"<Account(id={self.id}, account_id={self.account_id}, customer_id={self.customer_id})>"
+18
View File
@@ -0,0 +1,18 @@
from sqlalchemy import Column, String, Date, Integer
from sqlalchemy.orm import relationship
from app.extensions import db
class Customer(db.Model):
__tablename__ = 'customers'
__bind_key__ = 'eco'
id = Column(Integer, primary_key=True)
affiliate_code = Column(String(3), nullable=False)
customer_id = Column(String(9), unique=True, nullable=False)
msisdn = Column(String(15), nullable=True)
created_at = Column(Date, nullable=False)
def __repr__(self):
return f"<Customer(id={self.id}, customer_id={self.customer_id}, msisdn={self.msisdn})>"
+21
View File
@@ -0,0 +1,21 @@
from app.eco.enums.installment_status import InstallmentStatus
from sqlalchemy import Column, String, Date, Numeric, ForeignKey, Enum, Integer, DateTime
from app.extensions import db
import enum
class Installment(db.Model):
__tablename__ = 'installments'
__bind_key__ = 'eco'
id = Column(Integer, primary_key=True)
loan_id = Column(Integer, nullable=False)
installment_number = Column(Integer, nullable=False)
due_date = Column(Date, nullable=False)
amount = Column(Numeric(12, 2), nullable=False)
paid_amount = Column(Numeric(12, 2), default=0.00)
status = Column(Enum(InstallmentStatus), default=InstallmentStatus.PENDING)
penalty_amount = Column(Numeric(12, 2), default=0.00)
def __repr__(self):
return f"<Installment(id={self.id}, loan_id={self.loan_id}, status={self.status})>"
+26
View File
@@ -0,0 +1,26 @@
from app.eco.enums.loan_status import LoanStatus
from sqlalchemy import Column, String, Date, Numeric, Enum
from app.extensions import db
import enum
class Loan(db.Model):
__tablename__ = 'loans'
__bind_key__ = 'eco'
id = Column(Integer, primary_key=True)
request_id = Column(String(50), unique=True, nullable=False)
affiliate_code = Column(String(3), nullable=False)
customer_id = Column(String(9), nullable=False)
account_id = Column(String(10), nullable=False)
product_id = Column(String(4), nullable=False)
debt_id = Column(String(20), unique=True, nullable=False)
initial_credit_amount = Column(Numeric(12, 2), nullable=False)
current_balance = Column(Numeric(12, 2), nullable=False)
interest_rate = Column(Numeric(5, 2), nullable=False)
status = Column(Enum(LoanStatus), default=LoanStatus.PENDING)
disbursement_date = Column(Date, nullable=False)
due_date = Column(Date, nullable=False)
lien_amount = Column(Numeric(12, 2), default=0.00)
def __repr__(self):
return f"<Loan(id={self.id}, debt_id={self.debt_id}, status={self.status})>"
+22
View File
@@ -0,0 +1,22 @@
from sqlalchemy import Column, String, Numeric, Date, Integer
from app.extensions import db
class Offer(db.Model):
__tablename__ = 'offers'
__bind_key__ = 'eco'
id = Column(Integer, primary_key=True)
offer_id = Column(String(50), unique=True, nullable=False)
session_id = Column(String(50), nullable=False)
customer_id = Column(String(9), nullable=False)
product_id = Column(String(4), nullable=False)
amount = Column(Numeric(12, 2), nullable=False)
upfront_payment = Column(Numeric(12, 2), nullable=False)
interest_rate = Column(Numeric(5, 2), nullable=False)
installment_amount = Column(Numeric(12, 2), nullable=False)
total_repayment_amount = Column(Numeric(12, 2), nullable=False)
expiration_date = Column(Date, nullable=False)
status = Column(String(20), default='AVAILABLE')
def __repr__(self):
return f"<Offer(id={self.id}, offer_id={self.offer_id}, status={self.status})>"
+18
View File
@@ -0,0 +1,18 @@
from sqlalchemy import Column, String, DateTime, Integer
from app.extensions import db
class Session(db.Model):
__tablename__ = 'sessions'
__bind_key__ = 'eco'
id = Column(Integer, primary_key=True)
session_id = Column(String(50), unique=True, nullable=False)
customer_id = Column(String(9), nullable=True)
account_id = Column(String(10), nullable=True)
msisdn = Column(String(15), nullable=False)
channel = Column(String(10), nullable=False)
expires_at = Column(DateTime, nullable=False)
status = Column(String(20), default='ACTIVE')
def __repr__(self):
return f"<Session(id={self.id}, session_id={self.session_id}, status={self.status})>"
+4 -24
View File
@@ -1,6 +1,5 @@
from datetime import datetime, timezone, timedelta
from itertools import product
from app.api.enums.loan_status import LoanStatus
from app.extensions import db
from app.models.customer import Customer
from app.models.account import Account
@@ -193,32 +192,13 @@ class Loan(db.Model):
Get all active loans with the same original_transaction ID.
"""
active_loans = cls.query.filter(
cls.original_transaction == original_transaction_id,
or_(
cls.status == LoanStatus.ACTIVE.value,
cls.status == LoanStatus.START_REPAY.value,
cls.status == LoanStatus.ACTIVE_PARTIAL.value,
)
active_loans = cls.query.filter_by(
original_transaction=original_transaction_id,
# status='active'
).all()
return active_loans
@classmethod
def get_current_active_loans_by_account_id(cls, account_id):
"""
Get the first active loan based on the accountID.
"""
first_active_loan = cls.query.filter(
cls.account_id == account_id,
or_(
cls.status == LoanStatus.ACTIVE.value,
cls.status == LoanStatus.START_REPAY.value,
cls.status == LoanStatus.ACTIVE_PARTIAL.value,
)
).order_by(cls.id.desc()).first()
return first_active_loan
@classmethod
def update_status(cls, loan_id, status):
@@ -266,7 +246,7 @@ class Loan(db.Model):
'loanRef': self.reference,
'productId': self.product_id,
'initialLoanAmount': self.initial_loan_amount,
'currentLoanAmount': self.balance,
'currentLoanAmount': self.current_loan_amount,
'defaultPenaltyFee': self.default_penalty_fee,
'continuousFee': self.continuous_fee,
'collectionType': self.collection_type,
-5
View File
@@ -17,11 +17,6 @@ class LoanRepaymentSchedule(db.Model):
total_repayment_amount = db.Column(db.Float, default=0.0)
paid = db.Column(db.Boolean, default=False)
paid_at = db.Column(db.DateTime, nullable=True)
due_process_date = db.Column(db.DateTime, nullable=True)
due_process_count = db.Column(db.Integer, default=0)
paid_status = db.Column(db.String(20), nullable=True)
repay_description = db.Column(db.String(255), nullable=True)
partial_balance = db.Column(db.Float, default=0.0)
created_at = db.Column(db.DateTime(timezone=True), server_default=func.now())
updated_at = db.Column(db.DateTime(timezone=True), server_default=func.now(), onupdate=func.now())
+2 -7
View File
@@ -36,8 +36,9 @@ class Repayment(db.Model):
def create_repayment(cls, customer_id, loan, transaction_id):
# Check that the loan is active
if loan.status not in [LoanStatus.ACTIVE, LoanStatus.START_REPAY, LoanStatus.ACTIVE_PARTIAL]:
if loan.status not in [LoanStatus.ACTIVE, LoanStatus.START_REPAY]:
raise ValueError(f"Repayment cannot be processed. Loan status: ({loan.status})")
repayment = cls(
customer_id=customer_id,
@@ -55,12 +56,6 @@ class Repayment(db.Model):
raise ValueError(f"Database integrity error: {err}")
return repayment
# Get loan repayments
@classmethod
def get_repayments_by_id(cls, loan_id):
return cls.query.filter_by(loan_id=loan_id).all()
def to_dict(self):
return {
+11 -21
View File
@@ -4,12 +4,6 @@ from app.models import account
from sqlalchemy.exc import IntegrityError
from sqlalchemy import and_, or_, not_
from sqlalchemy.sql import func
from app.api.enums import TransactionType
import logging
logger = logging.getLogger(__name__)
class Transaction(db.Model):
__tablename__ = 'transactions'
@@ -26,7 +20,7 @@ class Transaction(db.Model):
phone_number = db.Column(db.String(50), nullable=True)
created_at = db.Column(db.DateTime(timezone=True), server_default=func.now())
updated_at = db.Column(db.DateTime(timezone=True), server_default=func.now(), onupdate=func.now())
def __repr__(self):
return f'<Transaction {self.id}>'
@@ -36,21 +30,17 @@ class Transaction(db.Model):
# if cls.query.filter_by(transaction_id=transaction_id).first():
# raise ValueError("Duplicate Transaction")
if cls.query.filter(and_(cls.transaction_id == transaction_id, cls.type == type)).first():
if type == TransactionType.REPAYMENT:
logger.info('Repayment transaction already exists :::: But we like to continue.')
now = datetime.now()
type = TransactionType.REPAYMENT + '.'+ now.strftime("%Y%m%d%H%M%S")
logger.info('Modify Type :::: {0}'.format(type))
else:
raise ValueError("Duplicate Transaction")
if cls.query.filter( and_( cls.transaction_id ==transaction_id, cls.type==type) ).first():
raise ValueError("Duplicate Transaction")
transaction = cls(
transaction_id=transaction_id,
customer_id=customer_id,
account_id=account_id,
type=type,
channel=channel,
transaction_id = transaction_id,
customer_id = customer_id,
account_id = account_id,
type = type,
channel = channel,
created_at=datetime.now(timezone.utc),
updated_at=datetime.now(timezone.utc)
)
@@ -64,4 +54,4 @@ class Transaction(db.Model):
@classmethod
def get_transaction_by_id(cls, transaction_id):
return cls.query.get(transaction_id)
return cls.query.get(transaction_id)
+15 -49
View File
@@ -1,7 +1,7 @@
{
"openapi": "3.0.3",
"info": {
"title": "Swagger Simbrella Core [10/22/2025] - OpenAPI 3.0",
"title": "Swagger Bank Channel to Simbrella FirstAdvance - OpenAPI 3.0",
"description": "This is a Simbrella FirstAdvance Backend Server with the OpenAPI 3.0 specification. \n\n\nSome useful links:\n- [Web Simulated Demo Page](https://digifi-salaryloan.chiefsoft.net/)\n- [Web Management Support Portal](https://digifi-office.chiefsoft.net/auth/login)",
"termsOfService": "http://swagger.io/terms/",
"contact": {
@@ -20,19 +20,12 @@
{
"url": "http://api.dev.simbrellang.net:4500"
},
{
{
"url": "https://api.dev.simbrellang.net"
},
{
"url": "http://10.2.249.133:4500"
}
],
"tags": [
{
"name": "Health",
"description": "System health check including DB status."
},
{
{
"name": "Authorize",
"description": "This feature will be used for authorizing customers.",
"externalDocs": {
@@ -90,45 +83,6 @@
}
],
"paths": {
"/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",
"events_service_status":"Connection Successful",
"bank_status":"Connection Successful",
"db_uri": "postgresql://user:****@localhost:5432/digifi_db",
"error": []
}
}
}
},
"500": {
"description": "Health check failed",
"content": {
"application/json": {
"example": {
"status": "failed",
"db_status": "Connection Failed",
"events_service_status":"Connection Failed",
"bank_status":"Connection Failed",
"db_uri": "Unavailable",
"error":["could not connect to server: Connection refused"]
}
}
}
}
}
}
},
"/Authorize": {
"$ref": "swagger/paths/Authorize.json"
},
@@ -177,6 +131,18 @@
"RepaymentResponse": {
"$ref": "swagger/schemas/RepaymentResponse.json"
},
"CustomerConsentRequest": {
"$ref": "swagger/schemas/CustomerConsentRequest.json"
},
"CustomerConsentResponse": {
"$ref": "swagger/schemas/CustomerConsentResponse.json"
},
"NotificationCallbackRequest": {
"$ref": "swagger/schemas/NotificationCallbackRequest.json"
},
"NotificationCallbackResponse": {
"$ref": "swagger/schemas/NotificationCallbackResponse.json"
},
"ApiResponse": {
"$ref": "swagger/schemas/ApiResponse.json"
},
+180
View File
@@ -0,0 +1,180 @@
{
"openapi": "3.0.3",
"info": {
"title": "Swagger Bank Channel to Simbrella FirstAdvance - OpenAPI 3.0",
"description": "This is a Simbrella FirstAdvance Backend Server with the OpenAPI 3.0 specification. \n\n\nSome useful links:\n- [Web Simulated Demo Page](https://digifi-salaryloan.chiefsoft.net/)\n- [Web Management Support Portal](https://digifi-office.chiefsoft.net/auth/login)",
"termsOfService": "http://swagger.io/terms/",
"contact": {
"email": "support@chiefsoft.com"
},
"license": {
"name": "Apache 2.0",
"url": "http://www.apache.org/licenses/LICENSE-2.0.html"
},
"version": "1.0.11"
},
"servers": [
{
"url": "http://localhost:4500"
},
{
"url": "http://api.dev.simbrellang.net:4500"
},
{
"url": "https://api.dev.simbrellang.net"
}
],
"tags": [
{
"name": "Authorize",
"description": "This feature will be used for authorizing customers.",
"externalDocs": {
"description": "Find out more",
"url": "https://www.simbrellang.net"
}
},
{
"name": "AuthorizeRefresh",
"description": "This feature will be used for refreshing authorized customers.",
"externalDocs": {
"description": "Find out more",
"url": "https://www.simbrellang.net"
}
},
{
"name": "EligibilityCheck",
"description": "Eligibility Check Request",
"externalDocs": {
"description": "Find out more",
"url": "https://www.simbrellang.net"
}
},
{
"name": "SelectOffer",
"description": "This method is used the send the offer the customer selected to Simbrella.",
"externalDocs": {
"description": "Find out more",
"url": "https://www.simbrellang.net"
}
},
{
"name": "ProvideLoan",
"description": "Provide Loan Request.",
"externalDocs": {
"description": "Find out more",
"url": "https://www.simbrellang.net"
}
},
{
"name": "LoanStatus",
"description": "Loan Information Request.",
"externalDocs": {
"description": "Find out more",
"url": "https://www.simbrellang.net"
}
},
{
"name": "Repayment",
"description": "Repayment Request.",
"externalDocs": {
"description": "Find out more",
"url": "https://www.simbrellang.net"
}
}
],
"paths": {
"/Authorize": {
"$ref": "swagger/paths/Authorize.json"
},
"/AuthorizeRefresh": {
"$ref": "swagger/paths/AuthorizeRefresh.json"
},
"/EligibilityCheck": {
"$ref": "swagger/paths/EligibilityCheck.json"
},
"/SelectOffer": {
"$ref": "swagger/paths/SelectOffer.json"
},
"/ProvideLoan": {
"$ref": "swagger/paths/ProvideLoan.json"
},
"/LoanStatus": {
"$ref": "swagger/paths/LoanStatus.json"
},
"/Repayment": {
"$ref": "swagger/paths/Repayment.json"
}
},
"components": {
"schemas": {
"EligibilityCheckRequest": {
"$ref": "swagger/schemas/EligibilityCheckRequest.json"
},
"EligibilityCheckResponse": {
"$ref": "swagger/schemas/EligibilityCheckResponse.json"
},
"SelectOfferRequest": {
"$ref": "swagger/schemas/SelectOfferRequest.json"
},
"SelectOfferResponse": {
"$ref": "swagger/schemas/SelectOfferResponse.json"
},
"LoanStatusRequest": {
"$ref": "swagger/schemas/LoanStatusRequest.json"
},
"LoanStatusResponse": {
"$ref": "swagger/schemas/LoanStatusResponse.json"
},
"RepaymentRequest": {
"$ref": "swagger/schemas/RepaymentRequest.json"
},
"RepaymentResponse": {
"$ref": "swagger/schemas/RepaymentResponse.json"
},
"CustomerConsentRequest": {
"$ref": "swagger/schemas/CustomerConsentRequest.json"
},
"CustomerConsentResponse": {
"$ref": "swagger/schemas/CustomerConsentResponse.json"
},
"NotificationCallbackRequest": {
"$ref": "swagger/schemas/NotificationCallbackRequest.json"
},
"NotificationCallbackResponse": {
"$ref": "swagger/schemas/NotificationCallbackResponse.json"
},
"ApiResponse": {
"$ref": "swagger/schemas/ApiResponse.json"
},
"AuthorizeResponse": {
"$ref": "swagger/schemas/AuthorizeResponse.json"
},
"AuthorizeRequest": {
"$ref": "swagger/schemas/AuthorizeRequest.json"
},
"AuthorizeRefreshResponse": {
"$ref": "swagger/schemas/AuthorizeRefreshResponse.json"
},
"AuthorizeRefreshRequest": {
"$ref": "swagger/schemas/AuthorizeRefreshRequest.json"
}
},
"securitySchemes": {
"basicAuth": {
"type": "http",
"scheme": "basic"
},
"bearerAuth": {
"type": "http",
"scheme": "bearer",
"bearerFormat": "JWT"
}
}
},
"security": [
{
"basicAuth": [],
"bearerAuth": []
}
]
}
+1 -1
View File
@@ -1,6 +1,6 @@
{
"post": {
"tags": ["AuthorizeRefresh"],
"tags": ["Authorize Refresh"],
"summary": "Customer Authorize Refresh Request",
"description": "Customer Authorize Refresh Request",
"operationId": "AuthorizeRefresh",
+56
View File
@@ -0,0 +1,56 @@
{
"post": {
"tags": [
"CustomerConsent"
],
"summary": "Customer Consent Request",
"description": "Customer Consent Request",
"operationId": "CustomerConsent",
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"$ref": "../schemas/CustomerConsentRequest.json"
}
},
"application/xml": {
"schema": {
"$ref": "../schemas/CustomerConsentRequest.json"
}
},
"application/x-www-form-urlencoded": {
"schema": {
"$ref": "../schemas/CustomerConsentRequest.json"
}
}
}
},
"responses": {
"200": {
"description": "Successful",
"content": {
"application/json": {
"schema": {
"$ref": "../schemas/CustomerConsentResponse.json"
}
},
"application/xml": {
"schema": {
"$ref": "../schemas/CustomerConsentResponse.json"
}
}
}
},
"400": {
"description": "Invalid request"
},
"422": {
"description": "Validation exception"
},
"500": {
"description": "Internal server error"
}
}
}
}
@@ -0,0 +1,57 @@
{
"post": {
"tags": [
"NotificationCallback"
],
"summary": "Loan Information Request ",
"description": "Loan Information Request",
"operationId": "NotificationCallback",
"requestBody": {
"description": "Post JSON to conduct eligibility tests",
"content": {
"application/json": {
"schema": {
"$ref": "../schemas/NotificationCallbackRequest.json"
}
},
"application/xml": {
"schema": {
"$ref": "../schemas/NotificationCallbackRequest.json"
}
},
"application/x-www-form-urlencoded": {
"schema": {
"$ref": "../schemas/NotificationCallbackRequest.json"
}
}
},
"required": true
},
"responses": {
"200": {
"description": "Successful operation",
"content": {
"application/json": {
"schema": {
"$ref": "../schemas/NotificationCallbackResponse.json"
}
},
"application/xml": {
"schema": {
"$ref": "../schemas/NotificationCallbackResponse.json"
}
}
}
},
"400": {
"description": "Invalid request"
},
"422": {
"description": "Validation exception"
},
"500": {
"description": "Internal server error"
}
}
}
}
@@ -0,0 +1,37 @@
{
"type": "object",
"properties": {
"type": {
"type": "string",
"example": "CustomerConsentRequest"
},
"transactionId": {
"type": "string",
"example": "20171209232177"
},
"customerId": {
"type": "string",
"example": "CN621868"
},
"accountId": {
"type": "string",
"example": "ACN8263457"
},
"requestTime": {
"type": "string",
"format": "date-time",
"example": "2019-10-18 14:26:21.063"
},
"consentType": {
"type": "string",
"example": "Revoke"
},
"channel": {
"type": "string",
"example": "USSD"
}
},
"xml": {
"name": "CustomerConsentRequest"
}
}
@@ -0,0 +1,16 @@
{
"type": "object",
"properties": {
"resultCode": {
"type": "string",
"example": "00"
},
"resultDescription": {
"type": "string",
"example": "Request is received"
}
},
"xml": {
"name": "CustomerConsentResponse"
}
}
@@ -3,7 +3,7 @@
"properties": {
"transactionId": {
"type": "string",
"example": "TRX201712RK9232P115"
"example": "Tr201712RK9232P115"
},
"countryCode": {
"type": "string",
@@ -11,19 +11,19 @@
},
"customerId": {
"type": "string",
"example": "5268548"
},
"accountId": {
"type": "string",
"example": "4348094226"
"example": "CN621868"
},
"msisdn": {
"type": "string",
"example": "2348093451342"
"example": "8093451342"
},
"channel": {
"type": "string",
"example": "USSD"
"example": "100"
},
"accountId": {
"type": "string",
"example": "ACN8263457"
}
},
"xml": {
@@ -3,19 +3,19 @@
"properties": {
"customerId": {
"type": "string",
"example": "5268548"
"example": "CN621868"
},
"transactionId": {
"type": "string",
"example": "TRX201712RK9232P115"
"example": "TX12345"
},
"countryCode": {
"type": "string",
"example": "NGR"
"example": "NG"
},
"msisdn": {
"type": "string",
"example": "2348093451342"
"example": "3451342"
},
"eligibleOffers": {
"type": "array",
@@ -24,42 +24,42 @@
"properties": {
"offerId": {
"type": "string",
"example": "SAL90000204"
"example": "Offer1"
},
"productId": {
"type": "string",
"example": "3MPC"
"example": "Product1"
},
"minAamount": {
"type": "number",
"format": "decimal",
"example": 20000.00
"example": 100.00
},
"maxAamount": {
"type": "number",
"format": "decimal",
"example": 31257.00
"example": 1000.00
},
"tenor": {
"type": "integer",
"example": 90
"example": 12
}
}
},
"example": [
{
"max_amount": "31257.00",
"min_amount": 20000.0,
"offerId": "SAL90000204",
"product_id": "3MPC",
"tenor": 90
{
"offerId": "Offer1",
"productId": "Product1",
"minAamount": 100.00,
"maxAamount": 1000.00,
"tenor": 12
},
{
"max_amount": "20838.00",
"min_amount": 5000.0,
"offerId": "SAL30000205",
"product_id": "AMPC",
"tenor": 30
"offerId": "Offer2",
"productId": "Product2",
"minAamount": 200.00,
"maxAamount": 2000.00,
"tenor": 24
}
]
},
+4 -4
View File
@@ -3,15 +3,15 @@
"properties": {
"transactionId": {
"type": "string",
"example": "TRCVIC73089465966"
"example": "Tr201712RK9232P115"
},
"customerId": {
"type": "string",
"example": "ZX48440946"
"example": "CN621868"
},
"msisdn": {
"type": "string",
"example": "2348093451342"
"example": "3451342"
},
"channel": {
"type": "string",
@@ -19,7 +19,7 @@
},
"accountId": {
"type": "string",
"example": "361005323"
"example": "ACN8263457"
}
},
"xml": {
+77 -127
View File
@@ -1,133 +1,83 @@
{
"type": "object",
"properties": {
"customerId": {
"type": "string",
"example": "ZX48440946"
},
"transactionId": {
"type": "string",
"example": "TRCVIC49381378037"
},
"accountId": {
"type": "string",
"example": "361005323"
},
"loans": {
"type": "array",
"items": {
"type": "object",
"properties": {
"debtId": {
"type": "integer",
"example": 80
},
"loanDate": {
"type": "object",
"properties": {
"customerId": {
"type": "string",
"format": "date-time",
"example": "2025-09-12T11:58:58"
},
"dueDate": {
"type": "string",
"format": "date-time",
"example": "2025-12-11T11:58:58"
},
"currentLoanAmount": {
"type": "number",
"format": "float",
"example": 30000.0
},
"initialLoanAmount": {
"type": "number",
"format": "float",
"example": 30000.0
},
"defaultPenaltyFee": {
"type": "number",
"format": "float",
"example": 0.0
},
"continuousFee": {
"type": "number",
"format": "float",
"example": 0.0
},
"productId": {
"type": "string",
"example": "3MPC"
},
"installmentAmount": {
"type": "number",
"format": "float",
"example": 10900.0
},
"repaymentAmount": {
"type": "number",
"format": "float",
"example": 32700.0
},
"loanRef": {
"type": "string",
"example": "TRCVIC73089465966USSD3MPC"
},
"status": {
"type": "string",
"example": "active"
},
"tenor": {
"type": "integer",
"example": 90
},
"transactionId": {
"type": "string",
"example": "TRCVIC73089465966"
},
"upfrontFee": {
"type": "number",
"format": "float",
"example": 622.5
}
}
}
},
"resultCode": {
"type": "string",
"example": "00"
},
"resultDescription": {
"type": "string",
"example": "Successful"
},
"totalDebtAmount": {
"type": "number",
"format": "float",
"example": 30000.0
},
"summary": {
"type": "object",
"properties": {
"totalOutstandingAmount": {
"type": "number",
"format": "float",
"example": 114450.0,
"description": "Total amount still owed across all unpaid loans."
"example": "CN621868"
},
"totalActiveLoanAmount": {
"type": "number",
"format": "float",
"example": 40000.0,
"description": "Total principal amount of currently active loans."
"transactionId": {
"type": "string",
"example": "Tr201712RK9232P115"
},
"totalSettledAmount": {
"type": "number",
"format": "float",
"example": 80000.0,
"description": "Total amount that has been fully repaid."
"loans": {
"type": "array",
"items": {
"type": "object",
"properties": {
"debtId": {
"type": "string",
"example": "123456789"
},
"loanDate": {
"type": "string",
"format": "date-time",
"example": "2019-10-18 14:26:21.063"
},
"dueDate": {
"type": "string",
"format": "date-time",
"example": "2019-11-20 14:26:21.063"
},
"currentLoanAmount": {
"type": "integer",
"example": 8500
},
"initialLoanAmount": {
"type": "integer",
"example": 10000
},
"defaultPenaltyFee": {
"type": "integer",
"example": 0
},
"continuousFee": {
"type": "integer",
"example": 0
},
"productId": {
"type": "string",
"example": "101"
},
"installment": {
"type": "array",
"items": {
"type": "object",
"properties": {
"amount": {
"type": "number",
"format": "float",
"example": 10000.0
},
"repaymentDate": {
"type": "string",
"example": "2025-04-24 10:31:"
}
}
}
}
}
}
}
},
"resultCode": {
"type": "string",
"example": "00"
},
"resultDescription": {
"type": "string",
"example": "Successful"
}
},
"xml": {
"name": "LoanStatusResponse"
}
},
"xml": {
"name": "LoanStatusResponse"
}
}
}
@@ -0,0 +1,50 @@
{
"type": "object",
"properties": {
"fbnTransactionId": {
"type": "string",
"example": "123456789"
},
"transactionId": {
"type": "string",
"example": "123456789"
},
"customerId": {
"type": "string",
"example": "CN621868"
},
"accountId": {
"type": "string",
"example": "ACN8263457"
},
"debtId": {
"type": "string",
"example": "987654321"
},
"transactionType": {
"type": "string",
"example": "Disbursement"
},
"amountProvided": {
"type": "number",
"format": "float",
"example": 1000.00
},
"amountCollected": {
"type": "number",
"format": "float",
"example": 0.00
},
"responseCode": {
"type": "string",
"example": "00"
},
"responseDescription": {
"type": "string",
"example": "Successful"
}
},
"xml": {
"name": "NotificationCallbackRequest"
}
}
@@ -0,0 +1,16 @@
{
"type": "object",
"properties": {
"resultCode": {
"type": "string",
"example": "00"
},
"resultDescription": {
"type": "string",
"example": "Successful"
}
},
"xml": {
"name": "NotificationCallbackResponse"
}
}
+9 -9
View File
@@ -3,40 +3,40 @@
"properties": {
"requestId": {
"type": "string",
"example": "RQID11170001371256908"
"example": "202111170001371256908"
},
"transactionId": {
"type": "string",
"example": "TRCVIC73089465966"
"example": "Tr201712RK9232P115"
},
"customerId": {
"type": "string",
"example": "ZX48440946"
"example": "CN621868"
},
"accountId": {
"type": "string",
"example": "361005323"
"example": "ACN8263457"
},
"msisdn": {
"type": "string",
"example": "2348093451342"
"example": "3451342"
},
"requestedAmount": {
"type": "number",
"format": "decimal",
"example": 20000
"example": 900
},
"collectionType": {
"type": "integer",
"example": 0
"example": 1
},
"offerId": {
"type": "integer",
"example": "SAL900004543304"
"example": 1127
},
"channel": {
"type": "string",
"example": "USSD"
"example": "100"
}
},
"xml": {
+32 -67
View File
@@ -1,75 +1,40 @@
{
"type": "object",
"properties": {
"requestId": {
"type": "string",
"example": "81757678335583"
},
"transactionId": {
"type": "string",
"example": "TRCVIC73089465966"
},
"loanRef": {
"type": "string",
"example": "TRCVIC73089465966USSD3MPC"
},
"customerId": {
"type": "string",
"example": "ZX48440946"
},
"accountId": {
"type": "string",
"example": "361005323"
},
"msisdn": {
"type": "string",
"example": "98016510058"
},
"schedule": {
"type": "array",
"description": "List of loan repayment components with due dates and amounts.",
"items": {
"type": "object",
"properties": {
"id": {
"type": "integer",
"example": 1
},
"amountDue": {
"type": "number",
"example": 2000.0
},
"componentName": {
"type": "object",
"properties": {
"requestId": {
"type": "string",
"example": "INTEREST"
},
"dueDate": {
"example": "202111170001371256908"
},
"transactionId": {
"type": "string",
"format": "date-time",
"example": "2026-01-13T11:36:39.890747+00:00"
},
"startDate": {
"example": "Tr201712RK9232P115"
},
"loanRef": {
"type": "string",
"format": "date-time",
"example": "2025-10-15T11:36:39.890747+00:00"
},
"loanRef": {
"example": "1620029887USSDAMPC"
},
"customerId": {
"type": "string",
"example": "TRX1760528156816285USSD3MPC"
}
"example": "CN621868"
},
"accountId": {
"type": "string",
"example": "ACN8263457"
},
"msisdn": {
"type": "string",
"example": "3451342"
},
"resultCode": {
"type": "string",
"example": "00"
},
"resultDescription": {
"type": "string",
"example": "Successful"
}
}
},
"resultCode": {
"type": "string",
"example": "00"
},
"resultDescription": {
"type": "string",
"example": "Successful"
"xml": {
"name": "ProvideLoanResponse"
}
},
"xml": {
"name": "ProvideLoanResponse"
}
}
}
+7 -7
View File
@@ -3,27 +3,27 @@
"properties": {
"msisdn": {
"type": "string",
"example": "2348093451342"
"example": "3451342"
},
"debtId": {
"type": "number",
"example": 80
"type": "string",
"example": "10"
},
"transactionId": {
"type": "string",
"example": "TRCVIC73089465966"
"example": "20171209232115"
},
"customerId": {
"type": "string",
"example": "ZX48440946"
"example": "CID0000025585"
},
"loanRef": {
"type": "string",
"example": "TRCVIC73089465966USSD3MPC"
"example": "Trx5847365252USSD3MPC"
},
"accountId": {
"type": "string",
"example": "361005323"
"example": "ACN8263457"
}
},
"xml": {
+25 -45
View File
@@ -1,48 +1,28 @@
{
"type": "object",
"properties": {
"Id": {
"type": "integer",
"example": 195
"type": "object",
"properties": {
"customerId": {
"type": "string",
"example": "CN621868"
},
"productId": {
"type": "string",
"example": "101"
},
"debtId": {
"type": "string",
"example": "273194670"
},
"resultCode": {
"type": "string",
"example": "00"
},
"resultDescription": {
"type": "string",
"example": "Successful"
}
},
"customerId": {
"type": "string",
"example": "ZX48440946"
},
"debtId": {
"type": "string",
"example": "80"
},
"initiated_by": {
"type": "string",
"example": "USER_INITIATED"
},
"loanRef": {
"type": "string",
"example": "TRCVIC73089465966USSD3MPC"
},
"productId": {
"type": "string",
"example": "3MPC"
},
"repayment_id": {
"type": "integer",
"example": 195
},
"transactionId": {
"type": "string",
"example": "TRCVIC73089465966"
},
"resultCode": {
"type": "string",
"example": "00"
},
"resultDescription": {
"type": "string",
"example": "Successful"
"xml": {
"name": "RepaymentResponse"
}
},
"xml": {
"name": "RepaymentResponse"
}
}
}
+12 -12
View File
@@ -3,40 +3,40 @@
"properties": {
"requestId": {
"type": "string",
"example": "RQID11170001371256908"
"example": "202111170001371256908"
},
"transactionId": {
"type": "string",
"example": "TRX1231231321232"
"example": "1231231321232"
},
"customerId": {
"type": "string",
"example": "CN6215268548868"
},
"accountId": {
"type": "string",
"example": "4348094226"
"example": "CN621868"
},
"msisdn": {
"type": "string",
"example": "2348093451342"
"example": "123456789"
},
"requestedAmount": {
"type": "number",
"example": 20000
"format": "double",
"example": 10000.55
},
"accountId": {
"type": "string",
"example": "ACN8263457"
},
"productId": {
"type": "string",
"example": "3MPC"
},
"offerId": {
"type": "string",
"example": "SAL900004543304"
"example": "101"
},
"channel": {
"type": "string",
"example": "USSD"
"example": ""
}
},
"xml": {
+113 -121
View File
@@ -1,129 +1,121 @@
{
"type": "object",
"properties": {
"requestId": {
"type": "string",
"example": "81757678225025"
},
"transactionId": {
"type": "string",
"example": "TRCVIC73089465966"
},
"customerId": {
"type": "string",
"example": "ZX48440946"
},
"accountId": {
"type": "string",
"example": "361005323"
},
"loan": {
"type": "array",
"items": {
"type": "object",
"properties": {
"offerId": {
"type": "object",
"properties": {
"requestId": {
"type": "string",
"example": "SAL90000204"
},
"productId": {
"example": "202111170001371256908"
},
"transactionId": {
"type": "string",
"example": "3MPC"
},
"amount": {
"type": "number",
"format": "float",
"example": 30000.0
},
"upfrontPayment": {
"type": "number",
"format": "float",
"example": 622.5
},
"interestRate": {
"type": "number",
"format": "float",
"example": 3.0
},
"interestFee": {
"type": "number",
"format": "float",
"example": 2700.0
},
"managementRate": {
"type": "number",
"format": "float",
"example": 1.0
},
"managementFee": {
"type": "number",
"format": "float",
"example": 300.0
},
"insuranceRate": {
"type": "number",
"format": "float",
"example": 1.0
},
"insuranceFee": {
"type": "number",
"format": "float",
"example": 300.0
},
"VATRate": {
"type": "number",
"format": "float",
"example": 7.5
},
"VATAmount": {
"type": "number",
"format": "float",
"example": 22.5
},
"recommendedRepaymentDates": {
"example": "1231231321232"
},
"customerId": {
"type": "string",
"example": "1256907"
},
"accountId": {
"type": "string",
"example": "5948306019"
},
"loan": {
"type": "array",
"items": {
"type": "string"
},
"example": [
"2025-10-12",
"2025-11-12",
"2025-12-12"
]
},
"installmentAmount": {
"type": "number",
"format": "float",
"example": 10900.0
},
"repaymentAmount": {
"type": "number",
"format": "float",
"example": 32700.0
},
"totalRepaymentAmount": {
"type": "number",
"format": "float",
"example": 33322.5
}
"type": "object",
"properties": {
"offerId": {
"type": "string",
"example": "14451"
},
"productId": {
"type": "string",
"example": "3MPC"
},
"amount": {
"type": "number",
"format": "float",
"example": 10000.0
},
"dueDate": {
"type": "string",
"example": "2025-04-24 10:31:"
},
"upfrontPayment": {
"type": "number",
"format": "float",
"example": 1000.0
},
"interestRate": {
"type": "number",
"format": "float",
"example": 3.0
},
"interestFee": {
"type": "number",
"format": "float",
"example": 3000.00
},
"ManagementRate": {
"type": "number",
"format": "float",
"example": 1.0
},
"ManagementFee": {
"type": "number",
"format": "float",
"example": 1.0
},
"InsuranceRate": {
"type": "number",
"format": "float",
"example": 1.0
},
"InsuranceFee": {
"type": "number",
"format": "float",
"example": 100.0
},
"VATRate": {
"type": "number",
"format": "float",
"example": 7.5
},
"VATamount": {
"type": "number",
"format": "float",
"example": 100.0
},
"installmentRepaymentDates": {
"type": "array",
"items": {
"type": "string"
},
"example": [
"2022-11-30"
]
},
"installmentAmount": {
"type": "number",
"format": "float",
"example": 11000.0
},
"totalRepaymentAmount": {
"type": "number",
"format": "float",
"example": 11000.0
}
}
}
},
"resultCode": {
"type": "string",
"example": "00"
},
"resultDescription": {
"type": "string",
"example": "Successful"
}
}
},
"outstandingDebtAmount": {
"type": "number",
"format": "float",
"example": 0
},
"resultCode": {
"type": "string",
"example": "00"
},
"resultDescription": {
"type": "string",
"example": "Successful"
"xml": {
"name": "SelectOffersResponse"
}
},
"xml": {
"name": "SelectOffersResponse"
}
}
}
+1 -12
View File
@@ -1,4 +1,3 @@
version: '3.8'
services:
digifi-bank-to-product-core:
build: .
@@ -12,14 +11,4 @@ services:
- DATABASE_URL=postgresql+psycopg2://${DATABASE_USER}:${DATABASE_PASSWORD}@${DATABASE_HOST}:${DATABASE_PORT}/${DATABASE_NAME}
volumes:
- .:/app
restart: always
networks:
- my_custom_network
networks:
my_custom_network:
driver: bridge
ipam:
config:
- subnet: 10.244.0.0/26
restart: always
-45
View File
@@ -1,45 +0,0 @@
"""empty message
Revision ID: 30b45df851fa
Revises: d59bfb9ead82
Create Date: 2025-08-26 13:48:27.458593
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = '30b45df851fa'
down_revision = 'd59bfb9ead82'
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.add_column(
'loan_repayment_schedules',
sa.Column('paid_status', sa.String(length=20), nullable=True),
)
op.add_column(
'loan_repayment_schedules',
sa.Column('repay_description', sa.String(length=255), nullable=True),
)
op.add_column(
'loan_repayment_schedules',
sa.Column('partial_balance', sa.Float(), nullable=True),
)
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.drop_column('loan_repayment_schedules', 'partial_balance')
op.drop_column('loan_repayment_schedules', 'repay_description')
op.drop_column('loan_repayment_schedules', 'paid_status')
# ### end Alembic commands ###
-38
View File
@@ -1,38 +0,0 @@
"""empty message
Revision ID: d59bfb9ead82
Revises: 05b5494ad406
Create Date: 2025-08-21 14:22:19.220158
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = 'd59bfb9ead82'
down_revision = '05b5494ad406'
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.add_column(
'loan_repayment_schedules',
sa.Column('due_process_date', sa.DateTime(), nullable=True)
)
# Add due_process_count column
op.add_column(
'loan_repayment_schedules',
sa.Column('due_process_count', sa.Integer(), nullable=True)
)
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.drop_column('loan_repayment_schedules', 'due_process_date')
op.drop_column('loan_repayment_schedules', 'due_process_count')
# ### end Alembic commands ###