python-myanmar-payments 4.0.0a1__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 (43) hide show
  1. python_myanmar_payments-4.0.0a1/.gitignore +16 -0
  2. python_myanmar_payments-4.0.0a1/CHANGELOG.md +28 -0
  3. python_myanmar_payments-4.0.0a1/LICENSE.md +21 -0
  4. python_myanmar_payments-4.0.0a1/PKG-INFO +43 -0
  5. python_myanmar_payments-4.0.0a1/README.md +12 -0
  6. python_myanmar_payments-4.0.0a1/pyproject.toml +107 -0
  7. python_myanmar_payments-4.0.0a1/skills/python-myanmar-payments/SKILL.md +115 -0
  8. python_myanmar_payments-4.0.0a1/src/python_myanmar_payments/__init__.py +111 -0
  9. python_myanmar_payments-4.0.0a1/src/python_myanmar_payments/_amount.py +178 -0
  10. python_myanmar_payments-4.0.0a1/src/python_myanmar_payments/_cache.py +80 -0
  11. python_myanmar_payments-4.0.0a1/src/python_myanmar_payments/_callback.py +188 -0
  12. python_myanmar_payments-4.0.0a1/src/python_myanmar_payments/_errors.py +94 -0
  13. python_myanmar_payments-4.0.0a1/src/python_myanmar_payments/_facade.py +271 -0
  14. python_myanmar_payments-4.0.0a1/src/python_myanmar_payments/_http.py +201 -0
  15. python_myanmar_payments-4.0.0a1/src/python_myanmar_payments/_json.py +94 -0
  16. python_myanmar_payments-4.0.0a1/src/python_myanmar_payments/_results.py +265 -0
  17. python_myanmar_payments-4.0.0a1/src/python_myanmar_payments/_status.py +66 -0
  18. python_myanmar_payments-4.0.0a1/src/python_myanmar_payments/_support.py +172 -0
  19. python_myanmar_payments-4.0.0a1/src/python_myanmar_payments/_validate.py +157 -0
  20. python_myanmar_payments-4.0.0a1/src/python_myanmar_payments/_values.py +59 -0
  21. python_myanmar_payments-4.0.0a1/src/python_myanmar_payments/_version.py +1 -0
  22. python_myanmar_payments-4.0.0a1/src/python_myanmar_payments/aya_pay.py +509 -0
  23. python_myanmar_payments-4.0.0a1/src/python_myanmar_payments/cyber_source.py +310 -0
  24. python_myanmar_payments-4.0.0a1/src/python_myanmar_payments/kbz_pay.py +556 -0
  25. python_myanmar_payments-4.0.0a1/src/python_myanmar_payments/py.typed +0 -0
  26. python_myanmar_payments-4.0.0a1/src/python_myanmar_payments/wave_money.py +431 -0
  27. python_myanmar_payments-4.0.0a1/src/python_myanmar_payments/yoma_mmqr.py +526 -0
  28. python_myanmar_payments-4.0.0a1/tests/__init__.py +0 -0
  29. python_myanmar_payments-4.0.0a1/tests/conftest.py +26 -0
  30. python_myanmar_payments-4.0.0a1/tests/fixtures/aya_pay/callback_payload.json +26 -0
  31. python_myanmar_payments-4.0.0a1/tests/fixtures/kbz_pay/sign_string.json +20 -0
  32. python_myanmar_payments-4.0.0a1/tests/fixtures/parity/vectors.json +536 -0
  33. python_myanmar_payments-4.0.0a1/tests/fixtures/wave_money/callback.json +26 -0
  34. python_myanmar_payments-4.0.0a1/tests/helpers.py +110 -0
  35. python_myanmar_payments-4.0.0a1/tests/test_amount.py +126 -0
  36. python_myanmar_payments-4.0.0a1/tests/test_aya_pay.py +336 -0
  37. python_myanmar_payments-4.0.0a1/tests/test_core.py +490 -0
  38. python_myanmar_payments-4.0.0a1/tests/test_cyber_source.py +207 -0
  39. python_myanmar_payments-4.0.0a1/tests/test_kbz_pay.py +504 -0
  40. python_myanmar_payments-4.0.0a1/tests/test_myanmar_payments.py +210 -0
  41. python_myanmar_payments-4.0.0a1/tests/test_parity.py +255 -0
  42. python_myanmar_payments-4.0.0a1/tests/test_wave_money.py +282 -0
  43. python_myanmar_payments-4.0.0a1/tests/test_yoma_mmqr.py +358 -0
@@ -0,0 +1,16 @@
1
+ __pycache__/
2
+ *.py[cod]
3
+ .venv/
4
+ dist/
5
+ build/
6
+ *.egg-info/
7
+ .coverage
8
+ .coverage.*
9
+ coverage.xml
10
+ htmlcov/
11
+ .pytest_cache/
12
+ .mypy_cache/
13
+ .ruff_cache/
14
+ .env
15
+ .DS_Store
16
+ /.idea
@@ -0,0 +1,28 @@
1
+ # Changelog
2
+
3
+ All notable changes to `python-myanmar-payments` will be documented in this file.
4
+
5
+ ## v4.0.0 - Unreleased
6
+
7
+ Initial release. The version number matches the other Laranex Myanmar payments packages (`php-myanmar-payments`, `laravel-myanmar-payments`, `go-myanmar-payments`, `node-myanmar-payments`), and the gateways, flows, validation, signing, status maps, results and errors are a port of them, sharing their test vectors. Pre-releases are tagged `v4.0.0-alpha.N` and published to PyPI as `4.0.0aN`.
8
+
9
+ ### Added
10
+ - Gateways: KBZ Pay (`pwa`, `qr`, `app`, `status`), Wave Money (`initiate`), AYA Payment Gateway (`services`, `initiate`, `status`, `verify_redirect`), Yoma MMQR (`initiate`, `renew_qr`, `status`, `forget_token`) and CyberSource Secure Acceptance (`initiate`); every gateway validates its payment data (`KbzPay.validate()` and so on) and verifies callbacks with `handle_callback`.
11
+ - A sync and an async client for every gateway that calls an API (`KbzPay` / `AsyncKbzPay`, `WaveMoney` / `AsyncWaveMoney`, `AyaPay` / `AsyncAyaPay`, `YomaMmqr` / `AsyncYomaMmqr`), sharing one implementation of signing, validation and response parsing. `CyberSource` makes no network calls and serves both.
12
+ - `httpx` is the only runtime dependency: pass your own `httpx.Client` / `httpx.AsyncClient` as `http_client`, or let the gateway create one with a 30 second `timeout` and close it with `close()` / `aclose()` or a `with` / `async with` block.
13
+ - Exact `Amount` type (`Amount.kyat()`, `Amount.parse()`, `Amount.of()`) kept as decimal text, so amounts never pass through a float; payment data also takes a whole `int`, decimal text or a `Decimal`, and rejects floats. Decimals are accepted only where the gateway documents them (KBZ Pay up to 2 places, CyberSource any); every gateway except CyberSource is MMK only.
14
+ - Typed payment data dataclasses (`KbzPayPaymentData`, `WaveMoneyPaymentData` with `WaveMoneyItem`, `AyaPayPaymentData`, `YomaMmqrPaymentData`, `CyberSourcePaymentData`) and a config class per gateway with `from_env()` (default `os.environ`), reading the same `*_SANDBOX`, credential and URL variables as the PHP, Laravel, Go and Node packages; a missing credential raises `ConfigurationError`. `sandbox` also takes text read like `*_SANDBOX` (`false`, `0`, `f`, `no` or `off` select production). Gateways, `MyanmarPayments` and `AsyncMyanmarPayments` take config objects or mappings of their keyword arguments; the facades build every gateway lazily, also from the environment.
15
+ - Result classes: `RedirectPayment`, `FormPayment` (with `to_html()`), `QrPayment` (with `qr_image_data_uri()`) and `AppPayment` (with `to_dict()`); `PaymentCallback` (with a gateway-independent `PaymentStatus` and the `Acknowledgement` each gateway expects, in its `acknowledgement` attribute) and `PaymentStatusResult`.
16
+ - `CallbackRequest(body=..., headers=..., query=...)` takes the raw body bytes, headers and query string from Django, Flask, FastAPI or any other framework; `CallbackRequest.from_json()` replays a stored callback. JSON numbers in callbacks are read without a float conversion, so signatures and amounts keep the gateway's exact text, and `raw` (like `parsed_body()`, `input()` and error `raw`) holds every JSON number as its exact text, e.g. `"1000.50"`.
17
+ - Errors: `PaymentError` and its subclasses `InvalidPaymentDataError` (per-field `errors`), `ApiError` (`gateway_code`, `gateway_message`, `http_status`, `raw`, network errors chained as `__cause__`), `SignatureVerificationError` and `ConfigurationError`.
18
+ - An injectable `TokenCache` (or `AsyncTokenCache` for the async client) with a thread-safe in-memory default for Yoma's access token; concurrent calls share one token request.
19
+ - Callback verification is the same in every Laranex payments SDK, checked by shared vectors (`tests/fixtures/parity/vectors.json`): numbers sign as the exact text sent, booleans as `true` / `false`, and a nested value (object or list) in a signed field fails verification.
20
+ - `Amount.equals()` and `==` compare by value, ignoring leading zeros and trailing fractional zeros (`"01000"` equals `Amount.kyat(1000)`); text that is not plain digits never equals.
21
+ - CyberSource callbacks trust only the fields listed in `signed_field_names`: `decision` and `req_reference_number` must be signed, and unsigned fields are left out of the result and `raw`.
22
+ - AYA Pay reads a `payload` whose `+` signs became spaces in an unencoded return query string, with or without base64 padding; partial padding, other alphabets and payloads that are not UTF-8 are rejected.
23
+ - Yoma MMQR caches its token under `myanmar-payments.yoma-mmqr.token.<sha256 of base URL|client id>`, the same key as the PHP, Go and Node SDKs, so services in different languages can share one cache.
24
+ - Callback, return and cancel URLs only need to be valid absolute http or https URLs; there is no HTTPS-only rule (gateways may still require HTTPS in production).
25
+ - Wave Money's sandbox is `https://preprodpayments.wavemoney.io:8107`, with checkout at `https://preprodpayments.wavemoney.io/authenticate`.
26
+ - Fully typed (`py.typed`, `mypy --strict`).
27
+ - Agent skill in `skills/python-myanmar-payments` so coding agents use the package correctly; install it with `npx skills add laranex/python-myanmar-payments`.
28
+ - Requires Python 3.10 or higher.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) Nay Thu Khant
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.
@@ -0,0 +1,43 @@
1
+ Metadata-Version: 2.5
2
+ Name: python-myanmar-payments
3
+ Version: 4.0.0a1
4
+ Summary: Python SDK for Myanmar payment gateways: KBZ Pay, Wave Money, AYA Pay, Yoma MMQR and CyberSource. Typed requests and results, exact amounts, sync and async clients.
5
+ Project-URL: Homepage, https://laranex.vercel.app/python-myanmar-payments
6
+ Project-URL: Documentation, https://laranex.vercel.app/python-myanmar-payments
7
+ Project-URL: Repository, https://github.com/laranex/python-myanmar-payments
8
+ Project-URL: Issues, https://github.com/laranex/python-myanmar-payments/issues
9
+ Project-URL: Changelog, https://github.com/laranex/python-myanmar-payments/blob/dev/CHANGELOG.md
10
+ Author-email: Nay Thu Khant <naythukhant644@gmail.com>
11
+ License-Expression: MIT
12
+ License-File: LICENSE.md
13
+ Keywords: aya-pay,cybersource,kbzpay,laranex,mmqr,myanmar,payment-gateway,payments,python-myanmar-payments,wave-money,wavepay,yoma
14
+ Classifier: Development Status :: 4 - Beta
15
+ Classifier: Framework :: AsyncIO
16
+ Classifier: Intended Audience :: Developers
17
+ Classifier: Operating System :: OS Independent
18
+ Classifier: Programming Language :: Python :: 3
19
+ Classifier: Programming Language :: Python :: 3 :: Only
20
+ Classifier: Programming Language :: Python :: 3.10
21
+ Classifier: Programming Language :: Python :: 3.11
22
+ Classifier: Programming Language :: Python :: 3.12
23
+ Classifier: Programming Language :: Python :: 3.13
24
+ Classifier: Programming Language :: Python :: 3.14
25
+ Classifier: Topic :: Office/Business :: Financial
26
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
27
+ Classifier: Typing :: Typed
28
+ Requires-Python: >=3.10
29
+ Requires-Dist: httpx>=0.25
30
+ Description-Content-Type: text/markdown
31
+
32
+ # Python Myanmar Payments
33
+
34
+ [![PyPI](https://img.shields.io/pypi/v/python-myanmar-payments.svg?style=flat-square)](https://pypi.org/project/python-myanmar-payments/)
35
+ [![Tests](https://github.com/laranex/python-myanmar-payments/actions/workflows/tests.yml/badge.svg)](https://github.com/laranex/python-myanmar-payments/actions/workflows/tests.yml)
36
+ [![Python](https://img.shields.io/pypi/pyversions/python-myanmar-payments.svg?style=flat-square)](https://pypi.org/project/python-myanmar-payments/)
37
+ [![License](https://img.shields.io/badge/license-MIT-blue.svg?style=flat-square)](LICENSE.md)
38
+
39
+ Python SDK for Myanmar payment gateways: KBZ Pay, Wave Money, AYA Pay, Yoma MMQR and CyberSource. Typed requests and results, exact amounts, sync and async clients. Built for humans and AI agents.
40
+
41
+ ## Documentation
42
+
43
+ Full documentation, including installation, usage and the AI agent skill, lives at **[laranex.vercel.app/python-myanmar-payments](https://laranex.vercel.app/python-myanmar-payments)**.
@@ -0,0 +1,12 @@
1
+ # Python Myanmar Payments
2
+
3
+ [![PyPI](https://img.shields.io/pypi/v/python-myanmar-payments.svg?style=flat-square)](https://pypi.org/project/python-myanmar-payments/)
4
+ [![Tests](https://github.com/laranex/python-myanmar-payments/actions/workflows/tests.yml/badge.svg)](https://github.com/laranex/python-myanmar-payments/actions/workflows/tests.yml)
5
+ [![Python](https://img.shields.io/pypi/pyversions/python-myanmar-payments.svg?style=flat-square)](https://pypi.org/project/python-myanmar-payments/)
6
+ [![License](https://img.shields.io/badge/license-MIT-blue.svg?style=flat-square)](LICENSE.md)
7
+
8
+ Python SDK for Myanmar payment gateways: KBZ Pay, Wave Money, AYA Pay, Yoma MMQR and CyberSource. Typed requests and results, exact amounts, sync and async clients. Built for humans and AI agents.
9
+
10
+ ## Documentation
11
+
12
+ Full documentation, including installation, usage and the AI agent skill, lives at **[laranex.vercel.app/python-myanmar-payments](https://laranex.vercel.app/python-myanmar-payments)**.
@@ -0,0 +1,107 @@
1
+ [build-system]
2
+ requires = ["hatchling>=1.27"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "python-myanmar-payments"
7
+ dynamic = ["version"]
8
+ description = "Python SDK for Myanmar payment gateways: KBZ Pay, Wave Money, AYA Pay, Yoma MMQR and CyberSource. Typed requests and results, exact amounts, sync and async clients."
9
+ readme = "README.md"
10
+ license = "MIT"
11
+ license-files = ["LICENSE.md"]
12
+ requires-python = ">=3.10"
13
+ authors = [{ name = "Nay Thu Khant", email = "naythukhant644@gmail.com" }]
14
+ keywords = [
15
+ "laranex",
16
+ "python-myanmar-payments",
17
+ "myanmar",
18
+ "payments",
19
+ "payment-gateway",
20
+ "kbzpay",
21
+ "wave-money",
22
+ "wavepay",
23
+ "aya-pay",
24
+ "yoma",
25
+ "mmqr",
26
+ "cybersource",
27
+ ]
28
+ classifiers = [
29
+ "Development Status :: 4 - Beta",
30
+ "Framework :: AsyncIO",
31
+ "Intended Audience :: Developers",
32
+ "Operating System :: OS Independent",
33
+ "Programming Language :: Python :: 3",
34
+ "Programming Language :: Python :: 3 :: Only",
35
+ "Programming Language :: Python :: 3.10",
36
+ "Programming Language :: Python :: 3.11",
37
+ "Programming Language :: Python :: 3.12",
38
+ "Programming Language :: Python :: 3.13",
39
+ "Programming Language :: Python :: 3.14",
40
+ "Topic :: Office/Business :: Financial",
41
+ "Topic :: Software Development :: Libraries :: Python Modules",
42
+ "Typing :: Typed",
43
+ ]
44
+ dependencies = ["httpx>=0.25"]
45
+
46
+ [project.urls]
47
+ Homepage = "https://laranex.vercel.app/python-myanmar-payments"
48
+ Documentation = "https://laranex.vercel.app/python-myanmar-payments"
49
+ Repository = "https://github.com/laranex/python-myanmar-payments"
50
+ Issues = "https://github.com/laranex/python-myanmar-payments/issues"
51
+ Changelog = "https://github.com/laranex/python-myanmar-payments/blob/dev/CHANGELOG.md"
52
+
53
+ [dependency-groups]
54
+ dev = [
55
+ "mypy>=1.13",
56
+ "pytest>=8.3",
57
+ "pytest-cov>=6.0",
58
+ "ruff>=0.8",
59
+ ]
60
+
61
+ [tool.hatch.version]
62
+ path = "src/python_myanmar_payments/_version.py"
63
+
64
+ [tool.hatch.build.targets.sdist]
65
+ include = [
66
+ "/src",
67
+ "/tests",
68
+ "/skills",
69
+ "/CHANGELOG.md",
70
+ "/LICENSE.md",
71
+ "/README.md",
72
+ ]
73
+
74
+ [tool.hatch.build.targets.wheel]
75
+ packages = ["src/python_myanmar_payments"]
76
+
77
+ [tool.pytest.ini_options]
78
+ testpaths = ["tests"]
79
+ addopts = ["--strict-markers", "--strict-config", "-ra"]
80
+ xfail_strict = true
81
+ filterwarnings = ["error"]
82
+
83
+ [tool.coverage.run]
84
+ branch = true
85
+ source = ["python_myanmar_payments"]
86
+
87
+ [tool.coverage.report]
88
+ show_missing = true
89
+ skip_covered = true
90
+ fail_under = 100
91
+ exclude_also = ["if TYPE_CHECKING:"]
92
+
93
+ [tool.mypy]
94
+ strict = true
95
+ python_version = "3.10"
96
+ files = ["src", "tests"]
97
+
98
+ [tool.ruff]
99
+ line-length = 100
100
+ target-version = "py310"
101
+ src = ["src", "tests"]
102
+
103
+ [tool.ruff.lint]
104
+ select = ["E", "W", "F", "I", "B", "UP", "SIM", "RUF", "C4", "PT", "PIE", "RET", "N"]
105
+
106
+ [tool.ruff.lint.per-file-ignores]
107
+ "tests/**" = ["N802"]
@@ -0,0 +1,115 @@
1
+ ---
2
+ name: python-myanmar-payments
3
+ description: >
4
+ Integrate Myanmar payment gateways (KBZ Pay, Wave Money, AYA Pay, Yoma MMQR, CyberSource) in a Python app (Django, Flask, FastAPI or plain Python) with python-myanmar-payments.
5
+ license: MIT
6
+ metadata:
7
+ author: Nay Thu Khant
8
+ ---
9
+
10
+ # Python Myanmar Payments
11
+
12
+ ## When to use
13
+
14
+ Use this skill when a Python app takes payments through KBZ Pay, Wave Money, AYA Payment Gateway, Yoma MMQR or CyberSource. Start payments and verify callbacks with the package's typed API; never build gateway signatures by hand. It works the same in Django, Flask, FastAPI or plain Python.
15
+
16
+ ## Install
17
+
18
+ ```bash
19
+ pip install python-myanmar-payments
20
+ ```
21
+
22
+ Requires Python 3.10+ and depends only on `httpx`. Import everything from `python_myanmar_payments`. Every gateway that calls an API has a sync class (`KbzPay`, `WaveMoney`, `AyaPay`, `YomaMmqr`) and an async twin (`AsyncKbzPay`, `AsyncWaveMoney`, `AsyncAyaPay`, `AsyncYomaMmqr`) with the same methods, awaited; use the async ones inside an event loop. `CyberSource` makes no network calls and serves both.
23
+
24
+ ## Configure
25
+
26
+ `MyanmarPayments.from_env()` reads `KBZ_PAY_*`, `WAVE_MONEY_*`, `AYA_PAY_*` (or `AYA_PGW_*`), `YOMA_MMQR_*` and `CYBER_SOURCE_*` from `os.environ` (the same variables as the PHP, Go and Node SDKs). `sandbox` defaults to `True`; set `*_SANDBOX=false` (or `sandbox=False`) in production.
27
+
28
+ ```python
29
+ from python_myanmar_payments import MyanmarPayments
30
+
31
+ payments = MyanmarPayments.from_env() # create once, share across requests
32
+ kbz = payments.kbz_pay() # also wave_money(), aya_pay(), yoma_mmqr(), cyber_source()
33
+ ```
34
+
35
+ - Or build one gateway: `KbzPay(KbzPayConfig(app_id=..., app_key=..., merchant_code=...))`, `KbzPay.from_env()` or `KbzPay(KbzPayConfig.from_env())`; in async code use `AsyncMyanmarPayments.from_env()` and `AsyncKbzPay`.
36
+ - Or pass the settings directly: gateways and `MyanmarPayments(kbz_pay={"app_id": ..., "app_key": ..., "merchant_code": ...})` take config objects or mappings of their keyword arguments; a string `sandbox` such as `"false"` is read like the variable.
37
+ - Options (keyword arguments): `http_client` (your own `httpx.Client` / `httpx.AsyncClient`) and `timeout` (seconds, default 30); close the clients the package created with `payments.close()` / `await payments.aclose()` or a `with` block. Yoma and the facades also take `token_cache` (any `TokenCache`, default `MemoryTokenCache`; back it with Redis when you run several processes).
38
+ - A missing credential raises `ConfigurationError` (`gateway`, `key`).
39
+
40
+ ## Use
41
+
42
+ ### Amounts
43
+
44
+ Amounts are `Amount.kyat(1000)`, `Amount.parse("1000.50")`, a whole `int`, decimal text or a `Decimal`; never a float such as `10.5`. Only KBZ Pay (up to 2 decimals) and CyberSource accept decimals; Wave, AYA and Yoma take whole kyat. Invalid data raises `InvalidPaymentDataError` with `errors` per field (`order_id`, `amount`, ...), before any request is sent. Compare a gateway's amount by value with `amount.equals(callback.amount)`.
45
+
46
+ ### Start a payment
47
+
48
+ Each gateway takes a data object (`KbzPayPaymentData`, `WaveMoneyPaymentData` with `WaveMoneyItem`s, `AyaPayPaymentData` with an `AyaPayMethod`, `YomaMmqrPaymentData`, `CyberSourcePaymentData` with a `CyberSourceTransactionType`) and returns a typed result:
49
+
50
+ ```python
51
+ from python_myanmar_payments import Amount, KbzPayPaymentData
52
+
53
+ payment = kbz.pwa(
54
+ KbzPayPaymentData(
55
+ order_id="ORDER_1",
56
+ amount=Amount.kyat(1000),
57
+ callback_url="https://shop.test/payments/kbz/callback",
58
+ )
59
+ )
60
+ return redirect(payment.url)
61
+ ```
62
+
63
+ - `RedirectPayment` (`url`) from `kbz.pwa(data)` and `wave.initiate(data)`. Wave fills `data.merchant_reference_id` when empty; store it.
64
+ - `FormPayment` from `aya.initiate(data)` and `cyber_source.initiate(data)` (no network call, never awaited): return `payment.to_html()` as an auto-submitting page, or render `action`, `fields` and `enctype` yourself.
65
+ - `QrPayment` from `kbz.qr(data)` (encode `qr_string`) and `yoma.initiate(data)` (`qr_image_data_uri()`, `expires_at`, `reference`). A Yoma QR lives `YomaMmqr.QR_LIFETIME_SECONDS` (120); renew it with `yoma.renew_qr(order_id)`.
66
+ - `AppPayment` from `kbz.app(data)`: send `payment.to_dict()` (`orderId`, `orderInfo`, `sign`, `signType`) to your mobile app.
67
+
68
+ AYA needs a channel: `aya.services()` lists `AyaPayService` entries (`key`, `supports(method)`), then `aya.initiate(AyaPayPaymentData(order_id="ORDER123", amount=1000, channel="kbz_pay", method=AyaPayMethod.QR))`.
69
+
70
+ ### Handle the callback
71
+
72
+ Build a `CallbackRequest` from the raw request, verify it, then reply:
73
+
74
+ ```python
75
+ from python_myanmar_payments import CallbackRequest, SignatureVerificationError
76
+
77
+ request = CallbackRequest(
78
+ body=django_request.body, # Flask: request.get_data(); FastAPI: await request.body()
79
+ headers=django_request.headers,
80
+ query=django_request.META["QUERY_STRING"], # Flask: request.query_string
81
+ )
82
+ try:
83
+ callback = kbz.handle_callback(request)
84
+ except SignatureVerificationError:
85
+ return HttpResponse("invalid signature", status=400)
86
+ if callback.is_successful():
87
+ ... # compare callback.amount with the order, then fulfill callback.order_id once
88
+ ack = callback.acknowledgement
89
+ return HttpResponse(ack.body, status=ack.status, headers=ack.headers)
90
+ ```
91
+
92
+ - Give the package the raw body bytes, the headers and the query string; `handle_callback()` and `verify_redirect()` are plain methods on the async classes too.
93
+ - Check AYA's browser return with `aya.verify_redirect(request)`.
94
+ - For production, store the verified callback, acknowledge immediately, then process it once in the background.
95
+
96
+ ### Check status and handle errors
97
+
98
+ - `kbz.status(order_id)`, `aya.status(order_id)` and `yoma.status(reference)` return `PaymentStatusResult` with `status` and `is_successful()`. Wave Money and CyberSource have no status API.
99
+ - Statuses: `PaymentStatus.SUCCESSFUL`, `PENDING`, `FAILED`, `CANCELED`, `EXPIRED`, `UNKNOWN`; `status.is_final()`.
100
+ - Gateway failures raise `ApiError` (`gateway_code`, `gateway_message`, `http_status`, `raw`; network errors are chained as `__cause__`). All errors extend `PaymentError`.
101
+
102
+ ## Test your app
103
+
104
+ - Pass an `httpx.Client(transport=httpx.MockTransport(handler))` (or an `httpx.AsyncClient`) as `http_client`, or mock with `respx`.
105
+ - Replay a stored callback with `CallbackRequest.from_json(payload, headers)` or `CallbackRequest(body=..., headers=..., query=...)`; it is still signature-checked.
106
+ - To test your own fulfillment code, build `PaymentCallback(order_id="ORDER_1", status=PaymentStatus.SUCCESSFUL, gateway_status="PAY_SUCCESS")` yourself.
107
+
108
+ ## Avoid
109
+
110
+ - Fulfilling orders from return pages or query strings; fulfill only from a verified callback or a status check.
111
+ - Treating `PENDING` or `UNKNOWN` as paid.
112
+ - Passing floats or `float(price)` as amounts; use `Amount.parse()` or a `Decimal`.
113
+ - Building the `CallbackRequest` from `request.json` / `request.POST` instead of the raw body.
114
+ - Reusing a Wave `merchant_reference_id`, or calling Yoma `initiate()` twice for one order (use `renew_qr()`).
115
+ - Opening a KBZ Pay PWA link outside a phone with the KBZ Pay app.
@@ -0,0 +1,111 @@
1
+ """Python SDK for Myanmar payment gateways.
2
+
3
+ KBZ Pay, Wave Money, AYA Pay, Yoma MMQR and CyberSource. Typed requests and
4
+ results, exact amounts, sync and async clients.
5
+ """
6
+
7
+ from ._amount import Amount, AmountInput
8
+ from ._cache import AsyncTokenCache, MemoryTokenCache, TokenCache
9
+ from ._callback import BodyInput, CallbackRequest, HeadersInput, QueryInput
10
+ from ._errors import (
11
+ ApiError,
12
+ ConfigurationError,
13
+ InvalidPaymentDataError,
14
+ PaymentError,
15
+ SignatureVerificationError,
16
+ )
17
+ from ._facade import AsyncMyanmarPayments, MyanmarPayments
18
+ from ._http import DEFAULT_TIMEOUT
19
+ from ._results import (
20
+ Acknowledgement,
21
+ AppPayment,
22
+ FormField,
23
+ FormPayment,
24
+ PaymentCallback,
25
+ PaymentResult,
26
+ PaymentStatusResult,
27
+ QrPayment,
28
+ RedirectPayment,
29
+ )
30
+ from ._status import PaymentFlow, PaymentStatus, resolve_status
31
+ from ._version import __version__
32
+ from .aya_pay import (
33
+ AsyncAyaPay,
34
+ AyaPay,
35
+ AyaPayConfig,
36
+ AyaPayMethod,
37
+ AyaPayPaymentData,
38
+ AyaPayService,
39
+ )
40
+ from .cyber_source import (
41
+ CyberSource,
42
+ CyberSourceConfig,
43
+ CyberSourcePaymentData,
44
+ CyberSourceTransactionType,
45
+ )
46
+ from .kbz_pay import AsyncKbzPay, KbzPay, KbzPayConfig, KbzPayPaymentData, KbzPaySigner
47
+ from .wave_money import (
48
+ AsyncWaveMoney,
49
+ WaveMoney,
50
+ WaveMoneyConfig,
51
+ WaveMoneyItem,
52
+ WaveMoneyPaymentData,
53
+ )
54
+ from .yoma_mmqr import AsyncYomaMmqr, YomaMmqr, YomaMmqrConfig, YomaMmqrPaymentData
55
+
56
+ __all__ = [
57
+ "DEFAULT_TIMEOUT",
58
+ "Acknowledgement",
59
+ "Amount",
60
+ "AmountInput",
61
+ "ApiError",
62
+ "AppPayment",
63
+ "AsyncAyaPay",
64
+ "AsyncKbzPay",
65
+ "AsyncMyanmarPayments",
66
+ "AsyncTokenCache",
67
+ "AsyncWaveMoney",
68
+ "AsyncYomaMmqr",
69
+ "AyaPay",
70
+ "AyaPayConfig",
71
+ "AyaPayMethod",
72
+ "AyaPayPaymentData",
73
+ "AyaPayService",
74
+ "BodyInput",
75
+ "CallbackRequest",
76
+ "ConfigurationError",
77
+ "CyberSource",
78
+ "CyberSourceConfig",
79
+ "CyberSourcePaymentData",
80
+ "CyberSourceTransactionType",
81
+ "FormField",
82
+ "FormPayment",
83
+ "HeadersInput",
84
+ "InvalidPaymentDataError",
85
+ "KbzPay",
86
+ "KbzPayConfig",
87
+ "KbzPayPaymentData",
88
+ "KbzPaySigner",
89
+ "MemoryTokenCache",
90
+ "MyanmarPayments",
91
+ "PaymentCallback",
92
+ "PaymentError",
93
+ "PaymentFlow",
94
+ "PaymentResult",
95
+ "PaymentStatus",
96
+ "PaymentStatusResult",
97
+ "QrPayment",
98
+ "QueryInput",
99
+ "RedirectPayment",
100
+ "SignatureVerificationError",
101
+ "TokenCache",
102
+ "WaveMoney",
103
+ "WaveMoneyConfig",
104
+ "WaveMoneyItem",
105
+ "WaveMoneyPaymentData",
106
+ "YomaMmqr",
107
+ "YomaMmqrConfig",
108
+ "YomaMmqrPaymentData",
109
+ "__version__",
110
+ "resolve_status",
111
+ ]
@@ -0,0 +1,178 @@
1
+ """The exact money amount type."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ import re
7
+ from decimal import Decimal
8
+
9
+ from ._errors import InvalidPaymentDataError
10
+
11
+ __all__ = ["Amount", "AmountInput"]
12
+
13
+ _PATTERN = re.compile(r"[0-9]+(?:\.[0-9]+)?")
14
+
15
+
16
+ class Amount:
17
+ """An exact, non-negative money amount, kept as decimal text.
18
+
19
+ It never passes through a float, so it is never rounded. Build one with
20
+ :meth:`Amount.kyat` for whole amounts, :meth:`Amount.parse` for decimal text or
21
+ :meth:`Amount.of` for any accepted input. Each gateway then checks it against
22
+ its documented rules (Wave Money, AYA and Yoma MMQR only accept whole kyat,
23
+ KBZ Pay up to 2 decimal places, CyberSource any).
24
+
25
+ Amounts are immutable and ``str(amount)`` is the exact text sent to the
26
+ gateway, e.g. ``"1000.50"``.
27
+ """
28
+
29
+ __slots__ = ("_value",)
30
+
31
+ _value: str
32
+
33
+ def __init__(self, amount: AmountInput) -> None:
34
+ """The same as :meth:`Amount.of`: ``Amount(1000)``, ``Amount("1000.50")``."""
35
+ object.__setattr__(self, "_value", Amount.of(amount)._value)
36
+
37
+ @classmethod
38
+ def _make(cls, value: str) -> Amount:
39
+ instance = object.__new__(cls)
40
+ object.__setattr__(instance, "_value", value)
41
+ return instance
42
+
43
+ def __setattr__(self, name: str, value: object) -> None:
44
+ raise AttributeError("Amount is immutable")
45
+
46
+ @classmethod
47
+ def kyat(cls, amount: int) -> Amount:
48
+ """A whole amount, e.g. ``Amount.kyat(1000)`` for 1000 MMK.
49
+
50
+ Works for whole units of any currency. Floats, booleans and negative
51
+ values raise an :class:`InvalidPaymentDataError` for the ``amount`` field.
52
+ """
53
+ if isinstance(amount, bool) or not isinstance(amount, int):
54
+ raise _invalid(
55
+ "The amount field must be a whole number of kyat; use "
56
+ "Amount.parse('10.50') for decimal amounts, "
57
+ f"got {amount!r}."
58
+ )
59
+ if amount < 0:
60
+ raise _invalid("The amount field must not be negative.")
61
+ return cls._make(str(amount))
62
+
63
+ @classmethod
64
+ def parse(cls, amount: str) -> Amount:
65
+ """A decimal amount written as plain digits, e.g. ``Amount.parse("1000.50")``.
66
+
67
+ Signs, exponents, spaces and thousands separators are rejected with an
68
+ :class:`InvalidPaymentDataError`. Leading zeros of the whole part are
69
+ removed (``"007.50"`` becomes ``"7.50"``); the fractional digits are kept
70
+ exactly as given.
71
+ """
72
+ if not isinstance(amount, str) or _PATTERN.fullmatch(amount) is None:
73
+ raise _invalid(
74
+ "The amount field must be a number such as 1000 or 1000.50, "
75
+ f"got {json.dumps(amount, ensure_ascii=False, default=repr)}."
76
+ )
77
+ whole, dot, fraction = amount.partition(".")
78
+ whole = whole.lstrip("0") or "0"
79
+ return cls._make(f"{whole}.{fraction}" if dot else whole)
80
+
81
+ @classmethod
82
+ def of(cls, amount: AmountInput) -> Amount:
83
+ """Any accepted amount input as an :class:`Amount`.
84
+
85
+ Takes an ``Amount`` (returned as is), a whole ``int`` (as
86
+ :meth:`kyat`), decimal text (as :meth:`parse`) or a finite, non-negative
87
+ :class:`~decimal.Decimal` (its exact digits, e.g. ``Decimal("1000.50")``
88
+ is ``"1000.50"``). Floats are never accepted.
89
+ """
90
+ if isinstance(amount, Amount):
91
+ return amount
92
+ if isinstance(amount, str):
93
+ return cls.parse(amount)
94
+ if isinstance(amount, Decimal):
95
+ if not amount.is_finite():
96
+ raise _invalid(f"The amount field must be a finite number, got {amount!r}.")
97
+ if amount < 0:
98
+ raise _invalid("The amount field must not be negative.")
99
+ return cls.parse(format(amount.copy_abs(), "f"))
100
+ return cls.kyat(amount)
101
+
102
+ def __str__(self) -> str:
103
+ return self._value
104
+
105
+ def __repr__(self) -> str:
106
+ return f"Amount('{self._value}')"
107
+
108
+ def __eq__(self, other: object) -> bool:
109
+ if isinstance(other, Amount):
110
+ return _normalize(self._value) == _normalize(other._value)
111
+ return NotImplemented
112
+
113
+ def __hash__(self) -> int:
114
+ return hash(_normalize(self._value))
115
+
116
+ def decimal_places(self) -> int:
117
+ """The number of fractional digits, e.g. ``2`` for ``1000.50``."""
118
+ _, dot, fraction = self._value.partition(".")
119
+ return len(fraction) if dot else 0
120
+
121
+ def whole_part(self) -> str:
122
+ """The digits before the decimal point, e.g. ``"1000"`` for ``1000.50``."""
123
+ return self._value.partition(".")[0]
124
+
125
+ def is_zero(self) -> bool:
126
+ """Whether the amount equals zero, e.g. ``0`` or ``0.00``."""
127
+ return self._value.replace(".", "").strip("0") == ""
128
+
129
+ def is_positive(self) -> bool:
130
+ """Whether the amount is greater than zero."""
131
+ return not self.is_zero()
132
+
133
+ def to_decimal(self) -> Decimal:
134
+ """The amount as an exact :class:`~decimal.Decimal`, e.g. ``Decimal("1000.50")``."""
135
+ return Decimal(self._value)
136
+
137
+ def equals(self, other: Amount | str | None) -> bool:
138
+ """Whether both amounts have the same value.
139
+
140
+ Leading zeros of the whole part and trailing zeros of the fraction are
141
+ ignored: ``1000``, ``01000`` and ``1000.00`` are equal. Text such as
142
+ ``callback.amount`` must be plain digits with an optional fraction;
143
+ anything else, and ``None``, is never equal, so a gateway amount that was
144
+ not sent never matches.
145
+ """
146
+ if isinstance(other, Amount):
147
+ return self == other
148
+ if not isinstance(other, str) or _PATTERN.fullmatch(other) is None:
149
+ return False
150
+ return _normalize(self._value) == _normalize(other)
151
+
152
+
153
+ AmountInput = Amount | int | str | Decimal
154
+ """What payment data accepts as an amount: an :class:`Amount`, a whole ``int``,
155
+ decimal text such as ``"1000.50"`` or a :class:`~decimal.Decimal`. Never a float."""
156
+
157
+
158
+ def _normalize(value: str) -> str:
159
+ whole, _, fraction = value.partition(".")
160
+ whole = whole.lstrip("0") or "0"
161
+ fraction = fraction.rstrip("0")
162
+ return f"{whole}.{fraction}" if fraction else whole
163
+
164
+
165
+ def _invalid(message: str) -> InvalidPaymentDataError:
166
+ return InvalidPaymentDataError({"amount": message})
167
+
168
+
169
+ def to_amount(value: object) -> Amount | None:
170
+ """``value`` as an :class:`Amount`, or ``None`` when it is not a valid amount."""
171
+ if isinstance(value, (Amount, str, Decimal)) or (
172
+ isinstance(value, int) and not isinstance(value, bool)
173
+ ):
174
+ try:
175
+ return Amount.of(value)
176
+ except InvalidPaymentDataError:
177
+ return None
178
+ return None