comgate_ruby 0.5.0

Sign up to get free protection for your applications and to get access to all the features.
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: []