python-getpaid-core 0.1.1__tar.gz → 3.0.0a3__tar.gz
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.
- python_getpaid_core-3.0.0a3/.github/workflows/ci.yml +33 -0
- {python_getpaid_core-0.1.1 → python_getpaid_core-3.0.0a3}/.gitignore +1 -0
- python_getpaid_core-3.0.0a3/.sisyphus/evidence/task-22-readme-core.txt +7 -0
- python_getpaid_core-3.0.0a3/PKG-INFO +107 -0
- python_getpaid_core-3.0.0a3/README.md +82 -0
- {python_getpaid_core-0.1.1 → python_getpaid_core-3.0.0a3}/docs/changelog.md +5 -6
- {python_getpaid_core-0.1.1 → python_getpaid_core-3.0.0a3}/docs/concepts.md +28 -36
- {python_getpaid_core-0.1.1 → python_getpaid_core-3.0.0a3}/docs/getting-started.md +21 -12
- {python_getpaid_core-0.1.1 → python_getpaid_core-3.0.0a3}/docs/reference.md +1 -1
- {python_getpaid_core-0.1.1 → python_getpaid_core-3.0.0a3}/pyproject.toml +4 -1
- {python_getpaid_core-0.1.1 → python_getpaid_core-3.0.0a3}/src/getpaid_core/__init__.py +15 -1
- python_getpaid_core-3.0.0a3/src/getpaid_core/backends/dummy.py +145 -0
- {python_getpaid_core-0.1.1 → python_getpaid_core-3.0.0a3}/src/getpaid_core/enums.py +23 -4
- python_getpaid_core-3.0.0a3/src/getpaid_core/flow.py +203 -0
- python_getpaid_core-3.0.0a3/src/getpaid_core/fsm.py +261 -0
- {python_getpaid_core-0.1.1 → python_getpaid_core-3.0.0a3}/src/getpaid_core/processor.py +20 -28
- {python_getpaid_core-0.1.1 → python_getpaid_core-3.0.0a3}/src/getpaid_core/protocols.py +5 -8
- {python_getpaid_core-0.1.1 → python_getpaid_core-3.0.0a3}/src/getpaid_core/registry.py +11 -15
- python_getpaid_core-3.0.0a3/src/getpaid_core/types.py +102 -0
- python_getpaid_core-3.0.0a3/src/getpaid_core/validators.py +13 -0
- python_getpaid_core-3.0.0a3/tests/conftest.py +211 -0
- python_getpaid_core-3.0.0a3/tests/test_dummy_backend.py +132 -0
- python_getpaid_core-3.0.0a3/tests/test_enums.py +47 -0
- python_getpaid_core-3.0.0a3/tests/test_flow.py +220 -0
- python_getpaid_core-3.0.0a3/tests/test_integration.py +79 -0
- python_getpaid_core-3.0.0a3/tests/test_processor.py +129 -0
- python_getpaid_core-3.0.0a3/tests/test_protocols.py +89 -0
- python_getpaid_core-3.0.0a3/tests/test_public_api.py +25 -0
- python_getpaid_core-3.0.0a3/tests/test_registry.py +114 -0
- python_getpaid_core-3.0.0a3/tests/test_state_engine.py +122 -0
- python_getpaid_core-3.0.0a3/tests/test_types.py +84 -0
- python_getpaid_core-3.0.0a3/tests/test_version.py +11 -0
- python_getpaid_core-0.1.1/PKG-INFO +0 -105
- python_getpaid_core-0.1.1/README.md +0 -80
- python_getpaid_core-0.1.1/src/getpaid_core/backends/dummy.py +0 -95
- python_getpaid_core-0.1.1/src/getpaid_core/flow.py +0 -144
- python_getpaid_core-0.1.1/src/getpaid_core/fsm.py +0 -223
- python_getpaid_core-0.1.1/src/getpaid_core/types.py +0 -52
- python_getpaid_core-0.1.1/src/getpaid_core/validators.py +0 -21
- python_getpaid_core-0.1.1/tests/conftest.py +0 -132
- python_getpaid_core-0.1.1/tests/test_dummy_backend.py +0 -152
- python_getpaid_core-0.1.1/tests/test_enums.py +0 -87
- python_getpaid_core-0.1.1/tests/test_flow.py +0 -208
- python_getpaid_core-0.1.1/tests/test_fsm.py +0 -292
- python_getpaid_core-0.1.1/tests/test_integration.py +0 -182
- python_getpaid_core-0.1.1/tests/test_processor.py +0 -184
- python_getpaid_core-0.1.1/tests/test_protocols.py +0 -85
- python_getpaid_core-0.1.1/tests/test_public_api.py +0 -35
- python_getpaid_core-0.1.1/tests/test_registry.py +0 -190
- python_getpaid_core-0.1.1/tests/test_types.py +0 -97
- python_getpaid_core-0.1.1/uv.lock +0 -936
- {python_getpaid_core-0.1.1 → python_getpaid_core-3.0.0a3}/.cookiecutter.json +0 -0
- {python_getpaid_core-0.1.1 → python_getpaid_core-3.0.0a3}/.gitattributes +0 -0
- {python_getpaid_core-0.1.1 → python_getpaid_core-3.0.0a3}/.github/dependabot.yml +0 -0
- {python_getpaid_core-0.1.1 → python_getpaid_core-3.0.0a3}/.github/labels.yml +0 -0
- {python_getpaid_core-0.1.1 → python_getpaid_core-3.0.0a3}/.github/release-drafter.yml +0 -0
- {python_getpaid_core-0.1.1 → python_getpaid_core-3.0.0a3}/.github/workflows/constraints.txt +0 -0
- {python_getpaid_core-0.1.1 → python_getpaid_core-3.0.0a3}/.github/workflows/labeler.yml +0 -0
- {python_getpaid_core-0.1.1 → python_getpaid_core-3.0.0a3}/.github/workflows/release.yml +0 -0
- {python_getpaid_core-0.1.1 → python_getpaid_core-3.0.0a3}/.github/workflows/tests.yml +0 -0
- {python_getpaid_core-0.1.1 → python_getpaid_core-3.0.0a3}/.plans/2026-02-13-getpaid-core-design.md +0 -0
- {python_getpaid_core-0.1.1 → python_getpaid_core-3.0.0a3}/.plans/2026-02-13-getpaid-core-implementation.md +0 -0
- {python_getpaid_core-0.1.1 → python_getpaid_core-3.0.0a3}/.pre-commit-config.yaml +0 -0
- {python_getpaid_core-0.1.1 → python_getpaid_core-3.0.0a3}/.readthedocs.yml +0 -0
- {python_getpaid_core-0.1.1 → python_getpaid_core-3.0.0a3}/CODE_OF_CONDUCT.md +0 -0
- {python_getpaid_core-0.1.1 → python_getpaid_core-3.0.0a3}/CONTRIBUTING.md +0 -0
- {python_getpaid_core-0.1.1 → python_getpaid_core-3.0.0a3}/LICENSE +0 -0
- {python_getpaid_core-0.1.1 → python_getpaid_core-3.0.0a3}/codecov.yml +0 -0
- {python_getpaid_core-0.1.1 → python_getpaid_core-3.0.0a3}/docs/codeofconduct.md +0 -0
- {python_getpaid_core-0.1.1 → python_getpaid_core-3.0.0a3}/docs/conf.py +0 -0
- {python_getpaid_core-0.1.1 → python_getpaid_core-3.0.0a3}/docs/contributing.md +0 -0
- {python_getpaid_core-0.1.1 → python_getpaid_core-3.0.0a3}/docs/index.md +0 -0
- {python_getpaid_core-0.1.1 → python_getpaid_core-3.0.0a3}/docs/license.md +0 -0
- {python_getpaid_core-0.1.1 → python_getpaid_core-3.0.0a3}/docs/requirements.txt +0 -0
- {python_getpaid_core-0.1.1 → python_getpaid_core-3.0.0a3}/src/getpaid_core/backends/__init__.py +0 -0
- {python_getpaid_core-0.1.1 → python_getpaid_core-3.0.0a3}/src/getpaid_core/exceptions.py +0 -0
- {python_getpaid_core-0.1.1 → python_getpaid_core-3.0.0a3}/src/getpaid_core/py.typed +0 -0
- {python_getpaid_core-0.1.1 → python_getpaid_core-3.0.0a3}/tests/__init__.py +0 -0
- {python_getpaid_core-0.1.1 → python_getpaid_core-3.0.0a3}/tests/test_exceptions.py +0 -0
- {python_getpaid_core-0.1.1 → python_getpaid_core-3.0.0a3}/tests/test_validators.py +0 -0
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
name: CI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [main]
|
|
6
|
+
pull_request:
|
|
7
|
+
|
|
8
|
+
jobs:
|
|
9
|
+
test:
|
|
10
|
+
runs-on: ubuntu-latest
|
|
11
|
+
strategy:
|
|
12
|
+
matrix:
|
|
13
|
+
python-version: ["3.12", "3.13"]
|
|
14
|
+
|
|
15
|
+
steps:
|
|
16
|
+
- uses: actions/checkout@v4
|
|
17
|
+
|
|
18
|
+
- name: Set up Python ${{ matrix.python-version }}
|
|
19
|
+
uses: actions/setup-python@v5
|
|
20
|
+
with:
|
|
21
|
+
python-version: ${{ matrix.python-version }}
|
|
22
|
+
|
|
23
|
+
- name: Install uv
|
|
24
|
+
run: pip install uv
|
|
25
|
+
|
|
26
|
+
- name: Install dependencies
|
|
27
|
+
run: uv sync
|
|
28
|
+
|
|
29
|
+
- name: Lint with ruff
|
|
30
|
+
run: uv run ruff check .
|
|
31
|
+
|
|
32
|
+
- name: Run tests
|
|
33
|
+
run: uv run pytest --tb=short
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: python-getpaid-core
|
|
3
|
+
Version: 3.0.0a3
|
|
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.12
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
17
|
+
Classifier: Topic :: Office/Business :: Financial
|
|
18
|
+
Classifier: Topic :: Office/Business :: Financial :: Point-Of-Sale
|
|
19
|
+
Classifier: Typing :: Typed
|
|
20
|
+
Requires-Python: >=3.12
|
|
21
|
+
Requires-Dist: anyio>=4.0
|
|
22
|
+
Requires-Dist: httpx>=0.27.0
|
|
23
|
+
Requires-Dist: transitions>=0.9.0
|
|
24
|
+
Description-Content-Type: text/markdown
|
|
25
|
+
|
|
26
|
+
# python-getpaid-core
|
|
27
|
+
|
|
28
|
+
[](https://pypi.org/project/python-getpaid-core/)
|
|
29
|
+
[](https://pypi.org/project/python-getpaid-core/)
|
|
30
|
+
[](https://github.com/django-getpaid/python-getpaid-core/blob/main/LICENSE)
|
|
31
|
+
|
|
32
|
+
**Framework-agnostic payment processing core.**
|
|
33
|
+
|
|
34
|
+
`python-getpaid-core` is the foundation of the Getpaid ecosystem. It provides the abstract interfaces, semantic payment update engine, and plugin registry needed to build a robust payment system without coupling your logic to a specific web framework or payment provider.
|
|
35
|
+
|
|
36
|
+
## Installation
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
pip install python-getpaid-core
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
## Quick Start: Creating a Custom Processor
|
|
43
|
+
|
|
44
|
+
To implement a new payment backend, subclass `BaseProcessor` and implement at least `prepare_transaction`.
|
|
45
|
+
|
|
46
|
+
```python
|
|
47
|
+
from getpaid_core import BaseProcessor
|
|
48
|
+
from getpaid_core.types import TransactionResult
|
|
49
|
+
|
|
50
|
+
class MyPaymentProcessor(BaseProcessor):
|
|
51
|
+
slug = "my-provider"
|
|
52
|
+
display_name = "My Payment Provider"
|
|
53
|
+
accepted_currencies = ["USD", "EUR"]
|
|
54
|
+
|
|
55
|
+
async def prepare_transaction(self, **kwargs) -> TransactionResult:
|
|
56
|
+
# Generate payment link or form data
|
|
57
|
+
return TransactionResult(
|
|
58
|
+
redirect_url=f"https://api.provider.com/pay/{self.payment.id}",
|
|
59
|
+
method="GET"
|
|
60
|
+
)
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
### Registering your Processor
|
|
64
|
+
|
|
65
|
+
Register your processor using entry points in your `pyproject.toml` so it can be discovered by the registry:
|
|
66
|
+
|
|
67
|
+
```toml
|
|
68
|
+
[project.entry-points."getpaid.backends"]
|
|
69
|
+
my-provider = "my_package.processors:MyPaymentProcessor"
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
## Architecture Overview
|
|
73
|
+
|
|
74
|
+
- **BaseProcessor**: The abstract base class that all payment gateway plugins must implement. It provides the standard interface for transaction preparation, callback handling, charging, and refunds.
|
|
75
|
+
- **PaymentFlow**: Manages the payment lifecycle using semantic payment update objects. It ensures that payments move between states (e.g., `NEW` -> `PREPARED` -> `PAID`) according to strict business rules.
|
|
76
|
+
- **PluginRegistry**: A central service for discovering and managing payment processors registered via `getpaid.backends` entry points.
|
|
77
|
+
- **State Engine**: Applies semantic payment and fraud events to payment objects, merges provider metadata, and tracks idempotent provider event IDs.
|
|
78
|
+
|
|
79
|
+
## API Summary
|
|
80
|
+
|
|
81
|
+
| Class / Module | Role |
|
|
82
|
+
| --- | --- |
|
|
83
|
+
| `BaseProcessor` | Abstract base for implementing payment gateways. |
|
|
84
|
+
| `PaymentFlow` | Semantic orchestration for payment lifecycles. |
|
|
85
|
+
| `PaymentStatus` | Enum for all possible payment states (NEW, PAID, FAILED, etc.). |
|
|
86
|
+
| `registry` | Singleton registry for backend discovery. |
|
|
87
|
+
| `TransactionResult` | Standard response for transaction initiation. |
|
|
88
|
+
| `GetPaidException` | Base exception for all payment-related errors. |
|
|
89
|
+
|
|
90
|
+
## Ecosystem
|
|
91
|
+
|
|
92
|
+
`getpaid-core` is the heart of a larger ecosystem designed to make payment processing easy in any Python web application.
|
|
93
|
+
|
|
94
|
+
### Framework Wrappers
|
|
95
|
+
- [django-getpaid](https://github.com/django-getpaid/django-getpaid) — Official Django integration.
|
|
96
|
+
- [litestar-getpaid](https://github.com/django-getpaid/litestar-getpaid) — Official Litestar integration.
|
|
97
|
+
- [fastapi-getpaid](https://github.com/django-getpaid/fastapi-getpaid) — Official FastAPI integration.
|
|
98
|
+
|
|
99
|
+
### Processor Plugins
|
|
100
|
+
- [python-getpaid-payu](https://github.com/django-getpaid/python-getpaid-payu) — PayU backend.
|
|
101
|
+
- [python-getpaid-paynow](https://github.com/django-getpaid/python-getpaid-paynow) — Paynow backend.
|
|
102
|
+
- [python-getpaid-bitpay](https://github.com/django-getpaid/python-getpaid-bitpay) — BitPay backend.
|
|
103
|
+
- [python-getpaid-przelewy24](https://github.com/django-getpaid/python-getpaid-przelewy24) — Przelewy24 backend.
|
|
104
|
+
|
|
105
|
+
## License
|
|
106
|
+
|
|
107
|
+
This project is licensed under the MIT License.
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
# python-getpaid-core
|
|
2
|
+
|
|
3
|
+
[](https://pypi.org/project/python-getpaid-core/)
|
|
4
|
+
[](https://pypi.org/project/python-getpaid-core/)
|
|
5
|
+
[](https://github.com/django-getpaid/python-getpaid-core/blob/main/LICENSE)
|
|
6
|
+
|
|
7
|
+
**Framework-agnostic payment processing core.**
|
|
8
|
+
|
|
9
|
+
`python-getpaid-core` is the foundation of the Getpaid ecosystem. It provides the abstract interfaces, semantic payment update engine, and plugin registry needed to build a robust payment system without coupling your logic to a specific web framework or payment provider.
|
|
10
|
+
|
|
11
|
+
## Installation
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
pip install python-getpaid-core
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
## Quick Start: Creating a Custom Processor
|
|
18
|
+
|
|
19
|
+
To implement a new payment backend, subclass `BaseProcessor` and implement at least `prepare_transaction`.
|
|
20
|
+
|
|
21
|
+
```python
|
|
22
|
+
from getpaid_core import BaseProcessor
|
|
23
|
+
from getpaid_core.types import TransactionResult
|
|
24
|
+
|
|
25
|
+
class MyPaymentProcessor(BaseProcessor):
|
|
26
|
+
slug = "my-provider"
|
|
27
|
+
display_name = "My Payment Provider"
|
|
28
|
+
accepted_currencies = ["USD", "EUR"]
|
|
29
|
+
|
|
30
|
+
async def prepare_transaction(self, **kwargs) -> TransactionResult:
|
|
31
|
+
# Generate payment link or form data
|
|
32
|
+
return TransactionResult(
|
|
33
|
+
redirect_url=f"https://api.provider.com/pay/{self.payment.id}",
|
|
34
|
+
method="GET"
|
|
35
|
+
)
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
### Registering your Processor
|
|
39
|
+
|
|
40
|
+
Register your processor using entry points in your `pyproject.toml` so it can be discovered by the registry:
|
|
41
|
+
|
|
42
|
+
```toml
|
|
43
|
+
[project.entry-points."getpaid.backends"]
|
|
44
|
+
my-provider = "my_package.processors:MyPaymentProcessor"
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
## Architecture Overview
|
|
48
|
+
|
|
49
|
+
- **BaseProcessor**: The abstract base class that all payment gateway plugins must implement. It provides the standard interface for transaction preparation, callback handling, charging, and refunds.
|
|
50
|
+
- **PaymentFlow**: Manages the payment lifecycle using semantic payment update objects. It ensures that payments move between states (e.g., `NEW` -> `PREPARED` -> `PAID`) according to strict business rules.
|
|
51
|
+
- **PluginRegistry**: A central service for discovering and managing payment processors registered via `getpaid.backends` entry points.
|
|
52
|
+
- **State Engine**: Applies semantic payment and fraud events to payment objects, merges provider metadata, and tracks idempotent provider event IDs.
|
|
53
|
+
|
|
54
|
+
## API Summary
|
|
55
|
+
|
|
56
|
+
| Class / Module | Role |
|
|
57
|
+
| --- | --- |
|
|
58
|
+
| `BaseProcessor` | Abstract base for implementing payment gateways. |
|
|
59
|
+
| `PaymentFlow` | Semantic orchestration for payment lifecycles. |
|
|
60
|
+
| `PaymentStatus` | Enum for all possible payment states (NEW, PAID, FAILED, etc.). |
|
|
61
|
+
| `registry` | Singleton registry for backend discovery. |
|
|
62
|
+
| `TransactionResult` | Standard response for transaction initiation. |
|
|
63
|
+
| `GetPaidException` | Base exception for all payment-related errors. |
|
|
64
|
+
|
|
65
|
+
## Ecosystem
|
|
66
|
+
|
|
67
|
+
`getpaid-core` is the heart of a larger ecosystem designed to make payment processing easy in any Python web application.
|
|
68
|
+
|
|
69
|
+
### Framework Wrappers
|
|
70
|
+
- [django-getpaid](https://github.com/django-getpaid/django-getpaid) — Official Django integration.
|
|
71
|
+
- [litestar-getpaid](https://github.com/django-getpaid/litestar-getpaid) — Official Litestar integration.
|
|
72
|
+
- [fastapi-getpaid](https://github.com/django-getpaid/fastapi-getpaid) — Official FastAPI integration.
|
|
73
|
+
|
|
74
|
+
### Processor Plugins
|
|
75
|
+
- [python-getpaid-payu](https://github.com/django-getpaid/python-getpaid-payu) — PayU backend.
|
|
76
|
+
- [python-getpaid-paynow](https://github.com/django-getpaid/python-getpaid-paynow) — Paynow backend.
|
|
77
|
+
- [python-getpaid-bitpay](https://github.com/django-getpaid/python-getpaid-bitpay) — BitPay backend.
|
|
78
|
+
- [python-getpaid-przelewy24](https://github.com/django-getpaid/python-getpaid-przelewy24) — Przelewy24 backend.
|
|
79
|
+
|
|
80
|
+
## License
|
|
81
|
+
|
|
82
|
+
This project is licensed under the MIT License.
|
|
@@ -12,12 +12,11 @@ framework-agnostic library.
|
|
|
12
12
|
- Fraud status enum (`FraudStatus`) with 4 states
|
|
13
13
|
- Backend method and confirmation method enums
|
|
14
14
|
- `BaseProcessor` abstract class for payment gateway plugins
|
|
15
|
-
-
|
|
16
|
-
- Transition
|
|
17
|
-
-
|
|
18
|
-
- Fraud message callback (`_store_fraud_message`)
|
|
15
|
+
- Semantic payment and fraud update engine
|
|
16
|
+
- Transition validation with `InvalidTransitionError`
|
|
17
|
+
- Provider metadata merging and callback idempotency tracking
|
|
19
18
|
- `PluginRegistry` with entry-point discovery and manual registration
|
|
20
19
|
- Runtime-checkable protocols: `Payment`, `Order`, `PaymentRepository`
|
|
21
|
-
-
|
|
22
|
-
`
|
|
20
|
+
- Dataclass response types: `BuyerInfo`, `ItemInfo`, `ChargeResult`,
|
|
21
|
+
`PaymentUpdate`, `RefundResult`, `TransactionResult`
|
|
23
22
|
- Structured exception hierarchy with `context` support
|
|
@@ -16,45 +16,35 @@ Payments move through these states:
|
|
|
16
16
|
| `REFUND_STARTED` | `"refund_started"` | Refund initiated |
|
|
17
17
|
| `REFUNDED` | `"refunded"` | Fully refunded or lock released |
|
|
18
18
|
|
|
19
|
-
## Payment
|
|
19
|
+
## Payment Events
|
|
20
20
|
|
|
21
21
|
```
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
▼ ▼
|
|
32
|
-
PARTIAL ◄──────────────────────┘
|
|
33
|
-
│
|
|
34
|
-
├── mark_as_paid ──────────► PAID
|
|
35
|
-
│
|
|
36
|
-
├── start_refund ──────────► REFUND_STARTED
|
|
37
|
-
│ │
|
|
38
|
-
│ cancel_refund │ confirm_refund
|
|
39
|
-
◄─────────────────────────────┘
|
|
40
|
-
│
|
|
41
|
-
└── mark_as_refunded ──────► REFUNDED
|
|
42
|
-
|
|
43
|
-
NEW/PREPARED/PRE_AUTH ──fail──► FAILED
|
|
44
|
-
PRE_AUTH ──release_lock──────► REFUNDED
|
|
22
|
+
prepared -> PREPARED
|
|
23
|
+
locked -> PRE_AUTH
|
|
24
|
+
charge_requested -> IN_CHARGE
|
|
25
|
+
payment_captured -> PARTIAL or PAID
|
|
26
|
+
failed -> FAILED
|
|
27
|
+
refund_requested -> REFUND_STARTED
|
|
28
|
+
refund_confirmed -> PARTIAL or REFUNDED
|
|
29
|
+
refund_cancelled -> active paid status
|
|
30
|
+
lock_released -> REFUNDED
|
|
45
31
|
```
|
|
46
32
|
|
|
47
|
-
### Transition
|
|
33
|
+
### Transition Rules
|
|
48
34
|
|
|
49
|
-
|
|
35
|
+
The state engine raises `InvalidTransitionError` when an event is incompatible
|
|
36
|
+
with the current payment state.
|
|
50
37
|
|
|
51
|
-
-
|
|
52
|
-
-
|
|
38
|
+
- You cannot capture a payment after it is already refunded.
|
|
39
|
+
- You cannot start a refund before the payment has been paid.
|
|
40
|
+
- Refund confirmation moves to `REFUNDED` only when `amount_refunded >= amount_paid`.
|
|
53
41
|
|
|
54
|
-
### Amount
|
|
42
|
+
### Amount Handling
|
|
55
43
|
|
|
56
|
-
-
|
|
57
|
-
-
|
|
44
|
+
- `locked_amount` stores a pre-authorized amount.
|
|
45
|
+
- `paid_amount` updates captured funds and may reduce `amount_locked`.
|
|
46
|
+
- `refunded_amount` tracks refund progress.
|
|
47
|
+
- `provider_data` stores provider-specific metadata such as refund IDs and applied callback IDs.
|
|
58
48
|
|
|
59
49
|
## Fraud Statuses
|
|
60
50
|
|
|
@@ -103,13 +93,14 @@ class Payment(Protocol):
|
|
|
103
93
|
currency: str
|
|
104
94
|
status: str
|
|
105
95
|
backend: str
|
|
106
|
-
external_id: str
|
|
107
|
-
description: str
|
|
96
|
+
external_id: str | None
|
|
97
|
+
description: str | None
|
|
108
98
|
amount_paid: Decimal
|
|
109
99
|
amount_locked: Decimal
|
|
110
100
|
amount_refunded: Decimal
|
|
111
101
|
fraud_status: str
|
|
112
102
|
fraud_message: str
|
|
103
|
+
provider_data: dict[str, Any]
|
|
113
104
|
```
|
|
114
105
|
|
|
115
106
|
### PaymentRepository Protocol
|
|
@@ -162,6 +153,7 @@ raise ChargeFailure("Gateway returned 500", context={"status_code": 500})
|
|
|
162
153
|
|------|-------------|
|
|
163
154
|
| `BuyerInfo` | TypedDict with `email`, `first_name`, `last_name`, `phone` (all optional) |
|
|
164
155
|
| `ItemInfo` | TypedDict with `name`, `quantity`, `unit_price` |
|
|
165
|
-
| `
|
|
166
|
-
| `
|
|
167
|
-
| `
|
|
156
|
+
| `ChargeResult` | Dataclass with `amount_charged`, `success`, `async_call`, `provider_data` |
|
|
157
|
+
| `PaymentUpdate` | Dataclass describing semantic payment/fraud events and amounts |
|
|
158
|
+
| `RefundResult` | Dataclass with refund amount and provider metadata |
|
|
159
|
+
| `TransactionResult` | Dataclass with redirect, method, external ID, and provider metadata |
|
|
@@ -77,21 +77,30 @@ from getpaid_core.registry import registry
|
|
|
77
77
|
registry.register(MyGatewayProcessor)
|
|
78
78
|
```
|
|
79
79
|
|
|
80
|
-
## Payment
|
|
80
|
+
## Payment Updates
|
|
81
81
|
|
|
82
|
-
Payments move through states
|
|
83
|
-
|
|
82
|
+
Payments move through states by applying semantic `PaymentUpdate` objects.
|
|
83
|
+
Processors return updates from callbacks and status polling, and the flow
|
|
84
|
+
applies them to the payment object:
|
|
84
85
|
|
|
85
86
|
```python
|
|
86
|
-
from
|
|
87
|
-
|
|
88
|
-
machine = create_payment_machine(payment)
|
|
87
|
+
from decimal import Decimal
|
|
89
88
|
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
payment
|
|
89
|
+
from getpaid_core.fsm import apply_payment_update
|
|
90
|
+
from getpaid_core.types import PaymentUpdate
|
|
91
|
+
|
|
92
|
+
apply_payment_update(
|
|
93
|
+
payment,
|
|
94
|
+
PaymentUpdate(payment_event="prepared"),
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
apply_payment_update(
|
|
98
|
+
payment,
|
|
99
|
+
PaymentUpdate(
|
|
100
|
+
payment_event="payment_captured",
|
|
101
|
+
paid_amount=Decimal("100.00"),
|
|
102
|
+
),
|
|
103
|
+
)
|
|
95
104
|
```
|
|
96
105
|
|
|
97
|
-
See {doc}`concepts` for the
|
|
106
|
+
See {doc}`concepts` for the lifecycle rules and semantic event mapping.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = 'python-getpaid-core'
|
|
3
|
-
|
|
3
|
+
dynamic = ["version"]
|
|
4
4
|
description = 'Framework-agnostic payment processing core.'
|
|
5
5
|
readme = 'README.md'
|
|
6
6
|
license = {text = 'MIT'}
|
|
@@ -52,6 +52,9 @@ build-backend = 'hatchling.build'
|
|
|
52
52
|
[tool.hatch.build.targets.wheel]
|
|
53
53
|
packages = ['src/getpaid_core']
|
|
54
54
|
|
|
55
|
+
[tool.hatch.version]
|
|
56
|
+
path = "src/getpaid_core/__init__.py"
|
|
57
|
+
|
|
55
58
|
[tool.pytest.ini_options]
|
|
56
59
|
testpaths = ['tests']
|
|
57
60
|
asyncio_mode = 'auto'
|
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
"""Getpaid Core -- framework-agnostic payment processing."""
|
|
2
2
|
|
|
3
|
-
__version__ = "0.
|
|
3
|
+
__version__ = "3.0.0a3"
|
|
4
4
|
|
|
5
5
|
from getpaid_core.enums import BackendMethod
|
|
6
6
|
from getpaid_core.enums import ConfirmationMethod
|
|
7
|
+
from getpaid_core.enums import FraudEvent
|
|
7
8
|
from getpaid_core.enums import FraudStatus
|
|
9
|
+
from getpaid_core.enums import PaymentEvent
|
|
8
10
|
from getpaid_core.enums import PaymentStatus
|
|
9
11
|
from getpaid_core.exceptions import ChargeFailure
|
|
10
12
|
from getpaid_core.exceptions import CommunicationError
|
|
@@ -16,24 +18,36 @@ from getpaid_core.exceptions import LockFailure
|
|
|
16
18
|
from getpaid_core.exceptions import RefundFailure
|
|
17
19
|
from getpaid_core.flow import PaymentFlow
|
|
18
20
|
from getpaid_core.processor import BaseProcessor
|
|
21
|
+
from getpaid_core.registry import PluginRegistry
|
|
19
22
|
from getpaid_core.registry import registry
|
|
23
|
+
from getpaid_core.types import ChargeResult
|
|
24
|
+
from getpaid_core.types import PaymentUpdate
|
|
25
|
+
from getpaid_core.types import RefundResult
|
|
26
|
+
from getpaid_core.types import TransactionResult
|
|
20
27
|
|
|
21
28
|
|
|
22
29
|
__all__ = [
|
|
23
30
|
"BackendMethod",
|
|
24
31
|
"BaseProcessor",
|
|
25
32
|
"ChargeFailure",
|
|
33
|
+
"ChargeResult",
|
|
26
34
|
"CommunicationError",
|
|
27
35
|
"ConfirmationMethod",
|
|
28
36
|
"CredentialsError",
|
|
37
|
+
"FraudEvent",
|
|
29
38
|
"FraudStatus",
|
|
30
39
|
"GetPaidException",
|
|
31
40
|
"InvalidCallbackError",
|
|
32
41
|
"InvalidTransitionError",
|
|
33
42
|
"LockFailure",
|
|
43
|
+
"PaymentEvent",
|
|
34
44
|
"PaymentFlow",
|
|
35
45
|
"PaymentStatus",
|
|
46
|
+
"PaymentUpdate",
|
|
47
|
+
"PluginRegistry",
|
|
36
48
|
"RefundFailure",
|
|
49
|
+
"RefundResult",
|
|
50
|
+
"TransactionResult",
|
|
37
51
|
"__version__",
|
|
38
52
|
"registry",
|
|
39
53
|
]
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
"""Dummy payment backend for development and testing."""
|
|
2
|
+
|
|
3
|
+
from collections.abc import Sequence
|
|
4
|
+
from decimal import Decimal
|
|
5
|
+
from typing import ClassVar
|
|
6
|
+
|
|
7
|
+
from getpaid_core.enums import BackendMethod
|
|
8
|
+
from getpaid_core.enums import FraudEvent
|
|
9
|
+
from getpaid_core.enums import PaymentEvent
|
|
10
|
+
from getpaid_core.processor import BaseProcessor
|
|
11
|
+
from getpaid_core.types import ChargeResult
|
|
12
|
+
from getpaid_core.types import PaymentUpdate
|
|
13
|
+
from getpaid_core.types import RefundResult
|
|
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[Sequence[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 = BackendMethod(self.get_setting("method", BackendMethod.REST))
|
|
33
|
+
if method is BackendMethod.POST:
|
|
34
|
+
return TransactionResult(
|
|
35
|
+
method=method,
|
|
36
|
+
redirect_url="https://dummy.example.com/form",
|
|
37
|
+
form_data={
|
|
38
|
+
"payment_id": self.payment.id,
|
|
39
|
+
"amount": f"{self.payment.amount_required:.2f}",
|
|
40
|
+
"currency": self.payment.currency,
|
|
41
|
+
},
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
return TransactionResult(
|
|
45
|
+
method=method,
|
|
46
|
+
redirect_url=f"https://dummy.example.com/pay/{self.payment.id}",
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
async def handle_callback(
|
|
50
|
+
self, data: dict, headers: dict, **kwargs
|
|
51
|
+
) -> PaymentUpdate | None:
|
|
52
|
+
event = data.get("event")
|
|
53
|
+
if event == "payment_confirmed":
|
|
54
|
+
amount = Decimal(
|
|
55
|
+
str(data.get("paid_amount", self.payment.amount_required))
|
|
56
|
+
)
|
|
57
|
+
return PaymentUpdate(
|
|
58
|
+
payment_event=PaymentEvent.PAYMENT_CAPTURED,
|
|
59
|
+
paid_amount=amount,
|
|
60
|
+
provider_event_id=str(
|
|
61
|
+
data.get("event_id", f"payment:{self.payment.id}")
|
|
62
|
+
),
|
|
63
|
+
)
|
|
64
|
+
if event == "payment_failed":
|
|
65
|
+
return PaymentUpdate(payment_event=PaymentEvent.FAILED)
|
|
66
|
+
if event == "payment_locked":
|
|
67
|
+
amount = Decimal(
|
|
68
|
+
str(data.get("locked_amount", self.payment.amount_required))
|
|
69
|
+
)
|
|
70
|
+
return PaymentUpdate(
|
|
71
|
+
payment_event=PaymentEvent.LOCKED,
|
|
72
|
+
locked_amount=amount,
|
|
73
|
+
)
|
|
74
|
+
if event == "refund_confirmed":
|
|
75
|
+
amount = Decimal(
|
|
76
|
+
str(data.get("refunded_amount", self.payment.amount_paid))
|
|
77
|
+
)
|
|
78
|
+
return PaymentUpdate(
|
|
79
|
+
payment_event=PaymentEvent.REFUND_CONFIRMED,
|
|
80
|
+
refunded_amount=amount,
|
|
81
|
+
provider_event_id=str(
|
|
82
|
+
data.get("event_id", f"refund:{self.payment.id}")
|
|
83
|
+
),
|
|
84
|
+
)
|
|
85
|
+
if event == "refund_cancelled":
|
|
86
|
+
return PaymentUpdate(payment_event=PaymentEvent.REFUND_CANCELLED)
|
|
87
|
+
if event == "fraud_review":
|
|
88
|
+
return PaymentUpdate(
|
|
89
|
+
fraud_event=FraudEvent.REVIEW,
|
|
90
|
+
fraud_message="Manual review required",
|
|
91
|
+
)
|
|
92
|
+
if event == "fraud_rejected":
|
|
93
|
+
return PaymentUpdate(
|
|
94
|
+
fraud_event=FraudEvent.REJECT,
|
|
95
|
+
fraud_message="Rejected by dummy backend",
|
|
96
|
+
)
|
|
97
|
+
if event == "fraud_accepted":
|
|
98
|
+
return PaymentUpdate(
|
|
99
|
+
fraud_event=FraudEvent.ACCEPT,
|
|
100
|
+
fraud_message="Accepted by dummy backend",
|
|
101
|
+
)
|
|
102
|
+
return None
|
|
103
|
+
|
|
104
|
+
async def fetch_payment_status(self, **kwargs) -> PaymentUpdate | None:
|
|
105
|
+
event = self.get_setting("confirmation_event", "payment_confirmed")
|
|
106
|
+
if event == "payment_locked":
|
|
107
|
+
return PaymentUpdate(
|
|
108
|
+
payment_event=PaymentEvent.LOCKED,
|
|
109
|
+
locked_amount=self.payment.amount_required,
|
|
110
|
+
provider_event_id=f"pull-lock:{self.payment.id}",
|
|
111
|
+
)
|
|
112
|
+
if event == "payment_failed":
|
|
113
|
+
return PaymentUpdate(
|
|
114
|
+
payment_event=PaymentEvent.FAILED,
|
|
115
|
+
provider_event_id=f"pull-fail:{self.payment.id}",
|
|
116
|
+
)
|
|
117
|
+
return PaymentUpdate(
|
|
118
|
+
payment_event=PaymentEvent.PAYMENT_CAPTURED,
|
|
119
|
+
paid_amount=self.payment.amount_required,
|
|
120
|
+
provider_event_id=f"pull-pay:{self.payment.id}",
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
async def charge(
|
|
124
|
+
self, amount: Decimal | None = None, **kwargs
|
|
125
|
+
) -> ChargeResult:
|
|
126
|
+
charged = amount if amount is not None else self.payment.amount_required
|
|
127
|
+
return ChargeResult(
|
|
128
|
+
amount_charged=charged,
|
|
129
|
+
success=True,
|
|
130
|
+
async_call=bool(kwargs.get("async_call", False)),
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
async def release_lock(self, **kwargs) -> Decimal:
|
|
134
|
+
return self.payment.amount_locked
|
|
135
|
+
|
|
136
|
+
async def start_refund(
|
|
137
|
+
self, amount: Decimal | None = None, **kwargs
|
|
138
|
+
) -> RefundResult:
|
|
139
|
+
return RefundResult(
|
|
140
|
+
amount=amount if amount is not None else self.payment.amount_paid,
|
|
141
|
+
provider_data={"refund_id": f"dummy-refund-{self.payment.id}"},
|
|
142
|
+
)
|
|
143
|
+
|
|
144
|
+
async def cancel_refund(self, **kwargs) -> bool:
|
|
145
|
+
return True
|
|
@@ -1,7 +1,4 @@
|
|
|
1
|
-
"""Payment processing enums.
|
|
2
|
-
|
|
3
|
-
Values are kept identical to django-getpaid for backward compatibility.
|
|
4
|
-
"""
|
|
1
|
+
"""Payment processing enums."""
|
|
5
2
|
|
|
6
3
|
from enum import StrEnum
|
|
7
4
|
|
|
@@ -29,6 +26,28 @@ class FraudStatus(StrEnum):
|
|
|
29
26
|
CHECK = "check"
|
|
30
27
|
|
|
31
28
|
|
|
29
|
+
class PaymentEvent(StrEnum):
|
|
30
|
+
"""Semantic payment lifecycle events reported by processors."""
|
|
31
|
+
|
|
32
|
+
PREPARED = "prepared"
|
|
33
|
+
LOCKED = "locked"
|
|
34
|
+
CHARGE_REQUESTED = "charge_requested"
|
|
35
|
+
PAYMENT_CAPTURED = "payment_captured"
|
|
36
|
+
FAILED = "failed"
|
|
37
|
+
REFUND_REQUESTED = "refund_requested"
|
|
38
|
+
REFUND_CONFIRMED = "refund_confirmed"
|
|
39
|
+
REFUND_CANCELLED = "refund_cancelled"
|
|
40
|
+
LOCK_RELEASED = "lock_released"
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class FraudEvent(StrEnum):
|
|
44
|
+
"""Semantic fraud review events reported by processors."""
|
|
45
|
+
|
|
46
|
+
REVIEW = "review"
|
|
47
|
+
ACCEPT = "accept"
|
|
48
|
+
REJECT = "reject"
|
|
49
|
+
|
|
50
|
+
|
|
32
51
|
class BackendMethod(StrEnum):
|
|
33
52
|
"""HTTP method used to initiate payment."""
|
|
34
53
|
|