python-myanmar-payments 4.0.0a1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- python_myanmar_payments/__init__.py +111 -0
- python_myanmar_payments/_amount.py +178 -0
- python_myanmar_payments/_cache.py +80 -0
- python_myanmar_payments/_callback.py +188 -0
- python_myanmar_payments/_errors.py +94 -0
- python_myanmar_payments/_facade.py +271 -0
- python_myanmar_payments/_http.py +201 -0
- python_myanmar_payments/_json.py +94 -0
- python_myanmar_payments/_results.py +265 -0
- python_myanmar_payments/_status.py +66 -0
- python_myanmar_payments/_support.py +172 -0
- python_myanmar_payments/_validate.py +157 -0
- python_myanmar_payments/_values.py +59 -0
- python_myanmar_payments/_version.py +1 -0
- python_myanmar_payments/aya_pay.py +509 -0
- python_myanmar_payments/cyber_source.py +310 -0
- python_myanmar_payments/kbz_pay.py +556 -0
- python_myanmar_payments/py.typed +0 -0
- python_myanmar_payments/wave_money.py +431 -0
- python_myanmar_payments/yoma_mmqr.py +526 -0
- python_myanmar_payments-4.0.0a1.dist-info/METADATA +43 -0
- python_myanmar_payments-4.0.0a1.dist-info/RECORD +24 -0
- python_myanmar_payments-4.0.0a1.dist-info/WHEEL +4 -0
- python_myanmar_payments-4.0.0a1.dist-info/licenses/LICENSE.md +21 -0
|
@@ -0,0 +1,431 @@
|
|
|
1
|
+
"""Wave Money (WavePay payment gateway): redirect payments and callbacks."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from collections.abc import Mapping, Sequence
|
|
6
|
+
from dataclasses import dataclass, field
|
|
7
|
+
from typing import Any, ClassVar
|
|
8
|
+
|
|
9
|
+
import httpx
|
|
10
|
+
|
|
11
|
+
from ._amount import Amount, AmountInput, to_amount
|
|
12
|
+
from ._callback import CallbackRequest, lossless_body
|
|
13
|
+
from ._errors import ApiError, SignatureVerificationError
|
|
14
|
+
from ._http import (
|
|
15
|
+
DEFAULT_TIMEOUT,
|
|
16
|
+
AsyncGateway,
|
|
17
|
+
GatewayResponse,
|
|
18
|
+
HttpRequest,
|
|
19
|
+
SyncGateway,
|
|
20
|
+
form_request,
|
|
21
|
+
)
|
|
22
|
+
from ._json import LosslessObject, dumps, to_plain_object
|
|
23
|
+
from ._results import PaymentCallback, RedirectPayment
|
|
24
|
+
from ._status import PaymentStatus, resolve_status
|
|
25
|
+
from ._support import (
|
|
26
|
+
EnvSource,
|
|
27
|
+
config_of,
|
|
28
|
+
default_env,
|
|
29
|
+
env_first,
|
|
30
|
+
env_int,
|
|
31
|
+
env_sandbox,
|
|
32
|
+
hmac_sha256_hex,
|
|
33
|
+
optional_setting,
|
|
34
|
+
query_escape,
|
|
35
|
+
random_hex,
|
|
36
|
+
require_setting,
|
|
37
|
+
safe_equal,
|
|
38
|
+
sandbox_flag,
|
|
39
|
+
trim_url,
|
|
40
|
+
)
|
|
41
|
+
from ._validate import AmountRule, Validator
|
|
42
|
+
from ._values import get, is_nested, optional, scalar_string, trimmed
|
|
43
|
+
|
|
44
|
+
__all__ = [
|
|
45
|
+
"AsyncWaveMoney",
|
|
46
|
+
"WaveMoney",
|
|
47
|
+
"WaveMoneyConfig",
|
|
48
|
+
"WaveMoneyItem",
|
|
49
|
+
"WaveMoneyPaymentData",
|
|
50
|
+
]
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
class WaveMoneyConfig:
|
|
54
|
+
"""Wave Money credentials and endpoints.
|
|
55
|
+
|
|
56
|
+
A missing credential raises a :class:`~python_myanmar_payments.ConfigurationError`.
|
|
57
|
+
"""
|
|
58
|
+
|
|
59
|
+
SANDBOX_URL: ClassVar[str] = "https://preprodpayments.wavemoney.io:8107"
|
|
60
|
+
PRODUCTION_URL: ClassVar[str] = "https://payments.wavemoney.io"
|
|
61
|
+
SANDBOX_AUTHENTICATE_URL: ClassVar[str] = "https://preprodpayments.wavemoney.io"
|
|
62
|
+
PRODUCTION_AUTHENTICATE_URL: ClassVar[str] = "https://payments.wavemoney.io"
|
|
63
|
+
DEFAULT_TIME_TO_LIVE_SECONDS: ClassVar[int] = 300
|
|
64
|
+
|
|
65
|
+
merchant_id: str
|
|
66
|
+
"""The merchant ID Wave issued."""
|
|
67
|
+
secret_key: str
|
|
68
|
+
"""The hash secret key Wave issued."""
|
|
69
|
+
merchant_name: str
|
|
70
|
+
"""Your business name, shown on Wave's payment page."""
|
|
71
|
+
time_to_live_seconds: int
|
|
72
|
+
"""Seconds the customer has to pay."""
|
|
73
|
+
sandbox: bool
|
|
74
|
+
"""Whether the test environment is used."""
|
|
75
|
+
base_url: str
|
|
76
|
+
"""The API base URL in use."""
|
|
77
|
+
authenticate_url: str
|
|
78
|
+
"""The host the customer is redirected to. Wave serves it without the API port."""
|
|
79
|
+
|
|
80
|
+
def __init__(
|
|
81
|
+
self,
|
|
82
|
+
*,
|
|
83
|
+
merchant_id: str = "",
|
|
84
|
+
secret_key: str = "",
|
|
85
|
+
merchant_name: str = "",
|
|
86
|
+
time_to_live_seconds: int | None = None,
|
|
87
|
+
sandbox: bool | str = True,
|
|
88
|
+
base_url: str | None = None,
|
|
89
|
+
authenticate_url: str | None = None,
|
|
90
|
+
) -> None:
|
|
91
|
+
self.merchant_id = require_setting("wave_money", "merchant_id", merchant_id)
|
|
92
|
+
self.secret_key = require_setting("wave_money", "secret_key", secret_key)
|
|
93
|
+
self.merchant_name = require_setting("wave_money", "merchant_name", merchant_name)
|
|
94
|
+
ttl = time_to_live_seconds
|
|
95
|
+
valid = isinstance(ttl, int) and not isinstance(ttl, bool) and ttl > 0
|
|
96
|
+
self.time_to_live_seconds = (
|
|
97
|
+
ttl if valid and ttl is not None else self.DEFAULT_TIME_TO_LIVE_SECONDS
|
|
98
|
+
)
|
|
99
|
+
self.sandbox = sandbox = sandbox_flag(sandbox)
|
|
100
|
+
self.base_url = trim_url(
|
|
101
|
+
optional_setting(base_url) or (self.SANDBOX_URL if sandbox else self.PRODUCTION_URL)
|
|
102
|
+
)
|
|
103
|
+
self.authenticate_url = trim_url(
|
|
104
|
+
optional_setting(authenticate_url)
|
|
105
|
+
or (self.SANDBOX_AUTHENTICATE_URL if sandbox else self.PRODUCTION_AUTHENTICATE_URL)
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
@classmethod
|
|
109
|
+
def from_env(cls, env: EnvSource | None = None) -> WaveMoneyConfig:
|
|
110
|
+
"""Reads the ``WAVE_MONEY_*`` environment variables.
|
|
111
|
+
|
|
112
|
+
``WAVE_MONEY_MERCHANT_ID``, ``WAVE_MONEY_SECRET_KEY``,
|
|
113
|
+
``WAVE_MONEY_MERCHANT_NAME`` (falling back to ``APP_NAME``),
|
|
114
|
+
``WAVE_MONEY_TIME_TO_LIVE_IN_SECONDS``, ``WAVE_MONEY_SANDBOX``,
|
|
115
|
+
``WAVE_MONEY_BASE_URL`` and ``WAVE_MONEY_AUTHENTICATE_URL``. Defaults to
|
|
116
|
+
``os.environ``.
|
|
117
|
+
"""
|
|
118
|
+
env = default_env() if env is None else env
|
|
119
|
+
return cls(
|
|
120
|
+
merchant_id=env_first(env, "WAVE_MONEY_MERCHANT_ID"),
|
|
121
|
+
secret_key=env_first(env, "WAVE_MONEY_SECRET_KEY"),
|
|
122
|
+
merchant_name=env_first(env, "WAVE_MONEY_MERCHANT_NAME", "APP_NAME"),
|
|
123
|
+
time_to_live_seconds=env_int(env, "WAVE_MONEY_TIME_TO_LIVE_IN_SECONDS"),
|
|
124
|
+
sandbox=env_sandbox(env, "WAVE_MONEY_SANDBOX"),
|
|
125
|
+
base_url=env_first(env, "WAVE_MONEY_BASE_URL"),
|
|
126
|
+
authenticate_url=env_first(env, "WAVE_MONEY_AUTHENTICATE_URL"),
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
def __repr__(self) -> str:
|
|
130
|
+
return f"WaveMoneyConfig(merchant_id={self.merchant_id!r}, sandbox={self.sandbox!r})"
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
@dataclass(frozen=True)
|
|
134
|
+
class WaveMoneyItem:
|
|
135
|
+
"""A line item shown on Wave's payment page."""
|
|
136
|
+
|
|
137
|
+
name: str
|
|
138
|
+
"""The item name."""
|
|
139
|
+
amount: AmountInput
|
|
140
|
+
"""The item amount in whole kyat, e.g. ``Amount.kyat(1000)`` or ``1000``."""
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
@dataclass(kw_only=True)
|
|
144
|
+
class WaveMoneyPaymentData:
|
|
145
|
+
"""A Wave Money payment request. Wave only accepts whole kyat (MMK)."""
|
|
146
|
+
|
|
147
|
+
order_id: str
|
|
148
|
+
"""Your order ID. One order can have several payment attempts."""
|
|
149
|
+
callback_url: str
|
|
150
|
+
"""The URL Wave posts the result to (``backend_result_url``)."""
|
|
151
|
+
return_url: str
|
|
152
|
+
"""Where Wave sends the customer back (``frontend_result_url``). Not proof of
|
|
153
|
+
payment."""
|
|
154
|
+
description: str
|
|
155
|
+
"""Shown to the customer."""
|
|
156
|
+
items: Sequence[WaveMoneyItem] = field(default_factory=list)
|
|
157
|
+
"""The line items shown on Wave's page; at least one."""
|
|
158
|
+
amount: AmountInput | None = None
|
|
159
|
+
"""The total in whole kyat. Leave it unset to charge the sum of the items."""
|
|
160
|
+
merchant_reference_id: str | None = None
|
|
161
|
+
"""The unique ID of this attempt; Wave rejects a reused one.
|
|
162
|
+
|
|
163
|
+
``initiate()`` fills it with a random ID when empty, so store it afterwards:
|
|
164
|
+
Wave's callback may omit ``orderId`` but always carries this."""
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
_STATUSES: Mapping[str, PaymentStatus] = {
|
|
168
|
+
"PAYMENT_CONFIRMED": PaymentStatus.SUCCESSFUL,
|
|
169
|
+
"INSUFFICIENT_BALANCE": PaymentStatus.PENDING,
|
|
170
|
+
"ACCOUNT_LOCKED": PaymentStatus.FAILED,
|
|
171
|
+
"BILL_COLLECTION_FAILED": PaymentStatus.FAILED,
|
|
172
|
+
"PAYMENT_REQUEST_CANCELLED": PaymentStatus.CANCELED,
|
|
173
|
+
"TRANSACTION_TIMED_OUT": PaymentStatus.EXPIRED,
|
|
174
|
+
"SCHEDULER_TRANSACTION_TIMED_OUT": PaymentStatus.EXPIRED,
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
_CALLBACK_FIELDS = (
|
|
178
|
+
"status",
|
|
179
|
+
"timeToLiveSeconds",
|
|
180
|
+
"merchantId",
|
|
181
|
+
"orderId",
|
|
182
|
+
"amount",
|
|
183
|
+
"backendResultUrl",
|
|
184
|
+
"merchantReferenceId",
|
|
185
|
+
"initiatorMsisdn",
|
|
186
|
+
"transactionId",
|
|
187
|
+
"paymentRequestId",
|
|
188
|
+
"requestTime",
|
|
189
|
+
)
|
|
190
|
+
|
|
191
|
+
_RULE = AmountRule("Wave Money", 0)
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
class _WaveMoneyBase:
|
|
195
|
+
"""What the sync and async Wave Money clients share."""
|
|
196
|
+
|
|
197
|
+
config: WaveMoneyConfig
|
|
198
|
+
"""The configuration in use."""
|
|
199
|
+
|
|
200
|
+
def __init__(self, config: WaveMoneyConfig | Mapping[str, Any]) -> None:
|
|
201
|
+
self.config = config_of(WaveMoneyConfig, config)
|
|
202
|
+
|
|
203
|
+
@staticmethod
|
|
204
|
+
def resolved_amount(data: WaveMoneyPaymentData) -> Amount | None:
|
|
205
|
+
"""The total that will be charged: ``amount``, or the exact sum of the items.
|
|
206
|
+
|
|
207
|
+
``None`` when an item amount is missing, malformed or has decimals.
|
|
208
|
+
"""
|
|
209
|
+
if data.amount is not None:
|
|
210
|
+
return to_amount(data.amount)
|
|
211
|
+
items = data.items
|
|
212
|
+
if not isinstance(items, (list, tuple)) or len(items) == 0:
|
|
213
|
+
return None
|
|
214
|
+
total = 0
|
|
215
|
+
for item in items:
|
|
216
|
+
amount = to_amount(getattr(item, "amount", None))
|
|
217
|
+
if amount is None or amount.decimal_places() > 0:
|
|
218
|
+
return None
|
|
219
|
+
total += int(str(amount))
|
|
220
|
+
return Amount.kyat(total)
|
|
221
|
+
|
|
222
|
+
@classmethod
|
|
223
|
+
def validate(cls, data: WaveMoneyPaymentData) -> None:
|
|
224
|
+
"""Checks the request against Wave's documented rules.
|
|
225
|
+
|
|
226
|
+
Raises an :class:`~python_myanmar_payments.InvalidPaymentDataError`.
|
|
227
|
+
"""
|
|
228
|
+
items = list(data.items) if isinstance(data.items, (list, tuple)) else []
|
|
229
|
+
validator = (
|
|
230
|
+
Validator()
|
|
231
|
+
.required("order_id", data.order_id)
|
|
232
|
+
.required("callback_url", data.callback_url)
|
|
233
|
+
.url("callback_url", data.callback_url)
|
|
234
|
+
.required("return_url", data.return_url)
|
|
235
|
+
.url("return_url", data.return_url)
|
|
236
|
+
.required("description", data.description)
|
|
237
|
+
.when(len(items) == 0, "items", "The items field must have at least one item.")
|
|
238
|
+
.string("merchant_reference_id", data.merchant_reference_id)
|
|
239
|
+
)
|
|
240
|
+
for index, item in enumerate(items):
|
|
241
|
+
validator.required(f"items.{index}.name", getattr(item, "name", None)).amount(
|
|
242
|
+
f"items.{index}.amount", getattr(item, "amount", None), _RULE
|
|
243
|
+
)
|
|
244
|
+
resolved = cls.resolved_amount(data)
|
|
245
|
+
if not (len(items) > 0 and data.amount is None and resolved is None):
|
|
246
|
+
validator.amount("amount", data.amount if data.amount is not None else resolved, _RULE)
|
|
247
|
+
validator.validate()
|
|
248
|
+
|
|
249
|
+
def handle_callback(self, request: CallbackRequest) -> PaymentCallback:
|
|
250
|
+
"""Verifies Wave's callback. Only ``PAYMENT_CONFIRMED`` means the customer paid.
|
|
251
|
+
|
|
252
|
+
``order_id`` falls back to ``merchantReferenceId`` when ``orderId`` is
|
|
253
|
+
missing, null or empty, because Wave marks ``orderId`` as optional.
|
|
254
|
+
"""
|
|
255
|
+
payload = lossless_body(request)
|
|
256
|
+
nested = any(is_nested(payload.get(name)) for name in _CALLBACK_FIELDS)
|
|
257
|
+
parts = []
|
|
258
|
+
for name in _CALLBACK_FIELDS:
|
|
259
|
+
text = scalar_string(payload.get(name))
|
|
260
|
+
parts.append("null" if text is None else text)
|
|
261
|
+
expected = self._hash(parts)
|
|
262
|
+
hash_value = payload.get("hashValue")
|
|
263
|
+
|
|
264
|
+
if (
|
|
265
|
+
nested
|
|
266
|
+
or not isinstance(hash_value, str)
|
|
267
|
+
or not safe_equal(expected, hash_value.lower())
|
|
268
|
+
):
|
|
269
|
+
raise SignatureVerificationError(
|
|
270
|
+
"Wave Money callback hash verification failed.", to_plain_object(payload)
|
|
271
|
+
)
|
|
272
|
+
|
|
273
|
+
gateway_status = trimmed(payload, "status")
|
|
274
|
+
return PaymentCallback(
|
|
275
|
+
order_id=get(payload, "orderId") or get(payload, "merchantReferenceId"),
|
|
276
|
+
status=resolve_status(_STATUSES, gateway_status),
|
|
277
|
+
gateway_status=gateway_status,
|
|
278
|
+
gateway_reference=optional(payload, "transactionId"),
|
|
279
|
+
amount=optional(payload, "amount"),
|
|
280
|
+
raw=to_plain_object(payload),
|
|
281
|
+
)
|
|
282
|
+
|
|
283
|
+
def _initiate_request(self, data: WaveMoneyPaymentData) -> HttpRequest:
|
|
284
|
+
self.validate(data)
|
|
285
|
+
if not data.merchant_reference_id:
|
|
286
|
+
data.merchant_reference_id = random_hex()
|
|
287
|
+
|
|
288
|
+
amount = str(self.resolved_amount(data))
|
|
289
|
+
ttl = str(self.config.time_to_live_seconds)
|
|
290
|
+
# Wave documents items as [{"name": "...", "amount": 1000}] with a numeric
|
|
291
|
+
# amount; the amount text is written as a JSON number without a float.
|
|
292
|
+
items = ",".join(
|
|
293
|
+
f'{{"name":{dumps(item.name)},"amount":{to_amount(item.amount)}}}'
|
|
294
|
+
for item in data.items
|
|
295
|
+
)
|
|
296
|
+
return form_request(
|
|
297
|
+
f"{self.config.base_url}/payment",
|
|
298
|
+
{
|
|
299
|
+
"time_to_live_in_seconds": ttl,
|
|
300
|
+
"merchant_id": self.config.merchant_id,
|
|
301
|
+
"order_id": data.order_id,
|
|
302
|
+
"merchant_reference_id": data.merchant_reference_id,
|
|
303
|
+
"frontend_result_url": data.return_url,
|
|
304
|
+
"backend_result_url": data.callback_url,
|
|
305
|
+
"amount": amount,
|
|
306
|
+
"payment_description": data.description,
|
|
307
|
+
"merchant_name": self.config.merchant_name,
|
|
308
|
+
"items": f"[{items}]",
|
|
309
|
+
"hash": self._hash(
|
|
310
|
+
[
|
|
311
|
+
ttl,
|
|
312
|
+
self.config.merchant_id,
|
|
313
|
+
data.order_id,
|
|
314
|
+
amount,
|
|
315
|
+
data.callback_url,
|
|
316
|
+
data.merchant_reference_id,
|
|
317
|
+
]
|
|
318
|
+
),
|
|
319
|
+
},
|
|
320
|
+
)
|
|
321
|
+
|
|
322
|
+
def _redirect(self, data: WaveMoneyPaymentData, response: GatewayResponse) -> RedirectPayment:
|
|
323
|
+
body = response.json()
|
|
324
|
+
transaction_id = get(body, "transaction_id")
|
|
325
|
+
if not response.successful() or get(body, "message") != "success" or transaction_id == "":
|
|
326
|
+
message = _error_message(body)
|
|
327
|
+
raise ApiError(
|
|
328
|
+
f"Wave Money payment request failed with HTTP {response.status}: {message}",
|
|
329
|
+
gateway_code="VALIDATION_ERROR" if "errors" in body else optional(body, "message"),
|
|
330
|
+
gateway_message=message,
|
|
331
|
+
http_status=response.status,
|
|
332
|
+
raw=to_plain_object(body),
|
|
333
|
+
)
|
|
334
|
+
return RedirectPayment(
|
|
335
|
+
order_id=data.order_id,
|
|
336
|
+
url=f"{self.config.authenticate_url}/authenticate?transaction_id="
|
|
337
|
+
f"{query_escape(transaction_id)}",
|
|
338
|
+
gateway_reference=transaction_id,
|
|
339
|
+
raw=to_plain_object(body),
|
|
340
|
+
)
|
|
341
|
+
|
|
342
|
+
def _hash(self, parts: Sequence[str]) -> str:
|
|
343
|
+
return hmac_sha256_hex(self.config.secret_key, "".join(parts))
|
|
344
|
+
|
|
345
|
+
|
|
346
|
+
def _error_message(body: LosslessObject) -> str:
|
|
347
|
+
errors = body.get("errors")
|
|
348
|
+
if isinstance(errors, dict):
|
|
349
|
+
lines = []
|
|
350
|
+
for name in sorted(errors):
|
|
351
|
+
entries = errors[name]
|
|
352
|
+
texts = [
|
|
353
|
+
text
|
|
354
|
+
for text in (
|
|
355
|
+
scalar_string(entry)
|
|
356
|
+
for entry in (entries if isinstance(entries, list) else [entries])
|
|
357
|
+
)
|
|
358
|
+
if text is not None
|
|
359
|
+
]
|
|
360
|
+
lines.append(f"{name}: {' '.join(texts)}")
|
|
361
|
+
return "; ".join(lines)
|
|
362
|
+
return get(body, "message") or "unexpected response"
|
|
363
|
+
|
|
364
|
+
|
|
365
|
+
class WaveMoney(_WaveMoneyBase, SyncGateway):
|
|
366
|
+
"""Wave Money with a synchronous HTTP client.
|
|
367
|
+
|
|
368
|
+
Redirect payments and callbacks. Wave has no status API, so the callback is
|
|
369
|
+
the only result. Use :class:`AsyncWaveMoney` in async code.
|
|
370
|
+
"""
|
|
371
|
+
|
|
372
|
+
def __init__(
|
|
373
|
+
self,
|
|
374
|
+
config: WaveMoneyConfig | Mapping[str, Any],
|
|
375
|
+
*,
|
|
376
|
+
http_client: httpx.Client | None = None,
|
|
377
|
+
timeout: float | None = DEFAULT_TIMEOUT,
|
|
378
|
+
) -> None:
|
|
379
|
+
super().__init__(config)
|
|
380
|
+
self._init_transport(http_client, timeout)
|
|
381
|
+
|
|
382
|
+
@classmethod
|
|
383
|
+
def from_env(
|
|
384
|
+
cls,
|
|
385
|
+
env: EnvSource | None = None,
|
|
386
|
+
*,
|
|
387
|
+
http_client: httpx.Client | None = None,
|
|
388
|
+
timeout: float | None = DEFAULT_TIMEOUT,
|
|
389
|
+
) -> WaveMoney:
|
|
390
|
+
"""A gateway configured from the ``WAVE_MONEY_*`` environment variables."""
|
|
391
|
+
return cls(WaveMoneyConfig.from_env(env), http_client=http_client, timeout=timeout)
|
|
392
|
+
|
|
393
|
+
def initiate(self, data: WaveMoneyPaymentData) -> RedirectPayment:
|
|
394
|
+
"""Creates a payment request and returns Wave's page to redirect the customer to.
|
|
395
|
+
|
|
396
|
+
Once ``data`` passes validation it fills ``data.merchant_reference_id``
|
|
397
|
+
with a random ID when empty, so store it afterwards. Invalid data is left
|
|
398
|
+
untouched.
|
|
399
|
+
"""
|
|
400
|
+
request = self._initiate_request(data)
|
|
401
|
+
return self._redirect(data, self._transport.send(request))
|
|
402
|
+
|
|
403
|
+
|
|
404
|
+
class AsyncWaveMoney(_WaveMoneyBase, AsyncGateway):
|
|
405
|
+
"""Wave Money with an async HTTP client. The same API as :class:`WaveMoney`, awaited."""
|
|
406
|
+
|
|
407
|
+
def __init__(
|
|
408
|
+
self,
|
|
409
|
+
config: WaveMoneyConfig | Mapping[str, Any],
|
|
410
|
+
*,
|
|
411
|
+
http_client: httpx.AsyncClient | None = None,
|
|
412
|
+
timeout: float | None = DEFAULT_TIMEOUT,
|
|
413
|
+
) -> None:
|
|
414
|
+
super().__init__(config)
|
|
415
|
+
self._init_transport(http_client, timeout)
|
|
416
|
+
|
|
417
|
+
@classmethod
|
|
418
|
+
def from_env(
|
|
419
|
+
cls,
|
|
420
|
+
env: EnvSource | None = None,
|
|
421
|
+
*,
|
|
422
|
+
http_client: httpx.AsyncClient | None = None,
|
|
423
|
+
timeout: float | None = DEFAULT_TIMEOUT,
|
|
424
|
+
) -> AsyncWaveMoney:
|
|
425
|
+
"""A gateway configured from the ``WAVE_MONEY_*`` environment variables."""
|
|
426
|
+
return cls(WaveMoneyConfig.from_env(env), http_client=http_client, timeout=timeout)
|
|
427
|
+
|
|
428
|
+
async def initiate(self, data: WaveMoneyPaymentData) -> RedirectPayment:
|
|
429
|
+
"""Creates a payment request and returns Wave's page to redirect the customer to."""
|
|
430
|
+
request = self._initiate_request(data)
|
|
431
|
+
return self._redirect(data, await self._transport.send(request))
|