python-getpaid-paynow 0.1.2__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_paynow-3.0.0a3/.github/workflows/ci.yml +33 -0
- {python_getpaid_paynow-0.1.2 → python_getpaid_paynow-3.0.0a3}/.gitignore +1 -0
- {python_getpaid_paynow-0.1.2 → python_getpaid_paynow-3.0.0a3}/PKG-INFO +42 -48
- python_getpaid_paynow-3.0.0a3/README.md +125 -0
- {python_getpaid_paynow-0.1.2 → python_getpaid_paynow-3.0.0a3}/docs/concepts.md +13 -12
- {python_getpaid_paynow-0.1.2 → python_getpaid_paynow-3.0.0a3}/docs/getting-started.md +1 -1
- {python_getpaid_paynow-0.1.2 → python_getpaid_paynow-3.0.0a3}/pyproject.toml +5 -4
- {python_getpaid_paynow-0.1.2 → python_getpaid_paynow-3.0.0a3}/src/getpaid_paynow/__init__.py +2 -0
- {python_getpaid_paynow-0.1.2 → python_getpaid_paynow-3.0.0a3}/src/getpaid_paynow/processor.py +73 -66
- python_getpaid_paynow-3.0.0a3/tests/conftest.py +120 -0
- python_getpaid_paynow-3.0.0a3/tests/test_callback.py +144 -0
- python_getpaid_paynow-3.0.0a3/tests/test_processor.py +208 -0
- python_getpaid_paynow-3.0.0a3/tests/test_public_api.py +7 -0
- python_getpaid_paynow-0.1.2/README.md +0 -131
- python_getpaid_paynow-0.1.2/tests/conftest.py +0 -143
- python_getpaid_paynow-0.1.2/tests/test_callback.py +0 -337
- python_getpaid_paynow-0.1.2/tests/test_processor.py +0 -375
- python_getpaid_paynow-0.1.2/uv.lock +0 -925
- {python_getpaid_paynow-0.1.2 → python_getpaid_paynow-3.0.0a3}/.readthedocs.yml +0 -0
- {python_getpaid_paynow-0.1.2 → python_getpaid_paynow-3.0.0a3}/CODE_OF_CONDUCT.md +0 -0
- {python_getpaid_paynow-0.1.2 → python_getpaid_paynow-3.0.0a3}/CONTRIBUTING.md +0 -0
- {python_getpaid_paynow-0.1.2 → python_getpaid_paynow-3.0.0a3}/LICENSE +0 -0
- {python_getpaid_paynow-0.1.2 → python_getpaid_paynow-3.0.0a3}/docs/changelog.md +0 -0
- {python_getpaid_paynow-0.1.2 → python_getpaid_paynow-3.0.0a3}/docs/codeofconduct.md +0 -0
- {python_getpaid_paynow-0.1.2 → python_getpaid_paynow-3.0.0a3}/docs/conf.py +0 -0
- {python_getpaid_paynow-0.1.2 → python_getpaid_paynow-3.0.0a3}/docs/configuration.md +0 -0
- {python_getpaid_paynow-0.1.2 → python_getpaid_paynow-3.0.0a3}/docs/contributing.md +0 -0
- {python_getpaid_paynow-0.1.2 → python_getpaid_paynow-3.0.0a3}/docs/index.md +0 -0
- {python_getpaid_paynow-0.1.2 → python_getpaid_paynow-3.0.0a3}/docs/license.md +0 -0
- {python_getpaid_paynow-0.1.2 → python_getpaid_paynow-3.0.0a3}/docs/reference.md +0 -0
- {python_getpaid_paynow-0.1.2 → python_getpaid_paynow-3.0.0a3}/docs/requirements.txt +0 -0
- {python_getpaid_paynow-0.1.2 → python_getpaid_paynow-3.0.0a3}/src/getpaid_paynow/client.py +0 -0
- {python_getpaid_paynow-0.1.2 → python_getpaid_paynow-3.0.0a3}/src/getpaid_paynow/py.typed +0 -0
- {python_getpaid_paynow-0.1.2 → python_getpaid_paynow-3.0.0a3}/src/getpaid_paynow/types.py +0 -0
- {python_getpaid_paynow-0.1.2 → python_getpaid_paynow-3.0.0a3}/tests/__init__.py +0 -0
- {python_getpaid_paynow-0.1.2 → python_getpaid_paynow-3.0.0a3}/tests/test_client.py +0 -0
- {python_getpaid_paynow-0.1.2 → python_getpaid_paynow-3.0.0a3}/tests/test_types.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
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: python-getpaid-paynow
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 3.0.0a3
|
|
4
4
|
Summary: Paynow payment gateway integration for python-getpaid ecosystem.
|
|
5
5
|
Project-URL: Homepage, https://github.com/django-getpaid/python-getpaid-paynow
|
|
6
6
|
Project-URL: Repository, https://github.com/django-getpaid/python-getpaid-paynow
|
|
@@ -18,43 +18,51 @@ Classifier: Topic :: Office/Business :: Financial :: Point-Of-Sale
|
|
|
18
18
|
Classifier: Typing :: Typed
|
|
19
19
|
Requires-Python: >=3.12
|
|
20
20
|
Requires-Dist: httpx>=0.27.0
|
|
21
|
-
Requires-Dist: python-getpaid-core>=0.
|
|
21
|
+
Requires-Dist: python-getpaid-core>=3.0.0a3
|
|
22
22
|
Description-Content-Type: text/markdown
|
|
23
23
|
|
|
24
|
-
# getpaid-paynow
|
|
24
|
+
# python-getpaid-paynow
|
|
25
25
|
|
|
26
|
-
[](https://pypi.org/project/python-getpaid-paynow/)
|
|
27
|
-
[](https://pypi.org/project/python-getpaid-paynow/)
|
|
27
|
+
[](https://opensource.org/licenses/MIT)
|
|
28
|
+
[](https://pypi.org/project/python-getpaid-paynow/)
|
|
29
29
|
|
|
30
|
-
[
|
|
31
|
-
|
|
32
|
-
async HTTP client (`PaynowClient`) and a payment processor (`PaynowProcessor`)
|
|
33
|
-
that integrates with getpaid-core's `BaseProcessor` interface. Authentication
|
|
34
|
-
uses API Key + HMAC-SHA256 signature against the Paynow V3 REST API.
|
|
30
|
+
Paynow payment processor for [python-getpaid](https://github.com/django-getpaid/python-getpaid-core) ecosystem.
|
|
31
|
+
Paynow is a modern Polish payment provider and a subsidiary of mBank.
|
|
35
32
|
|
|
36
33
|
## Architecture
|
|
37
34
|
|
|
38
35
|
The plugin is split into two layers:
|
|
39
36
|
|
|
40
|
-
- **`PaynowClient`** -- low-level async HTTP client wrapping the Paynow V3
|
|
41
|
-
|
|
42
|
-
HMAC-SHA256 request signing. Can be used standalone or as an async context
|
|
43
|
-
manager for connection reuse.
|
|
44
|
-
- **`PaynowProcessor`** -- high-level payment processor implementing
|
|
45
|
-
`BaseProcessor`. Orchestrates payment creation, callback/notification
|
|
46
|
-
handling, status polling, and refunds. Integrates with the getpaid-core FSM
|
|
47
|
-
for state transitions.
|
|
37
|
+
- **`PaynowClient`** -- low-level async HTTP client wrapping the Paynow V3 REST API. Uses `httpx.AsyncClient` with API Key authentication and HMAC-SHA256 request signing. Can be used standalone or as an async context manager for connection reuse.
|
|
38
|
+
- **`PaynowProcessor`** -- high-level payment processor implementing `BaseProcessor`. Orchestrates payment creation, callback/notification handling, status polling, and refunds using semantic payment updates.
|
|
48
39
|
|
|
49
40
|
## Key Features
|
|
50
41
|
|
|
51
42
|
- **Create payment** -- register a payment and get a redirect URL
|
|
52
|
-
- **Notification handling** -- verify HMAC signature and process status changes
|
|
53
|
-
- **Status polling** -- fetch current payment status via API
|
|
43
|
+
- **Notification handling** -- verify HMAC-SHA256 signature and process status changes
|
|
44
|
+
- **Status polling** -- fetch current payment status via API (PULL flow)
|
|
54
45
|
- **Refund** -- create, check, and cancel refunds
|
|
55
46
|
- **Payment methods** -- retrieve available payment methods
|
|
56
|
-
- **
|
|
57
|
-
|
|
47
|
+
- **Sandbox mode** -- full support for testing environment
|
|
48
|
+
|
|
49
|
+
**Note:** Paynow does not support pre-authorization flows. Immediate capture is used for all transactions. The `charge()` and `release_lock()` methods raise `NotImplementedError`.
|
|
50
|
+
|
|
51
|
+
## Supported Currencies
|
|
52
|
+
|
|
53
|
+
The processor supports the following 4 currencies:
|
|
54
|
+
- **PLN** (Polish Złoty)
|
|
55
|
+
- **EUR** (Euro)
|
|
56
|
+
- **GBP** (British Pound)
|
|
57
|
+
- **USD** (US Dollar)
|
|
58
|
+
|
|
59
|
+
## Installation
|
|
60
|
+
|
|
61
|
+
Install the package using pip:
|
|
62
|
+
|
|
63
|
+
```bash
|
|
64
|
+
pip install python-getpaid-paynow
|
|
65
|
+
```
|
|
58
66
|
|
|
59
67
|
## Quick Usage
|
|
60
68
|
|
|
@@ -86,16 +94,16 @@ async def main():
|
|
|
86
94
|
anyio.run(main)
|
|
87
95
|
```
|
|
88
96
|
|
|
89
|
-
### With
|
|
97
|
+
### With python-getpaid
|
|
90
98
|
|
|
91
|
-
Register the plugin via entry point in `pyproject.toml
|
|
99
|
+
Register the plugin via entry point in `pyproject.toml` (if not using the pre-packaged version):
|
|
92
100
|
|
|
93
101
|
```toml
|
|
94
102
|
[project.entry-points."getpaid.backends"]
|
|
95
103
|
paynow = "getpaid_paynow.processor:PaynowProcessor"
|
|
96
104
|
```
|
|
97
105
|
|
|
98
|
-
Then configure in your
|
|
106
|
+
Then configure in your project settings:
|
|
99
107
|
|
|
100
108
|
```python
|
|
101
109
|
GETPAID_BACKEND_SETTINGS = {
|
|
@@ -103,8 +111,8 @@ GETPAID_BACKEND_SETTINGS = {
|
|
|
103
111
|
"api_key": "your-api-key",
|
|
104
112
|
"signature_key": "your-signature-key",
|
|
105
113
|
"sandbox": True,
|
|
106
|
-
"notification_url": "https://
|
|
107
|
-
"continue_url": "https://
|
|
114
|
+
"notification_url": "https://your-site.com/payments/{payment_id}/callback/",
|
|
115
|
+
"continue_url": "https://your-site.com/payments/{payment_id}/return/",
|
|
108
116
|
}
|
|
109
117
|
}
|
|
110
118
|
```
|
|
@@ -119,35 +127,21 @@ GETPAID_BACKEND_SETTINGS = {
|
|
|
119
127
|
| `notification_url` | `str` | `""` | Notification URL template; use `{payment_id}` placeholder |
|
|
120
128
|
| `continue_url` | `str` | `""` | Return URL template; use `{payment_id}` placeholder |
|
|
121
129
|
|
|
122
|
-
## Supported Currencies
|
|
123
|
-
|
|
124
|
-
PLN, EUR, USD, GBP (4 total).
|
|
125
|
-
|
|
126
|
-
## Limitations
|
|
127
|
-
|
|
128
|
-
Paynow does not support pre-authorization. The `charge()` and
|
|
129
|
-
`release_lock()` methods raise `NotImplementedError`.
|
|
130
|
-
|
|
131
130
|
## Requirements
|
|
132
131
|
|
|
133
132
|
- Python 3.12+
|
|
134
|
-
- `python-getpaid-core >= 0.
|
|
133
|
+
- `python-getpaid-core >= 3.0.0a3`
|
|
135
134
|
- `httpx >= 0.27.0`
|
|
136
135
|
|
|
137
|
-
##
|
|
136
|
+
## Links
|
|
138
137
|
|
|
139
|
-
- [python-getpaid-core](https://github.com/django-getpaid/python-getpaid-core)
|
|
140
|
-
- [
|
|
138
|
+
- **Core Library:** [python-getpaid-core](https://github.com/django-getpaid/python-getpaid-core)
|
|
139
|
+
- **Official Paynow Documentation:** [docs.paynow.pl](https://docs.paynow.pl/)
|
|
140
|
+
- **GitHub Repository:** [django-getpaid/python-getpaid-paynow](https://github.com/django-getpaid/python-getpaid-paynow)
|
|
141
141
|
|
|
142
142
|
## License
|
|
143
143
|
|
|
144
|
-
MIT
|
|
145
|
-
|
|
146
|
-
## Disclaimer
|
|
147
|
-
|
|
148
|
-
This project has nothing in common with the
|
|
149
|
-
[getpaid](http://code.google.com/p/getpaid/) plone project.
|
|
150
|
-
It is part of the `django-getpaid` / `python-getpaid` ecosystem.
|
|
144
|
+
This project is licensed under the MIT License.
|
|
151
145
|
|
|
152
146
|
## Credits
|
|
153
147
|
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
# python-getpaid-paynow
|
|
2
|
+
|
|
3
|
+
[](https://pypi.org/project/python-getpaid-paynow/)
|
|
4
|
+
[](https://opensource.org/licenses/MIT)
|
|
5
|
+
[](https://pypi.org/project/python-getpaid-paynow/)
|
|
6
|
+
|
|
7
|
+
Paynow payment processor for [python-getpaid](https://github.com/django-getpaid/python-getpaid-core) ecosystem.
|
|
8
|
+
Paynow is a modern Polish payment provider and a subsidiary of mBank.
|
|
9
|
+
|
|
10
|
+
## Architecture
|
|
11
|
+
|
|
12
|
+
The plugin is split into two layers:
|
|
13
|
+
|
|
14
|
+
- **`PaynowClient`** -- low-level async HTTP client wrapping the Paynow V3 REST API. Uses `httpx.AsyncClient` with API Key authentication and HMAC-SHA256 request signing. Can be used standalone or as an async context manager for connection reuse.
|
|
15
|
+
- **`PaynowProcessor`** -- high-level payment processor implementing `BaseProcessor`. Orchestrates payment creation, callback/notification handling, status polling, and refunds using semantic payment updates.
|
|
16
|
+
|
|
17
|
+
## Key Features
|
|
18
|
+
|
|
19
|
+
- **Create payment** -- register a payment and get a redirect URL
|
|
20
|
+
- **Notification handling** -- verify HMAC-SHA256 signature and process status changes
|
|
21
|
+
- **Status polling** -- fetch current payment status via API (PULL flow)
|
|
22
|
+
- **Refund** -- create, check, and cancel refunds
|
|
23
|
+
- **Payment methods** -- retrieve available payment methods
|
|
24
|
+
- **Sandbox mode** -- full support for testing environment
|
|
25
|
+
|
|
26
|
+
**Note:** Paynow does not support pre-authorization flows. Immediate capture is used for all transactions. The `charge()` and `release_lock()` methods raise `NotImplementedError`.
|
|
27
|
+
|
|
28
|
+
## Supported Currencies
|
|
29
|
+
|
|
30
|
+
The processor supports the following 4 currencies:
|
|
31
|
+
- **PLN** (Polish Złoty)
|
|
32
|
+
- **EUR** (Euro)
|
|
33
|
+
- **GBP** (British Pound)
|
|
34
|
+
- **USD** (US Dollar)
|
|
35
|
+
|
|
36
|
+
## Installation
|
|
37
|
+
|
|
38
|
+
Install the package using pip:
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
pip install python-getpaid-paynow
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
## Quick Usage
|
|
45
|
+
|
|
46
|
+
### Standalone Client
|
|
47
|
+
|
|
48
|
+
```python
|
|
49
|
+
import anyio
|
|
50
|
+
from decimal import Decimal
|
|
51
|
+
from getpaid_paynow import PaynowClient
|
|
52
|
+
|
|
53
|
+
async def main():
|
|
54
|
+
async with PaynowClient(
|
|
55
|
+
api_key="your-api-key",
|
|
56
|
+
signature_key="your-signature-key",
|
|
57
|
+
api_url="https://api.sandbox.paynow.pl",
|
|
58
|
+
) as client:
|
|
59
|
+
# Create a payment
|
|
60
|
+
response = await client.create_payment(
|
|
61
|
+
amount=Decimal("49.99"),
|
|
62
|
+
currency="PLN",
|
|
63
|
+
external_id="order-001",
|
|
64
|
+
description="Order #001",
|
|
65
|
+
buyer_email="buyer@example.com",
|
|
66
|
+
continue_url="https://shop.example.com/return/order-001",
|
|
67
|
+
)
|
|
68
|
+
redirect_url = response["redirectUrl"]
|
|
69
|
+
print(f"Redirect buyer to: {redirect_url}")
|
|
70
|
+
|
|
71
|
+
anyio.run(main)
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
### With python-getpaid
|
|
75
|
+
|
|
76
|
+
Register the plugin via entry point in `pyproject.toml` (if not using the pre-packaged version):
|
|
77
|
+
|
|
78
|
+
```toml
|
|
79
|
+
[project.entry-points."getpaid.backends"]
|
|
80
|
+
paynow = "getpaid_paynow.processor:PaynowProcessor"
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
Then configure in your project settings:
|
|
84
|
+
|
|
85
|
+
```python
|
|
86
|
+
GETPAID_BACKEND_SETTINGS = {
|
|
87
|
+
"paynow": {
|
|
88
|
+
"api_key": "your-api-key",
|
|
89
|
+
"signature_key": "your-signature-key",
|
|
90
|
+
"sandbox": True,
|
|
91
|
+
"notification_url": "https://your-site.com/payments/{payment_id}/callback/",
|
|
92
|
+
"continue_url": "https://your-site.com/payments/{payment_id}/return/",
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
## Configuration Reference
|
|
98
|
+
|
|
99
|
+
| Key | Type | Default | Description |
|
|
100
|
+
|-----|------|---------|-------------|
|
|
101
|
+
| `api_key` | `str` | *required* | API key from Paynow merchant panel |
|
|
102
|
+
| `signature_key` | `str` | *required* | Signature key for HMAC calculation |
|
|
103
|
+
| `sandbox` | `bool` | `True` | Use sandbox or production API |
|
|
104
|
+
| `notification_url` | `str` | `""` | Notification URL template; use `{payment_id}` placeholder |
|
|
105
|
+
| `continue_url` | `str` | `""` | Return URL template; use `{payment_id}` placeholder |
|
|
106
|
+
|
|
107
|
+
## Requirements
|
|
108
|
+
|
|
109
|
+
- Python 3.12+
|
|
110
|
+
- `python-getpaid-core >= 3.0.0a3`
|
|
111
|
+
- `httpx >= 0.27.0`
|
|
112
|
+
|
|
113
|
+
## Links
|
|
114
|
+
|
|
115
|
+
- **Core Library:** [python-getpaid-core](https://github.com/django-getpaid/python-getpaid-core)
|
|
116
|
+
- **Official Paynow Documentation:** [docs.paynow.pl](https://docs.paynow.pl/)
|
|
117
|
+
- **GitHub Repository:** [django-getpaid/python-getpaid-paynow](https://github.com/django-getpaid/python-getpaid-paynow)
|
|
118
|
+
|
|
119
|
+
## License
|
|
120
|
+
|
|
121
|
+
This project is licensed under the MIT License.
|
|
122
|
+
|
|
123
|
+
## Credits
|
|
124
|
+
|
|
125
|
+
Created by [Dominik Kozaczko](https://github.com/dekoza).
|
|
@@ -23,7 +23,7 @@ The Paynow payment flow follows a create-redirect-notify pattern:
|
|
|
23
23
|
┌──────────┐ ◄────────────────────────────────┘
|
|
24
24
|
│ Your │
|
|
25
25
|
│ Server │ 1. verify_callback (check HMAC signature)
|
|
26
|
-
│ │ 2. handle_callback (
|
|
26
|
+
│ │ 2. handle_callback (return semantic payment update)
|
|
27
27
|
└──────────┘
|
|
28
28
|
```
|
|
29
29
|
|
|
@@ -46,8 +46,9 @@ The Paynow payment flow follows a create-redirect-notify pattern:
|
|
|
46
46
|
mismatch.
|
|
47
47
|
|
|
48
48
|
5. **Handle callback** — `PaynowProcessor.handle_callback()` maps the Paynow
|
|
49
|
-
status to
|
|
50
|
-
`
|
|
49
|
+
status to semantic updates: `CONFIRMED` returns `payment_captured`,
|
|
50
|
+
`PENDING` returns `prepared`, and `REJECTED`, `ERROR`, `EXPIRED`,
|
|
51
|
+
`ABANDONED` return `failed`.
|
|
51
52
|
|
|
52
53
|
:::{note}
|
|
53
54
|
Unlike Przelewy24, Paynow does **not** require a separate verification step
|
|
@@ -154,17 +155,17 @@ The plugin supports both notification models:
|
|
|
154
155
|
|
|
155
156
|
- **PULL** — `PaynowProcessor.fetch_payment_status()` calls
|
|
156
157
|
`PaynowClient.get_payment_status()` to poll the payment status. Returns a
|
|
157
|
-
`
|
|
158
|
+
semantic `PaymentUpdate`.
|
|
158
159
|
|
|
159
|
-
| Paynow Status |
|
|
160
|
-
|
|
160
|
+
| Paynow Status | Semantic Event |
|
|
161
|
+
|---------------|----------------|
|
|
161
162
|
| `NEW` | `None` |
|
|
162
|
-
| `PENDING` | `
|
|
163
|
-
| `CONFIRMED` | `
|
|
164
|
-
| `REJECTED` | `
|
|
165
|
-
| `ERROR` | `
|
|
166
|
-
| `EXPIRED` | `
|
|
167
|
-
| `ABANDONED` | `
|
|
163
|
+
| `PENDING` | `prepared` |
|
|
164
|
+
| `CONFIRMED` | `payment_captured` |
|
|
165
|
+
| `REJECTED` | `failed` |
|
|
166
|
+
| `ERROR` | `failed` |
|
|
167
|
+
| `EXPIRED` | `failed` |
|
|
168
|
+
| `ABANDONED` | `failed` |
|
|
168
169
|
|
|
169
170
|
## Supported Operations
|
|
170
171
|
|
|
@@ -92,7 +92,7 @@ payment ID at runtime.
|
|
|
92
92
|
### 3. Process payments
|
|
93
93
|
|
|
94
94
|
The framework adapter handles the rest — creating payments, redirecting
|
|
95
|
-
buyers, receiving notifications, and
|
|
95
|
+
buyers, receiving notifications, and applying semantic payment updates.
|
|
96
96
|
|
|
97
97
|
## Sandbox vs Production
|
|
98
98
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = 'python-getpaid-paynow'
|
|
3
|
-
|
|
3
|
+
dynamic = ["version"]
|
|
4
4
|
description = 'Paynow payment gateway integration for python-getpaid ecosystem.'
|
|
5
5
|
readme = 'README.md'
|
|
6
6
|
license = {text = 'MIT'}
|
|
@@ -19,7 +19,7 @@ classifiers = [
|
|
|
19
19
|
'Typing :: Typed',
|
|
20
20
|
]
|
|
21
21
|
dependencies = [
|
|
22
|
-
'python-getpaid-core>=0.
|
|
22
|
+
'python-getpaid-core>=3.0.0a3',
|
|
23
23
|
'httpx>=0.27.0',
|
|
24
24
|
]
|
|
25
25
|
|
|
@@ -52,6 +52,9 @@ build-backend = 'hatchling.build'
|
|
|
52
52
|
[tool.hatch.build.targets.wheel]
|
|
53
53
|
packages = ['src/getpaid_paynow']
|
|
54
54
|
|
|
55
|
+
[tool.hatch.version]
|
|
56
|
+
path = "src/getpaid_paynow/__init__.py"
|
|
57
|
+
|
|
55
58
|
[tool.pytest.ini_options]
|
|
56
59
|
testpaths = ['tests']
|
|
57
60
|
asyncio_mode = 'auto'
|
|
@@ -108,8 +111,6 @@ include = ['tests/**']
|
|
|
108
111
|
unresolved-attribute = 'ignore'
|
|
109
112
|
invalid-argument-type = 'ignore'
|
|
110
113
|
|
|
111
|
-
# The processor uses FSM methods (may_trigger, confirm_payment,
|
|
112
|
-
# mark_as_paid, fail) dynamically bound by the transitions library.
|
|
113
114
|
[[tool.ty.overrides]]
|
|
114
115
|
include = ['src/getpaid_paynow/processor.py']
|
|
115
116
|
[tool.ty.overrides.rules]
|
{python_getpaid_paynow-0.1.2 → python_getpaid_paynow-3.0.0a3}/src/getpaid_paynow/processor.py
RENAMED
|
@@ -1,17 +1,17 @@
|
|
|
1
1
|
"""Paynow payment processor."""
|
|
2
2
|
|
|
3
|
-
import contextlib
|
|
4
3
|
import hmac as hmac_mod
|
|
5
4
|
import logging
|
|
6
5
|
from decimal import Decimal
|
|
7
6
|
from typing import ClassVar
|
|
8
7
|
|
|
8
|
+
from getpaid_core.enums import PaymentEvent
|
|
9
9
|
from getpaid_core.exceptions import InvalidCallbackError
|
|
10
10
|
from getpaid_core.processor import BaseProcessor
|
|
11
11
|
from getpaid_core.types import ChargeResponse
|
|
12
|
-
from getpaid_core.types import
|
|
12
|
+
from getpaid_core.types import PaymentUpdate
|
|
13
|
+
from getpaid_core.types import RefundResult
|
|
13
14
|
from getpaid_core.types import TransactionResult
|
|
14
|
-
from transitions.core import MachineError
|
|
15
15
|
|
|
16
16
|
from .client import PaynowClient
|
|
17
17
|
from .types import Currency
|
|
@@ -42,8 +42,8 @@ class PaynowProcessor(BaseProcessor):
|
|
|
42
42
|
def _get_client(self) -> PaynowClient:
|
|
43
43
|
"""Create a PaynowClient from processor config."""
|
|
44
44
|
return PaynowClient(
|
|
45
|
-
api_key=self.get_setting("api_key"),
|
|
46
|
-
signature_key=self.get_setting("signature_key"),
|
|
45
|
+
api_key=str(self.get_setting("api_key", "")),
|
|
46
|
+
signature_key=str(self.get_setting("signature_key", "")),
|
|
47
47
|
api_url=self.get_paywall_baseurl(),
|
|
48
48
|
)
|
|
49
49
|
|
|
@@ -87,15 +87,13 @@ class PaynowProcessor(BaseProcessor):
|
|
|
87
87
|
|
|
88
88
|
redirect_url = response.get("redirectUrl", "")
|
|
89
89
|
payment_id = response.get("paymentId", "")
|
|
90
|
-
|
|
91
|
-
if payment_id:
|
|
92
|
-
self.payment.external_id = payment_id
|
|
90
|
+
provider_data = {"paynow_status": response.get("status", "")}
|
|
93
91
|
|
|
94
92
|
return TransactionResult(
|
|
95
|
-
redirect_url=redirect_url,
|
|
96
|
-
form_data=None,
|
|
97
93
|
method="GET",
|
|
98
|
-
|
|
94
|
+
redirect_url=redirect_url or None,
|
|
95
|
+
external_id=payment_id or None,
|
|
96
|
+
provider_data=provider_data,
|
|
99
97
|
)
|
|
100
98
|
|
|
101
99
|
async def verify_callback(
|
|
@@ -119,9 +117,7 @@ class PaynowProcessor(BaseProcessor):
|
|
|
119
117
|
if isinstance(raw_body, (bytes, bytearray)):
|
|
120
118
|
raw_body = raw_body.decode("utf-8")
|
|
121
119
|
if not isinstance(raw_body, str):
|
|
122
|
-
raise InvalidCallbackError(
|
|
123
|
-
"raw_body must be a str or bytes value."
|
|
124
|
-
)
|
|
120
|
+
raise InvalidCallbackError("raw_body must be a str or bytes value.")
|
|
125
121
|
|
|
126
122
|
received_sig = ""
|
|
127
123
|
for key, value in headers.items():
|
|
@@ -152,73 +148,79 @@ class PaynowProcessor(BaseProcessor):
|
|
|
152
148
|
|
|
153
149
|
async def handle_callback(
|
|
154
150
|
self, data: dict, headers: dict, **kwargs
|
|
155
|
-
) -> None:
|
|
156
|
-
"""Handle Paynow notification and update
|
|
157
|
-
|
|
158
|
-
Paynow sends notification on every status change.
|
|
159
|
-
No separate verify step is needed. The flow:
|
|
160
|
-
1. Extract paymentId and status from notification
|
|
161
|
-
2. Store paymentId as external_id
|
|
162
|
-
3. Map Paynow status to FSM transition
|
|
163
|
-
|
|
164
|
-
Notifications may arrive multiple times and out of order.
|
|
165
|
-
Uses ``contextlib.suppress(MachineError)`` for idempotent
|
|
166
|
-
transitions.
|
|
167
|
-
"""
|
|
151
|
+
) -> PaymentUpdate | None:
|
|
152
|
+
"""Handle Paynow notification and return a semantic update."""
|
|
168
153
|
payment_id: str = data.get("paymentId", "")
|
|
169
154
|
paynow_status: str = data.get("status", "")
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
155
|
+
modified_at: str = data.get("modifiedAt", "")
|
|
156
|
+
provider_event_id = (
|
|
157
|
+
":".join(
|
|
158
|
+
part
|
|
159
|
+
for part in (payment_id, paynow_status, modified_at)
|
|
160
|
+
if part
|
|
161
|
+
)
|
|
162
|
+
or None
|
|
163
|
+
)
|
|
164
|
+
provider_data = {"paynow_status": paynow_status}
|
|
165
|
+
external_id = payment_id or self.payment.external_id
|
|
173
166
|
|
|
174
167
|
if paynow_status == PaynowPaymentStatus.CONFIRMED:
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
self.payment.id,
|
|
183
|
-
self.payment.status,
|
|
184
|
-
)
|
|
168
|
+
return PaymentUpdate(
|
|
169
|
+
payment_event=PaymentEvent.PAYMENT_CAPTURED,
|
|
170
|
+
paid_amount=self.payment.amount_required,
|
|
171
|
+
external_id=external_id,
|
|
172
|
+
provider_event_id=provider_event_id,
|
|
173
|
+
provider_data=provider_data,
|
|
174
|
+
)
|
|
185
175
|
elif paynow_status in (
|
|
186
176
|
PaynowPaymentStatus.REJECTED,
|
|
187
177
|
PaynowPaymentStatus.ERROR,
|
|
188
178
|
PaynowPaymentStatus.EXPIRED,
|
|
189
179
|
PaynowPaymentStatus.ABANDONED,
|
|
190
180
|
):
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
"Paynow status %s for payment %s — no FSM action",
|
|
197
|
-
paynow_status,
|
|
198
|
-
self.payment.id,
|
|
181
|
+
return PaymentUpdate(
|
|
182
|
+
payment_event=PaymentEvent.FAILED,
|
|
183
|
+
external_id=external_id,
|
|
184
|
+
provider_event_id=provider_event_id,
|
|
185
|
+
provider_data=provider_data,
|
|
199
186
|
)
|
|
187
|
+
return PaymentUpdate(
|
|
188
|
+
external_id=external_id,
|
|
189
|
+
provider_event_id=provider_event_id,
|
|
190
|
+
provider_data=provider_data,
|
|
191
|
+
)
|
|
200
192
|
|
|
201
|
-
async def fetch_payment_status(self, **kwargs) ->
|
|
193
|
+
async def fetch_payment_status(self, **kwargs) -> PaymentUpdate | None:
|
|
202
194
|
"""PULL flow: fetch payment status from Paynow API."""
|
|
203
195
|
client = self._get_client()
|
|
204
196
|
response = await client.get_payment_status(
|
|
205
197
|
self.payment.external_id,
|
|
206
198
|
)
|
|
199
|
+
payment_id = response.get("paymentId") or self.payment.external_id
|
|
207
200
|
paynow_status = response.get("status", "")
|
|
208
201
|
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
202
|
+
provider_data = {"paynow_status": paynow_status}
|
|
203
|
+
if paynow_status == PaynowPaymentStatus.CONFIRMED:
|
|
204
|
+
return PaymentUpdate(
|
|
205
|
+
payment_event=PaymentEvent.PAYMENT_CAPTURED,
|
|
206
|
+
paid_amount=self.payment.amount_required,
|
|
207
|
+
external_id=payment_id,
|
|
208
|
+
provider_event_id=f"poll:{payment_id}:{paynow_status}",
|
|
209
|
+
provider_data=provider_data,
|
|
210
|
+
)
|
|
211
|
+
if paynow_status in {
|
|
212
|
+
PaynowPaymentStatus.REJECTED,
|
|
213
|
+
PaynowPaymentStatus.ERROR,
|
|
214
|
+
PaynowPaymentStatus.EXPIRED,
|
|
215
|
+
PaynowPaymentStatus.ABANDONED,
|
|
216
|
+
}:
|
|
217
|
+
return PaymentUpdate(
|
|
218
|
+
payment_event=PaymentEvent.FAILED,
|
|
219
|
+
external_id=payment_id,
|
|
220
|
+
provider_event_id=f"poll:{payment_id}:{paynow_status}",
|
|
221
|
+
provider_data=provider_data,
|
|
222
|
+
)
|
|
223
|
+
return None
|
|
222
224
|
|
|
223
225
|
async def charge(
|
|
224
226
|
self, amount: Decimal | None = None, **kwargs
|
|
@@ -236,7 +238,7 @@ class PaynowProcessor(BaseProcessor):
|
|
|
236
238
|
|
|
237
239
|
async def start_refund(
|
|
238
240
|
self, amount: Decimal | None = None, **kwargs
|
|
239
|
-
) ->
|
|
241
|
+
) -> RefundResult:
|
|
240
242
|
"""Start a refund via Paynow API."""
|
|
241
243
|
client = self._get_client()
|
|
242
244
|
refund_amount = amount or self.payment.amount_paid
|
|
@@ -245,13 +247,18 @@ class PaynowProcessor(BaseProcessor):
|
|
|
245
247
|
amount=refund_amount,
|
|
246
248
|
)
|
|
247
249
|
refund_id = response.get("refundId", "")
|
|
250
|
+
provider_data = {}
|
|
248
251
|
if refund_id:
|
|
249
|
-
|
|
250
|
-
return refund_amount
|
|
252
|
+
provider_data["refund_id"] = refund_id
|
|
253
|
+
return RefundResult(amount=refund_amount, provider_data=provider_data)
|
|
251
254
|
|
|
252
255
|
async def cancel_refund(self, **kwargs) -> bool:
|
|
253
256
|
"""Cancel an awaiting refund via Paynow API."""
|
|
254
257
|
client = self._get_client()
|
|
255
|
-
refund_id =
|
|
258
|
+
refund_id = self.payment.provider_data.get("refund_id")
|
|
259
|
+
if not refund_id:
|
|
260
|
+
refund_id = getattr(self.payment, "external_refund_id", "")
|
|
261
|
+
if not refund_id:
|
|
262
|
+
raise InvalidCallbackError("Missing refund identifier")
|
|
256
263
|
await client.cancel_refund(refund_id)
|
|
257
264
|
return True
|