comgate_ruby 0.5.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: a7d8a030a101f774292a7f3214a1d3338238afa11fbc9c98e348db15259d6fde
4
+ data.tar.gz: 6253ba9e4d7e3a1737f491b3efccbd82bd85afc1c802c97c4939e3114dcc88ed
5
+ SHA512:
6
+ metadata.gz: 47713672ee0beed059c9e26b90bbaebded0cdc29db78f08e20c9e406678c2ffd59562a439f81c8b5a2a9d0716ab7b637af9dcc86068a9010df0e0350c4984ff9
7
+ data.tar.gz: 9d2a09ac2489566918c5f131764636c34113bfb92cc57be659b72d349bbd78c7ebbec0fb7c2d5108e28261a4f728dc6ef4ca312dc684f331a12981ad7c6cf988
data/.rubocop.yml ADDED
@@ -0,0 +1,26 @@
1
+ AllCops:
2
+ NewCops: enable
3
+ TargetRubyVersion: 3.0
4
+
5
+ Style/StringLiterals:
6
+ Enabled: true
7
+ EnforcedStyle: double_quotes
8
+
9
+ Style/StringLiteralsInInterpolation:
10
+ Enabled: true
11
+ EnforcedStyle: double_quotes
12
+
13
+ Layout/LineLength:
14
+ Max: 120
15
+
16
+ Style/Documentation:
17
+ Enabled: false
18
+
19
+ Metrics/ClassLength:
20
+ Enabled: false
21
+
22
+ Metrics/MethodLength:
23
+ Enabled: false
24
+
25
+ Metrics/AbcSize:
26
+ Max: 25
data/.tool-versions ADDED
@@ -0,0 +1,3 @@
1
+ ruby 3.1.2
2
+
3
+
data/CHANGELOG.md ADDED
@@ -0,0 +1,5 @@
1
+ ## [0.5.0] - 2023-04-20
2
+
3
+ - Initial release
4
+
5
+
data/Gemfile ADDED
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ source "https://rubygems.org"
4
+
5
+ # Specify your gem's dependencies in comgate_ruby.gemspec
6
+ gemspec
7
+
8
+ gem "minitest", "~> 5.0"
9
+ gem "pry-byebug"
10
+ gem "rake", "~> 13.0"
11
+ gem "rspec-expectations"
12
+ gem "rubocop", "~> 1.21"
data/Gemfile.lock ADDED
@@ -0,0 +1,60 @@
1
+ PATH
2
+ remote: .
3
+ specs:
4
+ comgate_ruby (0.5.0)
5
+
6
+ GEM
7
+ remote: https://rubygems.org/
8
+ specs:
9
+ ast (2.4.2)
10
+ byebug (11.1.3)
11
+ coderay (1.1.3)
12
+ diff-lcs (1.5.0)
13
+ json (2.6.3)
14
+ method_source (1.0.0)
15
+ minitest (5.18.0)
16
+ parallel (1.22.1)
17
+ parser (3.2.0.0)
18
+ ast (~> 2.4.1)
19
+ pry (0.14.2)
20
+ coderay (~> 1.1)
21
+ method_source (~> 1.0)
22
+ pry-byebug (3.10.1)
23
+ byebug (~> 11.0)
24
+ pry (>= 0.13, < 0.15)
25
+ rainbow (3.1.1)
26
+ rake (13.0.6)
27
+ regexp_parser (2.6.2)
28
+ rexml (3.2.5)
29
+ rspec-expectations (3.12.2)
30
+ diff-lcs (>= 1.2.0, < 2.0)
31
+ rspec-support (~> 3.12.0)
32
+ rspec-support (3.12.0)
33
+ rubocop (1.44.0)
34
+ json (~> 2.3)
35
+ parallel (~> 1.10)
36
+ parser (>= 3.2.0.0)
37
+ rainbow (>= 2.2.2, < 4.0)
38
+ regexp_parser (>= 1.8, < 3.0)
39
+ rexml (>= 3.2.5, < 4.0)
40
+ rubocop-ast (>= 1.24.1, < 2.0)
41
+ ruby-progressbar (~> 1.7)
42
+ unicode-display_width (>= 2.4.0, < 3.0)
43
+ rubocop-ast (1.24.1)
44
+ parser (>= 3.1.1.0)
45
+ ruby-progressbar (1.11.0)
46
+ unicode-display_width (2.4.2)
47
+
48
+ PLATFORMS
49
+ x86_64-linux
50
+
51
+ DEPENDENCIES
52
+ comgate_ruby!
53
+ minitest (~> 5.0)
54
+ pry-byebug
55
+ rake (~> 13.0)
56
+ rspec-expectations
57
+ rubocop (~> 1.21)
58
+
59
+ BUNDLED WITH
60
+ 2.3.22
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2023 Sinfin
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/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2023 Petr Mlčoch
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
13
+ all 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
21
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,185 @@
1
+ # comgate_ruby
2
+ Client for Comgate payment gateway
3
+
4
+ ## API docs for Comgate
5
+ https://help.comgate.cz/docs/api-protokol
6
+
7
+ ## Installation
8
+
9
+ Install the gem and add to the application's Gemfile by executing:
10
+
11
+ $ bundle add comgate_ruby
12
+
13
+ If bundler is not being used to manage dependencies, install the gem by executing:
14
+
15
+ $ gem install comgate_ruby
16
+
17
+ ## Usage
18
+ ### 1) set gateway object
19
+ As singleton on app init or for each transaction:
20
+ ```ruby
21
+ gateway = Commgate::Gateway.new(merchant_gateway_id: ":comgate_id",
22
+ test_calls: false,
23
+ secret: ":comgate_secret")
24
+ ```
25
+
26
+ ### 2) prepare endpoint
27
+ Comgate sends POST requests to your app about transactions updates. The URL of it needs to be setup in Comgate Client portal. At endpoint, just call `gateway.process_payment_callback(params)`, which will return
28
+ `{state: :paid, transaction_id: ":transID"}` (at least). See bullets 4) and 5) in Single payment process bellow.
29
+ ### 3) call appropriate method
30
+ (see bellow)
31
+
32
+ ## Usecases
33
+ ### Single payment process
34
+ 1) Start transaction by `gateway.start_transaction(payment_data)`. Response is
35
+ ```ruby
36
+ #<Comgate::Response:0x00007f56800295a8
37
+ @array=nil,
38
+ @errors=nil,
39
+ @hash={:code=>0, :message=>"OK", :transaction_id=>"AB12-CD34-EF56", :redirect_to=>"https://payments.comgate.cz/client/instructions/index?id=AB12-CD34-EF56"},
40
+ @http_code=200,
41
+ @params_conversion_hash=
42
+ { .... }
43
+ @redirect_to="https://payments.comgate.cz/client/instructions/index?id=AB12-CD34-EF56">
44
+ ```
45
+ Important part is `response.hash` :
46
+ ```ruby
47
+ {
48
+ code: 0,
49
+ message: "OK",
50
+ transaction_id: "AB12-CD34-EF56"
51
+ redirect_to: "https://payments.comgate.cz/client/instructions/index?id=AB12-CD34-EF56",
52
+ }
53
+ ```
54
+ 2) Redirect user to `response.redirect_to` page (=> display Comgate form).
55
+ 3) Client will (not) pay.
56
+ 4) Comgate will send request to your defined endpoint about status change of transaction. Call `gateway.process_state_change(payload)`, which will return
57
+ `{state: :paid, transaction_id: ":transID"}`(and maybe some more informations).
58
+ 5) Now is Your time to handle payment (or other state like `cancelled`, `authorized`).
59
+
60
+ ### Recurring payments
61
+ 1) Use `gateway.start_recurring_transaction(payment_data)` and store `transaction_id`.
62
+ 2) Create following payments `gateway.repeat_recurring_transaction(payment_data: new_payment_data.merge({transaction_id: ":transID"}) }})`. No redirection here. Price can change in each payment.
63
+ 3) Handle status change like bullets 4) and 5) in single payment
64
+
65
+ ### Preauthorized payments
66
+ 1) Use `gateway.start_preauthorized_transaction(payment_data)` and store `transaction_id`.
67
+ 2a) Confirm payment by `gateway.confirm_preauthorized_transaction(payment_data.merge({transaction_id: ":transID"}))` (price cannot exceed preauthorized amount)
68
+ 2b) Cancel payment by `gateway.cancel_preauthorized_transaction(transaction_id: ":transID")`
69
+ 3) Handle status change like bullets 4) and 5) in single payment
70
+
71
+ ### Verification payments
72
+ 1) Use `gateway.start_verification_transaction(payment_data)` and store `transaction_id`.
73
+ 2) If payment is succesfull, bank will refund payment immediatelly.
74
+ 3) Then you can create (repeat) payments like reccuring payments.
75
+
76
+ ### Refund payment
77
+ 1) Call `gateway.refund_transaction(payment_data.merge({transaction_id: ":transID"}))` (refunded value cannot exceed paid amount)
78
+ 2) Handle status change like bullets 4) and 5) in single payment
79
+
80
+ ### Cancel payment
81
+ 1) Call `gateway.cancel_transaction(transaction_id: ":transID")`
82
+ 2) Handle status change like bullets 4) and 5) in single payment
83
+
84
+ ### Check payment state (ad-hoc)
85
+ 0) The endpoint must be always implemented, this is just additional way to check payment state
86
+ 1) Call `gateway.check_state(transaction_id: ":transID")`. It will return `{state: :paid, transaction_id: ":transID"}` and some more infos.
87
+ 2) Handle status change like bullet 5) in single payment
88
+
89
+ ### Get payment methods allowed to merchant
90
+ 1) Call `gateway.allowed_payment_methods(params)`. It will return array of allowed payment methods in `response.array`.
91
+ ```ruby
92
+ [
93
+ { id: "BANK_CZ_CS_P",
94
+ name: "Česká spořitelna - PLATBA 24",
95
+ description: "On-line platba pro majitele účtu u České spořitelny.",
96
+ logo_url: "https://payments.comgate.cz/assets/images/logos/BANK_CZ_CS_P.png" },
97
+ { id: "BANK_CZ_FB_P",
98
+ name: "Fio banka - PayMyway",
99
+ description: "On-line platba pro majitele účtu u Fio banky.",
100
+ logo_url: "https://payments.comgate.cz/assets/images/logos/BANK_CZ_FB.png" }
101
+ ]
102
+ ```
103
+
104
+ ### Get list of transfers for date
105
+ 1) Call `gateway.transfers_from(date)`. Array of transfers will be returned in `response.array`.
106
+ ```ruby
107
+ [
108
+ { transfer_id: 1234567,
109
+ transfer_date: date,
110
+ account_counter_party: "0/0000",
111
+ account_outgoing: "123456789/0000",
112
+ variable_symbol: "12345678"}
113
+ ]
114
+ ```
115
+
116
+ ## Parameters
117
+ Structure of parameters is unchanged across most of methods, but you can leave out unused keys. You will get error if You do not pass required key.
118
+ See `test/comgate/test_gateway.rb` for examples.
119
+ Also returned hash have consistent structure (very similar to input params)
120
+ Maximal mixed version looks like:
121
+ ```ruby
122
+ {
123
+ code: 0, # output
124
+ message: "OK", # output
125
+ transfer_id: "1234-abcd-45678", # input/output
126
+ test: true, # input (handle as test call)/ output (created by test call)
127
+ state: :paid, # output (:pending, :paid, :cancelled, :authorized)
128
+ merchant: {
129
+ gateway_id: "some_id_from_comgate", # output (input is set at gateway init)
130
+ target_shop_account: "12345678/1234", # input (change against default)/ output
131
+ },
132
+ payment: {
133
+ amount_in_cents: 12_900, # input/output
134
+ currency: "CZK", # input/output
135
+ label: "Payment for 2 straws", # input/output
136
+ reference_id: "our eshop order #1 reference", # input/output
137
+ method: "CARD_CZ_CSOB_2", # input (selected method; or use "ALL") / output
138
+ product_name: "product name ABC", # input/output
139
+ fee: nil, # output ( if automatic deduction of the payment fee is set at Comgate)
140
+ variable_symbol: 739_689_656, # output (so I acctually do not know where it came from)
141
+ apple_pay_payload: "raw apple pay payload", # input
142
+ dynamic_expiration: false, # input (see https://help.comgate.cz/v1/docs/expirace-plateb )
143
+ expiration_time: "10h", # input ( use "m" or "h" or "d", but only one of them; allowed rage "30m".."7d")
144
+ },
145
+ payer: {
146
+ email: "payer1@gmail.com", # input/output
147
+ phone: "+420778899", # input/output
148
+ account_number: "account_num", # output
149
+ account_name: "payer account name" # output
150
+ },
151
+ options: {
152
+ country_code: "DE", # input (can restrict allowed payment methods)
153
+ language_code: "sk" # input
154
+ },
155
+ headers: {} # not actually used now
156
+ }
157
+ ```
158
+ ## Response
159
+ Response returned from `gateway` call is `Comgate::Response` instance.
160
+ You can check redirection `response.redirect? ? response.redirect_to : nil`.
161
+ Most of the time, the response shoul be hash-like, stored in `response.hash`. But for lists there will be array in `response.array`.
162
+ If there are errors from API call , they will be in `response.errors`. But note, that gateway will raise them and not return `Comgate::Response` instance.
163
+ And you can also check `result.http_code` (which is surprisingly 200 from API errors)
164
+
165
+ ## Errors
166
+ Connection errors or API error responses are raised as RuntimeError with message like ` "{:api=>[\"[Error #1309] incorrect amount\"]}"`.
167
+ Error Number and text can be found in `lib/comgate/response.rb`.
168
+ This may be refactored in future.
169
+
170
+ ## One more thing
171
+ This gem extends `Hash` with methods `deep_symbolize_keys` and `deep_merge` (if needed).
172
+
173
+ ## Development
174
+
175
+ After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake test` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
176
+
177
+ To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and the created tag, and push the `.gem` file to [rubygems.org](https://rubygems.org).
178
+
179
+ ## Contributing
180
+
181
+ Bug reports and pull requests are welcome on GitHub at https://github.com/[USERNAME]/comgate_ruby.
182
+
183
+ ## License
184
+
185
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
data/Rakefile ADDED
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bundler/gem_tasks"
4
+ require "rake/testtask"
5
+
6
+ Rake::TestTask.new(:test) do |t|
7
+ t.libs << "test"
8
+ t.libs << "lib"
9
+ t.test_files = FileList["test/**/test_*.rb"]
10
+ end
11
+
12
+ require "rubocop/rake_task"
13
+
14
+ RuboCop::RakeTask.new
15
+
16
+ task default: %i[test rubocop]
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "lib/comgate/version"
4
+
5
+ Gem::Specification.new do |spec|
6
+ spec.name = "comgate_ruby"
7
+ spec.version = Comgate::VERSION
8
+ spec.authors = ["Petr Mlčoch"]
9
+ spec.email = ["foton@centrum.cz"]
10
+
11
+ spec.summary = "Client for Comgate payment gateway"
12
+ spec.description = "Write a longer description or delete this line."
13
+ spec.homepage = "https://github.com/sinfin/comgate_ruby"
14
+ spec.license = "MIT"
15
+ spec.required_ruby_version = ">= 3.0"
16
+
17
+ spec.metadata["allowed_push_host"] = "https://rubygems.org"
18
+
19
+ spec.metadata["homepage_uri"] = spec.homepage
20
+ spec.metadata["source_code_uri"] = "https://github.com/sinfin/comgate_ruby"
21
+ spec.metadata["changelog_uri"] = "https://github.com/sinfin/comgate_ruby/blob/main/CHANGELOG.md"
22
+
23
+ # Specify which files should be added to the gem when it is released.
24
+ # The `git ls-files -z` loads the files in the RubyGem that have been added into git.
25
+ spec.files = Dir.chdir(__dir__) do
26
+ `git ls-files -z`.split("\x0").reject do |f|
27
+ (f == __FILE__) || f.match(%r{\A(?:(?:bin|test|spec|features)/|\.(?:git|travis|circleci)|appveyor)})
28
+ end
29
+ end
30
+ spec.bindir = "exe"
31
+ spec.executables = spec.files.grep(%r{\Aexe/}) { |f| File.basename(f) }
32
+ spec.require_paths = ["lib"]
33
+
34
+ # Uncomment to register a new dependency of your gem
35
+ # spec.add_dependency "example-gem", "~> 1.0"
36
+
37
+ # For more information and examples about making a new gem, check out our
38
+ # guide at: https://bundler.io/guides/creating_gem.html
39
+ spec.metadata["rubygems_mfa_required"] = "true"
40
+ end
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ class BaseService
4
+ attr_reader :result, :errors
5
+
6
+ def self.call(*args, **keyword_args)
7
+ if args.empty?
8
+ new(**keyword_args).call
9
+ elsif keyword_args.empty?
10
+ new(*args).call
11
+ else
12
+ new(*args, **keyword_args).call
13
+ end
14
+ end
15
+
16
+ def call
17
+ build_result
18
+ self # always returnning service itself, to get to `errors`, `result`
19
+ end
20
+
21
+ def initialize(*_args, **_keyword_args)
22
+ @result = nil
23
+ @errors = {}
24
+ end
25
+
26
+ def success?
27
+ errors.empty?
28
+ end
29
+
30
+ def failure?
31
+ !success?
32
+ end
33
+ alias failed? failure?
34
+ end
@@ -0,0 +1,180 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "openssl"
4
+ require "net/http"
5
+ require "json"
6
+
7
+ module Comgate
8
+ class ApiCaller < BaseService
9
+ KNOWN_CONNECTION_ERRORS = [
10
+ Timeout::Error,
11
+ Errno::EINVAL,
12
+ Errno::ECONNRESET,
13
+ EOFError,
14
+ SocketError,
15
+ Net::ReadTimeout,
16
+ Net::HTTPBadResponse,
17
+ Net::HTTPHeaderSyntaxError,
18
+ Net::ProtocolError
19
+ ].freeze
20
+
21
+ attr_reader :payload, :url
22
+
23
+ def initialize(url:, payload:, test_call: false)
24
+ super()
25
+ @url = url
26
+ @payload = payload
27
+ @payload.merge!(test: "true") if test_call
28
+ end
29
+
30
+ def build_result
31
+ call_api
32
+ process_response
33
+ end
34
+
35
+ private
36
+
37
+ attr_accessor :response
38
+
39
+ def call_api
40
+ self.response = https_conn.request(request)
41
+ rescue *KNOWN_CONNECTION_ERRORS => e
42
+ handle_connection_error(e)
43
+ end
44
+
45
+ def process_response
46
+ return unless errors.empty?
47
+
48
+ api_log(:debug, "Comgate API RESPONSE: #{response} with body:\n#{response.body}")
49
+
50
+ @result = {
51
+ http_code: response.code.to_i,
52
+ redirect_to: find_redirect_to,
53
+ response_body: decoded_response_body
54
+ }
55
+
56
+ record_api_error
57
+ end
58
+
59
+ def https_conn
60
+ @https_conn ||= Net::HTTP.start(service_uri.host, service_uri.port, connection_options)
61
+ end
62
+
63
+ def request
64
+ request = Net::HTTP::Post.new service_uri.request_uri, headers
65
+ request.body = encoded_request_body
66
+
67
+ debug_msg = "Commgate API REQUEST: #{request} to #{service_uri} " \
68
+ "with headers: #{headers}\n and body:\n#{request.body}"
69
+ api_log(:debug, debug_msg)
70
+
71
+ request
72
+ end
73
+
74
+ def service_uri
75
+ @service_uri ||= URI.parse(url)
76
+ end
77
+
78
+ def headers
79
+ { "Content-Type" => "application/x-www-form-urlencoded" }
80
+ end
81
+
82
+ def connection_options
83
+ {
84
+ use_ssl: true,
85
+ verify_mode: OpenSSL::SSL::VERIFY_PEER,
86
+ keep_alive_timeout: 30
87
+ # ciphers: secure_and_available_ciphers,
88
+ # cert: OpenSSL::X509::Certificate.new(File.read(configuration.certificate_path)),
89
+ # cert_password: configuration.certificate_password,
90
+ # key: OpenSSL::PKey::RSA.new(File.read(configuration.private_key_path), configuration.private_key_password),
91
+ # cert_store: post_signum_ca_store
92
+ }
93
+ end
94
+
95
+ def find_redirect_to
96
+ case response&.code&.to_i
97
+ when 302
98
+ response_location
99
+ when 200
100
+ decoded_response_body.is_a?(Hash) ? decoded_response_body["redirect"] : nil
101
+ end
102
+ end
103
+
104
+ def response_redirect?
105
+ !redirect_to.nil?
106
+ end
107
+
108
+ def record_api_error
109
+ return unless api_error?
110
+
111
+ errors[:api] = ["[Error ##{result[:response_body]["error"]}] #{result[:response_body]["message"]}"]
112
+ @result[:errors] = { api: { code: result[:response_body]["error"].to_i,
113
+ message: result[:response_body]["message"] } }
114
+ end
115
+
116
+ def api_error?
117
+ return false unless decoded_response_body.is_a?(Hash)
118
+
119
+ decoded_response_body["error"].to_i.positive?
120
+ end
121
+
122
+ def handle_connection_error(error)
123
+ errors[:connection] = ["#{error.class} > #{service_uri} - #{error}"]
124
+
125
+ @result = {
126
+ http_code: 500,
127
+ errors: { connection: [{ code: 500, message: "#{error.class} > #{service_uri} - #{error}" }] }
128
+ }
129
+ end
130
+
131
+ def encoded_request_body
132
+ URI.encode_www_form(payload)
133
+ end
134
+
135
+ def decoded_response_body
136
+ @decoded_response_body ||= case response_content_type
137
+ when :url_encoded
138
+ URI.decode_www_form(response.body).to_h
139
+ when :json
140
+ JSON.parse(response.body)
141
+ end
142
+ end
143
+
144
+ def api_log(level, message)
145
+ if defined?(Rails) && Rails.respond_to?(:logger)
146
+ Rails.logger.send(forced_log_level(level), message)
147
+ else
148
+ puts("#{Time.now} [#{forced_log_level(level)}] #{message}")
149
+ end
150
+ end
151
+
152
+ def forced_log_level(original_level)
153
+ levels = { debug: 0, info: 1, error: 2 }
154
+ minimal_level = :error
155
+ levels[original_level] > levels[minimal_level] ? original_level : minimal_level
156
+ end
157
+
158
+ def response_location
159
+ path_or_url = response["location"]
160
+ return nil if path_or_url == ""
161
+
162
+ response_uri = URI.parse(path_or_url)
163
+ response_uri = URI.join(url, response_uri) if response_uri.relative?
164
+ response_uri.to_s
165
+ end
166
+
167
+ def response_content_type
168
+ rct = response["content-type"]
169
+ return nil if rct.nil?
170
+
171
+ if rct.include?("json")
172
+ :json
173
+ elsif rct.include?("form-urlencoded")
174
+ :url_encoded
175
+ else
176
+ raise "Uncaptured content type: '#{rct}'"
177
+ end
178
+ end
179
+ end
180
+ end
@@ -0,0 +1,214 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "base64"
4
+
5
+ module Comgate
6
+ class Gateway
7
+ BASE_URL = "https://payments.comgate.cz/v1.0"
8
+ DATA_CONVERSION_HASH = {
9
+ curr: %i[payment currency],
10
+ email: %i[payer email],
11
+ label: %i[payment label],
12
+ method: %i[payment method],
13
+ price: %i[payment amount_in_cents],
14
+ amount: %i[payment amount_in_cents],
15
+ refId: %i[payment reference_id],
16
+ account: %i[merchant target_shop_account],
17
+ applePayPayload: %i[payment apple_pay_payload],
18
+ country: %i[options country_code],
19
+ dynamicExpiration: %i[payment dynamic_expiration],
20
+ embedded: %i[options embedded_iframe],
21
+ expirationTime: %i[payment expiration_time],
22
+ lang: %i[options language_code],
23
+ name: %i[payment product_name],
24
+ phone: %i[payer phone],
25
+ preauth: %i[payment preauthorization],
26
+ verification: %i[payment verification_payment],
27
+
28
+ # responses
29
+ transId: %i[transaction_id],
30
+ transferId: %i[transfer_id],
31
+ code: %i[code],
32
+ message: %i[message],
33
+ payerId: %i[payer id],
34
+ payerName: %i[payer account_name],
35
+ payer_name: %i[payer account_name],
36
+ payerAcc: %i[payer account_number],
37
+ payer_acc: %i[payer account_number],
38
+ fee: %i[payment fee],
39
+ methods: %i[methods],
40
+ vs: %i[payment variable_symbol],
41
+ variableSymbol: %i[variable_symbol],
42
+ transferDate: %i[transfer_date],
43
+ accountCounterparty: %i[account_counterparty],
44
+ accountOutgoing: %i[account_outgoing],
45
+ status: %i[state],
46
+ test: %i[test],
47
+ merchant: %i[merchant gateway_id],
48
+ secret: %i[secret],
49
+ redirect: %i[redirect_to]
50
+ }.freeze
51
+
52
+ attr_reader :options
53
+
54
+ def initialize(options)
55
+ @options = options
56
+ return unless options[:merchant_gateway_id].nil? || options[:secret].nil? || options[:test_calls].nil?
57
+
58
+ raise ArgumentError "options have to include :merchant_gateway_id, :secret and :test_calls"
59
+ end
60
+
61
+ def test_calls_used?
62
+ options[:test_calls] == true
63
+ end
64
+
65
+ def start_transaction(payment_data)
66
+ make_call(url: "#{BASE_URL}/create",
67
+ payload: single_payment_payload(payment_data),
68
+ test_call: test_call?(payment_data[:test]))
69
+ end
70
+
71
+ def start_recurring_transaction(payment_data)
72
+ make_call(url: "#{BASE_URL}/create",
73
+ payload: single_payment_payload(payment_data).merge(initRecurring: true),
74
+ test_call: test_call?(payment_data[:test]))
75
+ end
76
+
77
+ def repeat_recurring_transaction(payment_data)
78
+ transaction_id = payment_data.delete(:transaction_id)
79
+ make_call(url: "#{BASE_URL}/recurring",
80
+ payload: single_payment_payload(payment_data).merge(initRecurringId: transaction_id),
81
+ test_call: test_call?(payment_data[:test]))
82
+ end
83
+
84
+ def start_verification_transaction(payment_data)
85
+ make_call(url: "#{BASE_URL}/create",
86
+ payload: single_payment_payload(payment_data).merge(verification: true),
87
+ test_call: test_call?(payment_data[:test]))
88
+ end
89
+
90
+ def start_preauthorized_transaction(payment_data)
91
+ make_call(url: "#{BASE_URL}/create",
92
+ payload: single_payment_payload(payment_data).merge(preauth: true),
93
+ test_call: test_call?(payment_data[:test]))
94
+ end
95
+
96
+ def confirm_preauthorized_transaction(payment_data)
97
+ params = convert_data_to_comgate_params(%i[transId amount], payment_data, required: true)
98
+
99
+ make_call(url: "#{BASE_URL}/capturePreauth",
100
+ payload: gateway_params.merge(params),
101
+ test_call: false)
102
+ end
103
+
104
+ def cancel_preauthorized_transaction(transaction_id:)
105
+ make_call(url: "#{BASE_URL}/cancelPreauth",
106
+ payload: gateway_params.merge(transId: transaction_id),
107
+ test_call: false)
108
+ end
109
+
110
+ def refund_transaction(payment_data)
111
+ refund_params = convert_data_to_comgate_params(%i[transId amount], payment_data, required: true)
112
+ refund_params.merge!(convert_data_to_comgate_params(%i[curr refId], payment_data, required: false))
113
+
114
+ make_call(url: "#{BASE_URL}/refund",
115
+ payload: gateway_params.merge(refund_params),
116
+ test_call: test_call?(payment_data[:test]))
117
+ end
118
+
119
+ def cancel_transaction(transaction_id:)
120
+ make_call(url: "#{BASE_URL}/cancel",
121
+ payload: gateway_params.merge(transId: transaction_id),
122
+ test_call: false)
123
+ end
124
+
125
+ def check_state(transaction_id:)
126
+ make_call(url: "#{BASE_URL}/status",
127
+ payload: gateway_params.merge(transId: transaction_id),
128
+ test_call: false)
129
+ end
130
+
131
+ def process_payment_callback(comgate_params)
132
+ Comgate::Response.new({ response_body: comgate_params }, DATA_CONVERSION_HASH)
133
+ end
134
+
135
+ def allowed_payment_methods(payment_data)
136
+ ph = gateway_params.merge(convert_data_to_comgate_params(%i[curr lang country], payment_data, required: false))
137
+
138
+ response = make_call(url: "#{BASE_URL}/methods",
139
+ payload: ph,
140
+ test_call: false,
141
+ conversion_hash: { name: [:name] })
142
+ response.array = response.hash[:methods]
143
+ response.hash = nil
144
+ response
145
+ end
146
+
147
+ def transfers_from(date_or_time)
148
+ date_str = date_or_time.strftime("%Y-%m-%d")
149
+
150
+ make_call(url: "#{BASE_URL}/transferList",
151
+ payload: gateway_params.merge({ date: date_str }),
152
+ test_call: false)
153
+ end
154
+
155
+ private
156
+
157
+ attr_reader :payment_data
158
+
159
+ def make_call(url:, payload:, test_call:, conversion_hash: DATA_CONVERSION_HASH)
160
+ raise "There are errors in pre-api-call phase: #{payload[:errors]}" unless payload[:errors].nil?
161
+
162
+ srv = Comgate::ApiCaller.call(url: url, payload: payload, test_call: test_call)
163
+ if srv.success?
164
+ Comgate::Response.new(srv.result, conversion_hash)
165
+ else
166
+ handle_failure_from(srv.errors)
167
+ end
168
+ end
169
+
170
+ def test_call?(test_from_data = nil)
171
+ test_from_data.nil? ? test_calls_used? : (test_from_data == true)
172
+ end
173
+
174
+ def handle_failure_from(errors)
175
+ raise errors.to_s
176
+ end
177
+
178
+ def single_payment_payload(payment_data)
179
+ required_keys = %i[curr email label method price refId]
180
+ optional_keys = %i[account applePayPayload country dynamicExpiration embedded expirationTime lang name phone
181
+ preauth verification]
182
+
183
+ ph = gateway_params.merge({ prepareOnly: true })
184
+ ph.merge!(convert_data_to_comgate_params(required_keys, payment_data, required: true))
185
+ ph.merge!(convert_data_to_comgate_params(optional_keys, payment_data, required: false))
186
+ ph[:applePayPayload] = Base64.encode64(ph[:applePayPayload]) unless ph[:applePayPayload].nil?
187
+ ph
188
+ end
189
+
190
+ def gateway_params
191
+ { merchant: options[:merchant_gateway_id],
192
+ secret: options[:secret] }
193
+ end
194
+
195
+ def convert_data_to_comgate_params(comgate_keys, data, required:)
196
+ h = {}
197
+ comgate_keys.each do |comg_key|
198
+ dig_keys = DATA_CONVERSION_HASH[comg_key.to_sym]
199
+ raise "comgate key '#{comg_key}' is not setup in conversion_hash" if dig_keys.nil?
200
+
201
+ value = data.dig(*dig_keys)
202
+ if value.nil?
203
+ if required
204
+ h[:errors] = [] if h[:errors].nil?
205
+ h[:errors] << "Missing value for `params#{dig_keys.collect { |k| "[:#{k}]" }.join}` in `#{data}`"
206
+ end
207
+ else
208
+ h[comg_key] = value
209
+ end
210
+ end
211
+ h
212
+ end
213
+ end
214
+ end
@@ -0,0 +1,142 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Comgate
4
+ class Response
5
+ RESPONSE_CODES = {
6
+ 0 => "OK",
7
+ 1100 => "unknown error",
8
+ 1102 => "the specified language is not supported",
9
+ 1103 => "method incorrectly specified",
10
+ 1104 => "unable to load payment",
11
+ 1107 => "payment price is not supported",
12
+ 1200 => "database error",
13
+ 1301 => "unknown e-shop",
14
+ 1303 => "the link or language is missing",
15
+ 1304 => "invalid category",
16
+ 1305 => "product description is missing",
17
+ 1306 => "select the correct method",
18
+ 1308 => "the selected payment method is not allowed",
19
+ 1309 => "incorrect amount",
20
+ 1310 => "unknown currency",
21
+ 1311 => "invalid e-shop bank account identifier",
22
+ 1316 => "e-shop does not allow recurring payments",
23
+ 1317 => "invalid method - does not support recurring payments",
24
+ 1318 => "initial payment not found",
25
+ 1319 => "can not create a payment, a problem on the part of the bank",
26
+ 1399 => "unexpected result from database",
27
+ 1400 => "wrong query",
28
+ 1401 => "the refunded payment is in the CANCELED state",
29
+ # or 1401 =>"Transaction FVN0-NS40-NA5B has not been authorized, current status: READY"
30
+ 1402 => "refund amount higher than allowed",
31
+ 1500 => "unexpected error"
32
+ }.freeze
33
+
34
+ attr_accessor :http_code, :redirect_to, :hash, :array, :errors
35
+ attr_reader :params_conversion_hash
36
+
37
+ def initialize(caller_result, params_conversion_hash = {})
38
+ @params_conversion_hash = params_conversion_hash
39
+
40
+ @http_code = caller_result[:http_code].to_i
41
+ @errors = fill_error_messages(caller_result[:errors])
42
+ @redirect_to = caller_result[:redirect_to]
43
+
44
+ converted_body = convert_comgate_params_to_data(caller_result[:response_body])
45
+
46
+ case converted_body
47
+ when Hash
48
+ @hash = converted_body
49
+ @array = nil
50
+ when Array
51
+ @array = converted_body
52
+ @hash = nil
53
+ end
54
+ end
55
+
56
+ def redirect?
57
+ !redirect_to.nil?
58
+ end
59
+
60
+ def error?
61
+ !errors.nil?
62
+ end
63
+
64
+ private
65
+
66
+ def convert_comgate_params_to_data(comgate_params)
67
+ h = transform_comgate_params(comgate_params)
68
+ return h unless h.is_a?(Hash)
69
+
70
+ cleanup_hash(h)
71
+ end
72
+
73
+ def cleanup_hash(rsp_hash) # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
74
+ rsp_hash.delete(:secret)
75
+
76
+ rsp_hash[:state] = rsp_hash[:state].to_s.downcase.to_sym unless rsp_hash[:state].nil?
77
+ unless rsp_hash.dig(:payment, :variable_symbol).nil?
78
+ rsp_hash[:payment][:variable_symbol] = rsp_hash.dig(:payment, :variable_symbol).to_i
79
+ end
80
+ unless rsp_hash.dig(:payment, :amount_in_cents).nil?
81
+ rsp_hash[:payment][:amount_in_cents] = rsp_hash.dig(:payment, :amount_in_cents).to_i
82
+ end
83
+ rsp_hash[:variable_symbol] = rsp_hash[:variable_symbol].to_i unless rsp_hash[:variable_symbol].nil?
84
+ if (fee = rsp_hash.dig(:payment, :fee))
85
+ rsp_hash[:payment][:fee] = fee == "unknown" ? nil : fee.to_f
86
+ end
87
+
88
+ rsp_hash[:code] = rsp_hash[:code].to_i if rsp_hash[:code]
89
+ rsp_hash[:error] = rsp_hash[:error].to_i if rsp_hash[:error]
90
+
91
+ rsp_hash[:message] = RESPONSE_CODES[rsp_hash[:code]] if rsp_hash[:message].to_s == "" && rsp_hash[:code]
92
+ rsp_hash
93
+ end
94
+
95
+ def transform_comgate_params(comgate_params)
96
+ case comgate_params
97
+ when Hash
98
+ transform_comgate_hash(comgate_params)
99
+ when Array
100
+ comgate_params.collect { |item| transform_comgate_params(item) }
101
+ when "true"
102
+ true
103
+ when "false"
104
+ false
105
+ else
106
+ comgate_params
107
+ end
108
+ end
109
+
110
+ def transform_comgate_hash(comgate_params)
111
+ h = {}
112
+ comgate_params.each_pair do |k, v|
113
+ build_keys = params_conversion_hash[k.to_sym]&.dup
114
+ transformed_value = transform_comgate_params(v)
115
+
116
+ if build_keys.nil? # not covered in params_conversion_hash
117
+ h[k.to_sym] = transformed_value
118
+ else
119
+ last_key = build_keys.delete(build_keys.last)
120
+ hash_at_level = h
121
+ build_keys.each do |bk|
122
+ hash_at_level[bk] = {} if hash_at_level[bk].nil?
123
+ hash_at_level = hash_at_level[bk]
124
+ end
125
+ hash_at_level[last_key] = transformed_value
126
+ end
127
+ end
128
+ h
129
+ end
130
+
131
+ def fill_error_messages(caller_errors)
132
+ return nil if caller_errors.nil?
133
+ return caller_errors if caller_errors[:api].nil?
134
+
135
+ caller_errors[:api] = caller_errors[:api].collect do |err_h|
136
+ err_h[:message] = RESPONSE_CODES[err_h[:code]] if err_h[:message].to_s == ""
137
+ err_h
138
+ end
139
+ caller_errors
140
+ end
141
+ end
142
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Comgate
4
+ VERSION = "0.5.0"
5
+ end
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "hash_enhancements"
4
+ require_relative "base_service"
5
+ require_relative "comgate/version"
6
+ require_relative "comgate/response"
7
+ require_relative "comgate/api_caller"
8
+ require_relative "comgate/gateway"
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ unless {}.respond_to?(:deep_merge)
4
+ class Hash
5
+ def deep_merge(other_hash)
6
+ result = dup
7
+ other_hash.each do |key, value|
8
+ existing_value = result[key]
9
+ result[key] = if existing_value.is_a?(Hash)
10
+ existing_value.deep_merge(value)
11
+ else
12
+ value
13
+ end
14
+ end
15
+ result
16
+ end
17
+ end
18
+ end
19
+
20
+ unless {}.respond_to?(:deep_symbolize_keys)
21
+ class Hash
22
+ def deep_symbolize_keys
23
+ result = {}
24
+ each_pair do |k, v|
25
+ result[k.to_sym] = case v
26
+ when Hash
27
+ v.deep_symbolize_keys
28
+ when Array
29
+ v.collect { |item| item.is_a?(Hash) ? item.deep_symbolize_keys : item }
30
+ else
31
+ v
32
+ end
33
+ end
34
+ result
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,4 @@
1
+ module ComgateRuby
2
+ VERSION: String
3
+ # See the writing guide of rbs: https://github.com/ruby/rbs#guides
4
+ end
metadata ADDED
@@ -0,0 +1,66 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: comgate_ruby
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.5.0
5
+ platform: ruby
6
+ authors:
7
+ - Petr Mlčoch
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2023-04-20 00:00:00.000000000 Z
12
+ dependencies: []
13
+ description: Write a longer description or delete this line.
14
+ email:
15
+ - foton@centrum.cz
16
+ executables: []
17
+ extensions: []
18
+ extra_rdoc_files: []
19
+ files:
20
+ - ".rubocop.yml"
21
+ - ".tool-versions"
22
+ - CHANGELOG.md
23
+ - Gemfile
24
+ - Gemfile.lock
25
+ - LICENSE
26
+ - LICENSE.txt
27
+ - README.md
28
+ - Rakefile
29
+ - comgate_ruby.gemspec
30
+ - lib/base_service.rb
31
+ - lib/comgate/api_caller.rb
32
+ - lib/comgate/gateway.rb
33
+ - lib/comgate/response.rb
34
+ - lib/comgate/version.rb
35
+ - lib/comgate_ruby.rb
36
+ - lib/hash_enhancements.rb
37
+ - sig/comgate_ruby.rbs
38
+ homepage: https://github.com/sinfin/comgate_ruby
39
+ licenses:
40
+ - MIT
41
+ metadata:
42
+ allowed_push_host: https://rubygems.org
43
+ homepage_uri: https://github.com/sinfin/comgate_ruby
44
+ source_code_uri: https://github.com/sinfin/comgate_ruby
45
+ changelog_uri: https://github.com/sinfin/comgate_ruby/blob/main/CHANGELOG.md
46
+ rubygems_mfa_required: 'true'
47
+ post_install_message:
48
+ rdoc_options: []
49
+ require_paths:
50
+ - lib
51
+ required_ruby_version: !ruby/object:Gem::Requirement
52
+ requirements:
53
+ - - ">="
54
+ - !ruby/object:Gem::Version
55
+ version: '3.0'
56
+ required_rubygems_version: !ruby/object:Gem::Requirement
57
+ requirements:
58
+ - - ">="
59
+ - !ruby/object:Gem::Version
60
+ version: '0'
61
+ requirements: []
62
+ rubygems_version: 3.3.7
63
+ signing_key:
64
+ specification_version: 4
65
+ summary: Client for Comgate payment gateway
66
+ test_files: []