Compare commits

...

35 Commits

Author SHA1 Message Date
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
ameye c95e2786b5 Merge branch 'loan_reference' of DigiFi/digifi-BankToProductCore into master 2025-05-27 11:14:42 +00:00
ameye 29b2697b0e Merge branch 'loan_reference' of DigiFi/digifi-BankToProductCore into master 2025-05-26 12:10:35 +00:00
20 changed files with 503 additions and 76 deletions
+1 -31
View File
@@ -17,44 +17,14 @@ class SimbrellaIntegration:
url = f"{SimbrellaIntegration.BASE_URL}/{SimbrellaIntegration.ENDPOINT_RAC_CHECKS}"
logger.info(f"Contacting Rack Checks EndPoint: {str(url)}", exc_info=True)
# {
# "transactionId": "T001",
# "fbnTransactionId": "Tr201712RK9232P115",
# "customerId": "CN621868",
# "accountId": "2017821799",
# "channel": "USSD",
# "countryCode": "NG"
# }
#
payload_old = {
"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",
],
}
payload = {
"customerId": customer_id,
"accountId": account_id,
"transactionId": str(transaction_id),
"fbnTransactionId": f"FBN{transaction_id}",
"fbnTransactionId": str(transaction_id),
"countryCode": "NG",
"channel": "USSD"
}
# logger.info(f"This is PayLoad: {str(payload)}", exc_info=True)
headers = {
"Content-Type": "application/json",
+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
+3 -1
View File
@@ -48,12 +48,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
+20 -10
View File
@@ -1,6 +1,5 @@
from flask import session, jsonify
from app.models.loan import Loan
from app.models.transaction_offers import TransactionOffer
from app.utils.logger import logger
from app.api.services.base_service import BaseService
from app.api.schemas.eligibility_check import EligibilityCheckSchema
@@ -70,20 +69,31 @@ class EligibilityCheckService(BaseService):
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.")
rack_checks_response = response['racResponse']
# -----------------TIME FOR ANALYSIS TO REGISTER OFFER ----------------------
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 ----------------------
# eligible_offers = []
try:
eligible_offers = OfferAnalysis.decide_offer(
@@ -176,10 +186,10 @@ class EligibilityCheckService(BaseService):
logger.error(f"Offer not found for offer_id: {offer_id} (customer_id: {customer_id})")
return False
daily_count = TransactionOffer.get_daily_loan_count(customer_id, offer_id)
daily_count = Loan.get_daily_loan_count(customer_id, offer.product_id)
logger.error(f"daily_count: {daily_count}, Max: {offer.max_daily_loans}")
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
+1
View File
@@ -55,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,
+108 -8
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:
@@ -33,8 +41,86 @@ class OfferAnalysis:
return transaction_offer, offer, eligible_amount, original_transaction
@staticmethod
def _analyze_rack_checks(rack_response):
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,
@@ -48,6 +134,17 @@ class OfferAnalysis:
# },
#
'''
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
@@ -56,16 +153,13 @@ class OfferAnalysis:
# if we have active offers - we have to feed off it
logger.info(f"**RACK ANALYSIS** {customer_id}")
# Analyze Rack Checks
# _analyze_rack_checks(rack_checks_response) --> We need detail analysis
# 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}")
@@ -117,12 +211,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,
+5 -3
View File
@@ -29,6 +29,7 @@ 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')
@@ -37,9 +38,9 @@ class RepaymentService(BaseService):
# 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)
@@ -61,7 +62,8 @@ class RepaymentService(BaseService):
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
+12 -3
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,12 +58,20 @@ 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 < offer.min_amount:
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 > offer.max_amount:
return ResponseHelper.error(result_description="The amount is greater than the maximum 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.")
+35 -1
View File
@@ -34,7 +34,7 @@ class Config:
# SIMBRELLA_ENDPOINT_RAC_CHECKS = os.getenv("SIMBRELLA_ENDPOINT_RAC_CHECKS", "RACCheck")
VALID_APP_ID = os.getenv("SIMBRELLA_APP_ID", "app1")
VALID_API_KEY = os.getenv("SIMBRELLA_API_KEY", "testtest-api-key-12345")
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")
@@ -48,4 +48,38 @@ class Config:
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"
]
settings = Config()
+3 -1
View File
@@ -9,6 +9,8 @@ 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
__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']
+24 -1
View File
@@ -1,4 +1,4 @@
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
@@ -45,6 +45,10 @@ class Loan(db.Model):
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",
@@ -211,6 +215,25 @@ 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.
+27
View File
@@ -21,6 +21,14 @@ class Repayment(db.Model):
created_at = db.Column(db.DateTime(timezone=True), server_default=func.now())
updated_at = db.Column(db.DateTime(timezone=True), server_default=func.now(), onupdate=func.now())
transaction_id = db.Column(db.String(50), nullable=True)
repay_date = db.Column(db.DateTime, default=datetime.now(timezone.utc))
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_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, transaction_id):
@@ -45,6 +53,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_type = db.Column(db.String(50))
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_type": self.account_type,
"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}>'
+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), server_default=func.now(), onupdate=func.now())
def __repr__(self):
return f'<Transaction {self.id}>'
+7 -17
View File
@@ -76,23 +76,6 @@ class TransactionOffer(db.Model):
"""
return cls.query.filter_by(customer_id=customer_id).count()
@classmethod
def get_daily_loan_count(cls, customer_id, offer_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,
offer_id=offer_id
).filter(
cls.created_at >= start_of_day,
cls.created_at < end_of_day
).count()
@classmethod
def get_latest_transaction_offer(cls, customer_id):
@@ -102,6 +85,13 @@ class TransactionOffer(db.Model):
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)
@@ -0,0 +1,35 @@
"""Migration on Tue Jun 10 08:41:00 WAT 2025
Revision ID: 0acd553309a1
Revises: 45790fd659fb
Create Date: 2025-06-10 08:41:45.222513
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = '0acd553309a1'
down_revision = '45790fd659fb'
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
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('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.PrimaryKeyConstraint('id')
)
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.drop_table('repayments_data')
# ### end Alembic commands ###
+52
View File
@@ -0,0 +1,52 @@
"""empty message
Revision ID: 1696ee63c28a
Revises: b54422fb31e0
Create Date: 2025-06-18 12:28:23.942143
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = '1696ee63c28a'
down_revision = 'b54422fb31e0'
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
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_type', 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('now()'), nullable=True),
sa.Column('created_at', sa.DateTime(), server_default=sa.text('now()'), nullable=True),
sa.Column('updated_at', sa.DateTime(), server_default=sa.text('now()'), nullable=True),
sa.PrimaryKeyConstraint('id')
)
with op.batch_alter_table('repayments', schema=None) as batch_op:
batch_op.add_column(sa.Column('initiated_by', sa.String(length=50), nullable=True))
batch_op.add_column(sa.Column('salary_amount', sa.Float(), nullable=True))
with op.batch_alter_table('repayments_data', schema=None) as batch_op:
batch_op.add_column(sa.Column('balance', sa.Float(), nullable=True))
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
with op.batch_alter_table('repayments_data', schema=None) as batch_op:
batch_op.drop_column('balance')
with op.batch_alter_table('repayments', schema=None) as batch_op:
batch_op.drop_column('salary_amount')
batch_op.drop_column('initiated_by')
op.drop_table('salaries')
# ### end Alembic commands ###
+54
View File
@@ -0,0 +1,54 @@
"""empty message
Revision ID: 45790fd659fb
Revises: b3a5e10bc77e
Create Date: 2025-06-04 12:37:48.180736
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = '45790fd659fb'
down_revision = 'b3a5e10bc77e'
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_result', sa.String(length=10), nullable=True))
batch_op.add_column(sa.Column('disburse_description', sa.String(length=100), nullable=True))
batch_op.add_column(sa.Column('verify_result', sa.String(length=10), nullable=True))
batch_op.add_column(sa.Column('verify_description', sa.String(length=100), nullable=True))
with op.batch_alter_table('repayments', schema=None) as batch_op:
batch_op.add_column(sa.Column('repay_date', sa.DateTime(), nullable=True))
batch_op.add_column(sa.Column('repay_result', sa.String(length=10), nullable=True))
batch_op.add_column(sa.Column('repay_description', sa.String(length=100), nullable=True))
batch_op.add_column(sa.Column('verify_date', sa.DateTime(), nullable=True))
batch_op.add_column(sa.Column('verify_result', sa.String(length=10), nullable=True))
batch_op.add_column(sa.Column('verify_description', sa.String(length=100), nullable=True))
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
with op.batch_alter_table('repayments', schema=None) as batch_op:
batch_op.drop_column('verify_description')
batch_op.drop_column('verify_result')
batch_op.drop_column('verify_date')
batch_op.drop_column('repay_description')
batch_op.drop_column('repay_result')
batch_op.drop_column('repay_date')
with op.batch_alter_table('loans', schema=None) as batch_op:
batch_op.drop_column('verify_description')
batch_op.drop_column('verify_result')
batch_op.drop_column('disburse_description')
batch_op.drop_column('disburse_result')
# ### end Alembic commands ###
+46
View File
@@ -0,0 +1,46 @@
"""empty message
Revision ID: b54422fb31e0
Revises: 0acd553309a1
Create Date: 2025-06-16 12:24:09.159498
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = 'b54422fb31e0'
down_revision = '0acd553309a1'
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
with op.batch_alter_table('repayments_data', schema=None) as batch_op:
batch_op.add_column(sa.Column('fbn_transaction_id', sa.String(length=50), nullable=True))
batch_op.add_column(sa.Column('customer_id', sa.String(length=50), nullable=True))
batch_op.add_column(sa.Column('account_id', sa.String(length=50), nullable=True))
batch_op.add_column(sa.Column('repayment_amount', sa.Float(), nullable=True))
batch_op.add_column(sa.Column('amount_collected', sa.Float(), nullable=True))
with op.batch_alter_table('transactions', schema=None) as batch_op:
batch_op.add_column(sa.Column('phone_number', 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('phone_number')
with op.batch_alter_table('repayments_data', schema=None) as batch_op:
batch_op.drop_column('amount_collected')
batch_op.drop_column('repayment_amount')
batch_op.drop_column('account_id')
batch_op.drop_column('customer_id')
batch_op.drop_column('fbn_transaction_id')
# ### end Alembic commands ###