import os from dataclasses import dataclass from typing import Optional import requests class PaymentGatewayError(RuntimeError): def __init__(self, message: str, status_code: Optional[int] = None, payload: Optional[dict] = None) -> None: super().__init__(message) self.status_code = status_code self.payload = payload or {} @dataclass class PaymentInitResult: external_id: str status: Optional[str] redirect_url: Optional[str] payload: dict class BasePaymentGateway: def create_payment( self, amount: int, currency: str, description: str, source: dict, callback_url: Optional[str], given_id: str, metadata: dict, ) -> PaymentInitResult: raise NotImplementedError def capture_payment(self, external_id: str, amount: Optional[int] = None) -> None: """Capture an authorized payment. Amount in minor units; omit for full capture.""" raise NotImplementedError def refund_payment(self, external_id: str, amount: Optional[int] = None) -> None: """Refund a paid/captured payment. Amount in minor units; omit for full refund.""" raise NotImplementedError class MoyasarGateway(BasePaymentGateway): def __init__(self) -> None: self.secret_key = os.getenv("MOYASAR_SECRET_KEY") self.publishable_key = os.getenv("MOYASAR_PUBLISHABLE_KEY") self.base_url = os.getenv("MOYASAR_BASE_URL", "https://api.moyasar.com") def _assert_config(self) -> None: if not self.secret_key or not self.publishable_key: raise ValueError("Moyasar credentials are not configured") def create_payment( self, amount: int, currency: str, description: str, source: dict, callback_url: Optional[str], given_id: str, metadata: dict, ) -> PaymentInitResult: self._assert_config() url = f"{self.base_url}/v1/payments" payload = { "amount": amount, "currency": currency, "description": description, "source": source, "given_id": given_id, "metadata": metadata, } if callback_url: payload["callback_url"] = callback_url try: response = requests.post(url, json=payload, auth=(self.secret_key, ""), timeout=10) except requests.RequestException as exc: raise PaymentGatewayError("Failed to reach Moyasar") from exc try: data = response.json() if response.content else {} except ValueError as exc: raise PaymentGatewayError("Invalid response from Moyasar") from exc if response.status_code not in (200, 201): raise PaymentGatewayError( "Moyasar returned an error", status_code=response.status_code, payload=data, ) redirect_url = None source_payload = data.get("source") or {} if isinstance(source_payload, dict): redirect_url = source_payload.get("transaction_url") return PaymentInitResult( external_id=data.get("id"), status=data.get("status"), redirect_url=redirect_url, payload=data, ) def capture_payment(self, external_id: str, amount: Optional[int] = None) -> None: """Capture an authorized payment. Amount in minor units; omit for full capture.""" self._assert_config() url = f"{self.base_url}/v1/payments/{external_id}/capture" payload = {} if amount is None else {"amount": amount} try: response = requests.post(url, json=payload, auth=(self.secret_key, ""), timeout=10) except requests.RequestException as exc: raise PaymentGatewayError("Failed to reach Moyasar for capture") from exc if response.status_code not in (200, 201): data = response.json() if response.content else {} raise PaymentGatewayError( "Moyasar capture failed", status_code=response.status_code, payload=data, ) def refund_payment(self, external_id: str, amount: Optional[int] = None) -> None: """Refund a paid/captured payment. Amount in minor units; omit for full refund.""" self._assert_config() url = f"{self.base_url}/v1/payments/{external_id}/refund" payload = {} if amount is None else {"amount": amount} try: response = requests.post(url, json=payload, auth=(self.secret_key, ""), timeout=10) except requests.RequestException as exc: raise PaymentGatewayError("Failed to reach Moyasar for refund") from exc if response.status_code not in (200, 201): data = response.json() if response.content else {} raise PaymentGatewayError( "Moyasar refund failed", status_code=response.status_code, payload=data, )