feat: unify gateway entry point via PaymentManager

This commit is contained in:
sankalp committed 2026-09-03 00:40:35 +05:45
1 parent f2affa6356
commit 19f530b4f5
1 file changed
+152
+152
View File
@@ -0,0 +1,152 @@
from typing import Any, TypeVar
from nepali_payment.enums import PaymentMethod, PaymentMode
from nepali_payment.exceptions import ValidationError
from nepali_payment.http import ApiService
from nepali_payment.service_factory import get_payment_service
T = TypeVar("T")
class PaymentManager:
"""The easy entry point for taking payments.
Pick a gateway (Khalti, eSewa, Fonepay, ConnectIPS), a mode (sandbox or
production) and your secret key once. Then you can start a payment, check a
payment, or refund tax without worrying about which provider you chose.
"""
def __init__(
self,
payment_method: PaymentMethod,
payment_mode: PaymentMode,
secret_key: str,
api=None,
config=None,
):
"""Create a manager for one gateway and one mode.
Args:
payment_method: The gateway to use (eSewa, Khalti, Fonepay,
ConnectIPS).
payment_mode: ``SANDBOX`` or ``PRODUCTION``.
secret_key: The provider's secret key, for signing / authentication.
Not used for ConnectIPS - that gateway uses ``config`` instead.
api: Your own HTTP client (optional). If left out, a new one is made.
config: For ConnectIPS only: a :class:`ConnectIpsConfig` holding the
merchant credentials and certificate.
"""
if not secret_key and payment_method != PaymentMethod.CONNECTIPS:
raise ValueError("Secret key cannot be null or empty.")
self._secret_key = secret_key
self._method = payment_method
self._mode = payment_mode
self._api = api
self._config = config
self._service_cache = None
def _api_service(self):
"""Return the shared HTTP client, creating it on the first call.
We reuse the same :class:`~nepali_payment.http.ApiService` (and its
pooled connection) for every operation. That way, when this manager runs
lots of calls in a loop, it never opens a fresh connection each time
(the "N+1 connections" problem).
"""
if self._api is None:
self._api = ApiService()
return self._api
def _service(self):
"""Build the service for our gateway, sharing the HTTP client.
The service is built once and reused. That is both faster (e.g. for
ConnectIPS it avoids re-parsing the RSA certificate on every call) and
keeps a stable HTTP client across operations.
"""
if self._service_cache is None:
self._service_cache = get_payment_service(
self._method, self._secret_key, self._mode, api=self._api_service(), config=self._config
)
return self._service_cache
def initiate_payment(self, result_cls: type[T], content: Any) -> T:
"""Start a payment and return a typed result.
Args:
result_cls: The shape the result should be turned into, e.g.
:class:`~nepali_payment.models.PaymentResult`.
content: The request model for your gateway.
Returns:
A ``result_cls`` wrapping the provider's response.
Raises:
ValidationError: If ``content`` is ``None``.
"""
if content is None:
raise ValidationError("Payment content cannot be null.")
service = self._service()
return service.initiate_payment(result_cls, content)
def verify_payment(self, result_cls: type[T], content: str) -> T:
"""Check whether a previously started payment went through.
Args:
result_cls: The shape the result should be turned into.
content: The provider's reference (e.g. Khalti ``pidx``, Fonepay PRN).
Returns:
A ``result_cls`` instance wrapping the verification response.
Raises:
ValidationError: If ``content`` is empty.
"""
if not content:
raise ValidationError("Verification content cannot be null or empty.")
service = self._service()
return service.verify_payment(result_cls, content)
def process_tax_refund(self, result_cls: type[T], request: Any) -> T:
"""Process a Fonepay tax refund (only available for Fonepay).
Args:
result_cls: The shape the result should be turned into.
request: A :class:`~nepali_payment.models.fonepay.TaxRefundRequest`.
Returns:
A ``result_cls`` wrapping the refund response.
Raises:
ValidationError: If the gateway is not Fonepay.
"""
service = self._service()
if hasattr(service, "process_tax_refund"):
return service.process_tax_refund(result_cls, request)
raise ValidationError("Tax refund is only supported for Fonepay.")
def process_static_qr(self, result_cls: type[T], request: Any) -> T:
"""Fetch the fixed Fonepay static QR (Fonepay only).
A static QR is one reusable merchant QR - the customer types in the
amount when scanning - unlike the per-transaction dynamic QR.
Args:
result_cls: The shape the result should be turned into.
request: A :class:`~nepali_payment.models.fonepay.StaticQrRequest`.
Returns:
A ``result_cls`` wrapping the static QR response.
Raises:
ValidationError: If the gateway is not Fonepay.
"""
service = self._service()
if hasattr(service, "process_static_qr"):
return service.process_static_qr(result_cls, request)
raise ValidationError("Static QR is only supported for Fonepay.")