Module: Bakong::Khqr
- Defined in:
- lib/bakong/khqr.rb,
lib/bakong/khqr/crc16.rb,
lib/bakong/khqr/error.rb,
lib/bakong/khqr/version.rb,
lib/bakong/khqr/khqr_tag.rb,
lib/bakong/khqr/constants.rb,
lib/bakong/khqr/error_codes.rb,
lib/bakong/khqr/khqr_subtag.rb,
lib/bakong/khqr/source_info.rb,
lib/bakong/khqr/helpers/http.rb,
lib/bakong/khqr/merchant_info.rb,
lib/bakong/khqr/individual_info.rb,
lib/bakong/khqr/helpers/deep_link.rb,
lib/bakong/khqr/merchant_code/crc.rb,
lib/bakong/khqr/tag_length_string.rb,
lib/bakong/khqr/controllers/decode.rb,
lib/bakong/khqr/helpers/cut_string.rb,
lib/bakong/khqr/controllers/generate.rb,
lib/bakong/khqr/helpers/check_account_id.rb,
lib/bakong/khqr/merchant_code/time_stamp.rb,
lib/bakong/khqr/merchant_code/country_code.rb,
lib/bakong/khqr/controllers/decode_non_khqr.rb,
lib/bakong/khqr/merchant_code/merchant_city.rb,
lib/bakong/khqr/merchant_code/merchant_name.rb,
lib/bakong/khqr/controllers/decode_validation.rb,
lib/bakong/khqr/merchant_code/additional_data.rb,
lib/bakong/khqr/merchant_code/transaction_amount.rb,
lib/bakong/khqr/merchant_code/transaction_currency.rb,
lib/bakong/khqr/merchant_code/merchant_category_code.rb,
lib/bakong/khqr/merchant_code/global_unique_identifier.rb,
lib/bakong/khqr/merchant_code/payload_format_indicator.rb,
lib/bakong/khqr/merchant_code/unionpay_merchant_account.rb,
lib/bakong/khqr/merchant_code/point_of_initiation_method.rb,
lib/bakong/khqr/merchant_code/merchant_information_language_template.rb
Overview
Top-level facade for the bakong-khqr SDK. Mirrors the public surface of the upstream JavaScript package (www.npmjs.com/package/bakong-khqr): generate, decode, verify, plus the Bakong Open API helpers.
Defined Under Namespace
Modules: CRC16, Controllers, Helpers, MerchantCode Classes: Error, IndividualInfo, MerchantInfo, SourceInfo, TagLengthString
Constant Summary collapse
- CRC_REGEXP =
/6304[A-Fa-f0-9]{4}\z/- VERSION =
"0.1.1"- KHQR_TAG =
Authoritative ordered list of top-level EMVCo tags the KHQR spec accepts, paired with the TLV builder/validator class responsible for that tag.
[ { tag: "00", type: :payload_format_indicator, required: true, instance: MerchantCode::PayloadFormatIndicator }, { tag: "01", type: :point_of_initiation_method, required: false, instance: MerchantCode::PointOfInitiationMethod }, { tag: "15", type: :union_pay_merchant, required: false, instance: MerchantCode::UnionpayMerchantAccount }, { tag: "29", type: :global_unique_identifier, required: true, sub: true, instance: MerchantCode::GlobalUniqueIdentifier }, { tag: "52", type: :merchant_category_code, required: true, instance: MerchantCode::MerchantCategoryCode }, { tag: "53", type: :transaction_currency, required: true, instance: MerchantCode::TransactionCurrency }, { tag: "54", type: :transaction_amount, required: false, instance: MerchantCode::TransactionAmount }, { tag: "58", type: :country_code, required: true, instance: MerchantCode::CountryCode }, { tag: "59", type: :merchant_name, required: true, instance: MerchantCode::MerchantName }, { tag: "60", type: :merchant_city, required: true, instance: MerchantCode::MerchantCity }, { tag: "62", type: :additional_data, required: false, sub: true, instance: MerchantCode::AdditionalData }, { tag: "64", type: :merchant_information_language_template, required: false, sub: true, instance: MerchantCode::MerchantInformationLanguageTemplate }, { tag: "99", type: :timestamp, required: false, sub: true, instance: MerchantCode::TimeStamp }, { tag: "63", type: :crc, required: true, instance: MerchantCode::CRC } ].freeze
- CURRENCY =
{ usd: 840, khr: 116 }.freeze
- MERCHANT_TYPE =
{ merchant: "merchant", individual: "individual" }.freeze
- EMV =
{ PAYLOAD_FORMAT_INDICATOR: "00", DEFAULT_PAYLOAD_FORMAT_INDICATOR: "01", POINT_OF_INITIATION_METHOD: "01", STATIC_QR: "11", DYNAMIC_QR: "12", MERCHANT_ACCOUNT_INFORMATION_INDIVIDUAL: "29", MERCHANT_ACCOUNT_INFORMATION_MERCHANT: "30", BAKONG_ACCOUNT_IDENTIFIER: "00", MERCHANT_ACCOUNT_INFORMATION_MERCHANT_ID: "01", INDIVIDUAL_ACCOUNT_INFORMATION: "01", MERCHANT_ACCOUNT_INFORMATION_ACQUIRING_BANK: "02", MERCHANT_CATEGORY_CODE: "52", DEFAULT_MERCHANT_CATEGORY_CODE: "5999", TRANSACTION_CURRENCY: "53", TRANSACTION_AMOUNT: "54", DEFAULT_TRANSACTION_AMOUNT: "0", COUNTRY_CODE: "58", DEFAULT_COUNTRY_CODE: "KH", MERCHANT_NAME: "59", MERCHANT_CITY: "60", DEFAULT_MERCHANT_CITY: "Phnom Penh", CRC: "63", CRC_LENGTH: "04", ADDITIONAL_DATA_TAG: "62", BILLNUMBER_TAG: "01", ADDITIONAL_DATA_FIELD_MOBILE_NUMBER: "02", STORELABEL_TAG: "03", TERMINAL_TAG: "07", PURPOSE_OF_TRANSACTION: "08", TIMESTAMP_TAG: "99", CREATION_TIMESTAMP: "00", EXPIRATION_TIMESTAMP: "01", MERCHANT_INFORMATION_LANGUAGE_TEMPLATE: "64", LANGUAGE_PREFERENCE: "00", MERCHANT_NAME_ALTERNATE_LANGUAGE: "01", MERCHANT_CITY_ALTERNATE_LANGUAGE: "02", UNIONPAY_MERCHANT_ACCOUNT: "15", INVALID_LENGTH: { KHQR: 12, MERCHANT_NAME: 25, BAKONG_ACCOUNT: 32, AMOUNT: 13, COUNTRY_CODE: 3, MERCHANT_CATEGORY_CODE: 4, MERCHANT_CITY: 15, TIMESTAMP: 13, TRANSACTION_AMOUNT: 14, TRANSACTION_CURRENCY: 3, BILL_NUMBER: 25, STORE_LABEL: 25, TERMINAL_LABEL: 25, PURPOSE_OF_TRANSACTION: 25, MERCHANT_ID: 32, ACQUIRING_BANK: 32, MOBILE_NUMBER: 25, ACCOUNT_INFORMATION: 32, MERCHANT_INFORMATION_LANGUAGE_TEMPLATE: 99, UPI_MERCHANT: 99, LANGUAGE_PREFERENCE: 2, MERCHANT_NAME_ALTERNATE_LANGUAGE: 25, MERCHANT_CITY_ALTERNATE_LANGUAGE: 15 }.freeze }.freeze
- ERROR_CODES =
{ BAKONG_ACCOUNT_ID_REQUIRED: { code: 1, message: "Bakong Account ID cannot be null or empty" }, MERCHANT_NAME_REQUIRED: { code: 2, message: "Merchant name cannot be null or empty" }, BAKONG_ACCOUNT_ID_INVALID: { code: 3, message: "Bakong Account ID is invalid" }, TRANSACTION_AMOUNT_INVALID: { code: 4, message: "Amount is invalid" }, MERCHANT_TYPE_REQUIRED: { code: 5, message: "Merchant type cannot be null or empty" }, BAKONG_ACCOUNT_ID_LENGTH_INVALID: { code: 6, message: "Bakong Account ID Length is Invalid" }, MERCHANT_NAME_LENGTH_INVALID: { code: 7, message: "Merchant Name Length is invalid" }, KHQR_INVALID: { code: 8, message: "KHQR provided is invalid" }, CURRENCY_TYPE_REQUIRED: { code: 9, message: "Currency type cannot be null or empty" }, BILL_NUMBER_LENGTH_INVALID: { code: 10, message: "Bill Name Length is invalid" }, STORE_LABEL_LENGTH_INVALID: { code: 11, message: "Store Label Length is invalid" }, TERMINAL_LABEL_LENGTH_INVALID: { code: 12, message: "Terminal Label Length is invalid" }, CONNECTION_TIMEOUT: { code: 13, message: "Cannot reach Bakong Open API service. Please check internet connection" }, INVALID_DEEP_LINK_SOURCE_INFO: { code: 14, message: "Source Info for Deep Link is invalid" }, INTERNAL_SERVER_ERROR: { code: 15, message: "Internal server error" }, PAYLOAD_FORMAT_INDICATOR_LENGTH_INVALID: { code: 16, message: "Payload Format indicator Length is invalid" }, POINT_INITIATION_LENGTH_INVALID: { code: 17, message: "Point of initiation Length is invalid" }, MERCHANT_CODE_LENGTH_INVALID: { code: 18, message: "Merchant code Length is invalid" }, TRANSACTION_CURRENCY_LENGTH_INVALID: { code: 19, message: "Transaction currency Length is invalid" }, COUNTRY_CODE_LENGTH_INVALID: { code: 20, message: "Country code Length is invalid" }, MERCHANT_CITY_LENGTH_INVALID: { code: 21, message: "Merchant city Length is invalid" }, CRC_LENGTH_INVALID: { code: 22, message: "CRC Length is invalid" }, PAYLOAD_FORMAT_INDICATOR_TAG_REQUIRED: { code: 23, message: "Payload format indicator tag required" }, CRC_TAG_REQUIRED: { code: 24, message: "CRC tag required" }, MERCHANT_CATEGORY_TAG_REQUIRED: { code: 25, message: "Merchant category tag required" }, COUNTRY_CODE_TAG_REQUIRED: { code: 26, message: "Country Code cannot be null or empty" }, MERCHANT_CITY_TAG_REQUIRED: { code: 27, message: "Merchant City cannot be null or empty" }, UNSUPPORTED_CURRENCY: { code: 28, message: "Unsupported currency" }, INVALID_DEEP_LINK_URL: { code: 29, message: "Deep Link URL is not valid" }, MERCHANT_ID_REQUIRED: { code: 30, message: "Merchant ID cannot be null or empty" }, ACQUIRING_BANK_REQUIRED: { code: 31, message: "Acquiring Bank cannot be null or empty" }, MERCHANT_ID_LENGTH_INVALID: { code: 32, message: "Merchant ID Length is invalid" }, ACQUIRING_BANK_LENGTH_INVALID: { code: 33, message: "Acquiring Bank Length is invalid" }, MOBILE_NUMBER_LENGTH_INVALID: { code: 34, message: "Mobile Number Length is invalid" }, ACCOUNT_INFORMATION_LENGTH_INVALID: { code: 35, message: "Account Information Length is invalid" }, TAG_NOT_IN_ORDER: { code: 36, message: "Tag is not in order" }, LANGUAGE_PREFERENCE_REQUIRED: { code: 37, message: "Language Preference cannot be null or empty" }, LANGUAGE_PREFERENCE_LENGTH_INVALID: { code: 38, message: "Language Preference Length is invalid" }, MERCHANT_NAME_ALTERNATE_LANGUAGE_REQUIRED: { code: 39, message: "Merchant Name Alternate Language cannot be null or empty" }, MERCHANT_NAME_ALTERNATE_LANGUAGE_LENGTH_INVALID: { code: 40, message: "Merchant Name Alternate Language Length is invalid" }, MERCHANT_CITY_ALTERNATE_LANGUAGE_LENGTH_INVALID: { code: 41, message: "Merchant City Alternate Language Length is invalid" }, PURPOSE_OF_TRANSACTION_LENGTH_INVALID: { code: 42, message: "Purpose of Transaction Length is invalid" }, UPI_ACCOUNT_INFORMATION_LENGTH_INVALID: { code: 43, message: "Upi Account Information Length is invalid" }, UPI_ACCOUNT_INFORMATION_INVALID_CURRENCY: { code: 44, message: "Upi Account Information Length does not accept USD" }, EXPIRATION_TIMESTAMP_REQUIRED: { code: 45, message: "Expiration timestamp is required for dynamic KHQR" }, KHQR_EXPIRED: { code: 46, message: "This dynamic KHQR has expired" }, INVALID_DYNAMIC_KHQR: { code: 47, message: "This dynamic KHQR has invalid field transaction amount" }, POINT_OF_INITIATION_METHOD_INVALID: { code: 48, message: "Point of Initiation Method is invalid" }, EXPIRATION_TIMESTAMP_LENGTH_INVALID: { code: 49, message: "Expiration timestamp length is invalid" }, EXPIRATION_TIMESTAMP_IN_THE_PAST: { code: 50, message: "Expiration timestamp is in the past" }, INVALID_MERCHANT_CATEGORY_CODE: { code: 51, message: "Invalid merchant category code" } }.freeze
- KHQR_SUBTAG =
Subtag schema for the composite top-level tags (29, 30, 62, 64, 99). ‘input` defines the keys we surface in a decoded result; `compare` is the lookup table mapping (tag, sub_tag) → field name.
{ input: [ { tag: "29", data: { bakong_account_id: nil, account_information: nil } }, { tag: "30", data: { bakong_account_id: nil, merchant_id: nil, acquiring_bank: nil } }, { tag: "62", data: { bill_number: nil, mobile_number: nil, store_label: nil, terminal_label: nil, purpose_of_transaction: nil } }, { tag: "64", data: { language_preference: nil, merchant_name_alternate_language: nil, merchant_city_alternate_language: nil } }, { tag: "99", data: { creation_timestamp: nil, expiration_timestamp: nil } } ].freeze, compare: [ { tag: "29", sub_tag: EMV[:BAKONG_ACCOUNT_IDENTIFIER], name: :bakong_account_id }, { tag: "29", sub_tag: EMV[:MERCHANT_ACCOUNT_INFORMATION_MERCHANT_ID], name: :account_information }, { tag: "29", sub_tag: EMV[:MERCHANT_ACCOUNT_INFORMATION_ACQUIRING_BANK], name: :acquiring_bank }, { tag: "62", sub_tag: EMV[:BILLNUMBER_TAG], name: :bill_number }, { tag: "62", sub_tag: EMV[:ADDITIONAL_DATA_FIELD_MOBILE_NUMBER], name: :mobile_number }, { tag: "62", sub_tag: EMV[:STORELABEL_TAG], name: :store_label }, { tag: "62", sub_tag: EMV[:PURPOSE_OF_TRANSACTION], name: :purpose_of_transaction }, { tag: "62", sub_tag: EMV[:TERMINAL_TAG], name: :terminal_label }, { tag: "64", sub_tag: EMV[:LANGUAGE_PREFERENCE], name: :language_preference }, { tag: "64", sub_tag: EMV[:MERCHANT_NAME_ALTERNATE_LANGUAGE], name: :merchant_name_alternate_language }, { tag: "64", sub_tag: EMV[:MERCHANT_CITY_ALTERNATE_LANGUAGE], name: :merchant_city_alternate_language }, { tag: "99", sub_tag: EMV[:CREATION_TIMESTAMP], name: :creation_timestamp }, { tag: "99", sub_tag: EMV[:EXPIRATION_TIMESTAMP], name: :expiration_timestamp } ].freeze }.freeze
Class Method Summary collapse
-
.check_bakong_account(url, account_id) ⇒ Hash
Check whether a Bakong account ID exists via the Bakong Open API.
-
.decode(khqr_string) ⇒ Hash
Parse a KHQR string into a snake_case symbol-keyed Hash.
-
.decode_non_khqr(qr) ⇒ Hash
Parse an arbitrary EMVCo TLV QR string (not necessarily KHQR-compliant).
-
.generate_deep_link(url, qr, source_info: nil) ⇒ Hash
Request a shortened deep link for a KHQR string via the Bakong Open API.
-
.generate_individual(info) ⇒ Hash
Generate a KHQR string for an individual recipient.
-
.generate_merchant(info) ⇒ Hash
Generate a KHQR string for a merchant.
-
.verify(khqr_string) ⇒ Boolean
Verify the CRC-16/CCITT-FALSE checksum embedded at the tail of a KHQR string and re-validate every TLV against the spec.
Class Method Details
.check_bakong_account(url, account_id) ⇒ Hash
Check whether a Bakong account ID exists via the Bakong Open API.
88 89 90 |
# File 'lib/bakong/khqr.rb', line 88 def check_bakong_account(url, account_id) Helpers::CheckAccountID.call(url, account_id) end |
.decode(khqr_string) ⇒ Hash
Parse a KHQR string into a snake_case symbol-keyed Hash.
53 54 55 |
# File 'lib/bakong/khqr.rb', line 53 def decode(khqr_string) Controllers::Decode.call(khqr_string) end |
.decode_non_khqr(qr) ⇒ Hash
Parse an arbitrary EMVCo TLV QR string (not necessarily KHQR-compliant).
60 61 62 |
# File 'lib/bakong/khqr.rb', line 60 def decode_non_khqr(qr) Controllers::DecodeNonKHQR.call(qr) end |
.generate_deep_link(url, qr, source_info: nil) ⇒ Hash
Request a shortened deep link for a KHQR string via the Bakong Open API.
98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 |
# File 'lib/bakong/khqr.rb', line 98 def generate_deep_link(url, qr, source_info: nil) raise Error.from(ERROR_CODES[:INVALID_DEEP_LINK_URL]) unless Helpers::DeepLink.valid_link?(url) raise Error.from(ERROR_CODES[:KHQR_INVALID]) unless verify(qr) if source_info && !source_info.complete? raise Error.from(ERROR_CODES[:INVALID_DEEP_LINK_SOURCE_INFO]) end payload = { qr: qr } payload[:sourceInfo] = source_info.to_h if source_info response = Helpers::DeepLink.call(url, payload) short_link = response.dig(:data, :shortLink) { short_link: short_link } end |
.generate_individual(info) ⇒ Hash
Generate a KHQR string for an individual recipient.
37 38 39 40 |
# File 'lib/bakong/khqr.rb', line 37 def generate_individual(info) qr = Controllers::Generate.call(info, :individual) { qr: qr, md5: Digest::MD5.hexdigest(qr) } end |
.generate_merchant(info) ⇒ Hash
Generate a KHQR string for a merchant.
45 46 47 48 |
# File 'lib/bakong/khqr.rb', line 45 def generate_merchant(info) qr = Controllers::Generate.call(info, :merchant) { qr: qr, md5: Digest::MD5.hexdigest(qr) } end |
.verify(khqr_string) ⇒ Boolean
Verify the CRC-16/CCITT-FALSE checksum embedded at the tail of a KHQR string and re-validate every TLV against the spec.
68 69 70 71 72 73 74 75 76 77 78 79 80 81 |
# File 'lib/bakong/khqr.rb', line 68 def verify(khqr_string) return false unless khqr_string.is_a?(String) return false unless khqr_string.match?(CRC_REGEXP) crc = khqr_string[-4..] body = khqr_string[0...-4] return false unless CRC16.compute(body) == crc.upcase return false if khqr_string.length < EMV[:INVALID_LENGTH][:KHQR] Controllers::DecodeValidation.call(khqr_string) true rescue Error, StandardError false end |