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 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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "promptpay-qrcode",
3
- "version": "1.1.0",
3
+ "version": "1.2.0",
4
4
  "description": "Zero-dependency PromptPay QR payload generator (PromptPay P2P, Tag 30 bill payment / Mae Manee, and KShop)",
5
5
  "main": "index.js",
6
6
  "type": "commonjs",