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 +7 -0
- data/README.md +189 -0
- data/VERSION +1 -0
- data/lib/paypal-api/access_token.rb +27 -0
- data/lib/paypal-api/client.rb +80 -0
- data/lib/paypal-api/collection.rb +18 -0
- data/lib/paypal-api/collections/authentication.rb +65 -0
- data/lib/paypal-api/collections/orders.rb +36 -0
- data/lib/paypal-api/collections/payments.rb +51 -0
- data/lib/paypal-api/collections/webhooks.rb +55 -0
- data/lib/paypal-api/config.rb +50 -0
- data/lib/paypal-api/error.rb +96 -0
- data/lib/paypal-api/failed_request_error_builder.rb +34 -0
- data/lib/paypal-api/network_error_builder.rb +28 -0
- data/lib/paypal-api/request.rb +67 -0
- data/lib/paypal-api/request_executor.rb +87 -0
- data/lib/paypal-api/response.rb +63 -0
- data/lib/paypal-api/version.rb +9 -0
- data/lib/paypal-api.rb +56 -0
- data/lib/paypal-rest-api.rb +3 -0
- metadata +65 -0
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
|
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"
|
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: []
|