Initial commit
This commit is contained in:
@@ -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
|
||||
Generated
+21
@@ -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
@@ -0,0 +1,6 @@
|
||||
<component name="InspectionProjectProfileManager">
|
||||
<settings>
|
||||
<option name="USE_PROJECT_PROFILE" value="false" />
|
||||
<version value="1.0" />
|
||||
</settings>
|
||||
</component>
|
||||
Generated
+6
@@ -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>
|
||||
Generated
+8
@@ -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
@@ -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>
|
||||
Generated
+130
@@ -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">{
|
||||
"associatedIndex": 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">{
|
||||
"keyToString": {
|
||||
"Flask server.FirstBankSimbrellaApi.executor": "Run",
|
||||
"RunOnceActivity.ShowReadmeOnStart": "true",
|
||||
"RunOnceActivity.git.unshallow": "true",
|
||||
"git-widget-placeholder": "master",
|
||||
"last_opened_file_path": "C:/Users/amuibi/PycharmProjects/FirstBankSimbrellaApi",
|
||||
"node.js.detected.package.eslint": "true",
|
||||
"node.js.detected.package.tslint": "true",
|
||||
"node.js.selected.package.eslint": "(autodetect)",
|
||||
"node.js.selected.package.tslint": "(autodetect)",
|
||||
"nodejs_package_manager_path": "npm",
|
||||
"vue.rearranger.settings.migration": "true"
|
||||
}
|
||||
}</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>
|
||||
@@ -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.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
@@ -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
|
||||
@@ -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")
|
||||
@@ -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)
|
||||
@@ -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)}
|
||||
@@ -0,0 +1,2 @@
|
||||
Flask~=3.1.0
|
||||
requests~=2.32.3
|
||||
+173
@@ -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()
|
||||
Reference in New Issue
Block a user