worldpay_cnp 0.1.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.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 89bf5fce70ba0dbff1493a07c9c63a9619a17a633b64bf163681b0fd002ec942
4
+ data.tar.gz: 74fa0c70f00a2df9a223e3f4644caab533cdb2efb95ea7707a1e1cfda457bd20
5
+ SHA512:
6
+ metadata.gz: ed19a23c8647fed3ce365d80f5494ca66342b88bf4c6ab095ed2b44fe8f7290ba9f1c3b215614dd7bc8acbcd420795f5db5bebaf21a2dd560102c271e231fab9
7
+ data.tar.gz: c8082c88313c40675b1ca1d6609b00a18e0153defd5cdb8c948db9b955f9a2b458d90f92110d06203a9b67b88f51b200cb7f1e3bdbbf3bd911ea6035d09e8422
@@ -0,0 +1,3 @@
1
+ USERNAME="YOUR_API_USERNAME"
2
+ PASSWORD="YOUR_API_PASSWORD"
3
+ MERCHANT_ID="YOUR_MERCHANT_ID"
@@ -0,0 +1,49 @@
1
+ name: Test Suite
2
+
3
+ on:
4
+ push:
5
+ branches: [ master ]
6
+ pull_request:
7
+ branches: [ master ]
8
+
9
+ jobs:
10
+ build:
11
+ runs-on: ubuntu-latest
12
+ strategy:
13
+ matrix:
14
+ ruby: [ '2.5', '2.6', '2.7' ]
15
+ name: Ruby ${{ matrix.ruby }}
16
+ steps:
17
+ - uses: actions/checkout@v2
18
+
19
+ - uses: actions/setup-ruby@v1
20
+ with:
21
+ ruby-version: ${{ matrix.ruby }}
22
+
23
+ - uses: actions/cache@v2
24
+ with:
25
+ path: vendor/bundle
26
+ key: ${{ runner.os }}-gems-${{ hashFiles('**/Gemfile.lock') }}
27
+ restore-keys: |
28
+ ${{ runner.os }}-gems-
29
+
30
+ - name: Install rubygems
31
+ run: |
32
+ gem update --system --no-document
33
+
34
+ - name: Install bundler
35
+ run: |
36
+ gem install bundler --no-document
37
+
38
+ - name: Install dependencies
39
+ run: |
40
+ bundle config path vendor/bundle
41
+ bundle install --jobs 4 --retry 3
42
+
43
+ - name: Run Tests
44
+ env:
45
+ USERNAME: test
46
+ PASSWORD: test
47
+ MERCHANT_ID: test
48
+ run: |
49
+ bundle exec rspec
@@ -0,0 +1,14 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /_yardoc/
4
+ /coverage/
5
+ /doc/
6
+ /pkg/
7
+ /spec/reports/
8
+ /tmp/
9
+
10
+ # rspec failure tracking
11
+ .rspec_status
12
+
13
+ .env
14
+ Notes.rb
data/.rspec ADDED
@@ -0,0 +1,3 @@
1
+ --format documentation
2
+ --color
3
+ --require spec_helper
@@ -0,0 +1,9 @@
1
+ # Changelog
2
+
3
+ ## Unreleased
4
+
5
+ ...
6
+
7
+ ## 0.1.0 (2020-10-30)
8
+
9
+ * Initial release of worldpay_cnp gem
@@ -0,0 +1,74 @@
1
+ # Contributor Covenant Code of Conduct
2
+
3
+ ## Our Pledge
4
+
5
+ In the interest of fostering an open and welcoming environment, we as
6
+ contributors and maintainers pledge to making participation in our project and
7
+ our community a harassment-free experience for everyone, regardless of age, body
8
+ size, disability, ethnicity, gender identity and expression, level of experience,
9
+ nationality, personal appearance, race, religion, or sexual identity and
10
+ orientation.
11
+
12
+ ## Our Standards
13
+
14
+ Examples of behavior that contributes to creating a positive environment
15
+ include:
16
+
17
+ * Using welcoming and inclusive language
18
+ * Being respectful of differing viewpoints and experiences
19
+ * Gracefully accepting constructive criticism
20
+ * Focusing on what is best for the community
21
+ * Showing empathy towards other community members
22
+
23
+ Examples of unacceptable behavior by participants include:
24
+
25
+ * The use of sexualized language or imagery and unwelcome sexual attention or
26
+ advances
27
+ * Trolling, insulting/derogatory comments, and personal or political attacks
28
+ * Public or private harassment
29
+ * Publishing others' private information, such as a physical or electronic
30
+ address, without explicit permission
31
+ * Other conduct which could reasonably be considered inappropriate in a
32
+ professional setting
33
+
34
+ ## Our Responsibilities
35
+
36
+ Project maintainers are responsible for clarifying the standards of acceptable
37
+ behavior and are expected to take appropriate and fair corrective action in
38
+ response to any instances of unacceptable behavior.
39
+
40
+ Project maintainers have the right and responsibility to remove, edit, or
41
+ reject comments, commits, code, wiki edits, issues, and other contributions
42
+ that are not aligned to this Code of Conduct, or to ban temporarily or
43
+ permanently any contributor for other behaviors that they deem inappropriate,
44
+ threatening, offensive, or harmful.
45
+
46
+ ## Scope
47
+
48
+ This Code of Conduct applies both within project spaces and in public spaces
49
+ when an individual is representing the project or its community. Examples of
50
+ representing a project or community include using an official project e-mail
51
+ address, posting via an official social media account, or acting as an appointed
52
+ representative at an online or offline event. Representation of a project may be
53
+ further defined and clarified by project maintainers.
54
+
55
+ ## Enforcement
56
+
57
+ Instances of abusive, harassing, or otherwise unacceptable behavior may be
58
+ reported by contacting the project team at jjfutbol@gmail.com. All
59
+ complaints will be reviewed and investigated and will result in a response that
60
+ is deemed necessary and appropriate to the circumstances. The project team is
61
+ obligated to maintain confidentiality with regard to the reporter of an incident.
62
+ Further details of specific enforcement policies may be posted separately.
63
+
64
+ Project maintainers who do not follow or enforce the Code of Conduct in good
65
+ faith may face temporary or permanent repercussions as determined by other
66
+ members of the project's leadership.
67
+
68
+ ## Attribution
69
+
70
+ This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
71
+ available at [https://contributor-covenant.org/version/1/4][version]
72
+
73
+ [homepage]: https://contributor-covenant.org
74
+ [version]: https://contributor-covenant.org/version/1/4/
data/Gemfile ADDED
@@ -0,0 +1,10 @@
1
+ source "https://rubygems.org"
2
+
3
+ # Specify your gem's dependencies in worldpay_cnp.gemspec
4
+ gemspec
5
+
6
+ gem "rake", "~> 12.0"
7
+ gem "rspec", "~> 3.0"
8
+ gem "dotenv"
9
+ gem "vcr"
10
+ gem "webmock"
@@ -0,0 +1,72 @@
1
+ PATH
2
+ remote: .
3
+ specs:
4
+ worldpay_cnp (0.1.0)
5
+ http (>= 4, < 5)
6
+ nokogiri (~> 1.0)
7
+
8
+ GEM
9
+ remote: https://rubygems.org/
10
+ specs:
11
+ addressable (2.7.0)
12
+ public_suffix (>= 2.0.2, < 5.0)
13
+ crack (0.4.4)
14
+ diff-lcs (1.4.4)
15
+ domain_name (0.5.20190701)
16
+ unf (>= 0.0.5, < 1.0.0)
17
+ dotenv (2.7.6)
18
+ ffi (1.13.1)
19
+ ffi-compiler (1.0.1)
20
+ ffi (>= 1.0.0)
21
+ rake
22
+ hashdiff (1.0.1)
23
+ http (4.4.1)
24
+ addressable (~> 2.3)
25
+ http-cookie (~> 1.0)
26
+ http-form_data (~> 2.2)
27
+ http-parser (~> 1.2.0)
28
+ http-cookie (1.0.3)
29
+ domain_name (~> 0.5)
30
+ http-form_data (2.3.0)
31
+ http-parser (1.2.1)
32
+ ffi-compiler (>= 1.0, < 2.0)
33
+ mini_portile2 (2.4.0)
34
+ nokogiri (1.10.10)
35
+ mini_portile2 (~> 2.4.0)
36
+ public_suffix (4.0.6)
37
+ rake (12.3.3)
38
+ rspec (3.9.0)
39
+ rspec-core (~> 3.9.0)
40
+ rspec-expectations (~> 3.9.0)
41
+ rspec-mocks (~> 3.9.0)
42
+ rspec-core (3.9.3)
43
+ rspec-support (~> 3.9.3)
44
+ rspec-expectations (3.9.2)
45
+ diff-lcs (>= 1.2.0, < 2.0)
46
+ rspec-support (~> 3.9.0)
47
+ rspec-mocks (3.9.1)
48
+ diff-lcs (>= 1.2.0, < 2.0)
49
+ rspec-support (~> 3.9.0)
50
+ rspec-support (3.9.3)
51
+ unf (0.1.4)
52
+ unf_ext
53
+ unf_ext (0.0.7.7)
54
+ vcr (6.0.0)
55
+ webmock (3.9.3)
56
+ addressable (>= 2.3.6)
57
+ crack (>= 0.3.2)
58
+ hashdiff (>= 0.4.0, < 2.0.0)
59
+
60
+ PLATFORMS
61
+ ruby
62
+
63
+ DEPENDENCIES
64
+ dotenv
65
+ rake (~> 12.0)
66
+ rspec (~> 3.0)
67
+ vcr
68
+ webmock
69
+ worldpay_cnp!
70
+
71
+ BUNDLED WITH
72
+ 2.1.4
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2020 Javier Julio
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.
@@ -0,0 +1,119 @@
1
+ # WorldpayCnp Ruby gem
2
+
3
+ ![Test Suite](https://github.com/jackpocket/worldpay-cnp/workflows/Test%20Suite/badge.svg)
4
+
5
+ A Ruby library for the Worldpay cnpAPI with a simple interface for creating transactions as a Ruby hash to XML and back. So no real request objects. Since the cnpAPI uses camelCase, this library will handle converting to and from snake_case for you. While Worldpay has an official [CnpOnline Ruby SDK](https://github.com/Vantiv/cnp-sdk-for-ruby), they no longer support it.
6
+
7
+ ## Installation
8
+
9
+ Add this line to your application's Gemfile:
10
+
11
+ ```ruby
12
+ gem 'worldpay_cnp'
13
+ ```
14
+
15
+ And then run `bundle install` or install directly with `gem install worldpay_cnp`.
16
+
17
+ ## Usage
18
+
19
+ ### Client Configuration
20
+
21
+ Create and configure a client with your API authentication.
22
+
23
+ ```ruby
24
+ client = WorldpayCnp::Client.new(
25
+ username: "YOUR_USERNAME",
26
+ password: "YOUR_PASSWORD",
27
+ merchant_id: "YOUR_MERCHANT_ID",
28
+ # These are the other available options with their default values
29
+ version: "12.8",
30
+ environment: :sandbox,
31
+ timeout: nil, # with an integer, it is in seconds
32
+ proxy: nil,
33
+ xml_namspace: "http://www.vantivcnp.com/schema",
34
+ xml_request_root: "cnpOnlineRequest"
35
+ )
36
+ ```
37
+
38
+ #### Using A Proxy
39
+
40
+ A client can be configured with a proxy.
41
+
42
+ ```ruby
43
+ WorldpayCnp::Client.new(
44
+ # ...other options
45
+ proxy: {
46
+ host: "127.0.0.1",
47
+ port: 5000,
48
+ username: "username",
49
+ password: "password",
50
+ }
51
+ )
52
+ ```
53
+
54
+ At a minimum, just the `host` and `port` fields are required to use a proxy.
55
+
56
+ ### Making Requests
57
+
58
+ Make an API request with a payload in the structure and order documented by the cnpAPI but **using snake_case**. The request payload will be **converted to camelCase** internally before submission.
59
+
60
+ Since generating part of the request body is taken care of (e.g. the XML root and authentication elements) we just specify the child element. For example, when creating a Sale transaction, it would look like:
61
+
62
+ ```ruby
63
+ client.create_transaction(
64
+ sale: {
65
+ "@id": "123",
66
+ "@report_group": "Default Report Group",
67
+ order_id: "456"
68
+ amount: "1000",
69
+ order_source: "ecommerce",
70
+ card: {
71
+ type: "VI",
72
+ number: "4457010000000009",
73
+ exp_date: "1025",
74
+ card_validation_num: "349",
75
+ }
76
+ }
77
+ )
78
+ ```
79
+
80
+ Any keys prefixed with "@" will be serialized to an XML attribute, while all others become XML elements. Note that this does not apply to responses.
81
+
82
+ **IMPORTANT**: The cnpAPI enforces the order of XML elements, so the hash keys provided must be declared in the same order as it would if it were XML.
83
+
84
+ For now, the library only supports cnpAPI online requests. No batch requests.
85
+
86
+ ### Handling Responses
87
+
88
+ The generic response object is essentially an underlying hash **in snake_case** form. No typed response objects. So considering the earlier Sale request example, we can use `dig` to retrieve specific values from the response.
89
+
90
+ ```ruby
91
+ response.status_code
92
+ # => 200
93
+ response.dig(:sale_response, :response)
94
+ # => "000"
95
+ response.dig(:sale_response, :message)
96
+ # => "Approved"
97
+ ```
98
+
99
+ Note: The response data starts with **the value of** the root XML element (attributes included) so do not specify the root XML key as part of the lookup process.
100
+
101
+ Since the cnpAPI returns an HTTP 200 response for format errors, an `WorldpayCnp::Error::InvalidFormatError` **will be raised with the included parsed response code and message**. The response code comes from the XML root `response` attribute and can be a value of "1" through "5" (note: as a string value).
102
+
103
+ ## Development
104
+
105
+ After checking out the repo, run `bin/setup` to install dependencies. Then, run `bundle exec rspec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment with methods `authenticated_client` and `sandbox_client` already available.
106
+
107
+ 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 tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
108
+
109
+ ## Contributing
110
+
111
+ Bug reports and pull requests are welcome on GitHub at https://github.com/jackpocket/worldpay_cnp. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [code of conduct](https://github.com/jackpocket/worldpay_cnp/blob/master/CODE_OF_CONDUCT.md).
112
+
113
+ ## License
114
+
115
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
116
+
117
+ ## Code of Conduct
118
+
119
+ Everyone interacting in the WorldpayCnp project's codebases, issue trackers, chat rooms and mailing lists is expected to follow the [code of conduct](https://github.com/jackpocket/worldpay_cnp/blob/master/CODE_OF_CONDUCT.md).
@@ -0,0 +1,6 @@
1
+ require "bundler/gem_tasks"
2
+ require "rspec/core/rake_task"
3
+
4
+ RSpec::Core::RakeTask.new(:spec)
5
+
6
+ task default: :spec
@@ -0,0 +1,29 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "bundler/setup"
4
+ require "dotenv/load"
5
+ require "worldpay_cnp"
6
+
7
+ # You can add fixtures and/or initialization code here to make experimenting
8
+ # with your gem easier. You can also use a different console, if you like.
9
+
10
+ def authenticated_client(**options)
11
+ WorldpayCnp::Client.new(
12
+ username: ENV["USERNAME"],
13
+ password: ENV["PASSWORD"],
14
+ merchant_id: ENV["MERCHANT_ID"],
15
+ environment: :prelive,
16
+ **options
17
+ )
18
+ end
19
+
20
+ def sandbox_client(**options)
21
+ WorldpayCnp::Client.new(environment: :sandbox, **options)
22
+ end
23
+
24
+ # (If you use this, don't forget to add pry to your Gemfile!)
25
+ # require "pry"
26
+ # Pry.start
27
+
28
+ require "irb"
29
+ IRB.start(__FILE__)
@@ -0,0 +1,26 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+
5
+ bundle install
6
+
7
+ read -r -p 'Do you have a Worldpay Prelive account? (y/n) ' has_account
8
+
9
+ if [[ "$has_account" =~ ^(No|no|N|n)$ ]]; then
10
+ echo "Then you'll need to get credentials first or rely on"
11
+ echo "the sandbox environment. No account or auth required."
12
+ exit 0
13
+ fi
14
+
15
+ echo "Enter your Prelive credentials below."
16
+ read -p 'API Username: ' username
17
+ read -p 'API Password: ' password
18
+ read -p 'Merchant Id: ' merchant_id
19
+
20
+ cp .env.sample .env
21
+
22
+ sed -i '' -e "s/YOUR_API_USERNAME/$username/g" .env
23
+ sed -i '' -e "s/YOUR_API_PASSWORD/$password/g" .env
24
+ sed -i '' -e "s/YOUR_MERCHANT_ID/$merchant_id/g" .env
25
+
26
+ echo "Done."
@@ -0,0 +1,12 @@
1
+ require "http"
2
+ require "nokogiri"
3
+ require "worldpay_cnp/version"
4
+ require "worldpay_cnp/refinements/deep_symbolize_keys"
5
+ require "worldpay_cnp/refinements/camel_case"
6
+ require "worldpay_cnp/refinements/snake_case"
7
+ require "worldpay_cnp/error"
8
+ require "worldpay_cnp/response"
9
+ require "worldpay_cnp/xml"
10
+ require "worldpay_cnp/configuration"
11
+ require "worldpay_cnp/api_client"
12
+ require "worldpay_cnp/client"
@@ -0,0 +1,73 @@
1
+ module WorldpayCnp
2
+ class ApiClient
3
+ HEADERS = {
4
+ 'Content-Type' => 'text/xml; charset=UTF-8',
5
+ 'User-Agent' => "WorldpayCnpRubyGem/#{WorldpayCnp::VERSION}",
6
+ 'X-Ruby-Version' => RUBY_VERSION,
7
+ 'X-Ruby-Platform' => RUBY_PLATFORM
8
+ }
9
+
10
+ INVALID_RESPONSE_VALUES = %w(1 2 3 4 5)
11
+
12
+ using Refinements::CamelCase
13
+ using Refinements::SnakeCase
14
+ using Refinements::DeepSymbolizeKeys
15
+
16
+ def initialize(config)
17
+ @config = config
18
+ end
19
+
20
+ def perform_post(url, data)
21
+ response = http_client.post(url, body: XML.serialize(data.to_camel_case))
22
+ process_response(response)
23
+ end
24
+
25
+ protected
26
+
27
+ def http_client
28
+ client = with_proxy? ? HTTP.via(*proxy) : HTTP
29
+ client.headers(HEADERS).timeout(@config.timeout || :null)
30
+ end
31
+
32
+ def with_proxy?
33
+ !proxy.nil? && !proxy.empty?
34
+ end
35
+
36
+ def proxy
37
+ @proxy ||= @config.proxy&.values_at(:host, :port, :username, :password)&.compact
38
+ end
39
+
40
+ def process_response(response)
41
+ data = parse_response_body(response.to_s)
42
+
43
+ if response.status.success?
44
+ handle_invalid_format_error(data, response.to_s)
45
+ Response.new(data, response.status.code, response.to_s)
46
+ else
47
+ raise Error.from_http_response(response.to_s, response.status.code)
48
+ end
49
+ end
50
+
51
+ def parse_response_body(body)
52
+ return {} if body.strip.empty?
53
+ hash_from_xml(body)
54
+ end
55
+
56
+ def hash_from_xml(xml)
57
+ result = XML.parse(xml)
58
+ if root_key = result.keys.first
59
+ result[root_key]&.to_snake_case&.deep_symbolize_keys
60
+ end
61
+ end
62
+
63
+ def handle_invalid_format_error(hash, body)
64
+ if INVALID_RESPONSE_VALUES.include?(hash.dig(:response))
65
+ raise Error::InvalidFormatError.new(
66
+ hash.dig(:message),
67
+ response_code: hash.dig(:response),
68
+ raw_response: body
69
+ )
70
+ end
71
+ end
72
+ end
73
+ end
@@ -0,0 +1,39 @@
1
+ module WorldpayCnp
2
+ class Client
3
+
4
+ attr_reader :config
5
+
6
+ # Initializes a new Client object
7
+ #
8
+ # @param options [Hash]
9
+ # @return [WorldpayCnp::Client]
10
+ def initialize(**options)
11
+ @config = Configuration.new(**options)
12
+ end
13
+
14
+ def create_transaction(data)
15
+ api_client.perform_post(@config.api_url, build_request_body(data))
16
+ end
17
+
18
+ private
19
+
20
+ def api_client
21
+ @api_client ||= ApiClient.new(@config)
22
+ end
23
+
24
+ def build_request_body(data)
25
+ {
26
+ @config.xml_request_root => {
27
+ '@xmlns' => @config.xml_namespace,
28
+ '@version' => @config.version,
29
+ '@merchantId' => @config.merchant_id,
30
+ 'authentication' => {
31
+ 'user' => @config.username,
32
+ 'password' => @config.password
33
+ }
34
+ }.merge(data)
35
+ }
36
+ end
37
+
38
+ end
39
+ end
@@ -0,0 +1,43 @@
1
+ module WorldpayCnp
2
+ class Configuration
3
+ ENVIRONMENTS = {
4
+ sandbox: "https://www.testvantivcnp.com/sandbox/communicator/online",
5
+ prelive: "https://payments.vantivprelive.com/vap/communicator/online",
6
+ production: "https://payments.vantivcnp.com/vap/communicator/online"
7
+ }.freeze
8
+
9
+ attr_reader :environment,
10
+ :username,
11
+ :password,
12
+ :merchant_id,
13
+ :version,
14
+ :timeout,
15
+ :proxy,
16
+ :xml_namespace,
17
+ :xml_request_root
18
+
19
+ def initialize(**options)
20
+ set_defaults
21
+ set_config(options)
22
+ end
23
+
24
+ def api_url
25
+ @api_url ||= ENVIRONMENTS[@environment.to_sym]
26
+ end
27
+
28
+ private
29
+
30
+ def set_defaults
31
+ @environment = :sandbox
32
+ @version = "12.8"
33
+ @xml_namespace = "http://www.vantivcnp.com/schema"
34
+ @xml_request_root = "cnpOnlineRequest"
35
+ end
36
+
37
+ def set_config(options)
38
+ options.each do |key, value|
39
+ instance_variable_set("@#{key}", value)
40
+ end
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,85 @@
1
+ module WorldpayCnp
2
+ class Error < StandardError
3
+ InvalidFormatError = Class.new(self)
4
+ XmlParseError = Class.new(self)
5
+
6
+ # Raised on a 4xx HTTP status code
7
+ ClientError = Class.new(self)
8
+
9
+ # Raised on the HTTP status code 403
10
+ Forbidden = Class.new(ClientError)
11
+
12
+ # Raised on the HTTP status code 404
13
+ NotFound = Class.new(ClientError)
14
+
15
+ # Raised on the HTTP status code 405
16
+ MethodNotAllowed = Class.new(ClientError)
17
+
18
+ # Raised on the HTTP status code 417
19
+ ExpectationFailed = Class.new(ClientError)
20
+
21
+ # Raised on a 5xx HTTP status code
22
+ ServerError = Class.new(self)
23
+
24
+ # Raised on the HTTP status code 500
25
+ InternalServerError = Class.new(ServerError)
26
+
27
+ # Raised on the HTTP status code 502
28
+ BadGateway = Class.new(ServerError)
29
+
30
+ # Raised on the HTTP status code 503
31
+ ServiceUnavailable = Class.new(ServerError)
32
+
33
+ # Raised on the HTTP status code 504
34
+ GatewayTimeout = Class.new(ServerError)
35
+
36
+ ERRORS_BY_STATUS = {
37
+ 403 => Error::Forbidden,
38
+ 404 => Error::NotFound,
39
+ 405 => Error::MethodNotAllowed,
40
+ 417 => Error::ExpectationFailed,
41
+ 500 => Error::InternalServerError,
42
+ 502 => Error::BadGateway,
43
+ 503 => Error::ServiceUnavailable,
44
+ 504 => Error::GatewayTimeout,
45
+ }
46
+
47
+ class << self
48
+ # Create a new error from an HTTP response body and status
49
+ #
50
+ # @param body [String]
51
+ # @param status [Integer]
52
+ # @return [WorldpayCnp::Error]
53
+ def from_http_response(body, status)
54
+ klass = ERRORS_BY_STATUS[status] || self
55
+ klass.new("Service error (Status: #{status})", raw_response: body)
56
+ end
57
+ end
58
+
59
+ # The raw HTTP response, if applicable
60
+ #
61
+ # @return [String]
62
+ attr_reader :raw_response
63
+
64
+ # The response code in the XML root element. The value of the
65
+ # response attribute in the following example:
66
+ #
67
+ # <cnpOnlineResponse version="12.17" xmlns="http://www.vantivcnp.com/schema"
68
+ # response="1" message="Error validating xml..." />
69
+ #
70
+ # @return [String]
71
+ attr_reader :response_code
72
+
73
+ # Initializes a new Error object
74
+ #
75
+ # @param message [Exception, String]
76
+ # @param code [String]
77
+ # @param raw_http_response [String]
78
+ # @return [WorldpayCnp::Error]
79
+ def initialize(message, response_code: '', raw_response: '')
80
+ @response_code = response_code
81
+ @raw_response = raw_response
82
+ super(message)
83
+ end
84
+ end
85
+ end
@@ -0,0 +1,44 @@
1
+ module WorldpayCnp
2
+ module Refinements
3
+ module CamelCase
4
+ refine Hash do
5
+
6
+ def to_camel_case
7
+ _to_camel_case(self)
8
+ end
9
+
10
+ private
11
+
12
+ def _to_camel_case(data)
13
+ case data
14
+ when Array
15
+ data.map { |value| _to_camel_case(value) }
16
+ when Hash
17
+ data.map { |key, value| [camel_case_key(key), _to_camel_case(value)] }.to_h
18
+ else
19
+ data
20
+ end
21
+ end
22
+
23
+ def camel_case_key(key)
24
+ case key
25
+ when Symbol
26
+ camel_case(key.to_s).to_sym
27
+ when String
28
+ camel_case(key).to_sym
29
+ else
30
+ key
31
+ end
32
+ end
33
+
34
+ def camel_case(string)
35
+ @acronyms ||= { 'au' => 'AU', 'iias' => 'IIAS' }
36
+ @acronym_regex ||= /#{@acronyms.values.join("|")}/
37
+ result = string.sub(/^[a-z\d]*/) { |match| @acronyms[match] || match }
38
+ result.gsub(/(?:_|(\/))([a-z\d]*)/i) { "#{$1}#{@acronyms[$2] || $2.capitalize}" }
39
+ end
40
+
41
+ end
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,28 @@
1
+ module WorldpayCnp
2
+ module Refinements
3
+ module DeepSymbolizeKeys
4
+ refine Hash do
5
+
6
+ def deep_symbolize_keys
7
+ _deep_transform_keys_in_object(self, &:to_sym)
8
+ end
9
+
10
+ private
11
+
12
+ def _deep_transform_keys_in_object(object, &block)
13
+ case object
14
+ when Hash
15
+ object.each_with_object({}) do |(key, value), result|
16
+ result[yield(key)] = _deep_transform_keys_in_object(value, &block)
17
+ end
18
+ when Array
19
+ object.map { |value| _deep_transform_keys_in_object(value, &block) }
20
+ else
21
+ object
22
+ end
23
+ end
24
+
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,47 @@
1
+ module WorldpayCnp
2
+ module Refinements
3
+ module SnakeCase
4
+ refine Hash do
5
+
6
+ def to_snake_case
7
+ _to_snake_case(self)
8
+ end
9
+
10
+ private
11
+
12
+ def _to_snake_case(data)
13
+ case data
14
+ when Array
15
+ data.map { |value| _to_snake_case(value) }
16
+ when Hash
17
+ data.map { |key, value| [underscore_key(key), _to_snake_case(value)] }.to_h
18
+ else
19
+ data
20
+ end
21
+ end
22
+
23
+ def underscore_key(key)
24
+ case key
25
+ when Symbol
26
+ underscore(key.to_s).to_sym
27
+ when String
28
+ underscore(key).to_sym
29
+ else
30
+ key
31
+ end
32
+ end
33
+
34
+ def underscore(string)
35
+ @__memoize_underscore ||= {}
36
+ return @__memoize_underscore[string] if @__memoize_underscore[string]
37
+ @__memoize_underscore[string] =
38
+ string.gsub(/([A-Z\d]+)([A-Z][a-z])/, '\1_\2')
39
+ .gsub(/([a-z\d])([A-Z])/, '\1_\2')
40
+ .downcase
41
+ @__memoize_underscore[string]
42
+ end
43
+
44
+ end
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,15 @@
1
+ module WorldpayCnp
2
+ class Response
3
+ extend Forwardable
4
+
5
+ attr_reader :status_code, :raw_response
6
+
7
+ delegate [:dig, :to_h, :to_hash] => :@data
8
+
9
+ def initialize(data, status_code, raw_response)
10
+ @data = data || {}
11
+ @status_code = status_code.to_i
12
+ @raw_response = raw_response
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,3 @@
1
+ module WorldpayCnp
2
+ VERSION = "0.1.0"
3
+ end
@@ -0,0 +1,17 @@
1
+ require "worldpay_cnp/xml/parser"
2
+ require "worldpay_cnp/xml/serializer"
3
+ require "worldpay_cnp/xml/nokogiri"
4
+
5
+ module WorldpayCnp
6
+ module XML
7
+ class << self
8
+ def parse(xml)
9
+ Nokogiri::Parser.new.call(xml)
10
+ end
11
+
12
+ def serialize(hash)
13
+ Nokogiri::Serializer.new.call(hash)
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,47 @@
1
+ module WorldpayCnp
2
+ module XML
3
+ module Nokogiri
4
+ class Parser
5
+ include XML::Parser
6
+
7
+ def call(xml)
8
+ super(xml)
9
+ rescue ::Nokogiri::XML::SyntaxError => error
10
+ raise Error::XmlParseError, error.message, error.backtrace
11
+ end
12
+
13
+ private
14
+
15
+ def root(xml)
16
+ ::Nokogiri::XML(xml) { |c| c.options = ::Nokogiri::XML::ParseOptions::NOBLANKS }.root
17
+ end
18
+
19
+ def value_with!(element)
20
+ return element.text if element.attributes.empty? && element.elements.empty?
21
+ element.to_h.merge(hash_with(*element.elements))
22
+ end
23
+ end
24
+
25
+ class Serializer
26
+ include XML::Serializer
27
+
28
+ def call(hash)
29
+ ::Nokogiri::XML::Document.new.tap { |d| add_xml_elements!(d, hash) }.to_s
30
+ end
31
+
32
+ private
33
+
34
+ def attributes_or_elements!(parent, key, value)
35
+ return parent[attribute_name(key)] = text_with(value) if attribute?(key)
36
+ element = ::Nokogiri::XML::Element.new(key.to_s, parent)
37
+ parent.add_child(element)
38
+ add_xml_elements!(element, value)
39
+ end
40
+
41
+ def insert_text!(element, text)
42
+ element.add_child(text_with(text))
43
+ end
44
+ end
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,33 @@
1
+ module WorldpayCnp
2
+ module XML
3
+ module Parser
4
+ def call(xml)
5
+ hash_with(root(xml))
6
+ end
7
+
8
+ private
9
+
10
+ def root(xml)
11
+ raise NotImplementedError
12
+ end
13
+
14
+ def value_with!(element)
15
+ raise NotImplementedError
16
+ end
17
+
18
+ def hash_with(*nodes)
19
+ nodes.each_with_object({}) do |node, hash|
20
+ inject_or_merge!(hash, node.name, value_with!(node))
21
+ end
22
+ end
23
+
24
+ def inject_or_merge!(hash, key, value)
25
+ if hash.key?(key)
26
+ cv = hash[key]
27
+ value = cv.is_a?(Array) ? cv.push(value) : [cv, value]
28
+ end
29
+ hash[key] = value
30
+ end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,43 @@
1
+ module WorldpayCnp
2
+ module XML
3
+ module Serializer
4
+ def call(hash)
5
+ raise NotImplementedError
6
+ end
7
+
8
+ private
9
+
10
+ def attributes_or_elements!(parent, key, value)
11
+ raise NotImplementedError
12
+ end
13
+
14
+ def insert_text!(element, text)
15
+ raise NotImplementedError
16
+ end
17
+
18
+ def add_xml_elements!(parent, obj)
19
+ case obj
20
+ when Hash
21
+ obj.each { |key, value| attributes_or_elements!(parent, key, value) }
22
+ when Array
23
+ obj.each { |value| add_xml_elements!(parent, value) }
24
+ else
25
+ insert_text!(parent, obj)
26
+ end
27
+ parent
28
+ end
29
+
30
+ def text_with(obj)
31
+ obj.to_s
32
+ end
33
+
34
+ def attribute?(key)
35
+ key.to_s.start_with?("@")
36
+ end
37
+
38
+ def attribute_name(key)
39
+ key.to_s.sub(/^@/, "")
40
+ end
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,30 @@
1
+ require_relative 'lib/worldpay_cnp/version'
2
+
3
+ Gem::Specification.new do |spec|
4
+ spec.name = "worldpay_cnp"
5
+ spec.version = WorldpayCnp::VERSION
6
+ spec.authors = ["Javier Julio"]
7
+ spec.email = ["javier@jackpocket.com"]
8
+
9
+ spec.summary = "A modern Ruby interface to the Worldpay cnpAPI."
10
+ spec.description = "A modern Ruby interface to the Worldpay cnpAPI."
11
+ spec.homepage = "https://github.com/jackpocket/worldpay-cnp"
12
+ spec.license = "MIT"
13
+ spec.required_ruby_version = Gem::Requirement.new(">= 2.5.0")
14
+
15
+ spec.add_dependency "http", '>= 4', '< 5'
16
+ spec.add_dependency "nokogiri", '~> 1.0'
17
+
18
+ spec.metadata["homepage_uri"] = spec.homepage
19
+ spec.metadata["source_code_uri"] = spec.homepage
20
+ spec.metadata["changelog_uri"] = "#{spec.homepage}/blob/master/CHANGELOG.md"
21
+
22
+ # Specify which files should be added to the gem when it is released.
23
+ # The `git ls-files -z` loads the files in the RubyGem that have been added into git.
24
+ spec.files = Dir.chdir(File.expand_path('..', __FILE__)) do
25
+ `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
26
+ end
27
+ spec.bindir = "exe"
28
+ spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
29
+ spec.require_paths = ["lib"]
30
+ end
metadata ADDED
@@ -0,0 +1,108 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: worldpay_cnp
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Javier Julio
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2020-10-30 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: http
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '4'
20
+ - - "<"
21
+ - !ruby/object:Gem::Version
22
+ version: '5'
23
+ type: :runtime
24
+ prerelease: false
25
+ version_requirements: !ruby/object:Gem::Requirement
26
+ requirements:
27
+ - - ">="
28
+ - !ruby/object:Gem::Version
29
+ version: '4'
30
+ - - "<"
31
+ - !ruby/object:Gem::Version
32
+ version: '5'
33
+ - !ruby/object:Gem::Dependency
34
+ name: nokogiri
35
+ requirement: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - "~>"
38
+ - !ruby/object:Gem::Version
39
+ version: '1.0'
40
+ type: :runtime
41
+ prerelease: false
42
+ version_requirements: !ruby/object:Gem::Requirement
43
+ requirements:
44
+ - - "~>"
45
+ - !ruby/object:Gem::Version
46
+ version: '1.0'
47
+ description: A modern Ruby interface to the Worldpay cnpAPI.
48
+ email:
49
+ - javier@jackpocket.com
50
+ executables: []
51
+ extensions: []
52
+ extra_rdoc_files: []
53
+ files:
54
+ - ".env.sample"
55
+ - ".github/workflows/ci.yml"
56
+ - ".gitignore"
57
+ - ".rspec"
58
+ - CHANGELOG.md
59
+ - CODE_OF_CONDUCT.md
60
+ - Gemfile
61
+ - Gemfile.lock
62
+ - LICENSE.txt
63
+ - README.md
64
+ - Rakefile
65
+ - bin/console
66
+ - bin/setup
67
+ - lib/worldpay_cnp.rb
68
+ - lib/worldpay_cnp/api_client.rb
69
+ - lib/worldpay_cnp/client.rb
70
+ - lib/worldpay_cnp/configuration.rb
71
+ - lib/worldpay_cnp/error.rb
72
+ - lib/worldpay_cnp/refinements/camel_case.rb
73
+ - lib/worldpay_cnp/refinements/deep_symbolize_keys.rb
74
+ - lib/worldpay_cnp/refinements/snake_case.rb
75
+ - lib/worldpay_cnp/response.rb
76
+ - lib/worldpay_cnp/version.rb
77
+ - lib/worldpay_cnp/xml.rb
78
+ - lib/worldpay_cnp/xml/nokogiri.rb
79
+ - lib/worldpay_cnp/xml/parser.rb
80
+ - lib/worldpay_cnp/xml/serializer.rb
81
+ - worldpay-cnp.gemspec
82
+ homepage: https://github.com/jackpocket/worldpay-cnp
83
+ licenses:
84
+ - MIT
85
+ metadata:
86
+ homepage_uri: https://github.com/jackpocket/worldpay-cnp
87
+ source_code_uri: https://github.com/jackpocket/worldpay-cnp
88
+ changelog_uri: https://github.com/jackpocket/worldpay-cnp/blob/master/CHANGELOG.md
89
+ post_install_message:
90
+ rdoc_options: []
91
+ require_paths:
92
+ - lib
93
+ required_ruby_version: !ruby/object:Gem::Requirement
94
+ requirements:
95
+ - - ">="
96
+ - !ruby/object:Gem::Version
97
+ version: 2.5.0
98
+ required_rubygems_version: !ruby/object:Gem::Requirement
99
+ requirements:
100
+ - - ">="
101
+ - !ruby/object:Gem::Version
102
+ version: '0'
103
+ requirements: []
104
+ rubygems_version: 3.1.4
105
+ signing_key:
106
+ specification_version: 4
107
+ summary: A modern Ruby interface to the Worldpay cnpAPI.
108
+ test_files: []