Compare commits

...

13 Commits

Author SHA1 Message Date
VivianDee 50ca27abfe [add]: Offer analysis 2025-05-07 12:07:57 +01:00
VivianDee 74066bae56 [add]: offer analysis 2025-05-06 07:09:36 +01:00
VivianDee 4c4ef909c2 [add]: Offer analysis 2025-05-05 17:03:39 +01:00
VivianDee fdd7c58fab [fix]: loan due date 2025-05-05 12:17:26 +01:00
ameye 4bd163fb31 eligible amount migration 2025-05-03 17:59:17 -04:00
ameye d77181f627 Fix loan id 2025-05-03 17:44:38 -04:00
CHIEFSOFT\ameye 4a236fdd2f eligible_amount 2025-05-03 17:40:38 -04:00
CHIEFSOFT\ameye cae7ffd772 transaction offer 2025-05-03 17:08:55 -04:00
ameye 4f92f2a1a0 Select offer update 2025-05-03 16:48:55 -04:00
CHIEFSOFT\ameye 03adb266bb approved_amount 2025-05-03 09:04:42 -04:00
CHIEFSOFT\ameye d28bf95c97 random play on the data 2025-05-03 08:52:25 -04:00
ameye bd6edf52e1 Merge branch 'loan_schedule_fix' of DigiFi/digifi-BankToProductCore into master 2025-04-30 12:22:14 +00:00
ameye 9dc431e66d Merge branch 'loan_schedule_fix' of DigiFi/digifi-BankToProductCore into master 2025-04-30 09:03:57 +00:00
12 changed files with 216 additions and 18 deletions
+1
View File
@@ -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)
+1
View File
@@ -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
+22 -3
View File
@@ -7,7 +7,9 @@ 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
import random
class EligibilityCheckService(BaseService):
TRANSACTION_TYPE = TransactionType.ELIGIBILITY_CHECK
@@ -55,12 +57,27 @@ 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
offers = Offer.get_all_offers()
@@ -68,7 +85,9 @@ class EligibilityCheckService(BaseService):
for offer in offers:
# Determine an approved amount
approved_amount = min(offer.max_amount, 5000)
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,
+24
View File
@@ -0,0 +1,24 @@
from app.models import Offer, TransactionOffer
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
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.")
return transaction_offer, offer, eligible_amount
+35 -8
View File
@@ -8,13 +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
@@ -45,16 +45,41 @@ class ProvideLoanService(BaseService):
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 = 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)
@@ -95,8 +120,10 @@ class ProvideLoanService(BaseService):
upfront_fee = upfront_fee,
repayment_amount = repayment_amount,
installment_amount = installment_amount,
eligible_amount=eligible_amount,
status = LoanStatus.ACTIVE,
tenor = offer.tenor
tenor = offer.tenor,
)
if not loan:
+1
View File
@@ -32,6 +32,7 @@ class SelectOfferService(BaseService):
customer_id = validated_data.get("customerId")
amount = validated_data.get("requestedAmount")
product_id = validated_data.get("productId")
offer_id = validated_data.get("offerId")
transaction_id = validated_data.get("transactionId")
request_id = validated_data.get("requestId")
+6 -1
View File
@@ -5,6 +5,7 @@ from app.models.account import Account
from sqlalchemy.exc import IntegrityError
from sqlalchemy.orm import relationship
from dateutil.relativedelta import relativedelta
from datetime import timedelta
class Loan(db.Model):
@@ -34,6 +35,7 @@ class Loan(db.Model):
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)
customer = relationship(
"Customer",
@@ -71,6 +73,7 @@ class Loan(db.Model):
repayment_amount,
installment_amount,
tenor,
eligible_amount,
status = "pending",
):
# Check if customer exists
@@ -79,6 +82,7 @@ class Loan(db.Model):
raise ValueError("Customer does not exist")
now = datetime.now(timezone.utc)
due_date = now + timedelta(days=tenor)
# Create and save the loan
loan = cls(
@@ -94,9 +98,10 @@ class Loan(db.Model):
upfront_fee = upfront_fee,
repayment_amount = repayment_amount,
installment_amount = installment_amount,
due_date=now,
due_date=due_date,
tenor = tenor,
status = status,
eligible_amount =eligible_amount
)
try:
+26 -6
View File
@@ -1,14 +1,14 @@
from datetime import datetime, timezone
from app.extensions import db
from sqlalchemy.orm import relationship
from sqlalchemy.dialects.postgresql import UUID
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.String, primary_key=True)
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)
@@ -16,6 +16,25 @@ class RACCheck(db.Model):
created_at = db.Column(db.DateTime, default=datetime.now(timezone.utc))
updated_at = db.Column(db.DateTime, default=datetime.now(timezone.utc), onupdate=datetime.now(timezone.utc))
@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):
"""
@@ -24,18 +43,19 @@ class RACCheck(db.Model):
rac_checks = cls.query.all()
if not rac_checks:
raise ValueError("No available RAC checks")
return None
return rac_checks
@classmethod
def get_rac_check_by_id(cls, check_id):
def get_rac_check(cls, customer_id, account_id):
"""
Return a RAC check by its ID.
"""
rac_check = cls.query.filter_by(id=check_id).first()
rac_check = cls.query.filter_by( customer_id = customer_id,
account_id = account_id,).first()
if not rac_check:
raise ValueError(f"RAC Check with ID {check_id} not found")
raise ValueError(f"RAC Check for customer not found")
return rac_check
def to_dict(self):
+12
View File
@@ -25,6 +25,18 @@ class TransactionOffer(db.Model):
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, offer_id, min_amount, max_amount, eligible_amount=None, product_id=None, tenor=None):
@@ -27,6 +27,10 @@
"example": "ACN8263457"
},
"productId": {
"type": "string",
"example": "2090"
},
"offerId": {
"type": "string",
"example": "101"
},
+52
View File
@@ -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 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 ###