Initial commit

This commit is contained in:
Azeez Muibi
2025-03-20 13:35:44 +01:00
commit af9a6148a1
48 changed files with 2451 additions and 0 deletions
+6
View File
@@ -0,0 +1,6 @@
DEBUG=True
PORT=5000
API_USERNAME=admin
API_PASSWORD=password
SIMBRELLA_APP_ID=your_app_id
SIMBRELLA_API_KEY=your_api_key
+21
View File
@@ -0,0 +1,21 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="PYTHON_MODULE" version="4">
<component name="Flask">
<option name="enabled" value="true" />
</component>
<component name="NewModuleRootManager">
<content url="file://$MODULE_DIR$">
<excludeFolder url="file://$MODULE_DIR$/.venv" />
</content>
<orderEntry type="jdk" jdkName="Python 3.13 (FirstBankSimbrellaApi)" jdkType="Python SDK" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
<component name="TemplatesService">
<option name="TEMPLATE_CONFIGURATION" value="Jinja2" />
<option name="TEMPLATE_FOLDERS">
<list>
<option value="$MODULE_DIR$/../FirstBankSimbrellaApi\templates" />
</list>
</option>
</component>
</module>
+6
View File
@@ -0,0 +1,6 @@
<component name="InspectionProjectProfileManager">
<settings>
<option name="USE_PROJECT_PROFILE" value="false" />
<version value="1.0" />
</settings>
</component>
+6
View File
@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="Black">
<option name="sdkName" value="Python 3.13 (FirstBankSimbrellaApi)" />
</component>
</project>
+8
View File
@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/.idea/FirstBankSimbrellaApi.iml" filepath="$PROJECT_DIR$/.idea/FirstBankSimbrellaApi.iml" />
</modules>
</component>
</project>
Generated
+6
View File
@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="$PROJECT_DIR$" vcs="Git" />
</component>
</project>
+130
View File
@@ -0,0 +1,130 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="AutoImportSettings">
<option name="autoReloadType" value="SELECTIVE" />
</component>
<component name="ChangeListManager">
<list default="true" id="934fdd1a-f31d-4df5-906a-7da283ff1489" name="Changes" comment="">
<change afterPath="$PROJECT_DIR$/.env" afterDir="false" />
<change afterPath="$PROJECT_DIR$/.idea/FirstBankSimbrellaApi.iml" afterDir="false" />
<change afterPath="$PROJECT_DIR$/.idea/inspectionProfiles/profiles_settings.xml" afterDir="false" />
<change afterPath="$PROJECT_DIR$/.idea/modules.xml" afterDir="false" />
<change afterPath="$PROJECT_DIR$/.idea/vcs.xml" afterDir="false" />
<change afterPath="$PROJECT_DIR$/.idea/workspace.xml" afterDir="false" />
<change afterPath="$PROJECT_DIR$/README.md" afterDir="false" />
<change afterPath="$PROJECT_DIR$/api/controllers/collection.py" afterDir="false" />
<change afterPath="$PROJECT_DIR$/api/controllers/consent.py" afterDir="false" />
<change afterPath="$PROJECT_DIR$/api/controllers/disbursement.py" afterDir="false" />
<change afterPath="$PROJECT_DIR$/api/controllers/eligibility.py" afterDir="false" />
<change afterPath="$PROJECT_DIR$/api/controllers/lien.py" afterDir="false" />
<change afterPath="$PROJECT_DIR$/api/controllers/loan.py" afterDir="false" />
<change afterPath="$PROJECT_DIR$/api/controllers/notification.py" afterDir="false" />
<change afterPath="$PROJECT_DIR$/api/controllers/offers.py" afterDir="false" />
<change afterPath="$PROJECT_DIR$/api/controllers/penal.py" afterDir="false" />
<change afterPath="$PROJECT_DIR$/api/controllers/rac.py" afterDir="false" />
<change afterPath="$PROJECT_DIR$/api/controllers/repayment.py" afterDir="false" />
<change afterPath="$PROJECT_DIR$/api/controllers/sms.py" afterDir="false" />
<change afterPath="$PROJECT_DIR$/api/controllers/token.py" afterDir="false" />
<change afterPath="$PROJECT_DIR$/api/controllers/transaction.py" afterDir="false" />
<change afterPath="$PROJECT_DIR$/api/middleware.py" afterDir="false" />
<change afterPath="$PROJECT_DIR$/api/models.py" afterDir="false" />
<change afterPath="$PROJECT_DIR$/api/routes.py" afterDir="false" />
<change afterPath="$PROJECT_DIR$/app.py" afterDir="false" />
<change afterPath="$PROJECT_DIR$/config.py" afterDir="false" />
<change afterPath="$PROJECT_DIR$/requirements.txt" afterDir="false" />
<change afterPath="$PROJECT_DIR$/test_api.py" afterDir="false" />
</list>
<option name="SHOW_DIALOG" value="false" />
<option name="HIGHLIGHT_CONFLICTS" value="true" />
<option name="HIGHLIGHT_NON_ACTIVE_CHANGELIST" value="false" />
<option name="LAST_RESOLUTION" value="IGNORE" />
</component>
<component name="FileTemplateManagerImpl">
<option name="RECENT_TEMPLATES">
<list>
<option value="Flask Main" />
<option value="Python Script" />
</list>
</option>
</component>
<component name="Git.Settings">
<option name="RECENT_GIT_ROOT_PATH" value="$PROJECT_DIR$" />
</component>
<component name="ProjectColorInfo">{
&quot;associatedIndex&quot;: 8
}</component>
<component name="ProjectId" id="2uZWu0pFY6y4wQAz6N1D8siabne" />
<component name="ProjectLevelVcsManager" settingsEditedManually="true">
<ConfirmationsSetting value="2" id="Add" />
</component>
<component name="ProjectViewState">
<option name="hideEmptyMiddlePackages" value="true" />
<option name="showLibraryContents" value="true" />
</component>
<component name="PropertiesComponent">{
&quot;keyToString&quot;: {
&quot;Flask server.FirstBankSimbrellaApi.executor&quot;: &quot;Run&quot;,
&quot;RunOnceActivity.ShowReadmeOnStart&quot;: &quot;true&quot;,
&quot;RunOnceActivity.git.unshallow&quot;: &quot;true&quot;,
&quot;git-widget-placeholder&quot;: &quot;master&quot;,
&quot;last_opened_file_path&quot;: &quot;C:/Users/amuibi/PycharmProjects/FirstBankSimbrellaApi&quot;,
&quot;node.js.detected.package.eslint&quot;: &quot;true&quot;,
&quot;node.js.detected.package.tslint&quot;: &quot;true&quot;,
&quot;node.js.selected.package.eslint&quot;: &quot;(autodetect)&quot;,
&quot;node.js.selected.package.tslint&quot;: &quot;(autodetect)&quot;,
&quot;nodejs_package_manager_path&quot;: &quot;npm&quot;,
&quot;vue.rearranger.settings.migration&quot;: &quot;true&quot;
}
}</component>
<component name="RecentsManager">
<key name="CopyFile.RECENT_KEYS">
<recent name="C:\Users\amuibi\PycharmProjects\FirstBankSimbrellaApi" />
<recent name="C:\Users\amuibi\PycharmProjects\FirstBankSimbrellaApi\api\controllers" />
<recent name="C:\Users\amuibi\PycharmProjects\FirstBankSimbrellaApi\api" />
</key>
</component>
<component name="RunManager">
<configuration name="FirstBankSimbrellaApi" type="Python.FlaskServer">
<module name="FirstBankSimbrellaApi" />
<option name="target" value="$PROJECT_DIR$/app.py" />
<option name="targetType" value="PATH" />
<option name="ENV_FILES" value="" />
<option name="INTERPRETER_OPTIONS" value="" />
<option name="PARENT_ENVS" value="true" />
<option name="SDK_HOME" value="" />
<option name="WORKING_DIRECTORY" value="" />
<option name="IS_MODULE_SDK" value="false" />
<option name="ADD_CONTENT_ROOTS" value="true" />
<option name="ADD_SOURCE_ROOTS" value="true" />
<EXTENSION ID="PythonCoverageRunConfigurationExtension" runner="coverage.py" />
<option name="launchJavascriptDebuger" value="false" />
<method v="2" />
</configuration>
</component>
<component name="SharedIndexes">
<attachedChunks>
<set>
<option value="bundled-js-predefined-d6986cc7102b-1632447f56bf-JavaScript-PY-243.26053.29" />
<option value="bundled-python-sdk-b1dbf8ef85a6-4df51de95216-com.jetbrains.pycharm.pro.sharedIndexes.bundled-PY-243.26053.29" />
</set>
</attachedChunks>
</component>
<component name="SpellCheckerSettings" RuntimeDictionaries="0" Folders="0" CustomDictionaries="0" DefaultDictionary="application-level" UseSingleDictionary="true" transferred="true" />
<component name="TaskManager">
<task active="true" id="Default" summary="Default task">
<changelist id="934fdd1a-f31d-4df5-906a-7da283ff1489" name="Changes" comment="" />
<created>1742456795887</created>
<option name="number" value="Default" />
<option name="presentableId" value="Default" />
<updated>1742456795887</updated>
<workItem from="1742456797057" duration="6307000" />
</task>
<servers />
</component>
<component name="TypeScriptGeneratedFilesManager">
<option name="version" value="3" />
</component>
<component name="com.intellij.coverage.CoverageDataManagerImpl">
<SUITE FILE_PATH="coverage/FirstBankSimbrellaApi$FirstBankSimbrellaApi.coverage" NAME="FirstBankSimbrellaApi Coverage Results" MODIFIED="1742461185899" SOURCE_PROVIDER="com.intellij.coverage.DefaultCoverageFileProvider" RUNNER="coverage.py" COVERAGE_BY_TEST_ENABLED="false" COVERAGE_TRACING_ENABLED="false" WORKING_DIRECTORY="" />
</component>
</project>
+16
View File
@@ -0,0 +1,16 @@
# Simbrella FirstAdvance API Flask Implementation
This project implements the Simbrella FirstAdvance API as defined in the OpenAPI 3.0 specification.
## Features
- Complete implementation of all API endpoints
- Authentication middleware for both Basic Auth and API Key auth
- Request/response validation
- Comprehensive error handling
- Logging
## Setup
1. Clone the repository
2. Create a virtual environment:
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+81
View File
@@ -0,0 +1,81 @@
"""
Controller for loan collection endpoints.
"""
from flask import Blueprint, request, jsonify
from api.middleware import api_key_required
from api.models import CollectLoanRequest, CollectLoanResponse
import logging
# Configure logger
logger = logging.getLogger(__name__)
# Create blueprint
collection_bp = Blueprint('collection', __name__)
@collection_bp.route('/CollectLoan', methods=['POST'])
@api_key_required
def collect_loan():
"""
Endpoint to process loan collection requests from Simbrella.
This method handles requests to collect money from user accounts.
When a request is received, FirstBank should check all user accounts
and collect as much money as possible to cover the existing loan
either partially or fully.
Returns:
JSON response with collection status
"""
try:
# Parse and validate request
data = request.get_json()
if not data:
return jsonify({
'resultCode': '400',
'resultDescription': 'Invalid JSON payload'
}), 400
# Validate required fields
required_fields = ['transactionId', 'fbnTransactionId', 'debtId', 'customerId',
'accountId', 'productId', 'collectAmount', 'collectionMethod',
'lienAmount', 'countryId']
for field in required_fields:
if field not in data:
return jsonify({
'resultCode': '422',
'resultDescription': f'Missing required field: {field}'
}), 422
# Create request model
req = CollectLoanRequest.from_dict(data)
# Process collection request (this would connect to the business logic)
# For demonstration, we'll return a mock response with partial collection
# Assume we collected 75% of the requested amount
collected_amount = req.collectAmount * 0.75
remaining_lien = req.lienAmount - collected_amount
# Create response
response = CollectLoanResponse(
transactionId=req.transactionId,
debtId=req.debtId,
customerId=req.customerId,
accountId=req.accountId,
productId=req.productId,
collectAmount=collected_amount,
lienAmount=remaining_lien,
resultCode="00",
resultDescription="Loan Collection Successful",
penalCharge=req.penalCharge if hasattr(req, 'penalCharge') else 0.0
)
logger.info(f"Processed collection for customer {req.customerId}, collected {collected_amount}")
return jsonify(response.to_dict())
except Exception as e:
logger.error(f"Error processing collection: {str(e)}")
return jsonify({
'resultCode': '500',
'resultDescription': 'Internal server error'
}), 500
+123
View File
@@ -0,0 +1,123 @@
"""
Controller for customer consent endpoints.
"""
from flask import Blueprint, request, jsonify
from api.middleware import basic_auth_required, api_key_required
from api.models import (
CustomerConsentRequest, CustomerConsentResponse,
RevokeEnableConsentRequest, RevokeEnableConsentResponse
)
import logging
# Configure logger
logger = logging.getLogger(__name__)
# Create blueprint
consent_bp = Blueprint('consent', __name__)
@consent_bp.route('/CustomerConsent', methods=['POST'])
@basic_auth_required
def customer_consent():
"""
Endpoint to process customer consent requests.
This method handles customer consent for loan services.
Returns:
JSON response with consent status
"""
try:
# Parse and validate request
data = request.get_json()
if not data:
return jsonify({
'resultCode': '400',
'resultDescription': 'Invalid JSON payload'
}), 400
# Validate required fields
required_fields = ['$type', 'transactionId', 'customerId', 'accountId',
'requestTime', 'consentType', 'channel']
for field in required_fields:
if field not in data:
return jsonify({
'resultCode': '422',
'resultDescription': f'Missing required field: {field}'
}), 422
# Create request model
req = CustomerConsentRequest.from_dict(data)
# Process consent request (this would connect to the business logic)
# For demonstration, we'll return a mock response
# Create response
response = CustomerConsentResponse(
resultCode="00",
resultDescription="Request is received"
)
logger.info(f"Processed consent request for customer {req.customerId}, type {req.consentType}")
return jsonify(response.to_dict())
except Exception as e:
logger.error(f"Error processing consent request: {str(e)}")
return jsonify({
'resultCode': '500',
'resultDescription': 'Internal server error'
}), 500
@consent_bp.route('/RevokeEnableConsent', methods=['POST'])
@api_key_required
def revoke_enable_consent():
"""
Endpoint to process consent revocation or enablement.
This method handles requests from Simbrella to revoke or enable customer consent.
Returns:
JSON response with operation status
"""
try:
# Parse and validate request
data = request.get_json()
if not data:
return jsonify({
'resultCode': '400',
'resultDescription': 'Invalid JSON payload'
}), 400
# Validate required fields
required_fields = ['transactionId', 'fbnTransactionId', 'customerId', 'accountId',
'processTime', 'consentType', 'countryId']
for field in required_fields:
if field not in data:
return jsonify({
'resultCode': '422',
'resultDescription': f'Missing required field: {field}'
}), 422
# Create request model
req = RevokeEnableConsentRequest.from_dict(data)
# Process revoke/enable consent request (this would connect to the business logic)
# For demonstration, we'll return a mock response
# Create response
response = RevokeEnableConsentResponse(
type="RevokeEnableConsentResponse",
customerId=req.customerId,
accountId=req.accountId,
resultCode="00",
resultDescription="Success"
)
logger.info(f"Processed revoke/enable consent for customer {req.customerId}, type {req.consentType}")
return jsonify(response.to_dict())
except Exception as e:
logger.error(f"Error processing revoke/enable consent: {str(e)}")
return jsonify({
'resultCode': '500',
'resultDescription': 'Internal server error'
}), 500
+78
View File
@@ -0,0 +1,78 @@
"""
Controller for loan disbursement endpoints.
"""
from flask import Blueprint, request, jsonify
from api.middleware import api_key_required
from api.models import DisbursementRequest, DisbursementResponse
import logging
# Configure logger
logger = logging.getLogger(__name__)
# Create blueprint
disbursement_bp = Blueprint('disbursement', __name__)
@disbursement_bp.route('/Disbursement', methods=['POST'])
@api_key_required
def disbursement():
"""
Endpoint to process loan disbursement requests from Simbrella.
This method handles requests to disburse loans to customer accounts.
The operation should be executed atomically, providing the loan and
collecting upfront fees within the same transaction.
Returns:
JSON response with disbursement status
"""
try:
# Parse and validate request
data = request.get_json()
if not data:
return jsonify({
'resultCode': '400',
'resultDescription': 'Invalid JSON payload'
}), 400
# Validate required fields
required_fields = ['requestId', 'debtId', 'transactionId', 'customerId',
'accountId', 'productId', 'provideAmount', 'countryId']
for field in required_fields:
if field not in data:
return jsonify({
'resultCode': '422',
'resultDescription': f'Missing required field: {field}'
}), 422
# Create request model
req = DisbursementRequest.from_dict(data)
# Process disbursement request (this would connect to the business logic)
# For demonstration, we'll return a mock response
# Create response
response = DisbursementResponse(
requestId=req.requestId,
debtId=req.debtId,
transactionId=req.transactionId,
customerId=req.customerId,
accountId=req.accountId,
productId=req.productId,
provideAmount=req.provideAmount,
resultCode="00",
resultDescription="Loan Request Completed Successfully!",
collectAmountInterest=req.collectAmountInterest,
collectAmountMgtFee=req.collectAmountMgtFee,
collectAmountInsurance=req.collectAmountInsurance,
collectAmountVAT=req.collectAmountVAT
)
logger.info(f"Processed disbursement for customer {req.customerId}, amount {req.provideAmount}")
return jsonify(response.to_dict())
except Exception as e:
logger.error(f"Error processing disbursement: {str(e)}")
return jsonify({
'resultCode': '500',
'resultDescription': 'Internal server error'
}), 500
+91
View File
@@ -0,0 +1,91 @@
"""
Controller for eligibility check endpoints.
"""
from flask import Blueprint, request, jsonify, current_app
from flask.typing import ResponseReturnValue
from api.middleware import basic_auth_required
from api.models import EligibilityCheckRequest, EligibilityCheckResponse, EligibleOffer
import logging
from typing import Dict, Any, List
# Configure logger
logger = logging.getLogger(__name__)
# Create blueprint
eligibility_bp = Blueprint('eligibility', __name__)
@eligibility_bp.route('/EligibilityCheck', methods=['POST'])
@basic_auth_required
def eligibility_check() -> ResponseReturnValue:
"""
Endpoint to check customer eligibility for loans.
This endpoint initiates the eligibility check process and performs RAC checks.
Returns:
JSON response with eligibility status and available offers
"""
try:
# Parse and validate request
data = request.get_json()
if not data:
logger.warning("Invalid JSON payload received")
return jsonify({
'resultCode': '400',
'resultDescription': 'Invalid JSON payload'
}), 400
# Validate required fields
required_fields = ['$type', 'transactionId', 'countryCode', 'customerId',
'accountId', 'lienAmount', 'channel']
missing_fields = [field for field in required_fields if field not in data]
if missing_fields:
logger.warning(f"Missing required fields: {', '.join(missing_fields)}")
return jsonify({
'resultCode': '422',
'resultDescription': f'Missing required fields: {", ".join(missing_fields)}'
}), 422
# Create request model
req = EligibilityCheckRequest.from_dict(data)
# Process eligibility check (this would connect to the business logic)
# For demonstration, we'll return a mock response
# Create sample offers
offers: List[EligibleOffer] = [
EligibleOffer(
minamount=5000.0,
maxamount=20000.0,
productId=101,
offerid=101,
Tenor=30
),
EligibleOffer(
minamount=10000.0,
maxamount=50000.0,
productId=102,
offerid=102,
Tenor=60
)
]
# Create response
response = EligibilityCheckResponse(
customerId=req.customerId,
transactionId=req.transactionId,
eligibleOffers=[offer.to_dict() for offer in offers],
resultCode="00",
resultDescription="Successful",
msisdn=req.msisdn
)
logger.info(f"Processed eligibility check for customer {req.customerId}")
return jsonify(response.to_dict())
except Exception as e:
logger.exception(f"Error processing eligibility check: {str(e)}")
return jsonify({
'resultCode': '500',
'resultDescription': f'Internal server error: {str(e)}'
}), 500
+65
View File
@@ -0,0 +1,65 @@
"""
Controller for lien check endpoints.
"""
from flask import Blueprint, request, jsonify
from api.middleware import api_key_required
from api.models import LienCheckRequest, LienCheckResponse
import logging
# Configure logger
logger = logging.getLogger(__name__)
# Create blueprint
lien_bp = Blueprint('lien', __name__)
@lien_bp.route('/LienCheck', methods=['POST'])
@api_key_required
def lien_check():
"""
Endpoint to check lien amount on an account.
This method is used to get the applied lien amount for a specific account.
Returns:
JSON response with lien amount details
"""
try:
# Parse and validate request
data = request.get_json()
if not data:
return jsonify({
'resultCode': '400',
'resultDescription': 'Invalid JSON payload'
}), 400
# Validate required fields
required_fields = ['transactionId', 'customerId', 'accountId', 'countryId']
for field in required_fields:
if field not in data:
return jsonify({
'resultCode': '422',
'resultDescription': f'Missing required field: {field}'
}), 422
# Create request model
req = LienCheckRequest.from_dict(data)
# Process lien check (this would connect to the business logic)
# For demonstration, we'll return a mock response
# Create response
response = LienCheckResponse(
lienAmount=20000.0,
resultCode="00",
resultDescription="Successful"
)
logger.info(f"Processed lien check for customer {req.customerId}, account {req.accountId}")
return jsonify(response.to_dict())
except Exception as e:
logger.error(f"Error processing lien check: {str(e)}")
return jsonify({
'resultCode': '500',
'resultDescription': 'Internal server error'
}), 500
+147
View File
@@ -0,0 +1,147 @@
"""
Controller for loan-related endpoints.
"""
from flask import Blueprint, request, jsonify
from api.middleware import basic_auth_required
from api.models import (
ProvideLoanRequest, ProvideLoanResponse,
LoanInformationRequest, LoanInformationResponse, Loan
)
from datetime import datetime, timedelta
import logging
# Configure logger
logger = logging.getLogger(__name__)
# Create blueprint
loan_bp = Blueprint('loan', __name__)
@loan_bp.route('/ProvideLoan', methods=['POST'])
@basic_auth_required
def provide_loan():
"""
Endpoint to process loan provision requests.
This method handles the request to provide a loan to a customer.
Returns:
JSON response with loan provision status
"""
try:
# Parse and validate request
data = request.get_json()
if not data:
return jsonify({
'resultCode': '400',
'resultDescription': 'Invalid JSON payload'
}), 400
# Validate required fields
required_fields = ['$type', 'requestId', 'transactionId', 'customerId',
'accountId', 'productId', 'lienAmount', 'requestedAmount',
'collectionType', 'loanType', 'channel']
for field in required_fields:
if field not in data:
return jsonify({
'resultCode': '422',
'resultDescription': f'Missing required field: {field}'
}), 422
# Create request model
req = ProvideLoanRequest.from_dict(data)
# Process loan provision (this would connect to the business logic)
# For demonstration, we'll return a mock response
# Create response
response = ProvideLoanResponse(
requestId=req.requestId,
transactionId=req.transactionId,
customerId=req.customerId,
accountId=req.accountId,
resultCode="00",
resultDescription="Loan provided successfully",
msisdn=req.msisdn if hasattr(req, 'msisdn') else None
)
logger.info(f"Processed loan provision for customer {req.customerId}")
return jsonify(response.to_dict())
except Exception as e:
logger.error(f"Error processing loan provision: {str(e)}")
return jsonify({
'resultCode': '500',
'resultDescription': 'Internal server error'
}), 500
@loan_bp.route('/LoanInformation', methods=['POST'])
@basic_auth_required
def loan_information():
"""
Endpoint to retrieve loan information.
This method provides information about a customer's existing loans.
Returns:
JSON response with loan information
"""
try:
# Parse and validate request
data = request.get_json()
if not data:
return jsonify({
'resultCode': '400',
'resultDescription': 'Invalid JSON payload'
}), 400
# Validate required fields
required_fields = ['$type', 'transactionId', 'customerId', 'channel']
for field in required_fields:
if field not in data:
return jsonify({
'resultCode': '422',
'resultDescription': f'Missing required field: {field}'
}), 422
# Create request model
req = LoanInformationRequest.from_dict(data)
# Process loan information request (this would connect to the business logic)
# For demonstration, we'll return a mock response
# Create sample loans
now = datetime.now()
loan_date = now - timedelta(days=15)
due_date = now + timedelta(days=15)
loans = [
Loan(
debtId="123456789",
loanDate=loan_date.strftime("%Y-%m-%d %H:%M:%S.%f")[:-3],
dueDate=due_date.strftime("%Y-%m-%d %H:%M:%S.%f")[:-3],
currentLoanAmount=8500.0,
initialLoanAmount=10000.0,
defaultFee=0.0,
continiousFee=0.0,
productId="101"
)
]
# Create response
response = LoanInformationResponse(
customerId=req.customerId,
loans=[loan.to_dict() for loan in loans],
resultCode="00",
resultDescription="Successful",
totalDebtAmount=8500.0
)
logger.info(f"Processed loan information request for customer {req.customerId}")
return jsonify(response.to_dict())
except Exception as e:
logger.error(f"Error processing loan information request: {str(e)}")
return jsonify({
'resultCode': '500',
'resultDescription': 'Internal server error'
}), 500
+70
View File
@@ -0,0 +1,70 @@
"""
Controller for notification callback endpoints.
"""
from flask import Blueprint, request, jsonify
from api.middleware import basic_auth_required
from api.models import NotificationCallbackRequest, NotificationCallbackResponse
import logging
# Configure logger
logger = logging.getLogger(__name__)
# Create blueprint
notification_bp = Blueprint('notification', __name__)
@notification_bp.route('/NotificationCallback', methods=['POST'])
@basic_auth_required
def notification_callback():
"""
Endpoint to receive transaction status notifications.
This method is used for informing Simbrella about the status of transactions
that FirstBank has processed.
Returns:
JSON response acknowledging receipt of notification
"""
try:
# Parse and validate request
data = request.get_json()
if not data:
return jsonify({
'resultCode': '400',
'resultDescription': 'Invalid JSON payload'
}), 400
# Validate required fields
required_fields = ['fbnTransactionId', 'transactionId', 'customerId', 'accountId',
'debtId', 'transactionType', 'amountProvided', 'amountCollected',
'responseCode', 'responseDescription']
for field in required_fields:
if field not in data:
return jsonify({
'resultCode': '422',
'resultDescription': f'Missing required field: {field}'
}), 422
# Create request model
req = NotificationCallbackRequest.from_dict(data)
# Process notification (this would connect to the business logic)
# For demonstration, we'll return a mock response
# Log the notification details
logger.info(f"Received notification for transaction {req.transactionId}, "
f"type {req.transactionType}, status {req.responseCode}")
# Create response
response = NotificationCallbackResponse(
resultCode="00",
resultDescription="Successful"
)
return jsonify(response.to_dict())
except Exception as e:
logger.error(f"Error processing notification callback: {str(e)}")
return jsonify({
'resultCode': '500',
'resultDescription': 'Internal server error'
}), 500
+92
View File
@@ -0,0 +1,92 @@
"""
Controller for offer selection endpoints.
"""
from flask import Blueprint, request, jsonify
from api.middleware import basic_auth_required
from api.models import SelectOffersRequest, SelectOffersResponse, Offer
import logging
# Configure logger
logger = logging.getLogger(__name__)
# Create blueprint
offers_bp = Blueprint('offers', __name__)
@offers_bp.route('/SelectOffer', methods=['POST'])
@basic_auth_required
def select_offer():
"""
Endpoint to send the offer the customer selected to Simbrella.
This method processes the customer's selected offer and returns detailed offer information.
Returns:
JSON response with detailed offer information
"""
try:
# Parse and validate request
data = request.get_json()
if not data:
return jsonify({
'resultCode': '400',
'resultDescription': 'Invalid JSON payload'
}), 400
# Validate required fields
required_fields = ['requestId', 'transactionId', 'customerId', 'accountId',
'msisdn', 'requestedAmount', 'productid', 'channel']
for field in required_fields:
if field not in data:
return jsonify({
'resultCode': '422',
'resultDescription': f'Missing required field: {field}'
}), 422
# Create request model
req = SelectOffersRequest.from_dict(data)
# Process offer selection (this would connect to the business logic)
# For demonstration, we'll return a mock response
# Create sample offers
offers = [
Offer(
offerId="14451",
productId=req.productid,
amount=req.requestedAmount,
upfrontPayment=req.requestedAmount * 0.1, # 10% upfront
interestRate=3.0,
Interest=req.requestedAmount * 0.03, # 3% interest
ManagementRate=1.0,
ManagementFee=req.requestedAmount * 0.01, # 1% management fee
InsuranceRate=1.0,
InsuranceFee=req.requestedAmount * 0.01, # 1% insurance
VATRate=7.5,
VATamount=(req.requestedAmount * 0.01) * 0.075, # VAT on management fee
recommendedRepaymentDates=["2023-04-30", "2023-05-30", "2023-06-29"],
installmentAmount=req.requestedAmount * 1.05 / 3, # Split into 3 payments with 5% total fees
totalRepaymentAmount=req.requestedAmount * 1.05 # Total with 5% fees
)
]
# Create response
response = SelectOffersResponse(
requestId=req.requestId,
transactionId=req.transactionId,
customerId=req.customerId,
accountId=req.accountId,
offers=[offer.to_dict() for offer in offers],
resultCode="00",
resultDescription="Successful",
outstandingDebtAmount=0.0
)
logger.info(f"Processed offer selection for customer {req.customerId}")
return jsonify(response.to_dict())
except Exception as e:
logger.error(f"Error processing offer selection: {str(e)}")
return jsonify({
'resultCode': '500',
'resultDescription': 'Internal server error'
}), 500
+67
View File
@@ -0,0 +1,67 @@
"""
Controller for penal charge endpoints.
"""
from flask import Blueprint, request, jsonify
from api.middleware import api_key_required
from api.models import PenalChargeRequest, PenalChargeResponse
import logging
# Configure logger
logger = logging.getLogger(__name__)
# Create blueprint
penal_bp = Blueprint('penal', __name__)
@penal_bp.route('/PenalCharge', methods=['POST'])
@api_key_required
def penal_charge():
"""
Endpoint to process penalty charge requests.
This method handles requests to charge customers for penalties
as per existing debt. Results of these requests will be received
from the NotificationCallback endpoint.
Returns:
JSON response acknowledging the penalty charge request
"""
try:
# Parse and validate request
data = request.get_json()
if not data:
return jsonify({
'resultCode': '400',
'resultDescription': 'Invalid JSON payload'
}), 400
# Validate required fields
required_fields = ['transactionId', 'fbnTransactionId', 'debtId', 'customerId',
'accountId', 'penalCharge', 'lienAmount', 'countryId']
for field in required_fields:
if field not in data:
return jsonify({
'resultCode': '422',
'resultDescription': f'Missing required field: {field}'
}), 422
# Create request model
req = PenalChargeRequest.from_dict(data)
# Process penal charge request (this would connect to the business logic)
# For demonstration, we'll return a mock response
# Create response
response = PenalChargeResponse(
resultCode="00",
resultDescription="Penal charge debited successfully"
)
logger.info(f"Processed penal charge for customer {req.customerId}, amount {req.penalCharge}")
return jsonify(response.to_dict())
except Exception as e:
logger.error(f"Error processing penal charge: {str(e)}")
return jsonify({
'resultCode': '500',
'resultDescription': 'Internal server error'
}), 500
+81
View File
@@ -0,0 +1,81 @@
"""
Controller for Risk Acceptance Criteria check endpoints.
"""
from flask import Blueprint, request, jsonify
from api.middleware import api_key_required
from api.models import RACCheckRequest, RACCheckResponse, RACResponse
import logging
# Configure logger
logger = logging.getLogger(__name__)
# Create blueprint
rac_bp = Blueprint('rac', __name__)
@rac_bp.route('/RACCheck', methods=['POST'])
@api_key_required
def rac_check():
"""
Endpoint to check if a customer passes the Risk Acceptance Criteria.
This method evaluates a customer against the bank's risk criteria.
Returns:
JSON response with RAC check results
"""
try:
# Parse and validate request
data = request.get_json()
if not data:
return jsonify({
'resultCode': '400',
'resultDescription': 'Invalid JSON payload'
}), 400
# Validate required fields
required_fields = ['transactionId', 'fbnTransactionId', 'customerId',
'accountId', 'RAC_Array']
for field in required_fields:
if field not in data:
return jsonify({
'resultCode': '422',
'resultDescription': f'Missing required field: {field}'
}), 422
# Create request model
req = RACCheckRequest.from_dict(data)
# Process RAC check (this would connect to the business logic)
# For demonstration, we'll return a mock response with all checks passing
# Create RAC response object
rac_response = RACResponse(
SalaryAccount="1",
BVN="1",
BVNAttachedtoAccount="1",
CRMS="1",
CRC="1",
AccountStatus="1",
Lien="1",
NoBounchedCheck="1",
Whitelist="1",
NoPastDueSalaryLoan="1",
NoPastDueOtherLoan="1"
)
# Create response
response = RACCheckResponse(
RACResponse=rac_response.to_dict(),
resultCode="00",
resultDescription="RAC Check Successful"
)
logger.info(f"Processed RAC check for customer {req.customerId}")
return jsonify(response.to_dict())
except Exception as e:
logger.error(f"Error processing RAC check: {str(e)}")
return jsonify({
'resultCode': '500',
'resultDescription': 'Internal server error'
}), 500
+68
View File
@@ -0,0 +1,68 @@
"""
Controller for repayment endpoints.
"""
from flask import Blueprint, request, jsonify
from api.middleware import basic_auth_required
from api.models import RepaymentRequest, RepaymentResponse
import logging
# Configure logger
logger = logging.getLogger(__name__)
# Create blueprint
repayment_bp = Blueprint('repayment', __name__)
@repayment_bp.route('/Repayment', methods=['POST'])
@basic_auth_required
def repayment():
"""
Endpoint to process loan repayment requests.
This method handles customer repayment of loans.
Returns:
JSON response with repayment status
"""
try:
# Parse and validate request
data = request.get_json()
if not data:
return jsonify({
'resultCode': '400',
'resultDescription': 'Invalid JSON payload'
}), 400
# Validate required fields
required_fields = ['$type', 'transactionId', 'customerId', 'debtId',
'productId', 'channel']
for field in required_fields:
if field not in data:
return jsonify({
'resultCode': '422',
'resultDescription': f'Missing required field: {field}'
}), 422
# Create request model
req = RepaymentRequest.from_dict(data)
# Process repayment request (this would connect to the business logic)
# For demonstration, we'll return a mock response
# Create response
response = RepaymentResponse(
customerId=req.customerId,
productId=req.productId,
debtId=req.debtId,
resultCode="00",
resultDescription="Repayment processed successfully"
)
logger.info(f"Processed repayment for customer {req.customerId}, debt {req.debtId}")
return jsonify(response.to_dict())
except Exception as e:
logger.error(f"Error processing repayment: {str(e)}")
return jsonify({
'resultCode': '500',
'resultDescription': 'Internal server error'
}), 500
+132
View File
@@ -0,0 +1,132 @@
"""
Controller for SMS notification endpoints.
"""
from flask import Blueprint, request, jsonify
from api.middleware import api_key_required
from api.models import SMSRequest, SMSResponse
import logging
# Configure logger
logger = logging.getLogger(__name__)
# Create blueprint
sms_bp = Blueprint('sms', __name__)
@sms_bp.route('/SMS', methods=['POST'])
@api_key_required
def send_sms():
"""
Endpoint to send SMS notifications.
This method handles requests to send SMS messages to customers.
Returns:
JSON response with SMS sending status
"""
try:
# Parse and validate request
data = request.get_json()
if not data:
return jsonify({
'resultCode': '400',
'resultDescription': 'Invalid JSON payload'
}), 400
# Validate required fields
required_fields = ['text', 'dest', 'unicode']
for field in required_fields:
if field not in data:
return jsonify({
'resultCode': '422',
'resultDescription': f'Missing required field: {field}'
}), 422
# Create request model
req = SMSRequest.from_dict(data)
# Process SMS request (this would connect to your business logic)
# For demonstration, we'll return a mock response
# Create response
response = SMSResponse(
data="",
statusCode=200,
IsSuccessful=True,
errorMessage=None
)
logger.info(f"Processed SMS request to {req.dest}")
return jsonify(response.to_dict())
except Exception as e:
logger.error(f"Error processing SMS request: {str(e)}")
return jsonify({
'resultCode': '500',
'resultDescription': 'Internal server error'
}), 500
@sms_bp.route('/BulkSMS', methods=['POST'])
@api_key_required
def send_bulk_sms():
"""
Endpoint to send bulk SMS notifications.
This method handles requests to send multiple SMS messages (up to 20)
in a single request.
Returns:
JSON response with bulk SMS sending status
"""
try:
# Parse and validate request
data = request.get_json()
if not data:
return jsonify({
'resultCode': '400',
'resultDescription': 'Invalid JSON payload'
}), 400
# Validate that data is an array
if not isinstance(data, list):
return jsonify({
'resultCode': '422',
'resultDescription': 'Request must be an array of SMS messages'
}), 422
# Validate array length
if len(data) > 20:
return jsonify({
'resultCode': '422',
'resultDescription': 'Maximum of 20 SMS messages allowed per request'
}), 422
# Validate each SMS in the array
for i, sms in enumerate(data):
required_fields = ['text', 'dest', 'unicode']
for field in required_fields:
if field not in sms:
return jsonify({
'resultCode': '422',
'resultDescription': f'Missing required field: {field} in SMS at index {i}'
}), 422
# Process bulk SMS request (this would connect to the business logic)
# For demonstration, we'll return a mock response
# Create response
response = SMSResponse(
data="",
statusCode=200,
IsSuccessful=True,
errorMessage=None
)
logger.info(f"Processed bulk SMS request with {len(data)} messages")
return jsonify(response.to_dict())
except Exception as e:
logger.error(f"Error processing bulk SMS request: {str(e)}")
return jsonify({
'resultCode': '500',
'resultDescription': 'Internal server error'
}), 500
+68
View File
@@ -0,0 +1,68 @@
"""
Controller for token validation endpoints.
"""
from flask import Blueprint, request, jsonify
from api.middleware import api_key_required
from api.models import ValidateTokenRequest, ValidateTokenResponse
import logging
# Configure logger
logger = logging.getLogger(__name__)
# Create blueprint
token_bp = Blueprint('token', __name__)
@token_bp.route('/ValidateToken', methods=['POST'])
@api_key_required
def validate_token():
"""
Endpoint to validate user authentication tokens.
This method is used when users from FirstBank access the Customer Care Portal.
It validates the soft/hard token code entered by the user.
Returns:
JSON response with token validation results
"""
try:
# Parse and validate request
data = request.get_json()
if not data:
return jsonify({
'resultCode': '400',
'resultDescription': 'Invalid JSON payload'
}), 400
# Validate required fields
required_fields = ['RequestId', 'UserId', 'CountryId', 'TokenCode']
for field in required_fields:
if field not in data:
return jsonify({
'resultCode': '422',
'resultDescription': f'Missing required field: {field}'
}), 422
# Create request model
req = ValidateTokenRequest.from_dict(data)
# Process token validation (this would connect to the business logic)
# For demonstration, we'll return a mock response with successful validation
# Create response
response = ValidateTokenResponse(
Authenticated=True,
AuthenticatedMessage=f"The user with ID {req.UserId} has successfully authenticated!",
ResponseCode="00",
ResponseMessage="Successful",
RequestId=req.RequestId
)
logger.info(f"Processed token validation for user {req.UserId}")
return jsonify(response.to_dict())
except Exception as e:
logger.error(f"Error processing token validation: {str(e)}")
return jsonify({
'resultCode': '500',
'resultDescription': 'Internal server error'
}), 500
+169
View File
@@ -0,0 +1,169 @@
"""
Controller for transaction check endpoints.
"""
from flask import Blueprint, request, jsonify
from flask.typing import ResponseReturnValue
from api.middleware import api_key_required
from api.models import (
TransactionCheckRequest, TransactionCheckResponse,
NewTransactionCheckRequest, NewTransactionCheckResponse,
TransactionData
)
import logging
from typing import Dict, Any
# Configure logger
logger = logging.getLogger(__name__)
# Create blueprint
transaction_bp = Blueprint('transaction', __name__)
@transaction_bp.route('/TransactionCheck', methods=['POST'])
@api_key_required
def transaction_check() -> ResponseReturnValue:
"""
Endpoint to check transaction status.
This method is used to double-check the response received from DisburseLoan
and CollectLoan Synchronous APIs. It verifies transaction results on FirstBank.
Returns:
JSON response with transaction status details
"""
try:
# Parse and validate request
data = request.get_json()
if not data:
logger.warning("Invalid JSON payload received")
return jsonify({
'resultCode': '400',
'resultDescription': 'Invalid JSON payload'
}), 400
# Validate required fields
required_fields = ['counter', 'TransactionId', 'requestID', 'customerId',
'accountId', 'countryId', 'transactionType']
missing_fields = [field for field in required_fields if field not in data]
if missing_fields:
logger.warning(f"Missing required fields: {', '.join(missing_fields)}")
return jsonify({
'resultCode': '422',
'resultDescription': f'Missing required fields: {", ".join(missing_fields)}'
}), 422
# Create request model
req = TransactionCheckRequest.from_dict(data)
# Process transaction check (this would connect to the business logic)
# For demonstration, we'll return a mock response
# Create response based on transaction type
provided_amount = 0.0
collected_amount = 0.0
if req.transactionType == "Disbursement":
provided_amount = 10000.0
elif req.transactionType == "Collection" or req.transactionType == "Penalty":
collected_amount = 7.50
# Create response
response = TransactionCheckResponse(
type_field="TransactionCheckResponse", # This will be converted to $type in JSON
nativeId=f"FBN20191031104405{req.customerId}",
customerId=req.customerId,
accountId=req.accountId,
providedAmount=provided_amount,
collectedAmount=collected_amount,
resultCode="00",
resultDescription=f"{req.transactionType} Status retrieved successfully."
)
logger.info(f"Processed transaction check for {req.transactionType}, ID {req.TransactionId}")
return jsonify(response.to_dict())
except Exception as e:
logger.exception(f"Error processing transaction check: {str(e)}")
return jsonify({
'resultCode': '500',
'resultDescription': f'Internal server error: {str(e)}'
}), 500
@transaction_bp.route('/NewTransactionCheck', methods=['POST'])
@api_key_required
def new_transaction_check() -> ResponseReturnValue:
"""
Endpoint to check status of transactions in asynchronous requests.
This method is used for checking the status of transactions when Simbrella
doesn't receive a callback notification within 5 minutes of the initial request.
Returns:
JSON response with detailed transaction status
"""
try:
# Parse and validate request
data = request.get_json()
if not data:
logger.warning("Invalid JSON payload received")
return jsonify({
'resultCode': '400',
'resultDescription': 'Invalid JSON payload'
}), 400
# Validate required fields
required_fields = ['transactionId', 'debtId', 'transactionType',
'fbnTransactionId', 'origTransactionId', 'customerId']
missing_fields = [field for field in required_fields if field not in data]
if missing_fields:
logger.warning(f"Missing required fields: {', '.join(missing_fields)}")
return jsonify({
'resultCode': '422',
'resultDescription': f'Missing required fields: {", ".join(missing_fields)}'
}), 422
# Create request model
req = NewTransactionCheckRequest.from_dict(data)
# Process new transaction check (this would connect to the business logic)
# For demonstration, we'll return a mock response
# Create transaction data based on transaction type
provided_amount = 0.0
collected_amount = 0.0
if req.transactionType == "Disbursement":
provided_amount = 1000.0
result_description = "Loan Provision is successful"
elif req.transactionType == "Collection":
collected_amount = 500.0
result_description = "Loan Collection is successful"
else: # PenalCharge
collected_amount = 50.0
result_description = "Penal Charge is successful"
# Create transaction data
transaction_data = TransactionData(
transactionId=req.origTransactionId,
providedAmount=provided_amount,
collectedAmount=collected_amount,
resultCode="00",
resultDescription=result_description
)
# Create response
response = NewTransactionCheckResponse(
transactionId=req.transactionId,
data=transaction_data.to_dict(),
resultCode="00",
resultDescription="SUCCESS"
)
logger.info(f"Processed new transaction check for {req.transactionType}, ID {req.transactionId}")
return jsonify(response.to_dict())
except Exception as e:
logger.exception(f"Error processing new transaction check: {str(e)}")
return jsonify({
'resultCode': '500',
'resultDescription': f'Internal server error: {str(e)}'
}), 500
+115
View File
@@ -0,0 +1,115 @@
"""
Middleware module for the Flask application.
"""
from flask import Flask, request, jsonify, g, current_app
from flask.typing import ResponseReturnValue
from functools import wraps
import base64
from typing import Callable, Any, TypeVar, cast
import logging
# Configure logger
logger = logging.getLogger(__name__)
F = TypeVar('F', bound=Callable[..., Any])
def register_middleware(app: Flask) -> None:
"""
Register middleware with the Flask application.
Args:
app: Flask application instance
"""
# Register CORS if needed
try:
from flask_cors import CORS
CORS(app, resources={r"/*": {"origins": app.config.get('CORS_ORIGINS', '*')}})
except ImportError:
logger.warning("flask-cors not installed. CORS support disabled.")
@app.before_request
def before_request() -> None:
"""Process request before it reaches the view function."""
# Log incoming requests
logger.debug(f"Received {request.method} request to {request.path}")
# You can add more global middleware here if needed
def basic_auth_required(f: F) -> F:
"""
Decorator for endpoints that require basic authentication.
Args:
f: Function to decorate
Returns:
Decorated function
"""
@wraps(f)
def decorated(*args: Any, **kwargs: Any) -> ResponseReturnValue:
auth = request.headers.get('Authorization')
if not auth or not auth.startswith('Basic '):
logger.warning("Authentication failed: No Basic Auth header")
return jsonify({
'resultCode': '01',
'resultDescription': 'Authentication required'
}), 401
try:
auth_decoded = base64.b64decode(auth[6:]).decode('utf-8')
username, password = auth_decoded.split(':', 1)
if username != current_app.config['API_USERNAME'] or password != current_app.config['API_PASSWORD']:
logger.warning(f"Authentication failed: Invalid credentials for user {username}")
return jsonify({
'resultCode': '01',
'resultDescription': 'Invalid credentials'
}), 401
g.user = username
logger.debug(f"Authentication successful for user {username}")
return f(*args, **kwargs)
except Exception as e:
logger.error(f"Authentication error: {str(e)}")
return jsonify({
'resultCode': '01',
'resultDescription': 'Invalid authentication format'
}), 401
return cast(F, decorated)
def api_key_required(f: F) -> F:
"""
Decorator for endpoints that require API key authentication.
Args:
f: Function to decorate
Returns:
Decorated function
"""
@wraps(f)
def decorated(*args: Any, **kwargs: Any) -> ResponseReturnValue:
app_id = request.headers.get('appID')
api_key = request.headers.get('apiKey')
if not app_id or not api_key:
logger.warning("API key authentication failed: Missing appID or apiKey")
return jsonify({
'resultCode': '01',
'resultDescription': 'API key authentication required'
}), 401
# Validate against configured API keys
if app_id != current_app.config['SIMBRELLA_APP_ID'] or api_key != current_app.config['SIMBRELLA_API_KEY']:
logger.warning(f"API key authentication failed: Invalid appID or apiKey")
return jsonify({
'resultCode': '01',
'resultDescription': 'Invalid API keys'
}), 401
g.api_client = 'simbrella'
logger.debug(f"API key authentication successful")
return f(*args, **kwargs)
return cast(F, decorated)
+459
View File
@@ -0,0 +1,459 @@
"""
Data models for request and response validation.
"""
from dataclasses import dataclass, field, asdict
from typing import List, Dict, Any, Optional, TypeVar, Type, cast
from datetime import datetime
import json
T = TypeVar('T', bound='BaseModel')
class BaseModel:
"""Base model with serialization capabilities."""
def __init__(self):
self.type_field = None
def to_dict(self) -> Dict[str, Any]:
"""Convert model to dictionary."""
result = {k: v for k, v in asdict(self).items() if v is not None}
# Handle special case for type_field
if hasattr(self, 'type_field') and self.type_field is not None:
result['$type'] = self.type_field
del result['type_field']
return result
def to_json(self) -> str:
"""Convert model to JSON string."""
return json.dumps(self.to_dict())
@classmethod
def from_dict(cls: Type[T], data: Dict[str, Any]) -> T:
"""Create model instance from dictionary."""
# Handle special case for $type field
if '$type' in data:
data_copy = data.copy()
data_copy['type_field'] = data_copy.pop('$type')
data = data_copy
return cls(**{k: v for k, v in data.items() if k in cls.__annotations__})
@dataclass
class EligibilityCheckRequest(BaseModel):
"""Model for eligibility check request."""
type_field: str # This will be mapped to $type in JSON
transactionId: str
countryCode: str
customerId: str
accountId: str
lienAmount: float
channel: str
msisdn: Optional[str] = None
@dataclass
class EligibleOffer(BaseModel):
"""Model for eligible offer."""
minamount: float
maxamount: float
productId: int
offerid: int
Tenor: int
@dataclass
class EligibilityCheckResponse(BaseModel):
"""Model for eligibility check response."""
customerId: str
transactionId: str
eligibleOffers: List[Dict[str, Any]]
resultCode: str
resultDescription: str
msisdn: Optional[str] = None
@dataclass
class SelectOffersRequest(BaseModel):
"""Model for select offers request."""
requestId: str
transactionId: str
customerId: str
accountId: str
msisdn: str
requestedAmount: float
productid: str
channel: str
@dataclass
class Offer(BaseModel):
"""Model for offer details."""
offerId: str
productId: str
amount: float
upfrontPayment: float
interestRate: float
Interest: float
ManagementRate: float
ManagementFee: float
InsuranceRate: float
InsuranceFee: float
VATRate: float
VATamount: float
recommendedRepaymentDates: List[str]
installmentAmount: float
totalRepaymentAmount: float
@dataclass
class SelectOffersResponse(BaseModel):
"""Model for select offers response."""
requestId: str
transactionId: str
customerId: str
accountId: str
offers: List[Dict[str, Any]]
resultCode: str
resultDescription: str
outstandingDebtAmount: Optional[float] = 0
@dataclass
class ProvideLoanRequest(BaseModel):
"""Model for provide loan request."""
type_field: str # This will be mapped to $type in JSON
requestId: str
transactionId: str
customerId: str
accountId: str
productId: str
lienAmount: float
requestedAmount: float
collectionType: int
loanType: int
channel: str
msisdn: Optional[str] = None
@dataclass
class ProvideLoanResponse(BaseModel):
"""Model for provide loan response."""
requestId: str
transactionId: str
customerId: str
accountId: str
resultCode: str
resultDescription: str
msisdn: Optional[str] = None
@dataclass
class LoanInformationRequest(BaseModel):
"""Model for loan information request."""
type_field: str # This will be mapped to $type in JSON
transactionId: str
customerId: str
channel: str
msisdn: Optional[str] = None
@dataclass
class Loan(BaseModel):
"""Model for loan details."""
debtId: str
loanDate: str
dueDate: str
currentLoanAmount: float
initialLoanAmount: float
defaultFee: float
continiousFee: float
productId: str
@dataclass
class LoanInformationResponse(BaseModel):
"""Model for loan information response."""
customerId: str
loans: List[Dict[str, Any]]
resultCode: str
resultDescription: str
totalDebtAmount: Optional[float] = None
@dataclass
class RepaymentRequest(BaseModel):
"""Model for repayment request."""
type_field: str # This will be mapped to $type in JSON
transactionId: str
customerId: str
debtId: str
productId: str
channel: str
msisdn: Optional[str] = None
@dataclass
class RepaymentResponse(BaseModel):
"""Model for repayment response."""
customerId: str
productId: str
debtId: str
resultCode: str
resultDescription: str
@dataclass
class CustomerConsentRequest(BaseModel):
"""Model for customer consent request."""
type_field: str # This will be mapped to $type in JSON
transactionId: str
customerId: str
accountId: str
requestTime: str
consentType: str
channel: str
@dataclass
class CustomerConsentResponse(BaseModel):
"""Model for customer consent response."""
resultCode: str
resultDescription: str
@dataclass
class NotificationCallbackRequest(BaseModel):
"""Model for notification callback request."""
fbnTransactionId: str
transactionId: str
customerId: str
accountId: str
debtId: str
transactionType: str
amountProvided: float
amountCollected: float
responseCode: str
responseDescription: str
@dataclass
class NotificationCallbackResponse(BaseModel):
"""Model for notification callback response."""
resultCode: str
resultDescription: str
@dataclass
class RACCheckRequest(BaseModel):
"""Model for RAC check request."""
transactionId: str
fbnTransactionId: str
customerId: str
accountId: str
RAC_Array: List[str]
@dataclass
class RACResponse(BaseModel):
"""Model for RAC response details."""
SalaryAccount: Optional[str] = None
BVN: Optional[str] = None
BVNAttachedtoAccount: Optional[str] = None
CRMS: Optional[str] = None
CRC: Optional[str] = None
AccountStatus: Optional[str] = None
Lien: Optional[str] = None
NoBounchedCheck: Optional[str] = None
Whitelist: Optional[str] = None
NoPastDueSalaryLoan: Optional[str] = None
NoPastDueOtherLoan: Optional[str] = None
@dataclass
class RACCheckResponse(BaseModel):
"""Model for RAC check response."""
RACResponse: Dict[str, Any]
resultCode: str
resultDescription: str
@dataclass
class DisbursementRequest(BaseModel):
"""Model for disbursement request."""
requestId: str
debtId: str
transactionId: str
customerId: str
accountId: str
productId: str
provideAmount: float
countryId: str
collectAmountInterest: Optional[float] = None
collectAmountMgtFee: Optional[float] = None
collectAmountInsurance: Optional[float] = None
collectAmountVAT: Optional[float] = None
comment: Optional[str] = None
@dataclass
class DisbursementResponse(BaseModel):
"""Model for disbursement response."""
requestId: str
debtId: str
transactionId: str
customerId: str
accountId: str
productId: str
provideAmount: float
resultCode: str
resultDescription: str
collectAmountInterest: Optional[float] = None
collectAmountMgtFee: Optional[float] = None
collectAmountInsurance: Optional[float] = None
collectAmountVAT: Optional[float] = None
@dataclass
class CollectLoanRequest(BaseModel):
"""Model for collect loan request."""
transactionId: str
fbnTransactionId: str
debtId: str
customerId: str
accountId: str
productId: str
collectAmount: float
collectionMethod: int
lienAmount: float
countryId: str
penalCharge: Optional[float] = 0.0
comment: Optional[str] = None
@dataclass
class CollectLoanResponse(BaseModel):
"""Model for collect loan response."""
transactionId: str
debtId: str
customerId: str
accountId: str
productId: str
collectAmount: float
lienAmount: float
resultCode: str
resultDescription: str
penalCharge: Optional[float] = 0.0
@dataclass
class TransactionCheckRequest(BaseModel):
"""Model for transaction check request."""
counter: str
TransactionId: str
requestID: str
customerId: str
accountId: str
countryId: str
transactionType: str
@dataclass
class TransactionCheckResponse(BaseModel):
"""Model for transaction check response."""
type_field: str # This will be mapped to $type in JSON
nativeId: str
customerId: str
accountId: str
providedAmount: float
collectedAmount: float
resultCode: str
resultDescription: str
@dataclass
class PenalChargeRequest(BaseModel):
"""Model for penal charge request."""
transactionId: str
fbnTransactionId: str
debtId: str
customerId: str
accountId: str
penalCharge: float
lienAmount: float
countryId: str
comment: Optional[str] = None
@dataclass
class PenalChargeResponse(BaseModel):
"""Model for penal charge response."""
resultCode: str
resultDescription: str
@dataclass
class RevokeEnableConsentRequest(BaseModel):
"""Model for revoke/enable consent request."""
transactionId: str
fbnTransactionId: str
customerId: str
accountId: str
processTime: str
consentType: str
countryId: str
comment: Optional[str] = None
@dataclass
class RevokeEnableConsentResponse(BaseModel):
"""Model for revoke/enable consent response."""
type_field: str # This will be mapped to $type in JSON
customerId: str
accountId: str
resultCode: str
resultDescription: str
@dataclass
class ValidateTokenRequest(BaseModel):
"""Model for validate token request."""
RequestId: str
UserId: str
CountryId: str
TokenCode: str
@dataclass
class ValidateTokenResponse(BaseModel):
"""Model for validate token response."""
Authenticated: bool
AuthenticatedMessage: str
ResponseCode: str
ResponseMessage: str
RequestId: str
@dataclass
class LienCheckRequest(BaseModel):
"""Model for lien check request."""
transactionId: str
customerId: str
accountId: str
countryId: str
@dataclass
class LienCheckResponse(BaseModel):
"""Model for lien check response."""
lienAmount: float
resultCode: str
resultDescription: str
@dataclass
class NewTransactionCheckRequest(BaseModel):
"""Model for new transaction check request."""
transactionId: str
debtId: str
transactionType: str
fbnTransactionId: str
origTransactionId: str
customerId: str
@dataclass
class TransactionData(BaseModel):
"""Model for transaction data."""
transactionId: str
providedAmount: float
collectedAmount: float
resultCode: str
resultDescription: str
@dataclass
class NewTransactionCheckResponse(BaseModel):
"""Model for new transaction check response."""
transactionId: str
data: Dict[str, Any]
resultCode: str
resultDescription: str
@dataclass
class SMSRequest(BaseModel):
"""Model for SMS request."""
text: str
dest: str
unicode: bool
@dataclass
class SMSResponse(BaseModel):
"""Model for SMS response."""
data: str
statusCode: int
IsSuccessful: bool
errorMessage: Optional[str] = None
+55
View File
@@ -0,0 +1,55 @@
"""
Routes module for the Flask application.
"""
from flask import Blueprint, Flask
import logging
# Configure logger
logger = logging.getLogger(__name__)
def register_blueprints(app: Flask) -> None:
"""
Register all blueprints with the Flask application.
Args:
app: Flask application instance
"""
# Import controllers
from api.controllers.eligibility import eligibility_bp
from api.controllers.offers import offers_bp
from api.controllers.loan import loan_bp
from api.controllers.repayment import repayment_bp
from api.controllers.consent import consent_bp
from api.controllers.notification import notification_bp
from api.controllers.rac import rac_bp
from api.controllers.disbursement import disbursement_bp
from api.controllers.collection import collection_bp
from api.controllers.transaction import transaction_bp
from api.controllers.penal import penal_bp
from api.controllers.token import token_bp
from api.controllers.lien import lien_bp
from api.controllers.sms import sms_bp
# Create main API blueprint
api_bp = Blueprint('api', __name__, url_prefix='/v1/api/salary')
# Register feature blueprints
api_bp.register_blueprint(eligibility_bp)
api_bp.register_blueprint(offers_bp)
api_bp.register_blueprint(loan_bp)
api_bp.register_blueprint(repayment_bp)
api_bp.register_blueprint(consent_bp)
api_bp.register_blueprint(notification_bp)
api_bp.register_blueprint(rac_bp)
api_bp.register_blueprint(disbursement_bp)
api_bp.register_blueprint(collection_bp)
api_bp.register_blueprint(transaction_bp)
api_bp.register_blueprint(penal_bp)
api_bp.register_blueprint(token_bp)
api_bp.register_blueprint(lien_bp)
api_bp.register_blueprint(sms_bp)
# Register main blueprint with app
app.register_blueprint(api_bp)
logger.info("All blueprints registered successfully")
+76
View File
@@ -0,0 +1,76 @@
"""
Simbrella FirstAdvance API Flask Implementation
This module serves as the entry point for the Flask application.
"""
from flask import Flask
from flask.typing import ResponseReturnValue
from config import Config
from api.routes import register_blueprints
from api.middleware import register_middleware
import logging
import socket
import sys
def create_app(config_class=Config) -> Flask:
"""
Factory pattern to create the Flask application.
Args:
config_class: Configuration class to use
Returns:
Flask application instance
"""
app = Flask(__name__)
app.config.from_object(config_class)
# Configure logging
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
)
# Register middleware
register_middleware(app)
# Register blueprints
register_blueprints(app)
# Register error handlers
@app.errorhandler(400)
def bad_request(error) -> ResponseReturnValue:
return {"resultCode": "400", "resultDescription": f"Bad request: {str(error)}"}, 400
@app.errorhandler(404)
def not_found(error) -> ResponseReturnValue:
return {"resultCode": "404", "resultDescription": "Resource not found"}, 404
@app.errorhandler(422)
def validation_error(error) -> ResponseReturnValue:
return {"resultCode": "422", "resultDescription": f"Validation error: {str(error)}"}, 422
@app.errorhandler(500)
def server_error(error) -> ResponseReturnValue:
return {"resultCode": "500", "resultDescription": "Internal server error"}, 500
return app
if __name__ == '__main__':
app = create_app()
# Get port from config or use a default
port = app.config.get('PORT', 5000)
# Try to run the app, with better error handling for socket issues
try:
app.run(debug=app.config.get('DEBUG', False), host='0.0.0.0', port=port)
except socket.error as e:
if e.errno == 10013: # Permission denied error on Windows
print(f"Error: Permission denied when trying to bind to port {port}.")
print("Try one of the following solutions:")
print(f"1. Use a different port (above 1024): set PORT=8080 in your environment variables")
print("2. Run the application with administrator privileges")
print("3. Check if another application is already using this port")
else:
print(f"Socket error: {e}")
sys.exit(1)
+40
View File
@@ -0,0 +1,40 @@
"""
Configuration module for the Flask application.
"""
import os
from typing import Any, Dict, List
from dataclasses import dataclass, field
def get_cors_origins() -> List[str]:
"""Get CORS origins from environment variable."""
return os.environ.get('CORS_ORIGINS', '*').split(',')
@dataclass
class Config:
"""Base configuration class."""
DEBUG: bool = os.environ.get('DEBUG', 'False').lower() == 'true'
TESTING: bool = os.environ.get('TESTING', 'False').lower() == 'true'
PORT: int = int(os.environ.get('PORT', 8080)) # Changed default port to 8080
# API credentials
API_USERNAME: str = os.environ.get('API_USERNAME', 'admin')
API_PASSWORD: str = os.environ.get('API_PASSWORD', 'password')
# API keys for Simbrella to FirstBank API
SIMBRELLA_APP_ID: str = os.environ.get('SIMBRELLA_APP_ID', '')
SIMBRELLA_API_KEY: str = os.environ.get('SIMBRELLA_API_KEY', '')
# Database configuration
DATABASE_URI: str = os.environ.get('DATABASE_URI', 'sqlite:///app.db')
# Logging configuration
LOG_LEVEL: str = os.environ.get('LOG_LEVEL', 'INFO')
# CORS settings
CORS_ORIGINS: List[str] = field(default_factory=get_cors_origins)
@classmethod
def to_dict(cls) -> Dict[str, Any]:
"""Convert config to dictionary for Flask."""
return {k: v for k, v in cls.__dict__.items()
if not k.startswith('__') and not callable(v)}
+2
View File
@@ -0,0 +1,2 @@
Flask~=3.1.0
requests~=2.32.3
+173
View File
@@ -0,0 +1,173 @@
import requests
import base64
import json
# Configuration
BASE_URL = "http://127.0.0.1:8080/v1/api/salary"
USERNAME = "admin"
PASSWORD = "password"
APP_ID = "your_app_id" # Replace with your actual app ID
API_KEY = "your_api_key" # Replace with your actual API key
# Authentication headers
basic_auth = base64.b64encode(f"{USERNAME}:{PASSWORD}".encode()).decode()
basic_auth_headers = {
"Content-Type": "application/json",
"Authorization": f"Basic {basic_auth}"
}
api_key_headers = {
"Content-Type": "application/json",
"appID": APP_ID,
"apiKey": API_KEY
}
def test_eligibility_check():
"""Test the EligibilityCheck endpoint."""
url = f"{BASE_URL}/EligibilityCheck"
payload = {
"$type": "EligibilityCheckRequest",
"transactionId": "Tr202503RK9232P115",
"countryCode": "NGR",
"customerId": "CN621868",
"accountId": "ACN8263457",
"msisdn": "2348012345678",
"lienAmount": 4.0,
"channel": "USSD"
}
response = requests.post(url, headers=basic_auth_headers, json=payload)
print(f"EligibilityCheck Status: {response.status_code}")
print(json.dumps(response.json(), indent=2))
print("-" * 50)
return response.json()
def test_select_offer():
"""Test the SelectOffer endpoint."""
url = f"{BASE_URL}/SelectOffer"
payload = {
"requestId": "202503170001371256908",
"transactionId": "1231231321232",
"customerId": "1256907",
"accountId": "5948306019",
"msisdn": "2348012345678",
"requestedAmount": 10000.0,
"productid": "101",
"channel": "USSD"
}
response = requests.post(url, headers=basic_auth_headers, json=payload)
print(f"SelectOffer Status: {response.status_code}")
print(json.dumps(response.json(), indent=2))
print("-" * 50)
return response.json()
def test_provide_loan():
"""Test the ProvideLoan endpoint."""
url = f"{BASE_URL}/ProvideLoan"
payload = {
"$type": "ProvideLoanRequest",
"requestId": "202503170001371256908",
"transactionId": "Tr202503RK9232P115",
"customerId": "CN621868",
"accountId": "ACN8263457",
"msisdn": "2348012345678",
"productId": "101",
"lienAmount": 400.0,
"requestedAmount": 10000.0,
"collectionType": 1,
"loanType": 0,
"channel": "USSD"
}
response = requests.post(url, headers=basic_auth_headers, json=payload)
print(f"ProvideLoan Status: {response.status_code}")
print(json.dumps(response.json(), indent=2))
print("-" * 50)
return response.json()
def test_loan_information():
"""Test the LoanInformation endpoint."""
url = f"{BASE_URL}/LoanInformation"
payload = {
"$type": "LoanInformationRequest",
"transactionId": "Tr202503RK9232P115",
"customerId": "CN621868",
"msisdn": "2348012345678",
"channel": "USSD"
}
response = requests.post(url, headers=basic_auth_headers, json=payload)
print(f"LoanInformation Status: {response.status_code}")
print(json.dumps(response.json(), indent=2))
print("-" * 50)
return response.json()
def test_rac_check():
"""Test the RACCheck endpoint."""
url = f"{BASE_URL}/RACCheck"
payload = {
"transactionId": "T001",
"fbnTransactionId": "Tr202503RK9232P115",
"customerId": "CN621868",
"accountId": "2017821799",
"RAC_Array": ["SalaryAccount", "BVN", "CRMS", "CRC", "AccountStatus", "Lien", "NoBounchedCheck", "Whitelist"]
}
response = requests.post(url, headers=api_key_headers, json=payload)
print(f"RACCheck Status: {response.status_code}")
print(json.dumps(response.json(), indent=2))
print("-" * 50)
return response.json()
def test_disbursement():
"""Test the Disbursement endpoint."""
url = f"{BASE_URL}/Disbursement"
payload = {
"requestId": "202503170001371256908",
"debtId": "273194670",
"transactionId": "T001",
"customerId": "CN621868",
"accountId": "2017821799",
"productId": "101",
"provideAmount": 100000.0,
"collectAmountInterest": 5000.0,
"collectAmountMgtFee": 1000.0,
"collectAmountInsurance": 1000.0,
"collectAmountVAT": 75.0,
"countryId": "01",
"comment": "Testing LoanRequest"
}
response = requests.post(url, headers=api_key_headers, json=payload)
print(f"Disbursement Status: {response.status_code}")
print(json.dumps(response.json(), indent=2))
print("-" * 50)
return response.json()
def run_all_tests():
"""Run all test functions."""
print("Starting API tests...\n")
# Test endpoints with basic auth
test_eligibility_check()
test_select_offer()
test_provide_loan()
test_loan_information()
# Test endpoints with API key auth
test_rac_check()
test_disbursement()
print("All tests completed!")
if __name__ == "__main__":
run_all_tests()