payriff 0.1.0__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.
- payriff-0.1.0/.env.example +4 -0
- payriff-0.1.0/.github/workflows/ci.yml +44 -0
- payriff-0.1.0/.github/workflows/release.yml +17 -0
- payriff-0.1.0/.gitignore +14 -0
- payriff-0.1.0/CHANGELOG.md +15 -0
- payriff-0.1.0/LICENSE +21 -0
- payriff-0.1.0/PKG-INFO +354 -0
- payriff-0.1.0/README.md +331 -0
- payriff-0.1.0/pyproject.toml +62 -0
- payriff-0.1.0/src/payriff/__init__.py +50 -0
- payriff-0.1.0/src/payriff/_endpoints.py +208 -0
- payriff-0.1.0/src/payriff/_transport.py +39 -0
- payriff-0.1.0/src/payriff/aio.py +290 -0
- payriff-0.1.0/src/payriff/client.py +290 -0
- payriff-0.1.0/src/payriff/enums.py +152 -0
- payriff-0.1.0/src/payriff/errors.py +31 -0
- payriff-0.1.0/src/payriff/models.py +177 -0
- payriff-0.1.0/src/payriff/py.typed +0 -0
- payriff-0.1.0/tests/conftest.py +18 -0
- payriff-0.1.0/tests/test_async.py +59 -0
- payriff-0.1.0/tests/test_callbacks.py +40 -0
- payriff-0.1.0/tests/test_client_unit.py +344 -0
- payriff-0.1.0/tests/test_enums.py +68 -0
- payriff-0.1.0/tests/test_real_responses.py +163 -0
- payriff-0.1.0/tests/test_sandbox.py +74 -0
- payriff-0.1.0/uv.lock +614 -0
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
name: CI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [main]
|
|
6
|
+
pull_request:
|
|
7
|
+
|
|
8
|
+
concurrency:
|
|
9
|
+
group: ${{ github.workflow }}-${{ github.ref }}
|
|
10
|
+
cancel-in-progress: true
|
|
11
|
+
|
|
12
|
+
jobs:
|
|
13
|
+
test:
|
|
14
|
+
runs-on: ubuntu-latest
|
|
15
|
+
strategy:
|
|
16
|
+
fail-fast: false
|
|
17
|
+
matrix:
|
|
18
|
+
python: ["3.10", "3.11", "3.12", "3.13"]
|
|
19
|
+
steps:
|
|
20
|
+
- uses: actions/checkout@v5
|
|
21
|
+
- uses: astral-sh/setup-uv@v7
|
|
22
|
+
with:
|
|
23
|
+
enable-cache: true
|
|
24
|
+
python-version: ${{ matrix.python }}
|
|
25
|
+
- run: uv sync
|
|
26
|
+
- run: uv run ruff format --check .
|
|
27
|
+
- run: uv run ruff check .
|
|
28
|
+
- run: uv run mypy src
|
|
29
|
+
- run: uv run pytest -m "not sandbox" --cov --cov-report=term-missing
|
|
30
|
+
|
|
31
|
+
sandbox:
|
|
32
|
+
runs-on: ubuntu-latest
|
|
33
|
+
steps:
|
|
34
|
+
- uses: actions/checkout@v5
|
|
35
|
+
- uses: astral-sh/setup-uv@v7
|
|
36
|
+
with:
|
|
37
|
+
enable-cache: true
|
|
38
|
+
python-version: "3.12"
|
|
39
|
+
- run: uv sync
|
|
40
|
+
- name: Run against the Payriff sandbox
|
|
41
|
+
run: uv run pytest -m sandbox -v
|
|
42
|
+
env:
|
|
43
|
+
PAYRIFF_SECRET_KEY: ${{ secrets.PAYRIFF_SECRET_KEY }}
|
|
44
|
+
PAYRIFF_MERCHANT_ID: ${{ secrets.PAYRIFF_MERCHANT_ID }}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
name: Release
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
tags: ["v*"]
|
|
6
|
+
|
|
7
|
+
jobs:
|
|
8
|
+
publish:
|
|
9
|
+
runs-on: ubuntu-latest
|
|
10
|
+
environment: pypi
|
|
11
|
+
permissions:
|
|
12
|
+
id-token: write
|
|
13
|
+
steps:
|
|
14
|
+
- uses: actions/checkout@v5
|
|
15
|
+
- uses: astral-sh/setup-uv@v7
|
|
16
|
+
- run: uv build
|
|
17
|
+
- uses: pypa/gh-action-pypi-publish@release/v1
|
payriff-0.1.0/.gitignore
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## 0.1.0
|
|
4
|
+
|
|
5
|
+
First release.
|
|
6
|
+
|
|
7
|
+
- `PayriffClient` and `AsyncPayriffClient`, covering create order, pre-authorisation, capture,
|
|
8
|
+
refund, order lookup, auto pay, card save, wallet transfer, and invoice create and lookup.
|
|
9
|
+
- Enums for every value the docs define, plus the numeric lookups that go with currencies and
|
|
10
|
+
payment statuses.
|
|
11
|
+
- `Response.payment_status` and `Response.settled`, so you can tell a settled payment apart from
|
|
12
|
+
a call that was merely accepted.
|
|
13
|
+
- `request` as an escape hatch for endpoints that aren't typed yet.
|
|
14
|
+
- Typed throughout, with `httpx` as the only dependency.
|
|
15
|
+
- Sandbox tests that run when a secret key is available and skip when it isn't.
|
payriff-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 martian56
|
|
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.
|
payriff-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,354 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: payriff
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Python client for the Payriff payment gateway
|
|
5
|
+
Project-URL: Homepage, https://github.com/martian56/payriff
|
|
6
|
+
Project-URL: Repository, https://github.com/martian56/payriff
|
|
7
|
+
Author: martian56
|
|
8
|
+
License-Expression: MIT
|
|
9
|
+
License-File: LICENSE
|
|
10
|
+
Keywords: azerbaijan,gateway,payment,payriff,sdk
|
|
11
|
+
Classifier: Development Status :: 4 - Beta
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
18
|
+
Classifier: Topic :: Office/Business :: Financial
|
|
19
|
+
Classifier: Typing :: Typed
|
|
20
|
+
Requires-Python: >=3.10
|
|
21
|
+
Requires-Dist: httpx>=0.27
|
|
22
|
+
Description-Content-Type: text/markdown
|
|
23
|
+
|
|
24
|
+
# payriff
|
|
25
|
+
|
|
26
|
+
Python client for the [Payriff](https://payriff.com) payment gateway. Sync and async, typed
|
|
27
|
+
throughout, with `httpx` as the only dependency.
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
pip install payriff
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
## Quick start
|
|
34
|
+
|
|
35
|
+
```python
|
|
36
|
+
from payriff import PayriffClient
|
|
37
|
+
|
|
38
|
+
client = PayriffClient("your-secret-key")
|
|
39
|
+
|
|
40
|
+
order = client.create_order(
|
|
41
|
+
amount=25.50,
|
|
42
|
+
description="Order 1024",
|
|
43
|
+
callback_url="https://shop.example/payriff/callback",
|
|
44
|
+
)
|
|
45
|
+
print(order.payment_url)
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
Send the customer to `order.payment_url`. Payriff posts to your `callback_url` as the status
|
|
49
|
+
changes.
|
|
50
|
+
|
|
51
|
+
## A whole checkout
|
|
52
|
+
|
|
53
|
+
The shape of a real integration, here with Flask. Two routes: one to start the payment, one to
|
|
54
|
+
receive the result.
|
|
55
|
+
|
|
56
|
+
```python
|
|
57
|
+
from flask import Flask, jsonify, redirect, request
|
|
58
|
+
|
|
59
|
+
from payriff import GatewayError, PayriffClient
|
|
60
|
+
|
|
61
|
+
app = Flask(__name__)
|
|
62
|
+
client = PayriffClient.from_env()
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
@app.post("/checkout/<cart_id>")
|
|
66
|
+
def checkout(cart_id):
|
|
67
|
+
cart = load_cart(cart_id)
|
|
68
|
+
try:
|
|
69
|
+
order = client.create_order(
|
|
70
|
+
amount=cart.total,
|
|
71
|
+
description=f"Order {cart_id}",
|
|
72
|
+
callback_url="https://shop.example/payriff/callback",
|
|
73
|
+
)
|
|
74
|
+
except GatewayError as exc:
|
|
75
|
+
app.logger.warning("payriff refused the order: %s %s", exc.code, exc.message)
|
|
76
|
+
return jsonify(error="could not start the payment"), 502
|
|
77
|
+
|
|
78
|
+
cart.payriff_order_id = order.order_id
|
|
79
|
+
cart.save()
|
|
80
|
+
return redirect(order.payment_url)
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
@app.post("/payriff/callback")
|
|
84
|
+
def callback():
|
|
85
|
+
event = client.parse_callback(request.get_json(force=True))
|
|
86
|
+
|
|
87
|
+
# The callback is unsigned, so nothing here is trusted until the API agrees.
|
|
88
|
+
order = client.get_order(event.order_id)
|
|
89
|
+
cart = load_cart_by_order(event.order_id)
|
|
90
|
+
|
|
91
|
+
if order.settled:
|
|
92
|
+
cart.mark_paid()
|
|
93
|
+
elif order.payment_status in {"DECLINED", "CANCELED"}:
|
|
94
|
+
cart.mark_failed(order.payment_status)
|
|
95
|
+
|
|
96
|
+
return "", 200
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
Two details worth copying. The callback handler decides on `order`, the value it fetched, never
|
|
100
|
+
on `event`, the value it was sent. And it answers `200` whatever the outcome, because a non-2xx
|
|
101
|
+
tells Payriff to deliver the same event again.
|
|
102
|
+
|
|
103
|
+
Callbacks can arrive more than once for one order, so make `mark_paid` idempotent.
|
|
104
|
+
|
|
105
|
+
## Two things that catch people out
|
|
106
|
+
|
|
107
|
+
### A success code doesn't mean the payment worked
|
|
108
|
+
|
|
109
|
+
The `code` field tells you whether Payriff accepted the call. Whether money actually moved is a
|
|
110
|
+
separate field, `paymentStatus`. `auto_pay` is where this hurts most: a declined card still comes
|
|
111
|
+
back as `00000`.
|
|
112
|
+
|
|
113
|
+
```python
|
|
114
|
+
charge = client.auto_pay(card_uuid, 10.00, "Subscription")
|
|
115
|
+
|
|
116
|
+
charge.ok # True
|
|
117
|
+
charge.payment_status # "CANCELED"
|
|
118
|
+
charge.settled # False
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
Gate your fulfilment on `settled`.
|
|
122
|
+
|
|
123
|
+
A hold is different again. A genuine pre-authorisation reports `PREAUTH_APPROVED`, and that is
|
|
124
|
+
the only status where money is reserved rather than taken. `settled` stays `False` for it and
|
|
125
|
+
`authorized` turns `True`.
|
|
126
|
+
|
|
127
|
+
```python
|
|
128
|
+
hold = client.get_order(order_id)
|
|
129
|
+
hold.authorized # True, the funds are reserved
|
|
130
|
+
hold.settled # False, nothing has moved yet
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
Watch out for accounts without pre-authorisation enabled. There, an order sent with
|
|
134
|
+
`operation=PRE_AUTH` is charged like an ordinary sale and comes back `APPROVED`, which `settled`
|
|
135
|
+
correctly reports as paid. Check `authorized`, not the operation you asked for.
|
|
136
|
+
|
|
137
|
+
### Callbacks aren't signed
|
|
138
|
+
|
|
139
|
+
There is no HMAC and no shared secret in the body, so there's nothing to verify. Anyone who
|
|
140
|
+
learns your callback URL can post to it. Treat the callback as a nudge to go ask the API what
|
|
141
|
+
happened:
|
|
142
|
+
|
|
143
|
+
```python
|
|
144
|
+
callback = client.parse_callback(request.json)
|
|
145
|
+
order = client.get_order(callback.order_id)
|
|
146
|
+
|
|
147
|
+
if order.settled:
|
|
148
|
+
fulfil(callback.order_id)
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
`parse_callback` reads the body and nothing more. The `get_order` call is what makes it safe.
|
|
152
|
+
|
|
153
|
+
## Configuration
|
|
154
|
+
|
|
155
|
+
```python
|
|
156
|
+
client = PayriffClient(
|
|
157
|
+
"your-secret-key",
|
|
158
|
+
merchant_id="ES100000",
|
|
159
|
+
currency="AZN",
|
|
160
|
+
language="EN",
|
|
161
|
+
callback_url="https://shop.example/payriff/callback",
|
|
162
|
+
)
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
`PayriffClient.from_env()` reads `PAYRIFF_SECRET_KEY`, `PAYRIFF_MERCHANT_ID`, `PAYRIFF_BASE_URL`
|
|
166
|
+
and `PAYRIFF_CALLBACK_URL` instead.
|
|
167
|
+
|
|
168
|
+
The secret goes into the `Authorization` header raw, with no `Bearer` prefix. The client handles
|
|
169
|
+
that for you.
|
|
170
|
+
|
|
171
|
+
## Async
|
|
172
|
+
|
|
173
|
+
Every method has an awaitable twin:
|
|
174
|
+
|
|
175
|
+
```python
|
|
176
|
+
from payriff import AsyncPayriffClient
|
|
177
|
+
|
|
178
|
+
async with AsyncPayriffClient("your-secret-key") as client:
|
|
179
|
+
order = await client.create_order(25.50, "Order 1024")
|
|
180
|
+
status = await client.get_order(order.order_id)
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
## Methods
|
|
184
|
+
|
|
185
|
+
| Method | Endpoint |
|
|
186
|
+
|---|---|
|
|
187
|
+
| `create_order` | `POST /api/v3/orders` |
|
|
188
|
+
| `reserve` | `POST /api/v3/orders` with `operation=PRE_AUTH` |
|
|
189
|
+
| `get_order` | `GET /api/v3/orders/{id}` |
|
|
190
|
+
| `complete` | `POST /api/v3/complete` |
|
|
191
|
+
| `refund` | `POST /api/v3/refund` |
|
|
192
|
+
| `auto_pay` | `POST /api/v3/autoPay` |
|
|
193
|
+
| `save_card`, `refund_card_save` | the two step card save flow |
|
|
194
|
+
| `transfer` | `POST /api/v3/payout` |
|
|
195
|
+
| `create_invoice` | `POST /api/v2/invoices` |
|
|
196
|
+
| `get_invoice` | `POST /api/v2/get-invoice` |
|
|
197
|
+
|
|
198
|
+
A couple of things worth knowing. The docs describe order information as a `POST`, but the API
|
|
199
|
+
answers that with a 405 and only takes `GET`, which is what this does. And `/api/v3/payout` moves
|
|
200
|
+
money between Payriff merchant wallets, so `transfer` seemed the honest name for it; if you were
|
|
201
|
+
hoping for a bank payout, this isn't it.
|
|
202
|
+
|
|
203
|
+
The v2 invoice endpoints want `merchant` set and refuse the call without it. The v3 order
|
|
204
|
+
endpoints do not care, so set `merchant_id` on the client if you touch invoices.
|
|
205
|
+
|
|
206
|
+
Bulk invoice and bulk payout are dashboard features driven by spreadsheet upload. There is no API
|
|
207
|
+
behind them, so there's nothing here for them either.
|
|
208
|
+
|
|
209
|
+
For anything else, go straight at it:
|
|
210
|
+
|
|
211
|
+
```python
|
|
212
|
+
client.request("POST", "/api/v3/directPay", {...})
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
## Pre-authorisation
|
|
216
|
+
|
|
217
|
+
`reserve` puts a hold on the card instead of charging it. You have 30 days to capture with
|
|
218
|
+
`complete`, after which the hold expires on its own. Capture less than you held and Payriff
|
|
219
|
+
releases the difference.
|
|
220
|
+
|
|
221
|
+
```python
|
|
222
|
+
hold = client.reserve(100.00, "Hotel booking", three_ds=True)
|
|
223
|
+
client.complete(hold.order_id, 80.00)
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
## Saving a card
|
|
227
|
+
|
|
228
|
+
To verify a card, Payriff charges 0.01 AZN and expects you to hand it straight back. Skipping the
|
|
229
|
+
refund leaves the customer out of pocket, so treat it as step two rather than tidying up:
|
|
230
|
+
|
|
231
|
+
```python
|
|
232
|
+
verification = client.save_card(callback_url="https://shop.example/payriff/callback")
|
|
233
|
+
# the customer completes verification.payment_url, then
|
|
234
|
+
client.refund_card_save(verification.order_id)
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
The `cardUuid` you need for `auto_pay` arrives in the callback.
|
|
238
|
+
|
|
239
|
+
## Responses
|
|
240
|
+
|
|
241
|
+
Branch on `code` rather than the HTTP status. The interesting fields live under `payload`, and
|
|
242
|
+
`Response` reads through to them:
|
|
243
|
+
|
|
244
|
+
```python
|
|
245
|
+
order.code # "00000"
|
|
246
|
+
order.code_name # "SUCCESS"
|
|
247
|
+
order.ok # the call was accepted
|
|
248
|
+
order.settled # money actually moved
|
|
249
|
+
order.authorized # funds held by a pre-authorisation, not yet captured
|
|
250
|
+
order.operation_type # "PURCHASE", "PRE_AUTH", ...
|
|
251
|
+
order.order_id, order.session_id, order.transaction_id, order.payment_url
|
|
252
|
+
order.payment_status # "PAID", "CANCELED", ...
|
|
253
|
+
order.transactions # per attempt: card mask, RRN, channel
|
|
254
|
+
order.payload # raw payload, occasionally a plain string such as "APPROVED"
|
|
255
|
+
order.raw # untouched body
|
|
256
|
+
```
|
|
257
|
+
|
|
258
|
+
## Enums
|
|
259
|
+
|
|
260
|
+
Values are case sensitive and always go over the wire as strings.
|
|
261
|
+
|
|
262
|
+
```python
|
|
263
|
+
from payriff import Currency, Language, PaymentStatus
|
|
264
|
+
|
|
265
|
+
client.create_order(10, "x", currency=Currency.USD, language=Language.AZ)
|
|
266
|
+
|
|
267
|
+
if order.payment_status == PaymentStatus.APPROVED:
|
|
268
|
+
...
|
|
269
|
+
```
|
|
270
|
+
|
|
271
|
+
| Enum | Values |
|
|
272
|
+
|---|---|
|
|
273
|
+
| `Operation` | PURCHASE, PRE_AUTH, COMPLETE, REFUND, REVERSE |
|
|
274
|
+
| `Language` | AZ, EN, RU, AR |
|
|
275
|
+
| `Currency` | AZN, USD, EUR, PKR, AED, SAR |
|
|
276
|
+
| `PaymentType` | ONETIME, DAILY, WEEKLY, MONTHLY, ANNUALLY |
|
|
277
|
+
| `InstallmentProduct` | BIRKART, ALBALI, BOLKART, TAMKART |
|
|
278
|
+
| `InvoiceStatus` | PENDING, ERROR, EXPIRED, PARTIAL, COMPLETE, CASH |
|
|
279
|
+
| `PaymentStatus` | CREATED, APPROVED, CANCELED, DECLINED, REFUNDED, PREAUTH_APPROVED, EXPIRED, REVERSE, PARTIAL_REFUND, PARTIAL, ACCEPTED, REFUND_IN_PROGRESS, CASH, PENDING, PREAUTH_EXPIRED |
|
|
280
|
+
| `ResultCode` | SUCCESS, WARNING, ERROR, INVALID_PARAMETERS, UNAUTHORIZED, TOKEN_NOT_PRESENT, INVALID_TOKEN, INVALID_ORIGIN, CHECKING |
|
|
281
|
+
| `GatewayResult` | 00, APPROVED, PREAUTH-APPROVED |
|
|
282
|
+
|
|
283
|
+
Members hash by value, so a raw string off the wire and an enum member reach the same dict entry.
|
|
284
|
+
`CURRENCY_NUMERIC` and `PAYMENT_STATUS_CODES` give you the ISO 4217 and internal numeric codes.
|
|
285
|
+
|
|
286
|
+
`PaymentStatus` carries two extras, `PAID` and `COMPLETED`. Payriff's enum reference leaves them
|
|
287
|
+
out, but the order information and autoPay examples both return them, so they are in here.
|
|
288
|
+
`SETTLED_PAYMENT_STATUSES` and `HELD_PAYMENT_STATUSES` are the sets behind `settled` and
|
|
289
|
+
`authorized`.
|
|
290
|
+
|
|
291
|
+
## Errors
|
|
292
|
+
|
|
293
|
+
`GatewayError` means Payriff refused the call: any result code other than `00000` or `01000`, or
|
|
294
|
+
a `4xx` that still carried an envelope. It gives you `code`, `message`, `internal_message` and
|
|
295
|
+
`response_id`.
|
|
296
|
+
|
|
297
|
+
`TransportError` covers the rest, so network failures and responses that either weren't JSON or
|
|
298
|
+
carried no envelope at all.
|
|
299
|
+
|
|
300
|
+
Both subclass `PayriffError` if you want to catch broadly.
|
|
301
|
+
|
|
302
|
+
```python
|
|
303
|
+
from payriff import GatewayError, PayriffError, TransportError
|
|
304
|
+
|
|
305
|
+
try:
|
|
306
|
+
order = client.create_order(25.50, "Order 1024")
|
|
307
|
+
except GatewayError as exc:
|
|
308
|
+
# Payriff answered and said no. exc.code tells you why.
|
|
309
|
+
log.warning("refused: %s %s", exc.code, exc.message)
|
|
310
|
+
except TransportError as exc:
|
|
311
|
+
# Nothing came back. Safe to retry.
|
|
312
|
+
log.error("payriff unreachable: %s", exc)
|
|
313
|
+
```
|
|
314
|
+
|
|
315
|
+
Retry `TransportError` if you like, but not `GatewayError`: the gateway already made up its mind
|
|
316
|
+
and the same call will be refused again.
|
|
317
|
+
|
|
318
|
+
The `01000` code is a bit odd. On a `2xx` it's a warning and comes back as a normal response. On
|
|
319
|
+
a `4xx` it's a refusal and raises, which is how Payriff answers an unknown application key.
|
|
320
|
+
|
|
321
|
+
## Testing
|
|
322
|
+
|
|
323
|
+
Payriff runs a sandbox. Point `PAYRIFF_BASE_URL` at it, use a sandbox key, and pay with the
|
|
324
|
+
published test cards:
|
|
325
|
+
|
|
326
|
+
| Brand | Number | Expiry | CVV | OTP |
|
|
327
|
+
|---|---|---|---|---|
|
|
328
|
+
| Visa | 4000007546012078 | 04/29 | 893 | 123456 |
|
|
329
|
+
| Mastercard | 5100007346013947 | 04/29 | 783 | 123456 |
|
|
330
|
+
|
|
331
|
+
`tests/test_sandbox.py` runs when `PAYRIFF_SECRET_KEY` is set and skips when it isn't, so a
|
|
332
|
+
checkout without a key still passes.
|
|
333
|
+
|
|
334
|
+
## What has been checked against the live gateway
|
|
335
|
+
|
|
336
|
+
Payriff's docs turned out to disagree with the API in a few places, so the flows below were run
|
|
337
|
+
against a real account and the parsing is built from the recorded bodies.
|
|
338
|
+
|
|
339
|
+
| Verified | Inferred from the docs only |
|
|
340
|
+
|---|---|
|
|
341
|
+
| create order, and the CREATED to PENDING to APPROVED or DECLINED lifecycle | complete, a capture |
|
|
342
|
+
| that a pre-auth order settles outright on an account without pre-auth | auto pay |
|
|
343
|
+
| order lookup, which is a GET despite the docs saying POST | card save and its refund |
|
|
344
|
+
| refund, which answers with a null payload, on both a purchase and a pre-auth | invoice create and lookup |
|
|
345
|
+
| the callback envelope, for a declined purchase | wallet transfer |
|
|
346
|
+
| result codes 15400, 15000 and 01000, and the 4xx behaviour | |
|
|
347
|
+
|
|
348
|
+
Pre-authorisation, autopay and invoicing are not switched on for the account used, so those
|
|
349
|
+
paths could not be exercised. Treat the right hand column as untested and please report anything
|
|
350
|
+
that looks wrong.
|
|
351
|
+
|
|
352
|
+
## Licence
|
|
353
|
+
|
|
354
|
+
MIT. Not affiliated with Payriff.
|