Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fortis: Initial implementation #5349

Merged
merged 1 commit into from
Dec 4, 2024
Merged

Conversation

Heavyblade
Copy link
Collaborator

@Heavyblade Heavyblade commented Nov 27, 2024

Summary:

Fortis Gateway initial implementation with support for:

  • Authorize
  • Purchase
  • Void
  • Capture
  • Refund
  • Credit
  • Store
  • Unstore

Net::HTTP Refinement:

Context:

There is an issue with the Fortis API because their back-end returns an "invalid credentials" error if the HTTP Request headers are not in lower-case format, but Ruby by default follows the HTTP 1.1 RFC 2616 on how to handle headers; Ruby camellize all headers from something like 'user-api-key' to 'User-Api-Key', that is performed by Net::HTTPHeader#capitalize, so to deal with that we had to find a way to force net/http to send lower-case headers.

Enter Refinements:

So with Ruby Refinements, we can monkey patch net/http but only in the context of ActiveMerchant requests without side-effects on other areas of the system, two changes were introduced:

  • The CaseSensitiveHeaders class, an alias for a hash, allows us to indicate in the Fortis adapter that we will need case-sensitive headers.
  • The net/http refinement => that creates one HTTP::Request class per HTTP method, injecting a module that will overwrite the #capitalize method, it also overwrites the post, put, patch and delete methods to use the custom requests only when the headers sent to the Connection class are CaseSensitiveHeaders.

Remote Test:

Finished in 114.375184 seconds.
22 tests, 53 assertions, 0 failures, 0 errors, 0 pendings, 0 omissions, 0 notifications
100% passed
0.19 tests/s, 0.46 assertions/s

Unit Tests:

Finished in 68.455283 seconds.
6138 tests, 80277 assertions, 0 failures, 0 errors, 0 pendings, 0 omissions, 0 notifications
100% passed

RuboCop:

807 files inspected, no offenses detected

@Heavyblade Heavyblade force-pushed the fortis_base_gateway_adapter branch 4 times, most recently from 77daa32 to c643967 Compare November 27, 2024 15:13
@Heavyblade Heavyblade force-pushed the fortis_base_gateway_adapter branch from c643967 to 20b9b8b Compare November 27, 2024 16:19
Copy link
Collaborator

@javierpedrozaing javierpedrozaing left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Awesome job 🚀

@@ -31,6 +31,7 @@ RuboCop::RakeTask.new

namespace :test do
Rake::TestTask.new(:units) do |t|
ENV['RUNNING_UNIT_TESTS'] = 'true'
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is this ENV needed ❓

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh, this one is a workaround for preventing Net::HTTP refinement from having some effect on unit tests mocs on test/unit/connection_test.rb

331 => 'Credit/Debit/Refund ach Charged Back'
}

REASON_MAPPING = {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

was the mapping for status and reason needed due the Fortis response does not return a readable message, or was it for another specific reason ❓

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Most of the times Fortis returns an status_mapping and a reason_mapping but not the actual message

end

def commit(action, post, method = :post, options = {})
add_location_id(post) if post.present?
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what is the advantage of sending the location_id in the post ❓

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is an optional normally that is usefull to identify the request source (Fortis customer normally are shops with card machines or terminals) and for the store transaction is a must.

@@ -76,7 +76,7 @@ def request(method, body, headers = {})
http.get(endpoint.request_uri, headers)
when :post
debug body
http.post(endpoint.request_uri, body, RUBY_184_POST_HEADERS.merge(headers))
http.post(endpoint.request_uri, body, headers.reverse_merge!(RUBY_184_POST_HEADERS))
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this change related to the HTTP version ❓

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Exactly, see:

the headers object is a CaseSensitiveHeaders object that acts like a Hash, the merge method called on RUBY_182_POST_HEADERS hash will return a new Hash not a CaseSensitveHeaders that I need in the refinement.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why using reverse merge? Is order of these headers also important?

lib/active_merchant/posts_data.rb Show resolved Hide resolved
@Heavyblade Heavyblade force-pushed the fortis_base_gateway_adapter branch from 20b9b8b to 3142cd0 Compare November 29, 2024 21:58
Copy link
Contributor

@naashton naashton left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Most of my review is around the changes to the http modules to accommodate this gateways quirkiness. It seems like the implementation should leave the existing gateways unaffected, but did you run any of the remote tests against any other gateways to see if they yielded any different results?

}

def initialize(options = {})
requires!(options, :user_id, :user_api_key, :developer_id, :location_id)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think based on our discussion, we probably can remove location_id and make it an optional class field.

@@ -76,7 +76,7 @@ def request(method, body, headers = {})
http.get(endpoint.request_uri, headers)
when :post
debug body
http.post(endpoint.request_uri, body, RUBY_184_POST_HEADERS.merge(headers))
http.post(endpoint.request_uri, body, headers.reverse_merge!(RUBY_184_POST_HEADERS))
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why using reverse merge? Is order of these headers also important?

private :capitalize
end

class CaseSensitivePost < Net::HTTP::Post; prepend InnocuousCapitalize; end
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is the prepend intended to give precedence to the existing behavior, which is capitalizing header fields?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

reponses:

  1. no because of order, but because of classes see => hash_object.merge!(case_sensitve_header_hash), en-up being a plain ruby Hash, but case_sensitve_header_hash.merge!(hash_object) updates CaseSensitiveHeaders instant but is still that class, so I can differentiate in the Refinement.

  2. yes exactly that

@Heavyblade Heavyblade force-pushed the fortis_base_gateway_adapter branch from 3142cd0 to 2f67c66 Compare December 3, 2024 15:24
Summary:
------------------------------
Fortis Gateway intial implementation with support for:

- Authorize
- Purchase
- Void
- Capture
- Refund
- Credit
- Store
- Unstore

Remote Test:
------------------------------
Finished in 114.375184 seconds.
22 tests, 53 assertions, 0 failures, 0 errors, 0 pendings,
0 omissions, 0 notifications
100% passed
0.19 tests/s, 0.46 assertions/s

Unit Tests:
------------------------------
Finished in 68.455283 seconds.
6138 tests, 80277 assertions, 0 failures, 0 errors, 0 pendings,
0 omissions, 0 notifications
100% passed

RuboCop:
------------------------------
807 files inspected, no offenses detected
@Heavyblade Heavyblade force-pushed the fortis_base_gateway_adapter branch from 38324b9 to 50f3987 Compare December 4, 2024 14:26
@Heavyblade Heavyblade merged commit 599f2cc into master Dec 4, 2024
5 checks passed
@Heavyblade Heavyblade deleted the fortis_base_gateway_adapter branch December 4, 2024 14:52
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants