This commit is contained in:
Azeez Muibi
2025-04-11 16:43:32 +01:00
parent 79f0ac63f6
commit e7243434a4
32 changed files with 766 additions and 311 deletions
+3 -1
View File
@@ -3,6 +3,7 @@ VALID_API_KEY=*************
BASIC_AUTH_USERNAME=****** BASIC_AUTH_USERNAME=******
BASIC_AUTH_PASSWORD=****** BASIC_AUTH_PASSWORD=******
SWAGGER_URL="/documentation" SWAGGER_URL="/documentation"
API_URL="/swagger.json" API_URL="/swagger.json"
@@ -10,6 +11,7 @@ JWT_SECRET_KEY=******
JWT_ACCESS_TOKEN_EXPIRES=****** JWT_ACCESS_TOKEN_EXPIRES=******
JWT_REFRESH_TOKEN_EXPIRES=****** JWT_REFRESH_TOKEN_EXPIRES=******
DATABASE_USER=***** DATABASE_USER=*****
DATABASE_PASSWORD=***** DATABASE_PASSWORD=*****
DATABASE_HOST=****** DATABASE_HOST=******
@@ -19,6 +21,6 @@ DATABASE_NAME=*****
# Flask Configuration # Flask Configuration
FLASK_APP=wsgi.py FLASK_APP=wsgi.py
FLASK_ENV=development FLASK_ENV=development
APP_PORT=4300 APP_PORT=4500
SIMBRELLA_BASE_URL=*************** SIMBRELLA_BASE_URL=***************
-10
View File
@@ -1,10 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="PYTHON_MODULE" version="4">
<component name="NewModuleRootManager">
<content url="file://$MODULE_DIR$">
<excludeFolder url="file://$MODULE_DIR$/.venv" />
</content>
<orderEntry type="jdk" jdkName="Python 3.13 (digifi-FirstCore)" jdkType="Python SDK" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
</module>
-6
View File
@@ -1,6 +0,0 @@
<component name="InspectionProjectProfileManager">
<settings>
<option name="USE_PROJECT_PROFILE" value="false" />
<version value="1.0" />
</settings>
</component>
-7
View File
@@ -1,7 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="Black">
<option name="sdkName" value="Python 3.13 (digifi-FirstCore)" />
</component>
<component name="ProjectRootManager" version="2" project-jdk-name="Python 3.13 (digifi-FirstCore)" project-jdk-type="Python SDK" />
</project>
-8
View File
@@ -1,8 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/.idea/digifi-FirstCore.iml" filepath="$PROJECT_DIR$/.idea/digifi-FirstCore.iml" />
</modules>
</component>
</project>
Generated
-6
View File
@@ -1,6 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="$PROJECT_DIR$" vcs="Git" />
</component>
</project>
+4 -2
View File
@@ -17,6 +17,8 @@ EXPOSE 5000
ENV FLASK_APP=app.py ENV FLASK_APP=app.py
ENV FLASK_RUN_HOST=0.0.0.0 ENV FLASK_RUN_HOST=0.0.0.0
RUN chmod +x scripts/entrypoint.sh #COPY scripts/enterypointone.sh scripts/enterypointone.sh
ENTRYPOINT ["scripts/entrypoint.sh"] RUN chmod +x scripts/enterypointone.sh
ENTRYPOINT ["scripts/enterypointone.sh"]
+75
View File
@@ -0,0 +1,75 @@
"""Update Offers
Revision ID: fd58e10e4968
Revises:
Create Date: 2025-03-28 15:47:35.620664
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = 'fd58e10e4968'
down_revision = None
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.create_table('accounts',
sa.Column('id', sa.String(length=50), nullable=False),
sa.Column('customer_id', sa.String(length=50), nullable=False),
sa.Column('account_type', sa.String(length=50), nullable=True),
sa.Column('status', sa.String(length=20), nullable=True),
sa.Column('lien_amount', sa.Float(), nullable=True),
sa.Column('created_at', sa.DateTime(), nullable=True),
sa.Column('updated_at', sa.DateTime(), nullable=True),
sa.PrimaryKeyConstraint('id')
)
op.create_table('customers',
sa.Column('id', sa.String(length=50), nullable=False),
sa.Column('msisdn', sa.String(length=20), nullable=False),
sa.Column('country_code', sa.String(length=3), nullable=False),
sa.Column('created_at', sa.DateTime(), nullable=True),
sa.Column('updated_at', sa.DateTime(), nullable=True),
sa.PrimaryKeyConstraint('id'),
sa.UniqueConstraint('msisdn')
)
op.create_table('loans',
sa.Column('id', sa.String(length=50), nullable=False),
sa.Column('customer_id', sa.String(length=50), nullable=False),
sa.Column('account_id', sa.String(length=50), nullable=False),
sa.Column('product_id', sa.String(length=20), nullable=False),
sa.Column('principal_amount', sa.Float(), nullable=False),
sa.Column('status', sa.String(length=20), nullable=True),
sa.Column('created_at', sa.DateTime(), nullable=True),
sa.Column('updated_at', sa.DateTime(), nullable=True),
sa.PrimaryKeyConstraint('id')
)
op.create_table('transactions',
sa.Column('id', sa.String(length=50), nullable=False),
sa.Column('account_id', sa.String(length=50), nullable=False),
sa.Column('type', sa.String(length=50), nullable=False),
sa.Column('amount', sa.Float(), nullable=False),
sa.Column('status', sa.String(length=20), nullable=True),
sa.Column('created_at', sa.DateTime(), nullable=True),
sa.Column('updated_at', sa.DateTime(), nullable=True),
sa.PrimaryKeyConstraint('id')
)
op.drop_table('test')
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.create_table('test',
sa.Column('id', sa.INTEGER(), autoincrement=True, nullable=False),
sa.Column('name', sa.VARCHAR(length=125), autoincrement=False, nullable=True)
)
op.drop_table('transactions')
op.drop_table('loans')
op.drop_table('customers')
op.drop_table('accounts')
# ### end Alembic commands ###
+12
View File
@@ -0,0 +1,12 @@
CREATE TABLE transactions (
id SERIAL,
transaction_id VARCHAR(50) NOT NULL,
account_id VARCHAR(50) NOT NULL,
type VARCHAR(50) NOT NULL,
channel VARCHAR(8) NOT NULL,
created_at timestamp with time zone DEFAULT now(),
updated_at timestamp with time zone DEFAULT now()
);
ALTER TABLE ONLY transactions
ADD CONSTRAINT transactions_id_key UNIQUE (id);
+2 -3
View File
@@ -38,7 +38,7 @@ def serve_paths(filename):
# Get All Transactions Endpoint # Get All Transactions Endpoint
@api.route("/transactions", methods=["GET"]) @api.route("/transactions", methods=["GET"])
@jwt_required() # @jwt_required()
def get_transactions(): def get_transactions():
# Extract query parameters for filtering # Extract query parameters for filtering
filters = { filters = {
@@ -56,7 +56,7 @@ def get_transactions():
# Get All Loans Endpoint # Get All Loans Endpoint
@api.route("/loans", methods=["GET"]) @api.route("/loans", methods=["GET"])
@jwt_required() # @jwt_required()
def get_loans(): def get_loans():
# Extract query parameters for filtering # Extract query parameters for filtering
filters = { filters = {
@@ -72,7 +72,6 @@ def get_loans():
response = LoanService.process_request(filters) response = LoanService.process_request(filters)
return response return response
# Authorize endpoint # Authorize endpoint
@api.route("/Authorize", methods=["POST"]) @api.route("/Authorize", methods=["POST"])
def authorize(): def authorize():
+5 -4
View File
@@ -49,10 +49,11 @@ class BaseService:
Create a new transaction. Create a new transaction.
""" """
return Transaction.create_transaction( return Transaction.create_transaction(
transaction_id =validated_data.get("transactionId"), transaction_id = validated_data.get("transactionId"),
account_id=validated_data.get("accountId"), ref_id = validated_data.get("refId") or validated_data.get("accountId"),
type=cls.TRANSACTION_TYPE, ref_model = validated_data.get("refModel", "account"),
channel=validated_data.get("channel"), type = cls.TRANSACTION_TYPE,
channel = validated_data.get("channel"),
) )
@classmethod @classmethod
+25 -19
View File
@@ -5,6 +5,7 @@ from app.utils.logger import logger
from app.api.schemas.customer_consent import CustomerConsentSchema from app.api.schemas.customer_consent import CustomerConsentSchema
from app.api.services.base_service import BaseService from app.api.services.base_service import BaseService
from app.api.enums import TransactionType from app.api.enums import TransactionType
from app.extensions import db
class CustomerConsentService(BaseService): class CustomerConsentService(BaseService):
@@ -22,36 +23,39 @@ class CustomerConsentService(BaseService):
dict: A standardized response. dict: A standardized response.
""" """
try: try:
with db.session.begin():
validated_data = CustomerConsentService.validate_data(data, CustomerConsentSchema())
account_id = validated_data.get('accountId')
customer_id = validated_data.get('customerId')
validated_data = CustomerConsentService.validate_data(data, CustomerConsentSchema()) if(CustomerConsentService.validate_account_ownership(account_id = account_id, customer_id = customer_id)):
account_id = validated_data.get('accountId')
customer_id = validated_data.get('customerId')
if(CustomerConsentService.validate_account_ownership(account_id = account_id, customer_id = customer_id)): transaction = CustomerConsentService.log_transaction(validated_data = validated_data)
transaction = CustomerConsentService.log_transaction(validated_data = validated_data)
if not transaction: if not transaction:
logger.error(f"Failed to log transaction") logger.error(f"Failed to log transaction")
return jsonify({
"message": "Failed to log transaction."
}), 400
else:
return jsonify({ return jsonify({
"message": "Failed to log transaction." "message": "Invalid Customer or Account"
}), 400 }), 400
else:
return jsonify({
"message": "Invalid Customer or Account"
}), 400
# Simulated processing logic # Simulated processing logic
response_data = { response_data = {
"resultCode": "00", "resultCode": "00",
"resultDescription": "Request is received" "resultDescription": "Request is received"
} }
return response_data db.session.commit()
return response_data
except ValidationError as err: except ValidationError as err:
logger.error(f"Validation Error: {getattr(err, 'messages', str(err))}") logger.error(f"Validation Error: {getattr(err, 'messages', str(err))}")
db.session.rollback()
return jsonify({ return jsonify({
"message": "Validation exception" "message": "Validation exception"
@@ -59,6 +63,7 @@ class CustomerConsentService(BaseService):
except ValueError as err: except ValueError as err:
logger.error(f"{getattr(err, 'messages', str(err))}") logger.error(f"{getattr(err, 'messages', str(err))}")
db.session.rollback()
return jsonify({ return jsonify({
"message": str(err) "message": str(err)
@@ -66,6 +71,7 @@ class CustomerConsentService(BaseService):
except Exception as e: except Exception as e:
logger.error(f"An error occurred: {str(e)}", exc_info=True) logger.error(f"An error occurred: {str(e)}", exc_info=True)
db.session.rollback()
return jsonify({ return jsonify({
"message": "Internal Server Error" "message": "Internal Server Error"
}) , 500 }) , 500
+60 -55
View File
@@ -5,6 +5,7 @@ from app.api.schemas.eligibility_check import EligibilityCheckSchema
from marshmallow import ValidationError from marshmallow import ValidationError
from app.api.enums import TransactionType from app.api.enums import TransactionType
from app.api.integrations import SimbrellaIntegration from app.api.integrations import SimbrellaIntegration
from app.extensions import db
class EligibilityCheckService(BaseService): class EligibilityCheckService(BaseService):
TRANSACTION_TYPE = TransactionType.ELIGIBILITY_CHECK TRANSACTION_TYPE = TransactionType.ELIGIBILITY_CHECK
@@ -21,71 +22,75 @@ class EligibilityCheckService(BaseService):
dict: A standardized response. dict: A standardized response.
""" """
try: try:
with db.session.begin():
validated_data = EligibilityCheckService.validate_data(data, EligibilityCheckSchema()) validated_data = EligibilityCheckService.validate_data(data, EligibilityCheckSchema())
account_id = validated_data.get('accountId') account_id = validated_data.get('accountId')
customer_id = validated_data.get('customerId') customer_id = validated_data.get('customerId')
transactionId = validated_data.get('transactionId') transactionId = validated_data.get('transactionId')
msisdn = validated_data.get('msisdn') msisdn = validated_data.get('msisdn')
customer = EligibilityCheckService.get_or_create_customer(validated_data = validated_data) customer = EligibilityCheckService.get_or_create_customer(validated_data = validated_data)
if (EligibilityCheckService.validate_account_ownership(account_id = account_id, customer_id = customer_id)): if (EligibilityCheckService.validate_account_ownership(account_id = account_id, customer_id = customer_id)):
transaction = EligibilityCheckService.log_transaction(validated_data = validated_data)
if not transaction: transaction = EligibilityCheckService.log_transaction(validated_data = validated_data)
logger.error(f"Failed to log transaction")
if not transaction:
logger.error(f"Failed to log transaction")
return jsonify({
"message": "Failed to log transaction."
}), 400
else:
return jsonify({ return jsonify({
"message": "Failed to log transaction." "message": "Invalid Customer or Account"
}), 400 }), 400
else:
return jsonify({
"message": "Invalid Customer or Account"
}), 400
# Call RACCheck # Call RACCheck
response = SimbrellaIntegration.rac_check( response = SimbrellaIntegration.rac_check(
customer_id = customer_id, customer_id = customer_id,
account_id = account_id, account_id = account_id,
transaction_id = transaction.id, transaction_id = transaction.id,
) )
logger.error(f"This is Response Returned ****** : {str(response)}") logger.error(f"This is Response Returned ****** : {str(response)}")
# this chck for error is not valid # this chck for error is not valid
logger.error(f"Check for ERROR is not valid ****** FIX THIS !!!!!") logger.error(f"Check for ERROR is not valid ****** FIX THIS !!!!!")
#if "error" in response or response.get("status") != 200: #if "error" in response or response.get("status") != 200:
# return jsonify({"message": "RACCheck failed"}), 400 # return jsonify({"message": "RACCheck failed"}), 400
offers = [ offers = [
{ {
"offerId": "SAL90", "offerId": "SAL90",
"productId": "2030", "productId": "2030",
"minAmount": 5000, "minAmount": 5000,
"maxAmount": 100000, "maxAmount": 100000,
"tenor": 30 "tenor": 30
}, },
{ {
"offerId": "SAL30", "offerId": "SAL30",
"productId": "2090", "productId": "2090",
"minAmount": 3000, "minAmount": 3000,
"maxAmount": 500000, "maxAmount": 500000,
"tenor": 90 "tenor": 90
} }
] ]
# Simulate processing # Simulate processing
response_data = { response_data = {
"customerId": customer_id, "customerId": customer_id,
"transactionId": transactionId, "transactionId": transactionId,
"countryCode": "NG", "countryCode": "NG",
"msisdn": msisdn, "msisdn": msisdn,
"eligibleOffers": offers, "eligibleOffers": offers,
"resultDescription": "Successful", "resultDescription": "Successful",
"resultCode": "00", "resultCode": "00",
"accountId": account_id "accountId": account_id
} }
return response_data
return response_data
except ValidationError as err: except ValidationError as err:
logger.error(f"Validation Error: {getattr(err, 'messages', str(err))}") logger.error(f"Validation Error: {getattr(err, 'messages', str(err))}")
+44 -32
View File
@@ -1,9 +1,11 @@
from flask import request, jsonify from flask import request, jsonify
from marshmallow import ValidationError from marshmallow import ValidationError
from app.models import Customer
from app.utils.logger import logger from app.utils.logger import logger
from app.api.schemas.loan_status import LoanStatusSchema from app.api.schemas.loan_status import LoanStatusSchema
from app.api.services.base_service import BaseService from app.api.services.base_service import BaseService
from app.api.enums import TransactionType from app.api.enums import TransactionType
from app.extensions import db
class LoanStatusService(BaseService): class LoanStatusService(BaseService):
@@ -21,12 +23,23 @@ class LoanStatusService(BaseService):
dict: A standardized response. dict: A standardized response.
""" """
try: try:
validated_data = LoanStatusService.validate_data(data, LoanStatusSchema()) with db.session.begin():
customer_id = validated_data.get('customerId') # Validate data
customer = LoanStatusService.get_or_create_customer(validated_data) validated_data = LoanStatusService.validate_data(data, LoanStatusSchema())
account = customer.accounts[0]
customer_id = validated_data.get('customerId')
customer = Customer.get_customer(customer_id)
transactionId = validated_data.get('transactionId')
# Get loans
loans = [loan.to_dict() for loan in customer.loans]
validated_data['refId'] = customer.id
validated_data['refModel'] = "customer"
if (LoanStatusService.validate_account_ownership(account_id = account.id, customer_id = customer_id)):
transaction = LoanStatusService.log_transaction(validated_data = validated_data) transaction = LoanStatusService.log_transaction(validated_data = validated_data)
if not transaction: if not transaction:
@@ -34,41 +47,38 @@ class LoanStatusService(BaseService):
return jsonify({ return jsonify({
"message": "Failed to log transaction." "message": "Failed to log transaction."
}), 400 }), 400
else:
return jsonify({
"message": "Invalid Customer or Account"
}), 400
loans = [ # loans = [
{ # {
"debtId": "123456789", # "debtId": "123456789",
"loanDate": "2019-10-18 14:26:21.063", # "loanDate": "2019-10-18 14:26:21.063",
"dueDate": "2019-11-20 14:26:21.063", # "dueDate": "2019-11-20 14:26:21.063",
"currentLoanAmount": 8500, # "currentLoanAmount": 8500,
"initialLoanAmount": 10000, # "initialLoanAmount": 10000,
"defaultPenaltyFee": 0, # "defaultPenaltyFee": 0,
"continuousFee": 0, # "continuousFee": 0,
"productId": "101" # "productId": "101"
# }
# ]
# Simulated processing logic
response_data = {
"customerId": customer_id,
"transactionId": transactionId,
"loans": loans,
"totalDebtAmount": 8500,
"resultCode": "00",
"resultDescription": "Successful"
} }
]
# Simulated processing logic db.session.commit()
response_data = { return response_data
"customerId": "CN621868",
"transactionId": "Tr201712RK9232P115",
"loans": loans,
"totalDebtAmount": 8500,
"resultCode": "00",
"resultDescription": "Successful"
}
return response_data
except ValidationError as err: except ValidationError as err:
logger.error(f"Validation Error: {getattr(err, 'messages', str(err))}") logger.error(f"Validation Error: {getattr(err, 'messages', str(err))}")
db.session.rollback()
return jsonify({ return jsonify({
"message": "Validation exception" "message": "Validation exception"
@@ -76,6 +86,7 @@ class LoanStatusService(BaseService):
except ValueError as err: except ValueError as err:
logger.error(f"{getattr(err, 'messages', str(err))}") logger.error(f"{getattr(err, 'messages', str(err))}")
db.session.rollback()
return jsonify({ return jsonify({
"message": str(err) "message": str(err)
@@ -83,6 +94,7 @@ class LoanStatusService(BaseService):
except Exception as e: except Exception as e:
logger.error(f"An error occurred: {str(e)}", exc_info=True) logger.error(f"An error occurred: {str(e)}", exc_info=True)
db.session.rollback()
return jsonify({ return jsonify({
"message": "Internal Server Error" "message": "Internal Server Error"
}) , 500 }) , 500
@@ -4,6 +4,7 @@ from app.api.services.base_service import BaseService
from app.api.enums import TransactionType from app.api.enums import TransactionType
from app.utils.logger import logger from app.utils.logger import logger
from app.api.schemas.notification_callback import NotificationCallbackSchema from app.api.schemas.notification_callback import NotificationCallbackSchema
from app.extensions import db
class NotificationCallbackService(BaseService): class NotificationCallbackService(BaseService):
TRANSACTION_TYPE = TransactionType.NOTIFICATION_CALLBACK TRANSACTION_TYPE = TransactionType.NOTIFICATION_CALLBACK
+59 -48
View File
@@ -8,6 +8,7 @@ from app.api.schemas.provide_loan import ProvideLoanSchema
from threading import Thread from threading import Thread
from app.models.loan import Loan from app.models.loan import Loan
from app.api.enums import LoanStatus from app.api.enums import LoanStatus
from app.extensions import db
class ProvideLoanService(BaseService): class ProvideLoanService(BaseService):
TRANSACTION_TYPE = TransactionType.PROVIDE_LOAN TRANSACTION_TYPE = TransactionType.PROVIDE_LOAN
@@ -25,67 +26,75 @@ class ProvideLoanService(BaseService):
dict: A standardized response. dict: A standardized response.
""" """
try: try:
validated_data = ProvideLoanService.validate_data(data, ProvideLoanSchema()) with db.session.begin():
account_id = validated_data.get('accountId') validated_data = ProvideLoanService.validate_data(data, ProvideLoanSchema())
customer_id = validated_data.get('customerId') account_id = validated_data.get('accountId')
request_id = validated_data.get('requestId') customer_id = validated_data.get('customerId')
transaction_id = validated_data.get('transactionId') request_id = validated_data.get('requestId')
transaction_id = validated_data.get('transactionId')
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)):
# Save the loan details
loan = Loan.create_loan(
customer_id=customer_id,
account_id=account_id,
offer_id=validated_data.get('offerId'),
principal_amount=validated_data.get('requestedAmount'),
status=LoanStatus.ACTIVE
)
if not loan: # Save the loan details
logger.error(f"Failed to save loan details") loan = Loan.create_loan(
customer_id=customer_id,
account_id=account_id,
offer_id=validated_data.get('offerId'),
principal_amount=validated_data.get('requestedAmount'),
status=LoanStatus.ACTIVE
)
if not loan:
logger.error(f"Failed to save loan details")
return jsonify({
"message": "Failed to save loan details."
}), 400
db.session.flush()
validated_data['refId'] = loan.id
validated_data['refModel'] = "loan"
# Log Transaction
transaction = ProvideLoanService.log_transaction(validated_data = validated_data)
if not transaction:
logger.error(f"Failed to log transaction")
return jsonify({
"message": "Failed to log transaction."
}), 400
else:
return jsonify({ return jsonify({
"message": "Failed to save loan details." "message": "Invalid Customer or Account"
}), 400 }), 400
# Log Transaction
transaction = ProvideLoanService.log_transaction(validated_data = validated_data)
if not transaction:
logger.error(f"Failed to log transaction")
return jsonify({
"message": "Failed to log transaction."
}), 400
response_data = {
else: "requestId": request_id,
return jsonify({ "transactionId": transaction_id,
"message": "Invalid Customer or Account" "customerId": customer_id,
}), 400 "accountId": account_id,
"msisdn": "3451342",
"resultCode": "00",
"resultDescription": "Successful"
}
response_data = { # KafkaIntegration.send_loan_request(loan_data = response_data, request_id = request_id)
"requestId": request_id, # Call Kafka in a background thread
"transactionId": transaction_id, thread = Thread(target=ProvideLoanService.async_send_to_kafka, args=(response_data, request_id, "PROCESS_PAYMENT"))
"customerId": customer_id, thread.start()
"accountId": account_id,
"msisdn": "3451342",
"resultCode": "00",
"resultDescription": "Successful"
}
db.session.commit()
# KafkaIntegration.send_loan_request(loan_data = response_data, request_id = request_id) return response_data
# Call Kafka in a background thread
thread = Thread(target=ProvideLoanService.async_send_to_kafka, args=(response_data, request_id, "PROCESS_PAYMENT"))
thread.start()
return response_data
except ValidationError as err: except ValidationError as err:
logger.error(f"Validation Error: {getattr(err, 'messages', str(err))}") logger.error(f"Validation Error: {getattr(err, 'messages', str(err))}")
db.session.rollback()
return jsonify({ return jsonify({
"message": "Validation exception" "message": "Validation exception"
@@ -93,6 +102,7 @@ class ProvideLoanService(BaseService):
except ValueError as err: except ValueError as err:
logger.error(f"{getattr(err, 'messages', str(err))}") logger.error(f"{getattr(err, 'messages', str(err))}")
db.session.rollback()
return jsonify({ return jsonify({
"message": str(err) "message": str(err)
@@ -100,6 +110,7 @@ class ProvideLoanService(BaseService):
except Exception as e: except Exception as e:
logger.error(f"An error occurred: {str(e)}", exc_info=True) logger.error(f"An error occurred: {str(e)}", exc_info=True)
db.session.rollback()
return jsonify({ return jsonify({
"message": "Internal Server Error" "message": "Internal Server Error"
}) , 500 }) , 500
+35 -30
View File
@@ -8,6 +8,7 @@ from app.api.schemas.repayment import RepaymentSchema
from app.api.services.base_service import BaseService from app.api.services.base_service import BaseService
from app.api.enums import TransactionType from app.api.enums import TransactionType
from threading import Thread from threading import Thread
from app.extensions import db
class RepaymentService(BaseService): class RepaymentService(BaseService):
TRANSACTION_TYPE = TransactionType.REPAYMENT TRANSACTION_TYPE = TransactionType.REPAYMENT
@@ -24,22 +25,20 @@ class RepaymentService(BaseService):
dict: A standardized response. dict: A standardized response.
""" """
try: try:
validated_data = RepaymentService.validate_data(data, RepaymentSchema()) with db.session.begin():
customer_id = validated_data.get('customerId') validated_data = RepaymentService.validate_data(data, RepaymentSchema())
customer = RepaymentService.get_or_create_customer(validated_data) customer_id = validated_data.get('customerId')
account = customer.accounts[0] request_id = validated_data.get('requestId')
validated_data['accountId'] = account.id loan_id = validated_data.get('debtId')
request_id = validated_data.get('requestId') product_id = validated_data.get('productId')
loan_id = validated_data.get('debtId')
if (RepaymentService.validate_account_ownership(account_id = account.id, customer_id = customer_id)):
# Save the repayment details # Save the repayment details
repayment = Repayment.create_repayment( repayment = Repayment.create_repayment(
customer_id = customer_id, customer_id = customer_id,
loan_id = loan_id, loan_id = loan_id,
product_id = validated_data.get('productId') product_id = product_id
) )
@@ -49,6 +48,11 @@ class RepaymentService(BaseService):
"message": "Failed to save repayment details." "message": "Failed to save repayment details."
}), 400 }), 400
db.session.flush()
validated_data['refId'] = repayment.id
validated_data['refModel'] = "repayment"
#Update Loan status #Update Loan status
Loan.update_status(loan_id = loan_id, status = LoanStatus.REPAID) Loan.update_status(loan_id = loan_id, status = LoanStatus.REPAID)
@@ -59,34 +63,33 @@ class RepaymentService(BaseService):
return jsonify({ return jsonify({
"message": "Failed to log transaction." "message": "Failed to log transaction."
}), 400 }), 400
else:
return jsonify({
"message": "Invalid Customer or Account"
}), 400
# Simulated processing logic
response_data = {
"customerId": "CN621868",
"productId": "101",
"debtId": "273194670",
"resultCode": "00",
"resultDescription": "Successful"
}
# return ResponseHelper.success( # Simulated processing logic
# data=response_data, response_data = {
# message="Repayment processed successfully" "customerId": customer_id,
# ) "productId": product_id,
"debtId": loan_id,
"resultCode": "00",
"resultDescription": "Successful"
}
# Call Kafka in a background thread # return ResponseHelper.success(
thread = Thread(target=RepaymentService.async_send_to_kafka, args=(response_data, request_id, "LOAN_REPAYMENT")) # data=response_data,
thread.start() # message="Repayment processed successfully"
# )
return response_data # Call Kafka in a background thread
thread = Thread(target=RepaymentService.async_send_to_kafka, args=(response_data, request_id, "LOAN_REPAYMENT"))
thread.start()
db.session.commit()
return response_data
except ValidationError as err: except ValidationError as err:
logger.error(f"Validation Error: {getattr(err, 'messages', str(err))}") logger.error(f"Validation Error: {getattr(err, 'messages', str(err))}")
db.session.rollback()
return jsonify({ return jsonify({
"message": "Validation exception" "message": "Validation exception"
@@ -94,6 +97,7 @@ class RepaymentService(BaseService):
except ValueError as err: except ValueError as err:
logger.error(f"{getattr(err, 'messages', str(err))}") logger.error(f"{getattr(err, 'messages', str(err))}")
db.session.rollback()
return jsonify({ return jsonify({
"message": str(err) "message": str(err)
@@ -101,6 +105,7 @@ class RepaymentService(BaseService):
except Exception as e: except Exception as e:
logger.error(f"An error occurred: {str(e)}", exc_info=True) logger.error(f"An error occurred: {str(e)}", exc_info=True)
db.session.rollback()
return jsonify({ return jsonify({
"message": "Internal Server Error" "message": "Internal Server Error"
}) , 500 }) , 500
+53 -53
View File
@@ -4,6 +4,8 @@ from app.api.services.base_service import BaseService
from app.api.enums import TransactionType from app.api.enums import TransactionType
from app.utils.logger import logger from app.utils.logger import logger
from app.api.schemas.select_offer import SelectOfferSchema from app.api.schemas.select_offer import SelectOfferSchema
from app.extensions import db
class SelectOfferService(BaseService): class SelectOfferService(BaseService):
TRANSACTION_TYPE = TransactionType.SELECT_OFFER TRANSACTION_TYPE = TransactionType.SELECT_OFFER
@@ -20,74 +22,72 @@ class SelectOfferService(BaseService):
dict: A standardized response. dict: A standardized response.
""" """
try: try:
validated_data = SelectOfferService.validate_data(data, SelectOfferSchema()) with db.session.begin():
account_id = validated_data.get('accountId') validated_data = SelectOfferService.validate_data(
customer_id = validated_data.get('customerId') data, SelectOfferSchema()
)
account_id = validated_data.get("accountId")
customer_id = validated_data.get("customerId")
if (SelectOfferService.validate_account_ownership(account_id = account_id, customer_id = customer_id)): if SelectOfferService.validate_account_ownership(
transaction = SelectOfferService.log_transaction(validated_data = validated_data) account_id=account_id, customer_id=customer_id
):
transaction = SelectOfferService.log_transaction(
validated_data=validated_data
)
if not transaction: if not transaction:
logger.error(f"Failed to log transaction") logger.error(f"Failed to log transaction")
return jsonify({ return jsonify({"message": "Failed to log transaction."}), 400
"message": "Failed to log transaction." else:
}), 400 return jsonify({"message": "Invalid Customer or Account"}), 400
else:
return jsonify({
"message": "Invalid Customer or Account"
}), 400
offers = [ offers = [
{ {
"offerId": "14451", "offerId": "14451",
"productId": "2030", "productId": "2030",
"amount": 10000.0, "amount": 10000.0,
"upfrontPayment": 1000.0, "upfrontPayment": 1000.0,
"interestRate": 3.0, "interestRate": 3.0,
"managementRate": 1.0, "managementRate": 1.0,
"managementFee": 1.0, "managementFee": 1.0,
"insuranceRate": 1.0, "insuranceRate": 1.0,
"insuranceFee": 100.0, "insuranceFee": 100.0,
"VATRate": 7.5, "VATRate": 7.5,
"VATAmount": 100.0, "VATAmount": 100.0,
"recommendedRepaymentDates": ["2022-11-30"], "recommendedRepaymentDates": ["2022-11-30"],
"installmentAmount": 11000.0, "installmentAmount": 11000.0,
"totalRepaymentAmount": 11000.0 "totalRepaymentAmount": 11000.0,
} }
] ]
# Business logic - selecting an offer # Business logic - selecting an offer
response_data = { response_data = {
"outstandingDebtAmount": 0, "outstandingDebtAmount": 0,
"requestId": "202111170001371256908", "requestId": "202111170001371256908",
"transactionId": transaction.id, "transactionId": transaction.id,
"customerId": customer_id, "customerId": customer_id,
"accountId": account_id, "accountId": account_id,
"loan": offers, "loan": offers,
"resultCode": "00", "resultCode": "00",
"resultDescription": "Successful" "resultDescription": "Successful",
} }
db.session.commit()
return response_data return response_data
except ValidationError as err: except ValidationError as err:
logger.error(f"Validation Error: {getattr(err, 'messages', str(err))}") logger.error(f"Validation Error: {getattr(err, 'messages', str(err))}")
db.session.rollback()
return jsonify({"message": "Validation exception"}), 422
return jsonify({ except ValueError as err:
"message": "Validation exception"
}) , 422
except ValueError as err:
logger.error(f"{getattr(err, 'messages', str(err))}") logger.error(f"{getattr(err, 'messages', str(err))}")
db.session.rollback()
return jsonify({ return jsonify({"message": str(err)}), 400
"message": str(err)
}) , 400
except Exception as e: except Exception as e:
logger.error(f"An error occurred: {str(e)}", exc_info=True) logger.error(f"An error occurred: {str(e)}", exc_info=True)
return jsonify({ db.session.rollback()
"message": "Internal Server Error" return jsonify({"message": "Internal Server Error"}), 500
}) , 500
-2
View File
@@ -31,9 +31,7 @@ class Account(db.Model):
try: try:
db.session.add(account) db.session.add(account)
db.session.commit()
except IntegrityError as err: except IntegrityError as err:
db.session.rollback()
raise ValueError(f"Database integrity error: {err}") raise ValueError(f"Database integrity error: {err}")
return account return account
+18 -2
View File
@@ -20,6 +20,13 @@ class Customer(db.Model):
back_populates="customer", back_populates="customer",
) )
loans = relationship(
"Loan",
primaryjoin="Customer.id == Loan.customer_id",
foreign_keys="Loan.customer_id",
back_populates="customer",
)
@classmethod @classmethod
def is_valid_customer(cls, customer_id): def is_valid_customer(cls, customer_id):
customer = cls.query.filter_by(id=customer_id).first() customer = cls.query.filter_by(id=customer_id).first()
@@ -44,11 +51,20 @@ class Customer(db.Model):
account_type=account_type account_type=account_type
) )
db.session.commit()
except IntegrityError as err: except IntegrityError as err:
db.session.rollback()
raise ValueError(f"Database integrity error: {err}") raise ValueError(f"Database integrity error: {err}")
return customer return customer
@classmethod
def get_customer(cls, customer_id):
"""
Get customer by ID.
"""
customer = cls.query.filter_by(id=customer_id).first()
if not customer:
raise ValueError(f"Customer does not exist")
return customer
def __repr__(self): def __repr__(self):
return f'<Customer {self.id}>' return f'<Customer {self.id}>'
+22 -3
View File
@@ -3,6 +3,8 @@ from app.extensions import db
from app.models.customer import Customer from app.models.customer import Customer
from app.models.account import Account from app.models.account import Account
from sqlalchemy.exc import IntegrityError from sqlalchemy.exc import IntegrityError
from sqlalchemy.orm import relationship
from app.models import Customer
class Loan(db.Model): class Loan(db.Model):
@@ -21,6 +23,12 @@ class Loan(db.Model):
created_at = db.Column(db.DateTime, default=datetime.now(timezone.utc)) 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)) updated_at = db.Column(db.DateTime, default=datetime.now(timezone.utc), onupdate=datetime.now(timezone.utc))
customer = relationship(
"Customer",
primaryjoin="Customer.id == Loan.customer_id",
foreign_keys=[customer_id],
back_populates="loans",
)
@classmethod @classmethod
def create_loan(cls, customer_id, account_id, offer_id, principal_amount, status='pending'): def create_loan(cls, customer_id, account_id, offer_id, principal_amount, status='pending'):
@@ -47,9 +55,7 @@ class Loan(db.Model):
try: try:
db.session.add(loan) db.session.add(loan)
db.session.commit()
except IntegrityError as err: except IntegrityError as err:
db.session.rollback()
raise ValueError(f"Database integrity error: {err}") raise ValueError(f"Database integrity error: {err}")
return loan return loan
@@ -93,7 +99,20 @@ class Loan(db.Model):
# Update loan status and the updated_at timestamp # Update loan status and the updated_at timestamp
loan.status = status loan.status = status
db.session.commit() def to_dict(self):
"""
Convert the Loan object to a dictionary format for JSON serialization.
"""
return {
'id': self.id,
'customer_id': self.customer_id,
'account_id': self.account_id,
'offer_id': self.offer_id,
'principal_amount': self.principal_amount,
'status': self.status,
'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): def __repr__(self):
return f'<Loan {self.id}>' return f'<Loan {self.id}>'
+16
View File
@@ -0,0 +1,16 @@
from datetime import datetime, timezone
from app.extensions import db
class Offer(db.Model):
__tablename__ = 'offers'
id = db.Column(db.Integer, primary_key=True)
product_id = db.Column(db.String, nullable=False)
min_amount = db.Column(db.Float, nullable=False)
max_amount = db.Column(db.Float, nullable=False)
tenor = db.Column(db.Integer, nullable=False)
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))
def __repr__(self):
return f'<LoanOffer {self.id}>'
+1 -3
View File
@@ -38,15 +38,13 @@ class Repayment(db.Model):
repayment = cls( repayment = cls(
customer_id=customer_id, customer_id=customer_id,
loan_id=loan.id, loan_id=loan_id,
product_id=product_id, product_id=product_id,
) )
try: try:
db.session.add(repayment) db.session.add(repayment)
db.session.commit()
except IntegrityError as err: except IntegrityError as err:
db.session.rollback()
raise ValueError(f"Database integrity error: {err}") raise ValueError(f"Database integrity error: {err}")
return repayment return repayment
+2 -2
View File
@@ -1,10 +1,10 @@
services: services:
digifi-core: digifi-first-core:
build: . build: .
env_file: env_file:
- .env - .env
ports: ports:
- "${APP_PORT:-4300}:5000" - "${APP_PORT:-4500}:5000"
environment: environment:
- FLASK_APP=${FLASK_APP} - FLASK_APP=${FLASK_APP}
- FLASK_ENV=${FLASK_ENV} - FLASK_ENV=${FLASK_ENV}
+1
View File
@@ -0,0 +1 @@
Single-database configuration for Flask.
+50
View File
@@ -0,0 +1,50 @@
# A generic, single database configuration.
[alembic]
# template used to generate migration files
# file_template = %%(rev)s_%%(slug)s
# set to 'true' to run the environment during
# the 'revision' command, regardless of autogenerate
# revision_environment = false
# Logging configuration
[loggers]
keys = root,sqlalchemy,alembic,flask_migrate
[handlers]
keys = console
[formatters]
keys = generic
[logger_root]
level = WARN
handlers = console
qualname =
[logger_sqlalchemy]
level = WARN
handlers =
qualname = sqlalchemy.engine
[logger_alembic]
level = INFO
handlers =
qualname = alembic
[logger_flask_migrate]
level = INFO
handlers =
qualname = flask_migrate
[handler_console]
class = StreamHandler
args = (sys.stderr,)
level = NOTSET
formatter = generic
[formatter_generic]
format = %(levelname)-5.5s [%(name)s] %(message)s
datefmt = %H:%M:%S
+113
View File
@@ -0,0 +1,113 @@
import logging
from logging.config import fileConfig
from flask import current_app
from alembic import context
# this is the Alembic Config object, which provides
# access to the values within the .ini file in use.
config = context.config
# Interpret the config file for Python logging.
# This line sets up loggers basically.
fileConfig(config.config_file_name)
logger = logging.getLogger('alembic.env')
def get_engine():
try:
# this works with Flask-SQLAlchemy<3 and Alchemical
return current_app.extensions['migrate'].db.get_engine()
except (TypeError, AttributeError):
# this works with Flask-SQLAlchemy>=3
return current_app.extensions['migrate'].db.engine
def get_engine_url():
try:
return get_engine().url.render_as_string(hide_password=False).replace(
'%', '%%')
except AttributeError:
return str(get_engine().url).replace('%', '%%')
# add your model's MetaData object here
# for 'autogenerate' support
# from myapp import mymodel
# target_metadata = mymodel.Base.metadata
config.set_main_option('sqlalchemy.url', get_engine_url())
target_db = current_app.extensions['migrate'].db
# other values from the config, defined by the needs of env.py,
# can be acquired:
# my_important_option = config.get_main_option("my_important_option")
# ... etc.
def get_metadata():
if hasattr(target_db, 'metadatas'):
return target_db.metadatas[None]
return target_db.metadata
def run_migrations_offline():
"""Run migrations in 'offline' mode.
This configures the context with just a URL
and not an Engine, though an Engine is acceptable
here as well. By skipping the Engine creation
we don't even need a DBAPI to be available.
Calls to context.execute() here emit the given string to the
script output.
"""
url = config.get_main_option("sqlalchemy.url")
context.configure(
url=url, target_metadata=get_metadata(), literal_binds=True
)
with context.begin_transaction():
context.run_migrations()
def run_migrations_online():
"""Run migrations in 'online' mode.
In this scenario we need to create an Engine
and associate a connection with the context.
"""
# this callback is used to prevent an auto-migration from being generated
# when there are no changes to the schema
# reference: http://alembic.zzzcomputing.com/en/latest/cookbook.html
def process_revision_directives(context, revision, directives):
if getattr(config.cmd_opts, 'autogenerate', False):
script = directives[0]
if script.upgrade_ops.is_empty():
directives[:] = []
logger.info('No changes in schema detected.')
conf_args = current_app.extensions['migrate'].configure_args
if conf_args.get("process_revision_directives") is None:
conf_args["process_revision_directives"] = process_revision_directives
connectable = get_engine()
with connectable.connect() as connection:
context.configure(
connection=connection,
target_metadata=get_metadata(),
**conf_args
)
with context.begin_transaction():
context.run_migrations()
if context.is_offline_mode():
run_migrations_offline()
else:
run_migrations_online()
+24
View File
@@ -0,0 +1,24 @@
"""${message}
Revision ID: ${up_revision}
Revises: ${down_revision | comma,n}
Create Date: ${create_date}
"""
from alembic import op
import sqlalchemy as sa
${imports if imports else ""}
# revision identifiers, used by Alembic.
revision = ${repr(up_revision)}
down_revision = ${repr(down_revision)}
branch_labels = ${repr(branch_labels)}
depends_on = ${repr(depends_on)}
def upgrade():
${upgrades if upgrades else "pass"}
def downgrade():
${downgrades if downgrades else "pass"}
@@ -0,0 +1,32 @@
"""Migration on Thu Apr 10 21:50:01 UTC 2025
Revision ID: 1340e7e578b9
Revises: b8f6fd76ead8
Create Date: 2025-04-10 21:50:32.113149
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = '1340e7e578b9'
down_revision = 'b8f6fd76ead8'
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
with op.batch_alter_table('transactions', schema=None) as batch_op:
batch_op.add_column(sa.Column('ref_model', sa.String(length=50), nullable=True))
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
with op.batch_alter_table('transactions', schema=None) as batch_op:
batch_op.drop_column('ref_model')
# ### end Alembic commands ###
@@ -0,0 +1,86 @@
"""Migration on Thu Apr 10 16:21:45 UTC 2025
Revision ID: b8f6fd76ead8
Revises:
Create Date: 2025-04-10 16:22:15.946157
"""
from alembic import op
import sqlalchemy as sa
from sqlalchemy.dialects import postgresql
# revision identifiers, used by Alembic.
revision = 'b8f6fd76ead8'
down_revision = None
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.create_table('repayments',
sa.Column('id', sa.Integer(), autoincrement=True, nullable=False),
sa.Column('loan_id', sa.String(length=50), nullable=False),
sa.Column('customer_id', sa.String(length=50), nullable=False),
sa.Column('product_id', sa.String(length=20), nullable=True),
sa.Column('created_at', sa.DateTime(), nullable=True),
sa.Column('updated_at', sa.DateTime(), nullable=True),
sa.PrimaryKeyConstraint('id')
)
with op.batch_alter_table('loans', schema=None) as batch_op:
batch_op.alter_column('id',
existing_type=sa.VARCHAR(length=50),
type_=sa.Integer(),
existing_nullable=False,
autoincrement=True,
existing_server_default=sa.text("nextval('loan_id_seq'::regclass)"))
with op.batch_alter_table('transactions', schema=None) as batch_op:
batch_op.alter_column('channel',
existing_type=sa.VARCHAR(length=8),
type_=sa.String(length=50),
existing_nullable=False)
batch_op.alter_column('created_at',
existing_type=postgresql.TIMESTAMP(timezone=True),
type_=sa.DateTime(),
existing_nullable=True,
existing_server_default=sa.text('now()'))
batch_op.alter_column('updated_at',
existing_type=postgresql.TIMESTAMP(timezone=True),
type_=sa.DateTime(),
existing_nullable=True,
existing_server_default=sa.text('now()'))
batch_op.drop_constraint('transactions_id_key', type_='unique')
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
with op.batch_alter_table('transactions', schema=None) as batch_op:
batch_op.create_unique_constraint('transactions_id_key', ['id'])
batch_op.alter_column('updated_at',
existing_type=sa.DateTime(),
type_=postgresql.TIMESTAMP(timezone=True),
existing_nullable=True,
existing_server_default=sa.text('now()'))
batch_op.alter_column('created_at',
existing_type=sa.DateTime(),
type_=postgresql.TIMESTAMP(timezone=True),
existing_nullable=True,
existing_server_default=sa.text('now()'))
batch_op.alter_column('channel',
existing_type=sa.String(length=50),
type_=sa.VARCHAR(length=8),
existing_nullable=False)
with op.batch_alter_table('loans', schema=None) as batch_op:
batch_op.alter_column('id',
existing_type=sa.Integer(),
type_=sa.VARCHAR(length=50),
existing_nullable=False,
autoincrement=True,
existing_server_default=sa.text("nextval('loan_id_seq'::regclass)"))
op.drop_table('repayments')
# ### end Alembic commands ###
+8
View File
@@ -0,0 +1,8 @@
#!/bin/sh
echo "Running DB migrations..."
#flask db migrate -m "Migration on $(date)"
#flask db upgrade
echo "Starting Gunicorn server..."
exec gunicorn -w 4 -b 0.0.0.0:5000 wsgi:wsgi_app