thai-bank-transfer-qr 0.2.1 → 0.3.0
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.
- package/README.md +22 -0
- package/index.js +93 -9
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -22,12 +22,34 @@ const payload = buildThaiQRBankTransfer({
|
|
|
22
22
|
console.log(payload)
|
|
23
23
|
```
|
|
24
24
|
|
|
25
|
+
PromptPay phone number QR (Tag 29 subtag 01, amount optional):
|
|
26
|
+
|
|
27
|
+
```js
|
|
28
|
+
import { buildThaiQRPromptPayPhone } from "thai-bank-transfer-qr"
|
|
29
|
+
|
|
30
|
+
const phonePayload = buildThaiQRPromptPayPhone({
|
|
31
|
+
phoneNumber: "+66 81 234 5678", // accepts +66 / 66 / 0-leading
|
|
32
|
+
amount: 25.5, // optional; omit for open amount
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
console.log(phonePayload)
|
|
36
|
+
```
|
|
37
|
+
|
|
25
38
|
## API
|
|
26
39
|
|
|
27
40
|
- buildThaiQRBankTransfer(options)
|
|
28
41
|
- bankCode: string (3 digits)
|
|
29
42
|
- accountNumber: string (digits only)
|
|
30
43
|
- amount: number|string (formatted to 2 decimals)
|
|
44
|
+
- buildThaiQRPromptPayPhone(options)
|
|
45
|
+
- phoneNumber: string|number (Thai mobile in +66 / 66 / 0 format)
|
|
46
|
+
- amount?: number|string (optional fixed amount; omit to allow payer entry)
|
|
47
|
+
- Validators (throw on invalid input):
|
|
48
|
+
- validateBankCode(bankCode)
|
|
49
|
+
- validateAccountNumber(accountNumber)
|
|
50
|
+
- validateAmount(amount)
|
|
51
|
+
- validateOptionalAmount(amount) // returns null if empty/undefined
|
|
52
|
+
- validatePhoneNumber(phoneNumber) // returns normalized 0066 + 9 digits
|
|
31
53
|
|
|
32
54
|
Also exports: `tlv`, `crc16CCITT`.
|
|
33
55
|
|
package/index.js
CHANGED
|
@@ -29,6 +29,96 @@ export function crc16CCITT(input) {
|
|
|
29
29
|
return crc.toString(16).toUpperCase().padStart(4, "0");
|
|
30
30
|
}
|
|
31
31
|
|
|
32
|
+
export function validateBankCode(bankCode) {
|
|
33
|
+
if (!/^\d{3}$/.test(bankCode)) {
|
|
34
|
+
throw new Error("bankCode must be 3 digits (e.g. 004)");
|
|
35
|
+
}
|
|
36
|
+
return bankCode;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function validateAccountNumber(accountNumber) {
|
|
40
|
+
if (!/^\d+$/.test(accountNumber)) {
|
|
41
|
+
throw new Error("accountNumber must be digits only");
|
|
42
|
+
}
|
|
43
|
+
if (accountNumber.length < 10 || accountNumber.length > 15) {
|
|
44
|
+
throw new Error("accountNumber must be 10-15 digits");
|
|
45
|
+
}
|
|
46
|
+
return accountNumber;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function validateAmount(amount) {
|
|
50
|
+
const formatted = (
|
|
51
|
+
typeof amount === "number" ? amount : parseFloat(amount)
|
|
52
|
+
).toFixed(2);
|
|
53
|
+
if (isNaN(Number(formatted))) {
|
|
54
|
+
throw new Error("amount must be a number");
|
|
55
|
+
}
|
|
56
|
+
return formatted;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function validateOptionalAmount(amount) {
|
|
60
|
+
if (amount === undefined || amount === null || String(amount).trim() === "") {
|
|
61
|
+
return null;
|
|
62
|
+
}
|
|
63
|
+
return validateAmount(amount);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function normalizeThaiMobile(phoneNumber) {
|
|
67
|
+
const digits = String(phoneNumber || "").replace(/\D+/g, "");
|
|
68
|
+
let national = digits;
|
|
69
|
+
if (national.startsWith("0066")) {
|
|
70
|
+
national = national.slice(4);
|
|
71
|
+
} else if (national.startsWith("66")) {
|
|
72
|
+
national = national.slice(2);
|
|
73
|
+
} else if (national.startsWith("0")) {
|
|
74
|
+
national = national.slice(1);
|
|
75
|
+
}
|
|
76
|
+
if (!/^\d{9}$/.test(national)) {
|
|
77
|
+
throw new Error("phoneNumber must be a Thai mobile (+66/66/0) with 9 digits after country/leading zero is stripped");
|
|
78
|
+
}
|
|
79
|
+
return `0066${national}`;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export function validatePhoneNumber(phoneNumber) {
|
|
83
|
+
return normalizeThaiMobile(phoneNumber);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Build PromptPay QR payload for Thai mobile number (Tag 29 subtag 01)
|
|
88
|
+
* @param {Object} options
|
|
89
|
+
* @param {string|number} options.phoneNumber Thai mobile in +66 / 66 / 0 formats
|
|
90
|
+
* @param {number|string} [options.amount] Optional fixed THB amount
|
|
91
|
+
* @returns {string}
|
|
92
|
+
*/
|
|
93
|
+
export function buildThaiQRPromptPayPhone({ phoneNumber, amount }) {
|
|
94
|
+
const normalized = validatePhoneNumber(phoneNumber);
|
|
95
|
+
const formattedAmount = validateOptionalAmount(amount);
|
|
96
|
+
const hasAmount = formattedAmount !== null;
|
|
97
|
+
|
|
98
|
+
const tag29 = tlv(
|
|
99
|
+
"29",
|
|
100
|
+
tlv("00", "A000000677010111") + tlv("01", normalized)
|
|
101
|
+
);
|
|
102
|
+
|
|
103
|
+
const parts = [
|
|
104
|
+
tlv("00", "01"),
|
|
105
|
+
tlv("01", "11"),
|
|
106
|
+
tag29,
|
|
107
|
+
tlv("52", "0000"),
|
|
108
|
+
tlv("53", "764"),
|
|
109
|
+
];
|
|
110
|
+
|
|
111
|
+
if (hasAmount) {
|
|
112
|
+
parts.push(tlv("54", formattedAmount));
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
parts.push(tlv("58", "TH"));
|
|
116
|
+
|
|
117
|
+
const withoutCRC = parts.join("") + "6304";
|
|
118
|
+
const crc = crc16CCITT(withoutCRC);
|
|
119
|
+
return withoutCRC + crc;
|
|
120
|
+
}
|
|
121
|
+
|
|
32
122
|
/**
|
|
33
123
|
* Build Thai QR Bank Transfer payload (EMVCo + Thai spec)
|
|
34
124
|
* @param {Object} options
|
|
@@ -38,15 +128,9 @@ export function crc16CCITT(input) {
|
|
|
38
128
|
* @returns {string}
|
|
39
129
|
*/
|
|
40
130
|
export function buildThaiQRBankTransfer({ bankCode, accountNumber, amount }) {
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
throw new Error("accountNumber must be digits only");
|
|
45
|
-
|
|
46
|
-
const amt = (
|
|
47
|
-
typeof amount === "number" ? amount : parseFloat(amount)
|
|
48
|
-
).toFixed(2);
|
|
49
|
-
if (isNaN(Number(amt))) throw new Error("amount must be a number");
|
|
131
|
+
validateBankCode(bankCode);
|
|
132
|
+
validateAccountNumber(accountNumber);
|
|
133
|
+
const amt = validateAmount(amount);
|
|
50
134
|
|
|
51
135
|
// Tag 29 (Bank transfer template) → 00=AID, 03=bankCode+accountNumber
|
|
52
136
|
const tag29 = tlv(
|