konthaina-khqr 0.1.2__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.
@@ -0,0 +1,13 @@
1
+ from .decode import decode
2
+ from .enums import Currency, MerchantType
3
+ from .generator import KHQRGenerator, KHQRResult
4
+ from .verify import verify
5
+
6
+ __all__ = [
7
+ "Currency",
8
+ "MerchantType",
9
+ "KHQRGenerator",
10
+ "KHQRResult",
11
+ "verify",
12
+ "decode",
13
+ ]
konthaina_khqr/cli.py ADDED
@@ -0,0 +1,74 @@
1
+ from __future__ import annotations
2
+
3
+ import argparse
4
+ from pathlib import Path
5
+
6
+ from .enums import Currency, MerchantType
7
+ from .generator import KHQRGenerator
8
+ from .verify import verify
9
+
10
+
11
+ def _parse_args() -> argparse.Namespace:
12
+ p = argparse.ArgumentParser(
13
+ prog="khqr", description="Generate KHQR payload strings (Bakong / Cambodia)"
14
+ )
15
+ p.add_argument("--type", choices=["individual", "merchant"], default="individual")
16
+ p.add_argument("--bakong", required=True, help="Bakong account ID / username")
17
+ p.add_argument("--name", required=True, help="Merchant name")
18
+ p.add_argument("--merchant-id", help="Merchant ID (required for merchant type)")
19
+ p.add_argument("--bank", help="Acquiring bank (required for merchant type)")
20
+ p.add_argument("--currency", choices=["KHR", "USD"], default="KHR")
21
+ p.add_argument("--amount", type=float, help="Amount (optional)")
22
+ p.add_argument("--city", default="Phnom Penh")
23
+ p.add_argument("--bill", help="Bill number (optional)")
24
+ p.add_argument("--mobile", help="Mobile number (optional)")
25
+ p.add_argument("--png", help="Output PNG path (requires konthaina-khqr[qrcode])")
26
+ p.add_argument(
27
+ "--verify", action="store_true", help="Verify generated payload CRC and exit 0/1"
28
+ )
29
+ return p.parse_args()
30
+
31
+
32
+ def _write_png(payload: str, out_path: str) -> None:
33
+ try:
34
+ import qrcode # type: ignore
35
+ except Exception as e: # pragma: no cover
36
+ raise SystemExit(
37
+ 'PNG output requires optional dependency. Install: pip install "konthaina-khqr[qrcode]"'
38
+ ) from e
39
+
40
+ img = qrcode.make(payload)
41
+ Path(out_path).parent.mkdir(parents=True, exist_ok=True)
42
+ img.save(out_path)
43
+
44
+
45
+ def main() -> None:
46
+ ns = _parse_args()
47
+
48
+ mtype = MerchantType.INDIVIDUAL if ns.type == "individual" else MerchantType.MERCHANT
49
+ g = KHQRGenerator(mtype).set_bakong_account_id(ns.bakong).set_merchant_name(ns.name)
50
+
51
+ if ns.type == "merchant":
52
+ if ns.merchant_id:
53
+ g.set_merchant_id(ns.merchant_id)
54
+ if ns.bank:
55
+ g.set_acquiring_bank(ns.bank)
56
+
57
+ g.set_currency(Currency.KHR if ns.currency == "KHR" else Currency.USD)
58
+ if ns.amount is not None:
59
+ g.set_amount(ns.amount)
60
+ if ns.city:
61
+ g.set_merchant_city(ns.city)
62
+ if ns.bill:
63
+ g.set_bill_number(ns.bill)
64
+ if ns.mobile:
65
+ g.set_mobile_number(ns.mobile)
66
+
67
+ res = g.generate()
68
+ print(res.qr)
69
+
70
+ if ns.png:
71
+ _write_png(res.qr, ns.png)
72
+
73
+ if ns.verify:
74
+ raise SystemExit(0 if verify(res.qr) else 1)
@@ -0,0 +1,270 @@
1
+ from __future__ import annotations
2
+
3
+ # CRC-16/CCITT-FALSE lookup table
4
+ CRC16_TABLE: list[int] = [
5
+ 0x0000,
6
+ 0x1021,
7
+ 0x2042,
8
+ 0x3063,
9
+ 0x4084,
10
+ 0x50A5,
11
+ 0x60C6,
12
+ 0x70E7,
13
+ 0x8108,
14
+ 0x9129,
15
+ 0xA14A,
16
+ 0xB16B,
17
+ 0xC18C,
18
+ 0xD1AD,
19
+ 0xE1CE,
20
+ 0xF1EF,
21
+ 0x1231,
22
+ 0x0210,
23
+ 0x3273,
24
+ 0x2252,
25
+ 0x52B5,
26
+ 0x4294,
27
+ 0x72F7,
28
+ 0x62D6,
29
+ 0x9339,
30
+ 0x8318,
31
+ 0xB37B,
32
+ 0xA35A,
33
+ 0xD3BD,
34
+ 0xC39C,
35
+ 0xF3FF,
36
+ 0xE3DE,
37
+ 0x2462,
38
+ 0x3443,
39
+ 0x0420,
40
+ 0x1401,
41
+ 0x64E6,
42
+ 0x74C7,
43
+ 0x44A4,
44
+ 0x5485,
45
+ 0xA56A,
46
+ 0xB54B,
47
+ 0x8528,
48
+ 0x9509,
49
+ 0xE5EE,
50
+ 0xF5CF,
51
+ 0xC5AC,
52
+ 0xD58D,
53
+ 0x3653,
54
+ 0x2672,
55
+ 0x1611,
56
+ 0x0630,
57
+ 0x76D7,
58
+ 0x66F6,
59
+ 0x5695,
60
+ 0x46B4,
61
+ 0xB75B,
62
+ 0xA77A,
63
+ 0x9719,
64
+ 0x8738,
65
+ 0xF7DF,
66
+ 0xE7FE,
67
+ 0xD79D,
68
+ 0xC7BC,
69
+ 0x48C4,
70
+ 0x58E5,
71
+ 0x6886,
72
+ 0x78A7,
73
+ 0x0840,
74
+ 0x1861,
75
+ 0x2802,
76
+ 0x3823,
77
+ 0xC9CC,
78
+ 0xD9ED,
79
+ 0xE98E,
80
+ 0xF9AF,
81
+ 0x8948,
82
+ 0x9969,
83
+ 0xA90A,
84
+ 0xB92B,
85
+ 0x5AF5,
86
+ 0x4AD4,
87
+ 0x7AB7,
88
+ 0x6A96,
89
+ 0x1A71,
90
+ 0x0A50,
91
+ 0x3A33,
92
+ 0x2A12,
93
+ 0xDBFD,
94
+ 0xCBDC,
95
+ 0xFBBF,
96
+ 0xEB9E,
97
+ 0x9B79,
98
+ 0x8B58,
99
+ 0xBB3B,
100
+ 0xAB1A,
101
+ 0x6CA6,
102
+ 0x7C87,
103
+ 0x4CE4,
104
+ 0x5CC5,
105
+ 0x2C22,
106
+ 0x3C03,
107
+ 0x0C60,
108
+ 0x1C41,
109
+ 0xEDAE,
110
+ 0xFD8F,
111
+ 0xCDEC,
112
+ 0xDDCD,
113
+ 0xAD2A,
114
+ 0xBD0B,
115
+ 0x8D68,
116
+ 0x9D49,
117
+ 0x7E97,
118
+ 0x6EB6,
119
+ 0x5ED5,
120
+ 0x4EF4,
121
+ 0x3E13,
122
+ 0x2E32,
123
+ 0x1E51,
124
+ 0x0E70,
125
+ 0xFF9F,
126
+ 0xEFBE,
127
+ 0xDFDD,
128
+ 0xCFFC,
129
+ 0xBF1B,
130
+ 0xAF3A,
131
+ 0x9F59,
132
+ 0x8F78,
133
+ 0x9188,
134
+ 0x81A9,
135
+ 0xB1CA,
136
+ 0xA1EB,
137
+ 0xD10C,
138
+ 0xC12D,
139
+ 0xF14E,
140
+ 0xE16F,
141
+ 0x1080,
142
+ 0x00A1,
143
+ 0x30C2,
144
+ 0x20E3,
145
+ 0x5004,
146
+ 0x4025,
147
+ 0x7046,
148
+ 0x6067,
149
+ 0x83B9,
150
+ 0x9398,
151
+ 0xA3FB,
152
+ 0xB3DA,
153
+ 0xC33D,
154
+ 0xD31C,
155
+ 0xE37F,
156
+ 0xF35E,
157
+ 0x02B1,
158
+ 0x1290,
159
+ 0x22F3,
160
+ 0x32D2,
161
+ 0x4235,
162
+ 0x5214,
163
+ 0x6277,
164
+ 0x7256,
165
+ 0xB5EA,
166
+ 0xA5CB,
167
+ 0x95A8,
168
+ 0x8589,
169
+ 0xF56E,
170
+ 0xE54F,
171
+ 0xD52C,
172
+ 0xC50D,
173
+ 0x34E2,
174
+ 0x24C3,
175
+ 0x14A0,
176
+ 0x0481,
177
+ 0x7466,
178
+ 0x6447,
179
+ 0x5424,
180
+ 0x4405,
181
+ 0xA7DB,
182
+ 0xB7FA,
183
+ 0x8799,
184
+ 0x97B8,
185
+ 0xE75F,
186
+ 0xF77E,
187
+ 0xC71D,
188
+ 0xD73C,
189
+ 0x26D3,
190
+ 0x36F2,
191
+ 0x0691,
192
+ 0x16B0,
193
+ 0x6657,
194
+ 0x7676,
195
+ 0x4615,
196
+ 0x5634,
197
+ 0xD94C,
198
+ 0xC96D,
199
+ 0xF90E,
200
+ 0xE92F,
201
+ 0x99C8,
202
+ 0x89E9,
203
+ 0xB98A,
204
+ 0xA9AB,
205
+ 0x5844,
206
+ 0x4865,
207
+ 0x7806,
208
+ 0x6827,
209
+ 0x18C0,
210
+ 0x08E1,
211
+ 0x3882,
212
+ 0x28A3,
213
+ 0xCB7D,
214
+ 0xDB5C,
215
+ 0xEB3F,
216
+ 0xFB1E,
217
+ 0x8BF9,
218
+ 0x9BD8,
219
+ 0xABBB,
220
+ 0xBB9A,
221
+ 0x4A75,
222
+ 0x5A54,
223
+ 0x6A37,
224
+ 0x7A16,
225
+ 0x0AF1,
226
+ 0x1AD0,
227
+ 0x2AB3,
228
+ 0x3A92,
229
+ 0xFD2E,
230
+ 0xED0F,
231
+ 0xDD6C,
232
+ 0xCD4D,
233
+ 0xBDAA,
234
+ 0xAD8B,
235
+ 0x9DE8,
236
+ 0x8DC9,
237
+ 0x7C26,
238
+ 0x6C07,
239
+ 0x5C64,
240
+ 0x4C45,
241
+ 0x3CA2,
242
+ 0x2C83,
243
+ 0x1CE0,
244
+ 0x0CC1,
245
+ 0xEF1F,
246
+ 0xFF3E,
247
+ 0xCF5D,
248
+ 0xDF7C,
249
+ 0xAF9B,
250
+ 0xBFBA,
251
+ 0x8FD9,
252
+ 0x9FF8,
253
+ 0x6E17,
254
+ 0x7E36,
255
+ 0x4E55,
256
+ 0x5E74,
257
+ 0x2E93,
258
+ 0x3EB2,
259
+ 0x0ED1,
260
+ 0x1EF0,
261
+ ]
262
+
263
+
264
+ def crc16_ccitt_false(data: str) -> str:
265
+ """Return CRC-16/CCITT-FALSE as 4-char uppercase hex."""
266
+ crc = 0xFFFF
267
+ for ch in data:
268
+ c = ord(ch)
269
+ crc = ((crc << 8) ^ CRC16_TABLE[((crc >> 8) ^ c) & 0xFF]) & 0xFFFF
270
+ return format(crc, "04X")
@@ -0,0 +1,8 @@
1
+ from __future__ import annotations
2
+
3
+ from .tlv import decode_tlv
4
+
5
+
6
+ def decode(qr: str) -> dict[str, str]:
7
+ """Decode top-level TLV fields from a KHQR payload."""
8
+ return decode_tlv(qr)
@@ -0,0 +1,17 @@
1
+ from __future__ import annotations
2
+
3
+ from enum import Enum
4
+
5
+
6
+ class Currency(str, Enum):
7
+ """Currency numeric codes used in KHQR."""
8
+
9
+ KHR = "116"
10
+ USD = "840"
11
+
12
+
13
+ class MerchantType(str, Enum):
14
+ """Merchant account structure used in KHQR."""
15
+
16
+ INDIVIDUAL = "individual"
17
+ MERCHANT = "merchant"
@@ -0,0 +1,2 @@
1
+ class KHQRValidationError(ValueError):
2
+ """Raised when required KHQR fields are missing or invalid."""
@@ -0,0 +1,193 @@
1
+ from __future__ import annotations
2
+
3
+ import hashlib
4
+ import time
5
+ from dataclasses import dataclass
6
+ from typing import Union
7
+
8
+ from .crc16 import crc16_ccitt_false
9
+ from .enums import Currency, MerchantType
10
+ from .exceptions import KHQRValidationError
11
+ from .tlv import format_tag
12
+
13
+
14
+ @dataclass(frozen=True)
15
+ class KHQRResult:
16
+ qr: str
17
+ timestamp: str
18
+ type: str
19
+ md5: str
20
+
21
+
22
+ class KHQRGenerator:
23
+ """KHQR payload generator (merchant-presented).
24
+
25
+ Generates KHQR payment payload strings compliant with the NBC KHQR v2.7-style layout.
26
+ """
27
+
28
+ def __init__(self, merchant_type: MerchantType = MerchantType.INDIVIDUAL):
29
+ self.merchant_type = merchant_type
30
+ self.data: dict[str, str] = {}
31
+
32
+ # -----------------------
33
+ # Builder setters
34
+ # -----------------------
35
+ def set_bakong_account_id(self, account_id: str) -> KHQRGenerator:
36
+ self.data["bakong_account_id"] = (account_id or "")[:32]
37
+ return self
38
+
39
+ def set_merchant_name(self, name: str) -> KHQRGenerator:
40
+ self.data["merchant_name"] = (name or "")[:25]
41
+ return self
42
+
43
+ def set_merchant_id(self, merchant_id: str) -> KHQRGenerator:
44
+ self.data["merchant_id"] = (merchant_id or "")[:32]
45
+ return self
46
+
47
+ def set_acquiring_bank(self, bank: str) -> KHQRGenerator:
48
+ self.data["acquiring_bank"] = (bank or "")[:32]
49
+ return self
50
+
51
+ def set_account_information(self, info: str) -> KHQRGenerator:
52
+ self.data["account_information"] = (info or "")[:32]
53
+ return self
54
+
55
+ def set_currency(self, currency: Union[str, Currency]) -> KHQRGenerator:
56
+ if isinstance(currency, str):
57
+ cur = currency.upper().strip()
58
+ currency = Currency.KHR if cur == "KHR" else Currency.USD
59
+ self.data["currency"] = currency.value
60
+ return self
61
+
62
+ def set_amount(self, amount: float) -> KHQRGenerator:
63
+ self.data["amount"] = f"{amount:.2f}"
64
+ return self
65
+
66
+ def set_merchant_city(self, city: str) -> KHQRGenerator:
67
+ self.data["merchant_city"] = (city or "")[:15]
68
+ return self
69
+
70
+ def set_bill_number(self, bill_number: str) -> KHQRGenerator:
71
+ self.data["bill_number"] = (bill_number or "")[:25]
72
+ return self
73
+
74
+ def set_mobile_number(self, mobile: str) -> KHQRGenerator:
75
+ self.data["mobile_number"] = (mobile or "")[:25]
76
+ return self
77
+
78
+ def set_store_label(self, label: str) -> KHQRGenerator:
79
+ self.data["store_label"] = (label or "")[:25]
80
+ return self
81
+
82
+ def set_terminal_label(self, label: str) -> KHQRGenerator:
83
+ self.data["terminal_label"] = (label or "")[:25]
84
+ return self
85
+
86
+ def set_purpose_of_transaction(self, purpose: str) -> KHQRGenerator:
87
+ self.data["purpose_of_transaction"] = (purpose or "")[:25]
88
+ return self
89
+
90
+ def set_upi_account_information(self, upi: str) -> KHQRGenerator:
91
+ self.data["upi_account_information"] = (upi or "")[:31]
92
+ return self
93
+
94
+ def set_language_preference(self, lang: str) -> KHQRGenerator:
95
+ self.data["language_preference"] = (lang or "")[:2]
96
+ return self
97
+
98
+ def set_merchant_name_alternate(self, name: str) -> KHQRGenerator:
99
+ self.data["merchant_name_alternate"] = (name or "")[:25]
100
+ return self
101
+
102
+ def set_merchant_city_alternate(self, city: str) -> KHQRGenerator:
103
+ self.data["merchant_city_alternate"] = (city or "")[:15]
104
+ return self
105
+
106
+ # -----------------------
107
+ # Generate
108
+ # -----------------------
109
+ def generate(self) -> KHQRResult:
110
+ self._validate()
111
+
112
+ qr = ""
113
+ qr += format_tag("00", "01") # Payload Format Indicator
114
+ qr += format_tag("01", "12") # Point of Initiation Method (static)
115
+
116
+ # UPI Merchant Account (Tag 15) - Optional
117
+ if self.data.get("upi_account_information"):
118
+ qr += format_tag("15", self.data["upi_account_information"])
119
+
120
+ # Account information template
121
+ if self.merchant_type == MerchantType.INDIVIDUAL:
122
+ tag29 = format_tag("00", self.data["bakong_account_id"])
123
+ if self.data.get("account_information"):
124
+ tag29 += format_tag("01", self.data["account_information"])
125
+ if self.data.get("acquiring_bank"):
126
+ tag29 += format_tag("02", self.data["acquiring_bank"])
127
+ qr += format_tag("29", tag29)
128
+ else:
129
+ tag30 = format_tag("00", self.data["bakong_account_id"])
130
+ tag30 += format_tag("01", self.data["merchant_id"])
131
+ tag30 += format_tag("02", self.data["acquiring_bank"])
132
+ qr += format_tag("30", tag30)
133
+
134
+ qr += format_tag("52", "5999") # Merchant Category Code
135
+ qr += format_tag("53", self.data.get("currency", Currency.KHR.value)) # Currency
136
+
137
+ if self.data.get("amount"):
138
+ qr += format_tag("54", self.data["amount"])
139
+
140
+ qr += format_tag("58", "KH") # Country Code
141
+ qr += format_tag("59", self.data["merchant_name"])
142
+ qr += format_tag("60", self.data.get("merchant_city", "Phnom Penh"))
143
+
144
+ # Additional Data Field (Tag 62)
145
+ tag62 = ""
146
+ if self.data.get("bill_number"):
147
+ tag62 += format_tag("01", self.data["bill_number"])
148
+ if self.data.get("mobile_number"):
149
+ tag62 += format_tag("02", self.data["mobile_number"])
150
+ if self.data.get("store_label"):
151
+ tag62 += format_tag("03", self.data["store_label"])
152
+ if self.data.get("terminal_label"):
153
+ tag62 += format_tag("07", self.data["terminal_label"])
154
+ if self.data.get("purpose_of_transaction"):
155
+ tag62 += format_tag("08", self.data["purpose_of_transaction"])
156
+ if tag62:
157
+ qr += format_tag("62", tag62)
158
+
159
+ # Merchant Alternate Language (Tag 64)
160
+ tag64 = ""
161
+ if self.data.get("language_preference"):
162
+ tag64 += format_tag("00", self.data["language_preference"])
163
+ if self.data.get("merchant_name_alternate"):
164
+ tag64 += format_tag("01", self.data["merchant_name_alternate"])
165
+ if self.data.get("merchant_city_alternate"):
166
+ tag64 += format_tag("02", self.data["merchant_city_alternate"])
167
+ if tag64:
168
+ qr += format_tag("64", tag64)
169
+
170
+ # Timestamp (Tag 99) - proprietary
171
+ timestamp = str(int(time.time() * 1000))
172
+ qr += format_tag("99", format_tag("00", timestamp))
173
+
174
+ # CRC (Tag 63)
175
+ crc = crc16_ccitt_false(qr + "6304")
176
+ qr += "6304" + crc
177
+
178
+ return KHQRResult(
179
+ qr=qr,
180
+ timestamp=timestamp,
181
+ type=self.merchant_type.value,
182
+ md5=hashlib.md5(qr.encode("utf-8")).hexdigest(),
183
+ )
184
+
185
+ def _validate(self) -> None:
186
+ if not self.data.get("bakong_account_id") or not self.data.get("merchant_name"):
187
+ raise KHQRValidationError("Bakong Account ID and Merchant Name are required")
188
+
189
+ if self.merchant_type == MerchantType.MERCHANT:
190
+ if not self.data.get("merchant_id") or not self.data.get("acquiring_bank"):
191
+ raise KHQRValidationError(
192
+ "Merchant ID and Acquiring Bank are required for merchant type"
193
+ )
File without changes
konthaina_khqr/tlv.py ADDED
@@ -0,0 +1,31 @@
1
+ from __future__ import annotations
2
+
3
+
4
+ def format_tag(tag: str, value: str) -> str:
5
+ """Format EMVCo tag-length-value segment."""
6
+ if value is None or value == "":
7
+ return ""
8
+ length = str(len(value)).zfill(2)
9
+ return f"{tag}{length}{value}"
10
+
11
+
12
+ def decode_tlv(payload: str) -> dict[str, str]:
13
+ """Decode a *flat* TLV string into a dict of tag->value.
14
+
15
+ Notes:
16
+ - KHQR has nested templates (e.g., tag 29/30/62/64). This helper is intentionally simple:
17
+ it only parses the top-level TLV into raw strings.
18
+ - Use this for debugging/logging; not as a full KHQR parser.
19
+ """
20
+ i = 0
21
+ out: dict[str, str] = {}
22
+ while i + 4 <= len(payload):
23
+ tag = payload[i : i + 2]
24
+ length = int(payload[i + 2 : i + 4])
25
+ start = i + 4
26
+ end = start + length
27
+ if end > len(payload):
28
+ break
29
+ out[tag] = payload[start:end]
30
+ i = end
31
+ return out
@@ -0,0 +1,14 @@
1
+ from __future__ import annotations
2
+
3
+ from .crc16 import crc16_ccitt_false
4
+
5
+
6
+ def verify(qr: str) -> bool:
7
+ """Verify KHQR string by checking CRC tag (63)."""
8
+ if not qr or len(qr) < 10:
9
+ return False
10
+ # CRC is last 4 hex chars; before it should contain "6304"
11
+ extracted_crc = qr[-4:]
12
+ body = qr[:-4] # includes 6304
13
+ expected = crc16_ccitt_false(body)
14
+ return extracted_crc.upper() == expected.upper()
@@ -0,0 +1,145 @@
1
+ Metadata-Version: 2.4
2
+ Name: konthaina-khqr
3
+ Version: 0.1.2
4
+ Summary: KHQR / EMVCo merchant-presented QR payload generator for Bakong (Cambodia), with CRC16 verification.
5
+ Author-email: Konthaina <konthaina87@gmail.com>
6
+ License: MIT License
7
+
8
+ Copyright (c) 2026 Konthaina
9
+
10
+ Permission is hereby granted, free of charge, to any person obtaining a copy
11
+ of this software and associated documentation files (the "Software"), to deal
12
+ in the Software without restriction, including without limitation the rights
13
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
14
+ copies of the Software, and to permit persons to whom the Software is
15
+ furnished to do so, subject to the following conditions:
16
+
17
+ The above copyright notice and this permission notice shall be included in all
18
+ copies or substantial portions of the Software.
19
+
20
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
21
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
22
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
23
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
24
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
25
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
26
+ SOFTWARE.
27
+
28
+ Project-URL: Homepage, https://github.com/yourname/konthaina-khqr
29
+ Project-URL: Repository, https://github.com/yourname/konthaina-khqr
30
+ Project-URL: Issues, https://github.com/yourname/konthaina-khqr/issues
31
+ Keywords: khqr,bakong,emvco,qr,cambodia
32
+ Classifier: Development Status :: 3 - Alpha
33
+ Classifier: License :: OSI Approved :: MIT License
34
+ Classifier: Programming Language :: Python :: 3
35
+ Classifier: Programming Language :: Python :: 3 :: Only
36
+ Classifier: Programming Language :: Python :: 3.9
37
+ Classifier: Programming Language :: Python :: 3.10
38
+ Classifier: Programming Language :: Python :: 3.11
39
+ Classifier: Programming Language :: Python :: 3.12
40
+ Classifier: Programming Language :: Python :: 3.13
41
+ Classifier: Typing :: Typed
42
+ Requires-Python: >=3.9
43
+ Description-Content-Type: text/markdown
44
+ License-File: LICENSE
45
+ Provides-Extra: qrcode
46
+ Requires-Dist: qrcode[pil]>=7.0; extra == "qrcode"
47
+ Provides-Extra: dev
48
+ Requires-Dist: pytest>=8; extra == "dev"
49
+ Requires-Dist: ruff>=0.6; extra == "dev"
50
+ Requires-Dist: mypy>=1.11; extra == "dev"
51
+ Requires-Dist: build>=1.2; extra == "dev"
52
+ Requires-Dist: twine>=5.1; extra == "dev"
53
+ Dynamic: license-file
54
+
55
+ # konthaina-khqr
56
+
57
+ KHQR / EMVCo merchant-presented QR payload generator for **Bakong / Cambodia** (NBC KHQR spec v2.7-style TLV) with **CRC-16/CCITT-FALSE** verification.
58
+
59
+ > This package generates the **payload string** (the EMV tag-length-value text) that you can encode into a QR image.
60
+
61
+ ## Install
62
+
63
+ ```bash
64
+ pip install konthaina-khqr
65
+ ```
66
+
67
+ Optional: generate QR images (PNG) using `qrcode`:
68
+
69
+ ```bash
70
+ pip install "konthaina-khqr[qrcode]"
71
+ ```
72
+
73
+ ## Quick start
74
+
75
+ ```python
76
+ from konthaina_khqr import KHQRGenerator, MerchantType, Currency
77
+
78
+ result = (
79
+ KHQRGenerator(MerchantType.INDIVIDUAL)
80
+ .set_bakong_account_id("john_smith@devb")
81
+ .set_merchant_name("John Smith")
82
+ .set_currency(Currency.USD)
83
+ .set_amount(100.50)
84
+ .set_merchant_city("Phnom Penh")
85
+ .generate()
86
+ )
87
+
88
+ print(result.qr) # KHQR payload string
89
+ print(result.md5) # md5 of payload
90
+ ```
91
+
92
+ ## Verify / decode
93
+
94
+ ```python
95
+ from konthaina_khqr import verify, decode
96
+
97
+ ok = verify(result.qr)
98
+ data = decode(result.qr) # simple TLV decode
99
+ ```
100
+
101
+ ## CLI
102
+
103
+ ```bash
104
+ khqr --type individual --bakong john_smith@devb --name "John Smith" --amount 1.25 --currency USD
105
+ ```
106
+
107
+ Generate a PNG (requires extras):
108
+
109
+ ```bash
110
+ khqr --type individual --bakong john_smith@devb --name "John Smith" --amount 1.25 --currency USD --png out.png
111
+ ```
112
+
113
+ ## Development
114
+
115
+ ```bash
116
+ python -m venv .venv
117
+ . .venv/bin/activate # Windows: .venv\Scripts\activate
118
+ pip install -e ".[dev]"
119
+ pytest
120
+ ruff check .
121
+ mypy src
122
+ ```
123
+
124
+ ## Build & publish
125
+
126
+ Build:
127
+
128
+ ```bash
129
+ python -m build
130
+ twine check dist/*
131
+ ```
132
+
133
+ Publish to **TestPyPI**:
134
+
135
+ ```bash
136
+ twine upload -r testpypi dist/*
137
+ ```
138
+
139
+ Publish to **PyPI**:
140
+
141
+ ```bash
142
+ twine upload dist/*
143
+ ```
144
+
145
+ For GitHub Actions + Trusted Publishing, see `.github/workflows/publish.yml`.
@@ -0,0 +1,16 @@
1
+ konthaina_khqr/__init__.py,sha256=Kj3FqXU-fQygbDIfEgXGUl2CqKSCUkj-ocGzkSh6Gtw,263
2
+ konthaina_khqr/cli.py,sha256=-9BtG1cETYo1W6GuKFmo8hR0d5w59SJUZplmHAdDP1k,2561
3
+ konthaina_khqr/crc16.py,sha256=tEvN5dpz3pyPTYRTQ81a8yDMZiNqs_IDEu4WDvM-LSQ,3437
4
+ konthaina_khqr/decode.py,sha256=tPtMF2di1c125oeCQlFk4kLUK_XpxIigaFu94sNxuhA,190
5
+ konthaina_khqr/enums.py,sha256=R0jAiNlVD4mhggdJgGHLOSrq6srZZZB8otcS-K4rMTs,308
6
+ konthaina_khqr/exceptions.py,sha256=JtpAkdWkVg7GY-PfgzcWocTq_N-sFOdSOXPbdJCPyJ8,106
7
+ konthaina_khqr/generator.py,sha256=NlEP_62kmsX8Xoh5oAseEzhqi66UGPd0DNptuj3zTuw,7180
8
+ konthaina_khqr/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
9
+ konthaina_khqr/tlv.py,sha256=-1tNFpunqeH5u8WktDCVh3S7owdkAPKHIXpoPjzl_kY,931
10
+ konthaina_khqr/verify.py,sha256=gbtY9ogyBnfffI743lc1JgONKQNUfutFf4sL-YCEfv8,430
11
+ konthaina_khqr-0.1.2.dist-info/licenses/LICENSE,sha256=SOTOaaGP2gKbSUcDvVAifOB9mbQPVOfUCohjHZ7RpdI,1066
12
+ konthaina_khqr-0.1.2.dist-info/METADATA,sha256=cAR-M9XjcHEGn1ThbTSKrZzBgpDp3SSrCRuGyBKmiEI,4330
13
+ konthaina_khqr-0.1.2.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
14
+ konthaina_khqr-0.1.2.dist-info/entry_points.txt,sha256=Zya-0-1FxubsDK7mZ4XBpaKsWppYnhXjeMFCRZHGy50,49
15
+ konthaina_khqr-0.1.2.dist-info/top_level.txt,sha256=1dRcgTNZi79QSczwDc0npVJVlCkBNcrTOx23kuIK2og,15
16
+ konthaina_khqr-0.1.2.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (80.9.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ khqr = konthaina_khqr.cli:main
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Konthaina
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
+ konthaina_khqr