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.
Files changed (3) hide show
  1. package/README.md +22 -0
  2. package/index.js +93 -9
  3. 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
- if (!/^\d{3}$/.test(bankCode))
42
- throw new Error("bankCode must be 3 digits (e.g. 004)");
43
- if (!/^\d+$/.test(accountNumber))
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(
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "thai-bank-transfer-qr",
3
- "version": "0.2.1",
3
+ "version": "0.3.0",
4
4
  "description": "Thai bank transfer QR payload builder (PromptPay – Tag 29 + CRC16)",
5
5
  "type": "module",
6
6
  "main": "index.js",