mpp-rb 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (54) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE +21 -0
  3. data/README.md +133 -0
  4. data/lib/mpp/body_digest.rb +37 -0
  5. data/lib/mpp/challenge.rb +115 -0
  6. data/lib/mpp/challenge_echo.rb +19 -0
  7. data/lib/mpp/challenge_id.rb +54 -0
  8. data/lib/mpp/client/transport.rb +137 -0
  9. data/lib/mpp/client.rb +9 -0
  10. data/lib/mpp/credential.rb +20 -0
  11. data/lib/mpp/errors.rb +190 -0
  12. data/lib/mpp/expires.rb +60 -0
  13. data/lib/mpp/extensions/mcp/capabilities.rb +23 -0
  14. data/lib/mpp/extensions/mcp/constants.rb +17 -0
  15. data/lib/mpp/extensions/mcp/decorator.rb +44 -0
  16. data/lib/mpp/extensions/mcp/errors.rb +110 -0
  17. data/lib/mpp/extensions/mcp/types.rb +205 -0
  18. data/lib/mpp/extensions/mcp/verify.rb +152 -0
  19. data/lib/mpp/extensions/mcp.rb +16 -0
  20. data/lib/mpp/json.rb +32 -0
  21. data/lib/mpp/methods/stripe/charge_intent.rb +90 -0
  22. data/lib/mpp/methods/stripe/client_method.rb +42 -0
  23. data/lib/mpp/methods/stripe/defaults.rb +14 -0
  24. data/lib/mpp/methods/stripe/stripe_method.rb +63 -0
  25. data/lib/mpp/methods/stripe.rb +14 -0
  26. data/lib/mpp/methods/tempo/account.rb +52 -0
  27. data/lib/mpp/methods/tempo/attribution.rb +112 -0
  28. data/lib/mpp/methods/tempo/client_method.rb +259 -0
  29. data/lib/mpp/methods/tempo/defaults.rb +77 -0
  30. data/lib/mpp/methods/tempo/fee_payer_envelope.rb +74 -0
  31. data/lib/mpp/methods/tempo/intents.rb +377 -0
  32. data/lib/mpp/methods/tempo/keychain.rb +31 -0
  33. data/lib/mpp/methods/tempo/proof.rb +127 -0
  34. data/lib/mpp/methods/tempo/rpc.rb +60 -0
  35. data/lib/mpp/methods/tempo/schemas.rb +96 -0
  36. data/lib/mpp/methods/tempo/transaction.rb +144 -0
  37. data/lib/mpp/methods/tempo.rb +22 -0
  38. data/lib/mpp/parsing.rb +252 -0
  39. data/lib/mpp/receipt.rb +31 -0
  40. data/lib/mpp/secure_compare.rb +25 -0
  41. data/lib/mpp/server/decorator.rb +32 -0
  42. data/lib/mpp/server/defaults.rb +45 -0
  43. data/lib/mpp/server/intent.rb +40 -0
  44. data/lib/mpp/server/method.rb +27 -0
  45. data/lib/mpp/server/middleware.rb +51 -0
  46. data/lib/mpp/server/mpp_handler.rb +97 -0
  47. data/lib/mpp/server/verify.rb +129 -0
  48. data/lib/mpp/server.rb +15 -0
  49. data/lib/mpp/store.rb +49 -0
  50. data/lib/mpp/units.rb +57 -0
  51. data/lib/mpp/version.rb +6 -0
  52. data/lib/mpp-rb.rb +3 -0
  53. data/lib/mpp.rb +68 -0
  54. metadata +111 -0
data/lib/mpp/errors.rb ADDED
@@ -0,0 +1,190 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ module Mpp
5
+ BASE_URI = "https://paymentauth.org/problems"
6
+
7
+ # Parse error for malformed payment headers.
8
+ class ParseError < StandardError; end
9
+
10
+ # Base verification error.
11
+ class VerificationError < StandardError; end
12
+
13
+ # Base class for all payment-related errors with RFC 9457 support.
14
+ class PaymentError < StandardError
15
+ extend T::Sig
16
+
17
+ sig { params(subclass: T::Class[T.anything]).returns(T.untyped) }
18
+ def self.inherited(subclass)
19
+ super
20
+ return if subclass.instance_variable_defined?(:@_mpp_configured)
21
+
22
+ subclass.instance_variable_set(:@_mpp_configured, true)
23
+ name = subclass.name&.split("::")&.last || "PaymentError"
24
+ unless subclass.instance_variable_defined?(:@type)
25
+ subclass.instance_variable_set(:@type,
26
+ "#{BASE_URI}/#{to_slug(name)}")
27
+ end
28
+ subclass.instance_variable_set(:@title, to_title(name)) unless subclass.instance_variable_defined?(:@title)
29
+ subclass.instance_variable_set(:@status, 402) unless subclass.instance_variable_defined?(:@status)
30
+ end
31
+
32
+ class << self
33
+ extend T::Sig
34
+
35
+ sig { returns(T.nilable(Integer)) }
36
+ attr_reader :status
37
+
38
+ sig { returns(T.nilable(String)) }
39
+ attr_reader :type
40
+
41
+ sig { returns(T.nilable(String)) }
42
+ attr_reader :title
43
+
44
+ private
45
+
46
+ sig { params(name: String).returns(String) }
47
+ def to_slug(name)
48
+ name.sub(/Error$/, "").gsub(/(?<=[a-z0-9])(?=[A-Z])/, "-").downcase
49
+ end
50
+
51
+ sig { params(name: String).returns(String) }
52
+ def to_title(name)
53
+ name.sub(/Error$/, "").gsub(/(?<=[a-z0-9])(?=[A-Z])/, " ")
54
+ end
55
+ end
56
+
57
+ @status = T.let(402, Integer)
58
+ @type = T.let("#{BASE_URI}/payment-error", String)
59
+ @title = T.let("Payment Error", String)
60
+
61
+ sig { returns(T.untyped) }
62
+ def status = self.class.status
63
+ sig { returns(T.untyped) }
64
+ def type = self.class.type
65
+ sig { returns(T.untyped) }
66
+ def title = self.class.title
67
+
68
+ # Convert to RFC 9457 Problem Details format.
69
+ sig { params(challenge_id: T.untyped).returns(T::Hash[T.untyped, T.untyped]) }
70
+ def to_problem_details(challenge_id: nil)
71
+ details = {
72
+ "type" => type,
73
+ "title" => title,
74
+ "status" => status,
75
+ "detail" => message
76
+ }
77
+ details["challengeId"] = challenge_id if challenge_id
78
+ details
79
+ end
80
+ end
81
+
82
+ class PaymentRequiredError < PaymentError
83
+ extend T::Sig
84
+
85
+ sig { params(realm: T.untyped, description: T.untyped).void }
86
+ def initialize(realm: nil, description: nil)
87
+ parts = ["Payment is required"]
88
+ parts << "for \"#{realm}\"" if realm
89
+ parts << "(#{description})" if description
90
+ super("#{parts.join(" ")}.")
91
+ end
92
+ end
93
+
94
+ class MalformedCredentialError < PaymentError
95
+ extend T::Sig
96
+
97
+ sig { params(reason: T.untyped).void }
98
+ def initialize(reason: nil)
99
+ msg = reason ? "Credential is malformed: #{reason}." : "Credential is malformed."
100
+ super(msg)
101
+ end
102
+ end
103
+
104
+ class InvalidChallengeError < PaymentError
105
+ extend T::Sig
106
+
107
+ sig { params(challenge_id: T.untyped, reason: T.untyped).void }
108
+ def initialize(challenge_id: nil, reason: nil)
109
+ id_part = challenge_id ? " \"#{challenge_id}\"" : ""
110
+ reason_part = reason ? ": #{reason}" : ""
111
+ super("Challenge#{id_part} is invalid#{reason_part}.")
112
+ end
113
+ end
114
+
115
+ class VerificationFailedError < PaymentError
116
+ extend T::Sig
117
+
118
+ sig { params(reason: T.untyped).void }
119
+ def initialize(reason: nil)
120
+ msg = reason ? "Payment verification failed: #{reason}." : "Payment verification failed."
121
+ super(msg)
122
+ end
123
+ end
124
+
125
+ class PaymentExpiredError < PaymentError
126
+ extend T::Sig
127
+
128
+ sig { params(expires: T.untyped).void }
129
+ def initialize(expires: nil)
130
+ msg = expires ? "Payment expired at #{expires}." : "Payment has expired."
131
+ super(msg)
132
+ end
133
+ end
134
+
135
+ class InvalidPayloadError < PaymentError
136
+ extend T::Sig
137
+
138
+ sig { params(reason: T.untyped).void }
139
+ def initialize(reason: nil)
140
+ msg = reason ? "Credential payload is invalid: #{reason}." : "Credential payload is invalid."
141
+ super(msg)
142
+ end
143
+ end
144
+
145
+ class BadRequestError < PaymentError
146
+ extend T::Sig
147
+
148
+ @status = T.let(400, Integer)
149
+
150
+ sig { params(reason: T.untyped).void }
151
+ def initialize(reason: nil)
152
+ msg = reason ? "Bad request: #{reason}." : "Bad request."
153
+ super(msg)
154
+ end
155
+ end
156
+
157
+ class PaymentInsufficientError < PaymentError
158
+ extend T::Sig
159
+
160
+ sig { params(reason: T.untyped).void }
161
+ def initialize(reason: nil)
162
+ msg = reason ? "Payment insufficient: #{reason}." : "Payment amount is insufficient."
163
+ super(msg)
164
+ end
165
+ end
166
+
167
+ class PaymentMethodUnsupportedError < PaymentError
168
+ extend T::Sig
169
+
170
+ @status = T.let(400, Integer)
171
+ @type = T.let("#{BASE_URI}/method-unsupported", String)
172
+ @title = T.let("Method Unsupported", String)
173
+
174
+ sig { params(method: T.untyped).void }
175
+ def initialize(method: nil)
176
+ msg = method ? "Payment method \"#{method}\" is not supported." : "Payment method is not supported."
177
+ super(msg)
178
+ end
179
+ end
180
+
181
+ class PaymentActionRequiredError < PaymentError
182
+ extend T::Sig
183
+
184
+ sig { params(reason: T.untyped).void }
185
+ def initialize(reason: nil)
186
+ msg = reason ? "Payment requires action: #{reason}." : "Payment requires action."
187
+ super(msg)
188
+ end
189
+ end
190
+ end
@@ -0,0 +1,60 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ require "time"
5
+
6
+ module Mpp
7
+ module Expires
8
+ extend T::Sig
9
+
10
+ module_function
11
+
12
+ # Format a Time as ISO 8601 with Z suffix and millisecond precision.
13
+ sig { params(time: Time).returns(String) }
14
+ def to_iso(time)
15
+ time.utc.strftime("%Y-%m-%dT%H:%M:%S.%LZ")
16
+ end
17
+
18
+ # Returns an ISO 8601 datetime string n seconds from now.
19
+ sig { params(n: BasicObject).returns(T.untyped) }
20
+ def seconds(n)
21
+ to_iso(Time.now.utc + n)
22
+ end
23
+
24
+ # Returns an ISO 8601 datetime string n minutes from now.
25
+ sig { params(n: Numeric).returns(String) }
26
+ def minutes(n)
27
+ to_iso(Time.now.utc + (n * 60))
28
+ end
29
+
30
+ # Returns an ISO 8601 datetime string n hours from now.
31
+ sig { params(n: Numeric).returns(String) }
32
+ def hours(n)
33
+ to_iso(Time.now.utc + (n * 3600))
34
+ end
35
+
36
+ # Returns an ISO 8601 datetime string n days from now.
37
+ sig { params(n: Numeric).returns(String) }
38
+ def days(n)
39
+ to_iso(Time.now.utc + (n * 86_400))
40
+ end
41
+
42
+ # Returns an ISO 8601 datetime string n weeks from now.
43
+ sig { params(n: Numeric).returns(String) }
44
+ def weeks(n)
45
+ to_iso(Time.now.utc + (n * 7 * 86_400))
46
+ end
47
+
48
+ # Returns an ISO 8601 datetime string n months (30 days) from now.
49
+ sig { params(n: Numeric).returns(String) }
50
+ def months(n)
51
+ to_iso(Time.now.utc + (n * 30 * 86_400))
52
+ end
53
+
54
+ # Returns an ISO 8601 datetime string n years (365 days) from now.
55
+ sig { params(n: Numeric).returns(String) }
56
+ def years(n)
57
+ to_iso(Time.now.utc + (n * 365 * 86_400))
58
+ end
59
+ end
60
+ end
@@ -0,0 +1,23 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ module Mpp
5
+ module Extensions
6
+ module MCP
7
+ extend T::Sig
8
+
9
+ module_function
10
+
11
+ # Build payment capabilities object for MCP.
12
+ sig { params(methods: T.untyped, intents: T.untyped).returns(T::Hash[T.untyped, T.untyped]) }
13
+ def payment_capabilities(methods, intents)
14
+ {
15
+ "payment" => {
16
+ "methods" => methods,
17
+ "intents" => intents
18
+ }
19
+ }
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,17 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ module Mpp
5
+ module Extensions
6
+ module MCP
7
+ META_CREDENTIAL = "org.paymentauth/credential"
8
+ META_RECEIPT = "org.paymentauth/receipt"
9
+
10
+ CODE_PAYMENT_REQUIRED = -32_042
11
+ CODE_PAYMENT_VERIFICATION_FAILED = -32_043
12
+ CODE_MALFORMED_CREDENTIAL = -32_602
13
+
14
+ HTTP_STATUS_PAYMENT_REQUIRED = 402
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,44 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ module Mpp
5
+ module Extensions
6
+ module MCP
7
+ extend T::Sig
8
+
9
+ module_function
10
+
11
+ # Wrapper for MCP tool handlers with payment verification.
12
+ #
13
+ # Usage:
14
+ # result = Mpp::Extensions::MCP.pay(mpp_handler, meta: params["_meta"],
15
+ # request: { "amount" => "1000" }, realm: "api.example.com") do |credential, receipt|
16
+ # # execute tool
17
+ # end
18
+ sig { params(intent: T.untyped, request: T.untyped, meta: T.untyped, realm: T.nilable(String), secret_key: T.nilable(String), method: T.nilable(String), expires_in: Integer, description: T.nilable(String), blk: T.untyped).returns(T.untyped) }
19
+ def pay_tool(intent:, request:, meta:, realm: nil, secret_key: nil,
20
+ method: nil, expires_in: DEFAULT_CHALLENGE_TTL, description: nil, &blk)
21
+ resolved_realm = realm || Mpp::Server::Defaults.detect_realm
22
+ resolved_secret_key = secret_key || Mpp::Server::Defaults.detect_secret_key
23
+
24
+ request_params = request.respond_to?(:call) ? request.call : request
25
+
26
+ result = verify_or_challenge(
27
+ meta: meta,
28
+ intent: intent,
29
+ request: request_params,
30
+ realm: resolved_realm,
31
+ secret_key: resolved_secret_key,
32
+ method: method,
33
+ expires_in: expires_in,
34
+ description: description
35
+ )
36
+
37
+ Kernel.raise PaymentRequiredError.new(challenges: [result]) if result.is_a?(MCPChallenge)
38
+
39
+ credential, receipt = result
40
+ yield credential, receipt
41
+ end
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,110 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ module Mpp
5
+ module Extensions
6
+ module MCP
7
+ class PaymentRequiredError < StandardError
8
+ extend T::Sig
9
+
10
+ sig { returns(T.untyped) }
11
+ attr_reader :challenges
12
+
13
+ sig { returns(Integer) }
14
+ attr_reader :code
15
+
16
+ sig { params(challenges: T.untyped, message: BasicObject).void }
17
+ def initialize(challenges:, message: "Payment Required")
18
+ @challenges = T.let(challenges, T.untyped)
19
+ @code = T.let(CODE_PAYMENT_REQUIRED, Integer)
20
+ super(message)
21
+ end
22
+
23
+ sig { returns(T::Hash[T.untyped, T.untyped]) }
24
+ def to_jsonrpc_error
25
+ {
26
+ "code" => CODE_PAYMENT_REQUIRED,
27
+ "message" => message,
28
+ "data" => {
29
+ "httpStatus" => HTTP_STATUS_PAYMENT_REQUIRED,
30
+ "challenges" => @challenges.map(&:to_dict)
31
+ }
32
+ }
33
+ end
34
+ end
35
+
36
+ class PaymentVerificationError < StandardError
37
+ extend T::Sig
38
+
39
+ sig { returns(T.untyped) }
40
+ attr_reader :challenges
41
+
42
+ sig { returns(T.untyped) }
43
+ attr_reader :reason
44
+
45
+ sig { returns(T.untyped) }
46
+ attr_reader :detail
47
+
48
+ sig { returns(Integer) }
49
+ attr_reader :code
50
+
51
+ sig { params(challenges: T.untyped, reason: T.untyped, detail: T.untyped, message: BasicObject).void }
52
+ def initialize(challenges:, reason: nil, detail: nil, message: "Payment Verification Failed")
53
+ @challenges = T.let(challenges, T.untyped)
54
+ @reason = T.let(reason, T.untyped)
55
+ @detail = T.let(detail, T.untyped)
56
+ @code = T.let(CODE_PAYMENT_VERIFICATION_FAILED, Integer)
57
+ super(message)
58
+ end
59
+
60
+ sig { returns(T::Hash[T.untyped, T.untyped]) }
61
+ def to_jsonrpc_error
62
+ data = {
63
+ "httpStatus" => HTTP_STATUS_PAYMENT_REQUIRED,
64
+ "challenges" => @challenges.map(&:to_dict)
65
+ }
66
+ if @reason || @detail
67
+ failure = {}
68
+ failure["reason"] = @reason if @reason
69
+ failure["detail"] = @detail if @detail
70
+ data["failure"] = failure
71
+ end
72
+ {
73
+ "code" => CODE_PAYMENT_VERIFICATION_FAILED,
74
+ "message" => message,
75
+ "data" => data
76
+ }
77
+ end
78
+ end
79
+
80
+ class MalformedCredentialError < StandardError
81
+ extend T::Sig
82
+
83
+ sig { returns(T.untyped) }
84
+ attr_reader :detail
85
+
86
+ sig { returns(Integer) }
87
+ attr_reader :code
88
+
89
+ sig { params(detail: T.untyped, message: BasicObject).void }
90
+ def initialize(detail:, message: "Invalid params")
91
+ @detail = T.let(detail, T.untyped)
92
+ @code = T.let(CODE_MALFORMED_CREDENTIAL, Integer)
93
+ super(message)
94
+ end
95
+
96
+ sig { returns(T::Hash[T.untyped, T.untyped]) }
97
+ def to_jsonrpc_error
98
+ {
99
+ "code" => CODE_MALFORMED_CREDENTIAL,
100
+ "message" => message,
101
+ "data" => {
102
+ "httpStatus" => HTTP_STATUS_PAYMENT_REQUIRED,
103
+ "detail" => @detail
104
+ }
105
+ }
106
+ end
107
+ end
108
+ end
109
+ end
110
+ end
@@ -0,0 +1,205 @@
1
+ # typed: true
2
+ # frozen_string_literal: true
3
+
4
+ require "base64"
5
+ require "json"
6
+ require "time"
7
+
8
+ module Mpp
9
+ module Extensions
10
+ module MCP
11
+ MCPChallenge = Data.define(:id, :realm, :method, :intent, :request,
12
+ :expires, :description, :digest, :opaque) do
13
+ def initialize(id:, realm:, method:, intent:, request:,
14
+ expires: nil, description: nil, digest: nil, opaque: nil)
15
+ super
16
+ end
17
+
18
+ def to_dict
19
+ result = {
20
+ "id" => id,
21
+ "realm" => realm,
22
+ "method" => method,
23
+ "intent" => intent,
24
+ "request" => request
25
+ }
26
+ result["expires"] = expires if expires
27
+ result["description"] = description if description
28
+ result["digest"] = digest if digest
29
+ result["opaque"] = opaque if opaque
30
+ result
31
+ end
32
+
33
+ def self.from_dict(data)
34
+ new(
35
+ id: data["id"],
36
+ realm: data["realm"],
37
+ method: data["method"],
38
+ intent: data["intent"],
39
+ request: data["request"],
40
+ expires: data["expires"],
41
+ description: data["description"],
42
+ digest: data["digest"],
43
+ opaque: data["opaque"]
44
+ )
45
+ end
46
+
47
+ def to_core
48
+ Mpp::Challenge.new(
49
+ id: id,
50
+ method: method,
51
+ intent: intent,
52
+ request: request,
53
+ digest: digest,
54
+ opaque: opaque
55
+ )
56
+ end
57
+
58
+ def self.from_core(challenge, realm, expires: nil, description: nil)
59
+ new(
60
+ id: challenge.id,
61
+ realm: realm,
62
+ method: challenge.method,
63
+ intent: challenge.intent,
64
+ request: challenge.request,
65
+ expires: expires,
66
+ description: description,
67
+ digest: challenge.digest,
68
+ opaque: challenge.opaque
69
+ )
70
+ end
71
+ end
72
+
73
+ MCPCredential = Data.define(:challenge, :payload, :source) do
74
+ def initialize(challenge:, payload:, source: nil)
75
+ super
76
+ end
77
+
78
+ def to_dict
79
+ result = {
80
+ "challenge" => challenge.to_dict,
81
+ "payload" => payload
82
+ }
83
+ result["source"] = source if source
84
+ result
85
+ end
86
+
87
+ def to_meta
88
+ {META_CREDENTIAL => to_dict}
89
+ end
90
+
91
+ def self.from_dict(data)
92
+ new(
93
+ challenge: MCPChallenge.from_dict(data["challenge"]),
94
+ payload: data["payload"],
95
+ source: data["source"]
96
+ )
97
+ end
98
+
99
+ def self.from_meta(meta)
100
+ return nil unless meta.key?(META_CREDENTIAL)
101
+
102
+ from_dict(meta[META_CREDENTIAL])
103
+ end
104
+
105
+ def to_core
106
+ request_json = Mpp::Json.compact_encode(challenge.request)
107
+ request_b64 = Base64.urlsafe_encode64(request_json, padding: false)
108
+
109
+ opaque_b64 = nil
110
+ if challenge.opaque
111
+ opaque_json = Mpp::Json.compact_encode(challenge.opaque)
112
+ opaque_b64 = Base64.urlsafe_encode64(opaque_json, padding: false)
113
+ end
114
+
115
+ echo = Mpp::ChallengeEcho.new(
116
+ id: challenge.id,
117
+ realm: challenge.realm,
118
+ method: challenge.method,
119
+ intent: challenge.intent,
120
+ request: request_b64,
121
+ expires: challenge.expires,
122
+ digest: challenge.digest,
123
+ opaque: opaque_b64
124
+ )
125
+ Mpp::Credential.new(
126
+ challenge: echo,
127
+ payload: payload,
128
+ source: source
129
+ )
130
+ end
131
+
132
+ def self.from_core(credential, challenge)
133
+ new(
134
+ challenge: challenge,
135
+ payload: credential.payload,
136
+ source: credential.source
137
+ )
138
+ end
139
+ end
140
+
141
+ MCPReceipt = Data.define(:status, :challenge_id, :method, :timestamp,
142
+ :reference, :settlement) do
143
+ def initialize(status:, challenge_id:, method:, timestamp:,
144
+ reference: nil, settlement: nil)
145
+ super
146
+ end
147
+
148
+ def to_dict
149
+ result = {
150
+ "status" => status,
151
+ "challengeId" => challenge_id,
152
+ "method" => method,
153
+ "timestamp" => timestamp
154
+ }
155
+ result["reference"] = reference if reference
156
+ result["settlement"] = settlement if settlement
157
+ result
158
+ end
159
+
160
+ def to_meta
161
+ {META_RECEIPT => to_dict}
162
+ end
163
+
164
+ def self.from_dict(data)
165
+ new(
166
+ status: data["status"],
167
+ challenge_id: data["challengeId"],
168
+ method: data["method"],
169
+ timestamp: data["timestamp"],
170
+ reference: data["reference"],
171
+ settlement: data["settlement"]
172
+ )
173
+ end
174
+
175
+ def self.from_meta(meta)
176
+ return nil unless meta.key?(META_RECEIPT)
177
+
178
+ from_dict(meta[META_RECEIPT])
179
+ end
180
+
181
+ def to_core
182
+ Mpp::Receipt.new(
183
+ status: status,
184
+ timestamp: Time.iso8601(timestamp.gsub("Z", "+00:00")),
185
+ reference: reference || ""
186
+ )
187
+ end
188
+
189
+ def self.from_core(receipt, challenge_id:, method:, settlement: nil)
190
+ ts = receipt.timestamp.utc.iso8601
191
+ ts = ts.sub(/\+00:00$/, "Z")
192
+
193
+ new(
194
+ status: receipt.status,
195
+ challenge_id: challenge_id,
196
+ method: method,
197
+ timestamp: ts,
198
+ reference: receipt.reference.empty? ? nil : receipt.reference,
199
+ settlement: settlement
200
+ )
201
+ end
202
+ end
203
+ end
204
+ end
205
+ end