Skip to content

Payment Integrations

This guide covers building payment gateway integrations using the online_payment backbone module.

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│ │
│ └─────────────┘ └─────────────┘ └─────────────┘ │
└─────────────────────────────────────────────────────────────────┘

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 {}
TypeDescriptionExample Providers
RedirectCustomer redirected to gateway, then returnsPayPal, Stripe Checkout
Embedded JSPayment form embedded in pageStripe Elements, Razorpay
QR CodeCustomer scans QR to payUPI, WeChat Pay
┌─────────┐ ┌─────────┐ ┌────────────┐ ┌──────────┐
│ 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
fullfinity/modules/payment_mygateway/
├── __init__.py
├── manifest.yaml
├── models/
│ ├── __init__.py
│ └── mygateway_provider.py
├── views/
│ └── mygateway_views.yaml
└── static/
└── img/
└── mygateway_logo.png
manifest.yaml
name: MyGateway Payments
identifier: payment_mygateway
version: '1.0'
category: module_category_integrations
integration_category: Online Payment
description: Accept payments via MyGateway
dependencies:
- online_payment
icon: CreditCard
image: /static/img/mygateway_logo.png
static_paths:
- img
models/mygateway_provider.py
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()
views/mygateway_views.yaml
- 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')

The online_payment module provides a webhook route:

POST /payment/webhook/{integration_id}

The webhook controller:

  1. Looks up the integration by ID
  2. Calls verify_webhook_signature() to validate
  3. Calls process_webhook() to get status
  4. Finds the transaction by gateway reference
  5. Calls transaction.process_gateway_response()

Your integration just needs to implement the interface methods.

For redirect-based flows, the backbone handles return:

GET /api/online_payment/return?txn={reference}&...

The controller:

  1. Finds transaction by reference
  2. Calls integration.process_return(params)
  3. Calls transaction.process_gateway_response()
  4. Redirects user to appropriate page

When a transaction is captured, PaymentTransaction automatically:

  1. Creates a Payment record
  2. Creates a Reconciliation linking payment to invoice
  3. Updates invoice payment status
  4. 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()

For flows where actions should only happen after payment:

# Initiating module stores pending action data
transaction = 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 success
async 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()

The online_payment module provides these endpoints:

EndpointAuthDescription
GET /api/online_payment/integrationsUserList available payment integrations
POST /api/online_payment/initiateUserCreate transaction, get integration instructions
GET /api/online_payment/transaction/{id}UserGet transaction status
GET /api/online_payment/returnPublicHandle redirect return
GET /api/online_payment/cancelPublicHandle cancelled payment
POST /payment/webhook/{id}PublicReceive gateway webhooks
# 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/"
}
}
  1. Create an Integration record for your provider
  2. Configure test credentials
  3. Enable the integration
  4. Create a test invoice
  5. Initiate payment via API or UI
  6. Complete payment in gateway sandbox
  7. Verify webhook updates transaction
  8. Check Payment record was created

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
  1. Always verify webhooks - Use HMAC signature verification
  2. Store gateway reference - Save the external ID for lookups
  3. Handle idempotency - Webhooks may be sent multiple times
  4. Use test mode - Implement payment_state (Test/Live) switching
  5. Log webhook payloads - Store in transaction.webhook_log for debugging
  6. Validate before enable - Check credentials in enable_integration()
  7. Support partial refunds - Accept optional amount parameter