promptpay-qrcode 1.1.0 → 1.2.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 +30 -1
- package/cli.js +7 -1
- package/decode.js +79 -3
- package/index.js +2 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -142,6 +142,34 @@ d.tags; // ordered [{ id, length, value }] of the top level
|
|
|
142
142
|
|
|
143
143
|
It throws on a malformed payload (a declared length running past the string).
|
|
144
144
|
|
|
145
|
+
### Detecting supported payment channels
|
|
146
|
+
|
|
147
|
+
A KShop/merchant QR carries a separate template per enrolled payment rail, so an
|
|
148
|
+
omitted template means that channel isn't offered. `channels(qr)` reports them:
|
|
149
|
+
|
|
150
|
+
```js
|
|
151
|
+
const { channels } = require('promptpay-qrcode');
|
|
152
|
+
|
|
153
|
+
channels(kshopQr);
|
|
154
|
+
// {
|
|
155
|
+
// promptpay: true,
|
|
156
|
+
// creditCard: true, // false if the merchant isn't card-enabled
|
|
157
|
+
// networks: ['visa', 'mastercard', 'unionpay'],
|
|
158
|
+
// promptpayTemplates: ['30', '31'],
|
|
159
|
+
// cardTemplates: ['02', '04', '15', '51'],
|
|
160
|
+
// }
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
So a KShop account configured **without** credit-card acceptance produces a QR
|
|
164
|
+
with no card templates, and `channels()` returns `creditCard: false`,
|
|
165
|
+
`networks: []`. PromptPay rails are detected from tags `29`/`30`/`31`; card
|
|
166
|
+
networks from the EMVCo template ranges (`02`–`16`) and from card RIDs in the
|
|
167
|
+
generic `26`–`51` range. `detach(qr)` also includes this under `.channels`.
|
|
168
|
+
|
|
169
|
+
> This reflects what the merchant has **enrolled** (capability advertised by the
|
|
170
|
+
> QR). Whether a specific card actually authorizes is still the acquirer's call
|
|
171
|
+
> at settlement.
|
|
172
|
+
|
|
145
173
|
### Cloning another KShop account from its master QR
|
|
146
174
|
|
|
147
175
|
`kshopParamsFrom(qr)` pulls out exactly the account-identifying fields you'd
|
|
@@ -276,8 +304,9 @@ The second `options` argument is passed straight through to `qrcode`
|
|
|
276
304
|
| `decode(payload)` | structured decode + CRC validation |
|
|
277
305
|
| `parseTLV(payload)` | low-level ordered `[{ id, length, value }]` |
|
|
278
306
|
| `kshopParamsFrom(qr)` | account params to clone a KShop master QR |
|
|
279
|
-
| `detach(qr)` | `{ type, account, transaction, decoded }` for any QR type |
|
|
307
|
+
| `detach(qr)` | `{ type, account, transaction, channels, decoded }` for any QR type |
|
|
280
308
|
| `detectType(fields)` | `'promptpay'` \| `'billpayment'` \| `'kshop'` \| `'unknown'` |
|
|
309
|
+
| `channels(qr)` | `{ promptpay, creditCard, networks, promptpayTemplates, cardTemplates }` |
|
|
281
310
|
| `crc16Ccitt(str)` / `crc16Hex(str)` | CRC16-CCITT (number / 4-char hex) |
|
|
282
311
|
| `formatMobile(str)` | 13-char PromptPay mobile proxy |
|
|
283
312
|
| `toFile(path, payload, opts?)` | `Promise<void>` — write PNG file *(needs `qrcode`)* |
|
package/cli.js
CHANGED
|
@@ -61,15 +61,21 @@ function main() {
|
|
|
61
61
|
}
|
|
62
62
|
|
|
63
63
|
if (json) {
|
|
64
|
-
console.log(JSON.stringify({ type: r.type, account: r.account, transaction: r.transaction, crc: d.crc }, null, 2));
|
|
64
|
+
console.log(JSON.stringify({ type: r.type, channels: r.channels, account: r.account, transaction: r.transaction, crc: d.crc }, null, 2));
|
|
65
65
|
return;
|
|
66
66
|
}
|
|
67
67
|
|
|
68
68
|
const ok = d.crc && d.crc.valid;
|
|
69
|
+
const ch = r.channels;
|
|
70
|
+
const channelStr =
|
|
71
|
+
(ch.promptpay ? 'PromptPay' : '') +
|
|
72
|
+
(ch.creditCard ? (ch.promptpay ? ' + ' : '') + 'Card (' + ch.networks.join(', ') + ')' : '') ||
|
|
73
|
+
'(none)';
|
|
69
74
|
console.log('Type : ' + r.type);
|
|
70
75
|
console.log('CRC : ' + (d.crc ? d.crc.value : '(none)') + (ok ? ' ✓ valid' : ' ✗ INVALID (expected ' + (d.crc && d.crc.expected) + ')'));
|
|
71
76
|
console.log('POI : ' + d.poiMethod + (d.static ? ' (static)' : ' (dynamic)'));
|
|
72
77
|
console.log('Amount : ' + (d.amount == null ? '(none — payer enters)' : d.amount));
|
|
78
|
+
console.log('Channels : ' + channelStr);
|
|
73
79
|
if (d.merchantName) console.log('Merchant : ' + d.merchantName + (d.merchantCity ? ' / ' + d.merchantCity : ''));
|
|
74
80
|
console.log('\nAccount (reusable):');
|
|
75
81
|
console.log(JSON.stringify(r.account, null, 2));
|
package/decode.js
CHANGED
|
@@ -174,6 +174,82 @@ function detectType(fields) {
|
|
|
174
174
|
return 'unknown';
|
|
175
175
|
}
|
|
176
176
|
|
|
177
|
+
// PromptPay merchant-account templates -> rail name.
|
|
178
|
+
const PROMPTPAY_TAG = { '29': 'credit-transfer', '30': 'bill-payment', '31': 'payment-innovation' };
|
|
179
|
+
|
|
180
|
+
// EMVCo fixed template-ID allocations for card networks.
|
|
181
|
+
const CARD_NETWORK_BY_TAG = {
|
|
182
|
+
'02': 'visa', '03': 'visa',
|
|
183
|
+
'04': 'mastercard', '05': 'mastercard',
|
|
184
|
+
'09': 'discover', '10': 'discover',
|
|
185
|
+
'11': 'amex', '12': 'amex',
|
|
186
|
+
'13': 'jcb', '14': 'jcb',
|
|
187
|
+
'15': 'unionpay', '16': 'unionpay',
|
|
188
|
+
};
|
|
189
|
+
|
|
190
|
+
// Card-network Registered Application Provider IDs (RID = first 10 chars of AID),
|
|
191
|
+
// used to classify the generic merchant-template range 26-51 by its sub-tag 00.
|
|
192
|
+
const CARD_RID = {
|
|
193
|
+
A000000003: 'visa', A000000004: 'mastercard', A000000025: 'amex',
|
|
194
|
+
A000000065: 'jcb', A000000152: 'discover', A000000333: 'unionpay',
|
|
195
|
+
};
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* Detect which payment rails a QR advertises. KShop (and other merchant QRs)
|
|
199
|
+
* include a separate template per enrolled rail, so an omitted template means
|
|
200
|
+
* that channel is not offered — e.g. a merchant not configured to accept cards
|
|
201
|
+
* has no card templates.
|
|
202
|
+
*
|
|
203
|
+
* Note: this reflects what the merchant has ENROLLED (capability advertised by
|
|
204
|
+
* the QR). Whether a given card actually authorizes is still the acquirer's
|
|
205
|
+
* decision at settlement.
|
|
206
|
+
*
|
|
207
|
+
* @param {string|object} qr A payload string or a prior decode() result.
|
|
208
|
+
* @returns {{
|
|
209
|
+
* promptpay: boolean, creditCard: boolean,
|
|
210
|
+
* networks: string[],
|
|
211
|
+
* promptpayTemplates: string[], cardTemplates: string[]
|
|
212
|
+
* }}
|
|
213
|
+
*/
|
|
214
|
+
function channels(qr) {
|
|
215
|
+
const d = typeof qr === 'string' ? decode(qr) : qr;
|
|
216
|
+
const networks = new Set();
|
|
217
|
+
const promptpayTemplates = [];
|
|
218
|
+
const cardTemplates = [];
|
|
219
|
+
|
|
220
|
+
for (const { id } of d.tags) {
|
|
221
|
+
if (PROMPTPAY_TAG[id]) {
|
|
222
|
+
promptpayTemplates.push(id);
|
|
223
|
+
continue;
|
|
224
|
+
}
|
|
225
|
+
if (CARD_NETWORK_BY_TAG[id]) {
|
|
226
|
+
networks.add(CARD_NETWORK_BY_TAG[id]);
|
|
227
|
+
cardTemplates.push(id);
|
|
228
|
+
continue;
|
|
229
|
+
}
|
|
230
|
+
// Generic merchant-template range 26-51: classify by AID/RID in sub-tag 00.
|
|
231
|
+
const n = parseInt(id, 10);
|
|
232
|
+
if (n >= 26 && n <= 51 && typeof d.fields[id] === 'object') {
|
|
233
|
+
const aid = d.fields[id]['00'] || '';
|
|
234
|
+
if (aid.startsWith('A000000677')) {
|
|
235
|
+
// A PromptPay template living in the 26-51 range (rare); count it too.
|
|
236
|
+
if (!promptpayTemplates.includes(id)) promptpayTemplates.push(id);
|
|
237
|
+
} else if (CARD_RID[aid.slice(0, 10)]) {
|
|
238
|
+
networks.add(CARD_RID[aid.slice(0, 10)]);
|
|
239
|
+
cardTemplates.push(id);
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
return {
|
|
245
|
+
promptpay: promptpayTemplates.length > 0,
|
|
246
|
+
creditCard: networks.size > 0,
|
|
247
|
+
networks: [...networks],
|
|
248
|
+
promptpayTemplates,
|
|
249
|
+
cardTemplates,
|
|
250
|
+
};
|
|
251
|
+
}
|
|
252
|
+
|
|
177
253
|
/**
|
|
178
254
|
* Detach a master QR into its reusable account info and its per-transaction
|
|
179
255
|
* values. Works for all three types (auto-detected):
|
|
@@ -189,7 +265,7 @@ function detectType(fields) {
|
|
|
189
265
|
* kshop -> generateKShopQR(transaction.amount, transaction.reference, account)
|
|
190
266
|
*
|
|
191
267
|
* @param {string|object} qr A payload string or a prior decode() result.
|
|
192
|
-
* @returns {{ type: string, account: object, transaction: object, decoded: object }}
|
|
268
|
+
* @returns {{ type: string, account: object, transaction: object, channels: object, decoded: object }}
|
|
193
269
|
*/
|
|
194
270
|
function detach(qr) {
|
|
195
271
|
const d = typeof qr === 'string' ? decode(qr) : qr;
|
|
@@ -227,7 +303,7 @@ function detach(qr) {
|
|
|
227
303
|
transaction = { amount: d.amount };
|
|
228
304
|
}
|
|
229
305
|
|
|
230
|
-
return { type, account, transaction, decoded: d };
|
|
306
|
+
return { type, account, transaction, channels: channels(d), decoded: d };
|
|
231
307
|
}
|
|
232
308
|
|
|
233
|
-
module.exports = { decode, parseTLV, kshopParamsFrom, detach, detectType };
|
|
309
|
+
module.exports = { decode, parseTLV, kshopParamsFrom, detach, detectType, channels };
|
package/index.js
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
const { crc16Ccitt, crc16Hex } = require('./crc');
|
|
4
4
|
const { generatePromptPay, generateBillPayment, formatMobile } = require('./promptpay');
|
|
5
5
|
const { generateKShopQR, KSHOP_DEFAULTS, AID_PAYMENT_INNOVATION, AID_PAYMENT_INNOVATION_BOT } = require('./kshop');
|
|
6
|
-
const { decode, parseTLV, kshopParamsFrom, detach, detectType } = require('./decode');
|
|
6
|
+
const { decode, parseTLV, kshopParamsFrom, detach, detectType, channels } = require('./decode');
|
|
7
7
|
const image = require('./image');
|
|
8
8
|
|
|
9
9
|
module.exports = {
|
|
@@ -22,6 +22,7 @@ module.exports = {
|
|
|
22
22
|
kshopParamsFrom,
|
|
23
23
|
detach,
|
|
24
24
|
detectType,
|
|
25
|
+
channels,
|
|
25
26
|
// Optional image helpers (require the `qrcode` package).
|
|
26
27
|
image,
|
|
27
28
|
toFile: image.toFile,
|
package/package.json
CHANGED