Compare commits

..

94 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
ameye c93e1a8bdd Merge branch 'mail_implementation' of DigiFi/digifi-BankToProductCore into master 2025-07-17 10:48:35 +00:00
VivianDee 6d8fe24718 [add]: send email in base service 2025-07-16 14:24:17 +01:00
VivianDee deaddd8132 [add]: mail 2025-07-16 14:19:10 +01:00
ameye 6e08b0680f Merge branch 'oracle_migration' of DigiFi/digifi-BankToProductCore into master 2025-07-09 23:30:59 +00:00
VivianDee 3c0e00d9e4 Merge branch 'oracle_migration' of https://gitlab.chiefsoft.net/DigiFi/digifi-BankToProductCore into oracle_migration 2025-07-09 12:34:54 +01:00
VivianDee b502c128f9 [fix]: migration 2025-07-09 12:33:50 +01:00
VivianDee c81a029e20 Update 48c62b4da905_.py 2025-07-09 12:33:50 +01:00
VivianDee 469d94cea1 [add]: migration 2025-07-09 12:33:50 +01:00
VivianDee d6aabb959e [add]: MIgration to oracle database 2025-07-09 12:33:50 +01:00
VivianDee 3a970816f5 [fix]: migration 2025-07-06 09:41:44 +01:00
VivianDee 65c1608b92 Update 48c62b4da905_.py 2025-07-06 09:33:04 +01:00
VivianDee 2493c075c0 [add]: balance to loan table 2025-07-06 09:26:59 +01:00
VivianDee 4ea06de2bc [add]: migration 2025-07-03 14:56:55 +01:00
VivianDee 1aa6a1710c [add]: MIgration to oracle database 2025-07-03 05:02:08 +01:00
ameye dff000dbb2 Merge branch 'add_balance_to_loan' of DigiFi/digifi-BankToProductCore into master 2025-06-23 10:41:24 +00:00
VivianDee 617738b785 [add]: balance to loan table 2025-06-23 11:13:40 +01:00
CHIEFSOFT\ameye 9db3b68b13 ACTIVE_PARTIAL = "active_partial" 2025-06-22 21:46:44 -04:00
CHIEFSOFT\ameye 1795db35be Revert "mercore starter"
This reverts commit 8bb5ce69e2.
2025-06-22 20:14:37 -04:00
CHIEFSOFT\ameye 8bb5ce69e2 mercore starter 2025-06-22 20:11:32 -04:00
CHIEFSOFT\ameye 5087afcca6 repayment id 2025-06-21 11:27:53 -04:00
CHIEFSOFT\ameye 11b52357ba reapyment is needed 2025-06-21 11:18:36 -04:00
ameye e8361668e5 Merge branch 'salary_table_fix' of DigiFi/digifi-BankToProductCore into master 2025-06-19 09:51:38 +00:00
VivianDee f659fa9cf2 [add]: salary model fix 2025-06-19 04:42:11 +01:00
CHIEFSOFT\ameye 23e340be27 Data adjust 2025-06-18 23:00:15 -04:00
ameye adc2498044 Merge branch 'add_salary_table' of DigiFi/digifi-BankToProductCore into master 2025-06-18 12:32:12 +00:00
VivianDee eb7f783b18 [add]: Salary table 2025-06-18 12:34:28 +01:00
ameye 5040002c54 Merge branch 'update_repayments_data_table' of DigiFi/digifi-BankToProductCore into master 2025-06-17 15:11:06 +00:00
VivianDee dc9415ff79 [fix]: Select offer requested amount 2025-06-16 15:40:50 +01:00
ameye 4822de764a Merge branch 'update_repayments_data_table' of DigiFi/digifi-BankToProductCore into master 2025-06-16 14:34:40 +00:00
VivianDee 51995a3e02 [update]: Repayments table 2025-06-16 12:27:04 +01:00
ameye 265bba2365 Merge branch 'loan_count_fix' of DigiFi/digifi-BankToProductCore into master 2025-06-13 18:53:52 +00:00
VivianDee 9985a58b56 [fix]: Max daily loan count 2025-06-13 19:11:10 +01:00
ameye b41df3fe02 Merge branch 'rac_check_analysis' of DigiFi/digifi-BankToProductCore into master 2025-06-13 13:01:45 +00:00
VivianDee 08fe04b7b9 Update offer_analysis.py 2025-06-13 13:57:10 +01:00
vivian.d.simbrellang.com 79317632b6 Merge branch 'rac_check_analysis' of DigiFi/digifi-BankToProductCore into master 2025-06-13 12:33:10 +00:00
VivianDee 0d87036b92 Update config.py 2025-06-13 13:32:03 +01:00
ameye 1734007476 Merge branch 'rac_check_analysis' of DigiFi/digifi-BankToProductCore into master 2025-06-12 22:46:20 +00:00
VivianDee 6ef2be9625 [add]: Offer analysis update 2025-06-12 15:24:14 +01:00
VivianDee 48020f5284 Update eligibility_check.py 2025-06-12 14:35:32 +01:00
VivianDee 1a6ac6a37f Update offer_analysis.py 2025-06-12 14:00:15 +01:00
ameye bbf6953dc5 Merge branch 'rac_check_analysis' of DigiFi/digifi-BankToProductCore into master 2025-06-12 11:44:46 +00:00
VivianDee a2158a768e [update]: Rac Check analysis 2025-06-11 15:19:24 +01:00
CHIEFSOFT\ameye 0af1b7567b Rack Rules 2025-06-10 06:58:53 -04:00
vivian.d.simbrellang.com 4d08983ae3 Merge branch 'repayments_data_model' of DigiFi/digifi-BankToProductCore into master 2025-06-10 07:44:44 +00:00
VivianDee 70e15cd325 [add]: Repayments data Model 2025-06-10 08:42:41 +01:00
CHIEFSOFT\ameye e8d930f9b8 offer analysis 2025-06-05 23:06:09 -04:00
CHIEFSOFT\ameye c400f1d69d addded comments 2025-06-05 18:49:14 -04:00
vivian.d.simbrellang.com f7daa12531 Merge branch 'loan_and_repayment_tables_update' of DigiFi/digifi-BankToProductCore into master 2025-06-04 11:45:24 +00:00
VivianDee 3242a57586 [update]: Loan and repayment table 2025-06-04 12:43:38 +01:00
ameye 463c0a0def Merge branch 'middlewear-integration' of DigiFi/digifi-BankToProductCore into master 2025-06-04 02:36:14 +00:00
VivianDee c061c9b5a4 Update eligibility_check.py 2025-06-02 08:16:12 -04:00
VivianDee 201fa4202e Update eligibility_check.py 2025-06-02 08:07:15 -04:00
CHIEFSOFT\ameye bb4d7ac064 Log added 2025-05-31 21:31:12 -04:00
CHIEFSOFT\ameye 5a2161acaa added acont id 2025-05-31 20:44:42 -04:00
VivianDee 10138f66f3 Update eligibility_check.py 2025-05-30 08:25:40 -04:00
CHIEFSOFT\ameye 9ea0027f71 channel 2025-05-29 15:19:29 -04:00
VivianDee d1b8d15f31 Update eligibility_check.py 2025-05-29 13:00:51 -04:00
ameye f716b47603 Merge branch 'loan_reference' of DigiFi/digifi-BankToProductCore into master 2025-05-28 20:22:53 +00:00
VivianDee ec5db19e20 [add]: loan max amount 2025-05-28 20:19:50 +01:00
VivianDee 729cc26698 [add]: loan ref to repayment 2025-05-28 20:05:38 +01:00
ameye c95e2786b5 Merge branch 'loan_reference' of DigiFi/digifi-BankToProductCore into master 2025-05-27 11:14:42 +00:00
VivianDee 65472d3f07 [update]: loan status response 2025-05-27 02:02:22 +01:00
ameye 29b2697b0e Merge branch 'loan_reference' of DigiFi/digifi-BankToProductCore into master 2025-05-26 12:10:35 +00:00
VivianDee 7a2ff6586f Update provide_loan.py 2025-05-26 13:01:27 +01:00
VivianDee 066ced55b0 Update provide_loan.py 2025-05-26 12:56:49 +01:00
CHIEFSOFT\ameye f6c98d9bfd repayment status 2025-05-25 06:33:46 -04:00
CHIEFSOFT\ameye 9e22e0fcf3 customer with loan listing 2025-05-24 07:41:20 -04:00
CHIEFSOFT\ameye c9aba07e9c Funct log 2025-05-24 07:10:44 -04:00
CHIEFSOFT\ameye 20c9a5c713 Remove logger 2025-05-24 07:08:03 -04:00
CHIEFSOFT\ameye 1cb0d88cc2 No need for new logger 2025-05-24 07:05:18 -04:00
CHIEFSOFT\ameye 3e9d5d4089 Looking for customer 2025-05-24 07:02:50 -04:00
CHIEFSOFT\ameye 2ae49ace86 fix call 2025-05-24 06:04:19 -04:00
CHIEFSOFT\ameye e9de001340 fix string 2025-05-24 05:50:17 -04:00
CHIEFSOFT\ameye 0bdc11423f Rack analysis 2025-05-24 05:37:21 -04:00
CHIEFSOFT\ameye a4ed936392 racResponse 2025-05-24 05:01:20 -04:00
CHIEFSOFT\ameye 1a315b1d80 SIMBRELLA_API_KEY 2025-05-24 04:52:58 -04:00
CHIEFSOFT\ameye 4c30f81bfd Updated racc check payload 2025-05-24 04:42:58 -04:00
CHIEFSOFT\ameye 916261fa94 Fix url 2025-05-24 04:34:11 -04:00
CHIEFSOFT\ameye 081b73a932 Added logger 2025-05-24 04:31:25 -04:00
CHIEFSOFT\ameye 6852986ce5 end poit config 2025-05-24 04:27:07 -04:00
CHIEFSOFT\ameye 0038c22577 SIMBRELLA_ENDPOINT_RAC_CHECKS 2025-05-24 04:22:40 -04:00
ameye 326ee87b13 Merge branch 'loan_min_amount' of DigiFi/digifi-BankToProductCore into master 2025-05-21 14:58:49 +00:00
VivianDee ca22ee86f7 [add]: Loan min amount 2025-05-21 15:49:11 +01:00
ameye aa033a50a3 Merge branch 'Offers_update' of DigiFi/digifi-BankToProductCore into master 2025-05-19 15:15:13 +00:00
VivianDee 31b0367e6a [update]: Offers 2025-05-19 14:37:19 +01:00
VivianDee 89760f81ed [update]: Offers Model 2025-05-19 11:51:44 +01:00
CHIEFSOFT\ameye 701840abd1 Removed product id 2025-05-18 08:33:18 -04:00
ameye df6c42ca2d Merge branch 'Response_fix' of DigiFi/digifi-BankToProductCore into master 2025-05-16 19:21:59 +00:00
ameye a321832d43 Merge branch 'Response_fix' of DigiFi/digifi-BankToProductCore into master 2025-05-14 21:50:14 +00:00
ameye d7b8addeb6 Merge branch 'Response_fix' of DigiFi/digifi-BankToProductCore into master 2025-05-12 18:01:27 +00:00
93 changed files with 2171 additions and 1304 deletions
+18 -9
View File
@@ -7,17 +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
# 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
+15
View File
@@ -0,0 +1,15 @@
from flask import Flask
from app.extensions import mail
from app.utils.mail import send_report_email, get_report_data
from app.config import settings
app = Flask(__name__)
app.config.from_object(settings)
mail.init_app(app)
with app.app_context():
report_data = get_report_data()
recipients = ["vdagbue@gmail.com"]
result = send_report_email(report_data, recipients)
print(result)
+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
```
+19 -1
View File
@@ -1,13 +1,16 @@
from re import U
from flask import Flask
from flask_mail import Mail
import os
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
from app.extensions import db, migrate
from app.extensions import db, migrate, mail
from flask_jwt_extended import (
JWTManager,
jwt_required,
@@ -18,6 +21,10 @@ 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
@@ -34,15 +41,26 @@ def create_app():
# Register blueprints
app.register_blueprint(api)
app.register_blueprint(eco, url_prefix="/eco")
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)
# Error Handlers
register_error_handlers(app)
from . import models
# Initialize Flask-Mail
mail.init_app(app)
# Database and Migrations
db.init_app(app)
+2
View File
@@ -3,4 +3,6 @@ from enum import Enum
class LoanStatus(str, Enum):
PENDING = "pending"
ACTIVE = "active"
ACTIVE_PARTIAL = "active_partial"
START_REPAY = "start_repay"
REPAID = "repaid"
+6 -17
View File
@@ -7,36 +7,25 @@ import logging
class SimbrellaIntegration:
BASE_URL = settings.SIMBRELLA_BASE_URL
ENDPOINT_RAC_CHECKS = settings.SIMBRELLA_ENDPOINT_RAC_CHECKS
@staticmethod
def rac_check(customer_id, account_id, transaction_id):
"""
Calls the RACCheck endpoit
"""
url = f"{SimbrellaIntegration.BASE_URL}/RACCheck"
url = f"{SimbrellaIntegration.BASE_URL}/{SimbrellaIntegration.ENDPOINT_RAC_CHECKS}"
logger.info(f"Contacting Rack Checks EndPoint: {str(url)}", exc_info=True)
payload = {
"customerId": customer_id,
"accountId": account_id,
"transactionId": str(transaction_id),
"fbnTransactionId": f"FBN{transaction_id}",
"RAC_Array": [
"SalaryAccount",
"BVN",
"BVNAttachedtoAccount",
"CRC",
"CRMS",
"AccountStatus",
"Lien",
"NoBouncedCheck",
"Whitelist",
"NoPastDueSalaryLoan",
"NoPastDueOtherLoan",
],
"fbnTransactionId": str(transaction_id),
"countryCode": "NG",
"channel": "USSD"
}
# logger.info(f"This is PayLoad: {str(payload)}", exc_info=True)
headers = {
"Content-Type": "application/json",
"x-api-key": f"{settings.VALID_API_KEY}",
+1
View File
@@ -88,6 +88,7 @@ def loan_status():
@jwt_required()
def repayment():
data = request.get_json()
logger.error(f"HERE 0000a **** ")
# logger.info(f"Repayment request received: {data}")
response = RepaymentService.process_request(data)
return response
+2 -2
View File
@@ -5,8 +5,8 @@ class RepaymentSchema(Schema):
type = fields.Str(required=False)
msisdn = fields.Str(required=False) #optional
debtId = fields.Str(required=True)
productId = fields.Str(required=True)
transactionId = fields.Str(required=True)
accountId = fields.Str(required=True)
customerId = fields.Str(required=True)
channel = fields.Str(required=True)
loanRef = fields.Str(required=True)
initiatedBy = fields.Str(required=False)
+7 -1
View File
@@ -4,6 +4,7 @@ from flask import jsonify
from marshmallow import ValidationError
import logging
from app.api.integrations import KafkaIntegration
from app.utils.mail import send_report_email, get_report_data
logger = logging.getLogger(__name__)
@@ -48,12 +49,14 @@ class BaseService:
"""
Create a new transaction.
"""
channel = "USSD" if validated_data.get("channel") is None else validated_data.get("channel")
return Transaction.create_transaction(
transaction_id = validated_data.get("transactionId"),
customer_id = validated_data.get('customerId', None),
account_id = validated_data.get("accountId", None),
type = cls.TRANSACTION_TYPE,
channel = validated_data.get("channel"),
channel = channel,
)
@classmethod
@@ -171,3 +174,6 @@ class BaseService:
# return {"rate": 0, "fee": 0, "due_days": 0}
@classmethod
def send_mail(cls, report_data, recipients):
send_report_email(report_data, recipients)
+58 -9
View File
@@ -1,5 +1,5 @@
from flask import session, jsonify
from app.models.transaction_offers import TransactionOffer
from app.models.loan import Loan
from app.utils.logger import logger
from app.api.services.base_service import BaseService
from app.api.schemas.eligibility_check import EligibilityCheckSchema
@@ -50,6 +50,12 @@ class EligibilityCheckService(BaseService):
return ResponseHelper.error(result_description="Invalid Customer or Account")
db.session.flush()
# Determine Loan count
is_eligible = EligibilityCheckService.check_loan_limits(customer_id)
if not is_eligible:
return ResponseHelper.error(result_description="Max loan count reached")
# Call RACCheck
response = SimbrellaIntegration.rac_check(
@@ -58,30 +64,44 @@ class EligibilityCheckService(BaseService):
transaction_id = transaction.transaction_id,
)
# this chck for error is not valid
# this chek for error is not valid
if response.status_code != 200:
return ResponseHelper.error(result_description="RACCheck failed")
response = response.json()
logger.info(f"This is Response (from Eligibility Check): {str(response)}", exc_info=True)
if not response or response['responseCode'] != '00':
if response:
logger.error(f"{response['responseMessage']}")
return ResponseHelper.error(result_description=f"RACCheck failed")
rack_checks_response = response['data']['racResponse']
rac_check = RACCheck.add_rac_check(
customer_id = customer_id,
account_id = account_id,
transaction_id = transaction.transaction_id,
data = response['RACResponse']
data = rack_checks_response
)
if not rac_check:
logger.error(f"Failed to save RACCheck")
return ResponseHelper.error(result_description="Failed to save RACCheck.")
# -----------------TIME FOR ANALYSIS TO REGISTER OFFER ----------------------
logger.error(f"Failed to save RACCheck")
return ResponseHelper.error(result_description="Failed to save RACCheck.")
# -----------------TIME FOR ANALYSIS TO REGISTER OFFER ----------------------
# eligible_offers = []
try:
eligible_offers = OfferAnalysis.decide_offer(
transaction_id=transactionId,
rac_check=rac_check,
validated_data=validated_data,
customer_id=customer_id
customer_id=customer_id,
rack_checks_response =rack_checks_response
)
except ValueError as ve:
logger.error(str(ve))
@@ -146,4 +166,33 @@ class EligibilityCheckService(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()
@staticmethod
def check_loan_limits(customer_id):
"""
Checks if a customer has exceeded the loan limits for given offer.
"""
loan = Loan.get_customer_last_loan(customer_id)
if not loan:
return True
offer_id = loan.offer_id[:5]
offer = Offer.get_offer_by_id(offer_id)
if not offer:
logger.error(f"Offer not found for offer_id: {offer_id} (customer_id: {customer_id})")
return False
daily_count = Loan.get_daily_loan_count(customer_id, offer.product_id)
logger.info(f"daily_count: {daily_count}, Max: {offer.max_daily_loans}")
if offer.max_daily_loans is not None and daily_count >= offer.max_daily_loans:
return False
return True
+5 -19
View File
@@ -29,38 +29,23 @@ class LoanStatusService(BaseService):
# Validate data
validated_data = LoanStatusService.validate_data(data, LoanStatusSchema())
customer_id = validated_data.get('customerId')
customer = Customer.get_customer(customer_id)
logger.info(f"Looking for customer *** {customer_id}")
customer = Customer.get_customer_with_loan_list(customer_id)
transactionId = validated_data.get('transactionId')
account_id = validated_data.get('accountId')
if(LoanStatusService.validate_account_ownership(account_id = account_id, customer_id = customer_id)):
# Get loans
loans = [loan.to_dict() for loan in customer.loans if loan.status == LoanStatus.ACTIVE]
transaction = LoanStatusService.log_transaction(validated_data = validated_data)
if not transaction:
logger.error(f"Failed to log transaction")
return ResponseHelper.error(result_description="Failed to log transaction.")
else:
return ResponseHelper.error(result_description="Invalid Customer or Account")
# loans = [
# {
# "debtId": "123456789",
# "loanDate": "2019-10-18 14:26:21.063",
# "dueDate": "2019-11-20 14:26:21.063",
# "currentLoanAmount": 8500,
# "initialLoanAmount": 10000,
# "defaultPenaltyFee": 0,
# "continuousFee": 0,
# "productId": "101"
# }
# ]
total_debt_amount = sum(
loan.get("currentLoanAmount") or 0
@@ -70,6 +55,7 @@ class LoanStatusService(BaseService):
# Simulated processing logic
response_data = {
"customerId": customer_id,
"accountId": account_id,
"transactionId": transactionId,
"loans": loans,
"totalDebtAmount": total_debt_amount,
+132 -7
View File
@@ -1,7 +1,15 @@
from decimal import Decimal
from app.models import Offer, TransactionOffer
from app.models.loan import Loan
import random
import logging
from app.config import Config
RAC_TRUE_CHECK_RULES = Config.rac_true_rules
RAC_FALSE_CHECK_RULES = Config.rac_false_rules
RAC_SALARY_PAYMENTS = Config.rac_salary_payments
logger = logging.getLogger(__name__)
class OfferAnalysis:
@@ -32,20 +40,127 @@ class OfferAnalysis:
original_transaction = transaction_id
return transaction_offer, offer, eligible_amount, original_transaction
@staticmethod
def _analyze_rack_checks(rack_response, offer):
logger.info(f"This is PayLoad for ANALYSYS ***** : {str(rack_response)}", exc_info=True)
logger.info(f"RACk TRUE RULES {str(RAC_TRUE_CHECK_RULES)}", exc_info=True)
logger.info(f"RACk FALSE RULES {str(RAC_FALSE_CHECK_RULES)}", exc_info=True)
logger.info(f"RACk SALARY PAYMENTS {str(RAC_SALARY_PAYMENTS)}", exc_info=True)
if not isinstance(rack_response, dict) or not offer :
raise ValueError("Invalid RAC response format.")
failed_true_rules = []
failed_false_rules = []
salaries = []
# Expects true
for rule in RAC_TRUE_CHECK_RULES:
if not rack_response.get(rule, False):
failed_true_rules.append(rule)
# Expects false
for rule in RAC_FALSE_CHECK_RULES:
if rack_response.get(rule, True):
failed_false_rules.append(rule)
# Salary rules
for key in RAC_SALARY_PAYMENTS:
value = rack_response.get(key)
if isinstance(value, Decimal):
# Only use values greater than 0
if value > 0:
salaries.append(value)
elif isinstance(value, (int, float, str)):
try:
value = Decimal(str(value))
if value > 0:
salaries.append(value)
except:
logger.warning(f"Could not convert value of {key} to Decimal: {value}")
if failed_true_rules or failed_false_rules or not salaries:
logger.warning(f"Failed TRUE rules: {failed_true_rules}")
logger.warning(f"Failed FALSE rules: {failed_false_rules}")
logger.warning("No salary records found in RAC response.")
raise ValueError(f"RAC analysis failed")
logger.info(f"These are the salary amounts ***** : {str(salaries)}", exc_info=True)
#Least salary in the last 6 months
min_salary = min(salaries)
# Check consistency rule
consistent_income = rack_response.get("rule7_consistent_salary_amount", False)
# Determine percentage based on offer tenor
tenor = offer.tenor
if tenor == 30 and consistent_income:
eligible_amount = min_salary * Decimal("0.5")
logger.info("Applying 50% of least salary in 6 months due to 1-month offer tenor with stable income.")
elif tenor == 90 and consistent_income:
eligible_amount = min_salary * Decimal("0.75")
logger.info("Applying 75% of least salary in 6 months due to 3-months offer tenor with stable income.")
else: # Income is not consistent
eligible_amount = 0
logger.info("Applying no percentage on least salary due unstable income.")
logger.info(f"Calculated eligible amount from RAC: {eligible_amount} based on {'stable' if consistent_income else 'unstable'} income.")
return eligible_amount.quantize(Decimal("1.00"))
# "racResponse": {
# "accountStatus": true,
# "bvnValidated": true,
# "creditBureauCheck": false,
# "crmsCheck": true,
# "hasLien": false,
# "hasPastDueLoan": false,
# "hasSalaryAccount": true,
# "isWhitelisted": true,
# "noBouncedCheck": true
# },
#
'''
30 days
Eligibility amount (monthly SOL) - Adoption of 50% of the least salary inflow in the past 6 months
to determine loan eligibility for potential customers.
3 months
Adoption of 75% of the least salary inflow in the past 6 months to determine loan eligibility for
potential customers" for customers that have unstable income. 3 months
'''
# rac_true_rules
return 0
@staticmethod
def decide_offer(transaction_id, rac_check, validated_data, customer_id):
def decide_offer(transaction_id, rac_check, validated_data, customer_id, rack_checks_response):
eligible_offers = []
# if we have active offers - we have to feed off it
logger.info(f"LOOOOOOOOOOOOOOOOOO** {customer_id}")
logger.info(f"**RACK ANALYSIS** {customer_id}")
# Analyze Rack Checks
# new_eligible_amount = OfferAnalysis._analyze_rack_checks(rack_checks_response) #--> We need detail analysis
# we can now find the origin transactions
# Find the last loan - it will have original_transaction
last_customer_loan = Loan.get_customer_last_loan(customer_id)
# logger.info(f"{last_customer_loan}")
new_eligible_amount = 0
if last_customer_loan:
original_transaction = last_customer_loan.original_transaction or last_customer_loan.transaction_id
logger.info(f"transaction_id |-| original_transaction === > {transaction_id} {original_transaction}")
@@ -63,6 +178,10 @@ class OfferAnalysis:
logger.info(f"sum_active_loans === > {sum_active_loans}")
real_eligible_amount = original_loan.eligible_amount - sum_active_loans
if real_eligible_amount < original_transaction_offer.min_amount:
logger.error(f"Max eligible amount ({real_eligible_amount}) is less than the minimum offer amount ({original_transaction_offer.min_amount}).")
raise ValueError("You are not eligible for a loan at this time.")
transaction_offer = TransactionOffer.create_transaction_offer(
customer_id=customer_id,
transaction_id=transaction_id,
@@ -93,12 +212,18 @@ class OfferAnalysis:
for offer in offers:
# Get approved amount
random_float = random.random() # temporary to play data
new_eligible_amount = OfferAnalysis._analyze_rack_checks(rack_checks_response, offer)
approved_amount = new_eligible_amount if new_eligible_amount > 0 else min(offer.max_amount, offer.max_amount * random_float)
approved_amount = new_eligible_amount
approved_amount = round(approved_amount, 2)
if approved_amount < offer.min_amount:
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,
transaction_id=transaction_id,
+13
View File
@@ -60,6 +60,13 @@ class ProvideLoanService(BaseService):
except ValueError as ve:
logger.error(str(ve))
return ResponseHelper.error(result_description=str(ve))
if(amount < transaction_offer.min_amount):
return ResponseHelper.error(result_description="The amount is less than the minimum allowed transaction amount.")
elif amount > transaction_offer.max_amount:
return ResponseHelper.error(result_description="The amount is greater than the maximum allowed transaction amount.")
# transaction_offer_id = int(offer_id[5:]) # The last part is int
@@ -105,6 +112,9 @@ class ProvideLoanService(BaseService):
insurance = charges["insurance"]
vat = charges["vat"]
padded_id = str(transaction_id).zfill(12)
loan_ref = f"{padded_id}{channel}{offer.product_id}"
# Save the loan details
loan = Loan.create_loan(
@@ -122,6 +132,7 @@ class ProvideLoanService(BaseService):
eligible_amount=eligible_amount,
status = LoanStatus.ACTIVE,
tenor = offer.tenor,
reference = loan_ref
)
if not loan:
@@ -151,10 +162,12 @@ class ProvideLoanService(BaseService):
else:
return ResponseHelper.error(result_description="Invalid Customer or Account")
response_data = {
"requestId": request_id,
"transactionId": transaction_id,
"loanRef": loan_ref,
"customerId": customer_id,
"accountId": account_id,
"msisdn": customer.msisdn
+20 -15
View File
@@ -29,48 +29,53 @@ class RepaymentService(BaseService):
try:
with db.session.begin():
validated_data = RepaymentService.validate_data(data, RepaymentSchema())
customer_id = validated_data.get('customerId')
request_id = validated_data.get('requestId')
loan_id = validated_data.get('debtId')
product_id = validated_data.get('productId')
account_id = validated_data.get('accountId')
customer = Customer.get_customer(customer_id)
loan_ref = validated_data.get('loanRef')
# customer = Customer.get_customer_with_loan_list(customer_id)
transaction_id = validated_data.get('transactionId')
initiated_by = validated_data.get('initiatedBy')
logger.error(f"HERE 0002a **** ")
if(RepaymentService.validate_account_ownership(account_id = account_id, customer_id = customer_id)):
logger.error(f"HERE 0001a **** ")
# Check loan exists
loan = Loan.get_customer_loan(loan_id = loan_id, customer_id = customer_id)
# Save the repayment details
repayment = Repayment.create_repayment(
customer_id = customer_id,
loan_id = loan_id,
product_id = product_id,
transaction_id=transaction_id
loan = loan,
transaction_id = transaction_id
)
if not repayment:
logger.error(f"Failed to save repayment details")
return ResponseHelper.error(result_description="Failed to save repayment details.")
#Update Loan status
Loan.update_status(loan_id = loan_id, status = LoanStatus.REPAID)
Loan.update_status(loan_id = loan_id, status = LoanStatus.START_REPAY) # repay started bu user
transaction = RepaymentService.log_transaction(validated_data = validated_data)
if not transaction:
logger.error(f"Failed to log transaction")
return ResponseHelper.error(result_description="Failed to log transaction.")
else:
else:
logger.error(f"Invalid Customer or AccountID {account_id} to CustomerID{customer_id} ")
return ResponseHelper.error(result_description="Invalid Customer or Account")
# Simulated processing logic
# TODO start using repayment_id instead if id or Id
response_data = {
"Id": repayment.id,
"repayment_id": repayment.id,
"initiated_by": repayment.initiated_by,
"transactionId": transaction_id,
"customerId": customer_id,
"productId": product_id,
"productId": loan.product_id,
"loanRef": loan_ref,
"debtId": loan_id
}
+16
View File
@@ -3,6 +3,7 @@ from marshmallow import ValidationError
from app.api.helpers.response_helper import ResponseHelper
from app.api.services.base_service import BaseService
from app.api.enums import TransactionType
from app.models.transaction_offers import TransactionOffer
from app.utils.logger import logger
from app.api.schemas.select_offer import SelectOfferSchema
from app.extensions import db
@@ -57,8 +58,23 @@ class SelectOfferService(BaseService):
# Get the offer by product ID
offer = Offer.get_offer_by_product_id(product_id)
transaction_offer = TransactionOffer.get_transaction_offer(transaction_offer_id=offer_id)
if not transaction_offer:
logger.error(f"offer {offer_id} not found for customer {customer_id} and transaction {transaction_id}.")
return ResponseHelper.error(result_description="Offer not found.")
db.session.flush()
if amount < transaction_offer.min_amount:
logger.error(f"The amount {amount} is less than the minimum allowed offer amount {transaction_offer.min_amount}.")
return ResponseHelper.error(result_description="The amount is less than the minimum allowed offer amount.")
elif amount > transaction_offer.eligible_amount:
logger.error(f"The amount {amount} is greater than the eligible offer amount {transaction_offer.eligible_amount}.")
return ResponseHelper.error(result_description="The amount is greater than the eligible offer amount.")
charges = SelectOfferService.calculate_charges(offer, amount)
upfront_payment = charges["upfront_payment"]
total_amount = charges["total_amount"]
+81 -6
View File
@@ -9,21 +9,38 @@ class Config:
API_URL = os.getenv("API_URL", "/swagger.json")
DEBUG = True
VALID_APP_ID = os.getenv("VALID_APP_ID", "app1")
VALID_API_KEY = os.getenv("VALID_API_KEY", "test-api-key-12345")
BASIC_AUTH_USERNAME = os.environ.get("BASIC_AUTH_USERNAME", "user")
BASIC_AUTH_PASSWORD = os.environ.get("BASIC_AUTH_PASSWORD", "password")
# Database Configuration
DATABASE_USER = os.environ.get("DATABASE_USER")
DATABASE_PASSWORD = os.environ.get("DATABASE_PASSWORD")
DATABASE_HOST = os.environ.get("DATABASE_HOST")
DATABASE_PORT = os.environ.get("DATABASE_PORT", 10532)
DATABASE_NAME = os.environ.get("DATABASE_NAME")
DATABASE_NAME = os.environ.get("DATABASE_NAME", "firstadvancedev")
DATABASE_SID = os.environ.get("DATABASE_SID", "FREE")
DNS = f"(DESCRIPTION=(ADDRESS=(PROTOCOL=TCP)(HOST={DATABASE_HOST})(PORT={DATABASE_PORT}))(CONNECT_DATA=(SID={DATABASE_SID})))"
# 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 = (
f"oracle+oracledb://{DATABASE_USER}:{DATABASE_PASSWORD}@{DNS}"
)
SQLALCHEMY_BINDS = {
"eco": f"postgresql+psycopg2://{ECO_DATABASE_USER}:{ECO_DATABASE_PASSWORD}@{ECO_DATABASE_HOST}:{ECO_DATABASE_PORT}/{ECO_DATABASE_NAME}"
}
SQLALCHEMY_DATABASE_URI = f"postgresql+psycopg2://{DATABASE_USER}:{DATABASE_PASSWORD}@{DATABASE_HOST}:{DATABASE_PORT}/{DATABASE_NAME}"
SQLALCHEMY_TRACK_MODIFICATIONS = False
SIMBRELLA_BASE_URL = os.getenv("SIMBRELLA_BASE_URL", "http://127.0.0.1:6337")
JWT_SECRET_KEY = os.getenv("JWT_SECRET_KEY", "secret-key")
JWT_ACCESS_TOKEN_EXPIRES = os.getenv("JWT_ACCESS_TOKEN_EXPIRES", timedelta(hours=1))
@@ -34,5 +51,63 @@ class Config:
# KAFKA_BROKER = 'dev-events.simbrellang.net:9085'
KAFKA_BROKER = os.getenv("KAFKA_BROKER", "dev-events.simbrellang.net:9085")
# SIMBRELLA_ENDPOINT_RAC_CHECKS = os.getenv("SIMBRELLA_ENDPOINT_RAC_CHECKS", "RACCheck")
VALID_APP_ID = os.getenv("SIMBRELLA_APP_ID", "app1")
VALID_API_KEY = os.getenv("SIMBRELLA_API_KEY", "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"
)
RAC_RESULT_accountStatus = os.environ.get("RAC_RESULT_accountStatus", "true")
RAC_RESULT_bvnValidated = os.environ.get("RAC_RESULT_bvnValidated", "true")
RAC_RESULT_creditBureauCheck = os.environ.get(
"RAC_RESULT_creditBureauCheck", "false"
)
RAC_RESULT_crmsCheck = os.environ.get("RAC_RESULT_crmsCheck", "true")
RAC_RESULT_hasLien = os.environ.get("RAC_RESULT_hasLien", "false")
RAC_RESULT_hasPastDueLoan = os.environ.get("RAC_RESULT_hasPastDueLoan", "false")
RAC_RESULT_hasSalaryAccount = os.environ.get("RAC_RESULT_hasSalaryAccount", "true")
RAC_RESULT_isWhitelisted = os.environ.get("RAC_RESULT_isWhitelisted", "true")
RAC_RESULT_noBouncedCheck = os.environ.get("RAC_RESULT_noBouncedCheck", "true")
rac_true_rules = [
"rule1_45day_sal",
"rule2_2m_sal",
"rule3_no_bounced_check",
"rule4_current_loan_payments",
"rule5_no_past_due_fadv_loan",
"rule6_no_past_due_other_loan",
"rule7_consistent_salary_amount",
"rule8_whitelisted",
"rule9_regular_account",
"rule10_bvn_validation",
"rule11_CRC_no_delinquency",
"rule12_CRMS_no_delinquency",
"rule13_BVN_ignore",
"rule14_no_lien",
"rule15_null_ignore",
]
rac_false_rules = []
rac_salary_payments = [
"salarypaymenT_1",
"salarypaymenT_2",
"salarypaymenT_3",
"salarypaymenT_4",
"salarypaymenT_5",
"salarypaymenT_6",
]
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_USE_TLS = True
MAIL_USE_SSL = False
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()
+2
View File
@@ -1,5 +1,7 @@
from flask_sqlalchemy import SQLAlchemy
from flask_migrate import Migrate
from flask_mail import Mail
mail = Mail()
db = SQLAlchemy()
migrate = Migrate()
+10 -1
View File
@@ -9,6 +9,15 @@ from .charge import Charge
from .rac_checks import RACCheck
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']
__all__ = ['Customer', 'Account', 'Loan', 'Transaction', 'Repayment', 'LoanCharge', 'Offer', 'Charge', 'RACCheck', 'LoanRepaymentSchedule', 'TransactionOffer', 'RepaymentsData', 'Salary']
+4 -2
View File
@@ -13,7 +13,7 @@ class Account(db.Model):
status = db.Column(db.String(20), default='active')
lien_amount = 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), onupdate=func.now())
updated_at = db.Column(db.DateTime(timezone=True), server_default=func.now(), onupdate=func.now())
customer = relationship(
"Customer",
@@ -27,7 +27,9 @@ class Account(db.Model):
account = cls(
id=id,
customer_id=customer_id,
account_type=account_type
account_type=account_type,
created_at=datetime.now(timezone.utc),
updated_at=datetime.now(timezone.utc)
)
try:
+4 -2
View File
@@ -14,7 +14,7 @@ class Charge(db.Model):
description = db.Column(db.Text, nullable=True)
due = db.Column(db.Integer, nullable=False)
created_at = db.Column(db.DateTime(timezone=True), server_default=func.now())
updated_at = db.Column(db.DateTime(timezone=True), onupdate=func.now())
updated_at = db.Column(db.DateTime(timezone=True), server_default=func.now(), onupdate=func.now())
offer = relationship(
"Offer",
primaryjoin="Charge.offer_id == Offer.id",
@@ -57,7 +57,9 @@ class Charge(db.Model):
code = code,
percent = percent,
description = description,
due = due_days
due = due_days,
created_at=datetime.now(timezone.utc),
updated_at=datetime.now(timezone.utc)
)
db.session.add(charge_obj)
+16 -3
View File
@@ -1,9 +1,12 @@
from datetime import datetime, timezone
from sqlalchemy.orm import relationship
#
# from app.api.services.offer_analysis import logger
from app.extensions import db
from app.models.account import Account
from sqlalchemy.exc import IntegrityError
from sqlalchemy.sql import func
# from app.utils.logger import logger
class Customer(db.Model):
__tablename__ = 'customers'
@@ -12,7 +15,7 @@ class Customer(db.Model):
msisdn = db.Column(db.String(20), unique=True, nullable=False)
country_code = db.Column(db.String(3), nullable=False)
created_at = db.Column(db.DateTime(timezone=True), server_default=func.now())
updated_at = db.Column(db.DateTime(timezone=True), onupdate=func.now())
updated_at = db.Column(db.DateTime(timezone=True), server_default=func.now(), onupdate=func.now())
accounts = relationship(
"Account",
primaryjoin="Customer.id == Account.customer_id",
@@ -45,9 +48,19 @@ class Customer(db.Model):
def create_customer(cls, id, msisdn, country_code, account_id, account_type='savings'):
if cls.query.filter_by(id=id).first():
raise ValueError("Customer already exists")
elif Account.query.filter_by(id=account_id).first():
raise ValueError("Account already exists")
elif cls.query.filter_by(msisdn=msisdn).first():
raise ValueError("msisdn already exists")
# Create the customer
customer = cls(id=id, msisdn=msisdn, country_code=country_code)
customer = cls(
id=id,
msisdn=msisdn,
country_code=country_code,
created_at=datetime.now(timezone.utc),
updated_at=datetime.now(timezone.utc)
)
try:
db.session.add(customer)
@@ -63,7 +76,7 @@ class Customer(db.Model):
return customer
@classmethod
def get_customer(cls, customer_id):
def get_customer_with_loan_list(cls, customer_id):
"""
Get customer by ID.
"""
+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})>"
+38 -3
View File
@@ -1,4 +1,5 @@
from datetime import datetime, timezone
from datetime import datetime, timezone, timedelta
from itertools import product
from app.extensions import db
from app.models.customer import Customer
from app.models.account import Account
@@ -34,15 +35,21 @@ class Loan(db.Model):
continuous_fee = db.Column(db.Float, default=0)
upfront_fee = db.Column(db.Float, nullable=True, default=0.0)
repayment_amount = db.Column(db.Float, nullable=True, default=0.0)
balance = db.Column(db.Float, nullable=True, default=0.0)
installment_amount = db.Column(db.Float, nullable=True, default=0.0)
status = db.Column(db.String(20), default='pending')
tenor = db.Column(db.Integer, nullable=True)
due_date = db.Column(db.DateTime, nullable=True)
created_at = db.Column(db.DateTime(timezone=True), server_default=func.now())
updated_at = db.Column(db.DateTime(timezone=True), onupdate=func.now())
updated_at = db.Column(db.DateTime(timezone=True), server_default=func.now(), onupdate=func.now())
eligible_amount = db.Column(db.Float, nullable=True, default=0.0)
disburse_date = db.Column(db.DateTime, nullable=True)
disburse_verify = db.Column(db.DateTime, nullable=True)
reference = db.Column(db.String(50), nullable=True)
disburse_result = db.Column(db.String(10), nullable=True)
disburse_description = db.Column(db.String(100), nullable=True)
verify_result = db.Column(db.String(10), nullable=True)
verify_description = db.Column(db.String(100), nullable=True)
customer = relationship(
"Customer",
@@ -82,6 +89,7 @@ class Loan(db.Model):
installment_amount,
tenor,
eligible_amount,
reference,
status = "pending",
):
# Check if customer exists
@@ -105,11 +113,15 @@ class Loan(db.Model):
current_loan_amount = initial_loan_amount,
upfront_fee = upfront_fee,
repayment_amount = repayment_amount,
balance = repayment_amount,
installment_amount = installment_amount,
due_date=due_date,
tenor = tenor,
status = status,
eligible_amount =eligible_amount
eligible_amount =eligible_amount,
reference = reference,
created_at=datetime.now(timezone.utc),
updated_at=datetime.now(timezone.utc)
)
try:
@@ -205,12 +217,34 @@ class Loan(db.Model):
# Update loan status and the updated_at timestamp
loan.status = status
@classmethod
def get_daily_loan_count(cls, customer_id, product_id):
"""
Returns the count of loans created today for a customer.
"""
start_of_day = datetime.now(timezone.utc).replace(hour=0, minute=0, second=0, microsecond=0)
end_of_day = start_of_day + timedelta(days=1)
return cls.query.filter_by(
customer_id=customer_id,
product_id=product_id,
).filter(
cls.created_at >= start_of_day,
cls.created_at < end_of_day
).count()
def to_dict(self):
"""
Convert the Loan object to a dictionary format for JSON serialization.
"""
return {
'debtId': self.id,
'transactionId': self.transaction_id,
'loanRef': self.reference,
'productId': self.product_id,
'initialLoanAmount': self.initial_loan_amount,
'currentLoanAmount': self.current_loan_amount,
'defaultPenaltyFee': self.default_penalty_fee,
@@ -220,6 +254,7 @@ class Loan(db.Model):
'repaymentAmount': self.repayment_amount,
'installmentAmount': self.installment_amount,
'status': self.status,
'tenor': self.tenor,
'dueDate': self.due_date.isoformat() if self.due_date else None,
'loanDate': self.created_at.isoformat() if self.created_at else None,
}
+4 -2
View File
@@ -18,7 +18,7 @@ class LoanCharge(db.Model):
due = db.Column(db.Integer, nullable=False)
due_date = db.Column(db.DateTime, nullable=True)
created_at = db.Column(db.DateTime(timezone=True), server_default=func.now())
updated_at = db.Column(db.DateTime(timezone=True), onupdate=func.now())
updated_at = db.Column(db.DateTime(timezone=True), server_default=func.now(), onupdate=func.now())
loan = relationship(
"Loan",
primaryjoin="LoanCharge.loan_id == Loan.id",
@@ -63,7 +63,9 @@ class LoanCharge(db.Model):
percent = percent,
description = description,
due = due_days,
due_date = now + timedelta(days=due_days)
due_date = now + timedelta(days=due_days),
created_at=datetime.now(timezone.utc),
updated_at=datetime.now(timezone.utc)
)
db.session.add(charge_obj)
+4 -2
View File
@@ -19,7 +19,7 @@ class LoanRepaymentSchedule(db.Model):
paid_at = db.Column(db.DateTime, nullable=True)
created_at = db.Column(db.DateTime(timezone=True), server_default=func.now())
updated_at = db.Column(db.DateTime(timezone=True), onupdate=func.now())
updated_at = db.Column(db.DateTime(timezone=True), server_default=func.now(), onupdate=func.now())
loan = relationship(
"Loan",
primaryjoin="LoanRepaymentSchedule.loan_id == Loan.id",
@@ -45,7 +45,9 @@ class LoanRepaymentSchedule(db.Model):
total_repayment_amount = round(loan.repayment_amount, 2),
installment_amount=round(loan.installment_amount, 2),
product_id = loan.product_id,
transaction_id = transaction_id
transaction_id = transaction_id,
created_at=datetime.now(timezone.utc),
updated_at=datetime.now(timezone.utc)
)
db.session.add(schedule)
+12 -4
View File
@@ -7,8 +7,8 @@ from sqlalchemy.sql import func
class Offer(db.Model):
__tablename__ = 'offers'
id = db.Column(db.String, primary_key=True)
product_id = db.Column(db.String, nullable=False)
id = db.Column(db.String(50), primary_key=True)
product_id = db.Column(db.String(50), nullable=False)
min_amount = db.Column(db.Float, nullable=False)
max_amount = db.Column(db.Float, nullable=False)
tenor = db.Column(db.Integer, nullable=False)
@@ -18,8 +18,12 @@ class Offer(db.Model):
insurance_rate = db.Column(db.Float, default=1.0)
vat_rate = db.Column(db.Float, default=7.5)
list_order = db.Column(db.Integer, nullable=True)
max_daily_loans = db.Column(db.Integer, nullable=True)
max_active_loans = db.Column(db.Integer, nullable=True)
max_life_loans = db.Column(db.Integer, nullable=True)
created_at = db.Column(db.DateTime(timezone=True), server_default=func.now())
updated_at = db.Column(db.DateTime(timezone=True), onupdate=func.now())
updated_at = db.Column(db.DateTime(timezone=True), server_default=func.now(), onupdate=func.now())
charges = relationship(
"Charge",
primaryjoin="Offer.id == Charge.offer_id",
@@ -79,7 +83,11 @@ class Offer(db.Model):
"interest_rate": self.interest_rate,
"management_rate": self.management_rate,
"insurance_rate": self.insurance_rate,
"vat_rate": self.vat_rate
"vat_rate": self.vat_rate,
"maxDailyLoans": self.max_daily_loans,
"maxActiveLoans": self.max_active_loans,
"maxLifeLoans": self.max_life_loans
}
def __repr__(self):
+21 -6
View File
@@ -3,6 +3,7 @@ from app.extensions import db
from sqlalchemy.orm import relationship
from sqlalchemy.exc import IntegrityError
from uuid import uuid4
import json
from sqlalchemy.types import JSON
from sqlalchemy.sql import func
@@ -11,11 +12,22 @@ class RACCheck(db.Model):
id = db.Column(db.Integer, primary_key=True, autoincrement=True)
transaction_id = db.Column(db.String(50), nullable=False)
customer_id = db.Column(db.String, nullable=False)
account_id = db.Column(db.String, nullable=False)
rac_response = db.Column(db.JSON, nullable=False)
customer_id = db.Column(db.String(50), nullable=False)
account_id = db.Column(db.String(50), nullable=False)
rac_response = db.Column(db.Text, nullable=False)
created_at = db.Column(db.DateTime(timezone=True), server_default=func.now())
updated_at = db.Column(db.DateTime(timezone=True), onupdate=func.now())
updated_at = db.Column(db.DateTime(timezone=True), server_default=func.now(), onupdate=func.now())
@property
def rac_response_data(self):
return json.loads(self.rac_response)
@rac_response_data.setter
def rac_response_data(self, value):
self.rac_response = json.dumps(value)
@classmethod
def add_rac_check(cls, customer_id, account_id, transaction_id, data = None):
@@ -25,8 +37,11 @@ class RACCheck(db.Model):
customer_id = customer_id,
account_id = account_id,
transaction_id = transaction_id,
rac_response = data
created_at=datetime.now(timezone.utc),
updated_at=datetime.now(timezone.utc)
)
rac_check.rac_response_data = data or {}
try:
db.session.add(rac_check)
@@ -64,7 +79,7 @@ class RACCheck(db.Model):
"transactionId": str(self.transaction_id),
"customerId": self.customer_id,
"accountId": self.account_id,
"racResponse": self.rac_response,
"racResponse": self.rac_response_data,
"createdAt": self.created_at.isoformat(),
"updatedAt": self.updated_at.isoformat() if self.updated_at else None
}
+38 -14
View File
@@ -19,30 +19,35 @@ class Repayment(db.Model):
customer_id = db.Column(db.String(50), nullable=False)
product_id = db.Column(db.String(20), nullable=True)
created_at = db.Column(db.DateTime(timezone=True), server_default=func.now())
updated_at = db.Column(db.DateTime(timezone=True), onupdate=func.now())
updated_at = db.Column(db.DateTime(timezone=True), server_default=func.now(), onupdate=func.now())
transaction_id = db.Column(db.String(50), nullable=True)
# repay_date = db.Column(db.DateTime, default=datetime.now(timezone.utc))
repay_date = db.Column(db.DateTime, nullable=True)
repay_result = db.Column(db.String(10), nullable=True)
repay_description = db.Column(db.String(100), nullable=True)
# verify_date = db.Column(db.DateTime, default=datetime.now(timezone.utc))
verify_date = db.Column(db.DateTime, nullable=True)
verify_result = db.Column(db.String(10), nullable=True)
verify_description = db.Column(db.String(100), nullable=True)
initiated_by = db.Column(db.String(50), nullable=True)
salary_amount = db.Column(db.Float, nullable=True, default=0.0)
@classmethod
def create_repayment(cls, customer_id, loan_id, product_id, transaction_id):
# Check customer exists
if not Customer.is_valid_customer(customer_id):
raise ValueError("Invalid customer")
# Check loan exists
loan = Loan.get_customer_loan(loan_id = loan_id, customer_id = customer_id)
def create_repayment(cls, customer_id, loan, transaction_id):
# Check that the loan is active
if loan.status != LoanStatus.ACTIVE:
if loan.status not in [LoanStatus.ACTIVE, LoanStatus.START_REPAY]:
raise ValueError(f"Repayment cannot be processed. Loan status: ({loan.status})")
repayment = cls(
customer_id=customer_id,
loan_id=loan_id,
product_id=product_id,
transaction_id = transaction_id
loan_id=loan.id,
product_id=loan.product_id,
transaction_id = transaction_id,
created_at=datetime.now(timezone.utc),
updated_at=datetime.now(timezone.utc),
initiated_by='USER_INITIATED'
)
try:
@@ -51,6 +56,25 @@ class Repayment(db.Model):
raise ValueError(f"Database integrity error: {err}")
return repayment
def to_dict(self):
return {
"id": self.id,
"loan_id": self.loan_id,
"customer_id": self.customer_id,
"product_id": self.product_id,
"created_at": self.created_at.isoformat() if self.created_at else None,
"updated_at": self.updated_at.isoformat() if self.updated_at else None,
"transaction_id": self.transaction_id,
"repay_date": self.repay_date.isoformat() if self.repay_date else None,
"repay_result": self.repay_result,
"repay_description": self.repay_description,
"verify_date": self.verify_date.isoformat() if self.verify_date else None,
"verify_result": self.verify_result,
"verify_description": self.verify_description,
"initiated_by": self.initiated_by,
"salary_amount": self.salary_amount
}
def __repr__(self):
return f'<Repayment {self.id}>'
+36
View File
@@ -0,0 +1,36 @@
from datetime import datetime, timezone
from app.extensions import db
class RepaymentsData(db.Model):
__tablename__ = "repayments_data"
id = db.Column(db.Integer, primary_key=True, autoincrement=True)
transaction_id = db.Column(db.String(50), nullable=False)
fbn_transaction_id = db.Column(db.String(50), nullable=True)
customer_id = db.Column(db.String(50), nullable=True)
account_id = db.Column(db.String(50), nullable=True)
repayment_amount = db.Column(db.Float, nullable=True, default=0.0)
amount_collected = db.Column(db.Float, nullable=True, default=0.0)
added_date = db.Column(db.DateTime(timezone=True), default=datetime.now(timezone.utc), nullable=False)
response_code = db.Column(db.String(10), nullable=True)
response_descr = db.Column(db.String(255), nullable=True)
balance = db.Column(db.Float, nullable=True, default=0.0)
def to_dict(self):
return {
"id": self.id,
"transaction_id": self.transaction_id,
"fbn_transaction_id": self.fbn_transaction_id,
"customer_id": self.customer_id,
"account_id": self.account_id,
"repayment_amount": self.repayment_amount,
"amount_collected": self.amount_collected,
"added_date": self.added_date.isoformat() if self.added_date else None,
"response_code": self.response_code,
"response_descr": self.response_descr,
"balance": self.balance,
}
def __repr__(self):
return f"<RepaymentsData id={self.id}, transaction_id={self.transaction_id}>"
+31
View File
@@ -0,0 +1,31 @@
from datetime import datetime, timezone
from app.extensions import db
from sqlalchemy.sql import func
class Salary(db.Model):
__tablename__ = 'salaries'
id = db.Column(db.Integer, primary_key=True, autoincrement=True)
customer_id = db.Column(db.String(50), nullable=False)
account_id = db.Column(db.String(50), nullable=True)
status = db.Column(db.String(20), default='active')
amount = db.Column(db.Float, nullable=False, default=0.0)
salary_date = db.Column(db.DateTime(timezone=False), server_default=func.now())
created_at = db.Column(db.DateTime(timezone=False), server_default=func.now())
updated_at = db.Column(db.DateTime(timezone=False), server_default=func.now(), onupdate=func.now())
def to_dict(self):
return {
"id": self.id,
"customer_id": self.customer_id,
"account_id": self.account_id,
"status": self.status,
"amount": self.amount,
"salary_date": self.salary_date.isoformat() if self.salary_date else None,
"created_at": self.created_at.isoformat() if self.created_at else None,
"updated_at": self.updated_at.isoformat() if self.updated_at else None
}
def __repr__(self):
return f'<Salary {self.id} - {self.amount}>'
+6 -2
View File
@@ -17,8 +17,10 @@ class Transaction(db.Model):
customer_id = db.Column(db.String(50), nullable=True)
type = db.Column(db.String(50), nullable=False)
channel = db.Column(db.String(50), nullable=False)
phone_number = db.Column(db.String(50), nullable=True)
created_at = db.Column(db.DateTime(timezone=True), server_default=func.now())
updated_at = db.Column(db.DateTime(timezone=True), onupdate=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}>'
@@ -38,7 +40,9 @@ class Transaction(db.Model):
customer_id = customer_id,
account_id = account_id,
type = type,
channel = channel
channel = channel,
created_at=datetime.now(timezone.utc),
updated_at=datetime.now(timezone.utc)
)
try:
+34 -5
View File
@@ -1,4 +1,5 @@
from datetime import datetime, timezone
from datetime import datetime, timezone, timedelta
from app.api.enums.loan_status import LoanStatus
from app.extensions import db
from sqlalchemy.orm import relationship
from sqlalchemy.sql import func
@@ -22,7 +23,7 @@ class TransactionOffer(db.Model):
tenor = db.Column(db.Integer, nullable=True) # tenor in months, typically
created_at = db.Column(db.DateTime(timezone=True), server_default=func.now())
updated_at = db.Column(db.DateTime(timezone=True), onupdate=func.now())
updated_at = db.Column(db.DateTime(timezone=True), server_default=func.now(), onupdate=func.now())
customer = relationship(
"Customer",
primaryjoin="Customer.id == TransactionOffer.customer_id",
@@ -31,12 +32,11 @@ class TransactionOffer(db.Model):
)
@classmethod
def is_valid_transaction_offer(cls, transaction_offer, customer_id, product_id):
transaction_offer = cls.query.filter_by(
id = transaction_offer,
customer_id = customer_id,
product_id = product_id
# product_id = product_id
# transaction_id = transaction_id,
).first()
@@ -59,7 +59,9 @@ class TransactionOffer(db.Model):
max_amount=max_amount,
eligible_amount=eligible_amount,
product_id=product_id,
tenor=tenor
tenor=tenor,
created_at=datetime.now(timezone.utc),
updated_at=datetime.now(timezone.utc)
)
db.session.add(transaction_offer)
@@ -67,6 +69,33 @@ class TransactionOffer(db.Model):
return transaction_offer
@classmethod
def get_lifetime_loan_count(cls, customer_id):
"""
Returns the total number of loans ever created for a customer.
"""
return cls.query.filter_by(customer_id=customer_id).count()
@classmethod
def get_latest_transaction_offer(cls, customer_id):
"""
Returns the most recent transaction offer for the given customer based on creation time.
"""
return cls.query.filter_by(customer_id=customer_id) \
.order_by(cls.created_at.desc()) \
.first()
@classmethod
def get_transaction_offer(cls, transaction_offer_id):
"""
Returns a transaction offer by its ID.
"""
return cls.query.get(transaction_offer_id)
def to_dict(self):
return {
'id': self.id,
+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": []
}
]
}
+2 -6
View File
@@ -9,10 +9,6 @@
"type": "string",
"example": "10"
},
"productId": {
"type": "string",
"example": "101"
},
"transactionId": {
"type": "string",
"example": "20171209232115"
@@ -21,9 +17,9 @@
"type": "string",
"example": "CID0000025585"
},
"channel": {
"loanRef": {
"type": "string",
"example": "USSD"
"example": "Trx5847365252USSD3MPC"
},
"accountId": {
"type": "string",
+40
View File
@@ -0,0 +1,40 @@
from flask_mail import Message
from flask import current_app
from app.extensions import mail
import pandas as pd
from io import BytesIO
def get_report_data():
"""
Fetch and return loan summary data.
"""
return [
{"Type": "Disbursement", "Count": 45},
{"Type": "Repayment", "Count": 32},
]
def send_report_email(report_data: list, recipients: list):
"""
Sends an HTML + Excel report to the given email recipients.
"""
df = pd.DataFrame(report_data)
output = BytesIO()
df.to_excel(output, index=False)
output.seek(0)
html_table = df.to_html(index=False, border=1)
msg = Message(
subject="Loan Report Summary",
recipients=recipients,
html=f"<h3>Loan Report Summary</h3>{html_table}",
)
msg.attach(
"loan_report.xlsx",
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
output.read()
)
with current_app.app_context():
mail.send(msg)
return "Report email sent"
+31
View File
@@ -0,0 +1,31 @@
"""empty message
Revision ID: 05b5494ad406
Revises: 33e09efd85e3
Create Date: 2025-07-06 09:28:26.264927
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = "05b5494ad406"
down_revision = "33e09efd85e3"
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.add_column("loans", sa.Column("balance", sa.Float(), nullable=True))
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.drop_column("loans", "balance")
# ### end Alembic commands ###
@@ -1,32 +0,0 @@
"""Migration on Thu Apr 10 21:50:01 UTC 2025
Revision ID: 1340e7e578b9
Revises: b8f6fd76ead8
Create Date: 2025-04-10 21:50:32.113149
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = '1340e7e578b9'
down_revision = 'b8f6fd76ead8'
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
with op.batch_alter_table('transactions', schema=None) as batch_op:
batch_op.add_column(sa.Column('ref_model', sa.String(length=50), nullable=True))
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
with op.batch_alter_table('transactions', schema=None) as batch_op:
batch_op.drop_column('ref_model')
# ### end Alembic commands ###
@@ -1,32 +0,0 @@
"""Migration on Sat May 10 09:54:34 UTC 2025
Revision ID: 173ea45db189
Revises: 3105abd795d4
Create Date: 2025-05-10 09:54:39.380499
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = '173ea45db189'
down_revision = '3105abd795d4'
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
with op.batch_alter_table('transaction_offers', schema=None) as batch_op:
batch_op.add_column(sa.Column('original_transaction', sa.String(length=50), nullable=True))
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
with op.batch_alter_table('transaction_offers', schema=None) as batch_op:
batch_op.drop_column('original_transaction')
# ### end Alembic commands ###
@@ -1,53 +0,0 @@
"""Migration on Thu Apr 24 17:42:25 UTC 2025
Revision ID: 1b2339f43824
Revises: de9ad96ba34e
Create Date: 2025-04-24 17:43:09.589626
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = '1b2339f43824'
down_revision = 'de9ad96ba34e'
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.create_table('rac_checks',
sa.Column('id', sa.String(), nullable=False),
sa.Column('transaction_id', sa.String(length=50), nullable=False),
sa.Column('customer_id', sa.String(), nullable=False),
sa.Column('account_id', sa.String(), nullable=False),
sa.Column('rac_response', sa.JSON(), nullable=False),
sa.Column('created_at', sa.DateTime(), nullable=True),
sa.Column('updated_at', sa.DateTime(), nullable=True),
sa.PrimaryKeyConstraint('id')
)
with op.batch_alter_table('loan_charges', schema=None) as batch_op:
batch_op.alter_column('amount',
existing_type=sa.NUMERIC(precision=10, scale=2),
type_=sa.Float(),
existing_nullable=True)
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
with op.batch_alter_table('loans', schema=None) as batch_op:
batch_op.drop_column('installment_amount')
batch_op.drop_column('repayment_amount')
batch_op.drop_column('upfront_fee')
with op.batch_alter_table('loan_charges', schema=None) as batch_op:
batch_op.alter_column('amount',
existing_type=sa.Float(),
type_=sa.NUMERIC(precision=10, scale=2),
existing_nullable=True)
op.drop_table('rac_checks')
# ### end Alembic commands ###
@@ -1,32 +0,0 @@
"""Migration on Wed Apr 16 18:35:18 UTC 2025
Revision ID: 287ecb02d3d7
Revises: a4847b997191
Create Date: 2025-04-16 18:36:04.632791
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = '287ecb02d3d7'
down_revision = 'a4847b997191'
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
with op.batch_alter_table('loan_charges', schema=None) as batch_op:
batch_op.drop_column('transaction_id')
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
with op.batch_alter_table('loan_charges', schema=None) as batch_op:
batch_op.add_column(sa.Column('transaction_id', sa.VARCHAR(length=50), autoincrement=False, nullable=True))
# ### end Alembic commands ###
@@ -1,42 +0,0 @@
"""Migration on Fri Apr 25 15:01:00 UTC 2025
Revision ID: 2a45dd99c9cb
Revises: 2cf0c177ca02
Create Date: 2025-04-25 15:01:51.129681
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = '2a45dd99c9cb'
down_revision = '2cf0c177ca02'
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
with op.batch_alter_table('loan_repayment_schedules', schema=None) as batch_op:
batch_op.add_column(sa.Column('product_id', sa.String(length=20), nullable=True))
batch_op.add_column(sa.Column('installment_amount', sa.Float(), nullable=True))
batch_op.add_column(sa.Column('total_repayment_amount', sa.Float(), nullable=True))
batch_op.drop_column('principal_amount')
batch_op.drop_column('interest_amount')
batch_op.drop_column('total_installment')
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
with op.batch_alter_table('loan_repayment_schedules', schema=None) as batch_op:
batch_op.add_column(sa.Column('total_installment', sa.DOUBLE_PRECISION(precision=53), autoincrement=False, nullable=True))
batch_op.add_column(sa.Column('interest_amount', sa.DOUBLE_PRECISION(precision=53), autoincrement=False, nullable=True))
batch_op.add_column(sa.Column('principal_amount', sa.DOUBLE_PRECISION(precision=53), autoincrement=False, nullable=True))
batch_op.drop_column('total_repayment_amount')
batch_op.drop_column('installment_amount')
batch_op.drop_column('product_id')
# ### end Alembic commands ###
@@ -1,41 +0,0 @@
"""Migration on Fri Apr 25 14:02:01 UTC 2025
Revision ID: 2cf0c177ca02
Revises: 1b2339f43824
Create Date: 2025-04-25 14:02:42.244146
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = '2cf0c177ca02'
down_revision = '1b2339f43824'
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.create_table('loan_repayment_schedules',
sa.Column('id', sa.Integer(), autoincrement=True, nullable=False),
sa.Column('loan_id', sa.Integer(), nullable=False),
sa.Column('installment_number', sa.Integer(), nullable=False),
sa.Column('due_date', sa.DateTime(), nullable=False),
sa.Column('principal_amount', sa.Float(), nullable=True),
sa.Column('interest_amount', sa.Float(), nullable=True),
sa.Column('total_installment', sa.Float(), nullable=True),
sa.Column('paid', sa.Boolean(), nullable=True),
sa.Column('paid_at', sa.DateTime(), nullable=True),
sa.Column('created_at', sa.DateTime(), nullable=True),
sa.Column('updated_at', sa.DateTime(), nullable=True),
sa.PrimaryKeyConstraint('id')
)
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.drop_table('loan_repayment_schedules')
# ### end Alembic commands ###
-250
View File
@@ -1,250 +0,0 @@
"""empty message
Revision ID: 2eee4157505f
Revises: 565bc3d0ba6e
Create Date: 2025-05-16 13:24:41.914400
"""
from alembic import op
import sqlalchemy as sa
from sqlalchemy.dialects import postgresql
# revision identifiers, used by Alembic.
revision = '2eee4157505f'
down_revision = '565bc3d0ba6e'
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
with op.batch_alter_table('accounts', schema=None) as batch_op:
batch_op.alter_column('created_at',
existing_type=postgresql.TIMESTAMP(),
type_=sa.DateTime(timezone=True),
existing_nullable=True)
batch_op.alter_column('updated_at',
existing_type=postgresql.TIMESTAMP(),
type_=sa.DateTime(timezone=True),
existing_nullable=True)
with op.batch_alter_table('charges', schema=None) as batch_op:
batch_op.alter_column('created_at',
existing_type=postgresql.TIMESTAMP(),
type_=sa.DateTime(timezone=True),
existing_nullable=True)
batch_op.alter_column('updated_at',
existing_type=postgresql.TIMESTAMP(),
type_=sa.DateTime(timezone=True),
existing_nullable=True)
with op.batch_alter_table('customers', schema=None) as batch_op:
batch_op.alter_column('created_at',
existing_type=postgresql.TIMESTAMP(),
type_=sa.DateTime(timezone=True),
existing_nullable=True)
batch_op.alter_column('updated_at',
existing_type=postgresql.TIMESTAMP(),
type_=sa.DateTime(timezone=True),
existing_nullable=True)
with op.batch_alter_table('loan_charges', schema=None) as batch_op:
batch_op.alter_column('created_at',
existing_type=postgresql.TIMESTAMP(),
type_=sa.DateTime(timezone=True),
existing_nullable=True)
batch_op.alter_column('updated_at',
existing_type=postgresql.TIMESTAMP(),
type_=sa.DateTime(timezone=True),
existing_nullable=True)
with op.batch_alter_table('loan_repayment_schedules', schema=None) as batch_op:
batch_op.alter_column('created_at',
existing_type=postgresql.TIMESTAMP(),
type_=sa.DateTime(timezone=True),
existing_nullable=True)
batch_op.alter_column('updated_at',
existing_type=postgresql.TIMESTAMP(),
type_=sa.DateTime(timezone=True),
existing_nullable=True)
with op.batch_alter_table('loans', schema=None) as batch_op:
batch_op.alter_column('created_at',
existing_type=postgresql.TIMESTAMP(),
type_=sa.DateTime(timezone=True),
existing_nullable=True)
batch_op.alter_column('updated_at',
existing_type=postgresql.TIMESTAMP(),
type_=sa.DateTime(timezone=True),
existing_nullable=True)
with op.batch_alter_table('offers', schema=None) as batch_op:
batch_op.alter_column('created_at',
existing_type=postgresql.TIMESTAMP(),
type_=sa.DateTime(timezone=True),
existing_nullable=True)
batch_op.alter_column('updated_at',
existing_type=postgresql.TIMESTAMP(),
type_=sa.DateTime(timezone=True),
existing_nullable=True)
with op.batch_alter_table('rac_checks', schema=None) as batch_op:
batch_op.alter_column('created_at',
existing_type=postgresql.TIMESTAMP(),
type_=sa.DateTime(timezone=True),
existing_nullable=True)
batch_op.alter_column('updated_at',
existing_type=postgresql.TIMESTAMP(),
type_=sa.DateTime(timezone=True),
existing_nullable=True)
with op.batch_alter_table('repayments', schema=None) as batch_op:
batch_op.alter_column('created_at',
existing_type=postgresql.TIMESTAMP(),
type_=sa.DateTime(timezone=True),
existing_nullable=True)
batch_op.alter_column('updated_at',
existing_type=postgresql.TIMESTAMP(),
type_=sa.DateTime(timezone=True),
existing_nullable=True)
with op.batch_alter_table('transaction_offers', schema=None) as batch_op:
batch_op.alter_column('created_at',
existing_type=postgresql.TIMESTAMP(),
type_=sa.DateTime(timezone=True),
existing_nullable=True)
batch_op.alter_column('updated_at',
existing_type=postgresql.TIMESTAMP(),
type_=sa.DateTime(timezone=True),
existing_nullable=True)
with op.batch_alter_table('transactions', schema=None) as batch_op:
batch_op.alter_column('created_at',
existing_type=postgresql.TIMESTAMP(),
type_=sa.DateTime(timezone=True),
existing_nullable=True,
existing_server_default=sa.text('now()'))
batch_op.alter_column('updated_at',
existing_type=postgresql.TIMESTAMP(),
type_=sa.DateTime(timezone=True),
existing_nullable=True,
existing_server_default=sa.text('now()'))
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
with op.batch_alter_table('transactions', schema=None) as batch_op:
batch_op.alter_column('updated_at',
existing_type=sa.DateTime(timezone=True),
type_=postgresql.TIMESTAMP(),
existing_nullable=True,
existing_server_default=sa.text('now()'))
batch_op.alter_column('created_at',
existing_type=sa.DateTime(timezone=True),
type_=postgresql.TIMESTAMP(),
existing_nullable=True,
existing_server_default=sa.text('now()'))
with op.batch_alter_table('transaction_offers', schema=None) as batch_op:
batch_op.alter_column('updated_at',
existing_type=sa.DateTime(timezone=True),
type_=postgresql.TIMESTAMP(),
existing_nullable=True)
batch_op.alter_column('created_at',
existing_type=sa.DateTime(timezone=True),
type_=postgresql.TIMESTAMP(),
existing_nullable=True)
with op.batch_alter_table('repayments', schema=None) as batch_op:
batch_op.alter_column('updated_at',
existing_type=sa.DateTime(timezone=True),
type_=postgresql.TIMESTAMP(),
existing_nullable=True)
batch_op.alter_column('created_at',
existing_type=sa.DateTime(timezone=True),
type_=postgresql.TIMESTAMP(),
existing_nullable=True)
with op.batch_alter_table('rac_checks', schema=None) as batch_op:
batch_op.alter_column('updated_at',
existing_type=sa.DateTime(timezone=True),
type_=postgresql.TIMESTAMP(),
existing_nullable=True)
batch_op.alter_column('created_at',
existing_type=sa.DateTime(timezone=True),
type_=postgresql.TIMESTAMP(),
existing_nullable=True)
with op.batch_alter_table('offers', schema=None) as batch_op:
batch_op.alter_column('updated_at',
existing_type=sa.DateTime(timezone=True),
type_=postgresql.TIMESTAMP(),
existing_nullable=True)
batch_op.alter_column('created_at',
existing_type=sa.DateTime(timezone=True),
type_=postgresql.TIMESTAMP(),
existing_nullable=True)
with op.batch_alter_table('loans', schema=None) as batch_op:
batch_op.alter_column('updated_at',
existing_type=sa.DateTime(timezone=True),
type_=postgresql.TIMESTAMP(),
existing_nullable=True)
batch_op.alter_column('created_at',
existing_type=sa.DateTime(timezone=True),
type_=postgresql.TIMESTAMP(),
existing_nullable=True)
with op.batch_alter_table('loan_repayment_schedules', schema=None) as batch_op:
batch_op.alter_column('updated_at',
existing_type=sa.DateTime(timezone=True),
type_=postgresql.TIMESTAMP(),
existing_nullable=True)
batch_op.alter_column('created_at',
existing_type=sa.DateTime(timezone=True),
type_=postgresql.TIMESTAMP(),
existing_nullable=True)
with op.batch_alter_table('loan_charges', schema=None) as batch_op:
batch_op.alter_column('updated_at',
existing_type=sa.DateTime(timezone=True),
type_=postgresql.TIMESTAMP(),
existing_nullable=True)
batch_op.alter_column('created_at',
existing_type=sa.DateTime(timezone=True),
type_=postgresql.TIMESTAMP(),
existing_nullable=True)
with op.batch_alter_table('customers', schema=None) as batch_op:
batch_op.alter_column('updated_at',
existing_type=sa.DateTime(timezone=True),
type_=postgresql.TIMESTAMP(),
existing_nullable=True)
batch_op.alter_column('created_at',
existing_type=sa.DateTime(timezone=True),
type_=postgresql.TIMESTAMP(),
existing_nullable=True)
with op.batch_alter_table('charges', schema=None) as batch_op:
batch_op.alter_column('updated_at',
existing_type=sa.DateTime(timezone=True),
type_=postgresql.TIMESTAMP(),
existing_nullable=True)
batch_op.alter_column('created_at',
existing_type=sa.DateTime(timezone=True),
type_=postgresql.TIMESTAMP(),
existing_nullable=True)
with op.batch_alter_table('accounts', schema=None) as batch_op:
batch_op.alter_column('updated_at',
existing_type=sa.DateTime(timezone=True),
type_=postgresql.TIMESTAMP(),
existing_nullable=True)
batch_op.alter_column('created_at',
existing_type=sa.DateTime(timezone=True),
type_=postgresql.TIMESTAMP(),
existing_nullable=True)
# ### end Alembic commands ###
-52
View File
@@ -1,52 +0,0 @@
"""empty message
Revision ID: 3105abd795d4
Revises: 95a52be203c4
Create Date: 2025-05-07 11:44:18.483694
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = '3105abd795d4'
down_revision = '95a52be203c4'
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
with op.batch_alter_table('rac_checks', schema=None) as batch_op:
# Step 1: Drop the default value
batch_op.alter_column('id',
server_default=None,
existing_type=sa.VARCHAR(),
existing_nullable=False
)
with op.batch_alter_table('rac_checks', schema=None) as batch_op:
# Step 2: Change the column type
batch_op.alter_column('id',
existing_type=sa.VARCHAR(),
type_=sa.Integer(),
existing_nullable=False,
autoincrement=True,
postgresql_using='id::integer'
)
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
with op.batch_alter_table('rac_checks', schema=None) as batch_op:
batch_op.alter_column('id',
existing_type=sa.Integer(),
type_=sa.VARCHAR(),
existing_nullable=False,
autoincrement=True,
existing_server_default=sa.text("''::character varying"))
# ### end Alembic commands ###
+280
View File
@@ -0,0 +1,280 @@
"""empty message
Revision ID: 33e09efd85e3
Revises:
Create Date: 2025-07-03 14:07:14.424548
"""
from alembic import op
import sqlalchemy as sa
from sqlalchemy import literal_column
from sqlalchemy import text
# revision identifiers, used by Alembic.
revision = '33e09efd85e3'
down_revision = None
branch_labels = None
depends_on = None
sequences_and_triggers = [
("transactions", "transactions_seq", "trg_transactions_id"),
("transaction_offers", "transaction_offers_seq", "trg_transaction_offers_id"),
("salaries", "salaries_seq", "trg_salaries_id"),
("repayments_data", "repayments_data_seq", "trg_repayments_data_id"),
("repayments", "repayments_seq", "trg_repayments_id"),
("rac_checks", "rac_checks_seq", "trg_rac_checks_id"),
("loans", "loans_seq", "trg_loans_id"),
(
"loan_repayment_schedules",
"loan_repayment_schedules_seq",
"trg_loan_repayment_schedules_id",
),
("loan_charges", "loan_charges_seq", "trg_loan_charges_id"),
("charges", "charges_seq", "trg_charges_id"),
]
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.create_table('accounts',
sa.Column('id', sa.String(length=50), nullable=False),
sa.Column('customer_id', sa.String(length=50), nullable=False),
sa.Column('account_type', sa.String(length=50), nullable=True),
sa.Column('status', sa.String(length=20), nullable=True),
sa.Column('lien_amount', sa.Float(), nullable=True),
sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('CURRENT_TIMESTAMP'), nullable=True),
sa.Column('updated_at', sa.DateTime(timezone=True), server_default=sa.text('CURRENT_TIMESTAMP'), nullable=True),
sa.PrimaryKeyConstraint('id')
)
op.create_table('charges',
sa.Column('id', sa.Integer(), autoincrement=True, nullable=False),
sa.Column('offer_id', sa.String(length=50), nullable=False),
sa.Column('code', sa.String(length=50), nullable=False),
sa.Column('percent', sa.Float(), nullable=True),
sa.Column('description', sa.Text(), nullable=True),
sa.Column('due', sa.Integer(), nullable=False),
sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('CURRENT_TIMESTAMP'), nullable=True),
sa.Column('updated_at', sa.DateTime(timezone=True), server_default=sa.text('CURRENT_TIMESTAMP'), nullable=True),
sa.PrimaryKeyConstraint('id')
)
op.create_table('customers',
sa.Column('id', sa.String(length=50), nullable=False),
sa.Column('msisdn', sa.String(length=20), nullable=False),
sa.Column('country_code', sa.String(length=3), nullable=False),
sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('CURRENT_TIMESTAMP'), nullable=True),
sa.Column('updated_at', sa.DateTime(timezone=True), server_default=sa.text('CURRENT_TIMESTAMP'), nullable=True),
sa.PrimaryKeyConstraint('id'),
sa.UniqueConstraint('msisdn')
)
op.create_table('loan_charges',
sa.Column('id', sa.Integer(), autoincrement=True, nullable=False),
sa.Column('loan_id', sa.Integer(), nullable=False),
sa.Column('transaction_id', sa.String(length=50), nullable=True),
sa.Column('code', sa.String(length=50), nullable=False),
sa.Column('amount', sa.Float(), nullable=True),
sa.Column('percent', sa.Float(), nullable=True),
sa.Column('description', sa.Text(), nullable=True),
sa.Column('due', sa.Integer(), nullable=False),
sa.Column('due_date', sa.DateTime(), nullable=True),
sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('CURRENT_TIMESTAMP'), nullable=True),
sa.Column('updated_at', sa.DateTime(timezone=True), server_default=sa.text('CURRENT_TIMESTAMP'), nullable=True),
sa.PrimaryKeyConstraint('id')
)
op.create_table('loan_repayment_schedules',
sa.Column('id', sa.Integer(), autoincrement=True, nullable=False),
sa.Column('loan_id', sa.Integer(), nullable=False),
sa.Column('transaction_id', sa.String(length=50), nullable=True),
sa.Column('product_id', sa.String(length=20), nullable=True),
sa.Column('installment_number', sa.Integer(), nullable=False),
sa.Column('due_date', sa.DateTime(), nullable=False),
sa.Column('installment_amount', sa.Float(), nullable=True),
sa.Column('total_repayment_amount', sa.Float(), nullable=True),
sa.Column('paid', sa.Boolean(), nullable=True),
sa.Column('paid_at', sa.DateTime(), nullable=True),
sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('CURRENT_TIMESTAMP'), nullable=True),
sa.Column('updated_at', sa.DateTime(timezone=True), server_default=sa.text('CURRENT_TIMESTAMP'), nullable=True),
sa.PrimaryKeyConstraint('id')
)
op.create_table('loans',
sa.Column('id', sa.Integer(), autoincrement=True, nullable=False),
sa.Column('customer_id', sa.String(length=50), nullable=False),
sa.Column('transaction_id', sa.String(length=50), nullable=True),
sa.Column('original_transaction', sa.String(length=50), nullable=True),
sa.Column('account_id', sa.String(length=50), nullable=False),
sa.Column('offer_id', sa.String(length=20), nullable=False),
sa.Column('product_id', sa.String(length=20), nullable=True),
sa.Column('collection_type', sa.String(length=20), nullable=True),
sa.Column('current_loan_amount', sa.Float(), nullable=True),
sa.Column('initial_loan_amount', sa.Float(), nullable=False),
sa.Column('default_penalty_fee', sa.Float(), nullable=True),
sa.Column('continuous_fee', sa.Float(), nullable=True),
sa.Column('upfront_fee', sa.Float(), nullable=True),
sa.Column('repayment_amount', sa.Float(), nullable=True),
sa.Column('installment_amount', sa.Float(), nullable=True),
sa.Column('status', sa.String(length=20), nullable=True),
sa.Column('tenor', sa.Integer(), nullable=True),
sa.Column('due_date', sa.DateTime(), nullable=True),
sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('CURRENT_TIMESTAMP'), nullable=True),
sa.Column('updated_at', sa.DateTime(timezone=True), server_default=sa.text('CURRENT_TIMESTAMP'), nullable=True),
sa.Column('eligible_amount', sa.Float(), nullable=True),
sa.Column('disburse_date', sa.DateTime(), nullable=True),
sa.Column('disburse_verify', sa.DateTime(), nullable=True),
sa.Column('reference', sa.String(length=50), nullable=True),
sa.Column('disburse_result', sa.String(length=10), nullable=True),
sa.Column('disburse_description', sa.String(length=100), nullable=True),
sa.Column('verify_result', sa.String(length=10), nullable=True),
sa.Column('verify_description', sa.String(length=100), nullable=True),
sa.PrimaryKeyConstraint('id')
)
op.create_table('offers',
sa.Column('id', sa.String(length=50), nullable=False),
sa.Column('product_id', sa.String(length=50), nullable=False),
sa.Column('min_amount', sa.Float(), nullable=False),
sa.Column('max_amount', sa.Float(), nullable=False),
sa.Column('tenor', sa.Integer(), nullable=False),
sa.Column('schedule', sa.Integer(), nullable=True),
sa.Column('interest_rate', sa.Float(), nullable=True),
sa.Column('management_rate', sa.Float(), nullable=True),
sa.Column('insurance_rate', sa.Float(), nullable=True),
sa.Column('vat_rate', sa.Float(), nullable=True),
sa.Column('list_order', sa.Integer(), nullable=True),
sa.Column('max_daily_loans', sa.Integer(), nullable=True),
sa.Column('max_active_loans', sa.Integer(), nullable=True),
sa.Column('max_life_loans', sa.Integer(), nullable=True),
sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('CURRENT_TIMESTAMP'), nullable=True),
sa.Column('updated_at', sa.DateTime(timezone=True), server_default=sa.text('CURRENT_TIMESTAMP'), nullable=True),
sa.PrimaryKeyConstraint('id')
)
op.create_table('rac_checks',
sa.Column('id', sa.Integer(), autoincrement=True, nullable=False),
sa.Column('transaction_id', sa.String(length=50), nullable=False),
sa.Column('customer_id', sa.String(length=50), nullable=False),
sa.Column('account_id', sa.String(length=50), nullable=False),
sa.Column('rac_response', sa.Text(), nullable=False),
sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('CURRENT_TIMESTAMP'), nullable=True),
sa.Column('updated_at', sa.DateTime(timezone=True), server_default=sa.text('CURRENT_TIMESTAMP'), nullable=True),
sa.PrimaryKeyConstraint('id')
)
op.create_table('repayments',
sa.Column('id', sa.Integer(), autoincrement=True, nullable=False),
sa.Column('loan_id', sa.String(length=50), nullable=False),
sa.Column('customer_id', sa.String(length=50), nullable=False),
sa.Column('product_id', sa.String(length=20), nullable=True),
sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('CURRENT_TIMESTAMP'), nullable=True),
sa.Column('updated_at', sa.DateTime(timezone=True), server_default=sa.text('CURRENT_TIMESTAMP'), nullable=True),
sa.Column('transaction_id', sa.String(length=50), nullable=True),
sa.Column('repay_date', sa.DateTime(), nullable=True),
sa.Column('repay_result', sa.String(length=10), nullable=True),
sa.Column('repay_description', sa.String(length=100), nullable=True),
sa.Column('verify_date', sa.DateTime(), nullable=True),
sa.Column('verify_result', sa.String(length=10), nullable=True),
sa.Column('verify_description', sa.String(length=100), nullable=True),
sa.Column('initiated_by', sa.String(length=50), nullable=True),
sa.Column('salary_amount', sa.Float(), nullable=True),
sa.PrimaryKeyConstraint('id')
)
op.create_table('repayments_data',
sa.Column('id', sa.Integer(), autoincrement=True, nullable=False),
sa.Column('transaction_id', sa.String(length=50), nullable=False),
sa.Column('fbn_transaction_id', sa.String(length=50), nullable=True),
sa.Column('customer_id', sa.String(length=50), nullable=True),
sa.Column('account_id', sa.String(length=50), nullable=True),
sa.Column('repayment_amount', sa.Float(), nullable=True),
sa.Column('amount_collected', sa.Float(), nullable=True),
sa.Column('added_date', sa.DateTime(timezone=True), nullable=False),
sa.Column('response_code', sa.String(length=10), nullable=True),
sa.Column('response_descr', sa.String(length=255), nullable=True),
sa.Column('balance', sa.Float(), nullable=True),
sa.PrimaryKeyConstraint('id')
)
op.create_table('salaries',
sa.Column('id', sa.Integer(), autoincrement=True, nullable=False),
sa.Column('customer_id', sa.String(length=50), nullable=False),
sa.Column('account_id', sa.String(length=50), nullable=True),
sa.Column('status', sa.String(length=20), nullable=True),
sa.Column('amount', sa.Float(), nullable=False),
sa.Column('salary_date', sa.DateTime(), server_default=sa.text('CURRENT_TIMESTAMP'), nullable=True),
sa.Column('created_at', sa.DateTime(), server_default=sa.text('CURRENT_TIMESTAMP'), nullable=True),
sa.Column('updated_at', sa.DateTime(), server_default=sa.text('CURRENT_TIMESTAMP'), nullable=True),
sa.PrimaryKeyConstraint('id')
)
op.create_table('transaction_offers',
sa.Column('id', sa.Integer(), autoincrement=True, nullable=False),
sa.Column('customer_id', sa.String(length=50), nullable=False),
sa.Column('transaction_id', sa.String(length=50), nullable=False),
sa.Column('original_transaction', sa.String(length=50), nullable=True),
sa.Column('offer_id', sa.String(length=20), nullable=False),
sa.Column('product_id', sa.String(length=20), nullable=True),
sa.Column('min_amount', sa.Float(), nullable=False),
sa.Column('max_amount', sa.Float(), nullable=False),
sa.Column('eligible_amount', sa.Float(), nullable=True),
sa.Column('tenor', sa.Integer(), nullable=True),
sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('CURRENT_TIMESTAMP'), nullable=True),
sa.Column('updated_at', sa.DateTime(timezone=True), server_default=sa.text('CURRENT_TIMESTAMP'), nullable=True),
sa.PrimaryKeyConstraint('id')
)
op.create_table('transactions',
sa.Column('id', sa.Integer(), autoincrement=True, nullable=False),
sa.Column('transaction_id', sa.String(length=50), nullable=False),
sa.Column('account_id', sa.String(length=50), nullable=True),
sa.Column('customer_id', sa.String(length=50), nullable=True),
sa.Column('type', sa.String(length=50), nullable=False),
sa.Column('channel', sa.String(length=50), nullable=False),
sa.Column('phone_number', sa.String(length=50), nullable=True),
sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('CURRENT_TIMESTAMP'), nullable=True),
sa.Column('updated_at', sa.DateTime(timezone=True), server_default=sa.text('CURRENT_TIMESTAMP'), nullable=True),
sa.PrimaryKeyConstraint('id')
)
for table, seq, trg in sequences_and_triggers:
op.execute(
text(
f"""
BEGIN
EXECUTE IMMEDIATE 'CREATE SEQUENCE {seq} START WITH 1 INCREMENT BY 1 NOCACHE NOCYCLE';
EXCEPTION
WHEN OTHERS THEN
IF SQLCODE != -955 THEN RAISE; END IF;
END;
"""
)
)
op.execute(
text(
f"""
CREATE OR REPLACE TRIGGER {trg}
BEFORE INSERT ON {table}
FOR EACH ROW
BEGIN
IF ||':'||'NEW.id IS NULL THEN
SELECT {seq}.NEXTVAL INTO '||':'||'NEW.id FROM dual;
END IF;
END;
"""
)
)
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.drop_table('transactions')
op.drop_table('transaction_offers')
op.drop_table('salaries')
op.drop_table('repayments_data')
op.drop_table('repayments')
op.drop_table('rac_checks')
op.drop_table('offers')
op.drop_table('loans')
op.drop_table('loan_repayment_schedules')
op.drop_table('loan_charges')
op.drop_table('customers')
op.drop_table('charges')
op.drop_table('accounts')
# ### end Alembic commands ###
for table, seq, trg in sequences_and_triggers:
op.execute(text(f"DROP TRIGGER {trg}"))
op.execute(text(f"DROP SEQUENCE {seq}"))
@@ -1,32 +0,0 @@
"""Migration for mloan table
Revision ID: 38acee611d55
Revises: f1e83a993034
Create Date: 2025-04-30 09:55:30.552838
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = '38acee611d55'
down_revision = 'f1e83a993034'
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
with op.batch_alter_table('loans', schema=None) as batch_op:
batch_op.add_column(sa.Column('tenor', sa.Integer(), nullable=True))
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
with op.batch_alter_table('loans', schema=None) as batch_op:
batch_op.drop_column('tenor')
# ### end Alembic commands ###
@@ -1,34 +0,0 @@
"""Migration on Sat May 10 12:54:52 UTC 2025
Revision ID: 565bc3d0ba6e
Revises: 173ea45db189
Create Date: 2025-05-10 12:54:56.683215
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = '565bc3d0ba6e'
down_revision = '173ea45db189'
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
with op.batch_alter_table('loans', schema=None) as batch_op:
batch_op.add_column(sa.Column('disburse_date', sa.DateTime(), nullable=True))
batch_op.add_column(sa.Column('disburse_verify', sa.DateTime(), nullable=True))
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
with op.batch_alter_table('loans', schema=None) as batch_op:
batch_op.drop_column('disburse_verify')
batch_op.drop_column('disburse_date')
# ### end Alembic commands ###
@@ -1,32 +0,0 @@
"""Migration on Fri Apr 11 14:15:19 UTC 2025
Revision ID: 610b7e9d15a6
Revises: 9bb0367eb486
Create Date: 2025-04-11 14:16:12.533227
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = '610b7e9d15a6'
down_revision = '9bb0367eb486'
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
with op.batch_alter_table('loans', schema=None) as batch_op:
batch_op.add_column(sa.Column('transaction_id', sa.String(length=50), nullable=True))
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
with op.batch_alter_table('loans', schema=None) as batch_op:
batch_op.drop_column('transaction_id')
# ### end Alembic commands ###
@@ -1,32 +0,0 @@
"""Migration on Mon Apr 14 15:15:05 UTC 2025
Revision ID: 783a023a477f
Revises: f6cd1bfc8832
Create Date: 2025-04-14 15:15:36.991148
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = '783a023a477f'
down_revision = 'f6cd1bfc8832'
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
with op.batch_alter_table('transactions', schema=None) as batch_op:
batch_op.add_column(sa.Column('customer_id', sa.String(length=50), nullable=True))
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
with op.batch_alter_table('transactions', schema=None) as batch_op:
batch_op.drop_column('customer_id')
# ### end Alembic commands ###
-41
View File
@@ -1,41 +0,0 @@
"""empty message
Revision ID: 86e701febdda
Revises: eb99c7fb9e09
Create Date: 2025-04-29 07:59:33.305967
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = '86e701febdda'
down_revision = 'eb99c7fb9e09'
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.create_table('transaction_offers',
sa.Column('id', sa.Integer(), autoincrement=True, nullable=False),
sa.Column('customer_id', sa.String(length=50), nullable=False),
sa.Column('transaction_id', sa.String(length=50), nullable=False),
sa.Column('offer_id', sa.String(length=20), nullable=False),
sa.Column('product_id', sa.String(length=20), nullable=True),
sa.Column('min_amount', sa.Float(), nullable=False),
sa.Column('max_amount', sa.Float(), nullable=False),
sa.Column('eligible_amount', sa.Float(), nullable=True),
sa.Column('tenor', sa.Integer(), nullable=True),
sa.Column('created_at', sa.DateTime(), nullable=True),
sa.Column('updated_at', sa.DateTime(), nullable=True),
sa.PrimaryKeyConstraint('id')
)
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.drop_table('transaction_offers')
# ### end Alembic commands ###
@@ -1,38 +0,0 @@
"""Migration on Sat Apr 26 12:50:46 UTC 2025
Revision ID: 89759cebb9c6
Revises: 2a45dd99c9cb
Create Date: 2025-04-26 12:50:49.771355
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = '89759cebb9c6'
down_revision = '2a45dd99c9cb'
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
with op.batch_alter_table('offers', schema=None) as batch_op:
batch_op.add_column(sa.Column('interest_rate', sa.Float(), nullable=True))
batch_op.add_column(sa.Column('management_rate', sa.Float(), nullable=True))
batch_op.add_column(sa.Column('insurance_rate', sa.Float(), nullable=True))
batch_op.add_column(sa.Column('vat_rate', sa.Float(), nullable=True))
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
with op.batch_alter_table('offers', schema=None) as batch_op:
batch_op.drop_column('vat_rate')
batch_op.drop_column('insurance_rate')
batch_op.drop_column('management_rate')
batch_op.drop_column('interest_rate')
# ### end Alembic commands ###
@@ -1,32 +0,0 @@
"""Migration on Sat May 3 21:53:29 UTC 2025
Revision ID: 95a52be203c4
Revises: 38acee611d55
Create Date: 2025-05-03 21:53:32.154029
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = '95a52be203c4'
down_revision = '38acee611d55'
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
with op.batch_alter_table('loans', schema=None) as batch_op:
batch_op.add_column(sa.Column('eligible_amount', sa.Float(), nullable=True))
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
with op.batch_alter_table('loans', schema=None) as batch_op:
batch_op.drop_column('eligible_amount')
# ### end Alembic commands ###
@@ -1,40 +0,0 @@
"""Migration on Fri Apr 11 12:48:01 UTC 2025
Revision ID: 9bb0367eb486
Revises: fd447d78b161
Create Date: 2025-04-11 12:48:36.145311
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = '9bb0367eb486'
down_revision = 'fd447d78b161'
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
with op.batch_alter_table('loans', schema=None) as batch_op:
batch_op.add_column(sa.Column('product_id', sa.String(length=20), nullable=True))
batch_op.add_column(sa.Column('current_loan_amount', sa.Float(), nullable=True))
batch_op.add_column(sa.Column('default_penalty_fee', sa.Float(), nullable=True))
batch_op.add_column(sa.Column('continuous_fee', sa.Float(), nullable=True))
batch_op.add_column(sa.Column('due_date', sa.DateTime(), nullable=True))
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
with op.batch_alter_table('loans', schema=None) as batch_op:
batch_op.drop_column('due_date')
batch_op.drop_column('continuous_fee')
batch_op.drop_column('default_penalty_fee')
batch_op.drop_column('current_loan_amount')
batch_op.drop_column('product_id')
# ### end Alembic commands ###
@@ -1,57 +0,0 @@
"""Migration on Wed Apr 16 17:42:49 UTC 2025
Revision ID: a4847b997191
Revises: 783a023a477f
Create Date: 2025-04-16 17:43:22.509659
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = 'a4847b997191'
down_revision = '783a023a477f'
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.create_table('loan_charges',
sa.Column('id', sa.Integer(), autoincrement=True, nullable=False),
sa.Column('loan_id', sa.Integer(), nullable=False),
sa.Column('transaction_id', sa.String(length=50), nullable=True),
sa.Column('code', sa.String(length=50), nullable=False),
sa.Column('amount', sa.Float(), nullable=True),
sa.Column('percent', sa.Float(), nullable=True),
sa.Column('description', sa.Text(), nullable=True),
sa.Column('due', sa.Integer(), nullable=False),
sa.Column('created_at', sa.DateTime(), nullable=True),
sa.Column('updated_at', sa.DateTime(), nullable=True),
sa.PrimaryKeyConstraint('id')
)
op.create_table('offers',
sa.Column('id', sa.String(), nullable=False),
sa.Column('product_id', sa.String(), nullable=False),
sa.Column('min_amount', sa.Float(), nullable=False),
sa.Column('max_amount', sa.Float(), nullable=False),
sa.Column('tenor', sa.Integer(), nullable=False),
sa.Column('created_at', sa.DateTime(), nullable=True),
sa.Column('updated_at', sa.DateTime(), nullable=True),
sa.PrimaryKeyConstraint('id')
)
with op.batch_alter_table('loans', schema=None) as batch_op:
batch_op.add_column(sa.Column('product_id', sa.String(length=20), nullable=True))
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
with op.batch_alter_table('loans', schema=None) as batch_op:
batch_op.drop_column('product_id')
op.drop_table('offers')
op.drop_table('loan_charges')
# ### end Alembic commands ###
@@ -1,86 +0,0 @@
"""Migration on Thu Apr 10 16:21:45 UTC 2025
Revision ID: b8f6fd76ead8
Revises:
Create Date: 2025-04-10 16:22:15.946157
"""
from alembic import op
import sqlalchemy as sa
from sqlalchemy.dialects import postgresql
# revision identifiers, used by Alembic.
revision = 'b8f6fd76ead8'
down_revision = None
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.create_table('repayments',
sa.Column('id', sa.Integer(), autoincrement=True, nullable=False),
sa.Column('loan_id', sa.String(length=50), nullable=False),
sa.Column('customer_id', sa.String(length=50), nullable=False),
sa.Column('product_id', sa.String(length=20), nullable=True),
sa.Column('created_at', sa.DateTime(), nullable=True),
sa.Column('updated_at', sa.DateTime(), nullable=True),
sa.PrimaryKeyConstraint('id')
)
with op.batch_alter_table('loans', schema=None) as batch_op:
batch_op.alter_column('id',
existing_type=sa.VARCHAR(length=50),
type_=sa.Integer(),
existing_nullable=False,
autoincrement=True,
existing_server_default=sa.text("nextval('loan_id_seq'::regclass)"))
with op.batch_alter_table('transactions', schema=None) as batch_op:
batch_op.alter_column('channel',
existing_type=sa.VARCHAR(length=8),
type_=sa.String(length=50),
existing_nullable=False)
batch_op.alter_column('created_at',
existing_type=postgresql.TIMESTAMP(timezone=True),
type_=sa.DateTime(),
existing_nullable=True,
existing_server_default=sa.text('now()'))
batch_op.alter_column('updated_at',
existing_type=postgresql.TIMESTAMP(timezone=True),
type_=sa.DateTime(),
existing_nullable=True,
existing_server_default=sa.text('now()'))
batch_op.drop_constraint('transactions_id_key', type_='unique')
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
with op.batch_alter_table('transactions', schema=None) as batch_op:
batch_op.create_unique_constraint('transactions_id_key', ['id'])
batch_op.alter_column('updated_at',
existing_type=sa.DateTime(),
type_=postgresql.TIMESTAMP(timezone=True),
existing_nullable=True,
existing_server_default=sa.text('now()'))
batch_op.alter_column('created_at',
existing_type=sa.DateTime(),
type_=postgresql.TIMESTAMP(timezone=True),
existing_nullable=True,
existing_server_default=sa.text('now()'))
batch_op.alter_column('channel',
existing_type=sa.String(length=50),
type_=sa.VARCHAR(length=8),
existing_nullable=False)
with op.batch_alter_table('loans', schema=None) as batch_op:
batch_op.alter_column('id',
existing_type=sa.Integer(),
type_=sa.VARCHAR(length=50),
existing_nullable=False,
autoincrement=True,
existing_server_default=sa.text("nextval('loan_id_seq'::regclass)"))
op.drop_table('repayments')
# ### end Alembic commands ###
@@ -1,38 +0,0 @@
"""Migration on Thu Apr 17 14:15:36 UTC 2025
Revision ID: de9ad96ba34e
Revises: ec8d97f9b584
Create Date: 2025-04-17 14:16:16.537466
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = 'de9ad96ba34e'
down_revision = 'ec8d97f9b584'
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.create_table('charges',
sa.Column('id', sa.Integer(), autoincrement=True, nullable=False),
sa.Column('offer_id', sa.String(length=50), nullable=False),
sa.Column('code', sa.String(length=50), nullable=False),
sa.Column('percent', sa.Float(), nullable=True),
sa.Column('description', sa.Text(), nullable=True),
sa.Column('due', sa.Integer(), nullable=False),
sa.Column('created_at', sa.DateTime(), nullable=True),
sa.Column('updated_at', sa.DateTime(), nullable=True),
sa.PrimaryKeyConstraint('id')
)
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.drop_table('charges')
# ### end Alembic commands ###
@@ -1,32 +0,0 @@
"""Migration on Sat Apr 26 19:02:17 UTC 2025
Revision ID: eb99c7fb9e09
Revises: 89759cebb9c6
Create Date: 2025-04-26 19:02:20.443678
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = 'eb99c7fb9e09'
down_revision = '89759cebb9c6'
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
with op.batch_alter_table('loans', schema=None) as batch_op:
batch_op.add_column(sa.Column('original_transaction', sa.String(length=50), nullable=True))
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
with op.batch_alter_table('loans', schema=None) as batch_op:
batch_op.drop_column('original_transaction')
# ### end Alembic commands ###
@@ -1,34 +0,0 @@
"""Migration on Thu Apr 17 10:40:05 UTC 2025
Revision ID: ec8d97f9b584
Revises: 287ecb02d3d7
Create Date: 2025-04-17 10:40:34.751272
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = 'ec8d97f9b584'
down_revision = '287ecb02d3d7'
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
with op.batch_alter_table('loan_charges', schema=None) as batch_op:
batch_op.add_column(sa.Column('transaction_id', sa.String(length=50), nullable=True))
batch_op.add_column(sa.Column('due_date', sa.DateTime(), nullable=True))
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
with op.batch_alter_table('loan_charges', schema=None) as batch_op:
batch_op.drop_column('due_date')
batch_op.drop_column('transaction_id')
# ### end Alembic commands ###
@@ -1,32 +0,0 @@
"""Migration on Tue Apr 29 20:43:35 UTC 2025
Revision ID: f1e83a993034
Revises: 86e701febdda
Create Date: 2025-04-29 20:43:38.595543
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = 'f1e83a993034'
down_revision = '86e701febdda'
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
with op.batch_alter_table('loan_repayment_schedules', schema=None) as batch_op:
batch_op.add_column(sa.Column('transaction_id', sa.String(length=50), nullable=True))
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
with op.batch_alter_table('loan_repayment_schedules', schema=None) as batch_op:
batch_op.drop_column('transaction_id')
# ### end Alembic commands ###
@@ -1,34 +0,0 @@
"""Migration on Fri Apr 11 14:34:36 UTC 2025
Revision ID: f6cd1bfc8832
Revises: 610b7e9d15a6
Create Date: 2025-04-11 14:35:07.093967
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = 'f6cd1bfc8832'
down_revision = '610b7e9d15a6'
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
with op.batch_alter_table('loans', schema=None) as batch_op:
batch_op.add_column(sa.Column('collection_type', sa.String(length=20), nullable=True))
batch_op.drop_column('product_id')
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
with op.batch_alter_table('loans', schema=None) as batch_op:
batch_op.add_column(sa.Column('product_id', sa.VARCHAR(length=20), autoincrement=False, nullable=True))
batch_op.drop_column('collection_type')
# ### end Alembic commands ###
@@ -1,38 +0,0 @@
"""Migration on Fri Apr 11 12:02:45 UTC 2025
Revision ID: fd447d78b161
Revises: 1340e7e578b9
Create Date: 2025-04-11 12:03:28.346671
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = 'fd447d78b161'
down_revision = '1340e7e578b9'
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
with op.batch_alter_table('transactions', schema=None) as batch_op:
batch_op.alter_column('account_id',
existing_type=sa.VARCHAR(length=50),
nullable=True)
batch_op.drop_column('ref_model')
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
with op.batch_alter_table('transactions', schema=None) as batch_op:
batch_op.add_column(sa.Column('ref_model', sa.VARCHAR(length=50), autoincrement=False, nullable=True))
batch_op.alter_column('account_id',
existing_type=sa.VARCHAR(length=50),
nullable=False)
# ### end Alembic commands ###
+4
View File
@@ -6,6 +6,7 @@ flask-sqlalchemy
flask-migrate
psycopg2-binary
alembic
oracledb
# Schema for validations
Flask-Marshmallow==0.15.0
@@ -39,3 +40,6 @@ confluent-kafka==1.9.2
python-dateutil
Flask-Mail==0.10.0
pandas==2.1.3
openpyxl==3.1.5