taler 0.3.0 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +18 -0
- data/README.md +20 -1
- data/lib/taler/client.rb +27 -13
- data/lib/taler/order.rb +4 -0
- data/lib/taler/version.rb +1 -1
- data/lib/taler.rb +38 -0
- data/sig/taler.rbs +37 -3
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 60ec832858a497c44d8211a3ac887052991b9a03ad309ffb3ed693205038c3c7
|
|
4
|
+
data.tar.gz: 247a12ee6e2ffbcc52e6887ddd7b205829de374cde7f37917c6d775db54affd9
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 2988d2ccf7fa226cd0c781175f7bdd964b37af55070a489d4380d341154e200e5326e24617a03907e74193225560cf2dfd24a7e6ba71373536240ee2e0f4d8ec
|
|
7
|
+
data.tar.gz: 86fdfeb371bb5cede0492b31c5d7e459c27280cc84a448671ebcdf2b7fe7ae472ed8b7effeabd9e0c13391e636dfded5776b557ae1d2617e42739e82fc278225
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,23 @@
|
|
|
1
1
|
## [Unreleased]
|
|
2
2
|
|
|
3
|
+
## [0.5.0] - 2026-10-07
|
|
4
|
+
|
|
5
|
+
- Raise `Taler::RequestError` when the backend responds with anything but
|
|
6
|
+
200 OK. The error carries the HTTP `status` and the response `body`, and
|
|
7
|
+
its message includes the backend's hint. Before, an error response was
|
|
8
|
+
parsed as a success and callers got a confusing `KeyError` or
|
|
9
|
+
`JSON::ParserError` instead.
|
|
10
|
+
- Raise `Taler::Error` on any other error during the HTTP request. This
|
|
11
|
+
could be a connection error, a network failure or something completely
|
|
12
|
+
unknown.
|
|
13
|
+
|
|
14
|
+
## [0.4.0] - 2026-08-25
|
|
15
|
+
|
|
16
|
+
- Restore deprecated password auth as access tokens need 2FA in production.
|
|
17
|
+
Instead of obtaining a token on-the-fly, the user would provide a token
|
|
18
|
+
to the app (and this gem) instead of the password. We can add support for
|
|
19
|
+
that another time.
|
|
20
|
+
|
|
3
21
|
## [0.3.0] - 2026-02-24
|
|
4
22
|
|
|
5
23
|
- Update docs and rbs file.
|
data/README.md
CHANGED
|
@@ -16,7 +16,7 @@ bundle add taler
|
|
|
16
16
|
require "taler"
|
|
17
17
|
|
|
18
18
|
backend_url = "https://backend.demo.taler.net/instances/sandbox"
|
|
19
|
-
|
|
19
|
+
password = "sandbox"
|
|
20
20
|
|
|
21
21
|
order = Taler::Order.new(backend_url:, password:)
|
|
22
22
|
order.create(
|
|
@@ -39,6 +39,25 @@ else
|
|
|
39
39
|
end
|
|
40
40
|
```
|
|
41
41
|
|
|
42
|
+
### Errors
|
|
43
|
+
|
|
44
|
+
Every request raises `Taler::RequestError` when the backend responds with
|
|
45
|
+
anything but 200 OK. The message includes the backend's explanation, and
|
|
46
|
+
the error carries the HTTP status and the response body:
|
|
47
|
+
|
|
48
|
+
```rb
|
|
49
|
+
begin
|
|
50
|
+
order.create(amount: "KUDOS:4", summary: "Order total")
|
|
51
|
+
rescue Taler::RequestError => e
|
|
52
|
+
puts e.message # The Taler backend responded with 401: The merchant refused the request due to lack of authorization.
|
|
53
|
+
puts e.status # 401
|
|
54
|
+
puts e.body # {"code" => 2015, "hint" => "...", "detail" => "..."}
|
|
55
|
+
rescue Taler::Error => e
|
|
56
|
+
puts e.message # E.g. could not reach destination
|
|
57
|
+
puts e.cause # A StandardError that was raised during the request, e.g. Errno::ETIMEDOUT
|
|
58
|
+
end
|
|
59
|
+
```
|
|
60
|
+
|
|
42
61
|
Read more in the official documentation:
|
|
43
62
|
|
|
44
63
|
- https://rubydoc.info/gems/taler
|
data/lib/taler/client.rb
CHANGED
|
@@ -18,6 +18,7 @@ module Taler
|
|
|
18
18
|
# Obtain an access token to authenticate all other API calls.
|
|
19
19
|
#
|
|
20
20
|
# @return [String]
|
|
21
|
+
# @raise [RequestError] If the backend rejects the request or asks for two-factor authentication.
|
|
21
22
|
def request_token
|
|
22
23
|
url = "#{@backend_url}/private/token"
|
|
23
24
|
payload = {scope: "write"}
|
|
@@ -33,6 +34,7 @@ module Taler
|
|
|
33
34
|
# payment.
|
|
34
35
|
# @return [Hash] The resonse from the backend, usually just containing an
|
|
35
36
|
# order id, e.g. `{order_id: "xxxxx"}`
|
|
37
|
+
# @raise [RequestError] If the backend rejects the request.
|
|
36
38
|
def create_order(amount:, summary:, fulfillment_url: nil, fulfillment_message: nil)
|
|
37
39
|
url = "#{@backend_url}/private/orders"
|
|
38
40
|
order = {
|
|
@@ -53,6 +55,7 @@ module Taler
|
|
|
53
55
|
|
|
54
56
|
# @param order_id [String]
|
|
55
57
|
# @return [Hash] The order status returned by the backend.
|
|
58
|
+
# @raise [RequestError] If the backend rejects the request.
|
|
56
59
|
def fetch_order(order_id)
|
|
57
60
|
url = "#{@backend_url}/private/orders/#{order_id}"
|
|
58
61
|
request(url)
|
|
@@ -63,6 +66,7 @@ module Taler
|
|
|
63
66
|
# @param reason [String] Why are you refunding?
|
|
64
67
|
#
|
|
65
68
|
# @return [Hash] Response from the merchant backend.
|
|
69
|
+
# @raise [RequestError] If the backend rejects the request.
|
|
66
70
|
def refund_order(order_id, refund:, reason:)
|
|
67
71
|
url = "#{@backend_url}/private/orders/#{order_id}/refund"
|
|
68
72
|
payload = {refund:, reason:}
|
|
@@ -76,16 +80,12 @@ module Taler
|
|
|
76
80
|
"secret-token:#{@password}"
|
|
77
81
|
end
|
|
78
82
|
|
|
79
|
-
# @return [String]
|
|
80
|
-
def access_token
|
|
81
|
-
@access_token ||= request_token
|
|
82
|
-
end
|
|
83
|
-
|
|
84
83
|
# @param url [String]
|
|
85
84
|
# @param token [String]
|
|
86
85
|
# @param payload [Hash]
|
|
87
|
-
# @return [
|
|
88
|
-
|
|
86
|
+
# @return [Hash] The parsed JSON response.
|
|
87
|
+
# @raise [RequestError] If the backend responds with anything but 200 OK.
|
|
88
|
+
def request(url, token: auth_token, payload: nil)
|
|
89
89
|
uri = URI(url)
|
|
90
90
|
headers = {
|
|
91
91
|
"Authorization" => "Bearer #{token}",
|
|
@@ -93,16 +93,30 @@ module Taler
|
|
|
93
93
|
"User-Agent" => "Taler Ruby"
|
|
94
94
|
}
|
|
95
95
|
|
|
96
|
+
response = http_request(uri, headers, payload)
|
|
97
|
+
|
|
98
|
+
# The merchant API answers 200 to every request this gem makes.
|
|
99
|
+
# Anything else explains a failure, including 202 which asks for
|
|
100
|
+
# two-factor authentication on the token endpoint.
|
|
101
|
+
# See: https://docs.taler.net/core/api-merchant.html
|
|
102
|
+
raise RequestError.new(response) unless response.is_a?(Net::HTTPOK)
|
|
103
|
+
|
|
104
|
+
JSON.parse(response.body)
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
# @param uri [URI]
|
|
108
|
+
# @param headers [Hash]
|
|
109
|
+
# @param payload [Hash]
|
|
110
|
+
# @return [Net::HTTPResponse]
|
|
111
|
+
def http_request(uri, headers, payload)
|
|
96
112
|
if payload.nil?
|
|
97
|
-
|
|
113
|
+
Net::HTTP.get_response(uri, headers)
|
|
98
114
|
else
|
|
99
115
|
headers["Content-Type"] = "application/json"
|
|
100
|
-
|
|
101
|
-
response = Net::HTTP.post(uri, data, headers)
|
|
102
|
-
body = response.body
|
|
116
|
+
Net::HTTP.post(uri, JSON.dump(payload), headers)
|
|
103
117
|
end
|
|
104
|
-
|
|
105
|
-
|
|
118
|
+
rescue => e
|
|
119
|
+
raise Error, e.message
|
|
106
120
|
end
|
|
107
121
|
end
|
|
108
122
|
end
|
data/lib/taler/order.rb
CHANGED
|
@@ -27,6 +27,7 @@ module Taler
|
|
|
27
27
|
# @param fulfillment_message [String] Text to display to the user after
|
|
28
28
|
# payment.
|
|
29
29
|
# @return [String] The id of the new order.
|
|
30
|
+
# @raise [RequestError] If the backend rejects the request.
|
|
30
31
|
def create(amount:, summary:, fulfillment_url: nil, fulfillment_message: nil)
|
|
31
32
|
response = @client.create_order(
|
|
32
33
|
amount:, summary:, fulfillment_url:, fulfillment_message:
|
|
@@ -57,6 +58,7 @@ module Taler
|
|
|
57
58
|
# Any simple type supported by JSON.
|
|
58
59
|
# @example
|
|
59
60
|
# fetch("order_status") #=> "unpaid"
|
|
61
|
+
# @raise [RequestError] If the backend rejects the request.
|
|
60
62
|
def fetch(key)
|
|
61
63
|
reload unless @status
|
|
62
64
|
@status.fetch(key)
|
|
@@ -68,6 +70,7 @@ module Taler
|
|
|
68
70
|
# user interaction, you want to call this.
|
|
69
71
|
#
|
|
70
72
|
# @return [Hash] The order status returned by the backend.
|
|
73
|
+
# @raise [RequestError] If the backend rejects the request.
|
|
71
74
|
def reload
|
|
72
75
|
@status = @client.fetch_order(@id)
|
|
73
76
|
end
|
|
@@ -78,6 +81,7 @@ module Taler
|
|
|
78
81
|
# @param reason [String] Why are you refunding?
|
|
79
82
|
#
|
|
80
83
|
# @return [Hash] Response from the merchant backend.
|
|
84
|
+
# @raise [RequestError] If the backend rejects the request.
|
|
81
85
|
def refund(refund:, reason:)
|
|
82
86
|
@client.refund_order(@id, refund:, reason:)
|
|
83
87
|
end
|
data/lib/taler/version.rb
CHANGED
data/lib/taler.rb
CHANGED
|
@@ -8,4 +8,42 @@ require_relative "taler/version"
|
|
|
8
8
|
module Taler
|
|
9
9
|
# Base error class for errors raised in this module
|
|
10
10
|
class Error < StandardError; end
|
|
11
|
+
|
|
12
|
+
# Raised when the merchant backend answers with anything but 200 OK.
|
|
13
|
+
#
|
|
14
|
+
# The backend explains errors with a JSON body like
|
|
15
|
+
# `{"code": 2015, "hint": "...", "detail": "..."}`. The hint is used
|
|
16
|
+
# as the error message. A body that is not JSON, like an HTML page
|
|
17
|
+
# from a proxy, is kept as text.
|
|
18
|
+
class RequestError < Error
|
|
19
|
+
# @return [Integer] The HTTP status code of the response.
|
|
20
|
+
attr_reader :status
|
|
21
|
+
|
|
22
|
+
# @return [Hash, String] The parsed JSON body or the raw body.
|
|
23
|
+
attr_reader :body
|
|
24
|
+
|
|
25
|
+
# @param response [Net::HTTPResponse]
|
|
26
|
+
def initialize(response)
|
|
27
|
+
@status = response.code.to_i
|
|
28
|
+
@body = error_body(response.body)
|
|
29
|
+
super("The Taler backend responded with #{status}: #{summary}")
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
private
|
|
33
|
+
|
|
34
|
+
# @return [String]
|
|
35
|
+
def summary
|
|
36
|
+
return body.fetch("hint") if body.is_a?(Hash) && body.key?("hint")
|
|
37
|
+
|
|
38
|
+
body.to_s.strip
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# @param body [String]
|
|
42
|
+
# @return [Hash, String] The parsed JSON body, or the raw body if it isn't JSON.
|
|
43
|
+
def error_body(body)
|
|
44
|
+
JSON.parse(body)
|
|
45
|
+
rescue JSON::ParserError
|
|
46
|
+
body
|
|
47
|
+
end
|
|
48
|
+
end
|
|
11
49
|
end
|
data/sig/taler.rbs
CHANGED
|
@@ -6,6 +6,31 @@ module Taler
|
|
|
6
6
|
class Error < StandardError
|
|
7
7
|
end
|
|
8
8
|
|
|
9
|
+
# Raised when the merchant backend answers with anything but 200 OK.
|
|
10
|
+
#
|
|
11
|
+
# The backend explains errors with a JSON body like
|
|
12
|
+
# `{"code": 2015, "hint": "...", "detail": "..."}`. The hint is used
|
|
13
|
+
# as the error message. A body that is not JSON, like an HTML page
|
|
14
|
+
# from a proxy, is kept as text.
|
|
15
|
+
class RequestError < Taler::Error
|
|
16
|
+
# sord warn - Net::HTTPResponse wasn't able to be resolved to a constant in this project
|
|
17
|
+
# _@param_ `response`
|
|
18
|
+
def initialize: (Net::HTTPResponse response) -> void
|
|
19
|
+
|
|
20
|
+
def summary: () -> String
|
|
21
|
+
|
|
22
|
+
# _@param_ `body`
|
|
23
|
+
#
|
|
24
|
+
# _@return_ — The parsed JSON body, or the raw body if it isn't JSON.
|
|
25
|
+
def error_body: (String body) -> (::Hash[untyped, untyped] | String)
|
|
26
|
+
|
|
27
|
+
# _@return_ — The HTTP status code of the response.
|
|
28
|
+
attr_reader status: Integer
|
|
29
|
+
|
|
30
|
+
# _@return_ — The parsed JSON body or the raw body.
|
|
31
|
+
attr_reader body: (::Hash[untyped, untyped] | String)
|
|
32
|
+
end
|
|
33
|
+
|
|
9
34
|
# Order representation with convenient access to the merchant API.
|
|
10
35
|
class Order
|
|
11
36
|
# Connect an Order to the Taler merchant backend.
|
|
@@ -136,13 +161,22 @@ module Taler
|
|
|
136
161
|
|
|
137
162
|
def auth_token: () -> String
|
|
138
163
|
|
|
139
|
-
def access_token: () -> String
|
|
140
|
-
|
|
141
164
|
# _@param_ `url`
|
|
142
165
|
#
|
|
143
166
|
# _@param_ `token`
|
|
144
167
|
#
|
|
145
168
|
# _@param_ `payload`
|
|
146
|
-
|
|
169
|
+
#
|
|
170
|
+
# _@return_ — The parsed JSON response.
|
|
171
|
+
def request: (String url, ?token: String, ?payload: ::Hash[untyped, untyped]?) -> ::Hash[untyped, untyped]
|
|
172
|
+
|
|
173
|
+
# sord warn - URI wasn't able to be resolved to a constant in this project
|
|
174
|
+
# sord warn - Net::HTTPResponse wasn't able to be resolved to a constant in this project
|
|
175
|
+
# _@param_ `uri`
|
|
176
|
+
#
|
|
177
|
+
# _@param_ `headers`
|
|
178
|
+
#
|
|
179
|
+
# _@param_ `payload`
|
|
180
|
+
def http_request: (URI uri, ::Hash[untyped, untyped] headers, ::Hash[untyped, untyped] payload) -> Net::HTTPResponse
|
|
147
181
|
end
|
|
148
182
|
end
|