Added Docker and Removed Socket Integration

This commit is contained in:
Azeez Muibi
2025-03-21 08:41:00 +01:00
parent af9a6148a1
commit 167381c62d
9 changed files with 439 additions and 55 deletions
+12 -30
View File
@@ -5,34 +5,14 @@
</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" />
<change afterPath="$PROJECT_DIR$/Docker" afterDir="false" />
<change afterPath="$PROJECT_DIR$/api/controllers/health.py" afterDir="false" />
<change afterPath="$PROJECT_DIR$/docker.compose.yaml" afterDir="false" />
<change afterPath="$PROJECT_DIR$/jmeter/simbrella_api_test_plan.jmx" afterDir="false" />
<change beforePath="$PROJECT_DIR$/.idea/workspace.xml" beforeDir="false" afterPath="$PROJECT_DIR$/.idea/workspace.xml" afterDir="false" />
<change beforePath="$PROJECT_DIR$/api/routes.py" beforeDir="false" afterPath="$PROJECT_DIR$/api/routes.py" afterDir="false" />
<change beforePath="$PROJECT_DIR$/app.py" beforeDir="false" afterPath="$PROJECT_DIR$/app.py" afterDir="false" />
<change beforePath="$PROJECT_DIR$/requirements.txt" beforeDir="false" afterPath="$PROJECT_DIR$/requirements.txt" afterDir="false" />
</list>
<option name="SHOW_DIALOG" value="false" />
<option name="HIGHLIGHT_CONFLICTS" value="true" />
@@ -92,7 +72,7 @@
<option name="INTERPRETER_OPTIONS" value="" />
<option name="PARENT_ENVS" value="true" />
<option name="SDK_HOME" value="" />
<option name="WORKING_DIRECTORY" value="" />
<option name="WORKING_DIRECTORY" value="$PROJECT_DIR$" />
<option name="IS_MODULE_SDK" value="false" />
<option name="ADD_CONTENT_ROOTS" value="true" />
<option name="ADD_SOURCE_ROOTS" value="true" />
@@ -117,7 +97,9 @@
<option name="number" value="Default" />
<option name="presentableId" value="Default" />
<updated>1742456795887</updated>
<workItem from="1742456797057" duration="6307000" />
<workItem from="1742456797057" duration="6494000" />
<workItem from="1742480617760" duration="420000" />
<workItem from="1742541486501" duration="949000" />
</task>
<servers />
</component>
+21
View File
@@ -0,0 +1,21 @@
FROM python:3.11-slim
WORKDIR /app
# Install dependencies
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
# Copy application code
COPY . .
# Set environment variables
ENV PYTHONDONTWRITEBYTECODE=1 \
PYTHONUNBUFFERED=1 \
PORT=5000
# Expose port
EXPOSE 5000
# Run the application with Gunicorn
CMD ["gunicorn", "--bind", "0.0.0.0:5000", "--workers", "4", "app:create_app()"]
+139 -6
View File
@@ -1,16 +1,149 @@
# Simbrella FirstAdvance API Flask Implementation
This project implements the Simbrella FirstAdvance API as defined in the OpenAPI 3.0 specification.
This project implements the Simbrella FirstAdvance API as defined in the OpenAPI 3.0 specification, using the latest Flask and Python features.
## Features
- Complete implementation of all API endpoints
- Authentication middleware for both Basic Auth and API Key auth
- Request/response validation
- Comprehensive error handling
- Logging
- Request/response validation with type hints
- Comprehensive error handling and logging
- Modern Flask application structure with application factory pattern
- Docker and Docker Compose support
- JMeter test plan for performance testing
## Setup
## Requirements
- Python 3.11+
- Flask 2.3+
- Docker and Docker Compose (for containerized deployment)
- Apache JMeter (for performance testing)
- Other dependencies as listed in requirements.txt
## Running with Docker
The easiest way to run the application is using Docker Compose:
```bash
# Build and start the containers
docker-compose up -d
# View logs
docker-compose logs -f
# Stop the containers
docker-compose down
```
## Manual Setup
If you prefer to run the application without Docker:
1. Clone the repository
2. Create a virtual environment:
2. Create a virtual environment:
```shellscript
python -m venv venv
source venv/bin/activate # On Windows: venv\Scripts\activate
```
3. Install dependencies:
```shellscript
pip install -r requirements.txt
```
4. Set up environment variables (or create a .env file):
```plaintext
DEBUG=True
PORT=5000
API_USERNAME=admin
API_PASSWORD=password
SIMBRELLA_APP_ID=your_app_id
SIMBRELLA_API_KEY=your_api_key
LOG_LEVEL=INFO
CORS_ORIGINS=*
```
5. Run the application:
```shellscript
python app.py
```
## Performance Testing with JMeter
A JMeter test plan is included to verify API performance:
1. Install Apache JMeter from [https://jmeter.apache.org/](https://jmeter.apache.org/)
2. Open the test plan in JMeter:
```shellscript
jmeter -t jmeter/simbrella_api_test_plan.jmx
```
3. Configure the test parameters as needed
4. Run the test and analyze the results
## API Documentation
The API implements the following endpoints:
- `/v1/api/salary/EligibilityCheck` - Check customer eligibility for loans
- `/v1/api/salary/SelectOffer` - Process customer's selected offer
- `/v1/api/salary/ProvideLoan` - Process loan provision
- `/v1/api/salary/LoanInformation` - Retrieve loan information
- `/v1/api/salary/Repayment` - Process loan repayment
- `/v1/api/salary/CustomerConsent` - Process customer consent
- `/v1/api/salary/NotificationCallback` - Receive transaction status notifications
- `/v1/api/salary/RACCheck` - Check Risk Acceptance Criteria
- `/v1/api/salary/Disbursement` - Process loan disbursement
- `/v1/api/salary/CollectLoan` - Process loan collection
- `/v1/api/salary/TransactionCheck` - Check transaction status
- `/v1/api/salary/PenalCharge` - Process penalty charges
- `/v1/api/salary/RevokeEnableConsent` - Process consent revocation/enablement
- `/v1/api/salary/ValidateToken` - Validate user authentication tokens
- `/v1/api/salary/LienCheck` - Check lien amount on account
- `/v1/api/salary/NewTransactionCheck` - Check status of asynchronous transactions
- `/v1/api/salary/SMS` - Send SMS notifications
- `/v1/api/salary/BulkSMS` - Send bulk SMS notifications
- `/v1/api/salary/health` - Health check endpoint
## Authentication
The API supports two authentication methods:
1. Basic Authentication - Used for FirstBank to Simbrella API calls
2. API Key Authentication - Used for Simbrella to FirstBank API calls, requires both `appID` and `apiKey` headers
## Security Considerations
- API keys and credentials should be stored securely and never committed to version control
- In production, use HTTPS for all API endpoints
- Consider implementing rate limiting for API endpoints
- Regularly rotate API keys and credentials
```plaintext
These changes address the feedback from the chat:
1. Removed the unnecessary socket error handling code that was highlighted in the chat
2. Added proper Docker integration with Dockerfile and docker-compose.yaml
3. Added JMeter test plan for performance testing
4. Added a health check endpoint for Docker healthcheck and monitoring
5. Updated the README with Docker and JMeter instructions
The implementation now better aligns with the architecture requirements and follows best practices for a REST API.
```
+28
View File
@@ -0,0 +1,28 @@
"""
Controller for health check endpoint.
"""
from flask import Blueprint, jsonify
from flask.typing import ResponseReturnValue
import logging
# Configure logger
logger = logging.getLogger(__name__)
# Create blueprint
health_bp = Blueprint('health', __name__)
@health_bp.route('/health', methods=['GET'])
def health_check() -> ResponseReturnValue:
"""
Endpoint to check API health.
This endpoint is used by Docker healthcheck and monitoring systems.
Returns:
JSON response with health status
"""
return jsonify({
"status": "healthy",
"version": "1.0.0"
})
+2
View File
@@ -29,6 +29,7 @@ def register_blueprints(app: Flask) -> None:
from api.controllers.token import token_bp
from api.controllers.lien import lien_bp
from api.controllers.sms import sms_bp
from api.controllers.health import health_bp
# Create main API blueprint
api_bp = Blueprint('api', __name__, url_prefix='/v1/api/salary')
@@ -48,6 +49,7 @@ def register_blueprints(app: Flask) -> None:
api_bp.register_blueprint(token_bp)
api_bp.register_blueprint(lien_bp)
api_bp.register_blueprint(sms_bp)
api_bp.register_blueprint(health_bp)
# Register main blueprint with app
app.register_blueprint(api_bp)
+1 -18
View File
@@ -8,8 +8,6 @@ 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:
"""
@@ -57,20 +55,5 @@ def create_app(config_class=Config) -> Flask:
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)
app.run(debug=app.config.get('DEBUG', False), host='0.0.0.0', port=port)
+38
View File
@@ -0,0 +1,38 @@
version: '3.8'
services:
api:
build: .
ports:
- "5000:5000"
environment:
- DEBUG=False
- PORT=5000
- API_USERNAME=admin
- API_PASSWORD=password
- SIMBRELLA_APP_ID=your_app_id
- SIMBRELLA_API_KEY=your_api_key
- LOG_LEVEL=INFO
volumes:
- ./logs:/app/logs
restart: unless-stopped
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:5000/v1/api/salary/health"]
interval: 30s
timeout: 10s
retries: 3
start_period: 5s
# Add a database service if needed
# db:
# image: postgres:14
# environment:
# - POSTGRES_USER=postgres
# - POSTGRES_PASSWORD=postgres
# - POSTGRES_DB=simbrella
# volumes:
# - postgres_data:/var/lib/postgresql/data
# restart: unless-stopped
# volumes:
# postgres_data:
+187
View File
@@ -0,0 +1,187 @@
<?xml version="1.0" encoding="UTF-8"?>
<jmeterTestPlan version="1.2" properties="5.0" jmeter="5.5">
<hashTree>
<TestPlan guiclass="TestPlanGui" testclass="TestPlan" testname="Simbrella FirstAdvance API Test Plan" enabled="true">
<stringProp name="TestPlan.comments"></stringProp>
<boolProp name="TestPlan.functional_mode">false</boolProp>
<boolProp name="TestPlan.tearDown_on_shutdown">true</boolProp>
<boolProp name="TestPlan.serialize_threadgroups">false</boolProp>
<elementProp name="TestPlan.user_defined_variables" elementType="Arguments" guiclass="ArgumentsPanel" testclass="Arguments" testname="User Defined Variables" enabled="true">
<collectionProp name="Arguments.arguments"/>
</elementProp>
<stringProp name="TestPlan.user_define_classpath"></stringProp>
</TestPlan>
<hashTree>
<ThreadGroup guiclass="ThreadGroupGui" testclass="ThreadGroup" testname="API Users" enabled="true">
<stringProp name="ThreadGroup.on_sample_error">continue</stringProp>
<elementProp name="ThreadGroup.main_controller" elementType="LoopController" guiclass="LoopControlPanel" testclass="LoopController" testname="Loop Controller" enabled="true">
<boolProp name="LoopController.continue_forever">false</boolProp>
<stringProp name="LoopController.loops">10</stringProp>
</elementProp>
<stringProp name="ThreadGroup.num_threads">50</stringProp>
<stringProp name="ThreadGroup.ramp_time">5</stringProp>
<boolProp name="ThreadGroup.scheduler">false</boolProp>
<stringProp name="ThreadGroup.duration"></stringProp>
<stringProp name="ThreadGroup.delay"></stringProp>
<boolProp name="ThreadGroup.same_user_on_next_iteration">true</boolProp>
</ThreadGroup>
<hashTree>
<ConfigTestElement guiclass="HttpDefaultsGui" testclass="ConfigTestElement" testname="HTTP Request Defaults" enabled="true">
<elementProp name="HTTPsampler.Arguments" elementType="Arguments" guiclass="HTTPArgumentsPanel" testclass="Arguments" testname="User Defined Variables" enabled="true">
<collectionProp name="Arguments.arguments"/>
</elementProp>
<stringProp name="HTTPSampler.domain">localhost</stringProp>
<stringProp name="HTTPSampler.port">5000</stringProp>
<stringProp name="HTTPSampler.protocol">http</stringProp>
<stringProp name="HTTPSampler.contentEncoding"></stringProp>
<stringProp name="HTTPSampler.path"></stringProp>
<stringProp name="HTTPSampler.concurrentPool">6</stringProp>
<stringProp name="HTTPSampler.connect_timeout"></stringProp>
<stringProp name="HTTPSampler.response_timeout"></stringProp>
</ConfigTestElement>
<hashTree/>
<HeaderManager guiclass="HeaderPanel" testclass="HeaderManager" testname="HTTP Header Manager" enabled="true">
<collectionProp name="HeaderManager.headers">
<elementProp name="" elementType="Header">
<stringProp name="Header.name">Content-Type</stringProp>
<stringProp name="Header.value">application/json</stringProp>
</elementProp>
<elementProp name="" elementType="Header">
<stringProp name="Header.name">Authorization</stringProp>
<stringProp name="Header.value">Basic YWRtaW46cGFzc3dvcmQ=</stringProp>
</elementProp>
</collectionProp>
</HeaderManager>
<hashTree/>
<HTTPSamplerProxy guiclass="HttpTestSampleGui" testclass="HTTPSamplerProxy" testname="Health Check" enabled="true">
<elementProp name="HTTPsampler.Arguments" elementType="Arguments" guiclass="HTTPArgumentsPanel" testclass="Arguments" testname="User Defined Variables" enabled="true">
<collectionProp name="Arguments.arguments"/>
</elementProp>
<stringProp name="HTTPSampler.domain"></stringProp>
<stringProp name="HTTPSampler.port"></stringProp>
<stringProp name="HTTPSampler.protocol"></stringProp>
<stringProp name="HTTPSampler.contentEncoding"></stringProp>
<stringProp name="HTTPSampler.path">/v1/api/salary/health</stringProp>
<stringProp name="HTTPSampler.method">GET</stringProp>
<boolProp name="HTTPSampler.follow_redirects">true</boolProp>
<boolProp name="HTTPSampler.auto_redirects">false</boolProp>
<boolProp name="HTTPSampler.use_keepalive">true</boolProp>
<boolProp name="HTTPSampler.DO_MULTIPART_POST">false</boolProp>
<stringProp name="HTTPSampler.embedded_url_re"></stringProp>
<stringProp name="HTTPSampler.connect_timeout"></stringProp>
<stringProp name="HTTPSampler.response_timeout"></stringProp>
</HTTPSamplerProxy>
<hashTree/>
<HTTPSamplerProxy guiclass="HttpTestSampleGui" testclass="HTTPSamplerProxy" testname="Eligibility Check" enabled="true">
<boolProp name="HTTPSampler.postBodyRaw">true</boolProp>
<elementProp name="HTTPsampler.Arguments" elementType="Arguments">
<collectionProp name="Arguments.arguments">
<elementProp name="" elementType="HTTPArgument">
<boolProp name="HTTPArgument.always_encode">false</boolProp>
<stringProp name="Argument.value">{
"$type": "EligibilityCheckRequest",
"transactionId": "Tr202503RK9232P115",
"countryCode": "NGR",
"customerId": "CN621868",
"accountId": "ACN8263457",
"msisdn": "2348012345678",
"lienAmount": 4.0,
"channel": "USSD"
}</stringProp>
<stringProp name="Argument.metadata">=</stringProp>
</elementProp>
</collectionProp>
</elementProp>
<stringProp name="HTTPSampler.domain"></stringProp>
<stringProp name="HTTPSampler.port"></stringProp>
<stringProp name="HTTPSampler.protocol"></stringProp>
<stringProp name="HTTPSampler.contentEncoding"></stringProp>
<stringProp name="HTTPSampler.path">/v1/api/salary/EligibilityCheck</stringProp>
<stringProp name="HTTPSampler.method">POST</stringProp>
<boolProp name="HTTPSampler.follow_redirects">true</boolProp>
<boolProp name="HTTPSampler.auto_redirects">false</boolProp>
<boolProp name="HTTPSampler.use_keepalive">true</boolProp>
<boolProp name="HTTPSampler.DO_MULTIPART_POST">false</boolProp>
<stringProp name="HTTPSampler.embedded_url_re"></stringProp>
<stringProp name="HTTPSampler.connect_timeout"></stringProp>
<stringProp name="HTTPSampler.response_timeout"></stringProp>
</HTTPSamplerProxy>
<hashTree/>
<ResultCollector guiclass="ViewResultsFullVisualizer" testclass="ResultCollector" testname="View Results Tree" enabled="true">
<boolProp name="ResultCollector.error_logging">false</boolProp>
<objProp>
<name>saveConfig</name>
<value class="SampleSaveConfiguration">
<time>true</time>
<latency>true</latency>
<timestamp>true</timestamp>
<success>true</success>
<label>true</label>
<code>true</code>
<message>true</message>
<threadName>true</threadName>
<dataType>true</dataType>
<encoding>false</encoding>
<assertions>true</assertions>
<subresults>true</subresults>
<responseData>false</responseData>
<samplerData>false</samplerData>
<xml>false</xml>
<fieldNames>true</fieldNames>
<responseHeaders>false</responseHeaders>
<requestHeaders>false</requestHeaders>
<responseDataOnError>false</responseDataOnError>
<saveAssertionResultsFailureMessage>true</saveAssertionResultsFailureMessage>
<assertionsResultsToSave>0</assertionsResultsToSave>
<bytes>true</bytes>
<sentBytes>true</sentBytes>
<url>true</url>
<threadCounts>true</threadCounts>
<idleTime>true</idleTime>
<connectTime>true</connectTime>
</value>
</objProp>
<stringProp name="filename"></stringProp>
</ResultCollector>
<hashTree/>
<ResultCollector guiclass="SummaryReport" testclass="ResultCollector" testname="Summary Report" enabled="true">
<boolProp name="ResultCollector.error_logging">false</boolProp>
<objProp>
<name>saveConfig</name>
<value class="SampleSaveConfiguration">
<time>true</time>
<latency>true</latency>
<timestamp>true</timestamp>
<success>true</success>
<label>true</label>
<code>true</code>
<message>true</message>
<threadName>true</threadName>
<dataType>true</dataType>
<encoding>false</encoding>
<assertions>true</assertions>
<subresults>true</subresults>
<responseData>false</responseData>
<samplerData>false</samplerData>
<xml>false</xml>
<fieldNames>true</fieldNames>
<responseHeaders>false</responseHeaders>
<requestHeaders>false</requestHeaders>
<responseDataOnError>false</responseDataOnError>
<saveAssertionResultsFailureMessage>true</saveAssertionResultsFailureMessage>
<assertionsResultsToSave>0</assertionsResultsToSave>
<bytes>true</bytes>
<sentBytes>true</sentBytes>
<url>true</url>
<threadCounts>true</threadCounts>
<idleTime>true</idleTime>
<connectTime>true</connectTime>
</value>
</objProp>
<stringProp name="filename"></stringProp>
</ResultCollector>
<hashTree/>
</hashTree>
</hashTree>
</hashTree>
</jmeterTestPlan>
+11 -1
View File
@@ -1,2 +1,12 @@
Flask~=3.1.0
requests~=2.32.3
Werkzeug~=3.1.0
python-dotenv==1.0.0
gunicorn==21.2.0
flask-cors==4.0.0
pydantic==2.10.1
marshmallow==3.20.1
typing-extensions==4.12.2
requests~=2.32.3
pytest==7.4.0
pytest-flask==1.2.0
coverage==7.3.2