pay-engine 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
pay_engine/__init__.py ADDED
@@ -0,0 +1,168 @@
1
+ """
2
+ pay-engine: All-in-one payment engine for Uzbekistan.
3
+ Supports Payme, Click, and Uzum Bank with one-liners, Telegram buttons, and auto-charge.
4
+ """
5
+
6
+ from typing import Any, Dict, List, Optional, Union
7
+
8
+ from .core.config import ClickConfig, PaymeConfig, UzumConfig
9
+ from .core.exceptions import (
10
+ CardTokenError,
11
+ ConfigurationError,
12
+ InvalidAmountError,
13
+ OrderNotFoundError,
14
+ PayEngineError,
15
+ SignatureVerificationError,
16
+ )
17
+ from .core.models import (
18
+ CardChargeRequest,
19
+ CardChargeResult,
20
+ PaymentRequest,
21
+ TransactionStatus,
22
+ WebhookResult,
23
+ )
24
+ from .providers.click.client import ClickClient
25
+ from .providers.click.handler import ClickHandler
26
+ from .providers.payme.client import PaymeClient
27
+ from .providers.payme.handler import PaymeHandler
28
+ from .providers.uzum.client import UzumClient
29
+ from .providers.uzum.handler import UzumHandler
30
+
31
+ __version__ = "0.1.0"
32
+
33
+
34
+ class _PaymeProxy:
35
+ """Convenience helper for Payme one-liners."""
36
+
37
+ @staticmethod
38
+ def link(
39
+ amount: float,
40
+ order_id: Union[str, int],
41
+ return_url: Optional[str] = None,
42
+ merchant_id: Optional[str] = None,
43
+ test_mode: Optional[bool] = None,
44
+ **kwargs: Any,
45
+ ) -> str:
46
+ client = PaymeClient(merchant_id=merchant_id, test_mode=test_mode)
47
+ return client.create_link(amount=amount, order_id=order_id, return_url=return_url, **kwargs)
48
+
49
+ @staticmethod
50
+ def charge(
51
+ card_token: str,
52
+ amount: float,
53
+ order_id: Union[str, int],
54
+ secret_key: Optional[str] = None,
55
+ description: Optional[str] = None,
56
+ **kwargs: Any,
57
+ ) -> CardChargeResult:
58
+ client = PaymeClient(secret_key=secret_key)
59
+ return client.charge_card(card_token=card_token, amount=amount, order_id=order_id, description=description, **kwargs)
60
+
61
+
62
+ class _ClickProxy:
63
+ """Convenience helper for Click one-liners."""
64
+
65
+ @staticmethod
66
+ def link(
67
+ amount: float,
68
+ order_id: Union[str, int],
69
+ return_url: Optional[str] = None,
70
+ service_id: Optional[str] = None,
71
+ merchant_id: Optional[str] = None,
72
+ **kwargs: Any,
73
+ ) -> str:
74
+ client = ClickClient(service_id=service_id, merchant_id=merchant_id)
75
+ return client.create_link(amount=amount, order_id=order_id, return_url=return_url, **kwargs)
76
+
77
+ @staticmethod
78
+ def charge(
79
+ card_token: str,
80
+ amount: float,
81
+ order_id: Union[str, int],
82
+ service_id: Optional[str] = None,
83
+ secret_key: Optional[str] = None,
84
+ merchant_user_id: Optional[str] = None,
85
+ **kwargs: Any,
86
+ ) -> CardChargeResult:
87
+ client = ClickClient(service_id=service_id, secret_key=secret_key, merchant_user_id=merchant_user_id)
88
+ return client.charge_card(card_token=card_token, amount=amount, order_id=order_id, **kwargs)
89
+
90
+
91
+ class _UzumProxy:
92
+ """Convenience helper for Uzum Bank one-liners."""
93
+
94
+ @staticmethod
95
+ def link(
96
+ amount: float,
97
+ order_id: Union[str, int],
98
+ return_url: Optional[str] = None,
99
+ shop_id: Optional[str] = None,
100
+ **kwargs: Any,
101
+ ) -> str:
102
+ client = UzumClient(shop_id=shop_id)
103
+ return client.create_link(amount=amount, order_id=order_id, return_url=return_url, **kwargs)
104
+
105
+
106
+ class PayEngine:
107
+ """Unified master engine for payments."""
108
+
109
+ def __init__(
110
+ self,
111
+ payme_merchant_id: Optional[str] = None,
112
+ payme_secret_key: Optional[str] = None,
113
+ click_service_id: Optional[str] = None,
114
+ click_merchant_id: Optional[str] = None,
115
+ click_secret_key: Optional[str] = None,
116
+ uzum_shop_id: Optional[str] = None,
117
+ uzum_secret_key: Optional[str] = None,
118
+ test_mode: bool = False,
119
+ ):
120
+ self.payme = PaymeClient(merchant_id=payme_merchant_id, secret_key=payme_secret_key, test_mode=test_mode) if (payme_merchant_id or PaymeConfig().merchant_id) else None
121
+ self.click = ClickClient(service_id=click_service_id, merchant_id=click_merchant_id, secret_key=click_secret_key) if (click_service_id or ClickConfig().service_id) else None
122
+ self.uzum = UzumClient(shop_id=uzum_shop_id, secret_key=uzum_secret_key) if (uzum_shop_id or UzumConfig().shop_id) else None
123
+
124
+ def get_links(
125
+ self,
126
+ amount: float,
127
+ order_id: Union[str, int],
128
+ return_url: Optional[str] = None,
129
+ ) -> Dict[str, str]:
130
+ """Returns checkout URLs for all configured providers."""
131
+ links = {}
132
+ if self.payme:
133
+ links["payme"] = self.payme.create_link(amount=amount, order_id=order_id, return_url=return_url)
134
+ if self.click:
135
+ links["click"] = self.click.create_link(amount=amount, order_id=order_id, return_url=return_url)
136
+ if self.uzum:
137
+ links["uzum"] = self.uzum.create_link(amount=amount, order_id=order_id, return_url=return_url)
138
+ return links
139
+
140
+
141
+ # Public helper instances
142
+ payme = _PaymeProxy()
143
+ click = _ClickProxy()
144
+ uzum = _UzumProxy()
145
+
146
+ __all__ = [
147
+ "PayEngine",
148
+ "payme",
149
+ "click",
150
+ "uzum",
151
+ "PaymeClient",
152
+ "PaymeHandler",
153
+ "ClickClient",
154
+ "ClickHandler",
155
+ "UzumClient",
156
+ "UzumHandler",
157
+ "PayEngineError",
158
+ "ConfigurationError",
159
+ "SignatureVerificationError",
160
+ "InvalidAmountError",
161
+ "OrderNotFoundError",
162
+ "CardTokenError",
163
+ "TransactionStatus",
164
+ "PaymentRequest",
165
+ "WebhookResult",
166
+ "CardChargeRequest",
167
+ "CardChargeResult",
168
+ ]
@@ -0,0 +1,35 @@
1
+ """Core module for pay-engine."""
2
+
3
+ from .config import ClickConfig, PaymeConfig, UzumConfig
4
+ from .exceptions import (
5
+ CardTokenError,
6
+ ConfigurationError,
7
+ InvalidAmountError,
8
+ OrderNotFoundError,
9
+ PayEngineError,
10
+ SignatureVerificationError,
11
+ )
12
+ from .models import (
13
+ CardChargeRequest,
14
+ CardChargeResult,
15
+ PaymentRequest,
16
+ TransactionStatus,
17
+ WebhookResult,
18
+ )
19
+
20
+ __all__ = [
21
+ "PayEngineError",
22
+ "ConfigurationError",
23
+ "SignatureVerificationError",
24
+ "InvalidAmountError",
25
+ "OrderNotFoundError",
26
+ "CardTokenError",
27
+ "TransactionStatus",
28
+ "PaymentRequest",
29
+ "WebhookResult",
30
+ "CardChargeRequest",
31
+ "CardChargeResult",
32
+ "PaymeConfig",
33
+ "ClickConfig",
34
+ "UzumConfig",
35
+ ]
@@ -0,0 +1,74 @@
1
+ """Configuration management for pay-engine."""
2
+
3
+ import os
4
+ from dataclasses import dataclass
5
+ from typing import Optional
6
+
7
+
8
+ def _load_env_file():
9
+ """Simple parser to load .env file if it exists, without needing python-dotenv."""
10
+ env_path = os.path.join(os.getcwd(), ".env")
11
+ if os.path.isfile(env_path):
12
+ try:
13
+ with open(env_path, "r", encoding="utf-8") as f:
14
+ for line in f:
15
+ line = line.strip()
16
+ if not line or line.startswith("#") or "=" not in line:
17
+ continue
18
+ key, val = line.split("=", 1)
19
+ key = key.strip()
20
+ val = val.strip().strip("'\"")
21
+ if key not in os.environ:
22
+ os.environ[key] = val
23
+ except Exception:
24
+ pass
25
+
26
+
27
+ # Automatically attempt to load .env on module import
28
+ _load_env_file()
29
+
30
+
31
+ @dataclass
32
+ class PaymeConfig:
33
+ merchant_id: Optional[str] = None
34
+ secret_key: Optional[str] = None
35
+ test_mode: bool = False
36
+
37
+ def __post_init__(self):
38
+ if not self.merchant_id:
39
+ self.merchant_id = os.getenv("PAYME_MERCHANT_ID")
40
+ if not self.secret_key:
41
+ self.secret_key = os.getenv("PAYME_SECRET_KEY")
42
+ test_env = os.getenv("PAYME_TEST_MODE", "").lower()
43
+ if test_env in ("1", "true", "yes"):
44
+ self.test_mode = True
45
+
46
+
47
+ @dataclass
48
+ class ClickConfig:
49
+ service_id: Optional[str] = None
50
+ merchant_id: Optional[str] = None
51
+ secret_key: Optional[str] = None
52
+ merchant_user_id: Optional[str] = None
53
+
54
+ def __post_init__(self):
55
+ if not self.service_id:
56
+ self.service_id = os.getenv("CLICK_SERVICE_ID")
57
+ if not self.merchant_id:
58
+ self.merchant_id = os.getenv("CLICK_MERCHANT_ID")
59
+ if not self.secret_key:
60
+ self.secret_key = os.getenv("CLICK_SECRET_KEY")
61
+ if not self.merchant_user_id:
62
+ self.merchant_user_id = os.getenv("CLICK_MERCHANT_USER_ID")
63
+
64
+
65
+ @dataclass
66
+ class UzumConfig:
67
+ shop_id: Optional[str] = None
68
+ secret_key: Optional[str] = None
69
+
70
+ def __post_init__(self):
71
+ if not self.shop_id:
72
+ self.shop_id = os.getenv("UZUM_SHOP_ID")
73
+ if not self.secret_key:
74
+ self.secret_key = os.getenv("UZUM_SECRET_KEY")
@@ -0,0 +1,30 @@
1
+ """Exceptions for pay-engine."""
2
+
3
+ class PayEngineError(Exception):
4
+ """Base exception for all pay-engine errors."""
5
+ pass
6
+
7
+
8
+ class ConfigurationError(PayEngineError):
9
+ """Raised when provider credentials or configurations are missing or invalid."""
10
+ pass
11
+
12
+
13
+ class SignatureVerificationError(PayEngineError):
14
+ """Raised when an incoming webhook signature or authentication fails."""
15
+ pass
16
+
17
+
18
+ class InvalidAmountError(PayEngineError):
19
+ """Raised when transaction amount is invalid, zero, or negative."""
20
+ pass
21
+
22
+
23
+ class OrderNotFoundError(PayEngineError):
24
+ """Raised when an order or transaction ID cannot be found."""
25
+ pass
26
+
27
+
28
+ class CardTokenError(PayEngineError):
29
+ """Raised when card tokenization, binding, or auto-charge fails."""
30
+ pass
@@ -0,0 +1,51 @@
1
+ """Data models for pay-engine."""
2
+
3
+ from dataclasses import dataclass, field
4
+ from enum import Enum
5
+ from typing import Any, Dict, Optional
6
+
7
+
8
+ class TransactionStatus(str, Enum):
9
+ PENDING = "pending"
10
+ PAID = "paid"
11
+ CANCELLED = "cancelled"
12
+ FAILED = "failed"
13
+
14
+
15
+ @dataclass
16
+ class PaymentRequest:
17
+ amount: float
18
+ order_id: str
19
+ description: Optional[str] = None
20
+ return_url: Optional[str] = None
21
+ extra_params: Dict[str, Any] = field(default_factory=dict)
22
+
23
+
24
+ @dataclass
25
+ class WebhookResult:
26
+ is_paid: bool
27
+ provider: str
28
+ order_id: str
29
+ amount: float
30
+ transaction_id: Optional[str] = None
31
+ status: TransactionStatus = TransactionStatus.PENDING
32
+ raw_data: Dict[str, Any] = field(default_factory=dict)
33
+ response_data: Optional[Dict[str, Any]] = None
34
+
35
+
36
+ @dataclass
37
+ class CardChargeRequest:
38
+ card_token: str
39
+ amount: float
40
+ order_id: str
41
+ description: Optional[str] = None
42
+
43
+
44
+ @dataclass
45
+ class CardChargeResult:
46
+ success: bool
47
+ order_id: str
48
+ amount: float
49
+ transaction_id: Optional[str] = None
50
+ message: Optional[str] = None
51
+ raw_data: Dict[str, Any] = field(default_factory=dict)
@@ -0,0 +1,14 @@
1
+ """Integrations package for pay-engine."""
2
+
3
+ from .fastapi import click_router, pay_router, payme_router, uzum_router
4
+ from .telegram import format_amount, get_payment_links, payment_buttons
5
+
6
+ __all__ = [
7
+ "payme_router",
8
+ "click_router",
9
+ "uzum_router",
10
+ "pay_router",
11
+ "payment_buttons",
12
+ "get_payment_links",
13
+ "format_amount",
14
+ ]
@@ -0,0 +1,137 @@
1
+ """FastAPI integration for pay-engine."""
2
+
3
+ import inspect
4
+ from typing import Any, Callable, Optional
5
+
6
+ from ..providers.click.handler import ClickHandler
7
+ from ..providers.payme.handler import PaymeHandler
8
+ from ..providers.uzum.handler import UzumHandler
9
+
10
+ try:
11
+ from fastapi import APIRouter, Request, Response
12
+ from fastapi.responses import JSONResponse
13
+ FASTAPI_AVAILABLE = True
14
+ except ImportError:
15
+ FASTAPI_AVAILABLE = False
16
+ APIRouter = Any # type: ignore
17
+ Request = Any # type: ignore
18
+ Response = Any # type: ignore
19
+ JSONResponse = Any # type: ignore
20
+
21
+
22
+ def _ensure_fastapi():
23
+ if not FASTAPI_AVAILABLE:
24
+ raise ImportError("FastAPI is not installed. Install it via 'pip install fastapi'.")
25
+
26
+
27
+ async def _run_callback(cb: Optional[Callable], *args):
28
+ if not cb:
29
+ return
30
+ if inspect.iscoroutinefunction(cb):
31
+ await cb(*args)
32
+ else:
33
+ cb(*args)
34
+
35
+
36
+ def payme_router(
37
+ on_success: Optional[Callable[[str, float], Any]] = None,
38
+ check_order: Optional[Callable[[str, float], bool]] = None,
39
+ secret_key: Optional[str] = None,
40
+ path: str = "/payme",
41
+ ) -> APIRouter:
42
+ """Creates a ready-to-mount FastAPI APIRouter for Payme JSON-RPC webhook."""
43
+ _ensure_fastapi()
44
+ router = APIRouter()
45
+
46
+ async def _on_success_wrapper(order_id: str, amount: float):
47
+ await _run_callback(on_success, order_id, amount)
48
+
49
+ handler = PaymeHandler(
50
+ secret_key=secret_key,
51
+ on_success=_on_success_wrapper,
52
+ check_order=check_order,
53
+ )
54
+
55
+ @router.post(path)
56
+ async def handle_payme(request: Request):
57
+ headers = dict(request.headers)
58
+ body = await request.body()
59
+ result = handler.process(headers, body)
60
+ return JSONResponse(content=result.response_data or {})
61
+
62
+ return router
63
+
64
+
65
+ def click_router(
66
+ on_success: Optional[Callable[[str, float], Any]] = None,
67
+ check_order: Optional[Callable[[str, float], bool]] = None,
68
+ service_id: Optional[str] = None,
69
+ secret_key: Optional[str] = None,
70
+ path: str = "/click",
71
+ ) -> APIRouter:
72
+ """Creates a ready-to-mount FastAPI APIRouter for Click webhook."""
73
+ _ensure_fastapi()
74
+ router = APIRouter()
75
+
76
+ async def _on_success_wrapper(order_id: str, amount: float):
77
+ await _run_callback(on_success, order_id, amount)
78
+
79
+ handler = ClickHandler(
80
+ service_id=service_id,
81
+ secret_key=secret_key,
82
+ on_success=_on_success_wrapper,
83
+ check_order=check_order,
84
+ )
85
+
86
+ @router.post(path)
87
+ async def handle_click(request: Request):
88
+ headers = dict(request.headers)
89
+ body = await request.body()
90
+ result = handler.process(headers, body)
91
+ return JSONResponse(content=result.response_data or {})
92
+
93
+ return router
94
+
95
+
96
+ def uzum_router(
97
+ on_success: Optional[Callable[[str, float], Any]] = None,
98
+ secret_key: Optional[str] = None,
99
+ path: str = "/uzum",
100
+ ) -> APIRouter:
101
+ """Creates a ready-to-mount FastAPI APIRouter for Uzum Bank webhook."""
102
+ _ensure_fastapi()
103
+ router = APIRouter()
104
+
105
+ async def _on_success_wrapper(order_id: str, amount: float):
106
+ await _run_callback(on_success, order_id, amount)
107
+
108
+ handler = UzumHandler(
109
+ secret_key=secret_key,
110
+ on_success=_on_success_wrapper,
111
+ )
112
+
113
+ @router.post(path)
114
+ async def handle_uzum(request: Request):
115
+ headers = dict(request.headers)
116
+ body = await request.body()
117
+ result = handler.process(headers, body)
118
+ return JSONResponse(content=result.response_data or {})
119
+
120
+ return router
121
+
122
+
123
+ def pay_router(
124
+ on_success: Optional[Callable[[str, float], Any]] = None,
125
+ check_order: Optional[Callable[[str, float], bool]] = None,
126
+ prefix: str = "/payments",
127
+ ) -> APIRouter:
128
+ """
129
+ Creates an all-in-one APIRouter including Payme (/payme), Click (/click), and Uzum (/uzum).
130
+ Mount with `app.include_router(pay_router(...))`.
131
+ """
132
+ _ensure_fastapi()
133
+ master = APIRouter(prefix=prefix)
134
+ master.include_router(payme_router(on_success=on_success, check_order=check_order))
135
+ master.include_router(click_router(on_success=on_success, check_order=check_order))
136
+ master.include_router(uzum_router(on_success=on_success))
137
+ return master
@@ -0,0 +1,107 @@
1
+ """Telegram bot integrations for pay-engine."""
2
+
3
+ from typing import Any, Dict, List, Optional, Union
4
+
5
+ from ..core.config import ClickConfig, PaymeConfig, UzumConfig
6
+ from ..providers.click.client import ClickClient
7
+ from ..providers.payme.client import PaymeClient
8
+ from ..providers.uzum.client import UzumClient
9
+
10
+ try:
11
+ from aiogram.types import InlineKeyboardButton, InlineKeyboardMarkup
12
+ AIOGRAM_AVAILABLE = True
13
+ except ImportError:
14
+ AIOGRAM_AVAILABLE = False
15
+ InlineKeyboardButton = Any # type: ignore
16
+ InlineKeyboardMarkup = Any # type: ignore
17
+
18
+
19
+ def format_amount(amount: float) -> str:
20
+ """Format amount with space separator, e.g. 100000 -> '100 000'."""
21
+ if amount.is_integer():
22
+ return f"{int(amount):,}".replace(",", " ")
23
+ return f"{amount:,.2f}".replace(",", " ")
24
+
25
+
26
+ def get_payment_links(
27
+ amount: float,
28
+ order_id: Union[str, int],
29
+ providers: Optional[List[str]] = None,
30
+ return_url: Optional[str] = None,
31
+ ) -> Dict[str, str]:
32
+ """
33
+ Generate payment URLs for multiple providers at once.
34
+ Only providers with valid credentials in env or config will be included.
35
+ """
36
+ if providers is None:
37
+ providers = ["payme", "click", "uzum"]
38
+
39
+ links = {}
40
+
41
+ if "payme" in providers:
42
+ cfg = PaymeConfig()
43
+ if cfg.merchant_id:
44
+ try:
45
+ links["payme"] = PaymeClient(merchant_id=cfg.merchant_id).create_link(
46
+ amount=amount, order_id=order_id, return_url=return_url
47
+ )
48
+ except Exception:
49
+ pass
50
+
51
+ if "click" in providers:
52
+ cfg = ClickConfig()
53
+ if cfg.service_id and cfg.merchant_id:
54
+ try:
55
+ links["click"] = ClickClient(
56
+ service_id=cfg.service_id, merchant_id=cfg.merchant_id
57
+ ).create_link(amount=amount, order_id=order_id, return_url=return_url)
58
+ except Exception:
59
+ pass
60
+
61
+ if "uzum" in providers:
62
+ cfg = UzumConfig()
63
+ if cfg.shop_id:
64
+ try:
65
+ links["uzum"] = UzumClient(shop_id=cfg.shop_id).create_link(
66
+ amount=amount, order_id=order_id, return_url=return_url
67
+ )
68
+ except Exception:
69
+ pass
70
+
71
+ return links
72
+
73
+
74
+ def payment_buttons(
75
+ amount: float,
76
+ order_id: Union[str, int],
77
+ providers: Optional[List[str]] = None,
78
+ return_url: Optional[str] = None,
79
+ custom_labels: Optional[Dict[str, str]] = None,
80
+ ) -> Any:
81
+ """
82
+ Generate Telegram inline keyboard with payment buttons.
83
+ Returns aiogram `InlineKeyboardMarkup` if aiogram is installed,
84
+ otherwise returns a dict representation suitable for any bot framework.
85
+ """
86
+ links = get_payment_links(amount=amount, order_id=order_id, providers=providers, return_url=return_url)
87
+ amount_str = format_amount(amount)
88
+
89
+ default_labels = {
90
+ "payme": f"💳 Payme ({amount_str} so'm)",
91
+ "click": f"🔹 Click ({amount_str} so'm)",
92
+ "uzum": f"🍇 Uzum Bank ({amount_str} so'm)",
93
+ }
94
+ labels = {**default_labels, **(custom_labels or {})}
95
+
96
+ if AIOGRAM_AVAILABLE:
97
+ keyboard = []
98
+ for prov, url in links.items():
99
+ btn = InlineKeyboardButton(text=labels.get(prov, prov.title()), url=url)
100
+ keyboard.append([btn])
101
+ return InlineKeyboardMarkup(inline_keyboard=keyboard)
102
+
103
+ # Fallback to standard dict format
104
+ raw_keyboard = []
105
+ for prov, url in links.items():
106
+ raw_keyboard.append([{"text": labels.get(prov, prov.title()), "url": url}])
107
+ return {"inline_keyboard": raw_keyboard}
@@ -0,0 +1,16 @@
1
+ """Providers package for pay-engine."""
2
+
3
+ from .base import BaseProvider
4
+ from .click import ClickClient, ClickHandler
5
+ from .payme import PaymeClient, PaymeHandler
6
+ from .uzum import UzumClient, UzumHandler
7
+
8
+ __all__ = [
9
+ "BaseProvider",
10
+ "PaymeClient",
11
+ "PaymeHandler",
12
+ "ClickClient",
13
+ "ClickHandler",
14
+ "UzumClient",
15
+ "UzumHandler",
16
+ ]
@@ -0,0 +1,40 @@
1
+ """Abstract Base Provider for pay-engine."""
2
+
3
+ from abc import ABC, abstractmethod
4
+ from typing import Any, Dict, Optional, Union
5
+
6
+ from ..core.models import CardChargeResult, WebhookResult
7
+
8
+
9
+ class BaseProvider(ABC):
10
+ """Base class for all payment providers."""
11
+
12
+ @abstractmethod
13
+ def create_link(
14
+ self,
15
+ amount: float,
16
+ order_id: Union[str, int],
17
+ return_url: Optional[str] = None,
18
+ **kwargs: Any,
19
+ ) -> str:
20
+ """Generate a direct payment checkout URL for customer."""
21
+ pass
22
+
23
+ @abstractmethod
24
+ def verify_webhook(
25
+ self,
26
+ headers: Dict[str, str],
27
+ body: Union[Dict[str, Any], bytes, str],
28
+ ) -> WebhookResult:
29
+ """Verify incoming webhook request and return parsed result."""
30
+ pass
31
+
32
+ def charge_card(
33
+ self,
34
+ card_token: str,
35
+ amount: float,
36
+ order_id: Union[str, int],
37
+ **kwargs: Any,
38
+ ) -> CardChargeResult:
39
+ """Execute automated payment from saved card token. Override in providers supporting it."""
40
+ raise NotImplementedError(f"{self.__class__.__name__} does not support direct card charging.")
@@ -0,0 +1,6 @@
1
+ """Click provider package."""
2
+
3
+ from .client import ClickClient
4
+ from .handler import ClickHandler
5
+
6
+ __all__ = ["ClickClient", "ClickHandler"]