thai_qr_pay 0.1.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.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 10ad6dfb66ac4d72976350536d490f9b342d04de47a592ad41620d8ad89c6e4e
4
+ data.tar.gz: b1f04737a1e41e736a109fd82a5a1921fc7cd0c191c3bfce632df7eb56953d20
5
+ SHA512:
6
+ metadata.gz: b1a848f79452f2c9adb643c88af872a599900445a6023c3e4174e0d2dde48603e7699043696c46ebfa985036f510a1225053496f22814bc3df935f14a08e2369
7
+ data.tar.gz: dc8aa130c9389eca193eeebeeb9368cf0b6dc9daa090b1504e36b989256d1e8a186a194cf0c85000b19c8ff54aa523918c5f2e24c9a5a32acfe8bef3bf1cfbd9
data/.gitignore ADDED
@@ -0,0 +1,56 @@
1
+ *.gem
2
+ *.rbc
3
+ /.config
4
+ /coverage/
5
+ /InstalledFiles
6
+ /pkg/
7
+ /spec/reports/
8
+ /spec/examples.txt
9
+ /test/tmp/
10
+ /test/version_tmp/
11
+ /tmp/
12
+
13
+ # Used by dotenv library to load environment variables.
14
+ # .env
15
+
16
+ # Ignore Byebug command history file.
17
+ .byebug_history
18
+
19
+ ## Specific to RubyMotion:
20
+ .dat*
21
+ .repl_history
22
+ build/
23
+ *.bridgesupport
24
+ build-iPhoneOS/
25
+ build-iPhoneSimulator/
26
+
27
+ ## Specific to RubyMotion (use of CocoaPods):
28
+ #
29
+ # We recommend against adding the Pods directory to your .gitignore. However
30
+ # you should judge for yourself, the pros and cons are mentioned at:
31
+ # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control
32
+ #
33
+ # vendor/Pods/
34
+
35
+ ## Documentation cache and generated files:
36
+ /.yardoc/
37
+ /_yardoc/
38
+ /doc/
39
+ /rdoc/
40
+
41
+ ## Environment normalization:
42
+ /.bundle/
43
+ /vendor/bundle
44
+ /lib/bundler/man/
45
+
46
+ # for a library or gem, you might want to ignore these files since the code is
47
+ # intended to run in multiple environments; otherwise, check them in:
48
+ Gemfile.lock
49
+ .ruby-version
50
+ .ruby-gemset
51
+
52
+ # unless supporting rvm < 1.11.0 or doing something fancy, ignore this:
53
+ .rvmrc
54
+
55
+ # Used by RuboCop. Remote config files pulled in from inherit_from directive.
56
+ # .rubocop-https?--*
data/Gemfile ADDED
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Gemfile
4
+ source 'https://rubygems.org'
5
+ gemspec
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Chayut Orapinpatipat
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,124 @@
1
+ # Thai QR Pay / ไทยคิวอาร์เพย์
2
+
3
+ A Ruby toolkit for Thailand’s EMVCo‐QR payments—PromptPay (AnyID & Bill Payment), TrueMoney, slip-verify, and BOT-barcode-to-QR conversion.
4
+ It provides:
5
+
6
+ - ASCII-numeric TLV parsing & encoding
7
+ - EMVCo CRC-16/XMODEM checksum computation & validation
8
+ - PromptPay payload generators (AnyID, Bill Payment)
9
+ - BOT barcode ↔ EMVCo QR Tag-30 conversion
10
+ - Slip-verify & TrueMoney slip-verify validators
11
+
12
+
13
+ Ruby Gem สำหรับการชำระเงินผ่าน QR ตามมาตรฐาน EMVCo ของประเทศไทย รองรับ PromptPay (AnyID & Bill Payment), TrueMoney, slip-verify และการแปลง BOT barcode เป็น EMVCo QR
14
+ ฟีเจอร์:
15
+
16
+ - การแยกวิเคราะห์ (parse) และสร้าง (encode) TLV แบบตัวเลข ASCII
17
+ - คำนวณและตรวจสอบค่า CRC-16/XMODEM ตามสเปค EMVCo
18
+ - สร้าง Payload สำหรับ PromptPay AnyID และ Bill Payment
19
+ - แปลง BOT barcode ↔ EMVCo QR Tag-30
20
+ - ตรวจสอบความถูกต้องของ Slip-verify และ TrueMoney slip-verify
21
+
22
+ ---
23
+
24
+ ## Installation / การติดตั้ง
25
+
26
+
27
+ ```bash
28
+ gem install thai_qr_pay
29
+ ````
30
+
31
+ ---
32
+
33
+ ## Usage / การใช้งาน
34
+
35
+ ```ruby
36
+ require 'thai_qr_pay'
37
+ ```
38
+
39
+ ### 1. Parsing a QR payload / วิเคราะห์ QR payload
40
+
41
+
42
+ ```ruby
43
+ qr_string = '000201010212...6304ABCD'
44
+ parser = ThaiQrPay::Parser.new(qr_string, strict: true)
45
+
46
+ puts parser.get_tag_value('00') # => '01'
47
+ puts parser.get_tag_value('29', '01') # nested sub-tag
48
+ ```
49
+
50
+ ---
51
+
52
+ ### 2. Generating PromptPay AnyID / สร้าง PromptPay AnyID
53
+
54
+ ```ruby
55
+ payload = ThaiQrPay::Generate::Promptpay::AnyID.payload(
56
+ type: 'MSISDN',
57
+ target: '0812345678',
58
+ amount: 100.0
59
+ )
60
+ # => "00020101021229...6304XXXX"
61
+ ```
62
+
63
+
64
+
65
+ ---
66
+
67
+ ### 3. Generating PromptPay Bill Payment / สร้าง PromptPay Bill Payment
68
+
69
+
70
+ ```ruby
71
+ payload = ThaiQrPay::Generate::Promptpay::BillPayment.payload(
72
+ biller_id: '1234567890123',
73
+ ref1: 'INV001',
74
+ amount: 250.50
75
+ )
76
+ ```
77
+
78
+
79
+ ---
80
+
81
+ ### 4. Converting BOT Barcode → QR Tag 30 / แปลง BOT Barcode เป็น QR Tag 30
82
+
83
+
84
+ ```ruby
85
+ raw = "|1234567890123\rINV001\r\r012345"
86
+ bot = ThaiQrPay::BOTBarcode.from_string(raw)
87
+ qr_tag30 = bot.to_qr_tag30
88
+ ```
89
+
90
+ ---
91
+
92
+ ### 5. Validating Slip Verify / ตรวจสอบ Slip-verify
93
+
94
+
95
+ ```ruby
96
+ result = ThaiQrPay::Validate::SlipVerify.call(qr_string)
97
+ if result
98
+ puts result[:sending_bank]
99
+ puts result[:trans_ref]
100
+ else
101
+ puts 'Invalid Slip-verify QR'
102
+ end
103
+ ```
104
+
105
+ ---
106
+
107
+ ### 6. Validating TrueMoney Slip Verify / ตรวจสอบ TrueMoney Slip-verify
108
+
109
+ ```ruby
110
+ tm_res = ThaiQrPay::Validate::TrueMoneySlipVerify.call(qr_string)
111
+ if tm_res
112
+ puts tm_res[:event_type]
113
+ puts tm_res[:transaction_id]
114
+ puts tm_res[:date]
115
+ else
116
+ puts 'Invalid TrueMoney Slip-verify QR'
117
+ end
118
+ ```
119
+
120
+
121
+ ## License
122
+
123
+ © 2025 Chayut Orapinpatipat
124
+ Released under the MIT License. See [LICENSE.txt](LICENSE.txt) for details.
data/Rakefile ADDED
@@ -0,0 +1,13 @@
1
+ # Rakefile
2
+ # frozen_string_literal: true
3
+
4
+ require 'bundler/gem_tasks' # provides build, install, release, etc.
5
+ require 'rspec/core/rake_task' # hook up RSpec
6
+
7
+ # Define the spec task
8
+ RSpec::Core::RakeTask.new(:spec) do |t|
9
+ t.pattern = 'spec/**/*_spec.rb'
10
+ end
11
+
12
+ # Default task: run specs
13
+ task default: :spec
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ThaiQrPay
4
+ # Parses BOT barcode strings and converts them into EMVCo QR Tag-30 payloads.
5
+ class BOTBarcode
6
+ attr_reader :biller_id, :ref1, :ref2, :amount, :_raw_amount
7
+
8
+ # str: raw BOT barcode like "|<biller>\r<ref1>\r<ref2>\r<amt>"
9
+ def self.from_string(str)
10
+ return unless str.start_with?('|')
11
+
12
+ parts = str[1..].split("\r", 4)
13
+ return unless parts.size == 4
14
+
15
+ bid, r1, r2, amt_str = parts
16
+ amount = amt_str != '0' ? (amt_str.to_i / 100.0) : nil
17
+ new(bid, r1, (r2.empty? ? nil : r2), amount, amt_str)
18
+ end
19
+
20
+ def initialize(biller_id, ref1, ref2 = nil, amount = nil, amt_str = nil)
21
+ @biller_id = biller_id
22
+ @ref1 = ref1
23
+ @ref2 = ref2
24
+ @amount = amount
25
+ @_raw_amount = amt_str || (amount ? format('%d', (amount * 100).to_i) : '0')
26
+ end
27
+
28
+ # Reproduce exactly the original barcode string, including any leading zeros
29
+ def to_s
30
+ "|#{biller_id}\r#{ref1}\r#{ref2 || ''}\r#{_raw_amount}"
31
+ end
32
+
33
+ def to_qr_tag30
34
+ ThaiQrPay::Generate::Promptpay::BillPayment.payload(
35
+ biller_id: biller_id,
36
+ amount: amount,
37
+ ref1: ref1,
38
+ ref2: ref2
39
+ )
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ThaiQrPay
4
+ # CRC-16/XMODEM checksum routines for EMVCo QR (poly=0x1021, init=0xFFFF).
5
+ module CRC16
6
+ # Compute CRC-16/XMODEM (poly=0x1021, init=0xFFFF) bitwise
7
+ def self.xmodem(data)
8
+ crc = 0xFFFF
9
+ data.bytes.each do |b|
10
+ crc ^= (b << 8)
11
+ 8.times do
12
+ crc = if (crc & 0x8000) != 0
13
+ ((crc << 1) ^ 0x1021) & 0xFFFF
14
+ else
15
+ (crc << 1) & 0xFFFF
16
+ end
17
+ end
18
+ end
19
+ crc
20
+ end
21
+
22
+ # Return 4-digit uppercase hex CRC
23
+ def self.checksum(payload)
24
+ xmodem(payload).to_s(16).upcase.rjust(4, '0')
25
+ end
26
+
27
+ # Append "<crc_tag>04<CRC>" to a TLV string
28
+ def self.with_crc(tlv_string, crc_tag = '63')
29
+ buffer = tlv_string + "#{crc_tag}04"
30
+ buffer + checksum(buffer)
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,48 @@
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
+ module Promptpay
9
+ # Generates PromptPay “AnyID” (mobile/NationalID/etc.) EMVCo QR payloads.
10
+ class AnyID
11
+ PROXY = {
12
+ 'MSISDN' => '01',
13
+ 'NATID' => '02',
14
+ 'EWALLETID' => '03',
15
+ 'BANKACC' => '04'
16
+ }.freeze
17
+
18
+ # type: one of 'MSISDN','NATID','EWALLETID','BANKACC'
19
+ # target: the ID string; amount: optional numeric
20
+ def self.payload(type:, target:, amount: nil)
21
+ raise Error, 'Unknown type' unless PROXY.key?(type)
22
+
23
+ if type == 'MSISDN'
24
+ # replace leading 0 with 66
25
+ target = target.sub(/^0/, '66')
26
+ end
27
+
28
+ sub = [
29
+ TLV.tag('00', 'A000000677010111'),
30
+ TLV.tag(PROXY[type], target)
31
+ ]
32
+ tag29 = TLV.encode(sub)
33
+
34
+ main = [
35
+ TLV.tag('00', '01'),
36
+ TLV.tag('01', amount ? '12' : '11'),
37
+ TLV.tag('29', tag29),
38
+ TLV.tag('53', '764'),
39
+ TLV.tag('58', 'TH')
40
+ ]
41
+ main << TLV.tag('54', format('%.2f', amount)) if amount
42
+
43
+ CRC16.with_crc(TLV.encode(main), '63')
44
+ end
45
+ end
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,37 @@
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
+ module Promptpay
9
+ # Generates PromptPay “Bill Payment” EMVCo QR payloads,
10
+ # including optional references and amount.
11
+ class BillPayment
12
+ # biller_id: string, amount: optional, ref1: string, ref2/3: optional
13
+ def self.payload(biller_id:, ref1:, amount: nil, ref2: nil, ref3: nil)
14
+ sub = [
15
+ TLV.tag('00', 'A000000677010112'),
16
+ TLV.tag('01', biller_id),
17
+ TLV.tag('02', ref1)
18
+ ]
19
+ sub << TLV.tag('03', ref2) if ref2
20
+ tag30 = TLV.encode(sub)
21
+
22
+ main = [
23
+ TLV.tag('00', '01'),
24
+ TLV.tag('01', amount ? '12' : '11'),
25
+ TLV.tag('30', tag30),
26
+ TLV.tag('53', '764'),
27
+ TLV.tag('58', 'TH')
28
+ ]
29
+ main << TLV.tag('54', format('%.2f', amount)) if amount
30
+ main << TLV.tag('62', TLV.encode([TLV.tag('07', ref3)])) if ref3
31
+
32
+ CRC16.with_crc(TLV.encode(main), '63')
33
+ end
34
+ end
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,54 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'thai_qr_pay/tlv'
4
+ require 'thai_qr_pay/crc16'
5
+
6
+ module ThaiQrPay
7
+ # Parses EMVCo QR payloads into TLV tags, with optional CRC checking.
8
+ class Parser
9
+ attr_reader :payload, :tags
10
+
11
+ # strict: verify CRC before parsing; sub_tags: decode nested TLV
12
+ def initialize(payload, strict: false, sub_tags: true)
13
+ @payload = payload.dup
14
+
15
+ if strict
16
+ exp = payload[-4..]
17
+ calc = CRC16.checksum(payload[0..-5])
18
+ raise Error, 'CRC mismatch' unless exp == calc
19
+ end
20
+
21
+ @tags = TLV.decode(payload)
22
+
23
+ return unless sub_tags
24
+
25
+ @tags.each do |t|
26
+ next unless t.value.match?(/^\d{4}/)
27
+
28
+ subt = TLV.decode(t.value)
29
+ t.sub_tags = subt if subt.map(&:length) == subt.map { |s| s.value.length }
30
+ end
31
+ end
32
+
33
+ # Fetch a Tag struct by id (and optional sub_id)
34
+ def get_tag(id, sub_id = nil)
35
+ t = @tags.find { |x| x.id == id }
36
+ return unless t
37
+ return t unless sub_id && t.sub_tags
38
+
39
+ t.sub_tags.find { |st| st.id == sub_id }
40
+ end
41
+
42
+ # Fetch just the value
43
+ def get_tag_value(id, sub_id = nil)
44
+ get_tag(id, sub_id)&.value
45
+ end
46
+
47
+ # Recompute & compare CRC (default tag '63')
48
+ def valid_crc?(crc_tag = '63')
49
+ filtered = @tags.reject { |t| t.id == crc_tag }
50
+ rebuilt = TLV.encode(filtered)
51
+ payload == CRC16.with_crc(rebuilt, crc_tag)
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ThaiQrPay
4
+ # TLV (Tag–Length–Value) parsing and encoding utilities
5
+ # for EMVCo-style QR payloads (PromptPay, TrueMoney, etc.).
6
+ module TLV
7
+ Tag = Struct.new(:id, :length, :value, :sub_tags)
8
+
9
+ # Decode an ASCII-numeric TLV string into an array of Tag structs
10
+ def self.decode(payload)
11
+ tags = []
12
+ idx = 0
13
+ while idx < payload.length
14
+ id = payload[idx, 2]
15
+ idx += 2
16
+ len = payload[idx, 2].to_i
17
+ idx += 2
18
+ value = payload[idx, len]
19
+ idx += len
20
+ tags << Tag.new(id, len, value, nil)
21
+ end
22
+ tags
23
+ end
24
+
25
+ # Encode an array of Tag structs back into an ASCII TLV string
26
+ def self.encode(tags)
27
+ tags.map do |t|
28
+ inner = t.sub_tags ? encode(t.sub_tags) : t.value
29
+ "#{t.id}#{inner.length.to_s.rjust(2, '0')}#{inner}"
30
+ end.join
31
+ end
32
+
33
+ # Helper to build a Tag struct from id + value
34
+ def self.tag(id, value)
35
+ Tag.new(id, value.length, value, nil)
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'thai_qr_pay/parser'
4
+
5
+ module ThaiQrPay
6
+ module Validate
7
+ # Validates EMVCo “slip verify” QR payloads:
8
+ # checks CRC and extracts sending_bank and trans_ref.
9
+ class SlipVerify
10
+ # payload: EMVCo mini-QR slip verify
11
+ # returns { sending_bank:, trans_ref: } or nil
12
+ def self.call(payload)
13
+ parser = ThaiQrPay::Parser.new(payload, strict: true)
14
+ api = parser.get_tag_value('00', '00')
15
+ bank = parser.get_tag_value('00', '01')
16
+ tr = parser.get_tag_value('00', '02')
17
+ return unless api == '000001' && bank && tr
18
+
19
+ { sending_bank: bank, trans_ref: tr }
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'thai_qr_pay/parser'
4
+
5
+ module ThaiQrPay
6
+ module Validate
7
+ # Validates TrueMoney “slip verify” QR payloads:
8
+ # checks CRC, then extracts event_type, transaction_id, and date.
9
+ class TrueMoneySlipVerify
10
+ # payload: TrueMoney slip verify format
11
+ # returns { event_type:, transaction_id:, date: } or nil
12
+ def self.call(payload)
13
+ parser = ThaiQrPay::Parser.new(payload, strict: true)
14
+ api = parser.get_tag_value('00', '00')
15
+ evt = parser.get_tag_value('00', '02')
16
+ txid = parser.get_tag_value('00', '03')
17
+ dt = parser.get_tag_value('00', '04')
18
+ return unless api == '01' && evt && txid && dt
19
+
20
+ { event_type: evt,
21
+ transaction_id: txid,
22
+ date: dt }
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,6 @@
1
+ # lib/thai_qr_pay/version.rb
2
+ # frozen_string_literal: true
3
+
4
+ module ThaiQrPay
5
+ VERSION = '0.1.0'
6
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'thai_qr_pay/version'
4
+ require 'thai_qr_pay/tlv'
5
+ require 'thai_qr_pay/crc16'
6
+ require 'thai_qr_pay/parser'
7
+ require 'thai_qr_pay/generate/promptpay/any_id'
8
+ require 'thai_qr_pay/generate/promptpay/bill_payment'
9
+ require 'thai_qr_pay/bot_barcode'
10
+ require 'thai_qr_pay/validate/slip_verify'
11
+ require 'thai_qr_pay/validate/true_money_slip_verify'
12
+
13
+ module ThaiQrPay
14
+ class Error < StandardError; end
15
+ end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ # spec/crc16_spec.rb
4
+ require 'thai_qr_pay/crc16'
5
+
6
+ RSpec.describe ThaiQrPay::CRC16 do
7
+ describe '.checksum' do
8
+ it 'computes the correct CRC-16/XMODEM for a known string' do
9
+ payload = '0002010102116304'
10
+ # precomputed CRC for that prefix:
11
+ expect(ThaiQrPay::CRC16.checksum(payload)).to eq('AD0A')
12
+ end
13
+ end
14
+
15
+ describe '.with_crc' do
16
+ it 'appends the CRC tag, length and checksum' do
17
+ base = '000201010211'
18
+ full = ThaiQrPay::CRC16.with_crc(base, '63')
19
+ # must end with tag '63', length '04' and the 4-hex CRC
20
+ expect(full).to match(/\A0002010102116304[0-9A-F]{4}\z/)
21
+ # and be valid when recomputed
22
+ prefix = full[0..-5]
23
+ crc = full[-4..]
24
+ expect(ThaiQrPay::CRC16.checksum(prefix)).to eq(crc)
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,68 @@
1
+ # frozen_string_literal: true
2
+
3
+ # spec/parser_spec.rb
4
+ require 'thai_qr_pay/parser'
5
+ require 'thai_qr_pay/generate/promptpay/any_id'
6
+
7
+ RSpec.describe ThaiQrPay::Parser do
8
+ let(:mobile) { '0812345678' }
9
+ let(:valid_pp_anyid) do
10
+ ThaiQrPay::Generate::Promptpay::AnyID.payload(
11
+ type: 'MSISDN',
12
+ target: mobile,
13
+ amount: 10.5
14
+ )
15
+ end
16
+
17
+ describe '#initialize' do
18
+ it 'parses tags into an array' do
19
+ parser = described_class.new(valid_pp_anyid)
20
+ expect(parser.tags).to be_an(Array)
21
+ expect(parser.get_tag('00').value).to eq('01')
22
+ end
23
+
24
+ it 'raises on bad CRC when strict: true' do
25
+ bad = valid_pp_anyid.sub(/.{4}$/, 'DEAD')
26
+ expect { described_class.new(bad, strict: true) }.to raise_error(ThaiQrPay::Error, /CRC mismatch/)
27
+ end
28
+
29
+ it 'does not raise on bad CRC when strict: false' do
30
+ bad = valid_pp_anyid.sub(/.{4}$/, 'DEAD')
31
+ expect { described_class.new(bad, strict: false) }.not_to raise_error
32
+ end
33
+ end
34
+
35
+ describe '#get_tag and #get_tag_value' do
36
+ subject(:parser) { described_class.new(valid_pp_anyid, strict: true) }
37
+
38
+ it 'fetches top-level tag struct' do
39
+ tag = parser.get_tag('54')
40
+ expect(tag).to be_a(ThaiQrPay::TLV::Tag)
41
+ expect(tag.id).to eq('54')
42
+ end
43
+
44
+ it 'fetches nested sub-tag value' do
45
+ # Tag '29' contains sub-tags; '01' inside gives the MSISDN with 66 prefix
46
+ expected_msisdn = mobile.sub(/^0/, '66')
47
+ expect(parser.get_tag_value('29', '01')).to eq(expected_msisdn)
48
+ end
49
+
50
+ it 'returns nil for missing tags' do
51
+ expect(parser.get_tag('99')).to be_nil
52
+ expect(parser.get_tag_value('29', '99')).to be_nil
53
+ end
54
+ end
55
+
56
+ describe '#valid_crc?' do
57
+ it 'returns true for correct CRC' do
58
+ parser = described_class.new(valid_pp_anyid, strict: true)
59
+ expect(parser.valid_crc?).to be true
60
+ end
61
+
62
+ it 'returns false for tampered payload' do
63
+ tampered = valid_pp_anyid.sub('12', '11')
64
+ parser = described_class.new(tampered, strict: false)
65
+ expect(parser.valid_crc?).to be false
66
+ end
67
+ end
68
+ end
@@ -0,0 +1,61 @@
1
+ # frozen_string_literal: true
2
+
3
+ # spec/thai_qr_pay_spec.rb
4
+ require 'thai_qr_pay'
5
+
6
+ RSpec.describe ThaiQrPay do
7
+ it 'has a version number' do
8
+ expect(ThaiQrPay::VERSION).not_to be_nil
9
+ end
10
+
11
+ context 'integration examples' do
12
+ let(:mobile) { '0812345678' }
13
+ let(:biller) { '1234567890123' }
14
+
15
+ it 'generates and parses a PromptPay AnyID QR payload' do
16
+ payload = ThaiQrPay::Generate::Promptpay::AnyID.payload(
17
+ type: 'MSISDN',
18
+ target: mobile,
19
+ amount: 50.0
20
+ )
21
+ parser = ThaiQrPay::Parser.new(payload, strict: true)
22
+
23
+ # Standard EMVCo tags
24
+ expect(parser.get_tag_value('00')).to eq('01') # Payload Format Indicator
25
+ expect(parser.get_tag_value('01')).to eq('12') # Point-of-Initiation Method
26
+
27
+ # Sub-tag 29.01 contains the converted mobile number (no padding)
28
+ expected_msisdn = mobile.sub(/^0/, '66')
29
+ expect(parser.get_tag_value('29', '01')).to eq(expected_msisdn)
30
+
31
+ expect(parser.valid_crc?).to be true
32
+ end
33
+
34
+ it 'generates and parses a PromptPay Bill Payment QR payload' do
35
+ payload = ThaiQrPay::Generate::Promptpay::BillPayment.payload(
36
+ biller_id: biller,
37
+ ref1: 'INV001',
38
+ amount: 250.25
39
+ )
40
+ parser = ThaiQrPay::Parser.new(payload, strict: true)
41
+
42
+ # Sub-tag 30.01 is the biller ID
43
+ expect(parser.get_tag_value('30', '01')).to eq(biller)
44
+ # Tag 54 is the amount
45
+ expect(parser.get_tag_value('54')).to eq('250.25')
46
+ expect(parser.valid_crc?).to be true
47
+ end
48
+
49
+ it 'round-trips BOT barcode to QR Tag 30 payload' do
50
+ raw = "|#{biller}\rINV001\r\r012345"
51
+ bot = ThaiQrPay::BOTBarcode.from_string(raw)
52
+ expect(bot).not_to be_nil
53
+ expect(bot.biller_id).to eq(biller)
54
+ expect(bot.to_s).to eq(raw)
55
+
56
+ qr = bot.to_qr_tag30
57
+ parser = ThaiQrPay::Parser.new(qr, strict: true)
58
+ expect(parser.get_tag_value('30', '01')).to eq(biller)
59
+ end
60
+ end
61
+ end
data/spec/tlv_spec.rb ADDED
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ # spec/tlv_spec.rb
4
+ require 'thai_qr_pay/tlv'
5
+
6
+ RSpec.describe ThaiQrPay::TLV do
7
+ let(:raw) { '000201010211' }
8
+ let(:tags) { ThaiQrPay::TLV.decode(raw) }
9
+
10
+ describe '.decode' do
11
+ it 'parses a simple TLV string into an array of Tag structs' do
12
+ expect(tags.size).to eq(2)
13
+ expect(tags[0].id).to eq('00')
14
+ expect(tags[0].length).to eq(2)
15
+ expect(tags[0].value).to eq('01')
16
+
17
+ expect(tags[1].id).to eq('01')
18
+ expect(tags[1].length).to eq(2)
19
+ expect(tags[1].value).to eq('11')
20
+ end
21
+
22
+ it 'round-trips correctly with .encode' do
23
+ round = ThaiQrPay::TLV.encode(tags)
24
+ expect(round).to eq(raw)
25
+ end
26
+ end
27
+
28
+ describe '.encode' do
29
+ it 'builds a TLV string from Tag structs' do
30
+ t0 = ThaiQrPay::TLV.tag('00', 'A1')
31
+ t1 = ThaiQrPay::TLV.tag('01', 'BCD')
32
+ expect(ThaiQrPay::TLV.encode([t0, t1])).to eq('0002A10103BCD')
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ # thai_qr_pay.gemspec
4
+
5
+ require_relative 'lib/thai_qr_pay/version'
6
+
7
+ Gem::Specification.new do |s|
8
+ s.name = 'thai_qr_pay'
9
+ s.version = ThaiQrPay::VERSION
10
+ s.summary = 'A Ruby toolkit for Thailand’s EMVCo QR payments (PromptPay, TrueMoney, slip-verify, BOT barcode conversion)'
11
+ s.description = <<~DESC
12
+ thai_qr_pay is a Ruby gem providing:
13
+ - ASCII-numeric TLV parsing and encoding
14
+ - EMVCo CRC-16/IBM-SDLC checksum computation and validation
15
+ - Generators for PromptPay AnyID, PromptPay Bill Payment, TrueMoney, etc.
16
+ - Validators for slip-verify and TrueMoney slip verify payloads
17
+ - BOT barcode → EMVCo QR Tag-30 conversion
18
+ DESC
19
+ s.authors = ['Chayut Orapinpatipat']
20
+ s.email = ['chayut_o@hotmail.com']
21
+ s.files = Dir.chdir(__dir__) { `git ls-files -z`.split("\x0") }
22
+ s.homepage = 'https://github.com/chayuto/thai-qr-pay'
23
+ s.metadata ||= {}
24
+ s.metadata['documentation_uri'] = 'https://github.com/chayuto/thai-qr-pay#readme'
25
+ s.license = 'MIT'
26
+
27
+ s.required_ruby_version = '>= 2.7'
28
+ s.add_development_dependency 'rake', '~> 13.0'
29
+ s.add_development_dependency 'rspec', '~> 3.0'
30
+ end
metadata ADDED
@@ -0,0 +1,99 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: thai_qr_pay
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Chayut Orapinpatipat
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2025-06-15 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: rake
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '13.0'
20
+ type: :development
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '13.0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: rspec
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '3.0'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '3.0'
41
+ description: |
42
+ thai_qr_pay is a Ruby gem providing:
43
+ - ASCII-numeric TLV parsing and encoding
44
+ - EMVCo CRC-16/IBM-SDLC checksum computation and validation
45
+ - Generators for PromptPay AnyID, PromptPay Bill Payment, TrueMoney, etc.
46
+ - Validators for slip-verify and TrueMoney slip verify payloads
47
+ - BOT barcode → EMVCo QR Tag-30 conversion
48
+ email:
49
+ - chayut_o@hotmail.com
50
+ executables: []
51
+ extensions: []
52
+ extra_rdoc_files: []
53
+ files:
54
+ - ".gitignore"
55
+ - Gemfile
56
+ - LICENSE
57
+ - README.md
58
+ - Rakefile
59
+ - lib/thai_qr_pay.rb
60
+ - lib/thai_qr_pay/bot_barcode.rb
61
+ - lib/thai_qr_pay/crc16.rb
62
+ - lib/thai_qr_pay/generate/promptpay/any_id.rb
63
+ - lib/thai_qr_pay/generate/promptpay/bill_payment.rb
64
+ - lib/thai_qr_pay/parser.rb
65
+ - lib/thai_qr_pay/tlv.rb
66
+ - lib/thai_qr_pay/validate/slip_verify.rb
67
+ - lib/thai_qr_pay/validate/true_money_slip_verify.rb
68
+ - lib/thai_qr_pay/version.rb
69
+ - spec/crc16_spec.rb
70
+ - spec/parser_spec.rb
71
+ - spec/thai_qr_pay_spec.rb
72
+ - spec/tlv_spec.rb
73
+ - thai_qr_pay.gemspec
74
+ homepage: https://github.com/chayuto/thai-qr-pay
75
+ licenses:
76
+ - MIT
77
+ metadata:
78
+ documentation_uri: https://github.com/chayuto/thai-qr-pay#readme
79
+ post_install_message:
80
+ rdoc_options: []
81
+ require_paths:
82
+ - lib
83
+ required_ruby_version: !ruby/object:Gem::Requirement
84
+ requirements:
85
+ - - ">="
86
+ - !ruby/object:Gem::Version
87
+ version: '2.7'
88
+ required_rubygems_version: !ruby/object:Gem::Requirement
89
+ requirements:
90
+ - - ">="
91
+ - !ruby/object:Gem::Version
92
+ version: '0'
93
+ requirements: []
94
+ rubygems_version: 3.5.11
95
+ signing_key:
96
+ specification_version: 4
97
+ summary: A Ruby toolkit for Thailand’s EMVCo QR payments (PromptPay, TrueMoney, slip-verify,
98
+ BOT barcode conversion)
99
+ test_files: []