Compare commits

...

64 Commits

Author SHA1 Message Date
VivianDee a6e7eaac3c [add]: accountId on loan and repayment 2025-04-14 21:04:17 +01:00
VivianDee 482a860bd2 [add]: account_id to loan status and repayment 2025-04-14 15:26:46 +01:00
VivianDee 6476b62d3c [update]: Loan Status 2025-04-11 15:54:26 +01:00
VivianDee e69335cb71 [add]: transaction fix 2025-04-11 13:15:56 +01:00
VivianDee 8a018545ec Update base_service.py 2025-04-11 00:17:59 +01:00
VivianDee 3031251519 Update base_service.py 2025-04-11 00:17:38 +01:00
VivianDee 1d97304f4e [update]: Loan status 2025-04-11 00:14:02 +01:00
VivianDee 9f9512b060 [add]: loan status 2025-04-11 00:02:35 +01:00
VivianDee d397c834f4 [add]: db sessions 2025-04-10 23:45:40 +01:00
VivianDee a196d4d3c4 [add]: ref_id and ref_model for transactions. And db session and rollback for transactions 2025-04-10 23:06:51 +01:00
VivianDee e5320c075e [add]: Update loan status after repayment 2025-04-10 18:35:26 +01:00
VivianDee 1081467f6f [add]: DB migrations fix and Save loan repayment details 2025-04-10 17:50:25 +01:00
VivianDee f252e33be2 [add]: Loan repayment event 2025-04-10 17:15:19 +01:00
VivianDee 6f15ae97f4 Merge branch 'master' of https://gitlab.chiefsoft.net/DigiFi/digifi-BankToProductCore 2025-04-10 17:03:24 +01:00
VivianDee 7cea5390c0 [add]: loan_repayment event 2025-04-10 17:02:54 +01:00
ameye 729ebd5d08 Merge branch 'loan_repayment_fix' of DigiFi/digifi-BankToProductCore into master 2025-04-10 13:07:36 +00:00
VivianDee 7026c8378b [fix]: Loan id 2025-04-10 13:23:47 +01:00
VivianDee b22d2f358e [fix]: Removed out product and lien amount 2025-04-10 12:58:30 +01:00
VivianDee f13ca508c2 Update provide_loan.py 2025-04-10 12:40:19 +01:00
VivianDee 87f1fa2152 [fix]: save loan 2025-04-10 12:35:46 +01:00
VivianDee 2c34c16c34 [fix]: Missing account id 2025-04-10 12:27:57 +01:00
VivianDee 1b973322c9 [add]: Loan details and repayment fix 2025-04-10 12:20:57 +01:00
CHIEFSOFT\ameye edd19b9a39 SQL Storage 2025-04-09 05:41:33 -04:00
CHIEFSOFT\ameye 773dedcd5b Arrange swagger 2025-04-07 15:29:29 -04:00
CHIEFSOFT\ameye eb4bcb025d Adjusted swagger file 2025-04-07 15:27:01 -04:00
CHIEFSOFT\ameye 37d808c144 offers fix 2025-04-07 14:02:17 -04:00
CHIEFSOFT\ameye 1f86d30c3a msisdn 2025-04-07 13:27:08 -04:00
CHIEFSOFT\ameye 946818dae6 Transaction ID added to return 2025-04-07 13:24:07 -04:00
CHIEFSOFT\ameye 333e48d32a Filters issues 2025-04-06 20:55:57 -04:00
CHIEFSOFT\ameye 6e24b498dc from sqlalchemy import and_, or_, not_ 2025-04-06 20:49:02 -04:00
CHIEFSOFT\ameye cbf57f4bfb Multiple filter 2025-04-06 20:46:25 -04:00
CHIEFSOFT\ameye bae92d36c6 transaction_id 2025-04-06 00:58:57 -04:00
CHIEFSOFT\ameye dd2da9c462 fix data 2025-04-06 00:53:14 -04:00
CHIEFSOFT\ameye f20563a9b1 transaction.id 2025-04-06 00:32:46 -04:00
CHIEFSOFT\ameye 85671081ec transaction_id 2025-04-06 00:28:44 -04:00
CHIEFSOFT\ameye 53cff748ed Return transactuin is 2025-04-06 00:18:12 -04:00
CHIEFSOFT\ameye 3b098969e2 removed migratin file 2025-04-06 00:04:29 -04:00
CHIEFSOFT\ameye 751bcb7d28 db.create_all() 2025-04-06 00:00:52 -04:00
CHIEFSOFT\ameye 25ccb240ca Fixed serialm 2025-04-05 23:55:05 -04:00
CHIEFSOFT\ameye de4554e247 reverst 2025-04-05 23:51:53 -04:00
CHIEFSOFT\ameye e1b3f4930e transactions 2025-04-05 23:36:52 -04:00
CHIEFSOFT\ameye 900d7b509c identity column 2025-04-05 23:33:51 -04:00
CHIEFSOFT\ameye 31ffbe686a table change 2025-04-05 23:30:48 -04:00
CHIEFSOFT\ameye 7b0d19c0e0 Review error state - error in test script 2025-04-05 22:26:47 -04:00
CHIEFSOFT\ameye 7087254859 test error 2025-04-05 22:23:49 -04:00
CHIEFSOFT\ameye d2d9603914 Check for error is not valid 2025-04-05 21:58:35 -04:00
CHIEFSOFT\ameye f2b5703bcb Seen responses 2025-04-05 21:55:41 -04:00
CHIEFSOFT\ameye 49c3422c94 import json 2025-04-05 21:51:41 -04:00
CHIEFSOFT\ameye 07f1db2050 Json dump result 2025-04-05 21:49:42 -04:00
CHIEFSOFT\ameye 7cbe3aefea result management 2025-04-05 21:44:33 -04:00
CHIEFSOFT\ameye e7ae1f6732 Respose code check 2025-04-05 21:19:50 -04:00
CHIEFSOFT\ameye 30233b7283 test Header for basic auths 2025-04-05 21:14:03 -04:00
CHIEFSOFT\ameye 3a30e0abff VALID_API_KEY 2025-04-05 21:03:16 -04:00
CHIEFSOFT\ameye d3b9708216 auth=auth 2025-04-05 20:59:15 -04:00
CHIEFSOFT\ameye 0e8bc63b4d response tracking to bank emulator - TEST 2025-04-05 19:11:58 -04:00
CHIEFSOFT\ameye cbc9d6363e Tracing rack chck issue 2025-04-05 18:59:35 -04:00
CHIEFSOFT\ameye 24fe303304 Missing on Api CALL - lienAmount 2025-04-05 18:47:32 -04:00
ameye 681ac19028 Merge branch 'vivian-update-01' of DigiFi/digifi-BankToProductCore into master 2025-04-04 21:48:47 +00:00
VivianDee d2ffe1c439 Merge branch 'bank-simbrella-calls' into vivian-update-01 2025-04-04 09:56:37 +01:00
VivianDee 12fc6d0ab6 Update .gitignore 2025-04-04 09:40:21 +01:00
VivianDee 04e5548a4a Create kafka.py 2025-04-04 09:39:36 +01:00
CHIEFSOFT\ameye 823ae9e2a7 Added uRL 2025-04-03 19:10:40 -04:00
VivianDee 79ac972841 [add]: threading for Kafa integration 2025-04-03 12:35:50 +01:00
VivianDee b180f8411d [add]: Kafka Integration 2025-04-03 10:49:21 +01:00
52 changed files with 1094 additions and 367 deletions
+2 -1
View File
@@ -4,4 +4,5 @@ app.log
.DS_Store
migrations/__pycache__/
migrations/*.pycg
./vscode
./vscode
.vscode/settings.json
+24 -24
View File
@@ -1,24 +1,24 @@
{
"editor.lineNumbers": "off",
"editor.padding.top": 3,
"editor.padding.bottom": 3,
"editor.formatOnSave": true,
"editor.formatOnPaste": true,
"editor.fontSize": 14,
"editor.lineHeight": 4.5,
"editor.suggestFontSize": 15,
// "editor.suggestLineHeight": 4,
"breadcrumbs.enabled": false,
"workbench.tips.enabled": false,
"workbench.statusBar.visible": false,
// "workbench.editor.showTabs": "single",
"git.enableSmartCommit": true,
"workbench.editor.editorActionsLocation": "hidden",
// "workbench.activityBar.location": "hidden",
"workbench.editor.enablePreviewFromQuickOpen": false,
"editor.lightbulb.enabled": "off",
"editor.selectionHighlight": false,
"editor.overviewRulerBorder": false,
"editor.renderLineHighlight": "none",
"editor.occurrencesHighlight": "off"
}
// {
// "editor.lineNumbers": "off",
// "editor.padding.top": 3,
// "editor.padding.bottom": 3,
// "editor.formatOnSave": true,
// "editor.formatOnPaste": true,
// "editor.fontSize": 14,
// "editor.lineHeight": 4.5,
// "editor.suggestFontSize": 15,
// // "editor.suggestLineHeight": 4,
// "breadcrumbs.enabled": false,
// "workbench.tips.enabled": false,
// "workbench.statusBar.visible": false,
// // "workbench.editor.showTabs": "single",
// "git.enableSmartCommit": true,
// "workbench.editor.editorActionsLocation": "hidden",
// // "workbench.activityBar.location": "hidden",
// "workbench.editor.enablePreviewFromQuickOpen": false,
// "editor.lightbulb.enabled": "off",
// "editor.selectionHighlight": false,
// "editor.overviewRulerBorder": false,
// "editor.renderLineHighlight": "none",
// "editor.occurrencesHighlight": "off"
// }
+14
View File
@@ -0,0 +1,14 @@
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);
+1
View File
@@ -26,6 +26,7 @@ def create_app():
CORS(app)
JWTManager(app)
CORS(app, supports_credentials=True)
# Swagger Doc
SWAGGER_URL = app.config.get("SWAGGER_URL")
+2 -1
View File
@@ -1 +1,2 @@
from .transaction_type import TransactionType
from .transaction_type import TransactionType
from .loan_status import LoanStatus
+6
View File
@@ -0,0 +1,6 @@
from enum import Enum
class LoanStatus(str, Enum):
PENDING = "pending"
ACTIVE = "active"
REPAID = "repaid"
+2 -1
View File
@@ -1 +1,2 @@
from .simbrella import SimbrellaClient
from .simbrella import SimbrellaIntegration
from .kafka import KafkaIntegration
+82
View File
@@ -0,0 +1,82 @@
from confluent_kafka import Producer
import json
import logging
from app.config import settings
logger = logging.getLogger(__name__)
class KafkaIntegration:
_producer = None
_config = {
"bootstrap.servers": settings.KAFKA_BROKER,
"client.id": "loan-service-producer",
"acks": "all",
"retries": 3,
"debug": "broker,topic,msg",
}
@staticmethod
def _get_producer():
"""Kafka producer"""
if not KafkaIntegration._producer:
KafkaIntegration._producer = Producer(KafkaIntegration._config)
logger.info(
f"Connected to Kafka broker at {KafkaIntegration._config['bootstrap.servers']}"
)
return KafkaIntegration._producer
@staticmethod
def delivery_report(err, msg):
"""Called once for each message produced"""
if err is not None:
logger.error(f'Message delivery failed: {err}')
raise RuntimeError(f"Message delivery failed: {err}")
else:
logger.debug(f'Message delivered to {msg.topic()} [{msg.partition()}] @ offset {msg.offset()}')
@staticmethod
def send_loan_request(loan_data, request_id, topic):
"""
Send loan request to topic
Args:
loan_data: Loan request payload as dict
request_id: Unique request identifier (used as Kafka key)
"""
try:
# Proceed to send loan request to Kafka
producer = KafkaIntegration._get_producer()
# Sending loan request message to Kafka
producer.produce(
topic=topic,
key=str(request_id),
value=json.dumps(loan_data).encode("utf-8"),
callback=KafkaIntegration.delivery_report,
)
producer.poll(0)
logger.info(f"Loan request {request_id} queued for processing")
except Exception as e:
logger.error(
f"Failed to send loan request to Kafka: {str(e)}", exc_info=True
)
raise Exception(f"Failed to send loan request to Kafka: {str(e)}")
@staticmethod
def flush():
"""Shutdown"""
producer = KafkaIntegration._get_producer()
producer.flush()
+16 -7
View File
@@ -1,8 +1,10 @@
import requests
import json
from requests.auth import HTTPBasicAuth
from app.utils.logger import logger
from app.config import settings
class SimbrellaClient:
class SimbrellaIntegration:
BASE_URL = settings.SIMBRELLA_BASE_URL
@staticmethod
@@ -10,7 +12,7 @@ class SimbrellaClient:
"""
Calls the RACCheck endpoit
"""
url = f"{SimbrellaClient.BASE_URL}/RACCheck"
url = f"{SimbrellaIntegration.BASE_URL}/RACCheck"
payload = {
"customerId": customer_id,
@@ -33,14 +35,21 @@ class SimbrellaClient:
]
}
logger.error(f"This is PayLoad: {str(payload)}",exc_info=True)
headers = {
'Content-Type': 'application/json',
'x-api-key': f'{settings.VALID_API_KEY}',
'App-Id': f'{settings.VALID_APP_ID}'
}
try:
response = requests.post(url, json=payload, timeout=10)
response = requests.post(url, json=payload, timeout=10, headers=headers)
logger.error(f"This is Response: {str(response)}", exc_info=True)
# Raise an error for non-200 responses
# response.raise_for_status()
if response.status_code != 200:
response.raise_for_status()
return response.json()
except requests.exceptions.RequestException as err:
logger.error(f"RACCheck API call failed: {str(err)}", exc_info=True)
return {"error": "RACCheck API error", "details": str(err)}
return {"error": "RACCheck API error"}
+2 -2
View File
@@ -15,12 +15,12 @@ def require_app_id(f):
if not app_id:
logger.error("Unauthorized access: Missing App-ID.")
return jsonify({"message": "Invalid request parameters"}), 400
return jsonify({"message": "Invalid request"}), 400
if app_id != VALID_APP_ID:
logger.error(f"Unauthorized access: Invalid App-ID {app_id}.")
return jsonify({"message": "Invalid request parameters"}), 400
return jsonify({"message": "Invalid request"}), 400
return f(*args, **kwargs)
+1 -1
View File
@@ -11,7 +11,7 @@ def require_auth(f):
def decorated(*args, **kwargs):
auth = request.headers.get('Authorization')
if not auth or not check_auth(auth):
return jsonify({"message": "Invalid request parameters"}), 401
return jsonify({"message": "Invalid request"}), 401
return f(*args, **kwargs)
return decorated
+1 -1
View File
@@ -4,4 +4,4 @@ from flask import request, jsonify
def enforce_json():
"""Middleware to enforce JSON Content-Type for incoming requests"""
if request.method in ["POST", "PUT", "PATCH"] and request.content_type != "application/json":
return jsonify({"message": "Invalid request parameters"}), 400
return jsonify({"message": "Invalid request"}), 400
+2 -2
View File
@@ -14,11 +14,11 @@ def require_api_key(f):
if not api_key:
logger.error("Unauthorized access: Missing API key.")
return jsonify({"message": "Invalid request parameters"}), 400
return jsonify({"message": "Invalid request"}), 400
if api_key != VALID_API_KEY:
logger.error("Unauthorized access: Invalid API key.")
return jsonify({"message": "Invalid request parameters"}), 400
return jsonify({"message": "Invalid request"}), 400
return f(*args, **kwargs)
+1
View File
@@ -3,6 +3,7 @@ from marshmallow import Schema, fields
# Loan Information Schema
class LoanStatusSchema(Schema):
transactionId = fields.Str(required=True)
accountId = fields.Str(required=True)
customerId = fields.Str(required=True)
msisdn = fields.Str(required=False)
channel = fields.Str(required=True)
+2 -2
View File
@@ -8,8 +8,8 @@ class ProvideLoanSchema(Schema):
customerId = fields.Str(required=True)
accountId = fields.Str(required=True)
msisdn = fields.Str(required=False)
productId = fields.Str(required=True)
lienAmount = fields.Float(required=True)
# productId = fields.Str(required=True)
# lienAmount = fields.Float(required=True)
requestedAmount = fields.Float(required=True)
collectionType = fields.Int(required=True)
offerId = fields.Int(required=True)
+1
View File
@@ -7,5 +7,6 @@ class RepaymentSchema(Schema):
debtId = fields.Str(required=True)
productId = fields.Str(required=True)
transactionId = fields.Str(required=True)
accountId = fields.Str(required=True)
customerId = fields.Str(required=True)
channel = fields.Str(required=True)
+11 -4
View File
@@ -3,6 +3,7 @@ from app.api.enums import TransactionType
from flask import jsonify
from marshmallow import ValidationError
import logging
from app.api.integrations import KafkaIntegration
logger = logging.getLogger(__name__)
@@ -48,8 +49,14 @@ class BaseService:
Create a new transaction.
"""
return Transaction.create_transaction(
id=validated_data.get("transactionId"),
account_id=validated_data.get("accountId"),
type=cls.TRANSACTION_TYPE,
channel=validated_data.get("channel"),
transaction_id = validated_data.get("transactionId"),
customer_id = validated_data.get('customerId', None),
account_id = validated_data.get("accountId", None),
type = cls.TRANSACTION_TYPE,
channel = validated_data.get("channel"),
)
@classmethod
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()
+29 -23
View File
@@ -4,7 +4,8 @@ from marshmallow import ValidationError
from app.utils.logger import logger
from app.api.schemas.customer_consent import CustomerConsentSchema
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):
@@ -22,36 +23,39 @@ class CustomerConsentService(BaseService):
dict: A standardized response.
"""
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())
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)
if(CustomerConsentService.validate_account_ownership(account_id = account_id, customer_id = customer_id)):
transaction = CustomerConsentService.log_transaction(validated_data = validated_data)
if not transaction:
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({
"message": "Failed to log transaction."
}), 400
else:
return jsonify({
"message": "Invalid Customer or Account"
}), 400
"message": "Invalid Customer or Account"
}), 400
# Simulated processing logic
response_data = {
"resultCode": "00",
"resultDescription": "Request is received"
}
# Simulated processing logic
response_data = {
"resultCode": "00",
"resultDescription": "Request is received"
}
return response_data
db.session.commit()
return response_data
except ValidationError as err:
logger.error(f"Validation Error: {getattr(err, 'messages', str(err))}")
db.session.rollback()
return jsonify({
"message": "Validation exception"
@@ -59,6 +63,7 @@ class CustomerConsentService(BaseService):
except ValueError as err:
logger.error(f"{getattr(err, 'messages', str(err))}")
db.session.rollback()
return jsonify({
"message": str(err)
@@ -66,6 +71,7 @@ class CustomerConsentService(BaseService):
except Exception as e:
logger.error(f"An error occurred: {str(e)}", exc_info=True)
db.session.rollback()
return jsonify({
"message": "Internal Server Error"
}) , 500
+62 -54
View File
@@ -4,7 +4,8 @@ from app.api.services.base_service import BaseService
from app.api.schemas.eligibility_check import EligibilityCheckSchema
from marshmallow import ValidationError
from app.api.enums import TransactionType
from app.api.integrations import SimbrellaClient
from app.api.integrations import SimbrellaIntegration
from app.extensions import db
class EligibilityCheckService(BaseService):
TRANSACTION_TYPE = TransactionType.ELIGIBILITY_CHECK
@@ -21,68 +22,75 @@ class EligibilityCheckService(BaseService):
dict: A standardized response.
"""
try:
with db.session.begin():
validated_data = EligibilityCheckService.validate_data(data, EligibilityCheckSchema())
account_id = validated_data.get('accountId')
customer_id = validated_data.get('customerId')
validated_data = EligibilityCheckService.validate_data(data, EligibilityCheckSchema())
account_id = validated_data.get('accountId')
customer_id = validated_data.get('customerId')
transactionId = validated_data.get('transactionId')
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)):
transaction = EligibilityCheckService.log_transaction(validated_data = validated_data)
if (EligibilityCheckService.validate_account_ownership(account_id = account_id, customer_id = customer_id)):
transaction = EligibilityCheckService.log_transaction(validated_data = validated_data)
if not transaction:
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({
"message": "Failed to log transaction."
}), 400
else:
return jsonify({
"message": "Invalid Customer or Account"
}), 400
# Call RACCheck
response = SimbrellaClient.rac_check(
customer_id = customer_id,
account_id = account_id,
transaction_id = transaction.id,
)
"message": "Invalid Customer or Account"
}), 400
# Call RACCheck
response = SimbrellaIntegration.rac_check(
customer_id = customer_id,
account_id = account_id,
transaction_id = transaction.id,
)
logger.error(f"This is Response Returned ****** : {str(response)}")
if "error" in response or response.get("status") != 200:
return jsonify({"message": "RACCheck failed", "error": response.get("message", response)}), 400
# this chck for error is not valid
logger.error(f"Check for ERROR is not valid ****** FIX THIS !!!!!")
#if "error" in response or response.get("status") != 200:
# return jsonify({"message": "RACCheck failed"}), 400
offers = [
{
"offerId": "SAL90",
"productId": "2030",
"minAmount": 5000,
"maxAmount": 100000,
"tenor": 30
},
{
"offerId": "SAL30",
"productId": "2090",
"minAmount": 3000,
"maxAmount": 500000,
"tenor": 90
}
]
offers = [
{
"offerId": "Offer1",
"productId": "Product1",
"minAamount": 100,
"maxAamount": 1000,
"tenor": 12
},
{
"offerId": "Offer2",
"productId": "Product2",
"minAamount": 200,
"maxAamount": 2000,
"tenor": 24
}
]
# Simulate processing
response_data = {
"customerId": customer_id,
"transactionId": transactionId,
"countryCode": "NG",
"msisdn": msisdn,
"eligibleOffers": offers,
"resultDescription": "Successful",
"resultCode": "00",
"accountId": account_id
}
# Simulate processing
response_data = {
"customerId": "CN621868",
"transactionId": "TX12345",
"countryCode": "NG",
"msisdn": "3451342",
"eligibleOffers": offers,
"resultDescription": "Successful",
"resultCode": "00",
"accountId": "ACN8263457"
}
return response_data
return response_data
except ValidationError as err:
logger.error(f"Validation Error: {getattr(err, 'messages', str(err))}")
+59 -38
View File
@@ -1,9 +1,12 @@
from flask import request, jsonify
from marshmallow import ValidationError
from app.api.enums.loan_status import LoanStatus
from app.models import Customer
from app.utils.logger import logger
from app.api.schemas.loan_status import LoanStatusSchema
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):
@@ -21,53 +24,69 @@ class LoanStatusService(BaseService):
dict: A standardized response.
"""
try:
validated_data = LoanStatusService.validate_data(data, LoanStatusSchema())
account_id = validated_data.get('accountId')
customer_id = validated_data.get('customerId')
with db.session.begin():
# Validate data
validated_data = LoanStatusService.validate_data(data, LoanStatusSchema())
if (LoanStatusService.validate_account_ownership(account_id = account_id, customer_id = customer_id)):
transaction = LoanStatusService.log_transaction(validated_data = validated_data)
if not transaction:
logger.error(f"Failed to log transaction")
customer_id = validated_data.get('customerId')
customer = Customer.get_customer(customer_id)
transactionId = validated_data.get('transactionId')
account_id = validated_data.get('accountId')
if(LoanStatusService.validate_account_ownership(account_id = account_id, customer_id = customer_id)):
# Get loans
loans = [loan.to_dict() for loan in customer.loans if loan.status == LoanStatus.ACTIVE]
transaction = LoanStatusService.log_transaction(validated_data = validated_data)
if not transaction:
logger.error(f"Failed to log transaction")
return jsonify({
"message": "Failed to log transaction."
}), 400
else:
return jsonify({
"message": "Failed to log transaction."
}), 400
else:
return jsonify({
"message": "Invalid Customer or Account"
}), 400
"message": "Invalid Customer or Account"
}), 400
loans = [
{
"debtId": "123456789",
"loanDate": "2019-10-18 14:26:21.063",
"dueDate": "2019-11-20 14:26:21.063",
"currentLoanAmount": 8500,
"initialLoanAmount": 10000,
"defaultPenaltyFee": 0,
"continuousFee": 0,
"productId": "101"
# loans = [
# {
# "debtId": "123456789",
# "loanDate": "2019-10-18 14:26:21.063",
# "dueDate": "2019-11-20 14:26:21.063",
# "currentLoanAmount": 8500,
# "initialLoanAmount": 10000,
# "defaultPenaltyFee": 0,
# "continuousFee": 0,
# "productId": "101"
# }
# ]
total_debt_amount = sum(
loan.get("currentLoanAmount") or 0
for loan in loans
)
# Simulated processing logic
response_data = {
"customerId": customer_id,
"transactionId": transactionId,
"loans": loans,
"totalDebtAmount": total_debt_amount,
"resultCode": "00",
"resultDescription": "Successful"
}
]
# Simulated processing logic
response_data = {
"customerId": "CN621868",
"transactionId": "Tr201712RK9232P115",
"loans": loans,
"totalDebtAmount": 8500,
"resultCode": "00",
"resultDescription": "Successful"
}
return response_data
db.session.commit()
return response_data
except ValidationError as err:
logger.error(f"Validation Error: {getattr(err, 'messages', str(err))}")
db.session.rollback()
return jsonify({
"message": "Validation exception"
@@ -75,6 +94,7 @@ class LoanStatusService(BaseService):
except ValueError as err:
logger.error(f"{getattr(err, 'messages', str(err))}")
db.session.rollback()
return jsonify({
"message": str(err)
@@ -82,6 +102,7 @@ class LoanStatusService(BaseService):
except Exception as e:
logger.error(f"An error occurred: {str(e)}", exc_info=True)
db.session.rollback()
return jsonify({
"message": "Internal Server Error"
}) , 500
+2 -1
View File
@@ -3,7 +3,8 @@ from marshmallow import ValidationError
from app.api.services.base_service import BaseService
from app.api.enums import TransactionType
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):
TRANSACTION_TYPE = TransactionType.NOTIFICATION_CALLBACK
+71 -27
View File
@@ -1,9 +1,14 @@
from flask import request, jsonify
from marshmallow import ValidationError
from app.api.integrations.kafka import KafkaIntegration
from app.api.services.base_service import BaseService
from app.api.enums import TransactionType
from app.utils.logger import logger
from app.api.schemas.provide_loan import ProvideLoanSchema
from app.api.schemas.provide_loan import ProvideLoanSchema
from threading import Thread
from app.models.loan import Loan
from app.api.enums import LoanStatus
from app.extensions import db
class ProvideLoanService(BaseService):
TRANSACTION_TYPE = TransactionType.PROVIDE_LOAN
@@ -21,41 +26,74 @@ class ProvideLoanService(BaseService):
dict: A standardized response.
"""
try:
validated_data = ProvideLoanService.validate_data(data, ProvideLoanSchema())
account_id = validated_data.get('accountId')
customer_id = validated_data.get('customerId')
with db.session.begin():
validated_data = ProvideLoanService.validate_data(data, ProvideLoanSchema())
account_id = validated_data.get('accountId')
customer_id = validated_data.get('customerId')
request_id = validated_data.get('requestId')
collection_type = validated_data.get('collectionType')
transaction_id = validated_data.get('transactionId')
if (ProvideLoanService.validate_account_ownership(account_id = account_id, customer_id = customer_id)):
transaction = ProvideLoanService.log_transaction(validated_data = validated_data)
if (ProvideLoanService.validate_account_ownership(account_id = account_id, customer_id = customer_id)):
if not transaction:
logger.error(f"Failed to log transaction")
# Save the loan details
loan = Loan.create_loan(
customer_id = customer_id,
account_id = account_id,
offer_id = validated_data.get('offerId'),
collection_type = collection_type,
transaction_id = validated_data.get('transactionId'),
initial_loan_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
# 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({
"message": "Failed to log transaction."
}), 400
else:
return jsonify({
"message": "Invalid Customer or Account"
}), 400
response_data = {
"requestId": "202111170001371256908",
"transactionId": "Tr201712RK9232P115",
"customerId": "CN621868",
"accountId": "ACN8263457",
"msisdn": "3451342",
"resultCode": "00",
"resultDescription": "Successful"
}
"message": "Invalid Customer or Account"
}), 400
response_data = {
"requestId": request_id,
"transactionId": transaction_id,
"customerId": customer_id,
"accountId": account_id,
"msisdn": "3451342",
"resultCode": "00",
"resultDescription": "Successful"
}
# KafkaIntegration.send_loan_request(loan_data = response_data, request_id = request_id)
# 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
db.session.commit()
return response_data
except ValidationError as err:
logger.error(f"Validation Error: {getattr(err, 'messages', str(err))}")
db.session.rollback()
return jsonify({
"message": "Validation exception"
@@ -63,6 +101,7 @@ class ProvideLoanService(BaseService):
except ValueError as err:
logger.error(f"{getattr(err, 'messages', str(err))}")
db.session.rollback()
return jsonify({
"message": str(err)
@@ -70,6 +109,11 @@ class ProvideLoanService(BaseService):
except Exception as e:
logger.error(f"An error occurred: {str(e)}", exc_info=True)
db.session.rollback()
return jsonify({
"message": "Internal Server Error"
}) , 500
}) , 500
+67 -27
View File
@@ -1,9 +1,15 @@
from flask import request, jsonify
from marshmallow import ValidationError
from app.api.enums.loan_status import LoanStatus
from app.models import Repayment
from app.models.customer import Customer
from app.models.loan import Loan
from app.utils.logger import logger
from app.api.schemas.repayment import RepaymentSchema
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 app.extensions import db
class RepaymentService(BaseService):
TRANSACTION_TYPE = TransactionType.REPAYMENT
@@ -20,42 +26,74 @@ class RepaymentService(BaseService):
dict: A standardized response.
"""
try:
validated_data = RepaymentService.validate_data(data, RepaymentSchema())
account_id = validated_data.get('accountId')
customer_id = validated_data.get('customerId')
with db.session.begin():
validated_data = RepaymentService.validate_data(data, RepaymentSchema())
customer_id = validated_data.get('customerId')
request_id = validated_data.get('requestId')
loan_id = validated_data.get('debtId')
product_id = validated_data.get('productId')
account_id = validated_data.get('accountId')
customer = Customer.get_customer(customer_id)
if(RepaymentService.validate_account_ownership(account_id = account_id, customer_id = customer_id)):
if (RepaymentService.validate_account_ownership(account_id = account_id, customer_id = customer_id)):
transaction = RepaymentService.log_transaction(validated_data = validated_data)
# Save the repayment details
repayment = Repayment.create_repayment(
customer_id = customer_id,
loan_id = loan_id,
product_id = product_id
if not transaction:
logger.error(f"Failed to log transaction")
)
if not repayment:
logger.error(f"Failed to save repayment details")
return jsonify({
"message": "Failed to save repayment details."
}), 400
#Update Loan status
Loan.update_status(loan_id = loan_id, status = LoanStatus.REPAID)
transaction = RepaymentService.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({
"message": "Failed to log transaction."
}), 400
else:
return jsonify({
"message": "Invalid Customer or Account"
}), 400
"message": "Invalid Customer or Account"
}), 400
# Simulated processing logic
response_data = {
"customerId": "CN621868",
"productId": "101",
"debtId": "273194670",
"resultCode": "00",
"resultDescription": "Successful"
}
# return ResponseHelper.success(
# data=response_data,
# message="Repayment processed successfully"
# )
# Simulated processing logic
response_data = {
"customerId": customer_id,
"productId": product_id,
"debtId": loan_id,
"resultCode": "00",
"resultDescription": "Successful"
}
return response_data
# return ResponseHelper.success(
# data=response_data,
# message="Repayment processed successfully"
# )
# 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:
logger.error(f"Validation Error: {getattr(err, 'messages', str(err))}")
db.session.rollback()
return jsonify({
"message": "Validation exception"
@@ -63,6 +101,7 @@ class RepaymentService(BaseService):
except ValueError as err:
logger.error(f"{getattr(err, 'messages', str(err))}")
db.session.rollback()
return jsonify({
"message": str(err)
@@ -70,6 +109,7 @@ class RepaymentService(BaseService):
except Exception as e:
logger.error(f"An error occurred: {str(e)}", exc_info=True)
db.session.rollback()
return jsonify({
"message": "Internal Server Error"
}) , 500
+57 -57
View File
@@ -1,12 +1,14 @@
from flask import request, jsonify
from marshmallow import ValidationError
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.api.schemas.select_offer import SelectOfferSchema
from app.api.schemas.select_offer import SelectOfferSchema
from app.extensions import db
class SelectOfferService(BaseService):
TRANSACTION_TYPE = TransactionType.SELECT_OFFER
TRANSACTION_TYPE = TransactionType.SELECT_OFFER
@staticmethod
def process_request(data):
@@ -20,74 +22,72 @@ class SelectOfferService(BaseService):
dict: A standardized response.
"""
try:
validated_data = SelectOfferService.validate_data(data, SelectOfferSchema())
account_id = validated_data.get('accountId')
customer_id = validated_data.get('customerId')
with db.session.begin():
validated_data = SelectOfferService.validate_data(
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)):
transaction = SelectOfferService.log_transaction(validated_data = validated_data)
if SelectOfferService.validate_account_ownership(
account_id=account_id, customer_id=customer_id
):
transaction = SelectOfferService.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({
"message": "Invalid Customer or Account"
}), 400
offers = [
if not transaction:
logger.error(f"Failed to log transaction")
return jsonify({"message": "Failed to log transaction."}), 400
else:
return jsonify({"message": "Invalid Customer or Account"}), 400
offers = [
{
"offerId": "14451",
"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": "14451",
"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,
}
]
# Business logic - selecting an offer
response_data = {
"outstandingDebtAmount": 0,
"requestId": "202111170001371256908",
"transactionId": "1231231321232",
"customerId": "1256907",
"accountId": "5948306019",
"loan": offers,
"resultCode": "00",
"resultDescription": "Successful"
# Business logic - selecting an offer
response_data = {
"outstandingDebtAmount": 0,
"requestId": "202111170001371256908",
"transactionId": transaction.id,
"customerId": customer_id,
"accountId": account_id,
"loan": offers,
"resultCode": "00",
"resultDescription": "Successful",
}
return response_data
db.session.commit()
return response_data
except ValidationError as err:
logger.error(f"Validation Error: {getattr(err, 'messages', str(err))}")
db.session.rollback()
return jsonify({"message": "Validation exception"}), 422
return jsonify({
"message": "Validation exception"
}) , 422
except ValueError as err:
except ValueError as err:
logger.error(f"{getattr(err, 'messages', str(err))}")
return jsonify({
"message": str(err)
}) , 400
db.session.rollback()
return jsonify({"message": str(err)}), 400
except Exception as e:
logger.error(f"An error occurred: {str(e)}", exc_info=True)
return jsonify({
"message": "Internal Server Error"
}) , 500
db.session.rollback()
return jsonify({"message": "Internal Server Error"}), 500
+3
View File
@@ -24,11 +24,14 @@ class Config:
SQLALCHEMY_TRACK_MODIFICATIONS = False
SIMBRELLA_BASE_URL = os.getenv("SIMBRELLA_BASE_URL", "http://127.0.0.1:6337")
JWT_SECRET_KEY = os.getenv("JWT_SECRET_KEY", "secret-key")
JWT_ACCESS_TOKEN_EXPIRES = os.getenv("JWT_ACCESS_TOKEN_EXPIRES", timedelta(hours=1))
JWT_REFRESH_TOKEN_EXPIRES = os.getenv(
"JWT_REFRESH_TOKEN_EXPIRES", timedelta(days=30)
)
KAFKA_BROKER = 'dev-events.simbrellang.net:9085'
settings = Config()
+2 -1
View File
@@ -2,5 +2,6 @@ from .customer import Customer
from .account import Account
from .loan import Loan
from .transaction import Transaction
from .repayment import Repayment
__all__ = ['Customer', 'Account', 'Loan', 'Transaction']
__all__ = ['Customer', 'Account', 'Loan', 'Transaction', 'Repayment']
+14 -2
View File
@@ -1,5 +1,7 @@
from datetime import datetime, timezone
from sqlalchemy.orm import relationship
from app.extensions import db
from sqlalchemy.exc import IntegrityError
class Account(db.Model):
__tablename__ = 'accounts'
@@ -12,6 +14,13 @@ class Account(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))
customer = relationship(
"Customer",
primaryjoin="Customer.id == Account.customer_id",
foreign_keys=[customer_id],
back_populates="accounts",
)
@classmethod
def create_account(cls, id, customer_id, account_type, status='active'):
account = cls(
@@ -19,8 +28,11 @@ class Account(db.Model):
customer_id=customer_id,
account_type=account_type
)
db.session.add(account)
db.session.commit()
try:
db.session.add(account)
except IntegrityError as err:
raise ValueError(f"Database integrity error: {err}")
return account
@classmethod
+42 -13
View File
@@ -1,6 +1,8 @@
from datetime import datetime, timezone
from sqlalchemy.orm import relationship
from app.extensions import db
from app.models.account import Account
from sqlalchemy.exc import IntegrityError
class Customer(db.Model):
__tablename__ = 'customers'
@@ -11,12 +13,26 @@ class Customer(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))
accounts = relationship(
"Account",
primaryjoin="Customer.id == Account.customer_id",
foreign_keys="Account.customer_id",
back_populates="customer",
)
loans = relationship(
"Loan",
primaryjoin="Customer.id == Loan.customer_id",
foreign_keys="Loan.customer_id",
back_populates="customer",
)
@classmethod
def is_eligible(cls, customer_id):
def is_valid_customer(cls, customer_id):
customer = cls.query.filter_by(id=customer_id).first()
if not customer:
return False, "Customer not found"
return True, "Customer is eligible"
return False
return True
@classmethod
def create_customer(cls, id, msisdn, country_code, account_id, account_type='savings'):
@@ -25,16 +41,29 @@ class Customer(db.Model):
# Create the customer
customer = cls(id=id, msisdn=msisdn, country_code=country_code)
db.session.add(customer)
# Create an associated account
account = Account.create_account(
id=account_id,
customer_id=id,
account_type=account_type
)
db.session.commit()
try:
db.session.add(customer)
# Create an associated account
account = Account.create_account(
id=account_id,
customer_id=id,
account_type=account_type
)
except IntegrityError as err:
raise ValueError(f"Database integrity error: {err}")
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):
+99 -5
View File
@@ -1,19 +1,70 @@
from datetime import datetime, timezone
from app.extensions import db
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 import Customer
class Loan(db.Model):
__tablename__ = 'loans'
id = db.Column(db.String(50), primary_key=True)
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=True)
account_id = db.Column(db.String(50), nullable=False)
product_id = db.Column(db.String(20), nullable=False)
principal_amount = db.Column(db.Float, nullable=False)
offer_id = db.Column(db.String(20), nullable=False)
collection_type = db.Column(db.String(20), nullable=True)
current_loan_amount = db.Column(db.Float, nullable=True)
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)
status = db.Column(db.String(20), default='pending')
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))
customer = relationship(
"Customer",
primaryjoin="Customer.id == Loan.customer_id",
foreign_keys=[customer_id],
back_populates="loans",
)
@classmethod
def create_loan(cls, customer_id, account_id, offer_id, initial_loan_amount, collection_type, transaction_id, status='pending'):
# Check if customer exists
is_valid = Customer.is_valid_customer(customer_id)
if not is_valid:
raise ValueError("Customer does not exist")
now = datetime.now(timezone.utc)
# Create and save the loan
loan = cls(
customer_id = customer_id,
account_id = account_id,
offer_id = offer_id,
collection_type = collection_type,
transaction_id = transaction_id,
initial_loan_amount = initial_loan_amount,
current_loan_amount = initial_loan_amount,
due_date=now,
status = status
)
try:
db.session.add(loan)
except IntegrityError as err:
raise ValueError(f"Database integrity error: {err}")
return loan
@classmethod
def has_active_loans(cls, customer_id):
@@ -23,9 +74,52 @@ class Loan(db.Model):
).count()
if active_loans > 0:
return False, "Customer has active loans"
return True, "No active loans"
return False
return True
@classmethod
def get_customer_loan(cls, loan_id, customer_id):
"""
Get customer's active loans.
"""
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 update_status(cls, loan_id, status):
"""
Update the status of the loan with the given loan_id.
"""
# Retrieve loan
loan = cls.query.get(loan_id)
if not loan:
raise ValueError(f"Loan with ID {loan_id} does not exist.")
if loan.status == status:
return
# Update loan status and the updated_at timestamp
loan.status = status
def to_dict(self):
"""
Convert the Loan object to a dictionary format for JSON serialization.
"""
return {
'debtId': self.id,
'initialLoanAmount': self.initial_loan_amount,
'currentLoanAmount': self.current_loan_amount,
'defaultPenaltyFee': self.default_penalty_fee,
'continuousFee': self.continuous_fee,
'collectionType': self.collection_type,
'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,
}
def __repr__(self):
return f'<Loan {self.id}>'
+53
View File
@@ -0,0 +1,53 @@
from datetime import datetime, timezone
from app.api.enums.loan_status import LoanStatus
from app.extensions import db
from app.models.customer import Customer
from app.models.loan import Loan
from sqlalchemy.exc import IntegrityError
class Repayment(db.Model):
__tablename__ = 'repayments'
id = db.Column(
db.Integer,
primary_key=True,
autoincrement=True,
)
loan_id = db.Column(db.String(50), nullable=False)
customer_id = db.Column(db.String(50), nullable=False)
product_id = db.Column(db.String(20), 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))
@classmethod
def create_repayment(cls, customer_id, loan_id, product_id):
# Check customer exists
if not Customer.is_valid_customer(customer_id):
raise ValueError("Invalid customer")
# Check loan exists
loan = Loan.get_customer_loan(loan_id = loan_id, customer_id = customer_id)
# Check that the loan is active
if loan.status != LoanStatus.ACTIVE:
raise ValueError(f"Repayment cannot be processed. Loan status: ({loan.status})")
repayment = cls(
customer_id=customer_id,
loan_id=loan_id,
product_id=product_id,
)
try:
db.session.add(repayment)
except IntegrityError as err:
raise ValueError(f"Database integrity error: {err}")
return repayment
def __repr__(self):
return f'<Repayment {self.id}>'
+23 -11
View File
@@ -1,12 +1,19 @@
from datetime import datetime, timezone
from app.extensions import db
from app.models import account
from sqlalchemy.exc import IntegrityError
from sqlalchemy import and_, or_, not_
class Transaction(db.Model):
__tablename__ = 'transactions'
id = db.Column(db.String(50), primary_key=True)
account_id = db.Column(db.String(50), nullable=False)
id = db.Column(
db.Integer,
primary_key=True,
autoincrement=True,
)
transaction_id = db.Column(db.String(50), nullable=False)
account_id = db.Column(db.String(50), nullable=True)
customer_id = db.Column(db.String(50), nullable=True)
type = db.Column(db.String(50), nullable=False)
channel = db.Column(db.String(50), nullable=False)
created_at = db.Column(db.DateTime, default=datetime.now(timezone.utc))
@@ -16,22 +23,27 @@ class Transaction(db.Model):
return f'<Transaction {self.id}>'
@classmethod
def create_transaction(cls, id, account_id, type, channel):
if cls.query.filter_by(id=id).first():
def create_transaction(cls, transaction_id, account_id, customer_id, type, channel):
# if cls.query.filter_by(transaction_id=transaction_id).first():
# raise ValueError("Duplicate Transaction")
if cls.query.filter( and_( cls.transaction_id ==transaction_id, cls.type==type) ).first():
raise ValueError("Duplicate Transaction")
transaction = cls(
id=id,
account_id=account_id,
type=type,
channel=channel
transaction_id = transaction_id,
customer_id = customer_id,
account_id = account_id,
type = type,
channel = channel
)
try:
db.session.add(transaction)
db.session.commit()
except IntegrityError as err:
db.session.rollback()
raise ValueError(f"Database integrity error: {err}")
return transaction
+25 -44
View File
@@ -19,9 +19,28 @@
},
{
"url": "http://api.dev.simbrellang.net:4500"
},
{
"url": "https://api.dev.simbrellang.net"
}
],
"tags": [
{
"name": "Authorize",
"description": "This feature will be used for authorizing customers.",
"externalDocs": {
"description": "Find out more",
"url": "https://www.simbrellang.net"
}
},
{
"name": "AuthorizeRefresh",
"description": "This feature will be used for refreshing authorized customers.",
"externalDocs": {
"description": "Find out more",
"url": "https://www.simbrellang.net"
}
},
{
"name": "EligibilityCheck",
"description": "Eligibility Check Request",
@@ -61,41 +80,15 @@
"description": "Find out more",
"url": "https://www.simbrellang.net"
}
},
{
"name": "CustomerConsent",
"description": "CustomerConsent Request.",
"externalDocs": {
"description": "Find out more",
"url": "https://www.simbrellang.net"
}
},
{
"name": "NotificationCallback",
"description": "This new feature will be used for informing Simbrella about status of the transactions that FBN have processed.",
"externalDocs": {
"description": "Find out more",
"url": "https://www.simbrellang.net"
}
},
{
"name": "Authorize",
"description": "This feature will be used for authorizing customers.",
"externalDocs": {
"description": "Find out more",
"url": "https://www.simbrellang.net"
}
},
{
"name": "AuthorizeRefresh",
"description": "This feature will be used for refreshing authorized customers.",
"externalDocs": {
"description": "Find out more",
"url": "https://www.simbrellang.net"
}
}
],
"paths": {
"/Authorize": {
"$ref": "swagger/paths/Authorize.json"
},
"/AuthorizeRefresh": {
"$ref": "swagger/paths/AuthorizeRefresh.json"
},
"/EligibilityCheck": {
"$ref": "swagger/paths/EligibilityCheck.json"
},
@@ -110,18 +103,6 @@
},
"/Repayment": {
"$ref": "swagger/paths/Repayment.json"
},
"/CustomerConsent": {
"$ref": "swagger/paths/CustomerConsent.json"
},
"/NotificationCallback": {
"$ref": "swagger/paths/NotificationCallback.json"
},
"/Authorize": {
"$ref": "swagger/paths/Authorize.json"
},
"/AuthorizeRefresh": {
"$ref": "swagger/paths/AuthorizeRefresh.json"
}
},
"components": {
+1 -1
View File
@@ -43,7 +43,7 @@
}
},
"400": {
"description": "Invalid request parameters"
"description": "Invalid request"
},
"422": {
"description": "Validation exception"
+1 -1
View File
@@ -44,7 +44,7 @@
}
},
"400": {
"description": "Invalid request parameters"
"description": "Invalid request"
},
"422": {
"description": "Validation exception"
+1 -1
View File
@@ -44,7 +44,7 @@
}
},
"400": {
"description": "Invalid request parameters"
"description": "Invalid request"
},
"422": {
"description": "Validation exception"
+1 -1
View File
@@ -44,7 +44,7 @@
}
},
"400": {
"description": "Invalid request parameters"
"description": "Invalid request"
},
"422": {
"description": "Validation exception"
+1 -1
View File
@@ -44,7 +44,7 @@
}
},
"400": {
"description": "Invalid request parameters"
"description": "Invalid request"
},
"422": {
"description": "Validation exception"
+1 -1
View File
@@ -43,7 +43,7 @@
}
},
"400": {
"description": "Invalid request parameters"
"description": "Invalid request"
},
"422": {
"description": "Validation exception"
+1 -1
View File
@@ -44,7 +44,7 @@
}
},
"400": {
"description": "Invalid request parameters"
"description": "Invalid request"
},
"422": {
"description": "Validation exception"
@@ -16,6 +16,10 @@
"channel": {
"type": "string",
"example": "USSD"
},
"accountId": {
"type": "string",
"example": "ACN8263457"
}
},
"xml": {
@@ -21,15 +21,6 @@
"type": "string",
"example": "3451342"
},
"productId": {
"type": "string",
"example": "101"
},
"lienAmount": {
"type": "number",
"format": "decimal",
"example": 400
},
"requestedAmount": {
"type": "number",
"format": "decimal",
+6 -2
View File
@@ -7,7 +7,7 @@
},
"debtId": {
"type": "string",
"example": "273194670"
"example": "10"
},
"productId": {
"type": "string",
@@ -19,11 +19,15 @@
},
"customerId": {
"type": "string",
"example": "CN621868"
"example": "CID0000025585"
},
"channel": {
"type": "string",
"example": "USSD"
},
"accountId": {
"type": "string",
"example": "ACN8263457"
}
},
"xml": {
@@ -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,32 @@
"""Migration on Fri Apr 11 14:15:19 UTC 2025
Revision ID: 610b7e9d15a6
Revises: 9bb0367eb486
Create Date: 2025-04-11 14:16:12.533227
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = '610b7e9d15a6'
down_revision = '9bb0367eb486'
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
with op.batch_alter_table('loans', schema=None) as batch_op:
batch_op.add_column(sa.Column('transaction_id', sa.String(length=50), nullable=True))
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
with op.batch_alter_table('loans', schema=None) as batch_op:
batch_op.drop_column('transaction_id')
# ### end Alembic commands ###
@@ -0,0 +1,32 @@
"""Migration on Mon Apr 14 15:15:05 UTC 2025
Revision ID: 783a023a477f
Revises: f6cd1bfc8832
Create Date: 2025-04-14 15:15:36.991148
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = '783a023a477f'
down_revision = 'f6cd1bfc8832'
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
with op.batch_alter_table('transactions', schema=None) as batch_op:
batch_op.add_column(sa.Column('customer_id', sa.String(length=50), nullable=True))
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
with op.batch_alter_table('transactions', schema=None) as batch_op:
batch_op.drop_column('customer_id')
# ### end Alembic commands ###
@@ -0,0 +1,40 @@
"""Migration on Fri Apr 11 12:48:01 UTC 2025
Revision ID: 9bb0367eb486
Revises: fd447d78b161
Create Date: 2025-04-11 12:48:36.145311
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = '9bb0367eb486'
down_revision = 'fd447d78b161'
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
with op.batch_alter_table('loans', schema=None) as batch_op:
batch_op.add_column(sa.Column('product_id', sa.String(length=20), nullable=True))
batch_op.add_column(sa.Column('current_loan_amount', sa.Float(), nullable=True))
batch_op.add_column(sa.Column('default_penalty_fee', sa.Float(), nullable=True))
batch_op.add_column(sa.Column('continuous_fee', sa.Float(), nullable=True))
batch_op.add_column(sa.Column('due_date', sa.DateTime(), nullable=True))
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
with op.batch_alter_table('loans', schema=None) as batch_op:
batch_op.drop_column('due_date')
batch_op.drop_column('continuous_fee')
batch_op.drop_column('default_penalty_fee')
batch_op.drop_column('current_loan_amount')
batch_op.drop_column('product_id')
# ### end Alembic commands ###
@@ -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 ###
@@ -0,0 +1,34 @@
"""Migration on Fri Apr 11 14:34:36 UTC 2025
Revision ID: f6cd1bfc8832
Revises: 610b7e9d15a6
Create Date: 2025-04-11 14:35:07.093967
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = 'f6cd1bfc8832'
down_revision = '610b7e9d15a6'
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
with op.batch_alter_table('loans', schema=None) as batch_op:
batch_op.add_column(sa.Column('collection_type', sa.String(length=20), nullable=True))
batch_op.drop_column('product_id')
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
with op.batch_alter_table('loans', schema=None) as batch_op:
batch_op.add_column(sa.Column('product_id', sa.VARCHAR(length=20), autoincrement=False, nullable=True))
batch_op.drop_column('collection_type')
# ### end Alembic commands ###
@@ -0,0 +1,38 @@
"""Migration on Fri Apr 11 12:02:45 UTC 2025
Revision ID: fd447d78b161
Revises: 1340e7e578b9
Create Date: 2025-04-11 12:03:28.346671
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = 'fd447d78b161'
down_revision = '1340e7e578b9'
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
with op.batch_alter_table('transactions', schema=None) as batch_op:
batch_op.alter_column('account_id',
existing_type=sa.VARCHAR(length=50),
nullable=True)
batch_op.drop_column('ref_model')
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
with op.batch_alter_table('transactions', schema=None) as batch_op:
batch_op.add_column(sa.Column('ref_model', sa.VARCHAR(length=50), autoincrement=False, nullable=True))
batch_op.alter_column('account_id',
existing_type=sa.VARCHAR(length=50),
nullable=False)
# ### end Alembic commands ###
+4
View File
@@ -30,3 +30,7 @@ requests
# JWT
flask-jwt-extended
# Kafka
confluent-kafka==1.9.2
+1
View File
@@ -1,6 +1,7 @@
#!/bin/sh
echo "Running DB migrations..."
flask db migrate -m "Migration on $(date)"
flask db upgrade
echo "Starting Gunicorn server..."