require 'httparty' class KbzMerchant class PaymentError < StandardError; end def initialize(payment_method) @payment_method = payment_method @url = @payment_method.gateway_url json_params = @payment_method.additional_parameters.inspect.undump params = JSON.parse(json_params) @notify_url = params['notify_url'] @app_id = params['app_id'] end def create_order(amount:, merch_order_id:, timeout: '120m') api_url = "#{@url}/precreate" payload = build_create_payload(amount, merch_order_id, timeout) response = send_request(payload, api_url) # audit PaymentGatewayAuditJob.perform_later({ receipt_no: merch_order_id, gateway_name: "MMQR", endpoint_url: api_url, event_type: "kbz.payment.precreate", request_body: payload, response_body: response, request_method: "POST", shop_code: Shop.current_shop.shop_code }) handle_response(response) end def close_order(merch_order_id:) api_url = "#{@url}/closeorder" payload = build_close_payload(merch_order_id) response = send_request(payload, api_url) # audit PaymentGatewayAuditJob.perform_later({ receipt_no: merch_order_id, gateway_name: "MMQR", endpoint_url: api_url, event_type: "kbz.payment.closeorder", request_body: payload, response_body: response, request_method: "POST", shop_code: Shop.current_shop.shop_code }) handle_response(response) end def query_order(merch_order_id:) api_url = "#{@url}/queryorder" payload = build_query_payload(merch_order_id) response = send_request(payload, api_url) handle_response(response) end private def build_create_payload(amount, merch_order_id, timeout) base_params = { method: 'kbz.payment.precreate', timestamp: Time.now.utc.to_i.to_s, nonce_str: SecureRandom.hex(16), notify_url: @notify_url, sign_type: 'SHA256', version: '1.0', biz_content: { appid: @app_id, merch_code: @payment_method.merchant_account_id, merch_order_id: merch_order_id, trade_type: 'PAY_BY_QRCODE', total_amount: amount.to_s, trans_currency: 'MMK', timeout_express: timeout }.compact } flattened = flatten_hash(base_params) base_params.merge(sign: generate_signature(flattened)) end def build_close_payload(merch_order_id) base_params = { method: 'kbz.payment.closeorder', timestamp: Time.now.utc.to_i.to_s, nonce_str: SecureRandom.hex(16), sign_type: 'SHA256', version: '3.0', biz_content: { appid: @app_id, merch_code: @payment_method.merchant_account_id, merch_order_id: merch_order_id }.compact } flattened = flatten_hash(base_params) base_params.merge(sign: generate_signature(flattened)) end def build_query_payload(merch_order_id) base_params = { method: 'kbz.payment.queryorder', timestamp: Time.now.utc.to_i.to_s, nonce_str: SecureRandom.hex(16), sign_type: 'SHA256', version: '3.0', biz_content: { appid: @app_id, merch_code: @payment_method.merchant_account_id, merch_order_id: merch_order_id }.compact } flattened = flatten_hash(base_params) base_params.merge(sign: generate_signature(flattened)) end def flatten_hash(hash, parent_key = nil) hash.each_with_object({}) do |(k, v), res| key = parent_key ? "#{k}" : k.to_s if v.is_a?(Hash) res.merge!(flatten_hash(v, key)) else res[key] = v.to_s end end end def generate_signature(flattened_params) sorted_params = flattened_params.except('sign', 'sign_type').sort string_a = sorted_params.map { |k, v| "#{k}=#{v}" }.join('&') puts "String a: #{string_a}" string_to_sign = "#{string_a}&key=#{@payment_method.auth_token}" puts "String to sign: #{string_to_sign}" Digest::SHA256.hexdigest(string_to_sign).upcase end def send_request(payload, url) headers = { 'Content-Type' => 'application/json', 'User-Agent' => 'KBZPay/1.0' } puts "Headers: #{headers}" puts "Payload: #{payload.to_json}" begin response = HTTParty.post( url, headers: headers, body: { Request: payload }.to_json, timeout: 15 ) Rails.logger.info "Response: #{response}" JSON.parse(response.body) rescue HTTParty::Error => e { error: true, code: 'http_error', message: "HTTP error: #{e.message}", alert: "Payment service unavailable. Please try again later." } rescue SocketError => e { error: true, code: 'network_error', message: "Network error: #{e.message}", alert: "Network connection failed. Please check your internet." } rescue JSON::ParserError => e { error: true, code: 'invalid_response', message: "Invalid response format: #{e.message}", alert: "Received invalid payment response. Please contact support." } rescue StandardError => e { error: true, code: 'unexpected_error', message: "Unexpected error: #{e.message}", alert: "An unexpected error occurred. Please try again." } end end def handle_response(response) if response['error'] { status: 'error', code: response['code'], message: response['message'] } elsif response.dig('Response', 'result') == 'SUCCESS' { status: 'success', data: response['Response'] } else error_code = response.dig('Response', 'code') error_message = response.dig('Response', 'msg') case error_code when 'OrderCenter.FAILED_CREATE_ORDER_FOR_DUPLICATED_MERCHANT_ORDER_ID' { status: 'failed', code: error_code, message: 'Duplicate order ID detected. Please use a unique reference ID.' } else { status: 'failed', code: error_code || 'unknown_error', message: error_message || 'Payment processing failed' } end end end end