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