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.
Files changed (37) hide show
  1. python_getpaid_paynow-3.0.0a3/.github/workflows/ci.yml +33 -0
  2. {python_getpaid_paynow-0.1.2 → python_getpaid_paynow-3.0.0a3}/.gitignore +1 -0
  3. {python_getpaid_paynow-0.1.2 → python_getpaid_paynow-3.0.0a3}/PKG-INFO +42 -48
  4. python_getpaid_paynow-3.0.0a3/README.md +125 -0
  5. {python_getpaid_paynow-0.1.2 → python_getpaid_paynow-3.0.0a3}/docs/concepts.md +13 -12
  6. {python_getpaid_paynow-0.1.2 → python_getpaid_paynow-3.0.0a3}/docs/getting-started.md +1 -1
  7. {python_getpaid_paynow-0.1.2 → python_getpaid_paynow-3.0.0a3}/pyproject.toml +5 -4
  8. {python_getpaid_paynow-0.1.2 → python_getpaid_paynow-3.0.0a3}/src/getpaid_paynow/__init__.py +2 -0
  9. {python_getpaid_paynow-0.1.2 → python_getpaid_paynow-3.0.0a3}/src/getpaid_paynow/processor.py +73 -66
  10. python_getpaid_paynow-3.0.0a3/tests/conftest.py +120 -0
  11. python_getpaid_paynow-3.0.0a3/tests/test_callback.py +144 -0
  12. python_getpaid_paynow-3.0.0a3/tests/test_processor.py +208 -0
  13. python_getpaid_paynow-3.0.0a3/tests/test_public_api.py +7 -0
  14. python_getpaid_paynow-0.1.2/README.md +0 -131
  15. python_getpaid_paynow-0.1.2/tests/conftest.py +0 -143
  16. python_getpaid_paynow-0.1.2/tests/test_callback.py +0 -337
  17. python_getpaid_paynow-0.1.2/tests/test_processor.py +0 -375
  18. python_getpaid_paynow-0.1.2/uv.lock +0 -925
  19. {python_getpaid_paynow-0.1.2 → python_getpaid_paynow-3.0.0a3}/.readthedocs.yml +0 -0
  20. {python_getpaid_paynow-0.1.2 → python_getpaid_paynow-3.0.0a3}/CODE_OF_CONDUCT.md +0 -0
  21. {python_getpaid_paynow-0.1.2 → python_getpaid_paynow-3.0.0a3}/CONTRIBUTING.md +0 -0
  22. {python_getpaid_paynow-0.1.2 → python_getpaid_paynow-3.0.0a3}/LICENSE +0 -0
  23. {python_getpaid_paynow-0.1.2 → python_getpaid_paynow-3.0.0a3}/docs/changelog.md +0 -0
  24. {python_getpaid_paynow-0.1.2 → python_getpaid_paynow-3.0.0a3}/docs/codeofconduct.md +0 -0
  25. {python_getpaid_paynow-0.1.2 → python_getpaid_paynow-3.0.0a3}/docs/conf.py +0 -0
  26. {python_getpaid_paynow-0.1.2 → python_getpaid_paynow-3.0.0a3}/docs/configuration.md +0 -0
  27. {python_getpaid_paynow-0.1.2 → python_getpaid_paynow-3.0.0a3}/docs/contributing.md +0 -0
  28. {python_getpaid_paynow-0.1.2 → python_getpaid_paynow-3.0.0a3}/docs/index.md +0 -0
  29. {python_getpaid_paynow-0.1.2 → python_getpaid_paynow-3.0.0a3}/docs/license.md +0 -0
  30. {python_getpaid_paynow-0.1.2 → python_getpaid_paynow-3.0.0a3}/docs/reference.md +0 -0
  31. {python_getpaid_paynow-0.1.2 → python_getpaid_paynow-3.0.0a3}/docs/requirements.txt +0 -0
  32. {python_getpaid_paynow-0.1.2 → python_getpaid_paynow-3.0.0a3}/src/getpaid_paynow/client.py +0 -0
  33. {python_getpaid_paynow-0.1.2 → python_getpaid_paynow-3.0.0a3}/src/getpaid_paynow/py.typed +0 -0
  34. {python_getpaid_paynow-0.1.2 → python_getpaid_paynow-3.0.0a3}/src/getpaid_paynow/types.py +0 -0
  35. {python_getpaid_paynow-0.1.2 → python_getpaid_paynow-3.0.0a3}/tests/__init__.py +0 -0
  36. {python_getpaid_paynow-0.1.2 → python_getpaid_paynow-3.0.0a3}/tests/test_client.py +0 -0
  37. {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
@@ -18,3 +18,4 @@ htmlcov/
18
18
 
19
19
  # Sphinx build output
20
20
  docs/_build/
21
+ uv.lock
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: python-getpaid-paynow
3
- Version: 0.1.2
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.1.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
- [![PyPI](https://img.shields.io/pypi/v/python-getpaid-paynow.svg)](https://pypi.org/project/python-getpaid-paynow/)
27
- [![Python Version](https://img.shields.io/pypi/pyversions/python-getpaid-paynow)](https://pypi.org/project/python-getpaid-paynow/)
28
- [![License](https://img.shields.io/pypi/l/python-getpaid-paynow)](https://github.com/django-getpaid/python-getpaid-paynow/blob/main/LICENSE)
26
+ [![PyPI version](https://img.shields.io/pypi/v/python-getpaid-paynow.svg)](https://pypi.org/project/python-getpaid-paynow/)
27
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
28
+ [![Python versions](https://img.shields.io/pypi/pyversions/python-getpaid-paynow.svg)](https://pypi.org/project/python-getpaid-paynow/)
29
29
 
30
- [Paynow](https://www.paynow.pl/) payment gateway plugin for the
31
- [python-getpaid](https://github.com/django-getpaid) ecosystem. Provides an
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
- REST API. Uses `httpx.AsyncClient` with API Key authentication and
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
- - **HMAC-SHA256 signatures** -- automatic request and notification signing
57
- - **PUSH and PULL** -- notification-based flow with optional status polling
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 django-getpaid
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 Django settings (or config dict):
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://shop.example.com/payments/{payment_id}/callback/",
107
- "continue_url": "https://shop.example.com/payments/{payment_id}/return/",
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.1.0`
133
+ - `python-getpaid-core >= 3.0.0a3`
135
134
  - `httpx >= 0.27.0`
136
135
 
137
- ## Related Projects
136
+ ## Links
138
137
 
139
- - [python-getpaid-core](https://github.com/django-getpaid/python-getpaid-core) -- core abstractions (protocols, FSM, processor base class)
140
- - [django-getpaid](https://github.com/django-getpaid/django-getpaid) -- Django adapter (models, views, admin)
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
+ [![PyPI version](https://img.shields.io/pypi/v/python-getpaid-paynow.svg)](https://pypi.org/project/python-getpaid-paynow/)
4
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
5
+ [![Python versions](https://img.shields.io/pypi/pyversions/python-getpaid-paynow.svg)](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 (update FSM state)
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 FSM transitions: `CONFIRMED` triggers `confirm_payment` +
50
- `mark_as_paid`; `REJECTED`, `ERROR`, `EXPIRED`, `ABANDONED` trigger `fail`.
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
- `PaymentStatusResponse` with the mapped FSM trigger.
158
+ semantic `PaymentUpdate`.
158
159
 
159
- | Paynow Status | Mapped FSM Trigger |
160
- |---------------|-------------------|
160
+ | Paynow Status | Semantic Event |
161
+ |---------------|----------------|
161
162
  | `NEW` | `None` |
162
- | `PENDING` | `confirm_prepared` |
163
- | `CONFIRMED` | `confirm_payment` |
164
- | `REJECTED` | `fail` |
165
- | `ERROR` | `fail` |
166
- | `EXPIRED` | `fail` |
167
- | `ABANDONED` | `fail` |
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 updating payment status via the FSM.
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
- version = "0.1.2"
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.1.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]
@@ -1,5 +1,7 @@
1
1
  """Paynow V3 payment gateway integration for python-getpaid ecosystem."""
2
2
 
3
+ __version__ = "3.0.0a3"
4
+
3
5
  __all__ = [
4
6
  "PaynowClient",
5
7
  "PaynowProcessor",
@@ -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 PaymentStatusResponse
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
- headers={},
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 FSM.
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
- if payment_id:
172
- self.payment.external_id = payment_id
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
- if self.payment.may_trigger("confirm_payment"):
176
- self.payment.confirm_payment()
177
- with contextlib.suppress(MachineError):
178
- self.payment.mark_as_paid()
179
- else:
180
- logger.debug(
181
- "Cannot confirm payment %s (status: %s)",
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
- if hasattr(self.payment, "fail"):
192
- with contextlib.suppress(MachineError):
193
- self.payment.fail()
194
- else:
195
- logger.debug(
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) -> PaymentStatusResponse:
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
- status_map: dict[str, str | None] = {
210
- PaynowPaymentStatus.NEW: None,
211
- PaynowPaymentStatus.PENDING: "confirm_prepared",
212
- PaynowPaymentStatus.CONFIRMED: "confirm_payment",
213
- PaynowPaymentStatus.REJECTED: "fail",
214
- PaynowPaymentStatus.ERROR: "fail",
215
- PaynowPaymentStatus.EXPIRED: "fail",
216
- PaynowPaymentStatus.ABANDONED: "fail",
217
- }
218
-
219
- return PaymentStatusResponse(
220
- status=status_map.get(paynow_status),
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
- ) -> Decimal:
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
- self.payment.external_refund_id = refund_id
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 = getattr(self.payment, "external_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