Compare commits
80 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 6d743ea09b | |||
| 4718c9c50b | |||
| feb97c3fa8 | |||
| 4bcaa3d13d | |||
| d6faa14b54 | |||
| 332c344efa | |||
| e377858c47 | |||
| ed64d2c97c | |||
| bbdb7214d1 | |||
| e9c50f75b1 | |||
| c330c3f0e7 | |||
| 976fb14614 | |||
| 334cb0f2d6 | |||
| 40158b1c54 | |||
| b7ae0e6baa | |||
| 89b621b9a8 | |||
| cc3cd5b72b | |||
| f573d5e643 | |||
| 09b57d81a2 | |||
| 6f8e269a50 | |||
| 4435ca2776 | |||
| d851222024 | |||
| 52ab33f260 | |||
| af7e0f8624 | |||
| c8ab2cd6ba | |||
| 8ac22fa95f | |||
| 57207faf6f | |||
| 9a90609d33 | |||
| 50ca27abfe | |||
| 74066bae56 | |||
| 4c4ef909c2 | |||
| fdd7c58fab | |||
| 4bd163fb31 | |||
| d77181f627 | |||
| 4a236fdd2f | |||
| cae7ffd772 | |||
| 4f92f2a1a0 | |||
| 03adb266bb | |||
| d28bf95c97 | |||
| bd6edf52e1 | |||
| b1260895e0 | |||
| 2addf25a67 | |||
| 9dc431e66d | |||
| 9dae2d951c | |||
| a1d44e0e23 | |||
| d9f972a425 | |||
| 8aa2c86ea2 | |||
| 9c42332a83 | |||
| 92eadbfa16 | |||
| 0fbdebceb3 | |||
| 488a1b4bdd | |||
| cdc74d05c4 | |||
| 1b92ede296 | |||
| 7de4e3651f | |||
| 5f9b1f4cb8 | |||
| ed95865834 | |||
| 6973630845 | |||
| 5d37ba30fb | |||
| e8044d8fed | |||
| cf0502459b | |||
| 851422c335 | |||
| ddbabcaca9 | |||
| c216c55928 | |||
| 0995f08aea | |||
| e034c0ff9d | |||
| 4d4e4fcd3e | |||
| 1cce111d1f | |||
| b9b7988877 | |||
| 841393c470 | |||
| bbb903b27c | |||
| c895cc36e0 | |||
| 67c6d909f8 | |||
| e08dfe9894 | |||
| 7d691db7a5 | |||
| 4b92c33d5a | |||
| 8cfa957cc0 | |||
| 5768b537b1 | |||
| bc894c7856 | |||
| 829bd976b2 | |||
| e04f54bf83 |
@@ -0,0 +1,35 @@
|
||||
# Environment Variables
|
||||
BASIC_AUTH_USERNAME=user
|
||||
BASIC_AUTH_PASSWORD=password
|
||||
|
||||
#swagger Configuration
|
||||
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_NAME=firstadvancedev
|
||||
# DATABASE_PORT=5432
|
||||
|
||||
# Flask Configuration
|
||||
FLASK_APP=wsgi.py
|
||||
FLASK_ENV=development
|
||||
APP_PORT=4500
|
||||
|
||||
|
||||
# Bank Call Service Connection
|
||||
SIMBRELLA_BASE_URL="https://bank-emulator.dev.simbrellang.net"
|
||||
VALID_APP_ID=app1
|
||||
VALID_API_KEY=test-api-key-12345
|
||||
|
||||
|
||||
# Event Bus Broker Configuration
|
||||
KAFKA_BROKER="10.0.0.246:9092"
|
||||
@@ -35,7 +35,7 @@ class SimbrellaIntegration:
|
||||
],
|
||||
}
|
||||
|
||||
logger.error(f"This is PayLoad: {str(payload)}", exc_info=True)
|
||||
logger.info(f"This is PayLoad: {str(payload)}", exc_info=True)
|
||||
|
||||
headers = {
|
||||
"Content-Type": "application/json",
|
||||
@@ -46,9 +46,11 @@ class SimbrellaIntegration:
|
||||
try:
|
||||
response = httpx.post(url, json=payload, headers=headers, timeout=10.0)
|
||||
|
||||
logger.error(f"This is Response: {str(response)}", exc_info=True)
|
||||
logger.info(f"This is Response: {str(response)}", exc_info=True)
|
||||
|
||||
return response
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"RACCheck API call failed: {str(e)}", exc_info=True)
|
||||
raise Exception(f"RACCheck API call failed: {str(e)}")
|
||||
|
||||
|
||||
@@ -9,5 +9,6 @@ class SelectOfferSchema(Schema):
|
||||
msisdn = fields.Str(required=True)
|
||||
requestedAmount = fields.Float(required=True)
|
||||
productId = fields.Str(required=True)
|
||||
offerId = fields.Str(required=True)
|
||||
channel = fields.Str(required=True)
|
||||
|
||||
|
||||
@@ -6,3 +6,4 @@ from app.api.services.repayment import RepaymentService
|
||||
from app.api.services.customer_consent import CustomerConsentService
|
||||
from app.api.services.notification_callback import NotificationCallbackService
|
||||
from app.api.services.authorization import AuthorizationService
|
||||
from app.api.services.offer_analysis import OfferAnalysis
|
||||
|
||||
@@ -60,3 +60,114 @@ class BaseService:
|
||||
def async_send_to_kafka(cls, loan_data, request_id, topic):
|
||||
KafkaIntegration.send_loan_request(loan_data = loan_data, request_id = request_id, topic = topic)
|
||||
KafkaIntegration.flush()
|
||||
|
||||
|
||||
@classmethod
|
||||
def calculate_charges(cls, offer, amount):
|
||||
"""
|
||||
Calculates and returns the charges for the given offer and amount.
|
||||
|
||||
Args:
|
||||
offer (Offer): The offer object that contains the charges.
|
||||
amount (float): The requested loan amount.
|
||||
|
||||
Returns:
|
||||
dict: A dictionary containing the calculated charges.
|
||||
"""
|
||||
if not offer or not offer.charges:
|
||||
logger.error(f"No charges found for offer ID {offer.id}")
|
||||
return {"error": "No charges found for the offer"}
|
||||
|
||||
loan_charges = offer.charges
|
||||
tenor = offer.schedule # offer.tenor // 30 # Convert to months
|
||||
interest = cls.get_charge_detail(rates = offer.interest_rate, charges = loan_charges, code = "INTEREST", amount = amount)
|
||||
management = cls.get_charge_detail(rates = offer.management_rate, charges = loan_charges, code = "MGTFEE", amount = amount)
|
||||
insurance = cls.get_charge_detail(rates = offer.insurance_rate, charges = loan_charges, code = "INSURANCE", amount = amount)
|
||||
vat = cls.get_charge_detail(rates = offer.vat_rate, charges = loan_charges, code = "VAT", amount = amount, management_fee = management["fee"])
|
||||
|
||||
# Separate fees into upfront and postpaid
|
||||
upfront_fees = [
|
||||
fee["fee"]
|
||||
for fee in [interest, management, insurance, vat]
|
||||
if fee["due_days"] == 0
|
||||
]
|
||||
|
||||
postpaid_fees = [
|
||||
fee["fee"]
|
||||
for fee in [interest, management, insurance, vat]
|
||||
if fee["due_days"] != 0
|
||||
]
|
||||
vat_test = vat["fee"]
|
||||
logger.info(f"VAT fee == *************** : {vat_test}")
|
||||
|
||||
# Up-front payment: (only those fees due immediately i.e due_days == 0)
|
||||
# upfront_payment = sum(upfront_fees)
|
||||
if offer.schedule == 1:
|
||||
upfront_payment = vat["fee"] + management["fee"] + insurance["fee"] + interest["fee"]
|
||||
interest_amount = interest["fee"]
|
||||
repayment_amount = amount
|
||||
else:
|
||||
upfront_payment = vat["fee"] + insurance["fee"]+management["fee"]
|
||||
interest_amount = interest["fee"]*offer.schedule
|
||||
repayment_amount = amount + interest_amount
|
||||
|
||||
|
||||
# Repayment amount: (principal + only those fees not due immediately i.e due_days != 0)
|
||||
# repayment_amount = amount + (sum(postpaid_fees) * tenor)
|
||||
|
||||
# Total amount: (upfront_payment + repayment_amount)
|
||||
total_amount = upfront_payment + repayment_amount
|
||||
|
||||
# Calculate the installment amount
|
||||
installment_amount = repayment_amount / offer.schedule
|
||||
|
||||
return {
|
||||
"interest": interest,
|
||||
"interest_amount": interest_amount,
|
||||
"management": management,
|
||||
"insurance": insurance,
|
||||
"vat": vat,
|
||||
"upfront_payment": round(upfront_payment, 2),
|
||||
"repayment_amount": round(repayment_amount, 2),
|
||||
"installment_amount": round(installment_amount, 2),
|
||||
"total_amount": round(total_amount, 2)
|
||||
}
|
||||
|
||||
|
||||
@classmethod
|
||||
def get_charge_detail(cls, rates, charges, code, amount, management_fee= 0.0):
|
||||
"""
|
||||
Get details for a specific charge code from a list of loan charges.
|
||||
|
||||
Returns default values if not found.
|
||||
"""
|
||||
fee = 0.0
|
||||
|
||||
if code == "VAT" and management_fee > 0:
|
||||
fee = management_fee * rates / 100
|
||||
else:
|
||||
fee = amount * rates / 100
|
||||
|
||||
return {
|
||||
"rate": rates,
|
||||
"fee": round(fee, 2),
|
||||
"due_days": 30,
|
||||
"code": code,
|
||||
"description" : "have no idea how to get this yet"
|
||||
}
|
||||
|
||||
# if charge.code == code:
|
||||
# if code == "VAT" and management_fee > 0:
|
||||
# fee = management_fee * rates / 100
|
||||
# else:
|
||||
# fee = amount * rates / 100
|
||||
#
|
||||
# return {
|
||||
# "rate": rates,
|
||||
# "fee": round(fee, 2),
|
||||
# "due_days": charge.due
|
||||
# }
|
||||
|
||||
# return {"rate": 0, "fee": 0, "due_days": 0}
|
||||
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
from flask import session, jsonify
|
||||
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
|
||||
@@ -6,7 +7,11 @@ from marshmallow import ValidationError
|
||||
from app.api.enums import TransactionType
|
||||
from app.api.integrations import SimbrellaIntegration
|
||||
from app.extensions import db
|
||||
from app.models import Offer
|
||||
from app.models import Offer, RACCheck
|
||||
from app.api.services.offer_analysis import OfferAnalysis
|
||||
|
||||
import random
|
||||
|
||||
|
||||
class EligibilityCheckService(BaseService):
|
||||
TRANSACTION_TYPE = TransactionType.ELIGIBILITY_CHECK
|
||||
@@ -54,14 +59,74 @@ class EligibilityCheckService(BaseService):
|
||||
response = SimbrellaIntegration.rac_check(
|
||||
customer_id = customer_id,
|
||||
account_id = account_id,
|
||||
transaction_id = transaction.id,
|
||||
transaction_id = transaction.transaction_id,
|
||||
)
|
||||
|
||||
# this chck for error is not valid
|
||||
if response.status_code != 200:
|
||||
return jsonify({"message": "RACCheck failed"}), 400
|
||||
|
||||
response = response.json()
|
||||
|
||||
rac_check = RACCheck.add_rac_check(
|
||||
customer_id = customer_id,
|
||||
account_id = account_id,
|
||||
transaction_id = transaction.transaction_id,
|
||||
data = response['RACResponse']
|
||||
)
|
||||
|
||||
if not rac_check:
|
||||
logger.error(f"Failed to save RACCheck")
|
||||
return jsonify({
|
||||
"message": "Failed to save RACCheck."
|
||||
}), 400
|
||||
# -----------------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=customer
|
||||
)
|
||||
except ValueError as ve:
|
||||
logger.error(str(ve))
|
||||
return jsonify({
|
||||
"message": str(ve)
|
||||
}), 400
|
||||
# -----------------------------------------------------------------------
|
||||
# s = Offer.get_all_offers()
|
||||
|
||||
offers = [offer.to_dict() for offer in Offer.get_all_offers()]
|
||||
# eligible_offers = []
|
||||
|
||||
# for offer in offers:
|
||||
# # Determine an approved amount
|
||||
# random_float = random.random() # temporary to play data
|
||||
# approved_amount = min(offer.max_amount, offer.max_amount * random_float) #temporary for now
|
||||
# approved_amount = round(approved_amount, 2)
|
||||
#
|
||||
# transaction_offer = TransactionOffer.create_transaction_offer(
|
||||
# customer_id = customer.id,
|
||||
# transaction_id = transaction.transaction_id,
|
||||
# offer_id = offer.id,
|
||||
# min_amount = offer.min_amount,
|
||||
# max_amount = offer.max_amount,
|
||||
# eligible_amount = approved_amount,
|
||||
# product_id = offer.product_id,
|
||||
# tenor = offer.tenor
|
||||
# )
|
||||
#
|
||||
# # Visible offer ID: offer_id + padded(transaction_offer.id)
|
||||
# padded_id = str(transaction_offer.id).zfill(6)
|
||||
# public_offer_id = f"{offer.id}{padded_id}"
|
||||
#
|
||||
# eligible_offers.append({
|
||||
# "offerId": public_offer_id,
|
||||
# "product_id": offer.product_id,
|
||||
# "min_amount": offer.min_amount,
|
||||
# "max_amount": approved_amount,
|
||||
# "tenor": offer.tenor
|
||||
# })
|
||||
|
||||
# Simulate processing
|
||||
response_data = {
|
||||
@@ -69,7 +134,7 @@ class EligibilityCheckService(BaseService):
|
||||
"transactionId": transactionId,
|
||||
"countryCode": "NG",
|
||||
"msisdn": msisdn,
|
||||
"eligibleOffers": offers,
|
||||
"eligibleOffers": eligible_offers,
|
||||
"resultDescription": "Successful",
|
||||
"resultCode": "00",
|
||||
"accountId": account_id
|
||||
|
||||
@@ -0,0 +1,97 @@
|
||||
from app.models import Offer, TransactionOffer
|
||||
from app.models.loan import Loan
|
||||
import random
|
||||
import logging
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
class OfferAnalysis:
|
||||
|
||||
@staticmethod
|
||||
def get_offer(transaction_id, rac_response, validated_data):
|
||||
customer_id = validated_data.get("customerId")
|
||||
product_id = validated_data.get("productId")
|
||||
offer_id = validated_data.get("offerId")
|
||||
|
||||
transaction_offer_id = int(offer_id[5:]) # The last part is int
|
||||
|
||||
logger.info(f"customer_id == *************** : {customer_id}")
|
||||
logger.info(f"product_id == *************** : {product_id}")
|
||||
logger.info(f"offer_id == *************** : {offer_id}")
|
||||
logger.info(f"transaction_offer_id == *************** : {transaction_offer_id}")
|
||||
|
||||
transaction_offer = TransactionOffer.is_valid_transaction_offer(transaction_offer_id, customer_id, product_id)
|
||||
|
||||
if not transaction_offer:
|
||||
raise ValueError("Invalid Transaction Offer.")
|
||||
|
||||
eligible_amount = transaction_offer.eligible_amount
|
||||
offer = Offer.is_valid_offer( transaction_offer.offer_id)
|
||||
|
||||
if not offer:
|
||||
raise ValueError("Invalid Offer.")
|
||||
original_transaction = transaction_id
|
||||
|
||||
return transaction_offer, offer, eligible_amount, original_transaction
|
||||
|
||||
@staticmethod
|
||||
def decide_offer(transaction_id, rac_check, validated_data, customer):
|
||||
|
||||
# if we have active offers - we have to feed off it
|
||||
|
||||
# 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
|
||||
|
||||
# real_eligible_amount = last_customer_loan.eligible_amount
|
||||
|
||||
# active_loans = Loan.get_active_loans_by_original_transaction(original_transaction)
|
||||
|
||||
# sum_active_loans = sum(loan.current_loan_amount for loan in active_loans)
|
||||
|
||||
# new_eligible_amount = max(real_eligible_amount - sum_active_loans, 0)
|
||||
|
||||
# logger.info(f"Real eligible: {real_eligible_amount}, Sum of active: {sum_active_loans}, New eligible: {new_eligible_amount}")
|
||||
|
||||
# Construct eligible_offers
|
||||
|
||||
offers = Offer.get_all_offers()
|
||||
eligible_offers = []
|
||||
|
||||
for offer in offers:
|
||||
# Get approved amount
|
||||
random_float = random.random() # temporary to play data
|
||||
|
||||
approved_amount = new_eligible_amount if new_eligible_amount > 0 else min(offer.max_amount, offer.max_amount * random_float)
|
||||
approved_amount = round(approved_amount, 2)
|
||||
|
||||
transaction_offer = TransactionOffer.create_transaction_offer(
|
||||
customer_id=customer.id,
|
||||
transaction_id=transaction_id,
|
||||
original_transaction=transaction_id,
|
||||
offer_id=offer.id,
|
||||
min_amount=offer.min_amount,
|
||||
max_amount=offer.max_amount,
|
||||
eligible_amount=approved_amount,
|
||||
product_id=offer.product_id,
|
||||
tenor=offer.tenor
|
||||
)
|
||||
|
||||
# Visible offer ID: offer_id + padded(transaction_offer.id)
|
||||
padded_id = str(transaction_offer.id).zfill(6)
|
||||
public_offer_id = f"{offer.id}{padded_id}"
|
||||
|
||||
eligible_offers.append({
|
||||
"offerId": public_offer_id,
|
||||
"product_id": offer.product_id,
|
||||
"min_amount": offer.min_amount,
|
||||
"max_amount": approved_amount,
|
||||
"tenor": offer.tenor
|
||||
})
|
||||
|
||||
return eligible_offers
|
||||
@@ -8,9 +8,13 @@ from app.models.loan_charge import LoanCharge
|
||||
from app.utils.logger import logger
|
||||
from app.api.schemas.provide_loan import ProvideLoanSchema
|
||||
from threading import Thread
|
||||
from app.models import Loan, Offer, Charge
|
||||
from app.models import Loan, Offer, Charge , TransactionOffer, RACCheck
|
||||
from app.api.enums import LoanStatus
|
||||
from app.extensions import db
|
||||
from datetime import datetime, timezone
|
||||
from dateutil.relativedelta import relativedelta
|
||||
from app.models import LoanRepaymentSchedule
|
||||
from app.api.services.offer_analysis import OfferAnalysis
|
||||
|
||||
class ProvideLoanService(BaseService):
|
||||
TRANSACTION_TYPE = TransactionType.PROVIDE_LOAN
|
||||
@@ -36,20 +40,46 @@ class ProvideLoanService(BaseService):
|
||||
collection_type = validated_data.get('collectionType')
|
||||
transaction_id = validated_data.get('transactionId')
|
||||
offer_id = validated_data.get('offerId')
|
||||
amount = validated_data.get("requestedAmount")
|
||||
product_id = validated_data.get("productId")
|
||||
|
||||
customer = Customer.is_valid_customer(customer_id)
|
||||
|
||||
if (ProvideLoanService.validate_account_ownership(account_id = account_id, customer_id = customer_id)):
|
||||
if (ProvideLoanService.validate_account_ownership(account_id = account_id, customer_id = customer_id)):
|
||||
|
||||
offer = Offer.is_valid_offer(offer_id)
|
||||
rac_response = RACCheck.get_rac_check(customer_id = customer_id, account_id = account_id)
|
||||
|
||||
if not offer:
|
||||
logger.error(f"Invalid Offer")
|
||||
try:
|
||||
transaction_offer, offer, eligible_amount, original_transaction = OfferAnalysis.get_offer(
|
||||
transaction_id=transaction_id,
|
||||
rac_response=rac_response,
|
||||
validated_data=validated_data
|
||||
)
|
||||
except ValueError as ve:
|
||||
logger.error(str(ve))
|
||||
return jsonify({
|
||||
"message": "Invalid Offer."
|
||||
"message": str(ve)
|
||||
}), 400
|
||||
|
||||
# transaction_offer_id = int(offer_id[5:]) # The last part is int
|
||||
|
||||
# transaction_offer = TransactionOffer.is_valid_transaction_offer(transaction_offer_id)
|
||||
|
||||
# if not transaction_offer:
|
||||
# logger.error(f"Invalid Transaction Offer")
|
||||
# return jsonify({
|
||||
# "message": "Invalid Transaction Offer."
|
||||
# }), 400
|
||||
|
||||
# eligible_amount = transaction_offer.eligible_amount
|
||||
# offer = Offer.is_valid_offer( transaction_offer.offer_id)
|
||||
|
||||
# if not offer:
|
||||
# logger.error(f"Invalid Offer")
|
||||
# return jsonify({
|
||||
# "message": "Invalid Offer."
|
||||
# }), 400
|
||||
|
||||
# Log Transaction
|
||||
transaction = ProvideLoanService.log_transaction(validated_data=validated_data)
|
||||
|
||||
@@ -58,7 +88,24 @@ class ProvideLoanService(BaseService):
|
||||
return jsonify({
|
||||
"message": "Failed to log transaction."
|
||||
}), 400
|
||||
|
||||
|
||||
db.session.flush()
|
||||
|
||||
charges = ProvideLoanService.calculate_charges(offer, amount)
|
||||
upfront_fee = charges["upfront_payment"]
|
||||
repayment_amount = charges["repayment_amount"]
|
||||
#installment_amount = charges["installment_amount"]
|
||||
num_schedules = offer.schedule
|
||||
|
||||
upfront_payment = charges["upfront_payment"]
|
||||
total_amount = charges["total_amount"]
|
||||
installment_amount = charges["installment_amount"]
|
||||
interest = charges["interest"]
|
||||
management = charges["management"]
|
||||
insurance = charges["insurance"]
|
||||
vat = charges["vat"]
|
||||
|
||||
|
||||
# Save the loan details
|
||||
loan = Loan.create_loan(
|
||||
@@ -68,23 +115,36 @@ class ProvideLoanService(BaseService):
|
||||
product_id = offer.product_id,
|
||||
collection_type = collection_type,
|
||||
transaction_id = validated_data.get('transactionId'),
|
||||
original_transaction = transaction_offer.original_transaction,
|
||||
initial_loan_amount = validated_data.get('requestedAmount'),
|
||||
status= LoanStatus.ACTIVE
|
||||
upfront_fee = upfront_fee,
|
||||
repayment_amount = repayment_amount,
|
||||
installment_amount = installment_amount,
|
||||
eligible_amount=eligible_amount,
|
||||
status = LoanStatus.ACTIVE,
|
||||
tenor = offer.tenor,
|
||||
)
|
||||
|
||||
db.session.flush()
|
||||
|
||||
|
||||
if not loan:
|
||||
logger.error(f"Failed to save loan details")
|
||||
return jsonify({
|
||||
"message": "Failed to save loan details."
|
||||
}), 400
|
||||
|
||||
|
||||
charges = Charge.get_offer_charges(offer.id)
|
||||
|
||||
logger.error(f"{charges}")
|
||||
db.session.flush()
|
||||
|
||||
schedule = LoanRepaymentSchedule.add_repayment_schedule(loan = loan, num_schedules = num_schedules, transaction_id = transaction_id)
|
||||
|
||||
|
||||
if not schedule:
|
||||
logger.error(f"Failed to create repayment schedule for loan ID {loan.id}")
|
||||
return jsonify({
|
||||
"message": "Failed to generate loan repayment schedule."
|
||||
}), 400
|
||||
|
||||
# charges = Charge.get_offer_charges(offer.id)
|
||||
|
||||
# logger.info(f"{charges}")
|
||||
|
||||
loan_id = loan.id
|
||||
|
||||
@@ -139,8 +199,4 @@ class ProvideLoanService(BaseService):
|
||||
db.session.rollback()
|
||||
return jsonify({
|
||||
"message": "Internal Server Error"
|
||||
}) , 500
|
||||
|
||||
|
||||
|
||||
|
||||
}) , 500
|
||||
@@ -5,7 +5,9 @@ from app.api.enums import TransactionType
|
||||
from app.utils.logger import logger
|
||||
from app.api.schemas.select_offer import SelectOfferSchema
|
||||
from app.extensions import db
|
||||
|
||||
from app.models import Offer
|
||||
from datetime import date
|
||||
from dateutil.relativedelta import relativedelta
|
||||
|
||||
class SelectOfferService(BaseService):
|
||||
TRANSACTION_TYPE = TransactionType.SELECT_OFFER
|
||||
@@ -28,6 +30,15 @@ class SelectOfferService(BaseService):
|
||||
)
|
||||
account_id = validated_data.get("accountId")
|
||||
customer_id = validated_data.get("customerId")
|
||||
amount = validated_data.get("requestedAmount")
|
||||
product_id = validated_data.get("productId")
|
||||
transaction_offer_id = validated_data.get("offerId")
|
||||
transaction_id = validated_data.get("transactionId")
|
||||
request_id = validated_data.get("requestId")
|
||||
|
||||
offer_id = int(transaction_offer_id[5:]) # The last part is int
|
||||
|
||||
#"offerId": "SAL30001129",
|
||||
|
||||
if SelectOfferService.validate_account_ownership(
|
||||
account_id=account_id, customer_id=customer_id
|
||||
@@ -42,30 +53,78 @@ class SelectOfferService(BaseService):
|
||||
else:
|
||||
return jsonify({"message": "Invalid Customer or Account"}), 400
|
||||
|
||||
# Get the offer by product ID
|
||||
offer = Offer.get_offer_by_product_id(product_id)
|
||||
|
||||
db.session.flush()
|
||||
|
||||
charges = SelectOfferService.calculate_charges(offer, amount)
|
||||
upfront_payment = charges["upfront_payment"]
|
||||
total_amount = charges["total_amount"]
|
||||
installment_amount = charges["installment_amount"]
|
||||
interest = charges["interest"]
|
||||
management = charges["management"]
|
||||
insurance = charges["insurance"]
|
||||
vat = charges["vat"]
|
||||
repayment_amount = charges["repayment_amount"]
|
||||
interest_amount = charges["interest_amount"]
|
||||
|
||||
|
||||
# Calculate the repayment dates
|
||||
tenor = offer.tenor
|
||||
start_date = date.today()
|
||||
|
||||
# Convert tenor to months
|
||||
months = offer.schedule # tenor // 30
|
||||
|
||||
recommended_repayment_dates = [
|
||||
(start_date + relativedelta(months=i + 1)).isoformat()
|
||||
for i in range(months)
|
||||
]
|
||||
|
||||
|
||||
|
||||
offers = [
|
||||
{
|
||||
"offerId": "SAL90",
|
||||
"productId": "2030",
|
||||
"amount": 10000.0,
|
||||
"upfrontPayment": 1000.0,
|
||||
"interestRate": 3.0,
|
||||
"managementRate": 1.0,
|
||||
"managementFee": 1.0,
|
||||
"insuranceRate": 1.0,
|
||||
"insuranceFee": 100.0,
|
||||
"VATRate": 7.5,
|
||||
"VATAmount": 100.0,
|
||||
"recommendedRepaymentDates": ["2022-11-30"],
|
||||
"installmentAmount": 11000.0,
|
||||
"totalRepaymentAmount": 11000.0,
|
||||
"offerId": transaction_offer_id,
|
||||
"productId": product_id,
|
||||
"amount": amount,
|
||||
"upfrontPayment": upfront_payment,
|
||||
"interestRate": offer.interest_rate,
|
||||
"interestAmount": interest_amount,
|
||||
"managementRate": offer.management_rate,
|
||||
"managementFee": management["fee"],
|
||||
"insuranceRate": offer.insurance_rate,
|
||||
"insuranceFee": insurance["fee"],
|
||||
"VATRate": offer.vat_rate,
|
||||
"VATAmount": vat["fee"],
|
||||
"recommendedRepaymentDates": recommended_repayment_dates,
|
||||
"repaymentAmount": repayment_amount,
|
||||
"installmentAmount": installment_amount,
|
||||
"totalRepaymentAmount": total_amount,
|
||||
}
|
||||
]
|
||||
|
||||
# "offerId": offer.id,
|
||||
# "productId": product_id,
|
||||
# "amount": amount,
|
||||
# "upfrontPayment": upfront_payment,
|
||||
# "interestRate": interest["rate"],
|
||||
# "managementRate": management["rate"],
|
||||
# "managementFee": management["fee"],
|
||||
# "insuranceRate": insurance["rate"],
|
||||
# "insuranceFee": insurance["fee"],
|
||||
# "VATRate": vat["rate"],
|
||||
# "VATAmount": vat["fee"],
|
||||
# "recommendedRepaymentDates": recommended_repayment_dates,
|
||||
# "installmentAmount": installment_amount,
|
||||
# "totalRepaymentAmount": total_amount,
|
||||
#
|
||||
# Business logic - selecting an offer
|
||||
response_data = {
|
||||
"outstandingDebtAmount": 0,
|
||||
"requestId": "202111170001371256908",
|
||||
"transactionId": transaction.id,
|
||||
"requestId": request_id,
|
||||
"transactionId": transaction_id,
|
||||
"customerId": customer_id,
|
||||
"accountId": account_id,
|
||||
"loan": offers,
|
||||
@@ -91,3 +150,4 @@ class SelectOfferService(BaseService):
|
||||
logger.error(f"An error occurred: {str(e)}", exc_info=True)
|
||||
db.session.rollback()
|
||||
return jsonify({"message": "Internal Server Error"}), 500
|
||||
|
||||
+2
-1
@@ -31,7 +31,8 @@ class Config:
|
||||
"JWT_REFRESH_TOKEN_EXPIRES", timedelta(days=30)
|
||||
)
|
||||
|
||||
KAFKA_BROKER = 'dev-events.simbrellang.net:9085'
|
||||
# KAFKA_BROKER = 'dev-events.simbrellang.net:9085'
|
||||
KAFKA_BROKER = os.getenv("KAFKA_BROKER", "dev-events.simbrellang.net:9085")
|
||||
|
||||
|
||||
settings = Config()
|
||||
|
||||
@@ -6,6 +6,9 @@ from .repayment import Repayment
|
||||
from .loan_charge import LoanCharge
|
||||
from .offer import Offer
|
||||
from .charge import Charge
|
||||
from .rac_checks import RACCheck
|
||||
from .loan_repayment_schedule import LoanRepaymentSchedule
|
||||
from .transaction_offers import TransactionOffer
|
||||
|
||||
|
||||
__all__ = ['Customer', 'Account', 'Loan', 'Transaction', 'Repayment', 'LoanCharge', 'Offer', 'Charge']
|
||||
__all__ = ['Customer', 'Account', 'Loan', 'Transaction', 'Repayment', 'LoanCharge', 'Offer', 'Charge', 'RACCheck', 'LoanRepaymentSchedule', 'TransactionOffer']
|
||||
@@ -27,6 +27,13 @@ class Customer(db.Model):
|
||||
back_populates="customer",
|
||||
)
|
||||
|
||||
transaction_offers = relationship(
|
||||
"TransactionOffer",
|
||||
primaryjoin="Customer.id == TransactionOffer.customer_id",
|
||||
foreign_keys="TransactionOffer.customer_id",
|
||||
back_populates="customer",
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def is_valid_customer(cls, customer_id):
|
||||
customer = cls.query.filter_by(id=customer_id).first()
|
||||
|
||||
+89
-9
@@ -4,7 +4,10 @@ from app.models.customer import Customer
|
||||
from app.models.account import Account
|
||||
from sqlalchemy.exc import IntegrityError
|
||||
from sqlalchemy.orm import relationship
|
||||
from app.models.loan_charge import LoanCharge
|
||||
from dateutil.relativedelta import relativedelta
|
||||
from datetime import timedelta
|
||||
import logging
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class Loan(db.Model):
|
||||
@@ -17,6 +20,7 @@ class Loan(db.Model):
|
||||
)
|
||||
customer_id = db.Column(db.String(50), nullable=False)
|
||||
transaction_id = db.Column(db.String(50), nullable=True)
|
||||
original_transaction = db.Column(db.String(50), nullable=True)
|
||||
account_id = db.Column(db.String(50), nullable=False)
|
||||
offer_id = db.Column(db.String(20), nullable=False)
|
||||
product_id = db.Column(db.String(20), nullable=True)
|
||||
@@ -25,10 +29,17 @@ class Loan(db.Model):
|
||||
initial_loan_amount = db.Column(db.Float, nullable=False)
|
||||
default_penalty_fee = db.Column(db.Float, default=0)
|
||||
continuous_fee = db.Column(db.Float, default=0)
|
||||
upfront_fee = db.Column(db.Float, nullable=True, default=0.0)
|
||||
repayment_amount = db.Column(db.Float, nullable=True, default=0.0)
|
||||
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, default=datetime.now(timezone.utc))
|
||||
updated_at = db.Column(db.DateTime, default=datetime.now(timezone.utc), onupdate=datetime.now(timezone.utc))
|
||||
eligible_amount = db.Column(db.Float, nullable=True, default=0.0)
|
||||
disburse_date = db.Column(db.DateTime, nullable=True)
|
||||
disburse_verify = db.Column(db.DateTime, nullable=True)
|
||||
|
||||
customer = relationship(
|
||||
"Customer",
|
||||
@@ -39,20 +50,44 @@ class Loan(db.Model):
|
||||
|
||||
loan_charges = relationship(
|
||||
"LoanCharge",
|
||||
primaryjoin="Loan.id == LoanCharge.loan_id",
|
||||
primaryjoin="LoanCharge.loan_id == Loan.id",
|
||||
foreign_keys="LoanCharge.loan_id",
|
||||
back_populates="loan",
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def create_loan(cls, customer_id, account_id, offer_id, product_id, initial_loan_amount, collection_type, transaction_id, status='pending'):
|
||||
loan_repayment_schedules = relationship(
|
||||
"LoanRepaymentSchedule",
|
||||
primaryjoin="LoanRepaymentSchedule.loan_id == Loan.id",
|
||||
foreign_keys="LoanRepaymentSchedule.loan_id",
|
||||
back_populates="loan",
|
||||
)
|
||||
|
||||
|
||||
@classmethod
|
||||
def create_loan(
|
||||
cls,
|
||||
customer_id,
|
||||
account_id,
|
||||
offer_id,
|
||||
product_id,
|
||||
initial_loan_amount,
|
||||
collection_type,
|
||||
transaction_id,
|
||||
original_transaction,
|
||||
upfront_fee,
|
||||
repayment_amount,
|
||||
installment_amount,
|
||||
tenor,
|
||||
eligible_amount,
|
||||
status = "pending",
|
||||
):
|
||||
# Check if customer exists
|
||||
customer = Customer.is_valid_customer(customer_id)
|
||||
if not customer:
|
||||
raise ValueError("Customer does not exist")
|
||||
|
||||
now = datetime.now(timezone.utc)
|
||||
due_date = now + timedelta(days=tenor)
|
||||
|
||||
# Create and save the loan
|
||||
loan = cls(
|
||||
@@ -62,10 +97,16 @@ class Loan(db.Model):
|
||||
product_id = product_id,
|
||||
collection_type = collection_type,
|
||||
transaction_id = transaction_id,
|
||||
original_transaction = original_transaction,
|
||||
initial_loan_amount = initial_loan_amount,
|
||||
current_loan_amount = initial_loan_amount,
|
||||
due_date=now,
|
||||
status = status
|
||||
upfront_fee = upfront_fee,
|
||||
repayment_amount = repayment_amount,
|
||||
installment_amount = installment_amount,
|
||||
due_date=due_date,
|
||||
tenor = tenor,
|
||||
status = status,
|
||||
eligible_amount =eligible_amount
|
||||
)
|
||||
|
||||
try:
|
||||
@@ -74,7 +115,6 @@ class Loan(db.Model):
|
||||
raise ValueError(f"Database integrity error: {err}")
|
||||
return loan
|
||||
|
||||
|
||||
@classmethod
|
||||
def has_active_loans(cls, customer_id):
|
||||
active_loans = cls.query.filter_by(
|
||||
@@ -90,13 +130,50 @@ class Loan(db.Model):
|
||||
@classmethod
|
||||
def get_customer_loan(cls, loan_id, customer_id):
|
||||
"""
|
||||
Get customer's active loans.
|
||||
Get customer's active loans by loan_id.
|
||||
"""
|
||||
loan = cls.query.filter_by(id = loan_id, customer_id = customer_id).first()
|
||||
if not loan:
|
||||
raise ValueError(f"Loan with ID {loan_id} does not exist or does not belong to customer {customer_id}.")
|
||||
return loan
|
||||
|
||||
|
||||
@classmethod
|
||||
def get_customer_last_loan(cls, customer_id):
|
||||
"""
|
||||
Get customer's active loans.
|
||||
"""
|
||||
logger.info(f"Find last loan for [customer_id] ==>>>> {customer_id}")
|
||||
loan = cls.query.filter_by( customer_id = customer_id).first()
|
||||
logger.info(f" Active Loan ==>>>> RESULT************************ AMEYE")
|
||||
if not loan:
|
||||
return None
|
||||
|
||||
loan = {
|
||||
"eligible_amount": 0,
|
||||
"loan_amount": 0,
|
||||
"customer_id": customer_id,
|
||||
"transaction_id": "",
|
||||
"resultDescription": "No Active Loan"
|
||||
}
|
||||
logger.info(f" Active Loan ==>>>> RESULT*********************")
|
||||
logger.info(f" Active Loan ==>>>> {loan}")
|
||||
|
||||
return loan
|
||||
|
||||
@classmethod
|
||||
def get_active_loans_by_original_transaction(cls, original_transaction_id):
|
||||
"""
|
||||
Get all active loans with the same original_transaction ID.
|
||||
"""
|
||||
|
||||
active_loans = cls.query.filter_by(
|
||||
original_transaction=original_transaction_id,
|
||||
# status='active'
|
||||
).all()
|
||||
|
||||
return active_loans
|
||||
|
||||
|
||||
@classmethod
|
||||
def update_status(cls, loan_id, status):
|
||||
"""
|
||||
@@ -125,6 +202,9 @@ class Loan(db.Model):
|
||||
'defaultPenaltyFee': self.default_penalty_fee,
|
||||
'continuousFee': self.continuous_fee,
|
||||
'collectionType': self.collection_type,
|
||||
'upfrontFee': self.upfront_fee,
|
||||
'repaymentAmount': self.repayment_amount,
|
||||
'installmentAmount': self.installment_amount,
|
||||
'status': self.status,
|
||||
'dueDate': self.due_date.isoformat() if self.due_date else None,
|
||||
'loanDate': self.created_at.isoformat() if self.created_at else None,
|
||||
|
||||
+13
-10
@@ -1,6 +1,7 @@
|
||||
from datetime import datetime, timezone, timedelta
|
||||
from app.extensions import db
|
||||
from sqlalchemy.orm import relationship
|
||||
from app.utils.logger import logger
|
||||
|
||||
|
||||
class LoanCharge(db.Model):
|
||||
@@ -35,8 +36,8 @@ class LoanCharge(db.Model):
|
||||
charges (list): A list of dictionaries with keys:
|
||||
code (str), amount (float), percent (float), description (str), due (int)
|
||||
"""
|
||||
if not charges or not isinstance(charges, list):
|
||||
raise ValueError("Charges must be a non-empty list of dictionaries")
|
||||
# if not charges or not isinstance(charges, list):
|
||||
# raise ValueError("Charges must be a non-empty list of dictionaries")
|
||||
|
||||
if loan_id is None:
|
||||
raise ValueError("loan_id cannot be None")
|
||||
@@ -44,21 +45,23 @@ class LoanCharge(db.Model):
|
||||
loan_charges = []
|
||||
now = datetime.now(timezone.utc)
|
||||
|
||||
for charge in charges:
|
||||
due_days = getattr(charge, "due", 0)
|
||||
amount = getattr(charge, "amount", 0.0)
|
||||
percent = getattr(charge, "percent", 0.0)
|
||||
|
||||
if amount == 0.0:
|
||||
amount = (percent / 100.0) * referenced_amount
|
||||
subset_keys = ['interest', 'management', 'insurance', 'vat']
|
||||
for item in subset_keys:
|
||||
charge = charges[item]
|
||||
due_days = charge['due_days'] # getattr(charge, "due_days", 0)
|
||||
amount = charge['fee'] # getattr(charge, "fee", 0.0)
|
||||
percent = charge['rate'] # getattr(charge, "rate", 0.0)
|
||||
code = charge['code'] # getattr(charge, "code","")
|
||||
description = charge['description'] # getattr(charge, "description", "")
|
||||
|
||||
charge_obj = cls(
|
||||
loan_id = loan_id,
|
||||
transaction_id = transaction_id,
|
||||
code = getattr(charge, "code"),
|
||||
code = code,
|
||||
amount = round(amount, 2),
|
||||
percent = percent,
|
||||
description = getattr(charge, "description", ""),
|
||||
description = description,
|
||||
due = due_days,
|
||||
due_date = now + timedelta(days=due_days)
|
||||
)
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
from datetime import datetime, timezone
|
||||
from app.extensions import db
|
||||
from sqlalchemy.orm import relationship
|
||||
from dateutil.relativedelta import relativedelta
|
||||
|
||||
class LoanRepaymentSchedule(db.Model):
|
||||
__tablename__ = 'loan_repayment_schedules'
|
||||
|
||||
id = db.Column(db.Integer, primary_key=True, autoincrement=True)
|
||||
loan_id = db.Column(db.Integer, nullable=False)
|
||||
transaction_id = db.Column(db.String(50), nullable=True)
|
||||
product_id = db.Column(db.String(20), nullable=True)
|
||||
installment_number = db.Column(db.Integer, nullable=False)
|
||||
due_date = db.Column(db.DateTime, nullable=False)
|
||||
installment_amount= db.Column(db.Float, default=0.0)
|
||||
total_repayment_amount = db.Column(db.Float, default=0.0)
|
||||
paid = db.Column(db.Boolean, default=False)
|
||||
paid_at = db.Column(db.DateTime, nullable=True)
|
||||
|
||||
created_at = db.Column(db.DateTime, default=datetime.now(timezone.utc))
|
||||
updated_at = db.Column(db.DateTime, default=datetime.now(timezone.utc), onupdate=datetime.now(timezone.utc))
|
||||
|
||||
loan = relationship(
|
||||
"Loan",
|
||||
primaryjoin="LoanRepaymentSchedule.loan_id == Loan.id",
|
||||
foreign_keys=[loan_id],
|
||||
back_populates="loan_repayment_schedules",
|
||||
)
|
||||
|
||||
|
||||
@classmethod
|
||||
def add_repayment_schedule(cls, loan, num_schedules, transaction_id):
|
||||
"""
|
||||
Add repayment schedules for a given loan.
|
||||
"""
|
||||
now = datetime.now(timezone.utc)
|
||||
schedules = []
|
||||
|
||||
for i in range(num_schedules):
|
||||
due_date = now + relativedelta(months=i + 1)
|
||||
schedule = LoanRepaymentSchedule(
|
||||
loan_id=loan.id,
|
||||
installment_number=i + 1,
|
||||
due_date=due_date,
|
||||
total_repayment_amount = round(loan.repayment_amount, 2),
|
||||
installment_amount=round(loan.installment_amount, 2),
|
||||
product_id = loan.product_id,
|
||||
transaction_id = transaction_id
|
||||
)
|
||||
|
||||
db.session.add(schedule)
|
||||
schedules.append(schedule)
|
||||
|
||||
return schedules
|
||||
|
||||
def to_dict(self):
|
||||
return {
|
||||
'id': self.id,
|
||||
'loanId': self.loan_id,
|
||||
'installmentNumber': self.installment_number,
|
||||
'dueDate': self.due_date.isoformat(),
|
||||
'principalAmount': self.principal_amount,
|
||||
'interestAmount': self.interest_amount,
|
||||
'totalInstallment': self.total_installment,
|
||||
'paid': self.paid,
|
||||
'paidAt': self.paid_at.isoformat() if self.paid_at else None
|
||||
}
|
||||
|
||||
def __repr__(self):
|
||||
return f'<LoanRepaymentSchedule Loan:{self.loan_id} Installment:{self.installment_number}>'
|
||||
+33
-1
@@ -11,6 +11,12 @@ class Offer(db.Model):
|
||||
min_amount = db.Column(db.Float, nullable=False)
|
||||
max_amount = db.Column(db.Float, nullable=False)
|
||||
tenor = db.Column(db.Integer, nullable=False)
|
||||
schedule = db.Column(db.Integer, nullable=True)
|
||||
interest_rate = db.Column(db.Float, default=3.0)
|
||||
management_rate = db.Column(db.Float, default=1.0)
|
||||
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)
|
||||
created_at = db.Column(db.DateTime, default=datetime.now(timezone.utc))
|
||||
updated_at = db.Column(db.DateTime, default=datetime.now(timezone.utc), onupdate=datetime.now(timezone.utc))
|
||||
|
||||
@@ -40,6 +46,28 @@ class Offer(db.Model):
|
||||
if not offer:
|
||||
return False
|
||||
return offer
|
||||
|
||||
@classmethod
|
||||
def get_offer_by_id(cls, offer_id):
|
||||
"""
|
||||
Return an offer by its ID.
|
||||
"""
|
||||
offer = cls.query.filter_by(id=str(offer_id)).first()
|
||||
|
||||
if not offer:
|
||||
raise ValueError(f"Offer with ID {offer_id} not found")
|
||||
return offer
|
||||
|
||||
@classmethod
|
||||
def get_offer_by_product_id(cls, product_id):
|
||||
"""
|
||||
Return an offer by its product ID.
|
||||
"""
|
||||
offer = cls.query.filter_by(product_id=str(product_id)).first()
|
||||
|
||||
if not offer:
|
||||
raise ValueError(f"Offer with Product ID {product_id} not found")
|
||||
return offer
|
||||
|
||||
def to_dict(self):
|
||||
return {
|
||||
@@ -47,7 +75,11 @@ class Offer(db.Model):
|
||||
"productId": self.product_id,
|
||||
"minAmount": self.min_amount,
|
||||
"maxAmount": self.max_amount,
|
||||
"tenor": self.tenor
|
||||
"tenor": self.tenor,
|
||||
"interest_rate": self.interest_rate,
|
||||
"management_rate": self.management_rate,
|
||||
"insurance_rate": self.insurance_rate,
|
||||
"vat_rate": self.vat_rate
|
||||
}
|
||||
|
||||
def __repr__(self):
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
from datetime import datetime, timezone
|
||||
from app.extensions import db
|
||||
from sqlalchemy.orm import relationship
|
||||
from sqlalchemy.exc import IntegrityError
|
||||
from uuid import uuid4
|
||||
from sqlalchemy.types import JSON
|
||||
|
||||
class RACCheck(db.Model):
|
||||
__tablename__ = 'rac_checks'
|
||||
|
||||
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)
|
||||
created_at = db.Column(db.DateTime, default=datetime.now(timezone.utc))
|
||||
updated_at = db.Column(db.DateTime, default=datetime.now(timezone.utc), onupdate=datetime.now(timezone.utc))
|
||||
|
||||
@classmethod
|
||||
def add_rac_check(cls, customer_id, account_id, transaction_id, data = None):
|
||||
|
||||
|
||||
# Save the response
|
||||
rac_check = cls(
|
||||
customer_id = customer_id,
|
||||
account_id = account_id,
|
||||
transaction_id = transaction_id,
|
||||
rac_response = data
|
||||
)
|
||||
|
||||
try:
|
||||
db.session.add(rac_check)
|
||||
except IntegrityError as err:
|
||||
raise ValueError(f"Database integrity error: {err}")
|
||||
return rac_check
|
||||
|
||||
|
||||
@classmethod
|
||||
def get_all_rac_checks(cls):
|
||||
"""
|
||||
Return all RAC checks in dictionary format.
|
||||
"""
|
||||
rac_checks = cls.query.all()
|
||||
|
||||
if not rac_checks:
|
||||
return None
|
||||
return rac_checks
|
||||
|
||||
@classmethod
|
||||
def get_rac_check(cls, customer_id, account_id):
|
||||
"""
|
||||
Return a RAC check by its ID.
|
||||
"""
|
||||
rac_check = cls.query.filter_by( customer_id = customer_id,
|
||||
account_id = account_id,).first()
|
||||
|
||||
if not rac_check:
|
||||
raise ValueError(f"RAC Check for customer not found")
|
||||
return rac_check
|
||||
|
||||
def to_dict(self):
|
||||
return {
|
||||
"id": str(self.id),
|
||||
"transactionId": str(self.transaction_id),
|
||||
"customerId": self.customer_id,
|
||||
"accountId": self.account_id,
|
||||
"racResponse": self.rac_response,
|
||||
"createdAt": self.created_at.isoformat(),
|
||||
"updatedAt": self.updated_at.isoformat() if self.updated_at else None
|
||||
}
|
||||
|
||||
def __repr__(self):
|
||||
return f'<RACCheck {self.id}>'
|
||||
@@ -0,0 +1,80 @@
|
||||
from datetime import datetime, timezone
|
||||
from app.extensions import db
|
||||
from sqlalchemy.orm import relationship
|
||||
|
||||
class TransactionOffer(db.Model):
|
||||
__tablename__ = 'transaction_offers'
|
||||
|
||||
id = db.Column(db.Integer, primary_key=True, autoincrement=True)
|
||||
customer_id = db.Column(db.String(50), nullable=False)
|
||||
transaction_id = db.Column(db.String(50), nullable=False)
|
||||
original_transaction = db.Column(db.String(50), nullable=True)
|
||||
offer_id = db.Column(db.String(20), nullable=False)
|
||||
product_id = db.Column(db.String(20), nullable=True)
|
||||
min_amount = db.Column(db.Float, nullable=False)
|
||||
max_amount = db.Column(db.Float, nullable=False)
|
||||
eligible_amount = db.Column(db.Float, nullable=True)
|
||||
tenor = db.Column(db.Integer, nullable=True) # tenor in months, typically
|
||||
|
||||
created_at = db.Column(db.DateTime, default=datetime.now(timezone.utc))
|
||||
updated_at = db.Column(db.DateTime, default=datetime.now(timezone.utc), onupdate=datetime.now(timezone.utc))
|
||||
|
||||
customer = relationship(
|
||||
"Customer",
|
||||
primaryjoin="Customer.id == TransactionOffer.customer_id",
|
||||
foreign_keys=[customer_id],
|
||||
back_populates="transaction_offers",
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def is_valid_transaction_offer(cls, offer_id, customer_id, product_id):
|
||||
transaction_offer = cls.query.filter_by(
|
||||
id = str(offer_id),
|
||||
customer_id = customer_id,
|
||||
# product_id = product_id
|
||||
# transaction_id = transaction_id,
|
||||
).first()
|
||||
|
||||
if not transaction_offer:
|
||||
return False
|
||||
return transaction_offer
|
||||
|
||||
@classmethod
|
||||
def create_transaction_offer(cls, customer_id, transaction_id, original_transaction, offer_id, min_amount, max_amount, eligible_amount=None, product_id=None, tenor=None):
|
||||
"""
|
||||
Class method to create and save a TransactionOffer.
|
||||
"""
|
||||
transaction_offer = cls(
|
||||
customer_id=customer_id,
|
||||
transaction_id=transaction_id,
|
||||
original_transaction=original_transaction,
|
||||
offer_id=offer_id,
|
||||
min_amount=min_amount,
|
||||
max_amount=max_amount,
|
||||
eligible_amount=eligible_amount,
|
||||
product_id=product_id,
|
||||
tenor=tenor
|
||||
)
|
||||
|
||||
db.session.add(transaction_offer)
|
||||
db.session.flush()
|
||||
|
||||
return transaction_offer
|
||||
|
||||
def to_dict(self):
|
||||
return {
|
||||
'id': self.id,
|
||||
'customerId': self.customer_id,
|
||||
'transactionId': self.transaction_id,
|
||||
'offerId': self.offer_id,
|
||||
'productId': self.product_id,
|
||||
'minAmount': self.min_amount,
|
||||
'maxAmount': self.max_amount,
|
||||
'eligibleAmount': self.eligible_amount,
|
||||
'tenor': self.tenor,
|
||||
'createdAt': self.created_at.isoformat() if self.created_at else None,
|
||||
'updatedAt': self.updated_at.isoformat() if self.updated_at else None,
|
||||
}
|
||||
|
||||
def __repr__(self):
|
||||
return f'<TransactionOffer {self.id}>'
|
||||
@@ -47,13 +47,26 @@
|
||||
"productId": {
|
||||
"type": "string",
|
||||
"example": "101"
|
||||
},
|
||||
"installment": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"amount": {
|
||||
"type": "number",
|
||||
"format": "float",
|
||||
"example": 10000.0
|
||||
},
|
||||
"repaymentDate": {
|
||||
"type": "string",
|
||||
"example": "2025-04-24 10:31:"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"totalDebtAmount": {
|
||||
"type": "integer",
|
||||
"example": 8500
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"resultCode": {
|
||||
"type": "string",
|
||||
|
||||
@@ -27,6 +27,10 @@
|
||||
"example": "ACN8263457"
|
||||
},
|
||||
"productId": {
|
||||
"type": "string",
|
||||
"example": "2090"
|
||||
},
|
||||
"offerId": {
|
||||
"type": "string",
|
||||
"example": "101"
|
||||
},
|
||||
|
||||
@@ -35,6 +35,10 @@
|
||||
"format": "float",
|
||||
"example": 10000.0
|
||||
},
|
||||
"dueDate": {
|
||||
"type": "string",
|
||||
"example": "2025-04-24 10:31:"
|
||||
},
|
||||
"upfrontPayment": {
|
||||
"type": "number",
|
||||
"format": "float",
|
||||
@@ -45,6 +49,11 @@
|
||||
"format": "float",
|
||||
"example": 3.0
|
||||
},
|
||||
"interestAmount": {
|
||||
"type": "number",
|
||||
"format": "float",
|
||||
"example": 3000.00
|
||||
},
|
||||
"ManagementRate": {
|
||||
"type": "number",
|
||||
"format": "float",
|
||||
@@ -75,7 +84,7 @@
|
||||
"format": "float",
|
||||
"example": 100.0
|
||||
},
|
||||
"recommendedRepaymentDates": {
|
||||
"installmentRepaymentDates": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
"""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 ###
|
||||
@@ -0,0 +1,53 @@
|
||||
"""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 ###
|
||||
@@ -0,0 +1,42 @@
|
||||
"""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 ###
|
||||
@@ -0,0 +1,41 @@
|
||||
"""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 ###
|
||||
@@ -0,0 +1,52 @@
|
||||
"""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 ###
|
||||
@@ -0,0 +1,32 @@
|
||||
"""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 ###
|
||||
@@ -0,0 +1,34 @@
|
||||
"""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 ###
|
||||
@@ -0,0 +1,41 @@
|
||||
"""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 ###
|
||||
@@ -0,0 +1,38 @@
|
||||
"""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 ###
|
||||
@@ -0,0 +1,32 @@
|
||||
"""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 ###
|
||||
@@ -0,0 +1,32 @@
|
||||
"""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 ###
|
||||
@@ -0,0 +1,32 @@
|
||||
"""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 ###
|
||||
@@ -35,3 +35,7 @@ flask-jwt-extended
|
||||
# Kafka
|
||||
confluent-kafka==1.9.2
|
||||
|
||||
|
||||
|
||||
python-dateutil
|
||||
|
||||
|
||||
Reference in New Issue
Block a user