qpay-sdk 1.0.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: bc257f4bb9cb15d977730172bacef5f3a8541626115dab42d5e4aacb211f60cd
4
+ data.tar.gz: 4f41766daa7f8b795db58f60134484eddfa4a554c081172d93ea8ef106fcc3a6
5
+ SHA512:
6
+ metadata.gz: 9796ce71359a8402226a70b0193426289e0d53e57b3d932119f6d090c9857151563f3fbe1a28885b3979fca79d9d87fce04e4a9650a1b2f20123bfb04a6e32e4
7
+ data.tar.gz: 399a6092fe9bfa1bd9f1cfba6da767cd8d6348b0d784ae147874591727d330943fbd819347a9335678865bb60cf2de4a80b4ee04bf9a90a61790bea5d43b4cc5
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2024 QPay Ruby SDK Authors
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.
@@ -0,0 +1,205 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "faraday"
4
+ require "json"
5
+
6
+ module QPay
7
+ class Client
8
+ include Endpoints::Auth
9
+ include Endpoints::Invoices
10
+ include Endpoints::Payments
11
+ include Endpoints::Ebarimt
12
+
13
+ TOKEN_BUFFER_SECONDS = 30
14
+
15
+ # Creates a new QPay client.
16
+ #
17
+ # @param config [QPay::Config] the configuration object
18
+ # @param faraday [Faraday::Connection, nil] optional custom Faraday connection
19
+ def initialize(config:, faraday: nil)
20
+ @config = config
21
+ @mutex = Mutex.new
22
+ @access_token = nil
23
+ @refresh_token = nil
24
+ @expires_at = 0
25
+ @refresh_expires_at = 0
26
+
27
+ @http = faraday || Faraday.new do |f|
28
+ f.options.timeout = 30
29
+ f.options.open_timeout = 10
30
+ f.adapter Faraday.default_adapter
31
+ end
32
+ end
33
+
34
+ private
35
+
36
+ def ensure_token
37
+ now = Time.now.to_i
38
+
39
+ can_refresh = false
40
+ refresh_tok = nil
41
+
42
+ @mutex.synchronize do
43
+ # Access token still valid
44
+ if @access_token && now < @expires_at - TOKEN_BUFFER_SECONDS
45
+ return
46
+ end
47
+
48
+ can_refresh = @refresh_token && !@refresh_token.empty? && now < @refresh_expires_at - TOKEN_BUFFER_SECONDS
49
+ refresh_tok = @refresh_token
50
+ end
51
+
52
+ # Try refresh first
53
+ if can_refresh
54
+ begin
55
+ token = do_refresh_token_http(refresh_tok)
56
+ store_token(token)
57
+ return
58
+ rescue QPay::Error
59
+ # Refresh failed, fall through to get new token
60
+ end
61
+ end
62
+
63
+ # Both expired or no tokens, get new token
64
+ token = get_token_request
65
+ store_token(token)
66
+ end
67
+
68
+ def store_token(token)
69
+ @mutex.synchronize do
70
+ @access_token = token.access_token
71
+ @refresh_token = token.refresh_token
72
+ @expires_at = token.expires_in
73
+ @refresh_expires_at = token.refresh_expires_in
74
+ end
75
+ end
76
+
77
+ def do_request(method, path, body: nil)
78
+ ensure_token
79
+
80
+ url = "#{@config.base_url}#{path}"
81
+ access_tok = @mutex.synchronize { @access_token }
82
+
83
+ response = @http.run_request(method.downcase.to_sym, url, body, {}) do |req|
84
+ req.headers["Content-Type"] = "application/json"
85
+ req.headers["Authorization"] = "Bearer #{access_tok}"
86
+ end
87
+
88
+ handle_response(response)
89
+ end
90
+
91
+ def do_basic_auth_request(method, path)
92
+ url = "#{@config.base_url}#{path}"
93
+
94
+ response = @http.run_request(method.downcase.to_sym, url, nil, {}) do |req|
95
+ req.headers["Authorization"] = "Basic #{basic_auth_credentials}"
96
+ end
97
+
98
+ body = response.body
99
+
100
+ unless response.success?
101
+ parsed = parse_json(body)
102
+ code = parsed["error"]
103
+ message = parsed["message"] || body
104
+ raise QPay::Error.new(
105
+ status_code: response.status,
106
+ code: code.nil? || code.empty? ? status_text(response.status) : code,
107
+ message: message.nil? || message.empty? ? body : message,
108
+ raw_body: body
109
+ )
110
+ end
111
+
112
+ parsed = parse_json(body)
113
+ build_token_response(parsed)
114
+ end
115
+
116
+ def handle_response(response)
117
+ body = response.body
118
+
119
+ unless response.success?
120
+ parsed = parse_json(body)
121
+ code = parsed["error"]
122
+ message = parsed["message"]
123
+ raise QPay::Error.new(
124
+ status_code: response.status,
125
+ code: code.nil? || code.empty? ? status_text(response.status) : code,
126
+ message: message.nil? || message.empty? ? body : message,
127
+ raw_body: body
128
+ )
129
+ end
130
+
131
+ return nil if body.nil? || body.empty?
132
+
133
+ parse_json(body)
134
+ end
135
+
136
+ def serialize(obj)
137
+ hash = obj.to_h.compact.transform_keys { |k| camelize_key(k) }
138
+ deep_serialize(hash).to_json
139
+ end
140
+
141
+ def deep_serialize(obj)
142
+ case obj
143
+ when Hash
144
+ obj.each_with_object({}) do |(k, v), result|
145
+ result[k] = deep_serialize(v)
146
+ end
147
+ when Array
148
+ obj.map { |item| deep_serialize(item) }
149
+ when Struct
150
+ deep_serialize(obj.to_h.compact.transform_keys { |k| camelize_key(k) })
151
+ else
152
+ obj
153
+ end
154
+ end
155
+
156
+ # Converts Ruby snake_case keys to the JSON keys expected by QPay API.
157
+ # Most keys are simple snake_case, but some need special handling.
158
+ KEY_MAP = {
159
+ "qpay_short_url" => "qPay_shortUrl",
160
+ "obj_id" => "object_id",
161
+ }.freeze
162
+
163
+ def camelize_key(key)
164
+ str = key.to_s
165
+ KEY_MAP.fetch(str, str)
166
+ end
167
+
168
+ def parse_json(body)
169
+ return {} if body.nil? || body.empty?
170
+
171
+ JSON.parse(body)
172
+ rescue JSON::ParserError
173
+ {}
174
+ end
175
+
176
+ def basic_auth_credentials
177
+ require "base64"
178
+ Base64.strict_encode64("#{@config.username}:#{@config.password}")
179
+ end
180
+
181
+ def status_text(code)
182
+ Rack::Utils::HTTP_STATUS_CODES[code] || "Unknown Status"
183
+ rescue NameError
184
+ # Rack not available, use basic mapping
185
+ HTTP_STATUS_TEXTS.fetch(code, "Unknown Status")
186
+ end
187
+
188
+ HTTP_STATUS_TEXTS = {
189
+ 200 => "OK",
190
+ 201 => "Created",
191
+ 204 => "No Content",
192
+ 400 => "Bad Request",
193
+ 401 => "Unauthorized",
194
+ 403 => "Forbidden",
195
+ 404 => "Not Found",
196
+ 405 => "Method Not Allowed",
197
+ 409 => "Conflict",
198
+ 422 => "Unprocessable Entity",
199
+ 429 => "Too Many Requests",
200
+ 500 => "Internal Server Error",
201
+ 502 => "Bad Gateway",
202
+ 503 => "Service Unavailable",
203
+ }.freeze
204
+ end
205
+ end
@@ -0,0 +1,45 @@
1
+ # frozen_string_literal: true
2
+
3
+ module QPay
4
+ class Config
5
+ attr_accessor :base_url, :username, :password, :invoice_code, :callback_url
6
+
7
+ def initialize(base_url:, username:, password:, invoice_code:, callback_url:)
8
+ @base_url = base_url
9
+ @username = username
10
+ @password = password
11
+ @invoice_code = invoice_code
12
+ @callback_url = callback_url
13
+ end
14
+
15
+ # Loads configuration from environment variables.
16
+ # Raises ArgumentError if any required variable is missing.
17
+ def self.from_env
18
+ mapping = {
19
+ "QPAY_BASE_URL" => :base_url,
20
+ "QPAY_USERNAME" => :username,
21
+ "QPAY_PASSWORD" => :password,
22
+ "QPAY_INVOICE_CODE" => :invoice_code,
23
+ "QPAY_CALLBACK_URL" => :callback_url,
24
+ }
25
+
26
+ values = {}
27
+ missing = []
28
+
29
+ mapping.each do |env_var, key|
30
+ val = ENV[env_var]
31
+ if val.nil? || val.empty?
32
+ missing << env_var
33
+ else
34
+ values[key] = val
35
+ end
36
+ end
37
+
38
+ unless missing.empty?
39
+ raise ArgumentError, "required environment variable(s) not set: #{missing.join(", ")}"
40
+ end
41
+
42
+ new(**values)
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,72 @@
1
+ # frozen_string_literal: true
2
+
3
+ module QPay
4
+ module Endpoints
5
+ module Auth
6
+ # Authenticates with QPay using Basic Auth and returns a new token pair.
7
+ # Also stores the token for subsequent requests.
8
+ #
9
+ # @return [QPay::TokenResponse]
10
+ def get_token
11
+ token = get_token_request
12
+ store_token(token)
13
+ token
14
+ end
15
+
16
+ # Uses the current refresh token to obtain a new access token.
17
+ #
18
+ # @return [QPay::TokenResponse]
19
+ def refresh_token
20
+ refresh_tok = @mutex.synchronize { @refresh_token }
21
+ token = do_refresh_token_http(refresh_tok)
22
+ store_token(token)
23
+ token
24
+ end
25
+
26
+ private
27
+
28
+ def get_token_request
29
+ do_basic_auth_request("POST", "/v2/auth/token")
30
+ end
31
+
32
+ def do_refresh_token_http(refresh_tok)
33
+ url = "#{@config.base_url}/v2/auth/refresh"
34
+ response = @http.post(url) do |req|
35
+ req.headers["Authorization"] = "Bearer #{refresh_tok}"
36
+ end
37
+
38
+ handle_token_response(response)
39
+ end
40
+
41
+ def handle_token_response(response)
42
+ body = response.body
43
+
44
+ unless response.success?
45
+ parsed = parse_json(body)
46
+ raise QPay::Error.new(
47
+ status_code: response.status,
48
+ code: parsed["error"],
49
+ message: parsed["message"] || body,
50
+ raw_body: body
51
+ )
52
+ end
53
+
54
+ parsed = parse_json(body)
55
+ build_token_response(parsed)
56
+ end
57
+
58
+ def build_token_response(data)
59
+ QPay::TokenResponse.new(
60
+ token_type: data["token_type"],
61
+ refresh_expires_in: data["refresh_expires_in"],
62
+ refresh_token: data["refresh_token"],
63
+ access_token: data["access_token"],
64
+ expires_in: data["expires_in"],
65
+ scope: data["scope"],
66
+ not_before_policy: data["not-before-policy"],
67
+ session_state: data["session_state"]
68
+ )
69
+ end
70
+ end
71
+ end
72
+ end
@@ -0,0 +1,129 @@
1
+ # frozen_string_literal: true
2
+
3
+ module QPay
4
+ module Endpoints
5
+ module Ebarimt
6
+ # Creates an ebarimt (electronic tax receipt) for a payment.
7
+ # POST /v2/ebarimt_v3/create
8
+ #
9
+ # @param request [QPay::CreateEbarimtRequest]
10
+ # @return [QPay::EbarimtResponse]
11
+ def create_ebarimt(request)
12
+ data = do_request("POST", "/v2/ebarimt_v3/create", body: serialize(request))
13
+ build_ebarimt_response(data)
14
+ end
15
+
16
+ # Cancels an ebarimt by payment ID.
17
+ # DELETE /v2/ebarimt_v3/{id}
18
+ #
19
+ # @param payment_id [String]
20
+ # @return [QPay::EbarimtResponse]
21
+ def cancel_ebarimt(payment_id)
22
+ data = do_request("DELETE", "/v2/ebarimt_v3/#{payment_id}")
23
+ build_ebarimt_response(data)
24
+ end
25
+
26
+ private
27
+
28
+ def build_ebarimt_response(data)
29
+ return nil if data.nil?
30
+
31
+ items = (data["barimt_items"] || []).map do |item|
32
+ QPay::EbarimtItem.new(
33
+ id: item["id"],
34
+ barimt_id: item["barimt_id"],
35
+ merchant_product_code: item["merchant_product_code"],
36
+ tax_product_code: item["tax_product_code"],
37
+ bar_code: item["bar_code"],
38
+ name: item["name"],
39
+ unit_price: item["unit_price"],
40
+ quantity: item["quantity"],
41
+ amount: item["amount"],
42
+ city_tax_amount: item["city_tax_amount"],
43
+ vat_amount: item["vat_amount"],
44
+ note: item["note"],
45
+ created_by: item["created_by"],
46
+ created_date: item["created_date"],
47
+ updated_by: item["updated_by"],
48
+ updated_date: item["updated_date"],
49
+ status: item["status"]
50
+ )
51
+ end
52
+
53
+ histories = (data["barimt_histories"] || []).map do |h|
54
+ QPay::EbarimtHistory.new(
55
+ id: h["id"],
56
+ barimt_id: h["barimt_id"],
57
+ ebarimt_receiver_type: h["ebarimt_receiver_type"],
58
+ ebarimt_receiver: h["ebarimt_receiver"],
59
+ ebarimt_register_no: h["ebarimt_register_no"],
60
+ ebarimt_bill_id: h["ebarimt_bill_id"],
61
+ ebarimt_date: h["ebarimt_date"],
62
+ ebarimt_mac_address: h["ebarimt_mac_address"],
63
+ ebarimt_internal_code: h["ebarimt_internal_code"],
64
+ ebarimt_bill_type: h["ebarimt_bill_type"],
65
+ ebarimt_qr_data: h["ebarimt_qr_data"],
66
+ ebarimt_lottery: h["ebarimt_lottery"],
67
+ ebarimt_lottery_msg: h["ebarimt_lottery_msg"],
68
+ ebarimt_error_code: h["ebarimt_error_code"],
69
+ ebarimt_error_msg: h["ebarimt_error_msg"],
70
+ ebarimt_response_code: h["ebarimt_response_code"],
71
+ ebarimt_response_msg: h["ebarimt_response_msg"],
72
+ note: h["note"],
73
+ barimt_status: h["barimt_status"],
74
+ barimt_status_date: h["barimt_status_date"],
75
+ ebarimt_sent_email: h["ebarimt_sent_email"],
76
+ ebarimt_receiver_phone: h["ebarimt_receiver_phone"],
77
+ tax_type: h["tax_type"],
78
+ created_by: h["created_by"],
79
+ created_date: h["created_date"],
80
+ updated_by: h["updated_by"],
81
+ updated_date: h["updated_date"],
82
+ status: h["status"]
83
+ )
84
+ end
85
+
86
+ QPay::EbarimtResponse.new(
87
+ id: data["id"],
88
+ ebarimt_by: data["ebarimt_by"],
89
+ g_wallet_id: data["g_wallet_id"],
90
+ g_wallet_customer_id: data["g_wallet_customer_id"],
91
+ ebarimt_receiver_type: data["ebarimt_receiver_type"],
92
+ ebarimt_receiver: data["ebarimt_receiver"],
93
+ ebarimt_district_code: data["ebarimt_district_code"],
94
+ ebarimt_bill_type: data["ebarimt_bill_type"],
95
+ g_merchant_id: data["g_merchant_id"],
96
+ merchant_branch_code: data["merchant_branch_code"],
97
+ merchant_terminal_code: data["merchant_terminal_code"],
98
+ merchant_staff_code: data["merchant_staff_code"],
99
+ merchant_register_no: data["merchant_register_no"],
100
+ g_payment_id: data["g_payment_id"],
101
+ paid_by: data["paid_by"],
102
+ object_type: data["object_type"],
103
+ obj_id: data["object_id"],
104
+ amount: data["amount"],
105
+ vat_amount: data["vat_amount"],
106
+ city_tax_amount: data["city_tax_amount"],
107
+ ebarimt_qr_data: data["ebarimt_qr_data"],
108
+ ebarimt_lottery: data["ebarimt_lottery"],
109
+ note: data["note"],
110
+ barimt_status: data["barimt_status"],
111
+ barimt_status_date: data["barimt_status_date"],
112
+ ebarimt_sent_email: data["ebarimt_sent_email"],
113
+ ebarimt_receiver_phone: data["ebarimt_receiver_phone"],
114
+ tax_type: data["tax_type"],
115
+ merchant_tin: data["merchant_tin"],
116
+ ebarimt_receipt_id: data["ebarimt_receipt_id"],
117
+ created_by: data["created_by"],
118
+ created_date: data["created_date"],
119
+ updated_by: data["updated_by"],
120
+ updated_date: data["updated_date"],
121
+ status: data["status"],
122
+ barimt_items: items,
123
+ barimt_transactions: data["barimt_transactions"] || [],
124
+ barimt_histories: histories
125
+ )
126
+ end
127
+ end
128
+ end
129
+ end
@@ -0,0 +1,70 @@
1
+ # frozen_string_literal: true
2
+
3
+ module QPay
4
+ module Endpoints
5
+ module Invoices
6
+ # Creates a detailed invoice with full options.
7
+ # POST /v2/invoice
8
+ #
9
+ # @param request [QPay::CreateInvoiceRequest]
10
+ # @return [QPay::InvoiceResponse]
11
+ def create_invoice(request)
12
+ data = do_request("POST", "/v2/invoice", body: serialize(request))
13
+ build_invoice_response(data)
14
+ end
15
+
16
+ # Creates a simple invoice with minimal fields.
17
+ # POST /v2/invoice
18
+ #
19
+ # @param request [QPay::CreateSimpleInvoiceRequest]
20
+ # @return [QPay::InvoiceResponse]
21
+ def create_simple_invoice(request)
22
+ data = do_request("POST", "/v2/invoice", body: serialize(request))
23
+ build_invoice_response(data)
24
+ end
25
+
26
+ # Creates an invoice with ebarimt (tax) information.
27
+ # POST /v2/invoice
28
+ #
29
+ # @param request [QPay::CreateEbarimtInvoiceRequest]
30
+ # @return [QPay::InvoiceResponse]
31
+ def create_ebarimt_invoice(request)
32
+ data = do_request("POST", "/v2/invoice", body: serialize(request))
33
+ build_invoice_response(data)
34
+ end
35
+
36
+ # Cancels an existing invoice by ID.
37
+ # DELETE /v2/invoice/{id}
38
+ #
39
+ # @param invoice_id [String]
40
+ # @return [void]
41
+ def cancel_invoice(invoice_id)
42
+ do_request("DELETE", "/v2/invoice/#{invoice_id}")
43
+ nil
44
+ end
45
+
46
+ private
47
+
48
+ def build_invoice_response(data)
49
+ return nil if data.nil?
50
+
51
+ urls = (data["urls"] || []).map do |u|
52
+ QPay::Deeplink.new(
53
+ name: u["name"],
54
+ description: u["description"],
55
+ logo: u["logo"],
56
+ link: u["link"]
57
+ )
58
+ end
59
+
60
+ QPay::InvoiceResponse.new(
61
+ invoice_id: data["invoice_id"],
62
+ qr_text: data["qr_text"],
63
+ qr_image: data["qr_image"],
64
+ qpay_short_url: data["qPay_shortUrl"],
65
+ urls: urls
66
+ )
67
+ end
68
+ end
69
+ end
70
+ end
@@ -0,0 +1,172 @@
1
+ # frozen_string_literal: true
2
+
3
+ module QPay
4
+ module Endpoints
5
+ module Payments
6
+ # Retrieves payment details by payment ID.
7
+ # GET /v2/payment/{id}
8
+ #
9
+ # @param payment_id [String]
10
+ # @return [QPay::PaymentDetail]
11
+ def get_payment(payment_id)
12
+ data = do_request("GET", "/v2/payment/#{payment_id}")
13
+ build_payment_detail(data)
14
+ end
15
+
16
+ # Checks if a payment has been made for an invoice.
17
+ # POST /v2/payment/check
18
+ #
19
+ # @param request [QPay::PaymentCheckRequest]
20
+ # @return [QPay::PaymentCheckResponse]
21
+ def check_payment(request)
22
+ data = do_request("POST", "/v2/payment/check", body: serialize(request))
23
+ build_payment_check_response(data)
24
+ end
25
+
26
+ # Returns a list of payments matching the given criteria.
27
+ # POST /v2/payment/list
28
+ #
29
+ # @param request [QPay::PaymentListRequest]
30
+ # @return [QPay::PaymentListResponse]
31
+ def list_payments(request)
32
+ data = do_request("POST", "/v2/payment/list", body: serialize(request))
33
+ build_payment_list_response(data)
34
+ end
35
+
36
+ # Cancels a payment (card transactions only).
37
+ # DELETE /v2/payment/cancel/{id}
38
+ #
39
+ # @param payment_id [String]
40
+ # @param request [QPay::PaymentCancelRequest, nil]
41
+ # @return [void]
42
+ def cancel_payment(payment_id, request: nil)
43
+ do_request("DELETE", "/v2/payment/cancel/#{payment_id}", body: request ? serialize(request) : nil)
44
+ nil
45
+ end
46
+
47
+ # Refunds a payment (card transactions only).
48
+ # DELETE /v2/payment/refund/{id}
49
+ #
50
+ # @param payment_id [String]
51
+ # @param request [QPay::PaymentRefundRequest, nil]
52
+ # @return [void]
53
+ def refund_payment(payment_id, request: nil)
54
+ do_request("DELETE", "/v2/payment/refund/#{payment_id}", body: request ? serialize(request) : nil)
55
+ nil
56
+ end
57
+
58
+ private
59
+
60
+ def build_card_transactions(items)
61
+ (items || []).map do |ct|
62
+ QPay::CardTransaction.new(
63
+ card_merchant_code: ct["card_merchant_code"],
64
+ card_terminal_code: ct["card_terminal_code"],
65
+ card_number: ct["card_number"],
66
+ card_type: ct["card_type"],
67
+ is_cross_border: ct["is_cross_border"],
68
+ amount: ct["amount"],
69
+ transaction_amount: ct["transaction_amount"],
70
+ currency: ct["currency"],
71
+ transaction_currency: ct["transaction_currency"],
72
+ date: ct["date"],
73
+ transaction_date: ct["transaction_date"],
74
+ status: ct["status"],
75
+ transaction_status: ct["transaction_status"],
76
+ settlement_status: ct["settlement_status"],
77
+ settlement_status_date: ct["settlement_status_date"]
78
+ )
79
+ end
80
+ end
81
+
82
+ def build_p2p_transactions(items)
83
+ (items || []).map do |pt|
84
+ QPay::P2PTransaction.new(
85
+ transaction_bank_code: pt["transaction_bank_code"],
86
+ account_bank_code: pt["account_bank_code"],
87
+ account_bank_name: pt["account_bank_name"],
88
+ account_number: pt["account_number"],
89
+ status: pt["status"],
90
+ amount: pt["amount"],
91
+ currency: pt["currency"],
92
+ settlement_status: pt["settlement_status"]
93
+ )
94
+ end
95
+ end
96
+
97
+ def build_payment_detail(data)
98
+ return nil if data.nil?
99
+
100
+ QPay::PaymentDetail.new(
101
+ payment_id: data["payment_id"],
102
+ payment_status: data["payment_status"],
103
+ payment_fee: data["payment_fee"],
104
+ payment_amount: data["payment_amount"],
105
+ payment_currency: data["payment_currency"],
106
+ payment_date: data["payment_date"],
107
+ payment_wallet: data["payment_wallet"],
108
+ transaction_type: data["transaction_type"],
109
+ object_type: data["object_type"],
110
+ obj_id: data["object_id"],
111
+ next_payment_date: data["next_payment_date"],
112
+ next_payment_datetime: data["next_payment_datetime"],
113
+ card_transactions: build_card_transactions(data["card_transactions"]),
114
+ p2p_transactions: build_p2p_transactions(data["p2p_transactions"])
115
+ )
116
+ end
117
+
118
+ def build_payment_check_response(data)
119
+ return nil if data.nil?
120
+
121
+ rows = (data["rows"] || []).map do |row|
122
+ QPay::PaymentCheckRow.new(
123
+ payment_id: row["payment_id"],
124
+ payment_status: row["payment_status"],
125
+ payment_amount: row["payment_amount"],
126
+ trx_fee: row["trx_fee"],
127
+ payment_currency: row["payment_currency"],
128
+ payment_wallet: row["payment_wallet"],
129
+ payment_type: row["payment_type"],
130
+ next_payment_date: row["next_payment_date"],
131
+ next_payment_datetime: row["next_payment_datetime"],
132
+ card_transactions: build_card_transactions(row["card_transactions"]),
133
+ p2p_transactions: build_p2p_transactions(row["p2p_transactions"])
134
+ )
135
+ end
136
+
137
+ QPay::PaymentCheckResponse.new(
138
+ count: data["count"],
139
+ paid_amount: data["paid_amount"],
140
+ rows: rows
141
+ )
142
+ end
143
+
144
+ def build_payment_list_response(data)
145
+ return nil if data.nil?
146
+
147
+ rows = (data["rows"] || []).map do |row|
148
+ QPay::PaymentListItem.new(
149
+ payment_id: row["payment_id"],
150
+ payment_date: row["payment_date"],
151
+ payment_status: row["payment_status"],
152
+ payment_fee: row["payment_fee"],
153
+ payment_amount: row["payment_amount"],
154
+ payment_currency: row["payment_currency"],
155
+ payment_wallet: row["payment_wallet"],
156
+ payment_name: row["payment_name"],
157
+ payment_description: row["payment_description"],
158
+ qr_code: row["qr_code"],
159
+ paid_by: row["paid_by"],
160
+ object_type: row["object_type"],
161
+ obj_id: row["object_id"]
162
+ )
163
+ end
164
+
165
+ QPay::PaymentListResponse.new(
166
+ count: data["count"],
167
+ rows: rows
168
+ )
169
+ end
170
+ end
171
+ end
172
+ end
@@ -0,0 +1,83 @@
1
+ # frozen_string_literal: true
2
+
3
+ module QPay
4
+ class Error < StandardError
5
+ attr_reader :status_code, :code, :raw_body
6
+
7
+ def initialize(status_code:, code: nil, message: nil, raw_body: nil)
8
+ @status_code = status_code
9
+ @code = code
10
+ @raw_body = raw_body
11
+ super(build_message(message))
12
+ end
13
+
14
+ private
15
+
16
+ def build_message(msg)
17
+ "qpay: #{@code} - #{msg} (status #{@status_code})"
18
+ end
19
+ end
20
+
21
+ # Returns [QPay::Error, true] if the error is a QPay::Error, [nil, false] otherwise.
22
+ def self.qpay_error?(err)
23
+ if err.is_a?(QPay::Error)
24
+ [err, true]
25
+ else
26
+ [nil, false]
27
+ end
28
+ end
29
+
30
+ # Error code constants
31
+ ACCOUNT_BANK_DUPLICATED = "ACCOUNT_BANK_DUPLICATED"
32
+ ACCOUNT_SELECTION_INVALID = "ACCOUNT_SELECTION_INVALID"
33
+ AUTHENTICATION_FAILED = "AUTHENTICATION_FAILED"
34
+ BANK_ACCOUNT_NOTFOUND = "BANK_ACCOUNT_NOTFOUND"
35
+ BANK_MCC_ALREADY_ADDED = "BANK_MCC_ALREADY_ADDED"
36
+ BANK_MCC_NOT_FOUND = "BANK_MCC_NOT_FOUND"
37
+ CARD_TERMINAL_NOTFOUND = "CARD_TERMINAL_NOTFOUND"
38
+ CLIENT_NOTFOUND = "CLIENT_NOTFOUND"
39
+ CLIENT_USERNAME_DUPLICATED = "CLIENT_USERNAME_DUPLICATED"
40
+ CUSTOMER_DUPLICATE = "CUSTOMER_DUPLICATE"
41
+ CUSTOMER_NOTFOUND = "CUSTOMER_NOTFOUND"
42
+ CUSTOMER_REGISTER_INVALID = "CUSTOMER_REGISTER_INVALID"
43
+ EBARIMT_CANCEL_NOTSUPPERDED = "EBARIMT_CANCEL_NOTSUPPERDED"
44
+ EBARIMT_NOT_REGISTERED = "EBARIMT_NOT_REGISTERED"
45
+ EBARIMT_QR_CODE_INVALID = "EBARIMT_QR_CODE_INVALID"
46
+ INFORM_NOTFOUND = "INFORM_NOTFOUND"
47
+ INPUT_CODE_REGISTERED = "INPUT_CODE_REGISTERED"
48
+ INPUT_NOTFOUND = "INPUT_NOTFOUND"
49
+ INVALID_AMOUNT = "INVALID_AMOUNT"
50
+ INVALID_OBJECT_TYPE = "INVALID_OBJECT_TYPE"
51
+ INVOICE_ALREADY_CANCELED = "INVOICE_ALREADY_CANCELED"
52
+ INVOICE_CODE_INVALID = "INVOICE_CODE_INVALID"
53
+ INVOICE_CODE_REGISTERED = "INVOICE_CODE_REGISTERED"
54
+ INVOICE_LINE_REQUIRED = "INVOICE_LINE_REQUIRED"
55
+ INVOICE_NOTFOUND = "INVOICE_NOTFOUND"
56
+ INVOICE_PAID = "INVOICE_PAID"
57
+ INVOICE_RECEIVER_DATA_ADDRESS_REQUIRED = "INVOICE_RECEIVER_DATA_ADDRESS_REQUIRED"
58
+ INVOICE_RECEIVER_DATA_EMAIL_REQUIRED = "INVOICE_RECEIVER_DATA_EMAIL_REQUIRED"
59
+ INVOICE_RECEIVER_DATA_PHONE_REQUIRED = "INVOICE_RECEIVER_DATA_PHONE_REQUIRED"
60
+ INVOICE_RECEIVER_DATA_REQUIRED = "INVOICE_RECEIVER_DATA_REQUIRED"
61
+ MAX_AMOUNT_ERR = "MAX_AMOUNT_ERR"
62
+ MCC_NOTFOUND = "MCC_NOTFOUND"
63
+ MERCHANT_ALREADY_REGISTERED = "MERCHANT_ALREADY_REGISTERED"
64
+ MERCHANT_INACTIVE = "MERCHANT_INACTIVE"
65
+ MERCHANT_NOTFOUND = "MERCHANT_NOTFOUND"
66
+ MIN_AMOUNT_ERR = "MIN_AMOUNT_ERR"
67
+ NO_CREDENDIALS = "NO_CREDENDIALS"
68
+ OBJECT_DATA_ERROR = "OBJECT_DATA_ERROR"
69
+ P2P_TERMINAL_NOTFOUND = "P2P_TERMINAL_NOTFOUND"
70
+ PAYMENT_ALREADY_CANCELED = "PAYMENT_ALREADY_CANCELED"
71
+ PAYMENT_NOT_PAID = "PAYMENT_NOT_PAID"
72
+ PAYMENT_NOTFOUND = "PAYMENT_NOTFOUND"
73
+ PERMISSION_DENIED = "PERMISSION_DENIED"
74
+ QRACCOUNT_INACTIVE = "QRACCOUNT_INACTIVE"
75
+ QRACCOUNT_NOTFOUND = "QRACCOUNT_NOTFOUND"
76
+ QRCODE_NOTFOUND = "QRCODE_NOTFOUND"
77
+ QRCODE_USED = "QRCODE_USED"
78
+ SENDER_BRANCH_DATA_REQUIRED = "SENDER_BRANCH_DATA_REQUIRED"
79
+ TAX_LINE_REQUIRED = "TAX_LINE_REQUIRED"
80
+ TAX_PRODUCT_CODE_REQUIRED = "TAX_PRODUCT_CODE_REQUIRED"
81
+ TRANSACTION_NOT_APPROVED = "TRANSACTION_NOT_APPROVED"
82
+ TRANSACTION_REQUIRED = "TRANSACTION_REQUIRED"
83
+ end
@@ -0,0 +1,226 @@
1
+ # frozen_string_literal: true
2
+
3
+ module QPay
4
+ # --- Auth ---
5
+
6
+ TokenResponse = Struct.new(
7
+ :token_type, :refresh_expires_in, :refresh_token,
8
+ :access_token, :expires_in, :scope,
9
+ :not_before_policy, :session_state,
10
+ keyword_init: true
11
+ )
12
+
13
+ # --- Common nested types ---
14
+
15
+ Address = Struct.new(
16
+ :city, :district, :street, :building,
17
+ :address, :zipcode, :longitude, :latitude,
18
+ keyword_init: true
19
+ )
20
+
21
+ SenderBranchData = Struct.new(
22
+ :register, :name, :email, :phone, :address,
23
+ keyword_init: true
24
+ )
25
+
26
+ SenderStaffData = Struct.new(
27
+ :name, :email, :phone,
28
+ keyword_init: true
29
+ )
30
+
31
+ InvoiceReceiverData = Struct.new(
32
+ :register, :name, :email, :phone, :address,
33
+ keyword_init: true
34
+ )
35
+
36
+ Account = Struct.new(
37
+ :account_bank_code, :account_number, :iban_number,
38
+ :account_name, :account_currency, :is_default,
39
+ keyword_init: true
40
+ )
41
+
42
+ Transaction = Struct.new(
43
+ :description, :amount, :accounts,
44
+ keyword_init: true
45
+ )
46
+
47
+ InvoiceLine = Struct.new(
48
+ :tax_product_code, :line_description, :line_quantity,
49
+ :line_unit_price, :note, :discounts, :surcharges, :taxes,
50
+ keyword_init: true
51
+ )
52
+
53
+ EbarimtInvoiceLine = Struct.new(
54
+ :tax_product_code, :line_description, :barcode,
55
+ :line_quantity, :line_unit_price, :note,
56
+ :classification_code, :taxes,
57
+ keyword_init: true
58
+ )
59
+
60
+ TaxEntry = Struct.new(
61
+ :tax_code, :discount_code, :surcharge_code,
62
+ :description, :amount, :note,
63
+ keyword_init: true
64
+ )
65
+
66
+ Deeplink = Struct.new(
67
+ :name, :description, :logo, :link,
68
+ keyword_init: true
69
+ )
70
+
71
+ # --- Invoice ---
72
+
73
+ CreateInvoiceRequest = Struct.new(
74
+ :invoice_code, :sender_invoice_no, :sender_branch_code,
75
+ :sender_branch_data, :sender_staff_data, :sender_staff_code,
76
+ :invoice_receiver_code, :invoice_receiver_data, :invoice_description,
77
+ :enable_expiry, :allow_partial, :minimum_amount,
78
+ :allow_exceed, :maximum_amount, :amount,
79
+ :callback_url, :sender_terminal_code, :sender_terminal_data,
80
+ :allow_subscribe, :subscription_interval, :subscription_webhook,
81
+ :note, :transactions, :lines,
82
+ keyword_init: true
83
+ )
84
+
85
+ CreateSimpleInvoiceRequest = Struct.new(
86
+ :invoice_code, :sender_invoice_no, :invoice_receiver_code,
87
+ :invoice_description, :sender_branch_code, :amount, :callback_url,
88
+ keyword_init: true
89
+ )
90
+
91
+ CreateEbarimtInvoiceRequest = Struct.new(
92
+ :invoice_code, :sender_invoice_no, :sender_branch_code,
93
+ :sender_staff_data, :sender_staff_code, :invoice_receiver_code,
94
+ :invoice_receiver_data, :invoice_description, :tax_type,
95
+ :district_code, :callback_url, :lines,
96
+ keyword_init: true
97
+ )
98
+
99
+ InvoiceResponse = Struct.new(
100
+ :invoice_id, :qr_text, :qr_image, :qpay_short_url, :urls,
101
+ keyword_init: true
102
+ )
103
+
104
+ # --- Payment ---
105
+
106
+ Offset = Struct.new(
107
+ :page_number, :page_limit,
108
+ keyword_init: true
109
+ )
110
+
111
+ PaymentCheckRequest = Struct.new(
112
+ :object_type, :obj_id, :offset,
113
+ keyword_init: true
114
+ )
115
+
116
+ PaymentCheckResponse = Struct.new(
117
+ :count, :paid_amount, :rows,
118
+ keyword_init: true
119
+ )
120
+
121
+ PaymentCheckRow = Struct.new(
122
+ :payment_id, :payment_status, :payment_amount,
123
+ :trx_fee, :payment_currency, :payment_wallet,
124
+ :payment_type, :next_payment_date, :next_payment_datetime,
125
+ :card_transactions, :p2p_transactions,
126
+ keyword_init: true
127
+ )
128
+
129
+ PaymentDetail = Struct.new(
130
+ :payment_id, :payment_status, :payment_fee,
131
+ :payment_amount, :payment_currency, :payment_date,
132
+ :payment_wallet, :transaction_type, :object_type,
133
+ :obj_id, :next_payment_date, :next_payment_datetime,
134
+ :card_transactions, :p2p_transactions,
135
+ keyword_init: true
136
+ )
137
+
138
+ CardTransaction = Struct.new(
139
+ :card_merchant_code, :card_terminal_code, :card_number,
140
+ :card_type, :is_cross_border, :amount,
141
+ :transaction_amount, :currency, :transaction_currency,
142
+ :date, :transaction_date, :status,
143
+ :transaction_status, :settlement_status, :settlement_status_date,
144
+ keyword_init: true
145
+ )
146
+
147
+ P2PTransaction = Struct.new(
148
+ :transaction_bank_code, :account_bank_code, :account_bank_name,
149
+ :account_number, :status, :amount,
150
+ :currency, :settlement_status,
151
+ keyword_init: true
152
+ )
153
+
154
+ PaymentListRequest = Struct.new(
155
+ :object_type, :obj_id, :start_date, :end_date, :offset,
156
+ keyword_init: true
157
+ )
158
+
159
+ PaymentListResponse = Struct.new(
160
+ :count, :rows,
161
+ keyword_init: true
162
+ )
163
+
164
+ PaymentListItem = Struct.new(
165
+ :payment_id, :payment_date, :payment_status,
166
+ :payment_fee, :payment_amount, :payment_currency,
167
+ :payment_wallet, :payment_name, :payment_description,
168
+ :qr_code, :paid_by, :object_type, :obj_id,
169
+ keyword_init: true
170
+ )
171
+
172
+ PaymentCancelRequest = Struct.new(
173
+ :callback_url, :note,
174
+ keyword_init: true
175
+ )
176
+
177
+ PaymentRefundRequest = Struct.new(
178
+ :callback_url, :note,
179
+ keyword_init: true
180
+ )
181
+
182
+ # --- Ebarimt ---
183
+
184
+ CreateEbarimtRequest = Struct.new(
185
+ :payment_id, :ebarimt_receiver_type, :ebarimt_receiver,
186
+ :district_code, :classification_code,
187
+ keyword_init: true
188
+ )
189
+
190
+ EbarimtResponse = Struct.new(
191
+ :id, :ebarimt_by, :g_wallet_id, :g_wallet_customer_id,
192
+ :ebarimt_receiver_type, :ebarimt_receiver, :ebarimt_district_code,
193
+ :ebarimt_bill_type, :g_merchant_id, :merchant_branch_code,
194
+ :merchant_terminal_code, :merchant_staff_code, :merchant_register_no,
195
+ :g_payment_id, :paid_by, :object_type, :obj_id,
196
+ :amount, :vat_amount, :city_tax_amount,
197
+ :ebarimt_qr_data, :ebarimt_lottery, :note,
198
+ :barimt_status, :barimt_status_date, :ebarimt_sent_email,
199
+ :ebarimt_receiver_phone, :tax_type, :merchant_tin,
200
+ :ebarimt_receipt_id, :created_by, :created_date,
201
+ :updated_by, :updated_date, :status,
202
+ :barimt_items, :barimt_transactions, :barimt_histories,
203
+ keyword_init: true
204
+ )
205
+
206
+ EbarimtItem = Struct.new(
207
+ :id, :barimt_id, :merchant_product_code, :tax_product_code,
208
+ :bar_code, :name, :unit_price, :quantity,
209
+ :amount, :city_tax_amount, :vat_amount, :note,
210
+ :created_by, :created_date, :updated_by, :updated_date, :status,
211
+ keyword_init: true
212
+ )
213
+
214
+ EbarimtHistory = Struct.new(
215
+ :id, :barimt_id, :ebarimt_receiver_type, :ebarimt_receiver,
216
+ :ebarimt_register_no, :ebarimt_bill_id, :ebarimt_date,
217
+ :ebarimt_mac_address, :ebarimt_internal_code, :ebarimt_bill_type,
218
+ :ebarimt_qr_data, :ebarimt_lottery, :ebarimt_lottery_msg,
219
+ :ebarimt_error_code, :ebarimt_error_msg,
220
+ :ebarimt_response_code, :ebarimt_response_msg,
221
+ :note, :barimt_status, :barimt_status_date,
222
+ :ebarimt_sent_email, :ebarimt_receiver_phone, :tax_type,
223
+ :created_by, :created_date, :updated_by, :updated_date, :status,
224
+ keyword_init: true
225
+ )
226
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module QPay
4
+ VERSION = "1.0.0"
5
+ end
data/lib/qpay.rb ADDED
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "qpay/version"
4
+ require_relative "qpay/errors"
5
+ require_relative "qpay/config"
6
+ require_relative "qpay/models"
7
+ require_relative "qpay/endpoints/auth"
8
+ require_relative "qpay/endpoints/invoices"
9
+ require_relative "qpay/endpoints/payments"
10
+ require_relative "qpay/endpoints/ebarimt"
11
+ require_relative "qpay/client"
data/qpay.gemspec ADDED
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "lib/qpay/version"
4
+
5
+ Gem::Specification.new do |spec|
6
+ spec.name = "qpay-sdk"
7
+ spec.version = QPay::VERSION
8
+ spec.authors = ["Usukhbayar"]
9
+ spec.email = ["coding.usukh@gmail.com"]
10
+ spec.license = "MIT"
11
+
12
+ spec.summary = "QPay V2 API Ruby SDK"
13
+ spec.description = "Ruby SDK for QPay V2 payment gateway API with auto token management."
14
+ spec.homepage = "https://github.com/qpay-sdk/qpay-ruby"
15
+
16
+ spec.required_ruby_version = ">= 3.0.0"
17
+
18
+ spec.files = Dir["lib/**/*.rb", "LICENSE", "qpay.gemspec"]
19
+ spec.require_paths = ["lib"]
20
+
21
+ spec.add_dependency "faraday", "~> 2.0"
22
+
23
+ spec.add_development_dependency "rake", "~> 13.0"
24
+ spec.add_development_dependency "rspec", "~> 3.0"
25
+ spec.add_development_dependency "webmock", "~> 3.0"
26
+
27
+ spec.metadata["homepage_uri"] = spec.homepage
28
+ spec.metadata["source_code_uri"] = spec.homepage
29
+ end
metadata ADDED
@@ -0,0 +1,113 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: qpay-sdk
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.0.0
5
+ platform: ruby
6
+ authors:
7
+ - Usukhbayar
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2026-02-26 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: faraday
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '2.0'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '2.0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: rake
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '13.0'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '13.0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: rspec
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '3.0'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '3.0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: webmock
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - "~>"
60
+ - !ruby/object:Gem::Version
61
+ version: '3.0'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - "~>"
67
+ - !ruby/object:Gem::Version
68
+ version: '3.0'
69
+ description: Ruby SDK for QPay V2 payment gateway API with auto token management.
70
+ email:
71
+ - coding.usukh@gmail.com
72
+ executables: []
73
+ extensions: []
74
+ extra_rdoc_files: []
75
+ files:
76
+ - LICENSE
77
+ - lib/qpay.rb
78
+ - lib/qpay/client.rb
79
+ - lib/qpay/config.rb
80
+ - lib/qpay/endpoints/auth.rb
81
+ - lib/qpay/endpoints/ebarimt.rb
82
+ - lib/qpay/endpoints/invoices.rb
83
+ - lib/qpay/endpoints/payments.rb
84
+ - lib/qpay/errors.rb
85
+ - lib/qpay/models.rb
86
+ - lib/qpay/version.rb
87
+ - qpay.gemspec
88
+ homepage: https://github.com/qpay-sdk/qpay-ruby
89
+ licenses:
90
+ - MIT
91
+ metadata:
92
+ homepage_uri: https://github.com/qpay-sdk/qpay-ruby
93
+ source_code_uri: https://github.com/qpay-sdk/qpay-ruby
94
+ post_install_message:
95
+ rdoc_options: []
96
+ require_paths:
97
+ - lib
98
+ required_ruby_version: !ruby/object:Gem::Requirement
99
+ requirements:
100
+ - - ">="
101
+ - !ruby/object:Gem::Version
102
+ version: 3.0.0
103
+ required_rubygems_version: !ruby/object:Gem::Requirement
104
+ requirements:
105
+ - - ">="
106
+ - !ruby/object:Gem::Version
107
+ version: '0'
108
+ requirements: []
109
+ rubygems_version: 3.0.3.1
110
+ signing_key:
111
+ specification_version: 4
112
+ summary: QPay V2 API Ruby SDK
113
+ test_files: []