thai_qr_pay 0.2.0 → 0.2.1

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 828bc541d1777d62f81d272d8da35181725baaebc3f574ca4d4b4a0db45685c4
4
- data.tar.gz: 7884145360ea3e7b7cb069b3c5a028f766dadedf6cb903e8eefaf4122a573dd1
3
+ metadata.gz: c9e83575512379820fcca739aa2ab4573f63f78c354aded6b6537e3fc70c4b0a
4
+ data.tar.gz: 19caeb222f1a43ef42d2cc6438f928f6b519e5f7df0b04647e3f79ae2546f416
5
5
  SHA512:
6
- metadata.gz: fac8a555db8d8abc8de436fcedd38894914d369ba25d2688144e1b7b9694c15a01acbd862983617878d7a2f87db6439d60962cf1b28ae0d9918b553ddcb043ff
7
- data.tar.gz: 50fbc9afbc67f268598d07b527e4031447f6961171c934a8443eb4a639de6328ad1b60a223c3f571f5190b1de37cbce2006fb0bf5ea600736e50804db3b08803
6
+ metadata.gz: fde0294a52a30ab085e58dbbe24bd2bcf1535b14b2196b2af37651133d2625a0ecb66cf7e0047c56424f23f10ef548796e29d5cc7d39a342d90ec8b6ec651c98
7
+ data.tar.gz: 1e4dcc046453c564d9a77c176ef3cd8a662ae832351888887c13085349df84408d00c891223d3c8f86f8864c69a7e029fd5c46a0f86ad947b57493d8c215bb65
@@ -21,8 +21,8 @@ module ThaiQrPay
21
21
  raise Error, 'Unknown type' unless PROXY.key?(type)
22
22
 
23
23
  if type == 'MSISDN'
24
- # replace leading 0 with 66
25
- target = target.sub(/^0/, '66')
24
+ t = target.sub(/^0/, '66')
25
+ target = t.rjust(13, '0')
26
26
  end
27
27
 
28
28
  sub = [
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'thai_qr_pay/tlv'
4
+ require 'thai_qr_pay/crc16'
5
+
6
+ module ThaiQrPay
7
+ module Generate
8
+ # Generates an EMVCo “slip verify” (mini-QR) payload
9
+ # Tag 00 = sub-TLV [000001, sendingBank, transRef], Tag 51 = 'TH', CRC tag = '91'
10
+ class SlipVerify
11
+ def self.payload(sending_bank:, trans_ref:)
12
+ inner = [
13
+ TLV.tag('00', '000001'),
14
+ TLV.tag('01', sending_bank),
15
+ TLV.tag('02', trans_ref)
16
+ ]
17
+ payload = [
18
+ TLV.tag('00', TLV.encode(inner)),
19
+ TLV.tag('51', 'TH')
20
+ ]
21
+ CRC16.with_crc(TLV.encode(payload), '91')
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,45 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'thai_qr_pay/tlv'
4
+ require 'thai_qr_pay/crc16'
5
+
6
+ module ThaiQrPay
7
+ module Generate
8
+ # Generates a TrueMoney “wallet” PromptPay-compatible QR
9
+ # Tag 29 = sub-TLV [A000000677010111, '03'+mobileNo], optional Tag 54 & Tag 81, CRC tag = '63'
10
+ class TrueMoney
11
+ def self.payload(mobile_no:, amount: nil, message: nil)
12
+ tag29 = TLV.encode([
13
+ TLV.tag('00', 'A000000677010111'),
14
+ TLV.tag('03', "14000#{mobile_no}")
15
+ ])
16
+
17
+ main = [
18
+ TLV.tag('00', '01'),
19
+ TLV.tag('01', amount ? '12' : '11'),
20
+ TLV.tag('29', tag29),
21
+ TLV.tag('53', '764'),
22
+ TLV.tag('58', 'TH')
23
+ ]
24
+ main << TLV.tag('54', format('%.2f', amount)) if amount
25
+
26
+ if message
27
+ # Tag 81 is UCS-2 BE hex of the message
28
+ encoded = encode_tag81(message)
29
+ main << TLV.tag('81', encoded)
30
+ end
31
+
32
+ CRC16.with_crc(TLV.encode(main), '63')
33
+ end
34
+
35
+ # @api private
36
+ def self.encode_tag81(message)
37
+ # TS: Buffer.from(msg,'utf16le').swap16().toString('hex').toUpperCase()
38
+ utf16le = message.encode('utf-16le')
39
+ # swap each pair of bytes
40
+ be_bytes = utf16le.bytes.each_slice(2).flat_map { |lo, hi| [hi, lo] }
41
+ be_bytes.pack('C*').unpack1('H*').upcase
42
+ end
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'thai_qr_pay/tlv'
4
+ require 'thai_qr_pay/crc16'
5
+
6
+ module ThaiQrPay
7
+ module Generate
8
+ # Generates a TrueMoney-specific slip-verify QR
9
+ # Tag 00 = sub-TLV [01,01,eventType,transactionId,date], CRC tag = '91'
10
+ class TrueMoneySlipVerify
11
+ def self.payload(event_type:, transaction_id:, date:)
12
+ inner = [
13
+ TLV.tag('00', '01'),
14
+ TLV.tag('01', '01'),
15
+ TLV.tag('02', event_type),
16
+ TLV.tag('03', transaction_id),
17
+ TLV.tag('04', date)
18
+ ]
19
+ payload = [TLV.tag('00', TLV.encode(inner))]
20
+ # per TS, CRC-tag '91', case-sensitive (we output uppercase)
21
+ CRC16.with_crc(TLV.encode(payload), '91')
22
+ end
23
+ end
24
+ end
25
+ end
@@ -2,5 +2,5 @@
2
2
  # frozen_string_literal: true
3
3
 
4
4
  module ThaiQrPay
5
- VERSION = '0.2.0'
5
+ VERSION = '0.2.1'
6
6
  end
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'thai_qr_pay/generate/slip_verify'
4
+ require 'thai_qr_pay/tlv'
5
+ require 'thai_qr_pay/crc16'
6
+
7
+ RSpec.describe ThaiQrPay::Generate::SlipVerify do
8
+ let(:bank) { 'KBank' }
9
+ let(:ref) { 'ABC123' }
10
+
11
+ subject(:payload) { described_class.payload(sending_bank: bank, trans_ref: ref) }
12
+
13
+ it 'returns a string' do
14
+ expect(payload).to be_a(String)
15
+ end
16
+
17
+ it 'contains Tag 00, Tag 51, and CRC Tag 91' do
18
+ tags = ThaiQrPay::TLV.decode(payload)
19
+ expect(tags.map(&:id)).to include('00', '51', '91')
20
+ tag51 = tags.find { |t| t.id == '51' }
21
+ expect(tag51.value).to eq('TH')
22
+ end
23
+
24
+ it 'embeds sending_bank and trans_ref as subtags of Tag 00' do
25
+ tag00 = ThaiQrPay::TLV.decode(payload).find { |t| t.id == '00' }
26
+ subtags = ThaiQrPay::TLV.decode(tag00.value)
27
+ expect(subtags.find { |st| st.id == '01' }.value).to eq(bank)
28
+ expect(subtags.find { |st| st.id == '02' }.value).to eq(ref)
29
+ end
30
+
31
+ it 'appends correct CRC using tag 91' do
32
+ body = payload[0..-5] # everything before the CRC hex
33
+ expected = ThaiQrPay::CRC16.checksum(body)
34
+ expect(payload).to end_with("9104#{expected}")
35
+ end
36
+ end
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'thai_qr_pay/generate/true_money_slip_verify'
4
+ require 'thai_qr_pay/tlv'
5
+ require 'thai_qr_pay/crc16'
6
+
7
+ RSpec.describe ThaiQrPay::Generate::TrueMoneySlipVerify do
8
+ let(:event) { 'PAYMENT' }
9
+ let(:txid) { 'TX123456' }
10
+ let(:date) { '20250615' }
11
+
12
+ subject(:payload) { described_class.payload(event_type: event, transaction_id: txid, date: date) }
13
+
14
+ it 'produces exactly two tags: 00 and 91' do
15
+ tags = ThaiQrPay::TLV.decode(payload)
16
+ expect(tags.map(&:id)).to eq(%w[00 91])
17
+ end
18
+
19
+ it 'nests subtags 00..04 correctly under Tag 00' do
20
+ inner = ThaiQrPay::TLV.decode(payload).find { |t| t.id == '00' }
21
+ subtags = ThaiQrPay::TLV.decode(inner.value)
22
+ expect(subtags.map(&:id)).to eq(%w[00 01 02 03 04])
23
+ expect(subtags.find { |st| st.id == '02' }.value).to eq(event)
24
+ expect(subtags.find { |st| st.id == '03' }.value).to eq(txid)
25
+ expect(subtags.find { |st| st.id == '04' }.value).to eq(date)
26
+ end
27
+
28
+ it 'appends correct CRC using Tag 91' do
29
+ body = payload[0..-5]
30
+ expected = ThaiQrPay::CRC16.checksum(body)
31
+ expect(payload).to end_with("9104#{expected}")
32
+ end
33
+ end
@@ -0,0 +1,79 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'thai_qr_pay/generate/true_money'
4
+ require 'thai_qr_pay/tlv'
5
+ require 'thai_qr_pay/crc16'
6
+
7
+ RSpec.describe ThaiQrPay::Generate::TrueMoney do
8
+ let(:mobile) { '0812345678' }
9
+ let(:amount) { 123.45 }
10
+ let(:message) { 'Hello' }
11
+
12
+ context 'without amount or message' do
13
+ subject(:payload) { described_class.payload(mobile_no: mobile) }
14
+
15
+ it 'uses point-of-initiation 11' do
16
+ tag01 = ThaiQrPay::TLV.decode(payload).find { |t| t.id == '01' }
17
+ expect(tag01.value).to eq('11')
18
+ end
19
+
20
+ it 'includes correct sub-tags in Tag 29' do
21
+ sub = ThaiQrPay::TLV.decode(payload).find { |t| t.id == '29' }
22
+ subtags = ThaiQrPay::TLV.decode(sub.value)
23
+ expect(subtags.find { |st| st.id == '00' }.value).to eq('A000000677010111')
24
+ expect(subtags.find { |st| st.id == '03' }.value).to eq("14000#{mobile}")
25
+ end
26
+
27
+ it 'does not include Tag 54 nor Tag 81' do
28
+ tags = ThaiQrPay::TLV.decode(payload)
29
+ ids = tags.map(&:id)
30
+ expect(ids).not_to include('54')
31
+ expect(ids).not_to include('81')
32
+ end
33
+
34
+ it 'ends with CRC Tag 63' do
35
+ expect(payload).to match(/63..[0-9A-F]{4}\z/)
36
+ end
37
+ end
38
+
39
+ context 'with amount only' do
40
+ subject(:payload) { described_class.payload(mobile_no: mobile, amount: amount) }
41
+
42
+ it 'uses point-of-initiation 12' do
43
+ tag01 = ThaiQrPay::TLV.decode(payload).find { |t| t.id == '01' }
44
+ expect(tag01.value).to eq('12')
45
+ end
46
+
47
+ it 'includes Tag 54 formatted to two decimals' do
48
+ tag54 = ThaiQrPay::TLV.decode(payload).find { |t| t.id == '54' }
49
+ expect(tag54.value).to eq('123.45')
50
+ end
51
+
52
+ it 'does not include Tag 81' do
53
+ ids = ThaiQrPay::TLV.decode(payload).map(&:id)
54
+ expect(ids).not_to include('81')
55
+ end
56
+ end
57
+
58
+ context 'with message only' do
59
+ subject(:payload) { described_class.payload(mobile_no: mobile, message: message) }
60
+
61
+ it 'includes Tag 81 with UCS-2 BE hex value' do
62
+ tag81 = ThaiQrPay::TLV.decode(payload).find { |t| t.id == '81' }
63
+ # "Hello" in UCS-2 BE hex:
64
+ expect(tag81.value).to eq('00480065006C006C006F')
65
+ end
66
+
67
+ it 'does not include Tag 54' do
68
+ expect(payload).not_to include('54')
69
+ end
70
+ end
71
+
72
+ context 'with both amount and message' do
73
+ subject(:payload) { described_class.payload(mobile_no: mobile, amount: amount, message: message) }
74
+
75
+ it 'includes both Tag 54 and Tag 81' do
76
+ expect(payload).to include('54', '81')
77
+ end
78
+ end
79
+ end
data/spec/parser_spec.rb CHANGED
@@ -43,7 +43,7 @@ RSpec.describe ThaiQrPay::Parser do
43
43
 
44
44
  it 'fetches nested sub-tag value' do
45
45
  # Tag '29' contains sub-tags; '01' inside gives the MSISDN with 66 prefix
46
- expected_msisdn = mobile.sub(/^0/, '66')
46
+ expected_msisdn = mobile.sub(/^0/, '66').rjust(13, '0')
47
47
  expect(parser.get_tag_value('29', '01')).to eq(expected_msisdn)
48
48
  end
49
49
 
@@ -25,7 +25,7 @@ RSpec.describe ThaiQrPay do
25
25
  expect(parser.get_tag_value('01')).to eq('12') # Point-of-Initiation Method
26
26
 
27
27
  # Sub-tag 29.01 contains the converted mobile number (no padding)
28
- expected_msisdn = mobile.sub(/^0/, '66')
28
+ expected_msisdn = mobile.sub(/^0/, '66').rjust(13, '0')
29
29
  expect(parser.get_tag_value('29', '01')).to eq(expected_msisdn)
30
30
 
31
31
  expect(parser.valid_crc?).to be true
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: thai_qr_pay
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.0
4
+ version: 0.2.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Chayut Orapinpatipat
@@ -61,12 +61,18 @@ files:
61
61
  - lib/thai_qr_pay/crc16.rb
62
62
  - lib/thai_qr_pay/generate/promptpay/any_id.rb
63
63
  - lib/thai_qr_pay/generate/promptpay/bill_payment.rb
64
+ - lib/thai_qr_pay/generate/slip_verify.rb
65
+ - lib/thai_qr_pay/generate/true_money.rb
66
+ - lib/thai_qr_pay/generate/true_money_slip_verify.rb
64
67
  - lib/thai_qr_pay/parser.rb
65
68
  - lib/thai_qr_pay/tlv.rb
66
69
  - lib/thai_qr_pay/validate/slip_verify.rb
67
70
  - lib/thai_qr_pay/validate/true_money_slip_verify.rb
68
71
  - lib/thai_qr_pay/version.rb
69
72
  - spec/crc16_spec.rb
73
+ - spec/generate/slip_verify_spec.rb
74
+ - spec/generate/true_money_slip_verify_spec.rb
75
+ - spec/generate/true_money_spec.rb
70
76
  - spec/parser_spec.rb
71
77
  - spec/thai_qr_pay_spec.rb
72
78
  - spec/tlv_spec.rb