17 Commits

Author SHA1 Message Date
VivianDee 33ee8a286b [fix]: request schems 2025-07-30 05:24:02 +01:00
VivianDee 038c5323b0 [add]: eco endpoints and dummy responses 2025-07-30 05:16:47 +01:00
ameye cfc40b89dc Merge branch 'health_check' of DigiFi/digifi-BankEmulator into master 2025-06-28 13:50:03 +00:00
VivianDee eb7c0f6221 [add]: health check 2025-06-27 15:58:12 +01:00
ameye d69bcd11ea Merge branch 'randomize_rac_check_values' of DigiFi/digifi-BankEmulator into master 2025-06-25 17:32:57 +00:00
VivianDee c4d7db5c59 Update rac_check.py 2025-06-25 17:31:02 +01:00
CHIEFSOFT\ameye d359532775 Fbn trax 2025-06-21 10:52:09 -04:00
CHIEFSOFT\ameye 2b12e1ab0b Partial simulation 2025-06-21 10:41:53 -04:00
vivian.d.simbrellang.com 23b352d9c3 Merge branch 'rac_check' of DigiFi/digifi-BankEmulator into master 2025-06-12 14:33:11 +00:00
VivianDee f665d7b8e4 Update rac_check.py 2025-06-12 15:32:00 +01:00
ameye 855f343626 Merge branch 'sync_payload' of DigiFi/digifi-BankEmulator into master 2025-06-09 19:40:35 +00:00
Chinenye Nmoh e7279a8c65 expanded disbursement endpoint 2025-06-09 20:31:32 +01:00
CHIEFSOFT\ameye 3e4fad5418 responseCode 2025-06-05 22:50:45 -04:00
CHIEFSOFT\ameye 4b018a26e9 res 2025-06-05 22:46:46 -04:00
CHIEFSOFT\ameye dfdfa51583 res[pose code 2025-06-05 22:43:38 -04:00
CHIEFSOFT\ameye 82053f41ce Fix emulator data 2025-06-05 18:18:56 -04:00
ameye 0833d0d0f2 Merge branch 'rac_check_update' of DigiFi/digifi-BankEmulator into master 2025-06-05 14:49:16 +00:00
54 changed files with 1244 additions and 70 deletions
Vendored
BIN
View File
Binary file not shown.
+5 -2
View File
@@ -55,7 +55,8 @@ This command will build the Docker image and start the Flask application in a co
You can check if the Flask application is running by accessing the `/health` endpoint. To perform a health check, run the following command: You can check if the Flask application is running by accessing the `/health` endpoint. To perform a health check, run the following command:
```bash ```bash
curl http://localhost:6337/health curl http://localhost:6337/api/health
curl http://localhost:6337/eco/health
``` ```
If the application is running properly, you should receive a response similar to this: If the application is running properly, you should receive a response similar to this:
@@ -71,7 +72,9 @@ If the application is running properly, you should receive a response similar to
You can check the Swagger Doc by accessing the `/documentation` endpoint. Run the following command: You can check the Swagger Doc by accessing the `/documentation` endpoint. Run the following command:
```bash ```bash
curl http://localhost:6337/documentation curl http://localhost:6337/api/documentation
curl http://localhost:6337/eco/documentation
``` ```
+26 -3
View File
@@ -1,10 +1,18 @@
from re import S
from flask import Flask from flask import Flask
import os import os
from flask_swagger_ui import get_swaggerui_blueprint from flask_swagger_ui import get_swaggerui_blueprint
from flask_cors import CORS from flask_cors import CORS
from app.config import Config from app.config import Config
from app.api.routes import api from app.api.routes import api
from app.eco.routes import eco
from app.errors import register_error_handlers from app.errors import register_error_handlers
from flask_jwt_extended import (
JWTManager,
jwt_required,
create_access_token,
get_jwt_identity,
)
def create_app(): def create_app():
""" Factory function to create a Flask app instance """ """ Factory function to create a Flask app instance """
@@ -14,16 +22,31 @@ def create_app():
app.config.from_object(Config) app.config.from_object(Config)
CORS(app) CORS(app)
JWTManager(app)
CORS(app, supports_credentials=True)
# Swagger Doc # Swagger Doc
SWAGGER_URL = app.config.get("SWAGGER_URL") SWAGGER_URL = app.config.get("SWAGGER_URL")
API_URL = app.config.get("API_URL") API_URL = app.config.get("API_URL")
# Register blueprints with /api prefix for the main API routes # Register blueprints with /api prefix for the main API routes
app.register_blueprint(api, url_prefix='/api') app.register_blueprint(api, url_prefix="/api")
app.register_blueprint(eco, url_prefix="/eco")
# Swagger UI Blueprint
api_docs = "/api" + SWAGGER_URL
api_url = "/api" + API_URL
swagger_ui_api = get_swaggerui_blueprint(api_docs, api_url)
swagger_ui_api.name = 'swagger_ui_api' # Rename blueprint
app.register_blueprint(swagger_ui_api, url_prefix=api_docs)
# Second UI (ECO)
eco_docs = "/eco" + SWAGGER_URL
eco_api = "/eco" + API_URL
swagger_ui_eco = get_swaggerui_blueprint(eco_docs, eco_api)
swagger_ui_eco.name = 'swagger_ui_eco' # Rename blueprint
app.register_blueprint(swagger_ui_eco, url_prefix=eco_docs)
swagger_ui_blueprint = get_swaggerui_blueprint(SWAGGER_URL, API_URL)
app.register_blueprint(swagger_ui_blueprint, url_prefix=SWAGGER_URL)
# Error Handlers # Error Handlers
+31 -2
View File
@@ -1,4 +1,5 @@
from flask import Flask, Blueprint, request, jsonify, send_from_directory from flask import Flask, Blueprint, request, jsonify, send_from_directory
import sys
import os import os
from app.api.services import ( from app.api.services import (
RACCheckService, RACCheckService,
@@ -16,6 +17,7 @@ from app.utils.logger import logger
from app.api.middlewares import require_api_key, require_app_id, enforce_json from app.api.middlewares import require_api_key, require_app_id, enforce_json
api = Blueprint("api", __name__) api = Blueprint("api", __name__)
@@ -162,7 +164,34 @@ def new_transaction_check():
response = NewTransactionCheckService.process_request(data) response = NewTransactionCheckService.process_request(data)
return response return response
# Health Check Endpoint # Health Check Endpoint
@api.route('/health', methods=['GET']) @api.route('/system-health-check', methods=['GET'])
def health_check(): def health_check():
return {"status": "ok"} , 200 """Basic system health check"""
try:
checks = {
"python_version": sys.version_info >= (3, 6),
"disk_space": os.statvfs('/').f_bavail * os.statvfs('/').f_frsize > 500 * 1024 * 1024,
"system_operational": True
}
if all(checks.values()):
return jsonify({
"status": "Active",
"responseCode": "00",
"responseMessage": "Successful"
}), 200
else:
return jsonify({
"status": "Degraded",
"responseCode": "01",
"responseMessage": "System check failed"
}), 200
except Exception as e:
return jsonify({
"status": "Error",
"responseCode": "99",
"responseMessage": f"Health check failed: {str(e)}"
}), 500
+8
View File
@@ -16,6 +16,7 @@ class DisbursementSchema(Schema):
comment = fields.Str(required=False, allow_none=True) comment = fields.Str(required=False, allow_none=True)
class DisburseLoanResponseSchema(Schema): class DisburseLoanResponseSchema(Schema):
transactionId = fields.Str(allow_none=True) transactionId = fields.Str(allow_none=True)
fbnTransactionId = fields.Str(allow_none=True) fbnTransactionId = fields.Str(allow_none=True)
@@ -31,5 +32,12 @@ class DisburseLoanResponseSchema(Schema):
countryId = fields.Str(allow_none=True) countryId = fields.Str(allow_none=True)
responseCode = fields.Str(allow_none=True) responseCode = fields.Str(allow_none=True)
responseMessage = fields.Str(allow_none=True) responseMessage = fields.Str(allow_none=True)
disburseMessage = fields.Str(allow_none=True)
disburseDate = fields.Str(allow_none=True)
disburseVerify = fields.Str(allow_none=True)
disburseDescription = fields.Str(allow_none=True)
verifyResult = fields.Str(allow_none=True)
verifyDescription = fields.Str(allow_none=True)
+4
View File
@@ -19,3 +19,7 @@ class TransactionVerifyResponseSchema(Schema):
collectedAmount = fields.Float(required=True) collectedAmount = fields.Float(required=True)
transactionId = fields.Str(allow_none=True) transactionId = fields.Str(allow_none=True)
transactionType = fields.Str(allow_none=True) transactionType = fields.Str(allow_none=True)
disburseVerify = fields.Str(allow_none=True)
verifyDescription = fields.Str(allow_none=True)
verifyResult = fields.Str(allow_none=True)
+51 -20
View File
@@ -4,18 +4,18 @@ from app.utils.logger import logger
from app.api.helpers.response_helper import ResponseHelper from app.api.helpers.response_helper import ResponseHelper
from app.api.schemas.collect_loan import CollectLoanSchema, CollectLoanResponseSchema from app.api.schemas.collect_loan import CollectLoanSchema, CollectLoanResponseSchema
"""
Process the CollectLoan request.
Args:
data (dict): The request data.
Returns:
tuple: JSON response and status code.
"""
class CollectLoanService: class CollectLoanService:
@staticmethod @staticmethod
def process_request(data): def process_request(data):
"""
Process the CollectLoan request.
Args:
data (dict): The request data.
Returns:
tuple: JSON response and status code.
"""
try: try:
logger.info("Processing CollectLoan request") logger.info("Processing CollectLoan request")
@@ -23,22 +23,53 @@ class CollectLoanService:
schema = CollectLoanSchema() schema = CollectLoanSchema()
validated_data = schema.load(data) validated_data = schema.load(data)
amountForCollection = validated_data.get("collectAmount")
amountCollected = amountForCollection
responseDescr= "Loan Collection Successful EMULATOR"
fullDescription= "Loan collection completed successfully EMULATOR"
responseMessage= "Loan collection completed successfully EMULATOR"
if amountForCollection in [5555.0, 6666.0, 7777.0, 8888.0 , 9999.0, 22222.0] :
amountCollected = amountForCollection * 0.85
responseDescr = "Partial Loan Collection Successful EMULATOR"
fullDescription = "Partial Loan collection completed successfully EMULATOR"
responseMessage = "Partial Loan collection completed successfully EMULATOR"
# Simulated processing logic # Simulated processing logic
response_data = { response_data = {
"transactionId": validated_data.get("transactionId", "T002"), "transactionId": validated_data.get("transactionId"),
"debtId": validated_data.get("debtId", "273194670"), "debtId": validated_data.get("debtId"),
"customerId": validated_data.get("customerId", "CN621868"), "customerId": validated_data.get("customerId"),
"accountId": validated_data.get("accountId", "2017821799"), "accountId": validated_data.get("accountId"),
"productId": validated_data.get("productId", "101"), "productId": validated_data.get("productId"),
"amountCollected": validated_data.get("collectAmount", 60000.00), "amountCollected": amountCollected,
"countryId": validated_data.get("countryId", "01"), "countryId": validated_data.get("countryId"),
"comment": validated_data.get("comment", "Testing CollectionLoanRequest"), "comment": validated_data.get("comment", "Testing CollectionLoanRequest EMULATOR"),
"responseCode": "00", "responseCode": "00",
"responseDescr": "Loan Collection Successful", "responseDescr": responseDescr,
"fullDescription": "Loan collection completed successfully", "fullDescription": fullDescription,
"responseMessage": "Loan collection completed successfully" "responseMessage": responseMessage
} }
# # Simulated processing logic
# response_data = {
# "transactionId": validated_data.get("transactionId", "T002"),
# "debtId": validated_data.get("debtId", "273194670"),
# "customerId": validated_data.get("customerId", "CN621868"),
# "accountId": validated_data.get("accountId", "2017821799"),
# "productId": validated_data.get("productId", "101"),
# "amountCollected": validated_data.get("collectAmount", 60000.00),
# "countryId": validated_data.get("countryId", "01"),
# "comment": validated_data.get("comment", "Testing CollectionLoanRequest"),
# "responseCode": "00",
# "responseDescr": "Loan Collection Successful",
# "fullDescription": "Loan collection completed successfully",
# "responseMessage": "Loan collection completed successfully"
# }
#
# Validate and serialize the response data # Validate and serialize the response data
response_schema = CollectLoanResponseSchema() response_schema = CollectLoanResponseSchema()
result = response_schema.dump(response_data) result = response_schema.dump(response_data)
+6 -2
View File
@@ -2,6 +2,7 @@ from flask import request, jsonify
from marshmallow import ValidationError from marshmallow import ValidationError
from app.utils.logger import logger from app.utils.logger import logger
from app.api.schemas.disbursement import DisbursementSchema, DisburseLoanResponseSchema from app.api.schemas.disbursement import DisbursementSchema, DisburseLoanResponseSchema
import datetime
class DisbursementService: class DisbursementService:
@staticmethod @staticmethod
@@ -26,7 +27,7 @@ class DisbursementService:
# For demo purposes, we simulate a response using the validated data # For demo purposes, we simulate a response using the validated data
response_data = { response_data = {
"transactionId": validated_data.get("transactionId"), "transactionId": validated_data.get("transactionId"),
"FbnTransactionId": validated_data.get("FbnTransactionId"), # Example or generated value "fbnTransactionId": validated_data.get("fbnTransactionId"), # Example or generated value
"debtId": validated_data.get("debtId"), "debtId": validated_data.get("debtId"),
"customerId": validated_data.get("customerId"), "customerId": validated_data.get("customerId"),
"accountId": validated_data.get("accountId"), "accountId": validated_data.get("accountId"),
@@ -38,7 +39,10 @@ class DisbursementService:
"collectAmountVAT": validated_data.get("collectAmountVAT"), "collectAmountVAT": validated_data.get("collectAmountVAT"),
"countryId": validated_data.get("countryId"), "countryId": validated_data.get("countryId"),
"responseCode": "00", # success code example "responseCode": "00", # success code example
"responseMessage": "Loan Request Completed Successfully!" "responseMessage": "Loan Request Completed Successfully!",
"disburseDate": datetime.datetime.now().isoformat(),
"disburseResult": "00",
"disburseDescription": "Loan Request Completed Successfully!",
} }
# Serialize response # Serialize response
+49 -36
View File
@@ -25,53 +25,66 @@ class RACCheckService:
schema = RACCheckSchema() schema = RACCheckSchema()
validated_data = schema.load(data) validated_data = schema.load(data)
# Simulated RAC check logic — create racResponse manually or via logic customer_id = validated_data["customerId"]
# rac_response = { is_valid = not (
# "hasSalaryAccount": True, int(customer_id[-1]) in [2, 7, 9]
# "bvnValidated": True, )
# "creditBureauCheck": False,
# "crmsCheck": True,
# "accountStatus": True, try:
# "hasLien": False, salary_count = int(str(customer_id)[-1]) + 1
# "noBouncedCheck": True, if salary_count < 1 or salary_count > 6:
# "isWhitelisted": True, salary_count = 3
# "hasPastDueLoan": False except ValueError:
# } salary_count = 3
salary_payments = {}
total_salary = 0
for i in range(1, salary_count + 1):
salary = ((salary_count + i ) * 7919) % 200000 + 10000
salary_payments[f"salarypaymenT_{i}"] = salary
total_salary += salary
average_salary = total_salary // salary_count if salary_count > 0 else 0
rac_response = { rac_response = {
"procesS_DATE": datetime.strptime("2025-06-05", "%Y-%m-%d").date(), "procesS_DATE": datetime.strptime("2025-06-05", "%Y-%m-%d").date(),
"ciF_ID": "416405737", "ciF_ID": "416405737",
"customeR_id": "7032744", "customeR_id": customer_id,
"salaccT_1": "4142904114", "salaccT_1": "4142904114",
"alerT_PHONE": "2348039301606", "alerT_PHONE": "2348039301606",
"averagE_SALARY": 5000, "averagE_SALARY": average_salary,
"loaN_OUSTANDING_BAL": 0, "loaN_OUSTANDING_BAL": 0,
"emi": 1000, "emi": 1000,
"eliG_AMT": 25000, "eliG_AMT": 25000,
"rule1_45day_sal": True, "rule1_45day_sal": is_valid,
"rule2_2m_sal": True, "rule2_2m_sal": is_valid,
"rule3_no_bounced_check": True, "rule3_no_bounced_check": is_valid,
"rule4_current_loan_payments": True, "rule4_current_loan_payments": True if is_valid is False else is_valid,
"rule5_no_past_due_fadv_loan": True, "rule5_no_past_due_fadv_loan": is_valid,
"rule6_no_past_due_other_loan": True, "rule6_no_past_due_other_loan": is_valid,
"rule7_consistent_salary_amount": True, "rule7_consistent_salary_amount": is_valid,
"rule8_whitelisted": True, "rule8_whitelisted": True if is_valid is False else is_valid,
"rule9_regular_account": True, "rule9_regular_account": True if is_valid is False else is_valid,
"rule10_bvn_validation": True, "rule10_bvn_validation": is_valid,
"rule11_CRC_no_delinquency": True, "rule11_CRC_no_delinquency": is_valid,
"rule12_CRMS_no_delinquency": True, "rule12_CRMS_no_delinquency": True if is_valid is False else is_valid,
"rule13_BVN_ignore": True, "rule13_BVN_ignore": is_valid,
"rule14_no_lien": True, "rule14_no_lien": is_valid,
"rule15_null_ignore": True, "rule15_null_ignore": True if is_valid is False else is_valid,
"overalL_ELIG": True, "overalL_ELIG": is_valid
"salarypaymenT_1": 180000, # "salarypaymenT_1": 180000,
"salarypaymenT_2": 50000, # "salarypaymenT_2": 50000,
"salarypaymenT_3": 70000, # "salarypaymenT_3": 70000,
"salarypaymenT_4": 0, # "salarypaymenT_4": 0,
"salarypaymenT_5": 0, # "salarypaymenT_5": 0,
"salarypaymenT_6": 0 # "salarypaymenT_6": 0
} }
rac_response.update(salary_payments)
full_response = { full_response = {
"transactionId": validated_data["transactionId"], "transactionId": validated_data["transactionId"],
+5 -1
View File
@@ -6,6 +6,7 @@ from app.api.schemas.transaction_verify import (
TransactionVerifySchema, TransactionVerifySchema,
TransactionVerifyResponseSchema TransactionVerifyResponseSchema
) )
import datetime
class TransactionVerifyService: class TransactionVerifyService:
@@ -37,7 +38,10 @@ class TransactionVerifyService:
"providedAmount": 0.0, "providedAmount": 0.0,
"collectedAmount": 7.50, "collectedAmount": 7.50,
"transactionId": validated_data.get("transactionId"), "transactionId": validated_data.get("transactionId"),
"transactionType": validated_data.get("transactionType") "transactionType": validated_data.get("transactionType"),
"disburseVerify": datetime.datetime.now().isoformat(),
"verifyResult": "00",
"verifyDescription": "Collect Status retrieved successfully.",
} }
# Validate and serialize response with TransactionVerifyResponseSchema # Validate and serialize response with TransactionVerifyResponseSchema
+14 -1
View File
@@ -1,11 +1,12 @@
import os import os
from dotenv import load_dotenv from dotenv import load_dotenv
from datetime import timedelta
class Config: class Config:
"""Base configuration for Flask app""" """Base configuration for Flask app"""
load_dotenv() load_dotenv()
SWAGGER_URL = os.getenv("SWAGGER_URL") SWAGGER_URL = os.getenv("SWAGGER_URL")
API_URL = '/api/swagger.json' API_URL = os.getenv("API_URL")
DEBUG = True DEBUG = True
VALID_APP_ID = os.getenv("VALID_APP_ID", "app1") VALID_APP_ID = os.getenv("VALID_APP_ID", "app1")
@@ -15,6 +16,18 @@ class Config:
# SQLALCHEMY_TRACK_MODIFICATIONS = False # SQLALCHEMY_TRACK_MODIFICATIONS = False
# SECRET_KEY = os.environ.get("SECRET_KEY", "your_secret_key") # SECRET_KEY = os.environ.get("SECRET_KEY", "your_secret_key")
JWT_SECRET_KEY = os.getenv("JWT_SECRET_KEY", "secret-key")
JWT_ACCESS_TOKEN_EXPIRES = os.getenv("JWT_ACCESS_TOKEN_EXPIRES", timedelta(hours=1))
JWT_REFRESH_TOKEN_EXPIRES = os.getenv(
"JWT_REFRESH_TOKEN_EXPIRES", timedelta(days=30)
)
BASIC_AUTH_USERNAME = os.environ.get("BASIC_AUTH_USERNAME", "user")
BASIC_AUTH_PASSWORD = os.environ.get("BASIC_AUTH_PASSWORD", "password")
DEBUG = True DEBUG = True
def configure(): def configure():
load_dotenv() load_dotenv()
settings = Config()
+1
View File
@@ -0,0 +1 @@
from .transaction_type import TransactionType
+7
View File
@@ -0,0 +1,7 @@
from enum import Enum
class InstallmentStatus(str, Enum):
PENDING = 'PENDING'
PAID = 'PAID'
OVERDUE = 'OVERDUE'
PARTIALLY_PAID = 'PARTIALLY_PAID'
+8
View File
@@ -0,0 +1,8 @@
from enum import Enum
class LoanStatus(str, Enum):
PENDING = 'PENDING'
ACTIVE = 'ACTIVE'
PAID = 'PAID'
DEFAULTED = 'DEFAULTED'
CLOSED = 'CLOSED'
+7
View File
@@ -0,0 +1,7 @@
from enum import Enum
class TransactionType(str, Enum):
DEBT_CLOSURE_NOTIFICATION = "debt_closure_notification"
DISBURSEMENT = "disbursement"
SEND_SMS = "send_sms"
COLLECT_LOAN = "collect_loan"
+112
View File
@@ -0,0 +1,112 @@
from flask import jsonify
from typing import Optional, Union, Dict, List, Any
class ResponseHelper:
"""
A helper class for building standardized JSON responses using resultCode and resultDescription.
"""
@staticmethod
def build_response(
result_code: str,
result_description: str,
data: Optional[Union[Dict, List, str]] = None
) -> Dict[str, Any]:
response = {
"resultCode": result_code,
"resultDescription": result_description
}
if isinstance(data, dict):
response.update(data)
return jsonify(response)
@staticmethod
def success(
result_description: str = "Successful",
result_code: str = "00",
data: Optional[Dict[str, Any]] = None
) -> Dict[str, Any]:
return ResponseHelper.build_response(result_code, result_description, data)
@staticmethod
def error(
result_description: str = "An error occurred",
result_code: str = "01",
data: Optional[Dict[str, Any]] = None
) -> Dict[str, Any]:
return ResponseHelper.build_response(result_code, result_description, data)
@staticmethod
def created(
result_description: str = "Resource created successfully",
result_code: str = "00",
data: Optional[Dict[str, Any]] = None
) -> Dict[str, Any]:
return ResponseHelper.build_response(result_code, result_description, data)
@staticmethod
def updated(
result_description: str = "Resource updated successfully",
result_code: str = "00",
data: Optional[Dict[str, Any]] = None
) -> Dict[str, Any]:
return ResponseHelper.build_response(result_code, result_description, data)
@staticmethod
def internal_server_error(
result_description: str = "Internal Server Error",
result_code: str = "500",
data: Optional[Dict[str, Any]] = None
) -> Dict[str, Any]:
return ResponseHelper.build_response(result_code, result_description, data)
@staticmethod
def unauthorized(
result_description: str = "Unauthorized",
result_code: str = "401",
data: Optional[Dict[str, Any]] = None
) -> Dict[str, Any]:
return ResponseHelper.build_response(result_code, result_description, data)
@staticmethod
def forbidden(
result_description: str = "Forbidden",
result_code: str = "403",
data: Optional[Dict[str, Any]] = None
) -> Dict[str, Any]:
return ResponseHelper.build_response(result_code, result_description, data)
@staticmethod
def not_found(
result_description: str = "Resource not found",
result_code: str = "404",
data: Optional[Dict[str, Any]] = None
) -> Dict[str, Any]:
return ResponseHelper.build_response(result_code, result_description, data)
@staticmethod
def unprocessable_entity(
result_description: str = "Unprocessable entity",
result_code: str = "422",
data: Optional[Dict[str, Any]] = None
) -> Dict[str, Any]:
return ResponseHelper.build_response(result_code, result_description, data)
@staticmethod
def method_not_allowed(
result_description: str = "Method Not Allowed",
result_code: str = "405",
data: Optional[Dict[str, Any]] = None
) -> Dict[str, Any]:
return ResponseHelper.build_response(result_code, result_description, data)
@staticmethod
def bad_request(
result_description: str = "Bad Request",
result_code: str = "400",
data: Optional[Dict[str, Any]] = None
) -> Dict[str, Any]:
return ResponseHelper.build_response(result_code, result_description, data)
+1
View File
@@ -0,0 +1 @@
from .routes import eco
+87
View File
@@ -0,0 +1,87 @@
from flask import Blueprint, request, jsonify, send_from_directory
from app.eco.services import (
AuthorizationService,
DisbursementService,
CollectLoanService,
DebtClosureNotificationService,
SendSMSService
)
from app.utils.logger import logger
from flask_jwt_extended import jwt_required
import os
eco = Blueprint("eco", __name__)
# Swagger JSON file
@eco.route("/swagger.json", methods=["GET"])
def swagger_json():
swagger_dir = os.path.join("swagger")
return send_from_directory(swagger_dir, "eco_digifi_swagger.json")
@eco.route("/swagger/<path:filename>")
def serve_paths(filename):
swagger_dir = os.path.join("swagger")
return send_from_directory(swagger_dir, filename)
# Disbursement (Simbrella -> EcoBank)
@eco.route("/Disbursement", methods=["POST"])
@jwt_required()
def disbursement():
data = request.get_json()
response = DisbursementService.process_request(data)
return response
# CollectLoan (Simbrella -> EcoBank)
@eco.route("/CollectLoan", methods=["POST"])
@jwt_required()
def collect_loan():
data = request.get_json()
response = CollectLoanService.process_request(data)
return response
# Debt Closure Notification (Simbrella -> EcoBank)
@eco.route("/DebtClosureNotification", methods=["POST"])
@jwt_required()
def debt_closure():
data = request.get_json()
response = DebtClosureNotificationService.process_request(data)
return response
# Send SMS (Simbrella -> EcoBank)
@eco.route("/SendSMS", methods=["POST"])
@jwt_required()
def send_sms():
data = request.get_json()
response = SendSMSService.process_request(data)
return response
# Health Check
@eco.route("/health", methods=["GET"])
def health_check():
return jsonify({"status": "ok"}), 200
# Authorize endpoint
@eco.route("/Authorize", methods=["POST"])
def authorize():
data = request.get_json()
response = AuthorizationService.process_request(data)
return response
# Authorize refresh endpoint
@eco.route("/AuthorizeRefresh", methods=["POST"])
@jwt_required(refresh=True)
def refresh():
response = AuthorizationService.process_refresh_request()
return response
+5
View File
@@ -0,0 +1,5 @@
from .authorization import AuthorizeRequestSchema
from .disbursement import DisbursementSchema
from .debt_closure_notification import DebtClosureNotificationSchema
from .send_sms import SendSmsSchema
from .collect_loan import CollectLoanSchema
+6
View File
@@ -0,0 +1,6 @@
from marshmallow import Schema, fields
class AuthorizeRequestSchema(Schema):
username = fields.Str(required=True)
password = fields.Str(required=True)
+13
View File
@@ -0,0 +1,13 @@
from marshmallow import Schema, fields, EXCLUDE
class CollectLoanSchema(Schema):
requestId = fields.Str(required=True)
affiliateCode = fields.Str(required=True)
debtId = fields.Int(required=True)
principal = fields.Decimal(required=True)
interest = fields.Decimal(required=True)
penalty = fields.Decimal(required=True)
collectAmount = fields.Decimal(required=True)
class Meta:
unknown = EXCLUDE
@@ -0,0 +1,11 @@
from marshmallow import Schema, fields, EXCLUDE
class DebtClosureNotificationSchema(Schema):
requestId = fields.Str(required=True)
affiliateCode = fields.Str(required=True)
customerId = fields.Str(required=True)
accountId = fields.Str(required=True)
debtId = fields.Int(required=True)
class Meta:
unknown = EXCLUDE
+15
View File
@@ -0,0 +1,15 @@
from marshmallow import Schema, fields, EXCLUDE
class DisbursementSchema(Schema):
requestId = fields.Str(required=True)
affiliateCode = fields.Str(required=True)
debtId = fields.Int(required=True)
productId = fields.Str(required=True)
customerId = fields.Str(required=True)
accountId = fields.Str(required=True)
provideAmount = fields.Decimal(required=True)
collectAmount = fields.Decimal(required=True)
interestRate = fields.Decimal(required=True)
class Meta:
unknown = EXCLUDE
+10
View File
@@ -0,0 +1,10 @@
from marshmallow import Schema, fields, EXCLUDE
class SendSmsSchema(Schema):
requestId = fields.Str(required=True)
phoneNums = fields.List(fields.Str(), required=True)
affiliateCode = fields.Str(required=True)
message = fields.Str(required=True)
class Meta:
unknown = EXCLUDE
+5
View File
@@ -0,0 +1,5 @@
from .authorization import AuthorizationService
from .disbursement import DisbursementService
from .debt_closure_notification import DebtClosureNotificationService
from .send_sms import SendSMSService
from .collect_loan import CollectLoanService
+102
View File
@@ -0,0 +1,102 @@
from flask import request, jsonify
from marshmallow import ValidationError
from app.eco.services.base_service import BaseService
from app.utils.logger import logger
from app.eco.schemas.authorization import AuthorizeRequestSchema
from app.eco.helpers.response_helper import ResponseHelper
from flask_jwt_extended import (
JWTManager,
jwt_required,
create_access_token,
create_refresh_token,
get_jwt_identity,
)
from app.config import Config
USERNAME = Config.BASIC_AUTH_USERNAME
PASSWORD = Config.BASIC_AUTH_PASSWORD
class AuthorizationService(BaseService):
@staticmethod
def process_request(data):
"""
Process the Authorization request.
Args:
data (dict): The request data.
Returns:
dict: A standardized response.
"""
try:
logger.info("Processing Authorization request")
if not data:
return ResponseHelper.bad_request(result_description="Missing JSON in request")
# Validate input data using the Authorization schema
schema = AuthorizeRequestSchema()
validated_data = schema.load(data) # Raises ValidationError if invalid
if (
validated_data["username"] != USERNAME
or validated_data["password"] != PASSWORD
):
return ResponseHelper.unauthorized(result_description="Invalid credentials")
access_token = create_access_token(identity=validated_data["username"])
refresh_token = create_refresh_token(identity=validated_data["username"])
# Simulated processing logic
response_data = {
"access_token": access_token,
"refresh_token": refresh_token,
}
return ResponseHelper.success(
data={"data": response_data}, result_description="Authorization processed successfully"
)
except ValidationError as e:
logger.error(f"Validation error: {e}")
return ResponseHelper.bad_request(result_description=f"Validation error: {e}")
except Exception as e:
logger.error(f"Error processing Authorization request: {e}")
return ResponseHelper.internal_server_error(
result_description=f"Error processing Authorization request: {e}"
)
@staticmethod
def process_refresh_request():
"""
Process the RefreshToken request.
Args:
data (dict): The request data.
Returns:
dict: A standardized response.
"""
try:
logger.info("Processing RefreshToken request")
identity = get_jwt_identity()
access_token = create_access_token(identity=identity)
# Simulated processing logic
response_data = {
"access_token": access_token,
}
return ResponseHelper.success(
data={"data": response_data}, result_description="RefreshToken processed successfully"
)
except Exception as e:
logger.error(f"Error processing RefreshToken request: {e}")
return ResponseHelper.internal_server_error(
result_description=f"Error processing RefreshToken request: {e}"
)
+32
View File
@@ -0,0 +1,32 @@
from app.eco.enums import TransactionType
from flask import jsonify
from marshmallow import ValidationError
import logging
logger = logging.getLogger(__name__)
class BaseService:
TRANSACTION_TYPE = None
@classmethod
def validate_data(cls, data, schema):
"""
Validate input data based on the provided schema.
"""
logger.info(f"Processing {cls.TRANSACTION_TYPE} request")
return schema.load(data)
# @classmethod
# def log_session(cls, validated_data):
# """
# Create a new session.
# """
# channel = "USSD" if validated_data.get("channel") is None else validated_data.get("channel")
# return Session.create_session(
# session_id = validated_data.get("transactionId"),
# customer_id = validated_data.get('customerId', None),
# account_id = validated_data.get("accountId", None),
# type = cls.TRANSACTION_TYPE,
# channel = channel,
# )
+33
View File
@@ -0,0 +1,33 @@
from app.eco.enums.transaction_type import TransactionType
from app.eco.services.base_service import BaseService
from app.eco.schemas.collect_loan import CollectLoanSchema
from marshmallow import ValidationError
from app.eco.helpers.response_helper import ResponseHelper
class CollectLoanService(BaseService):
TRANSACTION_TYPE = TransactionType.COLLECT_LOAN
@staticmethod
def process_request(data):
"""
Process the loan collection request.
Args:
data (dict): The request data.
Returns:
dict: A standardized response.
"""
try:
validated_data = CollectLoanService.validate_data(data, CollectLoanSchema())
response_data = {
"transactionId": "01135062",
"amountCollected": 900.0,
}
return ResponseHelper.success(data=response_data)
except ValidationError as err:
return ResponseHelper.unprocessable_entity(result_description="Validation exception")
except Exception as e:
return ResponseHelper.internal_server_error()
@@ -0,0 +1,27 @@
from app.eco.enums.transaction_type import TransactionType
from app.eco.services.base_service import BaseService
from app.eco.schemas.debt_closure_notification import DebtClosureNotificationSchema
from marshmallow import ValidationError
from app.eco.helpers.response_helper import ResponseHelper
class DebtClosureNotificationService(BaseService):
TRANSACTION_TYPE = TransactionType.DEBT_CLOSURE_NOTIFICATION
@staticmethod
def process_request(data):
"""
Process the debt closure notification request.
Args:
data (dict): The request data.
Returns:
dict: A standardized response.
"""
try:
validated_data = DebtClosureNotificationService.validate_data(data, DebtClosureNotificationSchema())
return ResponseHelper.success()
except ValidationError as err:
return ResponseHelper.unprocessable_entity(result_description="Validation exception")
except Exception as e:
return ResponseHelper.internal_server_error()
+32
View File
@@ -0,0 +1,32 @@
from app.eco.enums.transaction_type import TransactionType
from app.eco.services.base_service import BaseService
from app.eco.schemas.disbursement import DisbursementSchema
from marshmallow import ValidationError
from app.eco.helpers.response_helper import ResponseHelper
class DisbursementService(BaseService):
TRANSACTION_TYPE = TransactionType.DISBURSEMENT
@staticmethod
def process_request(data):
"""
Process the disbursement request.
Args:
data (dict): The request data.
Returns:
dict: A standardized response.
"""
try:
validated_data = DisbursementService.validate_data(data, DisbursementSchema())
response_data = {
"transactionId": "SIM01135042",
}
return ResponseHelper.success(data=response_data)
except ValidationError as err:
return ResponseHelper.unprocessable_entity(result_description="Validation exception")
except Exception as e:
return ResponseHelper.internal_server_error()
+31
View File
@@ -0,0 +1,31 @@
from app.eco.enums.transaction_type import TransactionType
from app.eco.services.base_service import BaseService
from app.eco.schemas.send_sms import SendSmsSchema
from marshmallow import ValidationError
from app.eco.helpers.response_helper import ResponseHelper
class SendSMSService(BaseService):
TRANSACTION_TYPE = TransactionType.SEND_SMS
@staticmethod
def process_request(data):
"""
Process the SMS sending request.
Args:
data (dict): The request data.
Returns:
dict: A standardized response.
"""
try:
validated_data = SendSMSService.validate_data(data, SendSmsSchema())
response_data = {
"undelivered": []
}
return ResponseHelper.success(data=response_data)
except ValidationError as err:
return ResponseHelper.unprocessable_entity(result_description="Validation exception")
except Exception as e:
return ResponseHelper.internal_server_error()
+3
View File
@@ -110,6 +110,9 @@
} }
], ],
"paths": { "paths": {
"/api/system-health-check": {
"$ref": "swagger/paths/HealthCheck.json"
},
"/api/rac-check": { "/api/rac-check": {
"$ref": "swagger/paths/RACCheck.json" "$ref": "swagger/paths/RACCheck.json"
}, },
+128
View File
@@ -0,0 +1,128 @@
{
"openapi": "3.0.3",
"info": {
"title": "bank Emulator Swagger Simbrella EcoBank - OpenAPI 3.0",
"description": "This is a Simbrella EcoBank bank Backend Server Emulator with the OpenAPI 3.0 specification. \n\n\nSome useful links:\n- [Web Simulated Demo Page](https://digifi-salaryloan.chiefsoft.net/)\n- [Web Management Support Portal](https://digifi-office.chiefsoft.net/auth/login)",
"termsOfService": "http://swagger.io/terms/",
"contact": {
"email": "support@chiefsoft.com"
},
"license": {
"name": "Apache 2.0",
"url": "http://www.apache.org/licenses/LICENSE-2.0.html"
},
"version": "1.0.11"
},
"servers": [
{
"url": "http://localhost:6337"
},
{
"url": "http://localhost:5000"
},
{
"url": "http://10.10.11.17:6337"
},
{
"url": "https://bank-emulator.dev.simbrellang.net"
}
],
"tags": [
{
"name": "Authentication",
"description": "EcoBank Authentication Token Request",
"externalDocs": {
"description": "Find out more",
"url": "https://www.simbrellang.net"
}
},
{
"name": "Disbursement",
"description": "Loan Disbursement Request to EcoBank",
"externalDocs": {
"description": "Find out more",
"url": "https://www.simbrellang.net"
}
},
{
"name": "CollectLoan",
"description": "Collect Loan Repayment from EcoBank Customer",
"externalDocs": {
"description": "Find out more",
"url": "https://www.simbrellang.net"
}
},
{
"name": "DebtClosureNotification",
"description": "Notify EcoBank of Loan Closure",
"externalDocs": {
"description": "Find out more",
"url": "https://www.simbrellang.net"
}
},
{
"name": "SendSMS",
"description": "Send SMS to EcoBank Customers",
"externalDocs": {
"description": "Find out more",
"url": "https://www.simbrellang.net"
}
}
],
"paths": {
"/eco/Authorize": {
"$ref": "swagger/paths/eco/Authentication.json"
},
"/eco/Disbursement": {
"$ref": "swagger/paths/eco/Disbursement.json"
},
"/eco/CollectLoan": {
"$ref": "swagger/paths/eco/CollectLoan.json"
},
"/eco/DebtClosureNotification": {
"$ref": "swagger/paths/eco/DebtClosureNotification.json"
},
"/eco/SendSMS": {
"$ref": "swagger/paths/eco/SendSMS.json"
}
},
"components": {
"schemas": {
"AuthenticationRequest": {
"$ref": "swagger/schemas/eco/AuthenticationRequest.json"
},
"AuthenticationResponse": {
"$ref": "swagger/schemas/eco/AuthenticationResponse.json"
},
"DebtClosureNotificationRequest": {
"$ref": "swagger/schemas/eco/DebtClosureNotificationRequest.json"
},
"DebtClosureNotificationResponse": {
"$ref": "swagger/schemas/eco/DebtClosureNotificationResponse.json"
},
"SendSMSRequest": {
"$ref": "swagger/schemas/eco/SendSMSRequest.json"
},
"SendSMSResponse": {
"$ref": "swagger/schemas/eco/SendSMSResponse.json"
}
},
"securitySchemes": {
"basicAuth": {
"type": "http",
"scheme": "basic"
},
"bearerAuth": {
"type": "http",
"scheme": "bearer",
"bearerFormat": "JWT"
}
}
},
"security": [
{
"basicAuth": [],
"bearerAuth": []
}
]
}
+19
View File
@@ -0,0 +1,19 @@
{
"get": {
"tags": ["System"],
"summary": "System Health Check",
"description": "Returns the current health status of the system",
"responses": {
"200": {
"description": "Successful operation",
"content": {
"application/json": {
"schema": {
"$ref": "../schemas/HealthCheckResponse.json"
}
}
}
}
}
}
}
+32
View File
@@ -0,0 +1,32 @@
{
"post": {
"tags": ["Authentication"],
"summary": "EcoBank Authentication",
"description": "Get bearer token from EcoBank",
"operationId": "authenticate",
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"$ref": "../../schemas/eco/AuthenticationRequest.json"
}
}
}
},
"responses": {
"200": {
"description": "Authentication Successful",
"content": {
"application/json": {
"schema": {
"$ref": "../../schemas/eco/AuthenticationResponse.json"
}
}
}
},
"400": { "description": "Invalid request" },
"500": { "description": "Internal server error" }
}
}
}
+33
View File
@@ -0,0 +1,33 @@
{
"post": {
"tags": ["CollectLoan"],
"summary": "Collect Loan Repayment",
"description": "Collect repayment amount from EcoBank customer",
"operationId": "collectLoan",
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"$ref": "../../schemas/eco/CollectLoanRequest.json"
}
}
}
},
"responses": {
"200": {
"description": "Loan Collection Successful",
"content": {
"application/json": {
"schema": {
"$ref": "../../schemas/eco/CollectLoanResponse.json"
}
}
}
},
"400": { "description": "Invalid request" },
"422": { "description": "Validation exception" },
"500": { "description": "Internal server error" }
}
}
}
@@ -0,0 +1,33 @@
{
"post": {
"tags": ["DebtClosureNotification"],
"summary": "Debt Closure Notification",
"description": "Notify EcoBank that debt has been fully repaid",
"operationId": "notifyDebtClosure",
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"$ref": "../../schemas/eco/DebtClosureNotificationRequest.json"
}
}
}
},
"responses": {
"200": {
"description": "Debt Closure Acknowledged",
"content": {
"application/json": {
"schema": {
"$ref": "../../schemas/eco/DebtClosureNotificationResponse.json"
}
}
}
},
"400": { "description": "Invalid request" },
"422": { "description": "Validation exception" },
"500": { "description": "Internal server error" }
}
}
}
+33
View File
@@ -0,0 +1,33 @@
{
"post": {
"tags": ["Disbursement"],
"summary": "Loan Disbursement",
"description": "Disburse loan to EcoBank customer account",
"operationId": "disburseLoan",
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"$ref": "../../schemas/eco/DisbursementRequest.json"
}
}
}
},
"responses": {
"200": {
"description": "Disbursement Successful",
"content": {
"application/json": {
"schema": {
"$ref": "../../schemas/eco/DisbursementResponse.json"
}
}
}
},
"400": { "description": "Invalid request" },
"422": { "description": "Validation exception" },
"500": { "description": "Internal server error" }
}
}
}
+33
View File
@@ -0,0 +1,33 @@
{
"post": {
"tags": ["SendSMS"],
"summary": "Send SMS Notification",
"description": "Send a message to one or more EcoBank customers",
"operationId": "sendSms",
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"$ref": "../../schemas/eco/SendSMSRequest.json"
}
}
}
},
"responses": {
"200": {
"description": "SMS Sent Successfully",
"content": {
"application/json": {
"schema": {
"$ref": "../../schemas/eco/SendSMSResponse.json"
}
}
}
},
"400": { "description": "Invalid request" },
"422": { "description": "Validation exception" },
"500": { "description": "Internal server error" }
}
}
}
@@ -70,6 +70,22 @@
"type": "string", "type": "string",
"example": "Loan Request Completed Successfully!", "example": "Loan Request Completed Successfully!",
"nullable": true "nullable": true
},
"disburseDate": {
"type": "string",
"format": "date-time",
"example": "2023-10-01T12:00:00Z",
"nullable": true
},
"disburseResult": {
"type": "string",
"example": "00",
"nullable": true
},
"disburseDescription": {
"type": "string",
"example": "Loan Request Completed Successfully!",
"nullable": true
} }
}, },
"required": [ "required": [
@@ -0,0 +1,17 @@
{
"type": "object",
"properties": {
"status": {
"type": "string",
"example": "Active"
},
"responseCode": {
"type": "string",
"example": "00"
},
"responseMessage": {
"type": "string",
"example": "Successful"
}
}
}
@@ -0,0 +1,17 @@
{
"type": "object",
"properties": {
"status": {
"type": "string",
"example": "Active"
},
"responseCode": {
"type": "string",
"example": "00"
},
"responseMessage": {
"type": "string",
"example": "Successful"
}
}
}
@@ -38,6 +38,20 @@
"transactionType": { "transactionType": {
"type": "string", "type": "string",
"example": "Disbursement" "example": "Disbursement"
},
"disburseVerify":{
"type": "string",
"format": "date-time",
"example": "2023-10-01T12:00:00Z",
"nullable": true
},
"verifyResult": {
"type": "string",
"example": "Success"
},
"verifyDescription": {
"type": "string",
"example": "Disbursement was verified and collection completed."
} }
}, },
"required": [ "required": [
@@ -0,0 +1,14 @@
{
"type": "object",
"required": ["username", "password"],
"properties": {
"username": {
"type": "string",
"example": "user"
},
"password": {
"type": "string",
"example": "password"
}
}
}
@@ -0,0 +1,16 @@
{
"type": "object",
"properties": {
"resultCode": {
"type": "string"
},
"resultDescription": {
"type": "string",
"example": "Authentication successful"
},
"token": {
"type": "string",
"example": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
}
}
}
@@ -0,0 +1,13 @@
{
"type": "object",
"required": ["requestId", "affiliateCode", "debtId", "principal", "interest", "penalty", "collectAmount"],
"properties": {
"requestId": { "type": "string", "example": "req-12345" },
"affiliateCode": { "type": "string", "example": "aff-67890" },
"debtId": { "type": "integer", "example": 123456 },
"principal": { "type": "number", "example": 1000.00 },
"interest": { "type": "number", "example": 100.00 },
"penalty": { "type": "number", "example": 50.00 },
"collectAmount": { "type": "number", "example": 1150.00 }
}
}
@@ -0,0 +1,9 @@
{
"type": "object",
"properties": {
"transactionId": { "type": "string", "example": "txn-12345" },
"amountCollected": { "type": "number", "example": 1150.00 },
"resultCode": { "type": "integer", "example": 0 },
"resultDescription": { "type": "string", "example": "Collection successful" }
}
}
@@ -0,0 +1,11 @@
{
"type": "object",
"required": ["requestId", "affiliateCode", "customerId", "accountId", "debtId"],
"properties": {
"requestId": { "type": "string", "example": "req-12345" },
"affiliateCode": { "type": "string", "example": "aff-67890" },
"customerId": { "type": "string", "example": "cust-54321" },
"accountId": { "type": "string", "example": "acc-98765" },
"debtId": { "type": "integer", "example": 123456 }
}
}
@@ -0,0 +1,7 @@
{
"type": "object",
"properties": {
"resultCode": { "type": "integer", "example": 0 },
"resultDescription": { "type": "string", "example": "Debt closure notification sent successfully" }
}
}
@@ -0,0 +1,15 @@
{
"type": "object",
"required": ["requestId", "affiliateCode", "debtId", "productId", "customerId", "accountId", "provideAmount", "collectAmount", "interestRate"],
"properties": {
"requestId": { "type": "string", "example": "req-12345" },
"affiliateCode": { "type": "string", "example": "aff-67890" },
"debtId": { "type": "integer", "example": 123456 },
"productId": { "type": "string", "example": "prod-78901" },
"customerId": { "type": "string", "example": "cust-54321" },
"accountId": { "type": "string", "example": "acc-98765" },
"provideAmount": { "type": "number", "example": 1000.00 },
"collectAmount": { "type": "number", "example": 1150.00 },
"interestRate": { "type": "number", "example": 5.0 }
}
}
@@ -0,0 +1,8 @@
{
"type": "object",
"properties": {
"transactionId": { "type": "string", "example": "txn-12345" },
"resultCode": { "type": "integer", "example": 0 },
"resultDescription": { "type": "string", "example": "Disbursement successful" }
}
}
@@ -0,0 +1,13 @@
{
"type": "object",
"required": ["requestId", "phoneNums", "affiliateCode", "message"],
"properties": {
"requestId": { "type": "string", "example": "req-12345" },
"phoneNums": {
"type": "array",
"items": { "type": "string", "example": "+1234567890" }
},
"affiliateCode": { "type": "string", "example": "aff-67890" },
"message": { "type": "string", "example": "Your verification code is 123456" }
}
}
@@ -0,0 +1,11 @@
{
"type": "object",
"properties": {
"undelivered": {
"type": "array",
"items": { "type": "string", "example": "+1234567890" }
},
"resultCode": { "type": "integer", "example": 0 },
"resultDescription": { "type": "string", "example": "SMS sent successfully" }
}
}
+2
View File
@@ -8,6 +8,8 @@ flask-swagger-ui
python-dotenv python-dotenv
flask-jwt-extended
flask-apispec
# Logging (Python Standard Library, for reference) # Logging (Python Standard Library, for reference)