paypal-rest-api 0.0.1

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: bb3399d7f3412a84242a4ba9bf2c38447cc1987ca7a90a7e9a51a2628edb7809
4
+ data.tar.gz: ab029c43b63e930b30727a127b356c75388df84d8410fd45aac8edac2a37fa53
5
+ SHA512:
6
+ metadata.gz: b33f4638da4db6992030b9a5fcae5d8836e6cf50cf600adbec07518e8e182f79cb86802f271b8b7276d2391078e8ec11f4a1472865af8bc4a496bbd42f7cb290
7
+ data.tar.gz: 583f9a28508b386c282fd9d1b793042abe461425da3cc4227d17f033263e71b8c17ad2abff09c722f90f62d598f09fe5bbf84c0a9ce789cbf29e948d7c223957
data/README.md ADDED
@@ -0,0 +1,189 @@
1
+ # PaypalAPI
2
+
3
+ ## Installation
4
+
5
+ ```bash
6
+ bundle add paypal-rest-api
7
+ ```
8
+
9
+ ## Features
10
+
11
+ - No dependencies;
12
+ - Automatic authorization & reauthorization;
13
+ - Auto-retries (configured);
14
+ - Automatically added Paypal-Request-Id header for idempotent requests if not
15
+ provided;
16
+
17
+ ## Usage
18
+
19
+ - All APIs accept `:query`, `:body` and `:headers` keyword parameters.
20
+ - Some APIs (like show, update, delete) require positional parameters with ID of
21
+ a resource.
22
+ - Response has `#body` method to get parsed JSON body.
23
+ This body has `symbolized` hash keys.
24
+ - Response contains methods to get original HTTP response.
25
+ - Failed request error (for non `2**` status codes) contains HTTP request and
26
+ response
27
+ - Failed request error (for network errors) contains request and original error
28
+
29
+ ```ruby
30
+ # Initiate client
31
+ client = PaypalAPI::Client.new(
32
+ client_id: ENV['PAYPAL_CLIENT_ID'],
33
+ client_secret: ENV['PAYPAL_CLIENT_SECRET'],
34
+ live: false
35
+ )
36
+
37
+ # APIs calls examples:
38
+ response = client.orders.create(body: body)
39
+ response = client.orders.show(order_id)
40
+ response = client.payments.capture(authorization_id, headers: headers)
41
+ response = client.webhooks.list(query: query)
42
+
43
+ # Client can be used directly to send request to any path
44
+ response = client.post(path, query: query, body: body, headers: headers)
45
+ response = client.get(path, query: query, body: body, headers: headers)
46
+ response = client.patch(path, query: query, body: body, headers: headers)
47
+ response = client.put(path, query: query, body: body, headers: headers)
48
+ response = client.delete(path, query: query, body: body, headers: headers)
49
+
50
+ # Getting response
51
+ response.body # parsed JSON. Parsed with `JSON.load(http_body, symbolyzed_keys: true)`
52
+ response[:foo] # Gets :foo attribute from parsed body
53
+ response.fetch(:foo) # Fetches :foo attribute from parsed body
54
+ response.http_response # original Net::HTTP::Response
55
+ response.http_body # original response string
56
+ response.http_status # Integer http status
57
+ response.http_headers # Hash with response headers (keys are strings)
58
+ response.requested_at # Time when request was sent
59
+ ```
60
+
61
+ Also PaypalAPI client can be added globally and class methods can be used instead:
62
+
63
+ ```ruby
64
+ # in config/initializers/paypal_api.rb
65
+ PaypalAPI.client = PaypalAPI::Client.new(...)
66
+
67
+ # in your business logic
68
+ response = PaypalAPI.orders.create(body: body)
69
+ response = PaypalAPI.webhooks.verify(body: body)
70
+
71
+ # same
72
+ PaypalAPI::Orders.create(body: body)
73
+ PaypalAPI::Webhooks.verify(body: body)
74
+
75
+ # Also now PaypalAPI class can be used as a client
76
+ response = PaypalAPI.post(path, query: query, body: body, headers: headers)
77
+ response = PaypalAPI.get(path, query: query, body: body, headers: headers)
78
+ response = PaypalAPI.patch(path, query: query, body: body, headers: headers)
79
+ response = PaypalAPI.put(path, query: query, body: body, headers: headers)
80
+ response = PaypalAPI.delete(path, query: query, body: body, headers: headers)
81
+ ```
82
+
83
+ ## Configuration options
84
+
85
+ PaypalAPI client accepts this additional options: `:live`, `:retries`, `:http_opts`
86
+
87
+ ### Option `:live`
88
+
89
+ PaypalAPI client can be defined with `live` option which is `false` by default.
90
+ When `live` is `false` all requests will be send to the sandbox endpoints.
91
+
92
+ ```ruby
93
+ client = PaypalAPI::Client.new(
94
+ live: true,
95
+ # ...
96
+ )
97
+ ```
98
+
99
+ ### Option `:retries`
100
+
101
+ This is a Hash with retries configuration.
102
+ By default retries are enabled, 3 retries with 0.25, 0.75, 1.5 seconds delay.
103
+ Default config: `{enabled: true, count: 3, sleep: [0.25, 0.75, 1.5]}`.
104
+ New options are merged with defaults.
105
+ Please keep `sleep` array same size as `count`.
106
+
107
+ Retries happen on any network error, on 409, 429, 5xx response status code.
108
+
109
+ ```ruby
110
+ client = PaypalAPI::Client.new(
111
+ retries: {count: 2, sleep: [0, 0]}
112
+ # ...
113
+ )
114
+ ```
115
+
116
+ ### Option `:http_opts`
117
+
118
+ This are the options that are provided to the `Net::HTTP.start` method,
119
+ like `:read_timeout`, `:write_timeout`, etc.
120
+
121
+ You can find full list of available options here <https://docs.ruby-lang.org/en/master/Net/HTTP.html#method-c-start>
122
+ (Please choose you version of ruby).
123
+
124
+ By default it is an empty hash.
125
+
126
+ ```ruby
127
+ client = PaypalAPI::Client.new(
128
+ http_opts: {read_timeout: 30, write_timeout: 30, open_timeout: 30}
129
+ # ...
130
+ )
131
+ ```
132
+
133
+ ## Errors
134
+
135
+ All APIs can raise error in case of network error or non-2xx response status code.
136
+
137
+ Errors structure:
138
+
139
+ - `PaypalAPI::Error`
140
+ - `PaypalAPI::NetworkError` - any network error
141
+ - `PaypalAPI::FailedRequest` - any non-2xx code error
142
+ - 400 - `PaypalAPI::BadRequestError`
143
+ - 401 - `PaypalAPI::UnauthorizedError`
144
+ - 403 - `PaypalAPI::ForbiddenError`
145
+ - 404 - `PaypalAPI::NotFoundError`
146
+ - 405 - `PaypalAPI::MethodNotAllowedError`
147
+ - 406 - `PaypalAPI::NotAcceptableError`
148
+ - 409 - `PaypalAPI::ConflictError`
149
+ - 415 - `PaypalAPI::UnsupportedMediaTypeError`
150
+ - 422 - `PaypalAPI::UnprocessableEntityError`
151
+ - 429 - `PaypalAPI::TooManyRequestsError`
152
+ - 5xx - `PaypalAPI::FatalError`
153
+ - 500 - `PaypalAPI::InternalServerError`
154
+ - 503 - `PaypalAPI::ServiceUnavailableError`
155
+
156
+ All errors have additional methods:
157
+
158
+ - `#response` - Original response object, can be nil in case of NetworkError
159
+ - `#request` - Original request object
160
+ - `#error_name` - Original error name
161
+ - `#error_message` - Original PayPal error `:message` or error `:description`
162
+ - `#error_debug_id` - Paypal debug_id found in response
163
+ - `#error_details` - Parsed PayPal error details found in parsed response
164
+ (with symbolized keys)
165
+
166
+ ```ruby
167
+ begin
168
+ response = PaypalAPI.payments.capture(authorization_id, body: body)
169
+ rescue PaypalAPI::Error => error
170
+ YourLogger.error(...)
171
+ end
172
+
173
+ ```
174
+
175
+ ## Development
176
+
177
+ ```bash
178
+ bundle install
179
+ rubocop
180
+ rspec
181
+ ```
182
+
183
+ ## Contributing
184
+
185
+ Bug reports and pull requests are welcome on GitHub at <https://github.com/aglushkov/paypal-api>.
186
+
187
+ ## License
188
+
189
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
data/VERSION ADDED
@@ -0,0 +1 @@
1
+ 0.0.1
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PaypalAPI
4
+ #
5
+ # AccessToken object stores authorization string and its expire time.
6
+ #
7
+ class AccessToken
8
+ attr_reader :requested_at, :expires_at, :authorization_string
9
+
10
+ def initialize(requested_at:, expires_in:, access_token:, token_type:)
11
+ @requested_at = requested_at
12
+ @expires_at = requested_at + expires_in
13
+ @authorization_string = "#{token_type} #{access_token}"
14
+ freeze
15
+ end
16
+
17
+ def expired?
18
+ Time.now >= expires_at
19
+ end
20
+
21
+ def inspect
22
+ "#<#{self.class.name} methods: (requested_at, expires_at, expired?, authorization_string)>"
23
+ end
24
+
25
+ alias_method :to_s, :inspect
26
+ end
27
+ end
@@ -0,0 +1,80 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PaypalAPI
4
+ #
5
+ # PaypalAPI Client
6
+ #
7
+ class Client
8
+ attr_reader :config
9
+
10
+ def initialize(client_id:, client_secret:, live: nil, http_opts: nil, retries: nil)
11
+ @config = PaypalAPI::Config.new(
12
+ client_id: client_id,
13
+ client_secret: client_secret,
14
+ live: live,
15
+ http_opts: http_opts,
16
+ retries: retries
17
+ )
18
+
19
+ @access_token = nil
20
+ end
21
+
22
+ def access_token
23
+ (@access_token.nil? || @access_token.expired?) ? refresh_access_token : @access_token
24
+ end
25
+
26
+ def refresh_access_token
27
+ response = authorization.generate_access_token
28
+
29
+ @access_token = AccessToken.new(
30
+ requested_at: response.requested_at,
31
+ expires_in: response.fetch(:expires_in),
32
+ access_token: response.fetch(:access_token),
33
+ token_type: response.fetch(:token_type)
34
+ )
35
+ end
36
+
37
+ def post(path, query: nil, body: nil, headers: nil)
38
+ execute_request(Net::HTTP::Post, path, query: query, body: body, headers: headers)
39
+ end
40
+
41
+ def get(path, query: nil, body: nil, headers: nil)
42
+ execute_request(Net::HTTP::Get, path, query: query, body: body, headers: headers)
43
+ end
44
+
45
+ def patch(path, query: nil, body: nil, headers: nil)
46
+ execute_request(Net::HTTP::Patch, path, query: query, body: body, headers: headers)
47
+ end
48
+
49
+ def put(path, query: nil, body: nil, headers: nil)
50
+ execute_request(Net::HTTP::Put, path, query: query, body: body, headers: headers)
51
+ end
52
+
53
+ def delete(path, query: nil, body: nil, headers: nil)
54
+ execute_request(Net::HTTP::Delete, path, query: query, body: body, headers: headers)
55
+ end
56
+
57
+ def authorization
58
+ Authentication.new(self)
59
+ end
60
+
61
+ def orders
62
+ Orders.new(self)
63
+ end
64
+
65
+ def payments
66
+ Payments.new(self)
67
+ end
68
+
69
+ def webhooks
70
+ Webhooks.new(self)
71
+ end
72
+
73
+ private
74
+
75
+ def execute_request(http_method, path, query: nil, body: nil, headers: nil)
76
+ request = Request.new(self, http_method, path, query: query, body: body, headers: headers)
77
+ RequestExecutor.call(request)
78
+ end
79
+ end
80
+ end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PaypalAPI
4
+ #
5
+ # Base class for all PayPal API collections classes
6
+ #
7
+ class Collection
8
+ attr_reader :client
9
+
10
+ def initialize(client)
11
+ @client = client
12
+ end
13
+
14
+ def self.client
15
+ PaypalAPI.client
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,65 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PaypalAPI
4
+ #
5
+ # Authentication APIs collection
6
+ #
7
+ # @see https://developer.paypal.com/api/rest/authentication/
8
+ #
9
+ class Authentication < Collection
10
+ #
11
+ # Generate access-token API request path
12
+ #
13
+ PATH = "/v1/oauth2/token"
14
+
15
+ #
16
+ # Common class and instance methods
17
+ #
18
+ module APIs
19
+ #
20
+ # Generates access token.
21
+ #
22
+ # @see https://developer.paypal.com/api/rest/authentication/
23
+ #
24
+ # Default headers are:
25
+ # { "content-type" => "application/x-www-form-urlencoded", "authorization" => "Basic <TOKEN>" }
26
+ #
27
+ # Default body is:
28
+ # {grant_type: "client_credentials"}
29
+ #
30
+ # @example
31
+ # PaypalAPI::Authentication.generate_access_token
32
+ # PaypalAPI.client.authorization.generate_access_token # same
33
+ #
34
+ # @param query [Hash, nil] Request query string parameters
35
+ # @param body [Hash, nil] Request body parameters
36
+ # @param body [Hash, nil] Request headers
37
+ #
38
+ # @raise [Error] on network error or non 2** status code returned from PayPal
39
+ # @return [Response] detailed http request-response representation
40
+ #
41
+ def generate_access_token(query: nil, body: nil, headers: nil)
42
+ body ||= {grant_type: "client_credentials"}
43
+
44
+ default_headers = {
45
+ "content-type" => "application/x-www-form-urlencoded",
46
+ "authorization" => "Basic #{["#{client.config.client_id}:#{client.config.client_secret}"].pack("m0")}"
47
+ }
48
+
49
+ client.post(PATH, query: query, body: body, headers: merge_headers!(default_headers, headers))
50
+ end
51
+
52
+ private
53
+
54
+ def merge_headers!(headers, other_headers)
55
+ return headers unless other_headers
56
+
57
+ other_headers = other_headers.transform_keys { |key| key.to_s.downcase }
58
+ headers.merge!(other_headers)
59
+ end
60
+ end
61
+
62
+ include APIs
63
+ extend APIs
64
+ end
65
+ end
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PaypalAPI
4
+ #
5
+ # Create, update, retrieve, authorize, and capture orders.
6
+ #
7
+ # https://developer.paypal.com/docs/api/orders/v2/
8
+ #
9
+ class Orders < Collection
10
+ module APIs
11
+ #
12
+ # @see https://developer.paypal.com/docs/api/orders/v2/#orders_authorize
13
+ #
14
+ def authorize(id, query: nil, body: nil, headers: nil)
15
+ client.post("/v2/checkout/orders/#{id}/authorize", query: query, body: body, headers: headers)
16
+ end
17
+
18
+ #
19
+ # @see https://developer.paypal.com/docs/api/orders/v2/#orders_create
20
+ #
21
+ def create(query: nil, body: nil, headers: nil)
22
+ client.post("/v2/checkout/orders", query: query, body: body, headers: headers)
23
+ end
24
+
25
+ #
26
+ # @see https://developer.paypal.com/docs/api/orders/v2/#orders_get
27
+ #
28
+ def show(id, query: nil, body: nil, headers: nil)
29
+ client.get("/v2/checkout/orders/#{id}", query: query, body: body, headers: headers)
30
+ end
31
+ end
32
+
33
+ include APIs
34
+ extend APIs
35
+ end
36
+ end
@@ -0,0 +1,51 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PaypalAPI
4
+ #
5
+ # Use in conjunction with the Orders API to authorize payments, capture authorized payments,
6
+ # refund payments that have already been captured, and show payment information.
7
+ #
8
+ # https://developer.paypal.com/docs/api/payments/v2
9
+ #
10
+ class Payments < Collection
11
+ module APIs
12
+ #
13
+ # @see https://developer.paypal.com/docs/api/payments/v2/#authorizations_capture
14
+ #
15
+ def capture(authorization_id, query: nil, body: nil, headers: nil)
16
+ client.post("/v2/payments/authorizations/#{authorization_id}/capture", query: query, body: body, headers: headers)
17
+ end
18
+
19
+ #
20
+ # @see https://developer.paypal.com/docs/api/payments/v2/#captures_refund
21
+ #
22
+ def refund(capture_id, query: nil, body: nil, headers: nil)
23
+ client.post("/v2/payments/captures/#{capture_id}/refund", query: query, body: body, headers: headers)
24
+ end
25
+
26
+ #
27
+ # @see https://developer.paypal.com/docs/api/payments/v2/#authorizations_get
28
+ #
29
+ def show_authorized(authorization_id, query: nil, body: nil, headers: nil)
30
+ client.get("/v2/payments/authorizations/#{authorization_id}", query: query, body: body, headers: headers)
31
+ end
32
+
33
+ #
34
+ # @see https://developer.paypal.com/docs/api/payments/v2/#captures_get
35
+ #
36
+ def show_captured(capture_id, query: nil, body: nil, headers: nil)
37
+ client.get("/v2/payments/captures/#{capture_id}", query: query, body: body, headers: headers)
38
+ end
39
+
40
+ #
41
+ # @see https://developer.paypal.com/docs/api/payments/v2/#authorizations_void
42
+ #
43
+ def void(authorization_id, query: nil, body: nil, headers: nil)
44
+ client.post("/v2/payments/authorizations/#{authorization_id}/void", query: query, body: body, headers: headers)
45
+ end
46
+ end
47
+
48
+ include APIs
49
+ extend APIs
50
+ end
51
+ end
@@ -0,0 +1,55 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PaypalAPI
4
+ #
5
+ # https://developer.paypal.com/docs/api/webhooks/v1/
6
+ #
7
+ class Webhooks < Collection
8
+ module APIs
9
+ #
10
+ # https://developer.paypal.com/docs/api/webhooks/v1/#webhooks_post
11
+ #
12
+ def create(query: nil, body: nil, headers: nil)
13
+ client.post("/v1/notifications/webhooks", query: query, body: body, headers: headers)
14
+ end
15
+
16
+ #
17
+ # https://developer.paypal.com/docs/api/webhooks/v1/#webhooks_delete
18
+ #
19
+ def delete(webhook_id, query: nil, body: nil, headers: nil)
20
+ client.delete("/v1/notifications/webhooks/#{webhook_id}", query: query, body: body, headers: headers)
21
+ end
22
+
23
+ #
24
+ # https://developer.paypal.com/docs/api/webhooks/v1/#webhooks_list
25
+ #
26
+ def list(query: nil, body: nil, headers: nil)
27
+ client.get("/v1/notifications/webhooks", query: query, body: body, headers: headers)
28
+ end
29
+
30
+ #
31
+ # https://developer.paypal.com/docs/api/webhooks/v1/#webhooks_get
32
+ #
33
+ def show(webhook_id, query: nil, body: nil, headers: nil)
34
+ client.get("/v1/notifications/webhooks/#{webhook_id}", query: query, body: body, headers: headers)
35
+ end
36
+
37
+ #
38
+ # https://developer.paypal.com/docs/api/webhooks/v1/#webhooks_update
39
+ #
40
+ def update(webhook_id, query: nil, body: nil, headers: nil)
41
+ client.patch("/v1/notifications/webhooks/#{webhook_id}", query: query, body: body, headers: headers)
42
+ end
43
+
44
+ #
45
+ # https://developer.paypal.com/docs/api/webhooks/v1/#verify-webhook-signature_post
46
+ #
47
+ def verify(query: nil, body: nil, headers: nil)
48
+ client.post("/v1/notifications/verify-webhook-signature", query: query, body: body, headers: headers)
49
+ end
50
+ end
51
+
52
+ include APIs
53
+ extend APIs
54
+ end
55
+ end
@@ -0,0 +1,50 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PaypalAPI
4
+ LIVE_URL = "https://api-m.paypal.com"
5
+ SANDBOX_URL = "https://api-m.sandbox.paypal.com"
6
+
7
+ DEFAULTS = {
8
+ live: false,
9
+ http_opts: {}.freeze,
10
+ retries: {enabled: true, count: 3, sleep: [0.25, 0.75, 1.5].freeze}.freeze
11
+ }.freeze
12
+
13
+ #
14
+ # Stores configuration for PaypalAPI Client
15
+ #
16
+ class Config
17
+ attr_reader :client_id, :client_secret, :live, :http_opts, :retries
18
+
19
+ def initialize(client_id:, client_secret:, live: nil, http_opts: nil, retries: nil)
20
+ @client_id = client_id
21
+ @client_secret = client_secret
22
+ @live = with_default(:live, live)
23
+ @http_opts = with_default(:http_opts, http_opts)
24
+ @retries = with_default(:retries, retries)
25
+ freeze
26
+ end
27
+
28
+ def url
29
+ live ? LIVE_URL : SANDBOX_URL
30
+ end
31
+
32
+ def inspect
33
+ "#<#{self.class.name} live: #{live}>"
34
+ end
35
+
36
+ alias_method :to_s, :inspect
37
+
38
+ private
39
+
40
+ def with_default(option_name, value)
41
+ default = DEFAULTS.fetch(option_name)
42
+
43
+ case value
44
+ when NilClass then default
45
+ when Hash then default.merge(value)
46
+ else value
47
+ end
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,96 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PaypalAPI
4
+ #
5
+ # Common interface for all errors
6
+ #
7
+ class Error < StandardError
8
+ attr_reader :response, :request, :error_name, :error_message, :error_debug_id, :error_details
9
+ end
10
+
11
+ #
12
+ # Raised when PayPal responds with any status code except 200, 201, 202, 204
13
+ #
14
+ class FailedRequest < Error
15
+ def initialize(message = nil, request:, response:)
16
+ super(message)
17
+ @request = request
18
+ @response = response
19
+
20
+ body = response.body
21
+ data = body.is_a?(Hash) ? body : {}
22
+ @error_name = data[:name] || data[:error] || response.http_response.class.name
23
+ @error_message = data[:message] || data[:error_description] || response.http_body.to_s
24
+ @error_debug_id = data[:debug_id]
25
+ @error_details = data[:details]
26
+ end
27
+ end
28
+
29
+ #
30
+ # Raised when a network raised when executing the request
31
+ # List of network errors can be found in errors/network_error_builder.rb
32
+ #
33
+ class NetworkError < Error
34
+ def initialize(message = nil, request:, error:)
35
+ super(message)
36
+ @request = request
37
+ @response = nil
38
+ @error_name = error.class.name
39
+ @error_message = error.message
40
+ @error_debug_id = nil
41
+ @error_details = nil
42
+ end
43
+ end
44
+
45
+ # 400
46
+ class BadRequestError < FailedRequest
47
+ end
48
+
49
+ # 401
50
+ class UnauthorizedError < FailedRequest
51
+ end
52
+
53
+ # 403
54
+ class ForbiddenError < FailedRequest
55
+ end
56
+
57
+ # 404
58
+ class NotFoundError < FailedRequest
59
+ end
60
+
61
+ # 405
62
+ class MethodNotAllowedError < FailedRequest
63
+ end
64
+
65
+ # 406
66
+ class NotAcceptableError < FailedRequest
67
+ end
68
+
69
+ # 409
70
+ class ConflictError < FailedRequest
71
+ end
72
+
73
+ # 415
74
+ class UnsupportedMediaTypeError < FailedRequest
75
+ end
76
+
77
+ # 422
78
+ class UnprocessableEntityError < FailedRequest
79
+ end
80
+
81
+ # 429
82
+ class TooManyRequestsError < FailedRequest
83
+ end
84
+
85
+ # 5xx
86
+ class FatalError < FailedRequest
87
+ end
88
+
89
+ # 500
90
+ class InternalServerError < FatalError
91
+ end
92
+
93
+ # 503
94
+ class ServiceUnavailableError < FatalError
95
+ end
96
+ end
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "net/http"
4
+
5
+ module PaypalAPI
6
+ #
7
+ # Builds PaypalAPI::FailedRequest error
8
+ #
9
+ class FailedRequestErrorBuilder
10
+ RESPONSE_ERROR_MAP = {
11
+ Net::HTTPBadRequest => BadRequestError, # 400
12
+ Net::HTTPUnauthorized => UnauthorizedError, # 401
13
+ Net::HTTPForbidden => ForbiddenError, # 403
14
+ Net::HTTPNotFound => NotFoundError, # 404
15
+ Net::HTTPMethodNotAllowed => MethodNotAllowedError, # 405
16
+ Net::HTTPNotAcceptable => NotAcceptableError, # 406
17
+ Net::HTTPConflict => ConflictError, # 409
18
+ Net::HTTPUnsupportedMediaType => UnsupportedMediaTypeError, # 415
19
+ Net::HTTPUnprocessableEntity => UnprocessableEntityError, # 422
20
+ Net::HTTPTooManyRequests => TooManyRequestsError, # 429
21
+ Net::HTTPInternalServerError => InternalServerError, # 500
22
+ Net::HTTPServiceUnavailable => ServiceUnavailableError # 503
23
+ }.freeze
24
+
25
+ class << self
26
+ def call(request:, response:)
27
+ http_response = response.http_response
28
+ error_message = "#{http_response.code} #{http_response.message}"
29
+ error_class = RESPONSE_ERROR_MAP.fetch(http_response.class, FailedRequest)
30
+ error_class.new(error_message, response: response, request: request)
31
+ end
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PaypalAPI
4
+ #
5
+ # Builds PaypalAPI::NetowrkError error
6
+ #
7
+ class NetworkErrorBuilder
8
+ ERRORS = [
9
+ EOFError,
10
+ Errno::ECONNABORTED,
11
+ Errno::ECONNREFUSED,
12
+ Errno::ECONNRESET,
13
+ Errno::EHOSTUNREACH,
14
+ Errno::EPIPE,
15
+ Errno::ETIMEDOUT,
16
+ IOError,
17
+ OpenSSL::SSL::SSLError,
18
+ SocketError,
19
+ Timeout::Error # Net::OpenTimeout, Net::ReadTimeout
20
+ ].freeze
21
+
22
+ class << self
23
+ def call(request:, error:)
24
+ NetworkError.new(error.message, request: request, error: error)
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,67 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "securerandom"
4
+ require "json"
5
+
6
+ module PaypalAPI
7
+ #
8
+ # Builds PaypalAPI::Request:
9
+ # - assigns query params
10
+ # - assigns body params
11
+ # - assigns Authentication header
12
+ # - assigns paypal-request-id header
13
+ # - assigns content-type header
14
+ #
15
+ class Request
16
+ attr_reader :client, :http_request
17
+ attr_accessor :requested_at
18
+
19
+ # rubocop:disable Metrics/ParameterLists
20
+ def initialize(client, request_type, path, body: nil, query: nil, headers: nil)
21
+ @client = client
22
+ @http_request = build_http_request(request_type, path, body: body, query: query, headers: headers)
23
+ @requested_at = nil
24
+ end
25
+ # rubocop:enable Metrics/ParameterLists
26
+
27
+ private
28
+
29
+ def build_http_request(request_type, path, body:, query:, headers:)
30
+ uri = request_uri(path, query)
31
+ http_request = request_type.new(uri)
32
+
33
+ add_headers(http_request, headers || {})
34
+ add_body(http_request, body)
35
+
36
+ http_request
37
+ end
38
+
39
+ def add_headers(http_request, headers)
40
+ headers.each { |key, value| http_request[key] = value }
41
+
42
+ http_request["content-type"] ||= "application/json"
43
+ http_request["authorization"] ||= client.access_token.authorization_string
44
+ http_request["paypal-request-id"] ||= SecureRandom.uuid if idempotent?(http_request)
45
+ end
46
+
47
+ def add_body(http_request, body)
48
+ return unless body
49
+
50
+ json?(http_request) ? http_request.body = JSON.dump(body) : http_request.set_form_data(body)
51
+ end
52
+
53
+ def request_uri(path, query)
54
+ uri = URI.join(client.config.url, path)
55
+ uri.query = URI.encode_www_form(query) if query && !query.empty?
56
+ uri
57
+ end
58
+
59
+ def idempotent?(http_request)
60
+ http_request.method != Net::HTTP::Get::METHOD
61
+ end
62
+
63
+ def json?(http_request)
64
+ http_request["content-type"].include?("json")
65
+ end
66
+ end
67
+ end
@@ -0,0 +1,87 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PaypalAPI
4
+ #
5
+ # Executes PaypalAPI::Request and returns PaypalAPI::Response or raises PaypalAPI::Error
6
+ #
7
+ class RequestExecutor
8
+ RETRYABLE_RESPONSES = [
9
+ Net::HTTPServerError, # 5xx
10
+ Net::HTTPConflict, # 409
11
+ Net::HTTPTooManyRequests # 429
12
+ ].freeze
13
+
14
+ class << self
15
+ def call(request)
16
+ http_response = execute(request)
17
+ response = Response.new(http_response, requested_at: request.requested_at)
18
+ raise FailedRequestErrorBuilder.call(request: request, response: response) unless http_response.is_a?(Net::HTTPSuccess)
19
+
20
+ response
21
+ end
22
+
23
+ private
24
+
25
+ def execute(request, retry_number: 0)
26
+ http_response = execute_http_request(request)
27
+ rescue *NetworkErrorBuilder::ERRORS => error
28
+ retry_on_network_error(request, error, retry_number)
29
+ else
30
+ retryable?(request, http_response, retry_number) ? retry_request(request, retry_number) : http_response
31
+ end
32
+
33
+ def execute_http_request(request)
34
+ http_request = request.http_request
35
+ http_opts = request.client.config.http_opts
36
+ uri = http_request.uri
37
+ request.requested_at = Time.now
38
+
39
+ Net::HTTP.start(uri.hostname, uri.port, use_ssl: true, **http_opts) do |http|
40
+ http.max_retries = 0 # we have custom retries logic
41
+ http.request(http_request)
42
+ end
43
+ end
44
+
45
+ def retry_on_network_error(request, error, retry_number)
46
+ raise NetworkErrorBuilder.call(request: request, error: error) if retries_limit_reached?(request, retry_number)
47
+
48
+ retry_request(request, retry_number)
49
+ end
50
+
51
+ def retry_request(request, current_retry_number)
52
+ sleep(retry_sleep_seconds(request, current_retry_number))
53
+ execute(request, retry_number: current_retry_number + 1)
54
+ end
55
+
56
+ def retries_limit_reached?(request, retry_number)
57
+ retry_number >= request.client.config.retries[:count]
58
+ end
59
+
60
+ def retry_sleep_seconds(request, current_retry_number)
61
+ seconds_per_retry = request.client.config.retries[:sleep]
62
+ seconds_per_retry[current_retry_number] || seconds_per_retry.last || 1
63
+ end
64
+
65
+ def retryable?(request, http_response, retry_number)
66
+ !http_response.is_a?(Net::HTTPSuccess) &&
67
+ !retries_limit_reached?(request, retry_number) &&
68
+ retryable_request?(request, http_response)
69
+ end
70
+
71
+ def retryable_request?(request, http_response)
72
+ return true if RETRYABLE_RESPONSES.any? { |retryable_class| http_response.is_a?(retryable_class) }
73
+
74
+ retry_unauthorized?(request, http_response)
75
+ end
76
+
77
+ def retry_unauthorized?(request, http_response)
78
+ return false unless http_response.is_a?(Net::HTTPUnauthorized) # 401
79
+ return false if http_response.uri.path == Authentication::PATH # it's already an Authentication request
80
+
81
+ # set new access-token
82
+ request.http_request["authorization"] = request.client.refresh_access_token.authorization_string
83
+ true
84
+ end
85
+ end
86
+ end
87
+ end
@@ -0,0 +1,63 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+
5
+ module PaypalAPI
6
+ #
7
+ # PaypalAPI::Response object
8
+ #
9
+ class Response
10
+ attr_reader :http_response, :requested_at
11
+
12
+ def initialize(http_response, requested_at:)
13
+ @requested_at = requested_at
14
+ @http_response = http_response
15
+ @http_status = nil
16
+ @http_headers = nil
17
+ @http_body = nil
18
+ @body = nil
19
+ end
20
+
21
+ def body
22
+ @body ||= json_response? ? parse_json(http_body) : http_body
23
+ end
24
+
25
+ def http_status
26
+ @http_status ||= http_response.code.to_i
27
+ end
28
+
29
+ def http_headers
30
+ @http_headers ||= http_response.each_header.to_h
31
+ end
32
+
33
+ def http_body
34
+ @http_body ||= http_response.body
35
+ end
36
+
37
+ def [](key)
38
+ body[key.to_sym] if body.is_a?(Hash)
39
+ end
40
+
41
+ def fetch(key)
42
+ data = body.is_a?(Hash) ? body : {}
43
+ data.fetch(key.to_sym)
44
+ end
45
+
46
+ def inspect
47
+ "#<#{self.class.name} (#{http_response.code})>"
48
+ end
49
+
50
+ private
51
+
52
+ def json_response?
53
+ content_type = http_response["content-type"]
54
+ !content_type.nil? && content_type.include?("json")
55
+ end
56
+
57
+ def parse_json(json)
58
+ JSON.parse(json, symbolize_names: true)
59
+ rescue JSON::ParserError, TypeError
60
+ json
61
+ end
62
+ end
63
+ end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PaypalAPI
4
+ # PaypalAPI gem version
5
+ #
6
+ # @return [String] SemVer gem version
7
+ #
8
+ VERSION = File.read(File.join(File.dirname(__FILE__), "../../VERSION")).strip
9
+ end
data/lib/paypal-api.rb ADDED
@@ -0,0 +1,56 @@
1
+ # frozen_string_literal: true
2
+
3
+ #
4
+ # PaypalAPI is a main gem module.
5
+ # It can store global PaypalAPI::Client for easier access to APIs.
6
+ #
7
+ # For example:
8
+ # # setup client in an initializer
9
+ # PaypalAPI.client = PaypalAPI::Client.new(...)
10
+ #
11
+ # # And then use anywhere
12
+ # PaypalAPI::Webhooks.list # or PaypalAPI.webhooks.list
13
+ #
14
+ module PaypalAPI
15
+ class << self
16
+ attr_writer :client
17
+
18
+ [:post, :get, :patch, :put, :delete].each do |method_name|
19
+ define_method(method_name) do |path, query: nil, body: nil, headers: nil|
20
+ client.public_send(method_name, path, query: query, body: body, headers: headers)
21
+ end
22
+ end
23
+
24
+ %i[
25
+ authorization
26
+ orders
27
+ payments
28
+ webhooks
29
+ ].each do |method_name|
30
+ define_method(method_name) do
31
+ client.public_send(method_name)
32
+ end
33
+ end
34
+
35
+ def client
36
+ raise "#{name}.client must be set" unless @client
37
+
38
+ @client
39
+ end
40
+ end
41
+ end
42
+
43
+ require_relative "paypal-api/access_token"
44
+ require_relative "paypal-api/client"
45
+ require_relative "paypal-api/collection"
46
+ require_relative "paypal-api/config"
47
+ require_relative "paypal-api/error"
48
+ require_relative "paypal-api/failed_request_error_builder"
49
+ require_relative "paypal-api/network_error_builder"
50
+ require_relative "paypal-api/request"
51
+ require_relative "paypal-api/request_executor"
52
+ require_relative "paypal-api/response"
53
+ require_relative "paypal-api/collections/authentication"
54
+ require_relative "paypal-api/collections/orders"
55
+ require_relative "paypal-api/collections/payments"
56
+ require_relative "paypal-api/collections/webhooks"
@@ -0,0 +1,3 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "paypal-api"
metadata ADDED
@@ -0,0 +1,65 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: paypal-rest-api
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ platform: ruby
6
+ authors:
7
+ - Andrey Glushkov
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2024-08-06 00:00:00.000000000 Z
12
+ dependencies: []
13
+ description: PayPal REST API with no dependencies.
14
+ email:
15
+ - aglushkov@shakuro.com
16
+ executables: []
17
+ extensions: []
18
+ extra_rdoc_files: []
19
+ files:
20
+ - README.md
21
+ - VERSION
22
+ - lib/paypal-api.rb
23
+ - lib/paypal-api/access_token.rb
24
+ - lib/paypal-api/client.rb
25
+ - lib/paypal-api/collection.rb
26
+ - lib/paypal-api/collections/authentication.rb
27
+ - lib/paypal-api/collections/orders.rb
28
+ - lib/paypal-api/collections/payments.rb
29
+ - lib/paypal-api/collections/webhooks.rb
30
+ - lib/paypal-api/config.rb
31
+ - lib/paypal-api/error.rb
32
+ - lib/paypal-api/failed_request_error_builder.rb
33
+ - lib/paypal-api/network_error_builder.rb
34
+ - lib/paypal-api/request.rb
35
+ - lib/paypal-api/request_executor.rb
36
+ - lib/paypal-api/response.rb
37
+ - lib/paypal-api/version.rb
38
+ - lib/paypal-rest-api.rb
39
+ homepage: https://github.com/aglushkov/paypal-api
40
+ licenses:
41
+ - MIT
42
+ metadata:
43
+ source_code_uri: https://github.com/aglushkov/paypal-api
44
+ documentation_uri: https://www.rubydoc.info/gems/serega
45
+ changelog_uri: https://github.com/aglushkov/paypal-api/blob/master/CHANGELOG.md
46
+ post_install_message:
47
+ rdoc_options: []
48
+ require_paths:
49
+ - lib
50
+ required_ruby_version: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ">="
53
+ - !ruby/object:Gem::Version
54
+ version: 2.6.0
55
+ required_rubygems_version: !ruby/object:Gem::Requirement
56
+ requirements:
57
+ - - ">="
58
+ - !ruby/object:Gem::Version
59
+ version: '0'
60
+ requirements: []
61
+ rubygems_version: 3.5.17
62
+ signing_key:
63
+ specification_version: 4
64
+ summary: PayPal REST API
65
+ test_files: []