python-getpaid-core 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.
- getpaid_core/__init__.py +39 -0
- getpaid_core/backends/__init__.py +1 -0
- getpaid_core/backends/dummy.py +95 -0
- getpaid_core/enums.py +44 -0
- getpaid_core/exceptions.py +37 -0
- getpaid_core/flow.py +140 -0
- getpaid_core/fsm.py +205 -0
- getpaid_core/processor.py +87 -0
- getpaid_core/protocols.py +57 -0
- getpaid_core/py.typed +0 -0
- getpaid_core/registry.py +74 -0
- getpaid_core/types.py +52 -0
- getpaid_core/validators.py +21 -0
- python_getpaid_core-0.1.0.dist-info/METADATA +103 -0
- python_getpaid_core-0.1.0.dist-info/RECORD +17 -0
- python_getpaid_core-0.1.0.dist-info/WHEEL +4 -0
- python_getpaid_core-0.1.0.dist-info/licenses/LICENSE +21 -0
getpaid_core/__init__.py
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
"""Getpaid Core -- framework-agnostic payment processing."""
|
|
2
|
+
|
|
3
|
+
__version__ = "0.1.0"
|
|
4
|
+
|
|
5
|
+
from getpaid_core.enums import BackendMethod
|
|
6
|
+
from getpaid_core.enums import ConfirmationMethod
|
|
7
|
+
from getpaid_core.enums import FraudStatus
|
|
8
|
+
from getpaid_core.enums import PaymentStatus
|
|
9
|
+
from getpaid_core.exceptions import ChargeFailure
|
|
10
|
+
from getpaid_core.exceptions import CommunicationError
|
|
11
|
+
from getpaid_core.exceptions import CredentialsError
|
|
12
|
+
from getpaid_core.exceptions import GetPaidException
|
|
13
|
+
from getpaid_core.exceptions import InvalidCallbackError
|
|
14
|
+
from getpaid_core.exceptions import InvalidTransitionError
|
|
15
|
+
from getpaid_core.exceptions import LockFailure
|
|
16
|
+
from getpaid_core.exceptions import RefundFailure
|
|
17
|
+
from getpaid_core.flow import PaymentFlow
|
|
18
|
+
from getpaid_core.processor import BaseProcessor
|
|
19
|
+
from getpaid_core.registry import registry
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
__all__ = [
|
|
23
|
+
"BackendMethod",
|
|
24
|
+
"BaseProcessor",
|
|
25
|
+
"ChargeFailure",
|
|
26
|
+
"CommunicationError",
|
|
27
|
+
"ConfirmationMethod",
|
|
28
|
+
"CredentialsError",
|
|
29
|
+
"FraudStatus",
|
|
30
|
+
"GetPaidException",
|
|
31
|
+
"InvalidCallbackError",
|
|
32
|
+
"InvalidTransitionError",
|
|
33
|
+
"LockFailure",
|
|
34
|
+
"PaymentFlow",
|
|
35
|
+
"PaymentStatus",
|
|
36
|
+
"RefundFailure",
|
|
37
|
+
"__version__",
|
|
38
|
+
"registry",
|
|
39
|
+
]
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Built-in payment backends."""
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
"""Dummy payment backend for development and testing.
|
|
2
|
+
|
|
3
|
+
Makes zero HTTP calls. Serves as a reference implementation
|
|
4
|
+
for backend authors.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from decimal import Decimal
|
|
8
|
+
from typing import ClassVar
|
|
9
|
+
|
|
10
|
+
from getpaid_core.fsm import ALLOWED_CALLBACKS
|
|
11
|
+
from getpaid_core.processor import BaseProcessor
|
|
12
|
+
from getpaid_core.types import ChargeResponse
|
|
13
|
+
from getpaid_core.types import PaymentStatusResponse
|
|
14
|
+
from getpaid_core.types import TransactionResult
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class DummyProcessor(BaseProcessor):
|
|
18
|
+
"""Dummy processor that simulates all payment operations."""
|
|
19
|
+
|
|
20
|
+
slug: ClassVar[str] = "dummy"
|
|
21
|
+
display_name: ClassVar[str] = "Dummy"
|
|
22
|
+
accepted_currencies: ClassVar[list[str]] = [
|
|
23
|
+
"PLN",
|
|
24
|
+
"EUR",
|
|
25
|
+
"USD",
|
|
26
|
+
"GBP",
|
|
27
|
+
"CHF",
|
|
28
|
+
"CZK",
|
|
29
|
+
]
|
|
30
|
+
|
|
31
|
+
async def prepare_transaction(self, **kwargs) -> TransactionResult:
|
|
32
|
+
method = self.get_setting("method", "REST")
|
|
33
|
+
if method == "POST":
|
|
34
|
+
return TransactionResult(
|
|
35
|
+
redirect_url="https://dummy.example.com/form",
|
|
36
|
+
form_data={
|
|
37
|
+
"payment_id": self.payment.id,
|
|
38
|
+
"amount": str(self.payment.amount_required),
|
|
39
|
+
"currency": self.payment.currency,
|
|
40
|
+
},
|
|
41
|
+
method="POST",
|
|
42
|
+
headers={},
|
|
43
|
+
)
|
|
44
|
+
elif method == "GET":
|
|
45
|
+
return TransactionResult(
|
|
46
|
+
redirect_url=(
|
|
47
|
+
f"https://dummy.example.com/pay/{self.payment.id}"
|
|
48
|
+
),
|
|
49
|
+
form_data=None,
|
|
50
|
+
method="GET",
|
|
51
|
+
headers={},
|
|
52
|
+
)
|
|
53
|
+
else:
|
|
54
|
+
return TransactionResult(
|
|
55
|
+
redirect_url=(
|
|
56
|
+
f"https://dummy.example.com/pay/{self.payment.id}"
|
|
57
|
+
),
|
|
58
|
+
form_data=None,
|
|
59
|
+
method="REST",
|
|
60
|
+
headers={},
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
async def handle_callback(
|
|
64
|
+
self, data: dict, headers: dict, **kwargs
|
|
65
|
+
) -> None:
|
|
66
|
+
new_status = data.get("new_status")
|
|
67
|
+
if new_status and new_status in ALLOWED_CALLBACKS:
|
|
68
|
+
trigger = getattr(self.payment, new_status, None)
|
|
69
|
+
if trigger and callable(trigger):
|
|
70
|
+
trigger()
|
|
71
|
+
|
|
72
|
+
async def fetch_payment_status(self, **kwargs) -> PaymentStatusResponse:
|
|
73
|
+
status = self.get_setting("confirmation_status", "confirm_payment")
|
|
74
|
+
return PaymentStatusResponse(status=status)
|
|
75
|
+
|
|
76
|
+
async def charge(
|
|
77
|
+
self, amount: Decimal | None = None, **kwargs
|
|
78
|
+
) -> ChargeResponse:
|
|
79
|
+
charged = amount if amount is not None else self.payment.amount_required
|
|
80
|
+
return ChargeResponse(
|
|
81
|
+
amount_charged=charged,
|
|
82
|
+
success=True,
|
|
83
|
+
async_call=False,
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
async def release_lock(self, **kwargs) -> Decimal:
|
|
87
|
+
return self.payment.amount_locked
|
|
88
|
+
|
|
89
|
+
async def start_refund(
|
|
90
|
+
self, amount: Decimal | None = None, **kwargs
|
|
91
|
+
) -> Decimal:
|
|
92
|
+
return amount if amount is not None else self.payment.amount_paid
|
|
93
|
+
|
|
94
|
+
async def cancel_refund(self, **kwargs) -> bool:
|
|
95
|
+
return True
|
getpaid_core/enums.py
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
"""Payment processing enums.
|
|
2
|
+
|
|
3
|
+
Values are kept identical to django-getpaid for backward compatibility.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from enum import StrEnum
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class PaymentStatus(StrEnum):
|
|
10
|
+
"""Internal payment status."""
|
|
11
|
+
|
|
12
|
+
NEW = "new"
|
|
13
|
+
PREPARED = "prepared"
|
|
14
|
+
PRE_AUTH = "pre-auth"
|
|
15
|
+
IN_CHARGE = "charge_started"
|
|
16
|
+
PARTIAL = "partially_paid"
|
|
17
|
+
PAID = "paid"
|
|
18
|
+
FAILED = "failed"
|
|
19
|
+
REFUND_STARTED = "refund_started"
|
|
20
|
+
REFUNDED = "refunded"
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class FraudStatus(StrEnum):
|
|
24
|
+
"""Fraud verification status."""
|
|
25
|
+
|
|
26
|
+
UNKNOWN = "unknown"
|
|
27
|
+
ACCEPTED = "accepted"
|
|
28
|
+
REJECTED = "rejected"
|
|
29
|
+
CHECK = "check"
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class BackendMethod(StrEnum):
|
|
33
|
+
"""HTTP method used to initiate payment."""
|
|
34
|
+
|
|
35
|
+
GET = "GET"
|
|
36
|
+
POST = "POST"
|
|
37
|
+
REST = "REST"
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class ConfirmationMethod(StrEnum):
|
|
41
|
+
"""How the payment gateway confirms payment status."""
|
|
42
|
+
|
|
43
|
+
PUSH = "PUSH"
|
|
44
|
+
PULL = "PULL"
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
"""Exception hierarchy for payment processing."""
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class GetPaidException(Exception):
|
|
5
|
+
"""Base exception for all getpaid errors."""
|
|
6
|
+
|
|
7
|
+
def __init__(self, message: str = "", context: dict | None = None) -> None:
|
|
8
|
+
super().__init__(message)
|
|
9
|
+
self.context = context or {}
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class CommunicationError(GetPaidException):
|
|
13
|
+
"""Error communicating with payment gateway."""
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class ChargeFailure(CommunicationError):
|
|
17
|
+
"""Failed to charge payment."""
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class LockFailure(CommunicationError):
|
|
21
|
+
"""Failed to lock (pre-authorize) payment."""
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class RefundFailure(CommunicationError):
|
|
25
|
+
"""Failed to process refund."""
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class CredentialsError(GetPaidException):
|
|
29
|
+
"""Invalid or missing gateway credentials."""
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class InvalidCallbackError(GetPaidException):
|
|
33
|
+
"""Callback verification failed."""
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class InvalidTransitionError(GetPaidException):
|
|
37
|
+
"""Attempted invalid state transition."""
|
getpaid_core/flow.py
ADDED
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
"""Payment flow orchestrator.
|
|
2
|
+
|
|
3
|
+
The main entry point for framework adapters. Orchestrates the
|
|
4
|
+
interaction between repository, processor, and state machine.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from getpaid_core.exceptions import InvalidTransitionError
|
|
8
|
+
from getpaid_core.fsm import ALLOWED_CALLBACKS
|
|
9
|
+
from getpaid_core.fsm import create_fraud_machine
|
|
10
|
+
from getpaid_core.fsm import create_payment_machine
|
|
11
|
+
from getpaid_core.protocols import Order
|
|
12
|
+
from getpaid_core.protocols import Payment
|
|
13
|
+
from getpaid_core.protocols import PaymentRepository
|
|
14
|
+
from getpaid_core.registry import registry
|
|
15
|
+
from getpaid_core.types import TransactionResult
|
|
16
|
+
from getpaid_core.validators import run_validators
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class PaymentFlow:
|
|
20
|
+
"""Core payment processing orchestrator.
|
|
21
|
+
|
|
22
|
+
Framework adapters create an instance with their repository
|
|
23
|
+
and backend configuration, then delegate to its methods.
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
def __init__(
|
|
27
|
+
self,
|
|
28
|
+
repository: PaymentRepository,
|
|
29
|
+
config: dict | None = None,
|
|
30
|
+
validators: list | None = None,
|
|
31
|
+
) -> None:
|
|
32
|
+
self.repository = repository
|
|
33
|
+
self.config = config or {}
|
|
34
|
+
self.validators = validators or []
|
|
35
|
+
|
|
36
|
+
async def create_payment(
|
|
37
|
+
self, order: Order, backend_slug: str, **kwargs
|
|
38
|
+
) -> Payment:
|
|
39
|
+
"""Create a new payment for an order."""
|
|
40
|
+
registry.get_by_slug(backend_slug)
|
|
41
|
+
payment = await self.repository.create(
|
|
42
|
+
order=order,
|
|
43
|
+
backend=backend_slug,
|
|
44
|
+
amount_required=order.get_total_amount(),
|
|
45
|
+
currency=order.get_currency(),
|
|
46
|
+
description=order.get_description(),
|
|
47
|
+
**kwargs,
|
|
48
|
+
)
|
|
49
|
+
return payment
|
|
50
|
+
|
|
51
|
+
async def prepare(self, payment: Payment, **kwargs) -> TransactionResult:
|
|
52
|
+
"""Prepare a payment for processing.
|
|
53
|
+
|
|
54
|
+
Runs validators, calls the backend's prepare_transaction(),
|
|
55
|
+
transitions to PREPARED, and persists.
|
|
56
|
+
"""
|
|
57
|
+
run_validators({"payment": payment}, validators=self.validators)
|
|
58
|
+
create_payment_machine(payment)
|
|
59
|
+
processor = self._get_processor(payment)
|
|
60
|
+
result = await processor.prepare_transaction(**kwargs)
|
|
61
|
+
payment.confirm_prepared()
|
|
62
|
+
await self.repository.save(payment)
|
|
63
|
+
return result
|
|
64
|
+
|
|
65
|
+
async def handle_callback(
|
|
66
|
+
self,
|
|
67
|
+
payment: Payment,
|
|
68
|
+
data: dict,
|
|
69
|
+
headers: dict,
|
|
70
|
+
**kwargs,
|
|
71
|
+
) -> None:
|
|
72
|
+
"""Handle an incoming PUSH callback from the gateway."""
|
|
73
|
+
processor = self._get_processor(payment)
|
|
74
|
+
await processor.verify_callback(data, headers, **kwargs)
|
|
75
|
+
create_payment_machine(payment)
|
|
76
|
+
create_fraud_machine(payment)
|
|
77
|
+
await processor.handle_callback(data, headers, **kwargs)
|
|
78
|
+
await self.repository.save(payment)
|
|
79
|
+
|
|
80
|
+
async def fetch_and_update_status(self, payment: Payment) -> Payment:
|
|
81
|
+
"""PULL flow: fetch status from gateway and update."""
|
|
82
|
+
processor = self._get_processor(payment)
|
|
83
|
+
create_payment_machine(payment)
|
|
84
|
+
create_fraud_machine(payment)
|
|
85
|
+
response = await processor.fetch_payment_status()
|
|
86
|
+
if response.get("status"):
|
|
87
|
+
callback = response["status"]
|
|
88
|
+
if callback not in ALLOWED_CALLBACKS:
|
|
89
|
+
raise InvalidTransitionError(
|
|
90
|
+
f"Callback {callback!r} not in ALLOWED_CALLBACKS"
|
|
91
|
+
)
|
|
92
|
+
trigger = getattr(payment, callback, None)
|
|
93
|
+
if trigger and callable(trigger):
|
|
94
|
+
trigger()
|
|
95
|
+
await self.repository.save(payment)
|
|
96
|
+
return payment
|
|
97
|
+
|
|
98
|
+
async def charge(self, payment: Payment, amount=None, **kwargs):
|
|
99
|
+
"""Charge a pre-authorized payment."""
|
|
100
|
+
processor = self._get_processor(payment)
|
|
101
|
+
create_payment_machine(payment)
|
|
102
|
+
result = await processor.charge(amount=amount, **kwargs)
|
|
103
|
+
if result["success"]:
|
|
104
|
+
payment.confirm_charge_sent()
|
|
105
|
+
await self.repository.save(payment)
|
|
106
|
+
return result
|
|
107
|
+
|
|
108
|
+
async def release_lock(self, payment: Payment, **kwargs):
|
|
109
|
+
"""Release a pre-authorized lock."""
|
|
110
|
+
processor = self._get_processor(payment)
|
|
111
|
+
create_payment_machine(payment)
|
|
112
|
+
amount = await processor.release_lock(**kwargs)
|
|
113
|
+
payment.release_lock()
|
|
114
|
+
await self.repository.save(payment)
|
|
115
|
+
return amount
|
|
116
|
+
|
|
117
|
+
async def start_refund(self, payment: Payment, amount=None, **kwargs):
|
|
118
|
+
"""Start a refund."""
|
|
119
|
+
processor = self._get_processor(payment)
|
|
120
|
+
create_payment_machine(payment)
|
|
121
|
+
refund_amount = await processor.start_refund(amount=amount, **kwargs)
|
|
122
|
+
payment.start_refund()
|
|
123
|
+
await self.repository.save(payment)
|
|
124
|
+
return refund_amount
|
|
125
|
+
|
|
126
|
+
async def cancel_refund(self, payment: Payment, **kwargs):
|
|
127
|
+
"""Cancel an in-progress refund."""
|
|
128
|
+
processor = self._get_processor(payment)
|
|
129
|
+
create_payment_machine(payment)
|
|
130
|
+
success = await processor.cancel_refund(**kwargs)
|
|
131
|
+
if success:
|
|
132
|
+
payment.cancel_refund()
|
|
133
|
+
await self.repository.save(payment)
|
|
134
|
+
return success
|
|
135
|
+
|
|
136
|
+
def _get_processor(self, payment: Payment):
|
|
137
|
+
"""Instantiate the processor for a payment."""
|
|
138
|
+
processor_class = registry.get_by_slug(payment.backend)
|
|
139
|
+
backend_config = self.config.get(payment.backend, {})
|
|
140
|
+
return processor_class(payment, config=backend_config)
|
getpaid_core/fsm.py
ADDED
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
"""Payment state machine using the transitions library.
|
|
2
|
+
|
|
3
|
+
Defines all valid payment and fraud status transitions.
|
|
4
|
+
The machine attaches trigger methods directly to payment objects.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from transitions import Machine
|
|
8
|
+
from transitions.core import MachineError
|
|
9
|
+
|
|
10
|
+
from getpaid_core.enums import FraudStatus
|
|
11
|
+
from getpaid_core.enums import PaymentStatus
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def _require_fully_paid(event_data):
|
|
15
|
+
"""Guard that raises MachineError when payment is not fully paid."""
|
|
16
|
+
model = event_data.model
|
|
17
|
+
if not model.is_fully_paid():
|
|
18
|
+
raise MachineError(
|
|
19
|
+
f"Transition '{event_data.event.name}' requires full payment."
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def _require_fully_refunded(event_data):
|
|
24
|
+
"""Guard that raises MachineError when payment is not fully refunded."""
|
|
25
|
+
model = event_data.model
|
|
26
|
+
if not model.is_fully_refunded():
|
|
27
|
+
raise MachineError(
|
|
28
|
+
f"Transition '{event_data.event.name}' requires full refund."
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _store_locked_amount(event_data):
|
|
33
|
+
"""After confirm_lock: store the locked amount on the payment."""
|
|
34
|
+
model = event_data.model
|
|
35
|
+
amount = event_data.kwargs.get("amount", None)
|
|
36
|
+
if amount is None:
|
|
37
|
+
amount = model.amount_required
|
|
38
|
+
model.amount_locked = amount
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def _accumulate_paid_amount(event_data):
|
|
42
|
+
"""After confirm_payment: accumulate paid amount on the payment."""
|
|
43
|
+
model = event_data.model
|
|
44
|
+
amount = event_data.kwargs.get("amount", None)
|
|
45
|
+
if amount is None:
|
|
46
|
+
if not model.amount_locked:
|
|
47
|
+
model.amount_locked = model.amount_required
|
|
48
|
+
amount = model.amount_locked
|
|
49
|
+
model.amount_paid += amount
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
PAYMENT_TRANSITIONS = [
|
|
53
|
+
{
|
|
54
|
+
"trigger": "confirm_prepared",
|
|
55
|
+
"source": PaymentStatus.NEW,
|
|
56
|
+
"dest": PaymentStatus.PREPARED,
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
"trigger": "confirm_lock",
|
|
60
|
+
"source": [PaymentStatus.NEW, PaymentStatus.PREPARED],
|
|
61
|
+
"dest": PaymentStatus.PRE_AUTH,
|
|
62
|
+
"after": _store_locked_amount,
|
|
63
|
+
},
|
|
64
|
+
{
|
|
65
|
+
"trigger": "confirm_charge_sent",
|
|
66
|
+
"source": PaymentStatus.PRE_AUTH,
|
|
67
|
+
"dest": PaymentStatus.IN_CHARGE,
|
|
68
|
+
},
|
|
69
|
+
{
|
|
70
|
+
"trigger": "confirm_payment",
|
|
71
|
+
"source": [
|
|
72
|
+
PaymentStatus.PRE_AUTH,
|
|
73
|
+
PaymentStatus.PREPARED,
|
|
74
|
+
PaymentStatus.IN_CHARGE,
|
|
75
|
+
PaymentStatus.PARTIAL,
|
|
76
|
+
],
|
|
77
|
+
"dest": PaymentStatus.PARTIAL,
|
|
78
|
+
"after": _accumulate_paid_amount,
|
|
79
|
+
},
|
|
80
|
+
{
|
|
81
|
+
"trigger": "mark_as_paid",
|
|
82
|
+
"source": PaymentStatus.PARTIAL,
|
|
83
|
+
"dest": PaymentStatus.PAID,
|
|
84
|
+
"before": _require_fully_paid,
|
|
85
|
+
},
|
|
86
|
+
{
|
|
87
|
+
"trigger": "release_lock",
|
|
88
|
+
"source": PaymentStatus.PRE_AUTH,
|
|
89
|
+
"dest": PaymentStatus.REFUNDED,
|
|
90
|
+
},
|
|
91
|
+
{
|
|
92
|
+
"trigger": "start_refund",
|
|
93
|
+
"source": [
|
|
94
|
+
PaymentStatus.PAID,
|
|
95
|
+
PaymentStatus.PARTIAL,
|
|
96
|
+
],
|
|
97
|
+
"dest": PaymentStatus.REFUND_STARTED,
|
|
98
|
+
},
|
|
99
|
+
{
|
|
100
|
+
"trigger": "cancel_refund",
|
|
101
|
+
"source": PaymentStatus.REFUND_STARTED,
|
|
102
|
+
"dest": PaymentStatus.PARTIAL,
|
|
103
|
+
},
|
|
104
|
+
{
|
|
105
|
+
"trigger": "confirm_refund",
|
|
106
|
+
"source": PaymentStatus.REFUND_STARTED,
|
|
107
|
+
"dest": PaymentStatus.PARTIAL,
|
|
108
|
+
},
|
|
109
|
+
{
|
|
110
|
+
"trigger": "mark_as_refunded",
|
|
111
|
+
"source": PaymentStatus.PARTIAL,
|
|
112
|
+
"dest": PaymentStatus.REFUNDED,
|
|
113
|
+
"before": _require_fully_refunded,
|
|
114
|
+
},
|
|
115
|
+
{
|
|
116
|
+
"trigger": "fail",
|
|
117
|
+
"source": [
|
|
118
|
+
PaymentStatus.NEW,
|
|
119
|
+
PaymentStatus.PRE_AUTH,
|
|
120
|
+
PaymentStatus.PREPARED,
|
|
121
|
+
],
|
|
122
|
+
"dest": PaymentStatus.FAILED,
|
|
123
|
+
},
|
|
124
|
+
]
|
|
125
|
+
|
|
126
|
+
FRAUD_TRANSITIONS = [
|
|
127
|
+
{
|
|
128
|
+
"trigger": "flag_as_fraud",
|
|
129
|
+
"source": FraudStatus.UNKNOWN,
|
|
130
|
+
"dest": FraudStatus.REJECTED,
|
|
131
|
+
},
|
|
132
|
+
{
|
|
133
|
+
"trigger": "flag_as_legit",
|
|
134
|
+
"source": FraudStatus.UNKNOWN,
|
|
135
|
+
"dest": FraudStatus.ACCEPTED,
|
|
136
|
+
},
|
|
137
|
+
{
|
|
138
|
+
"trigger": "flag_for_check",
|
|
139
|
+
"source": FraudStatus.UNKNOWN,
|
|
140
|
+
"dest": FraudStatus.CHECK,
|
|
141
|
+
},
|
|
142
|
+
{
|
|
143
|
+
"trigger": "mark_as_fraud",
|
|
144
|
+
"source": FraudStatus.CHECK,
|
|
145
|
+
"dest": FraudStatus.REJECTED,
|
|
146
|
+
},
|
|
147
|
+
{
|
|
148
|
+
"trigger": "mark_as_legit",
|
|
149
|
+
"source": FraudStatus.CHECK,
|
|
150
|
+
"dest": FraudStatus.ACCEPTED,
|
|
151
|
+
},
|
|
152
|
+
]
|
|
153
|
+
|
|
154
|
+
ALLOWED_CALLBACKS: frozenset[str] = frozenset(
|
|
155
|
+
{
|
|
156
|
+
"confirm_prepared",
|
|
157
|
+
"confirm_lock",
|
|
158
|
+
"confirm_charge_sent",
|
|
159
|
+
"confirm_payment",
|
|
160
|
+
"mark_as_paid",
|
|
161
|
+
"release_lock",
|
|
162
|
+
"start_refund",
|
|
163
|
+
"cancel_refund",
|
|
164
|
+
"confirm_refund",
|
|
165
|
+
"mark_as_refunded",
|
|
166
|
+
"fail",
|
|
167
|
+
}
|
|
168
|
+
)
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
def create_payment_machine(payment) -> Machine:
|
|
172
|
+
"""Attach payment FSM to a payment object.
|
|
173
|
+
|
|
174
|
+
The transitions library adds trigger methods directly to the
|
|
175
|
+
object (confirm_prepared, confirm_lock, fail, etc.).
|
|
176
|
+
"""
|
|
177
|
+
return Machine(
|
|
178
|
+
model=payment,
|
|
179
|
+
states=PaymentStatus,
|
|
180
|
+
transitions=PAYMENT_TRANSITIONS,
|
|
181
|
+
initial=payment.status or PaymentStatus.NEW,
|
|
182
|
+
model_attribute="status",
|
|
183
|
+
auto_transitions=False,
|
|
184
|
+
send_event=True,
|
|
185
|
+
)
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
def _store_fraud_message(event):
|
|
189
|
+
"""Before callback: store message kwarg on the payment object."""
|
|
190
|
+
message = event.kwargs.get("message", "")
|
|
191
|
+
event.model.fraud_message = message
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
def create_fraud_machine(payment) -> Machine:
|
|
195
|
+
"""Attach fraud status FSM to a payment object."""
|
|
196
|
+
return Machine(
|
|
197
|
+
model=payment,
|
|
198
|
+
states=FraudStatus,
|
|
199
|
+
transitions=FRAUD_TRANSITIONS,
|
|
200
|
+
initial=payment.fraud_status or FraudStatus.UNKNOWN,
|
|
201
|
+
model_attribute="fraud_status",
|
|
202
|
+
auto_transitions=False,
|
|
203
|
+
send_event=True,
|
|
204
|
+
before_state_change=[_store_fraud_message],
|
|
205
|
+
)
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
"""Base payment processor abstract class.
|
|
2
|
+
|
|
3
|
+
All payment backends subclass BaseProcessor and implement at minimum
|
|
4
|
+
prepare_transaction(). Other methods are optional depending on the
|
|
5
|
+
payment gateway's capabilities.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from abc import ABC
|
|
9
|
+
from abc import abstractmethod
|
|
10
|
+
from decimal import Decimal
|
|
11
|
+
from typing import ClassVar
|
|
12
|
+
|
|
13
|
+
from getpaid_core.protocols import Payment
|
|
14
|
+
from getpaid_core.types import ChargeResponse
|
|
15
|
+
from getpaid_core.types import PaymentStatusResponse
|
|
16
|
+
from getpaid_core.types import TransactionResult
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class BaseProcessor(ABC):
|
|
20
|
+
"""Base class for payment backend processors."""
|
|
21
|
+
|
|
22
|
+
slug: ClassVar[str] = ""
|
|
23
|
+
display_name: ClassVar[str] = ""
|
|
24
|
+
accepted_currencies: ClassVar[list[str]] = []
|
|
25
|
+
logo_url: ClassVar[str | None] = None
|
|
26
|
+
sandbox_url: ClassVar[str] = ""
|
|
27
|
+
production_url: ClassVar[str] = ""
|
|
28
|
+
|
|
29
|
+
def __init__(self, payment: Payment, config: dict | None = None) -> None:
|
|
30
|
+
self.payment = payment
|
|
31
|
+
self.config = config or {}
|
|
32
|
+
|
|
33
|
+
def get_setting(self, name: str, default=None):
|
|
34
|
+
"""Read a setting from backend config."""
|
|
35
|
+
return self.config.get(name, default)
|
|
36
|
+
|
|
37
|
+
def get_paywall_baseurl(self) -> str:
|
|
38
|
+
"""Return sandbox or production URL based on config."""
|
|
39
|
+
sandbox = self.get_setting("sandbox", True)
|
|
40
|
+
return self.sandbox_url if sandbox else self.production_url
|
|
41
|
+
|
|
42
|
+
@abstractmethod
|
|
43
|
+
async def prepare_transaction(self, **kwargs) -> TransactionResult:
|
|
44
|
+
"""Prepare data for initiating a payment.
|
|
45
|
+
|
|
46
|
+
Returns a TransactionResult that the framework adapter
|
|
47
|
+
converts into an HTTP response.
|
|
48
|
+
"""
|
|
49
|
+
...
|
|
50
|
+
|
|
51
|
+
async def verify_callback(
|
|
52
|
+
self, data: dict, headers: dict, **kwargs
|
|
53
|
+
) -> None:
|
|
54
|
+
"""Verify callback authenticity.
|
|
55
|
+
|
|
56
|
+
Raise GetPaidException to reject. Default: no-op.
|
|
57
|
+
"""
|
|
58
|
+
|
|
59
|
+
async def handle_callback(
|
|
60
|
+
self, data: dict, headers: dict, **kwargs
|
|
61
|
+
) -> None:
|
|
62
|
+
"""Handle async PUSH callback from payment gateway."""
|
|
63
|
+
raise NotImplementedError
|
|
64
|
+
|
|
65
|
+
async def fetch_payment_status(self, **kwargs) -> PaymentStatusResponse:
|
|
66
|
+
"""PULL flow: fetch payment status from gateway."""
|
|
67
|
+
raise NotImplementedError
|
|
68
|
+
|
|
69
|
+
async def charge(
|
|
70
|
+
self, amount: Decimal | None = None, **kwargs
|
|
71
|
+
) -> ChargeResponse:
|
|
72
|
+
"""Charge a pre-authorized payment."""
|
|
73
|
+
raise NotImplementedError
|
|
74
|
+
|
|
75
|
+
async def release_lock(self, **kwargs) -> Decimal:
|
|
76
|
+
"""Release pre-authorized lock. Return locked amount."""
|
|
77
|
+
raise NotImplementedError
|
|
78
|
+
|
|
79
|
+
async def start_refund(
|
|
80
|
+
self, amount: Decimal | None = None, **kwargs
|
|
81
|
+
) -> Decimal:
|
|
82
|
+
"""Start a refund. Return refund amount."""
|
|
83
|
+
raise NotImplementedError
|
|
84
|
+
|
|
85
|
+
async def cancel_refund(self, **kwargs) -> bool:
|
|
86
|
+
"""Cancel in-progress refund. Return True if ok."""
|
|
87
|
+
raise NotImplementedError
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
"""Protocols defining framework integration contracts.
|
|
2
|
+
|
|
3
|
+
Framework adapters (django-getpaid, litestar-getpaid, etc.) provide
|
|
4
|
+
concrete implementations. Any object with the right shape satisfies
|
|
5
|
+
the protocol -- no inheritance required.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from decimal import Decimal
|
|
9
|
+
from typing import Protocol
|
|
10
|
+
from typing import runtime_checkable
|
|
11
|
+
|
|
12
|
+
from getpaid_core.types import BuyerInfo
|
|
13
|
+
from getpaid_core.types import ItemInfo
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@runtime_checkable
|
|
17
|
+
class Order(Protocol):
|
|
18
|
+
"""What the core expects from an order object."""
|
|
19
|
+
|
|
20
|
+
def get_total_amount(self) -> Decimal: ...
|
|
21
|
+
def get_buyer_info(self) -> BuyerInfo: ...
|
|
22
|
+
def get_description(self) -> str: ...
|
|
23
|
+
def get_currency(self) -> str: ...
|
|
24
|
+
def get_items(self) -> list[ItemInfo]: ...
|
|
25
|
+
def get_return_url(self, success: bool | None = None) -> str: ...
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@runtime_checkable
|
|
29
|
+
class Payment(Protocol):
|
|
30
|
+
"""What the core expects from a payment object."""
|
|
31
|
+
|
|
32
|
+
id: str
|
|
33
|
+
order: Order
|
|
34
|
+
amount_required: Decimal
|
|
35
|
+
currency: str
|
|
36
|
+
status: str
|
|
37
|
+
backend: str
|
|
38
|
+
external_id: str
|
|
39
|
+
description: str
|
|
40
|
+
amount_paid: Decimal
|
|
41
|
+
amount_locked: Decimal
|
|
42
|
+
amount_refunded: Decimal
|
|
43
|
+
fraud_status: str
|
|
44
|
+
fraud_message: str
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
@runtime_checkable
|
|
48
|
+
class PaymentRepository(Protocol):
|
|
49
|
+
"""Persistence abstraction. Framework adapters implement this."""
|
|
50
|
+
|
|
51
|
+
async def get_by_id(self, payment_id: str) -> Payment: ...
|
|
52
|
+
async def create(self, **kwargs) -> Payment: ...
|
|
53
|
+
async def save(self, payment: Payment) -> Payment: ...
|
|
54
|
+
async def update_status(
|
|
55
|
+
self, payment_id: str, status: str, **fields
|
|
56
|
+
) -> Payment: ...
|
|
57
|
+
async def list_by_order(self, order_id: str) -> list[Payment]: ...
|
getpaid_core/py.typed
ADDED
|
File without changes
|
getpaid_core/registry.py
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
"""Plugin registry for payment backends.
|
|
2
|
+
|
|
3
|
+
Primary discovery via entry_points. Manual registration for
|
|
4
|
+
testing and dynamic scenarios.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from importlib.metadata import entry_points
|
|
8
|
+
|
|
9
|
+
from getpaid_core.processor import BaseProcessor
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
ENTRY_POINT_GROUP = "getpaid.backends"
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class PluginRegistry:
|
|
16
|
+
"""Discovers and stores payment backend processors."""
|
|
17
|
+
|
|
18
|
+
def __init__(self) -> None:
|
|
19
|
+
self._backends: dict[str, type[BaseProcessor]] = {}
|
|
20
|
+
self._discovered = False
|
|
21
|
+
|
|
22
|
+
def discover(self) -> None:
|
|
23
|
+
"""Load all backends registered via entry_points."""
|
|
24
|
+
eps = entry_points(group=ENTRY_POINT_GROUP)
|
|
25
|
+
for ep in eps:
|
|
26
|
+
processor_class = ep.load()
|
|
27
|
+
if isinstance(processor_class, type) and issubclass(
|
|
28
|
+
processor_class, BaseProcessor
|
|
29
|
+
):
|
|
30
|
+
self._backends[processor_class.slug] = processor_class
|
|
31
|
+
self._discovered = True
|
|
32
|
+
|
|
33
|
+
def register(self, processor_class: type[BaseProcessor]) -> None:
|
|
34
|
+
"""Manual registration for testing or dynamic use."""
|
|
35
|
+
self._backends[processor_class.slug] = processor_class
|
|
36
|
+
|
|
37
|
+
def unregister(self, slug: str) -> None:
|
|
38
|
+
"""Remove a backend by slug."""
|
|
39
|
+
self._backends.pop(slug, None)
|
|
40
|
+
|
|
41
|
+
def get_for_currency(self, currency: str) -> list[type[BaseProcessor]]:
|
|
42
|
+
"""Return all backends supporting the given currency."""
|
|
43
|
+
self._ensure_discovered()
|
|
44
|
+
return [
|
|
45
|
+
b
|
|
46
|
+
for b in self._backends.values()
|
|
47
|
+
if currency in b.accepted_currencies
|
|
48
|
+
]
|
|
49
|
+
|
|
50
|
+
def get_choices(self, currency: str) -> list[tuple[str, str]]:
|
|
51
|
+
"""Return (slug, display_name) pairs for a currency."""
|
|
52
|
+
return [
|
|
53
|
+
(b.slug, b.display_name) for b in self.get_for_currency(currency)
|
|
54
|
+
]
|
|
55
|
+
|
|
56
|
+
def get_by_slug(self, slug: str) -> type[BaseProcessor]:
|
|
57
|
+
"""Return a backend class by slug. Raises KeyError."""
|
|
58
|
+
self._ensure_discovered()
|
|
59
|
+
return self._backends[slug]
|
|
60
|
+
|
|
61
|
+
def get_all_currencies(self) -> set[str]:
|
|
62
|
+
"""Return all currencies supported by all backends."""
|
|
63
|
+
self._ensure_discovered()
|
|
64
|
+
currencies: set[str] = set()
|
|
65
|
+
for b in self._backends.values():
|
|
66
|
+
currencies.update(b.accepted_currencies)
|
|
67
|
+
return currencies
|
|
68
|
+
|
|
69
|
+
def _ensure_discovered(self) -> None:
|
|
70
|
+
if not self._discovered:
|
|
71
|
+
self.discover()
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
registry = PluginRegistry()
|
getpaid_core/types.py
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
"""Core type definitions for payment processing."""
|
|
2
|
+
|
|
3
|
+
from decimal import Decimal
|
|
4
|
+
from typing import TypedDict
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class BuyerInfo(TypedDict, total=False):
|
|
8
|
+
"""Buyer/customer information."""
|
|
9
|
+
|
|
10
|
+
email: str
|
|
11
|
+
first_name: str
|
|
12
|
+
last_name: str
|
|
13
|
+
phone: str
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class ItemInfo(TypedDict):
|
|
17
|
+
"""Single item in an order."""
|
|
18
|
+
|
|
19
|
+
name: str
|
|
20
|
+
quantity: int
|
|
21
|
+
unit_price: Decimal
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class ChargeResponse(TypedDict):
|
|
25
|
+
"""Response from charging a pre-authorized payment."""
|
|
26
|
+
|
|
27
|
+
amount_charged: Decimal
|
|
28
|
+
success: bool
|
|
29
|
+
async_call: bool
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class PaymentStatusResponse(TypedDict, total=False):
|
|
33
|
+
"""Response from fetching payment status from gateway."""
|
|
34
|
+
|
|
35
|
+
amount: Decimal | None
|
|
36
|
+
status: str | None
|
|
37
|
+
external_id: str | None
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class TransactionResult(TypedDict):
|
|
41
|
+
"""Result of preparing a transaction.
|
|
42
|
+
|
|
43
|
+
Framework adapters convert this into framework-specific responses:
|
|
44
|
+
- GET: redirect to redirect_url
|
|
45
|
+
- POST: render form that auto-submits to redirect_url with form_data
|
|
46
|
+
- REST: return JSON or handle internally
|
|
47
|
+
"""
|
|
48
|
+
|
|
49
|
+
redirect_url: str | None
|
|
50
|
+
form_data: dict | None
|
|
51
|
+
method: str # 'GET', 'POST', or 'REST'
|
|
52
|
+
headers: dict[str, str]
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
"""Pluggable payment validation system.
|
|
2
|
+
|
|
3
|
+
Validators are callables that receive a data dict, optionally
|
|
4
|
+
modify it, and return it. They raise GetPaidException to reject.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from collections.abc import Callable
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def run_validators(
|
|
11
|
+
data: dict,
|
|
12
|
+
validators: list[Callable] | None = None,
|
|
13
|
+
) -> dict:
|
|
14
|
+
"""Run a chain of validators on payment data.
|
|
15
|
+
|
|
16
|
+
Each validator receives the data dict and must return it
|
|
17
|
+
(possibly modified). Raise GetPaidException to reject.
|
|
18
|
+
"""
|
|
19
|
+
for validator in validators or []:
|
|
20
|
+
data = validator(data)
|
|
21
|
+
return data
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: python-getpaid-core
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Framework-agnostic payment processing core.
|
|
5
|
+
Project-URL: Homepage, https://github.com/django-getpaid/python-getpaid-core
|
|
6
|
+
Project-URL: Repository, https://github.com/django-getpaid/python-getpaid-core
|
|
7
|
+
Project-URL: Documentation, https://getpaid-core.readthedocs.io
|
|
8
|
+
Project-URL: Changelog, https://github.com/django-getpaid/python-getpaid-core/releases
|
|
9
|
+
Author-email: Dominik Kozaczko <dominik@kozaczko.info>
|
|
10
|
+
License: MIT
|
|
11
|
+
License-File: LICENSE
|
|
12
|
+
Classifier: Development Status :: 3 - Alpha
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
19
|
+
Classifier: Topic :: Office/Business :: Financial
|
|
20
|
+
Classifier: Topic :: Office/Business :: Financial :: Point-Of-Sale
|
|
21
|
+
Classifier: Typing :: Typed
|
|
22
|
+
Requires-Python: >=3.10
|
|
23
|
+
Requires-Dist: anyio>=4.0
|
|
24
|
+
Requires-Dist: httpx>=0.27.0
|
|
25
|
+
Requires-Dist: transitions>=0.9.0
|
|
26
|
+
Description-Content-Type: text/markdown
|
|
27
|
+
|
|
28
|
+
# getpaid-core
|
|
29
|
+
|
|
30
|
+
[](https://pypi.org/project/python-getpaid-core/)
|
|
31
|
+
[](https://pypi.org/project/python-getpaid-core/)
|
|
32
|
+
[](https://github.com/django-getpaid/python-getpaid-core/blob/main/LICENSE)
|
|
33
|
+
|
|
34
|
+
Framework-agnostic payment processing library for Python. Provides the core
|
|
35
|
+
abstractions — enums, protocols, FSM, processor base class, plugin registry,
|
|
36
|
+
and exception hierarchy — that framework-specific adapters build on.
|
|
37
|
+
|
|
38
|
+
## Architecture
|
|
39
|
+
|
|
40
|
+
getpaid-core defines the **what** of payment processing without coupling to
|
|
41
|
+
any web framework:
|
|
42
|
+
|
|
43
|
+
- **Enums** (`PaymentStatus`, `FraudStatus`, `BackendMethod`, `ConfirmationMethod`)
|
|
44
|
+
define all valid states and methods.
|
|
45
|
+
- **Protocols** (`Payment`, `Order`, `PaymentRepository`) define structural
|
|
46
|
+
contracts that framework models must satisfy.
|
|
47
|
+
- **FSM** (`create_payment_machine`, `create_fraud_machine`) attaches
|
|
48
|
+
state-machine triggers to payment objects at runtime using the `transitions`
|
|
49
|
+
library.
|
|
50
|
+
- **BaseProcessor** is an abstract class that payment gateway plugins subclass
|
|
51
|
+
to implement `prepare_transaction`, `handle_callback`, `charge`, etc.
|
|
52
|
+
- **PluginRegistry** discovers and stores payment backend processors via
|
|
53
|
+
entry points or manual registration.
|
|
54
|
+
- **Exceptions** provide a structured hierarchy for payment errors.
|
|
55
|
+
|
|
56
|
+
## Framework Adapters
|
|
57
|
+
|
|
58
|
+
- **[django-getpaid](https://github.com/django-getpaid/django-getpaid)** —
|
|
59
|
+
Django adapter (models, views, forms, admin)
|
|
60
|
+
|
|
61
|
+
## Installation
|
|
62
|
+
|
|
63
|
+
```bash
|
|
64
|
+
pip install python-getpaid-core
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
You typically install this as a dependency of a framework adapter rather than
|
|
68
|
+
directly.
|
|
69
|
+
|
|
70
|
+
## Quick Example
|
|
71
|
+
|
|
72
|
+
```python
|
|
73
|
+
from getpaid_core.enums import PaymentStatus
|
|
74
|
+
from getpaid_core.fsm import create_payment_machine
|
|
75
|
+
|
|
76
|
+
# Any object satisfying the Payment protocol works
|
|
77
|
+
payment = MyPayment(status=PaymentStatus.NEW, amount_required=100)
|
|
78
|
+
machine = create_payment_machine(payment)
|
|
79
|
+
|
|
80
|
+
# FSM trigger methods are attached directly to the object
|
|
81
|
+
payment.confirm_prepared()
|
|
82
|
+
assert payment.status == PaymentStatus.PREPARED
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
## Requirements
|
|
86
|
+
|
|
87
|
+
- Python 3.12+
|
|
88
|
+
- transitions
|
|
89
|
+
- httpx
|
|
90
|
+
- anyio
|
|
91
|
+
|
|
92
|
+
## License
|
|
93
|
+
|
|
94
|
+
MIT
|
|
95
|
+
|
|
96
|
+
## Disclaimer
|
|
97
|
+
|
|
98
|
+
This project has nothing in common with the
|
|
99
|
+
[getpaid](http://code.google.com/p/getpaid/) plone project.
|
|
100
|
+
|
|
101
|
+
## Credits
|
|
102
|
+
|
|
103
|
+
Created by [Dominik Kozaczko](https://github.com/dekoza).
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
getpaid_core/__init__.py,sha256=soSScP8r9yiOz7kv3dO4uS3x6AX8TnIxul_RzkULUSg,1198
|
|
2
|
+
getpaid_core/enums.py,sha256=arfIwqyHDBRXLb_DbLVQMJvMALG5dkN26KGR1w6G5Wg,866
|
|
3
|
+
getpaid_core/exceptions.py,sha256=XJtXvUdH_mNTP66okUvfvJfY13UURVW0_WPXapbjG9c,913
|
|
4
|
+
getpaid_core/flow.py,sha256=IBgMGArjWxUGjW9mZAIA0pl1IqV_EPkLfy3JxluHiTQ,5312
|
|
5
|
+
getpaid_core/fsm.py,sha256=nxAECNyVthxopNjwenbkbMA7wa7bUnS9o_0od3r0gGU,5637
|
|
6
|
+
getpaid_core/processor.py,sha256=5p1AFObhaF9EO5GWt9KNti9Oxq9whMr68-vSsQf8v6A,2870
|
|
7
|
+
getpaid_core/protocols.py,sha256=CnvgEx5P8u3kYuSDKo_XuQm8hG2ze0Us31KZDn5V8G8,1660
|
|
8
|
+
getpaid_core/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
9
|
+
getpaid_core/registry.py,sha256=1VMncWbZuK3blrzxLygwWi2YSP67WOYs0X5CAa2r4Pg,2421
|
|
10
|
+
getpaid_core/types.py,sha256=Ougk4LBCvlVPkFWJfp0E3rE1hhv7DPKDFnuulfg3shE,1183
|
|
11
|
+
getpaid_core/validators.py,sha256=TLOvMUG51spEVhm4uvSjismpzOesOjljLmd6VWghuWU,570
|
|
12
|
+
getpaid_core/backends/__init__.py,sha256=7xPzfO9dlCcB8I6ZGW9dk24xYxvSrQMe7WTWdu5Phg0,33
|
|
13
|
+
getpaid_core/backends/dummy.py,sha256=-A4yCP3yZM_ctijoZYaM01MmaRgE_itAReVMZnMqxPQ,3088
|
|
14
|
+
python_getpaid_core-0.1.0.dist-info/METADATA,sha256=4nyRd1AjfDgpjEz0QRqrRjiSJx7eSSkhWEqrwfDDOyY,3621
|
|
15
|
+
python_getpaid_core-0.1.0.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
|
|
16
|
+
python_getpaid_core-0.1.0.dist-info/licenses/LICENSE,sha256=Nds7k4ZmahVulUUqzk6BZ3Lib61U75QHrBzWXqAhjZQ,1072
|
|
17
|
+
python_getpaid_core-0.1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright © 2022 Dominik Kozaczko
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|