Payment Integrations
This guide covers building payment gateway integrations using the online_payment backbone module.
Architecture
Section titled “Architecture”The payment system is provider-agnostic. The online_payment module handles:
- Transaction lifecycle management
- API endpoints for initiating/tracking payments
- Webhook infrastructure
- Payment record creation and reconciliation
Payment provider modules (Stripe, PayPal, Razorpay) implement the integration interface.
┌─────────────────────────────────────────────────────────────────┐│ CONSUMING MODULES ││ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ ││ │ portal │ │ sales │ │ invoicing │ ││ │ (Pay Invoice)│ │(Pay Quote) │ │(Collect) │ ││ └──────┬──────┘ └──────┬──────┘ └──────┬──────┘ ││ │ │ │ ││ └────────────────┼────────────────┘ ││ ▼ ││ ┌────────────────────────────────┐ ││ │ online_payment (backbone) │ ││ │ - PaymentTransaction model │ ││ │ - PaymentLink model │ ││ │ - API routes & webhooks │ ││ └────────────────┬───────────────┘ ││ │ ││ ┌────────────────┼────────────────┐ ││ ▼ ▼ ▼ ││ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ ││ │payment_stripe│ │payment_razorpay│ │payment_paypal│ ││ └─────────────┘ └─────────────┘ └─────────────┘ │└─────────────────────────────────────────────────────────────────┘Integration Interface
Section titled “Integration Interface”The PaymentIntegration model in online_payment defines the interface:
class PaymentIntegration(Model): __inherit__ = "Integration"
# Payment-specific fields payment_state = Selection(choices=["Test", "Live"], default="Test") payment_integration_type = Selection( choices=["Redirect", "Embedded JS", "QR Code"], default="Redirect", ) payment_display_name = Char(max_length=100) payment_sequence = Integer(default=10)
# --- Methods to override ---
async def create_payment_session(self, transaction, integration_type: str) -> dict: """Create payment session with gateway.""" raise NotImplementedError()
async def process_webhook(self, event_type: str, payload: dict) -> dict: """Process webhook event from gateway.""" raise NotImplementedError()
async def process_return(self, request_data: dict) -> dict: """Handle return from redirect flow.""" return {"status": "pending"}
async def verify_webhook_signature(self, payload: bytes, signature: str) -> bool: """Verify webhook signature.""" return True
async def refund_transaction(self, transaction, amount: float = None) -> dict: """Process refund.""" raise NotImplementedError()
async def get_client_config(self) -> dict: """Get client-side configuration for embedded JS.""" return {}Integration Types
Section titled “Integration Types”| Type | Description | Example Providers |
|---|---|---|
Redirect | Customer redirected to gateway, then returns | PayPal, Stripe Checkout |
Embedded JS | Payment form embedded in page | Stripe Elements, Razorpay |
QR Code | Customer scans QR to pay | UPI, WeChat Pay |
Transaction Lifecycle
Section titled “Transaction Lifecycle”┌─────────┐ ┌─────────┐ ┌────────────┐ ┌──────────┐│ Draft │ ──► │ Pending │ ──► │ Authorized │ ──► │ Captured │└─────────┘ └─────────┘ └────────────┘ └──────────┘ │ │ ▼ ▼ ┌─────────┐ ┌──────────┐ │ Failed │ │ Refunded │ └─────────┘ └──────────┘ │ ▼ ┌───────────┐ │ Cancelled │ └───────────┘- Draft: Transaction created, not yet initiated
- Pending: Sent to gateway, awaiting response
- Authorized: Payment authorized, not yet captured
- Captured: Payment successful, funds received
- Failed: Payment failed
- Cancelled: Customer cancelled
- Refunded: Payment refunded
Building a Payment Provider Module
Section titled “Building a Payment Provider Module”Step 1: Create Module Structure
Section titled “Step 1: Create Module Structure”fullfinity/modules/payment_mygateway/├── __init__.py├── manifest.yaml├── models/│ ├── __init__.py│ └── mygateway_provider.py├── views/│ └── mygateway_views.yaml└── static/ └── img/ └── mygateway_logo.pngStep 2: Module Manifest
Section titled “Step 2: Module Manifest”name: MyGateway Paymentsidentifier: payment_mygatewayversion: '1.0'category: module_category_integrationsintegration_category: Online Paymentdescription: Accept payments via MyGatewaydependencies:- online_paymenticon: CreditCardimage: /static/img/mygateway_logo.pngstatic_paths:- imgStep 3: Provider Model
Section titled “Step 3: Provider Model”from fullfinity.engine.base import *import logging
logger = logging.getLogger(__name__)
class MyGatewayIntegration(Model): """Extends Integration with MyGateway-specific fields and methods.""" __inherit__ = "Integration"
# Gateway credentials (company_scoped for multi-company) mygateway_api_key = Char( max_length=255, description="API Key", hint="Your MyGateway API key", company_scoped=True, ) mygateway_secret_key = Char( max_length=255, description="Secret Key", company_scoped=True, ) mygateway_webhook_secret = Char( max_length=255, description="Webhook Secret", company_scoped=True, )
async def _is_mygateway(self) -> bool: """Check if this integration is MyGateway.""" await self.fetch_related("module") return self.module and self.module.identifier == "payment_mygateway"
async def enable_integration(self): """Validate credentials before enabling.""" if await self._is_mygateway(): if not self.mygateway_api_key: raise UserError("Please enter your MyGateway API Key.") if not self.mygateway_secret_key: raise UserError("Please enter your MyGateway Secret Key.") await super().enable_integration()
async def create_payment_session(self, transaction, integration_type: str = "Redirect") -> dict: """Create payment session with MyGateway.""" if not await self._is_mygateway(): return await super().create_payment_session(transaction, integration_type)
await transaction.fetch_related("currency", "contact")
# Call MyGateway API to create session session = await self._api_create_session( amount=transaction.amount, currency=transaction.currency.name if transaction.currency else "USD", reference=transaction.reference, return_url=self._build_return_url(transaction, "success"), cancel_url=self._build_return_url(transaction, "cancel"), )
# Store gateway reference transaction.gateway_reference = session["id"] await transaction.save()
# Return based on integration type if integration_type == "Embedded JS": return { "integration_type": "embedded_js", "client_config": { "api_key": self.mygateway_api_key, "session_id": session["id"], "js_url": "https://js.mygateway.com/v1/", }, } else: return { "integration_type": "redirect", "redirect_url": session["checkout_url"], }
def _build_return_url(self, transaction, status: str) -> str: """Build return URL with transaction reference.""" base_url = transaction.return_url if status == "success" else transaction.cancel_url if not base_url: return "" separator = "&" if "?" in base_url else "?" return f"{base_url}{separator}txn={transaction.reference}"
async def verify_webhook_signature(self, payload: bytes, signature: str) -> bool: """Verify MyGateway webhook signature.""" if not await self._is_mygateway(): return await super().verify_webhook_signature(payload, signature)
if not self.mygateway_webhook_secret: logger.warning(f"MyGateway integration {self.id} has no webhook secret") return False
# Implement signature verification (HMAC, etc.) import hmac import hashlib expected = hmac.new( self.mygateway_webhook_secret.encode(), payload, hashlib.sha256 ).hexdigest() return hmac.compare_digest(expected, signature)
async def process_webhook(self, event_type: str, payload: dict) -> dict: """Process MyGateway webhook events.""" if not await self._is_mygateway(): return await super().process_webhook(event_type, payload)
# Map gateway events to transaction status status_map = { "payment.completed": "captured", "payment.failed": "failed", "payment.cancelled": "cancelled", "payment.refunded": "refunded", }
status = status_map.get(event_type, "pending") data = payload.get("data", {})
return { "status": status, "transaction_reference": data.get("payment_id"), "gateway_status": event_type, "error_message": data.get("error_message"), }
async def process_return(self, request_data: dict) -> dict: """Handle return from redirect flow.""" if not await self._is_mygateway(): return await super().process_return(request_data)
# Verify payment status with gateway API session_id = request_data.get("session_id") if session_id: status = await self._api_get_session_status(session_id) return { "status": "captured" if status == "completed" else "pending", "gateway_reference": session_id, } return {"status": "pending"}
async def refund_transaction(self, transaction, amount: float = None) -> dict: """Process MyGateway refund.""" if not await self._is_mygateway(): return await super().refund_transaction(transaction, amount)
try: refund = await self._api_create_refund( payment_id=transaction.gateway_reference, amount=amount or transaction.amount, ) return { "success": True, "refund_reference": refund["id"], } except Exception as e: return { "success": False, "error_message": str(e), }
# --- Private API methods ---
async def _api_create_session(self, amount, currency, reference, return_url, cancel_url): """Create payment session via MyGateway API.""" # Implement API call import httpx async with httpx.AsyncClient() as client: response = await client.post( "https://api.mygateway.com/v1/sessions", headers={"Authorization": f"Bearer {self.mygateway_secret_key}"}, json={ "amount": int(amount * 100), # cents "currency": currency, "reference": reference, "success_url": return_url, "cancel_url": cancel_url, }, ) response.raise_for_status() return response.json()
async def _api_get_session_status(self, session_id): """Get session status from MyGateway API.""" import httpx async with httpx.AsyncClient() as client: response = await client.get( f"https://api.mygateway.com/v1/sessions/{session_id}", headers={"Authorization": f"Bearer {self.mygateway_secret_key}"}, ) response.raise_for_status() return response.json().get("status")
async def _api_create_refund(self, payment_id, amount): """Create refund via MyGateway API.""" import httpx async with httpx.AsyncClient() as client: response = await client.post( "https://api.mygateway.com/v1/refunds", headers={"Authorization": f"Bearer {self.mygateway_secret_key}"}, json={ "payment_id": payment_id, "amount": int(amount * 100), }, ) response.raise_for_status() return response.json()Step 4: View Extension
Section titled “Step 4: View Extension”- data_type: UiView identifier: integration_form_view __inherit__: integration_form_view arch: - type: field name: mygateway_api_key position: after:enabled properties: visible: Q(module__identifier__eq='payment_mygateway') - type: field name: mygateway_secret_key position: after:mygateway_api_key properties: widget: Password visible: Q(module__identifier__eq='payment_mygateway') - type: field name: mygateway_webhook_secret position: after:mygateway_secret_key properties: widget: Password visible: Q(module__identifier__eq='payment_mygateway')Webhook Handling
Section titled “Webhook Handling”The online_payment module provides a webhook route:
POST /payment/webhook/{integration_id}The webhook controller:
- Looks up the integration by ID
- Calls
verify_webhook_signature()to validate - Calls
process_webhook()to get status - Finds the transaction by gateway reference
- Calls
transaction.process_gateway_response()
Your integration just needs to implement the interface methods.
Return URL Handling
Section titled “Return URL Handling”For redirect-based flows, the backbone handles return:
GET /api/online_payment/return?txn={reference}&...The controller:
- Finds transaction by reference
- Calls
integration.process_return(params) - Calls
transaction.process_gateway_response() - Redirects user to appropriate page
Payment Success Actions
Section titled “Payment Success Actions”When a transaction is captured, PaymentTransaction automatically:
- Creates a
Paymentrecord - Creates a
Reconciliationlinking payment to invoice - Updates invoice payment status
- Calls document-specific success hooks (e.g., SaleOrder confirmation)
# PaymentTransaction._on_payment_success()async def _on_payment_success(self): """Called after successful capture.""" # Handle SaleOrder confirmation with signature if self.document_model == "SaleOrder" and self.document_id: await self._confirm_sale_order()Deferred Actions Pattern
Section titled “Deferred Actions Pattern”For flows where actions should only happen after payment:
# Initiating module stores pending action datatransaction = await PaymentTransaction.create( integration=integration_id, document_model="SaleOrder", document_id=order.id, amount=amount, pending_action_data={ "signature_data": { "signature": signature_image, "signed_by": signer_name, } },)
# PaymentTransaction applies on successasync def _confirm_sale_order(self): pending_data = self.pending_action_data or {} signature_data = pending_data.get("signature_data", {})
if signature_data: order.signature = signature_data.get("signature") order.signed_by = signature_data.get("signed_by") await order.save()
await order.action_confirm()API Endpoints
Section titled “API Endpoints”The online_payment module provides these endpoints:
| Endpoint | Auth | Description |
|---|---|---|
GET /api/online_payment/integrations | User | List available payment integrations |
POST /api/online_payment/initiate | User | Create transaction, get integration instructions |
GET /api/online_payment/transaction/{id} | User | Get transaction status |
GET /api/online_payment/return | Public | Handle redirect return |
GET /api/online_payment/cancel | Public | Handle cancelled payment |
POST /payment/webhook/{id} | Public | Receive gateway webhooks |
Initiate Response Format
Section titled “Initiate Response Format”# For redirect flow{ "transaction_id": 123, "reference": "TXN-ABC123", "integration_type": "redirect", "redirect_url": "https://checkout.mygateway.com/session/xyz"}
# For embedded JS flow{ "transaction_id": 123, "reference": "TXN-ABC123", "integration_type": "embedded_js", "client_config": { "api_key": "pk_test_...", "session_id": "sess_xyz", "js_url": "https://js.mygateway.com/v1/" }}Testing
Section titled “Testing”- Create an Integration record for your provider
- Configure test credentials
- Enable the integration
- Create a test invoice
- Initiate payment via API or UI
- Complete payment in gateway sandbox
- Verify webhook updates transaction
- Check Payment record was created
Reference Implementation
Section titled “Reference Implementation”See payment_stripe module for a complete example:
- stripe_provider.py - Full Stripe integration
- Supports both Checkout (redirect) and Elements (embedded JS)
- Webhook signature verification
- Refund support
Best Practices
Section titled “Best Practices”- Always verify webhooks - Use HMAC signature verification
- Store gateway reference - Save the external ID for lookups
- Handle idempotency - Webhooks may be sent multiple times
- Use test mode - Implement
payment_state(Test/Live) switching - Log webhook payloads - Store in
transaction.webhook_logfor debugging - Validate before enable - Check credentials in
enable_integration() - Support partial refunds - Accept optional amount parameter
Next Steps
Section titled “Next Steps”- Integrations - General integration framework
- Creating Modules - Module development guide