bakong-v2 1.0.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.
- bakong_v2-1.0.0/LICENSE +21 -0
- bakong_v2-1.0.0/MANIFEST.in +1 -0
- bakong_v2-1.0.0/PKG-INFO +104 -0
- bakong_v2-1.0.0/README.md +79 -0
- bakong_v2-1.0.0/bakong_v2/__init__.py +3 -0
- bakong_v2-1.0.0/bakong_v2/khqr.py +277 -0
- bakong_v2-1.0.0/bakong_v2/sdk/__init__.py +37 -0
- bakong_v2-1.0.0/bakong_v2/sdk/additional_data_field.py +94 -0
- bakong_v2-1.0.0/bakong_v2/sdk/amount.py +39 -0
- bakong_v2-1.0.0/bakong_v2/sdk/assets/KHR.png +0 -0
- bakong_v2-1.0.0/bakong_v2/sdk/assets/USD.png +0 -0
- bakong_v2-1.0.0/bakong_v2/sdk/assets/bold.ttf +0 -0
- bakong_v2-1.0.0/bakong_v2/sdk/assets/khqr.png +0 -0
- bakong_v2-1.0.0/bakong_v2/sdk/assets/logo.png +0 -0
- bakong_v2-1.0.0/bakong_v2/sdk/assets/regular.ttf +0 -0
- bakong_v2-1.0.0/bakong_v2/sdk/country_code.py +25 -0
- bakong_v2-1.0.0/bakong_v2/sdk/crc.py +49 -0
- bakong_v2-1.0.0/bakong_v2/sdk/emv.py +88 -0
- bakong_v2-1.0.0/bakong_v2/sdk/emv_parser.py +25 -0
- bakong_v2-1.0.0/bakong_v2/sdk/global_unique_identifier.py +47 -0
- bakong_v2-1.0.0/bakong_v2/sdk/hash.py +27 -0
- bakong_v2-1.0.0/bakong_v2/sdk/image_tools.py +231 -0
- bakong_v2-1.0.0/bakong_v2/sdk/mcc.py +36 -0
- bakong_v2-1.0.0/bakong_v2/sdk/merchant_city.py +41 -0
- bakong_v2-1.0.0/bakong_v2/sdk/merchant_name.py +39 -0
- bakong_v2-1.0.0/bakong_v2/sdk/payload_format_indicator.py +26 -0
- bakong_v2-1.0.0/bakong_v2/sdk/point_of_initiation.py +30 -0
- bakong_v2-1.0.0/bakong_v2/sdk/timestamp.py +55 -0
- bakong_v2-1.0.0/bakong_v2/sdk/transaction_currency.py +39 -0
- bakong_v2-1.0.0/bakong_v2/sdk/version.py +1 -0
- bakong_v2-1.0.0/bakong_v2.egg-info/PKG-INFO +104 -0
- bakong_v2-1.0.0/bakong_v2.egg-info/SOURCES.txt +35 -0
- bakong_v2-1.0.0/bakong_v2.egg-info/dependency_links.txt +1 -0
- bakong_v2-1.0.0/bakong_v2.egg-info/requires.txt +4 -0
- bakong_v2-1.0.0/bakong_v2.egg-info/top_level.txt +1 -0
- bakong_v2-1.0.0/setup.cfg +4 -0
- bakong_v2-1.0.0/setup.py +27 -0
bakong_v2-1.0.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024 BAN Sothen
|
|
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 @@
|
|
|
1
|
+
recursive-include bakong_v2/sdk/assets *
|
bakong_v2-1.0.0/PKG-INFO
ADDED
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: bakong-v2
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: A Python package for Bakong KHQR payment integration using the BakongV2 API. (Unofficial)
|
|
5
|
+
Home-page: https://bakong-v2.vercel.app
|
|
6
|
+
Author: Lim Visa
|
|
7
|
+
Classifier: Programming Language :: Python :: 3
|
|
8
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
9
|
+
Classifier: Operating System :: OS Independent
|
|
10
|
+
Requires-Python: >=3.6
|
|
11
|
+
Description-Content-Type: text/markdown
|
|
12
|
+
License-File: LICENSE
|
|
13
|
+
Provides-Extra: image
|
|
14
|
+
Requires-Dist: pillow; extra == "image"
|
|
15
|
+
Requires-Dist: qrcode; extra == "image"
|
|
16
|
+
Dynamic: author
|
|
17
|
+
Dynamic: classifier
|
|
18
|
+
Dynamic: description
|
|
19
|
+
Dynamic: description-content-type
|
|
20
|
+
Dynamic: home-page
|
|
21
|
+
Dynamic: license-file
|
|
22
|
+
Dynamic: provides-extra
|
|
23
|
+
Dynamic: requires-python
|
|
24
|
+
Dynamic: summary
|
|
25
|
+
|
|
26
|
+
# Bakong-V2
|
|
27
|
+
|
|
28
|
+
A Python package for Bakong KHQR payment integration powered by [BakongV2 API](https://bakong-v2.vercel.app).
|
|
29
|
+
|
|
30
|
+
All requests go through the BakongV2 relay — no direct connection to NBC needed, no Cambodia IP required.
|
|
31
|
+
|
|
32
|
+
## Installation
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
pip install bakong-v2
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
For QR image generation:
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
pip install "bakong-v2[image]"
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
## Quick Start
|
|
45
|
+
|
|
46
|
+
```python
|
|
47
|
+
from bakong_v2 import KHQR
|
|
48
|
+
|
|
49
|
+
# Use your token from https://t.me/bakong_relay_bot
|
|
50
|
+
khqr = KHQR("your_token_here")
|
|
51
|
+
|
|
52
|
+
# Generate QR string (local, no API call)
|
|
53
|
+
qr = khqr.create_qr(
|
|
54
|
+
bank_account="your_name@bank",
|
|
55
|
+
merchant_name="Your Name",
|
|
56
|
+
merchant_city="Phnom Penh",
|
|
57
|
+
amount=9800,
|
|
58
|
+
currency="KHR",
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
# Generate MD5 hash (local, no API call)
|
|
62
|
+
md5 = khqr.generate_md5(qr)
|
|
63
|
+
|
|
64
|
+
# Check payment status
|
|
65
|
+
status = khqr.check_payment(md5)
|
|
66
|
+
print(status) # "PAID" or "UNPAID"
|
|
67
|
+
|
|
68
|
+
# Get payment details
|
|
69
|
+
info = khqr.get_payment(md5)
|
|
70
|
+
print(info)
|
|
71
|
+
|
|
72
|
+
# Generate deeplink
|
|
73
|
+
link = khqr.generate_deeplink(
|
|
74
|
+
qr=qr,
|
|
75
|
+
appDeepLinkCallback="https://your-site.com/callback",
|
|
76
|
+
appIconUrl="https://your-site.com/logo.png",
|
|
77
|
+
appName="MyApp",
|
|
78
|
+
)
|
|
79
|
+
print(link)
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
## All Methods
|
|
83
|
+
|
|
84
|
+
| Method | Description |
|
|
85
|
+
|--------|-------------|
|
|
86
|
+
| `create_qr()` | Generate KHQR string locally |
|
|
87
|
+
| `generate_md5()` | Compute MD5 hash of QR string |
|
|
88
|
+
| `generate_deeplink()` | Generate Bakong deeplink |
|
|
89
|
+
| `check_payment(md5)` | Check payment by MD5 → "PAID"/"UNPAID" |
|
|
90
|
+
| `get_payment(md5)` | Get payment details by MD5 |
|
|
91
|
+
| `check_payment_by_hash(hash)` | Check payment by transaction hash |
|
|
92
|
+
| `get_payment_by_hash(hash)` | Get payment details by hash |
|
|
93
|
+
| `check_payment_by_short_hash(hash, amount, currency)` | Check payment by short hash |
|
|
94
|
+
| `get_payment_by_short_hash(hash, amount, currency)` | Get payment details by short hash |
|
|
95
|
+
| `check_payment_by_instruction_ref(ref)` | Check payment by instruction ref |
|
|
96
|
+
| `check_payment_by_external_ref(ref)` | Check payment by external ref |
|
|
97
|
+
| `check_account(account_id)` | Check Bakong account info |
|
|
98
|
+
| `check_bulk_payments(md5_list)` | Bulk check by MD5 list (max 50) |
|
|
99
|
+
| `check_bulk_payments_by_hash(hash_list)` | Bulk check by hash list (max 50) |
|
|
100
|
+
| `qr_image()` | Generate QR image (requires `[image]` extras) |
|
|
101
|
+
|
|
102
|
+
## License
|
|
103
|
+
|
|
104
|
+
MIT
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
# Bakong-V2
|
|
2
|
+
|
|
3
|
+
A Python package for Bakong KHQR payment integration powered by [BakongV2 API](https://bakong-v2.vercel.app).
|
|
4
|
+
|
|
5
|
+
All requests go through the BakongV2 relay — no direct connection to NBC needed, no Cambodia IP required.
|
|
6
|
+
|
|
7
|
+
## Installation
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
pip install bakong-v2
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
For QR image generation:
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
pip install "bakong-v2[image]"
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## Quick Start
|
|
20
|
+
|
|
21
|
+
```python
|
|
22
|
+
from bakong_v2 import KHQR
|
|
23
|
+
|
|
24
|
+
# Use your token from https://t.me/bakong_relay_bot
|
|
25
|
+
khqr = KHQR("your_token_here")
|
|
26
|
+
|
|
27
|
+
# Generate QR string (local, no API call)
|
|
28
|
+
qr = khqr.create_qr(
|
|
29
|
+
bank_account="your_name@bank",
|
|
30
|
+
merchant_name="Your Name",
|
|
31
|
+
merchant_city="Phnom Penh",
|
|
32
|
+
amount=9800,
|
|
33
|
+
currency="KHR",
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
# Generate MD5 hash (local, no API call)
|
|
37
|
+
md5 = khqr.generate_md5(qr)
|
|
38
|
+
|
|
39
|
+
# Check payment status
|
|
40
|
+
status = khqr.check_payment(md5)
|
|
41
|
+
print(status) # "PAID" or "UNPAID"
|
|
42
|
+
|
|
43
|
+
# Get payment details
|
|
44
|
+
info = khqr.get_payment(md5)
|
|
45
|
+
print(info)
|
|
46
|
+
|
|
47
|
+
# Generate deeplink
|
|
48
|
+
link = khqr.generate_deeplink(
|
|
49
|
+
qr=qr,
|
|
50
|
+
appDeepLinkCallback="https://your-site.com/callback",
|
|
51
|
+
appIconUrl="https://your-site.com/logo.png",
|
|
52
|
+
appName="MyApp",
|
|
53
|
+
)
|
|
54
|
+
print(link)
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
## All Methods
|
|
58
|
+
|
|
59
|
+
| Method | Description |
|
|
60
|
+
|--------|-------------|
|
|
61
|
+
| `create_qr()` | Generate KHQR string locally |
|
|
62
|
+
| `generate_md5()` | Compute MD5 hash of QR string |
|
|
63
|
+
| `generate_deeplink()` | Generate Bakong deeplink |
|
|
64
|
+
| `check_payment(md5)` | Check payment by MD5 → "PAID"/"UNPAID" |
|
|
65
|
+
| `get_payment(md5)` | Get payment details by MD5 |
|
|
66
|
+
| `check_payment_by_hash(hash)` | Check payment by transaction hash |
|
|
67
|
+
| `get_payment_by_hash(hash)` | Get payment details by hash |
|
|
68
|
+
| `check_payment_by_short_hash(hash, amount, currency)` | Check payment by short hash |
|
|
69
|
+
| `get_payment_by_short_hash(hash, amount, currency)` | Get payment details by short hash |
|
|
70
|
+
| `check_payment_by_instruction_ref(ref)` | Check payment by instruction ref |
|
|
71
|
+
| `check_payment_by_external_ref(ref)` | Check payment by external ref |
|
|
72
|
+
| `check_account(account_id)` | Check Bakong account info |
|
|
73
|
+
| `check_bulk_payments(md5_list)` | Bulk check by MD5 list (max 50) |
|
|
74
|
+
| `check_bulk_payments_by_hash(hash_list)` | Bulk check by hash list (max 50) |
|
|
75
|
+
| `qr_image()` | Generate QR image (requires `[image]` extras) |
|
|
76
|
+
|
|
77
|
+
## License
|
|
78
|
+
|
|
79
|
+
MIT
|
|
@@ -0,0 +1,277 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import warnings
|
|
3
|
+
import urllib.request
|
|
4
|
+
import urllib.parse
|
|
5
|
+
from typing import Any
|
|
6
|
+
from urllib.error import HTTPError
|
|
7
|
+
|
|
8
|
+
from .sdk.crc import CRC
|
|
9
|
+
from .sdk.mcc import MCC
|
|
10
|
+
from .sdk.hash import HASH
|
|
11
|
+
from .sdk.amount import Amount
|
|
12
|
+
from .sdk.timestamp import TimeStamp
|
|
13
|
+
from .sdk.image_tools import ImageTools
|
|
14
|
+
from .sdk.country_code import CountryCode
|
|
15
|
+
from .sdk.merchant_city import MerchantCity
|
|
16
|
+
from .sdk.merchant_name import MerchantName
|
|
17
|
+
from .sdk.point_of_initiation import PointOfInitiation
|
|
18
|
+
from .sdk.transaction_currency import TransactionCurrency
|
|
19
|
+
from .sdk.additional_data_field import AdditionalDataField
|
|
20
|
+
from .sdk.payload_format_indicator import PayloadFormatIndicator
|
|
21
|
+
from .sdk.global_unique_identifier import GlobalUniqueIdentifier
|
|
22
|
+
|
|
23
|
+
from .sdk.version import __version__
|
|
24
|
+
|
|
25
|
+
API_BASE = "https://bakong-v2.vercel.app/api/v2/bakong"
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class KHQR:
|
|
29
|
+
def __init__(self, token: str | None = None):
|
|
30
|
+
self.__crc = CRC()
|
|
31
|
+
self.__mcc = MCC()
|
|
32
|
+
self.__hash = HASH()
|
|
33
|
+
self.__amount = Amount()
|
|
34
|
+
self.__timestamp = TimeStamp()
|
|
35
|
+
self.__image_tools = ImageTools()
|
|
36
|
+
self.__country_code = CountryCode()
|
|
37
|
+
self.__merchant_city = MerchantCity()
|
|
38
|
+
self.__merchant_name = MerchantName()
|
|
39
|
+
self.__point_of_initiation = PointOfInitiation()
|
|
40
|
+
self.__transaction_currency = TransactionCurrency()
|
|
41
|
+
self.__additional_data_field = AdditionalDataField()
|
|
42
|
+
self.__payload_format_indicator = PayloadFormatIndicator()
|
|
43
|
+
self.__global_unique_identifier = GlobalUniqueIdentifier()
|
|
44
|
+
self.__token = token
|
|
45
|
+
|
|
46
|
+
def __check_token(self):
|
|
47
|
+
if not self.__token:
|
|
48
|
+
raise ValueError("Token is required. Get yours at https://t.me/bakong_relay_bot")
|
|
49
|
+
|
|
50
|
+
def __api_get(self, endpoint: str, params: dict[str, Any]) -> dict[str, Any]:
|
|
51
|
+
self.__check_token()
|
|
52
|
+
params["token"] = self.__token
|
|
53
|
+
url = f"{API_BASE}{endpoint}?{urllib.parse.urlencode(params, doseq=True)}"
|
|
54
|
+
try:
|
|
55
|
+
with urllib.request.urlopen(url, timeout=15) as resp:
|
|
56
|
+
data = json.loads(resp.read().decode())
|
|
57
|
+
except HTTPError as e:
|
|
58
|
+
body = e.read().decode()
|
|
59
|
+
try:
|
|
60
|
+
err = json.loads(body)
|
|
61
|
+
msg = err.get("error", body)
|
|
62
|
+
except json.JSONDecodeError:
|
|
63
|
+
msg = body
|
|
64
|
+
raise ValueError(f"API error ({e.code}): {msg}")
|
|
65
|
+
except TimeoutError:
|
|
66
|
+
raise ValueError("API took too long to respond. Please try again later.")
|
|
67
|
+
except Exception as e:
|
|
68
|
+
raise ValueError(f"Failed to connect to API: {e}")
|
|
69
|
+
if isinstance(data, dict) and "error" in data:
|
|
70
|
+
raise ValueError(f"API error: {data['error']}")
|
|
71
|
+
return data
|
|
72
|
+
|
|
73
|
+
def __api_get_with_retry(self, endpoint: str, params: dict[str, Any], retries: int = 2) -> dict[str, Any]:
|
|
74
|
+
last_err = None
|
|
75
|
+
for attempt in range(retries + 1):
|
|
76
|
+
try:
|
|
77
|
+
return self.__api_get(endpoint, params)
|
|
78
|
+
except ValueError as e:
|
|
79
|
+
last_err = e
|
|
80
|
+
if attempt < retries:
|
|
81
|
+
continue
|
|
82
|
+
raise last_err
|
|
83
|
+
|
|
84
|
+
def create_qr(
|
|
85
|
+
self,
|
|
86
|
+
bank_account: str,
|
|
87
|
+
merchant_name: str,
|
|
88
|
+
merchant_city: str,
|
|
89
|
+
amount: float,
|
|
90
|
+
currency: str,
|
|
91
|
+
store_label: str | None = None,
|
|
92
|
+
phone_number: str | None = None,
|
|
93
|
+
bill_number: str | None = None,
|
|
94
|
+
terminal_label: str | None = None,
|
|
95
|
+
static: bool = False,
|
|
96
|
+
expiration: int = 1
|
|
97
|
+
) -> str:
|
|
98
|
+
if amount <= 0:
|
|
99
|
+
static = True
|
|
100
|
+
qr_data = self.__payload_format_indicator.value()
|
|
101
|
+
qr_data += self.__point_of_initiation.static() if static else self.__point_of_initiation.dynamic()
|
|
102
|
+
qr_data += self.__global_unique_identifier.value(bank_account)
|
|
103
|
+
qr_data += self.__mcc.value()
|
|
104
|
+
qr_data += self.__transaction_currency.value(currency)
|
|
105
|
+
if not static:
|
|
106
|
+
qr_data += self.__amount.value(amount)
|
|
107
|
+
qr_data += self.__country_code.value()
|
|
108
|
+
qr_data += self.__merchant_name.value(merchant_name)
|
|
109
|
+
qr_data += self.__merchant_city.value(merchant_city)
|
|
110
|
+
additional_data = self.__additional_data_field.value(
|
|
111
|
+
store_label=store_label,
|
|
112
|
+
phone_number=phone_number,
|
|
113
|
+
bill_number=bill_number,
|
|
114
|
+
terminal_label=terminal_label,
|
|
115
|
+
)
|
|
116
|
+
if additional_data:
|
|
117
|
+
qr_data += additional_data
|
|
118
|
+
qr_data += self.__timestamp.value(static, expiration)
|
|
119
|
+
qr_data += self.__crc.value(qr_data)
|
|
120
|
+
return qr_data
|
|
121
|
+
|
|
122
|
+
def generate_md5(self, qr: str) -> str:
|
|
123
|
+
return self.__hash.md5(qr)
|
|
124
|
+
|
|
125
|
+
def generate_deeplink(
|
|
126
|
+
self,
|
|
127
|
+
qr: str,
|
|
128
|
+
appDeepLinkCallback: str | None = None,
|
|
129
|
+
appIconUrl: str = "https://bakong.nbc.gov.kh/images/logo.svg",
|
|
130
|
+
appName: str = "MyAppName",
|
|
131
|
+
callback: str | None = None
|
|
132
|
+
) -> str | None:
|
|
133
|
+
if callback is not None:
|
|
134
|
+
warnings.warn(
|
|
135
|
+
f"\n\n{'!'*31} DEPRECATION WARNING {'!'*31}\n"
|
|
136
|
+
f"Parameter 'callback' is deprecated in bakong-v2.\n"
|
|
137
|
+
f"Please update your code to use 'appDeepLinkCallback' instead.\n"
|
|
138
|
+
f"{'!'*83}\n",
|
|
139
|
+
DeprecationWarning,
|
|
140
|
+
stacklevel=2
|
|
141
|
+
)
|
|
142
|
+
if appDeepLinkCallback is None:
|
|
143
|
+
appDeepLinkCallback = callback
|
|
144
|
+
if appDeepLinkCallback is None:
|
|
145
|
+
appDeepLinkCallback = "https://bakong.nbc.org.kh"
|
|
146
|
+
|
|
147
|
+
params = {
|
|
148
|
+
"qr": qr,
|
|
149
|
+
"AppName": appName,
|
|
150
|
+
"AppIconUrl": appIconUrl,
|
|
151
|
+
"callback": appDeepLinkCallback,
|
|
152
|
+
}
|
|
153
|
+
data = self.__api_get_with_retry("/generate-deeplink", params)
|
|
154
|
+
if data.get("responseCode") == 0:
|
|
155
|
+
d = data.get("data")
|
|
156
|
+
if isinstance(d, dict):
|
|
157
|
+
return d.get("shortLink") or d.get("fullLink")
|
|
158
|
+
return None
|
|
159
|
+
|
|
160
|
+
def check_payment(self, md5: str) -> str:
|
|
161
|
+
params = {"md5": md5}
|
|
162
|
+
data = self.__api_get_with_retry("/check-payment", params)
|
|
163
|
+
if data.get("responseCode") == 0:
|
|
164
|
+
return "PAID"
|
|
165
|
+
return "UNPAID"
|
|
166
|
+
|
|
167
|
+
def get_payment(self, md5: str) -> dict[str, Any] | None:
|
|
168
|
+
params = {"md5": md5}
|
|
169
|
+
data = self.__api_get_with_retry("/check-payment", params)
|
|
170
|
+
if data.get("responseCode") == 0:
|
|
171
|
+
d = data.get("data")
|
|
172
|
+
return d if isinstance(d, dict) else None
|
|
173
|
+
return None
|
|
174
|
+
|
|
175
|
+
def check_account(self, account_id: str) -> dict[str, Any] | None:
|
|
176
|
+
params = {"accountId": account_id}
|
|
177
|
+
data = self.__api_get_with_retry("/check-account", params)
|
|
178
|
+
if data.get("responseCode") == 0:
|
|
179
|
+
d = data.get("data")
|
|
180
|
+
return d if isinstance(d, dict) else None
|
|
181
|
+
return None
|
|
182
|
+
|
|
183
|
+
def check_payment_by_hash(self, hash: str) -> str:
|
|
184
|
+
params = {"hash": hash}
|
|
185
|
+
data = self.__api_get_with_retry("/check-payment", params)
|
|
186
|
+
if data.get("responseCode") == 0:
|
|
187
|
+
return "PAID"
|
|
188
|
+
return "UNPAID"
|
|
189
|
+
|
|
190
|
+
def get_payment_by_hash(self, hash: str) -> dict[str, Any] | None:
|
|
191
|
+
params = {"hash": hash}
|
|
192
|
+
data = self.__api_get_with_retry("/check-payment", params)
|
|
193
|
+
if data.get("responseCode") == 0:
|
|
194
|
+
d = data.get("data")
|
|
195
|
+
return d if isinstance(d, dict) else None
|
|
196
|
+
return None
|
|
197
|
+
|
|
198
|
+
def check_payment_by_short_hash(self, hash: str, amount: float, currency: str) -> str:
|
|
199
|
+
params = {"hash": hash, "amount": amount, "currency": currency}
|
|
200
|
+
data = self.__api_get_with_retry("/check-payment", params)
|
|
201
|
+
if data.get("responseCode") == 0:
|
|
202
|
+
return "PAID"
|
|
203
|
+
return "UNPAID"
|
|
204
|
+
|
|
205
|
+
def get_payment_by_short_hash(self, hash: str, amount: float, currency: str) -> dict[str, Any] | None:
|
|
206
|
+
params = {"hash": hash, "amount": amount, "currency": currency}
|
|
207
|
+
data = self.__api_get_with_retry("/check-payment", params)
|
|
208
|
+
if data.get("responseCode") == 0:
|
|
209
|
+
d = data.get("data")
|
|
210
|
+
return d if isinstance(d, dict) else None
|
|
211
|
+
return None
|
|
212
|
+
|
|
213
|
+
def check_payment_by_instruction_ref(self, instruction_ref: str) -> str:
|
|
214
|
+
params = {"instructionRef": instruction_ref}
|
|
215
|
+
data = self.__api_get_with_retry("/check-payment", params)
|
|
216
|
+
if data.get("responseCode") == 0:
|
|
217
|
+
return "PAID"
|
|
218
|
+
return "UNPAID"
|
|
219
|
+
|
|
220
|
+
def check_payment_by_external_ref(self, external_ref: str) -> str:
|
|
221
|
+
params = {"externalRef": external_ref}
|
|
222
|
+
data = self.__api_get_with_retry("/check-payment", params)
|
|
223
|
+
if data.get("responseCode") == 0:
|
|
224
|
+
return "PAID"
|
|
225
|
+
return "UNPAID"
|
|
226
|
+
|
|
227
|
+
def check_bulk_payments(self, md5_list: list[str]) -> list[str]:
|
|
228
|
+
if len(md5_list) > 50:
|
|
229
|
+
raise ValueError("The md5_list exceeds the allowed limit of 50 hashes per request.")
|
|
230
|
+
params = {"md5": md5_list}
|
|
231
|
+
data = self.__api_get_with_retry("/check-payment-list", params)
|
|
232
|
+
data_list = data.get("data")
|
|
233
|
+
if not isinstance(data_list, list):
|
|
234
|
+
return []
|
|
235
|
+
paid = []
|
|
236
|
+
for item in data_list:
|
|
237
|
+
if isinstance(item, dict) and item.get("status") == "SUCCESS":
|
|
238
|
+
md5 = item.get("md5")
|
|
239
|
+
if isinstance(md5, str):
|
|
240
|
+
paid.append(md5)
|
|
241
|
+
return paid
|
|
242
|
+
|
|
243
|
+
def check_bulk_payments_by_hash(self, hash_list: list[str]) -> list[str]:
|
|
244
|
+
if len(hash_list) > 50:
|
|
245
|
+
raise ValueError("The hash_list exceeds the allowed limit of 50 hashes per request.")
|
|
246
|
+
params = {"hash": hash_list}
|
|
247
|
+
data = self.__api_get_with_retry("/check-payment-list", params)
|
|
248
|
+
data_list = data.get("data")
|
|
249
|
+
if not isinstance(data_list, list):
|
|
250
|
+
return []
|
|
251
|
+
paid = []
|
|
252
|
+
for item in data_list:
|
|
253
|
+
if isinstance(item, dict) and item.get("status") == "SUCCESS":
|
|
254
|
+
h = item.get("hash")
|
|
255
|
+
if isinstance(h, str):
|
|
256
|
+
paid.append(h)
|
|
257
|
+
return paid
|
|
258
|
+
|
|
259
|
+
def qr_image(
|
|
260
|
+
self, qr: str,
|
|
261
|
+
format: str = "png",
|
|
262
|
+
output_path: str | None = None,
|
|
263
|
+
) -> str | bytes:
|
|
264
|
+
result = self.__image_tools.generate(qr)
|
|
265
|
+
f = format.lower()
|
|
266
|
+
if f in ("jpeg", "jpg"):
|
|
267
|
+
return result.to_jpeg(output_path)
|
|
268
|
+
elif f == "webp":
|
|
269
|
+
return result.to_webp(output_path)
|
|
270
|
+
elif f == "bytes":
|
|
271
|
+
return result.to_bytes()
|
|
272
|
+
elif f == "base64":
|
|
273
|
+
return result.to_base64()
|
|
274
|
+
elif f == "base64_uri":
|
|
275
|
+
return result.to_data_uri()
|
|
276
|
+
else:
|
|
277
|
+
return result.to_png(output_path)
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
from .emv import EMV
|
|
2
|
+
from .crc import CRC
|
|
3
|
+
from .mcc import MCC
|
|
4
|
+
from .hash import HASH
|
|
5
|
+
from .amount import Amount
|
|
6
|
+
from .timestamp import TimeStamp
|
|
7
|
+
from .emv_parser import EMVParser
|
|
8
|
+
from .image_tools import ImageTools
|
|
9
|
+
from .country_code import CountryCode
|
|
10
|
+
from .merchant_city import MerchantCity
|
|
11
|
+
from .merchant_name import MerchantName
|
|
12
|
+
from .point_of_initiation import PointOfInitiation
|
|
13
|
+
from .transaction_currency import TransactionCurrency
|
|
14
|
+
from .additional_data_field import AdditionalDataField
|
|
15
|
+
from .global_unique_identifier import GlobalUniqueIdentifier
|
|
16
|
+
from .payload_format_indicator import PayloadFormatIndicator
|
|
17
|
+
from .version import __version__
|
|
18
|
+
|
|
19
|
+
__all__ = [
|
|
20
|
+
"EMV",
|
|
21
|
+
"CRC",
|
|
22
|
+
"MCC",
|
|
23
|
+
"HASH",
|
|
24
|
+
"Amount",
|
|
25
|
+
"TimeStamp",
|
|
26
|
+
"EMVParser",
|
|
27
|
+
"ImageTools",
|
|
28
|
+
"CountryCode",
|
|
29
|
+
"MerchantCity",
|
|
30
|
+
"MerchantName",
|
|
31
|
+
"PointOfInitiation",
|
|
32
|
+
"TransactionCurrency",
|
|
33
|
+
"AdditionalDataField",
|
|
34
|
+
"GlobalUniqueIdentifier",
|
|
35
|
+
"PayloadFormatIndicator",
|
|
36
|
+
"__version__"
|
|
37
|
+
]
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
from .emv import EMV
|
|
2
|
+
|
|
3
|
+
# Initialize EMV instance
|
|
4
|
+
emv = EMV()
|
|
5
|
+
|
|
6
|
+
class AdditionalDataField:
|
|
7
|
+
def __init__(self):
|
|
8
|
+
"""
|
|
9
|
+
Initialize the AdditionalDataField class with settings from the EMV configuration.
|
|
10
|
+
"""
|
|
11
|
+
self.__additional_data_tag = emv.addtion_data_tag
|
|
12
|
+
self.__store_label_tag = emv.store_label
|
|
13
|
+
self.__mobile_number_tag = emv.addition_data_field_mobile_number
|
|
14
|
+
self.__bill_number_tag = emv.billnumber_tag
|
|
15
|
+
self.__terminal_label_tag = emv.terminal_label
|
|
16
|
+
|
|
17
|
+
# Maximum lengths according to EMVCo / Bakong spec
|
|
18
|
+
self.__store_label_max = emv.invalid_length_store_label
|
|
19
|
+
self.__mobile_max = emv.invalid_length_mobile_number
|
|
20
|
+
self.__bill_max = emv.invalid_length_bill_number
|
|
21
|
+
self.__terminal_max = emv.invalid_length_terminal_label
|
|
22
|
+
|
|
23
|
+
def __format_field(self, tag: str, value: str) -> str:
|
|
24
|
+
"""Format a single sub-field: TAG + LENGTH (02) + VALUE"""
|
|
25
|
+
value_str = str(value).strip()
|
|
26
|
+
if not value_str:
|
|
27
|
+
return ""
|
|
28
|
+
length = f"{len(value_str):02d}"
|
|
29
|
+
return f"{tag}{length}{value_str}"
|
|
30
|
+
|
|
31
|
+
def __validate_length(self, value: str, max_length: int, field_name: str):
|
|
32
|
+
"""
|
|
33
|
+
Validate the length of a field value.
|
|
34
|
+
|
|
35
|
+
:param value: The value to be validated.
|
|
36
|
+
:param field_name: The name of the field for error reporting.
|
|
37
|
+
:raises ValueError: If the value exceeds the maximum allowed length.
|
|
38
|
+
"""
|
|
39
|
+
if len(value) > max_length:
|
|
40
|
+
raise ValueError(f"{field_name} cannot exceed {max_length} characters. Your input length: {len(value)} characters.")
|
|
41
|
+
|
|
42
|
+
def value(
|
|
43
|
+
self,
|
|
44
|
+
store_label: str | None = None,
|
|
45
|
+
phone_number: str | None = None,
|
|
46
|
+
bill_number: str | None = None,
|
|
47
|
+
terminal_label: str | None = None,
|
|
48
|
+
) -> str:
|
|
49
|
+
"""
|
|
50
|
+
Combine all formatted values into a single string with a length prefix.
|
|
51
|
+
|
|
52
|
+
:param store_label: The store label.
|
|
53
|
+
:param phone_number: The phone number.
|
|
54
|
+
:param bill_number: The bill number.
|
|
55
|
+
:param terminal_label: The terminal label.
|
|
56
|
+
:return: Combined formatted string with length prefix.
|
|
57
|
+
"""
|
|
58
|
+
sub_fields = []
|
|
59
|
+
|
|
60
|
+
# Store Label (usually Tag 03)
|
|
61
|
+
if store_label:
|
|
62
|
+
self.__validate_length(store_label, self.__store_label_max, "Store label")
|
|
63
|
+
sub_fields.append(self.__format_field(self.__store_label_tag, store_label))
|
|
64
|
+
|
|
65
|
+
# Phone Number (usually Tag 04) - with Khmer phone normalization
|
|
66
|
+
if phone_number:
|
|
67
|
+
digits = ''.join(c for c in str(phone_number) if c.isdigit())
|
|
68
|
+
if digits.startswith('855'):
|
|
69
|
+
digits = digits[3:]
|
|
70
|
+
if not digits.startswith('0'):
|
|
71
|
+
digits = '0' + digits
|
|
72
|
+
|
|
73
|
+
self.__validate_length(digits, self.__mobile_max, "Phone number")
|
|
74
|
+
sub_fields.append(self.__format_field(self.__mobile_number_tag, digits))
|
|
75
|
+
|
|
76
|
+
# Bill Number (usually Tag 05)
|
|
77
|
+
if bill_number:
|
|
78
|
+
self.__validate_length(bill_number, self.__bill_max, "Bill number")
|
|
79
|
+
sub_fields.append(self.__format_field(self.__bill_number_tag, bill_number))
|
|
80
|
+
|
|
81
|
+
# Terminal Label (usually Tag 07)
|
|
82
|
+
if terminal_label:
|
|
83
|
+
self.__validate_length(terminal_label, self.__terminal_max, "Terminal label")
|
|
84
|
+
sub_fields.append(self.__format_field(self.__terminal_label_tag, terminal_label))
|
|
85
|
+
|
|
86
|
+
if not sub_fields:
|
|
87
|
+
# No additional data → return empty (do not include tag 62 at all)
|
|
88
|
+
return ""
|
|
89
|
+
|
|
90
|
+
combined = "".join(sub_fields)
|
|
91
|
+
total_length = f"{len(combined):02d}"
|
|
92
|
+
|
|
93
|
+
# Final format: 62 + LL + subfields
|
|
94
|
+
return f"{self.__additional_data_tag}{total_length}{combined}"
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
from .emv import EMV
|
|
2
|
+
|
|
3
|
+
emv = EMV()
|
|
4
|
+
|
|
5
|
+
class Amount:
|
|
6
|
+
def __init__(self):
|
|
7
|
+
self.__transaction_amount = emv.transaction_amount # "54"
|
|
8
|
+
self.__max_length = emv.invalid_length_amount
|
|
9
|
+
|
|
10
|
+
def value(self, amount: float | int | str) -> str:
|
|
11
|
+
"""
|
|
12
|
+
Get the formatted amount value.
|
|
13
|
+
|
|
14
|
+
:param amount: The transaction amount to be formatted.
|
|
15
|
+
:return: Formatted string including transaction amount tag, length, and amount.
|
|
16
|
+
"""
|
|
17
|
+
if not isinstance(amount, (int, float, str)):
|
|
18
|
+
raise ValueError("Amount must be a number or numeric string")
|
|
19
|
+
|
|
20
|
+
try:
|
|
21
|
+
amount_float = float(amount)
|
|
22
|
+
except ValueError:
|
|
23
|
+
raise ValueError(f"Invalid amount value: {amount}. Amount must be a number or a string representing a number.")
|
|
24
|
+
|
|
25
|
+
# Format amount (no trailing zeros)
|
|
26
|
+
amount_str = f"{amount_float:.2f}".rstrip("0").rstrip(".")
|
|
27
|
+
|
|
28
|
+
# EMV length = length of VALUE ONLY
|
|
29
|
+
length_of_amount = len(amount_str)
|
|
30
|
+
|
|
31
|
+
if length_of_amount > self.__max_length:
|
|
32
|
+
raise ValueError(
|
|
33
|
+
f"Formatted Amount exceeds maximum length of {self.__max_length} characters."
|
|
34
|
+
f"Your input length: {length_of_amount} characters."
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
length_str = str(length_of_amount).zfill(2)
|
|
38
|
+
|
|
39
|
+
return f"{self.__transaction_amount}{length_str}{amount_str}"
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|