A Django module for integrating various payment gateways, designed with a clean Hexagonal (Ports and Adapters) Architecture. This architecture promotes separation of concerns, making the core payment logic independent of specific frameworks (like Django) and external services (like payment gateways).
Currently, this module provides an adapter for FlutterWave (specifically for bank transfers and webhook handling).
- Hexagonal Architecture (Ports & Adapters):
- Decoupled Core Logic: The
core_logic.pycontains business rules and orchestrates payment flows, unaware of Django or specific payment gateways. - Clear Interfaces (Ports):
PaymentGatewayInterfaceandClientRepositoryInterfacedefine contracts for external interactions. - Concrete Implementations (Adapters):
PayStackAdapterFlutterWaveAdapterandDjangoClientRepositoryAdapterprovide specific implementations for these interfaces.
- Decoupled Core Logic: The
- Payment Initiation: Supports initiating payments via integrated gateways (e.g. PayStack Charge, FlutterWave bank transfer).
- Webhook Handling: Designed to process incoming webhook notifications from payment gateways to update transaction statuses.
- Data Transfer Objects (DTOs): Ensures clear and consistent data structures between layers.
- Django REST Framework Integration: Provides API endpoints for initiating payments and handling webhooks.
- Extensible: Easily add support for new payment gateways by creating new adapters.
- Testable: The decoupled nature makes unit testing of core logic and adapters more straightforward.
This module follows the Ports and Adapters (Hexagonal) Architecture:
-
Core Logic (
core_logic.py):- Contains the application's business rules (e.g.,
PaymentServiceCore). - It is completely independent of web frameworks, databases, or third-party services.
- Defines Ports (interfaces) through which it communicates with the outside world.
- Contains the application's business rules (e.g.,
-
Ports (
payments_ports_and_adapters.py,repositories_ports_and_adapters.py):PaymentGatewayInterface: Defines the contract for how the core logic interacts with any payment gateway (e.g.,process_payment,handle_webhook).ClientRepositoryInterface: Defines the contract for how the core logic interacts with data storage for clients and transactions (e.g.,get_client_by_email,create_payment_transaction).
-
Adapters (
payments_ports_and_adapters.py,repositories_ports_and_adapters.py):- Primary/Driving Adapters (Input):
- Django views (
views.py) and serializers (serializers.py) adapt incoming HTTP requests to calls on theservices.pylayer, which then interacts with thePaymentServiceCore.
- Django views (
- Secondary/Driven Adapters (Output):
FlutterWaveAdapter: ImplementsPaymentGatewayInterfaceto interact with the FlutterWave API.PayStackAdapter: ImplementsPaymentGatewayInterfaceto interact with the PayStack API.DjangoClientRepositoryAdapter: ImplementsClientRepositoryInterfaceto interact with the Django ORM for data persistence.
- Primary/Driving Adapters (Input):
-
Services (
services.py):- Acts as an application service layer, orchestrating the instantiation of adapters and coordinating calls between the primary adapters (views) and the core logic.
Flow Example (Initiate Payment):
API View (Django) -> initiate_payment (services.py) -> PaymentServiceCore.initiate_payment (core_logic.py) -> FlutterWaveAdapter.process_payment & DjangoClientRepositoryAdapter.create_payment_transaction
- Python (3.8+ recommended)
- Pip & Virtualenv
- Django (4.x+ recommended)
- Django REST Framework
requestslibrarydrf-spectacular(for API schema generation)- A running database compatible with Django (e.g., PostgreSQL, MySQL, SQLite for development)
- A FlutterWave account with API Secret Key and Bank Transfer Endpoint URL.
git clone github.com/Igboke/payment_gateway_service_api.git
cd payment_gateway_service_apipython -m venv venv
source venv/bin/activate # On Windows: venv\Scripts\activatepip install -r requirements.txtCreate a .env file in your project root (where manage.py is typically located) and add your credentials and settings.
Never commit your .env file to version control! Add .env to your .gitignore file.
# .env
DEBUG=True
SECRET_KEY='your-django-secret-key'
FLUTTERWAVE_SECRET_KEY='your_flutterwave_secret_key'Ensure your Django settings.py can load these (e.g., using python-dotenv or django-environ):
# settings.py
import os
from dotenv import load_dotenv
load_dotenv() # Loads variables from .env
# ...
SECRET_KEY = os.getenv('SECRET_KEY')
DEBUG = os.getenv('DEBUG', 'False') == 'True'
# Custom settings for your payment module
FLUTTERWAVE_SECRET_KEY = os.getenv('FLUTTERWAVE_SECRET_KEY')
PAYSTACK_SECRET_KEY = os.getenv("PAYSTACK_SECRET_KEY")
# ...python manage.py runserverThe API endpoints should now be accessible, typically under http://127.0.0.1:8000/api/payments/.
The following endpoints are provided by views.py and configured in urls.py:
-
Endpoint:
POST /api/payments/v1/createpayment/ -
Description: Initiates a bank transfer payment request via FlutterWave.
-
Request Body (
application/json):{ "email": "client@example.com", "currency": "NGN", // Or other supported currency "is_permanent": false // Optional, defaults to false } -
email(string, required): Email of the client making the payment. -
currency(string, required): Currency code (e.g., "NGN", "USD"). -
is_permanent(boolean, optional): Indicates if the payment is for a permanent virtual account (FlutterWave specific). -
Success Response (200 OK):
{ "transaction_ref": "your-unique-transaction-ref-uuid", "gateway_response": { "status": "success", "message": "Transfer Queued Successfully", "data": { /* ... Flutterwave specific data ... */ }, "meta": { "authorization": { "transfer_reference": "FW-TRANSFER-REF-123", "transfer_account": "0123456789", "transfer_bank": "Access Bank", "account_expiration": "2023-12-31T23:00:00.000Z", "transfer_note": "Please make a bank transfer to this account", "transfer_amount": 5000, // Amount might be here or in request "mode": "test" } } } } -
Error Responses:
-
400 Bad Request: Invalid input data. -
404 Not Found: Client or order not found. -
500 Internal Server Error: Gateway communication error or other server-side issues.
-
Endpoint:
POST /api/payments/v1/webhook/ -
Description: Receives webhook notifications from the payment gateway (e.g., FlutterWave) to update transaction status.
-
Request Body (
application/json): The structure of the webhook payload is determined by the payment gateway (e.g., FlutterWave). Example (Flutterwave successful transfer):{ "event": "charge.completed", "data": { "id": 123456, "tx_ref": "your-unique-transaction-ref-uuid", "flw_ref": "FLW-REF-ABCDEF123456", "amount": 5000, "currency": "NGN", "status": "successful", // or "failed", "pending" // ... other Flutterwave specific data ... } } -
Success Response (200 OK):
// The content of the successful response for a webhook can vary. // Often, a simple confirmation is sufficient. // Based on update_model_from_webhook, it returns a boolean. true
Or, if returning a DTO:
{ "status": "success", // Or "updated" "message": "Transaction updated successfully" }Important: Gateways usually expect a
200 OKresponse quickly to acknowledge receipt. -
Error Responses:
-
400 Bad Request: Invalid payload, missing crucial data. -
404 Not Found: Transaction corresponding to the webhook not found.
drf-spectacular is configured, you can access auto-generated API documentation at:
- Swagger UI:
http://127.0.0.1:8000/api/schema/swagger-ui/ - ReDoc:
http://127.0.0.1:8000/api/schema/redoc/
To run tests for this app (e.g., Apis):
python manage.py test Apis.testsOr to run all tests in the project:
python manage.py testcore_logic.py: Contains thePaymentServiceCoreand DTOs used internally by the core.payments_ports_and_adapters.py:PaymentGatewayInterface(Port)PaymentDetails,GatewayProcessPaymentResponseDTO,GatewayWebhookEventDTO(Data contracts for the port)FlutterWaveAdapter(Adapter for FlutterWave)PayStackAdapter(Adapter for PayStack)
repositories_ports_and_adapters.py:ClientRepositoryInterface(Port)ClientDTO,PaymentTransactionDTO,CreateTransactionDTO,UpdateTransactionDTO(Data contracts for the port)DjangoClientRepositoryAdapter(Adapter for Django ORM)
services.py: Application service layer, orchestrates interactions between views, core logic, and adapters.views.py: Django REST Framework API views for handling HTTP requests.serializers.py: Django REST Framework serializers for request/response data validation and transformation.urls.py: URL routing for the payment API endpoints.models.py(Implicit, fromOrders.models,Products.modelsetc.): Django models forPaymentTransaction,Orders,ClientModel, etc.
One of the key benefits of this architecture is the ease of adding new payment gateways:
-
Define DTOs (if needed): If the new gateway has significantly different request/response structures that cannot be mapped to existing DTOs (
PaymentDetails,GatewayProcessPaymentResponseDTO,GatewayWebhookEventDTO), define new ones or adapt existing ones. -
Create New Adapter:
- Create a new class (e.g.,
StripeAdapter) inpayments_ports_and_adapters.py. - This class must implement the
PaymentGatewayInterface. - Implement the
process_payment,handle_webhook, andverify_paymentmethods, translating data to/from the new gateway's API and your core DTOs.
- Create a new class (e.g.,
-
Update Service Layer (
services.py):-
Modify the
initiate_payment(and potentially a newhandle_webhook_service) function inservices.pyto allow selection of the desired gateway adapter. This could be based on a parameter in the request, configuration, or client settings. -
Instantiate the new adapter when appropriate.
-
Example:
# services.py # ... from .payments_ports_and_adapters import FlutterWaveAdapter, PayStackAdapter, StripeAdapter # Import new adapter def initiate_payment(validated_data) -> Dict[str, Any]: client_repo_adapter = DjangoClientRepositoryAdapter() #Modify this to extend to a new adapter if random.random() < 0.5: payment_gateway_adapter = PayStackAdapter() payment_gateway_name = "PayStack" else: payment_gateway_adapter = FlutterWaveAdapter() payment_gateway_name = "FlutterWave" payment_service = PaymentServiceCore( gateway_adapter=payment_gateway_adapter, client_repository=client_repo_adapter ) # ... rest of the logic
-
-
Update API Layer (
views.py,serializers.py):- If necessary, update serializers to accept gateway selection.
- Update views to pass the chosen gateway to the service layer.
Contributions are welcome! If you'd like to contribute, please:
- Fork the repository.
- Create a new branch (
git checkout -b feature/your-feature-name). - Make your changes.
- Write tests for your changes.
- Ensure all tests pass.
- Commit your changes (
git commit -m 'Add some feature'). - Push to the branch (
git push origin feature/your-feature-name). - Open a Pull Request.
Please ensure your code adheres to any existing coding standards and includes relevant documentation.