Class: Mpp::Client::Transport

Inherits:
Object
  • Object
show all
Extended by:
T::Sig
Defined in:
lib/mpp/client/transport.rb

Overview

Payment-aware HTTP client that handles 402 Payment Required responses.

Wraps Net::HTTP and automatically:

  1. Detects 402 responses with WWW-Authenticate: Payment headers

  2. Parses the challenge and finds a matching payment method

  3. Creates credentials and retries the request

  4. Returns the final response

Instance Method Summary collapse

Constructor Details

#initialize(methods:, events: nil) ⇒ Transport

Returns a new instance of Transport.



22
23
24
25
# File 'lib/mpp/client/transport.rb', line 22

def initialize(methods:, events: nil)
  @methods = T.let(methods.to_h { |m| [m.name, m] }, T::Hash[String, T.untyped])
  @events = T.let(events || Mpp::Events.client_dispatcher, Mpp::Events::Dispatcher)
end

Instance Method Details

#delete(url, **kwargs) ⇒ Object



178
179
180
# File 'lib/mpp/client/transport.rb', line 178

def delete(url, **kwargs)
  request("DELETE", url, **kwargs)
end

#get(url, **kwargs) ⇒ Object



163
164
165
# File 'lib/mpp/client/transport.rb', line 163

def get(url, **kwargs)
  request("GET", url, **kwargs)
end

#on(name, handler = nil, &block) ⇒ Object



28
29
30
# File 'lib/mpp/client/transport.rb', line 28

def on(name, handler = nil, &block)
  @events.on(name, handler, &block)
end

#on_challenge_received(handler = nil, &block) ⇒ Object



33
34
35
# File 'lib/mpp/client/transport.rb', line 33

def on_challenge_received(handler = nil, &block)
  on(Mpp::Events::CHALLENGE_RECEIVED, handler, &block)
end

#on_credential_created(handler = nil, &block) ⇒ Object



38
39
40
# File 'lib/mpp/client/transport.rb', line 38

def on_credential_created(handler = nil, &block)
  on(Mpp::Events::CREDENTIAL_CREATED, handler, &block)
end

#on_payment_failed(handler = nil, &block) ⇒ Object



43
44
45
# File 'lib/mpp/client/transport.rb', line 43

def on_payment_failed(handler = nil, &block)
  on(Mpp::Events::PAYMENT_FAILED, handler, &block)
end

#on_payment_response(handler = nil, &block) ⇒ Object



48
49
50
# File 'lib/mpp/client/transport.rb', line 48

def on_payment_response(handler = nil, &block)
  on(Mpp::Events::PAYMENT_RESPONSE, handler, &block)
end

#post(url, **kwargs) ⇒ Object



168
169
170
# File 'lib/mpp/client/transport.rb', line 168

def post(url, **kwargs)
  request("POST", url, **kwargs)
end

#put(url, **kwargs) ⇒ Object



173
174
175
# File 'lib/mpp/client/transport.rb', line 173

def put(url, **kwargs)
  request("PUT", url, **kwargs)
end

#request(method, url, headers: {}, body: nil) ⇒ Object



55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
# File 'lib/mpp/client/transport.rb', line 55

def request(method, url, headers: {}, body: nil)
  uri = URI(url)
  response = send_request(uri, method, headers, body)

  return response unless response.code.to_i == 402

  # Parse WWW-Authenticate headers
  www_auth_headers = response.get_fields("www-authenticate") || []
  challenge, matched_method = find_matching_challenge(www_auth_headers, input: url, response: response)
  return response unless challenge && matched_method

  # Check expiry before paying (client-side guardrail)
  if challenge.expires
    begin
      expires_dt = Time.iso8601(challenge.expires.gsub("Z", "+00:00"))
      return response if expires_dt < Time.now.utc
    rescue ArgumentError
      # If we can't parse, let server validate
    end
  end

  auth_header = T.let(nil, T.nilable(String))
  create_credential = Kernel.lambda do
    auth_header ||= credential_authorization(matched_method.create_credential(challenge))
  end

  begin
    event_credential = nil
    if @events.has_handlers?(Mpp::Events::CHALLENGE_RECEIVED)
      # challenge.received can override credential creation; first non-empty credential wins.
      event_credential = @events.emit_first(Mpp::Events::CHALLENGE_RECEIVED, {
        challenge: challenge,
        challenges: [challenge],
        create_credential: create_credential,
        input: url,
        method: matched_method,
        response: response
      })
    end
    auth_header = credential_authorization(event_credential) unless event_credential.nil?
    auth_header ||= create_credential.call

    if @events.has_handlers?(Mpp::Events::CREDENTIAL_CREATED)
      @events.emit(Mpp::Events::CREDENTIAL_CREATED, {
        challenge: challenge,
        credential: auth_header,
        input: url,
        method: matched_method,
        response: response
      })
    end
  rescue => e
    if @events.has_handlers?(Mpp::Events::PAYMENT_FAILED)
      @events.emit(Mpp::Events::PAYMENT_FAILED, {
        challenge: challenge,
        challenges: [challenge],
        error: e,
        input: url,
        method: matched_method,
        response: response
      })
    end
    raise
  end

  retry_headers = headers.merge("Authorization" => auth_header)
  payment_response = nil
  begin
    payment_response = send_request(uri, method, retry_headers, body)
  rescue => e
    if @events.has_handlers?(Mpp::Events::PAYMENT_FAILED)
      @events.emit(Mpp::Events::PAYMENT_FAILED, {
        challenge: challenge,
        challenges: [challenge],
        credential: auth_header,
        error: e,
        input: url,
        method: matched_method,
        response: response
      })
    end
    raise
  end

  if payment_response.code.to_i.between?(200, 299) && @events.has_handlers?(Mpp::Events::PAYMENT_RESPONSE)
    @events.emit(Mpp::Events::PAYMENT_RESPONSE, {
      challenge: challenge,
      credential: auth_header,
      input: url,
      method: matched_method,
      response: payment_response
    })
  elsif @events.has_handlers?(Mpp::Events::PAYMENT_FAILED)
    @events.emit(Mpp::Events::PAYMENT_FAILED, {
      challenge: challenge,
      challenges: [challenge],
      credential: auth_header,
      error: Mpp::VerificationFailedError.new(reason: "retry returned HTTP #{payment_response.code}"),
      input: url,
      method: matched_method,
      response: payment_response
    })
  end

  payment_response
end