Skip to content

Commit

Permalink
feat(api): Introduce new error
Browse files Browse the repository at this point in the history
  • Loading branch information
julienbourdeau committed Feb 19, 2025
1 parent 734fa0a commit 41c6850
Show file tree
Hide file tree
Showing 5 changed files with 103 additions and 15 deletions.
19 changes: 17 additions & 2 deletions app/controllers/concerns/api_errors.rb
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,20 @@ def method_not_allowed_error(code:)
)
end

def thirdpary_error(error:)
def payment_provider_error(error_result)
render(
json: {
status: error_result.status,
error: error_result.message,
payment_provider: error_result.payment_provider,
payment_provider_code: error_result.payment_provider_code,
details: error_result.details
},
status: error_result.status
)
end

def thirdparty_error(error:)
render(
json: {
status: 422,
Expand All @@ -83,8 +96,10 @@ def render_error_response(error_result)
forbidden_error(code: error_result.error.code)
when BaseService::UnauthorizedFailure
unauthorized_error(message: error_result.error.message)
when BaseService::PaymentProviderFailure
payment_provider_error(error_result.error)
when BaseService::ThirdPartyFailure
thirdpary_error(error: error_result.error)
thirdparty_error(error: error_result.error)
else
raise(error_result.error)
end
Expand Down
14 changes: 14 additions & 0 deletions app/services/base_service.rb
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,20 @@ def initialize(result, message:)
end
end

class PaymentProviderFailure < FailedResult
attr_reader :status, :details, :payment_provider, :payment_provider_code

def initialize(result, message:, status:, details:,
payment_provider:, payment_provider_code:)
@status = status
@details = details
@payment_provider = payment_provider
@payment_provider_code = payment_provider_code

super(result, "#{@payment_provider} (#{@payment_provider_code}): #{message}")
end
end

class ThirdPartyFailure < FailedResult
attr_reader :third_party, :error_message

Expand Down
17 changes: 16 additions & 1 deletion app/services/payment_provider_customers/stripe_service.rb
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ def generate_checkout_url(send_webhook: true)
result
rescue ::Stripe::InvalidRequestError, ::Stripe::PermissionError => e
deliver_error_webhook(e)
result
fail! e
rescue ::Stripe::AuthenticationError => e
deliver_error_webhook(e)

Expand Down Expand Up @@ -203,6 +203,21 @@ def deliver_error_webhook(stripe_error)
)
end

def fail!(stripe_error)
result.fail_with_error!(PaymentProviderFailure.new(
result,
message: stripe_error.message,
status: stripe_error.http_status,
payment_provider: stripe_payment_provider.name,
payment_provider_code: stripe_payment_provider.code,
details: {
request_id: stripe_error.request_id,
code: stripe_error.code,
error: stripe_error.error
}
))
end

def handle_missing_customer(organization_id, metadata)
# NOTE: Stripe customer was not created from lago
return result unless metadata&.key?(:lago_customer_id)
Expand Down
35 changes: 32 additions & 3 deletions spec/requests/api/v1/customers_controller_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -445,6 +445,14 @@
let(:organization) { create(:organization) }
let(:stripe_provider) { create(:stripe_provider, organization:) }
let(:customer) { create(:customer, organization:) }
let(:stripe_api_response) do
{
id: "cs_test_c1oxrHXtPSAZmMoKqRqrGej2s4tbkBiONQOhmu52URzM78tpLmhIID5MIr",
object: "checkout.session",
url: "https://checkout.stripe.com/c/pay/cs_test_c1oxrH"
}
end
let(:stripe_api_status) { 200 }

before do
create(
Expand All @@ -455,8 +463,7 @@

customer.update!(payment_provider: "stripe", payment_provider_code: stripe_provider.code)

allow(::Stripe::Checkout::Session).to receive(:create)
.and_return({"url" => "https://example.com"})
stub_request(:post, %r{/v1/checkout/sessions}).to_return(status: stripe_api_status, body: stripe_api_response.to_json)
end

include_examples "requires API permission", "customer", "write"
Expand All @@ -467,7 +474,29 @@
aggregate_failures do
expect(response).to have_http_status(:success)

expect(json[:customer][:checkout_url]).to eq("https://example.com")
expect(json[:customer][:checkout_url]).to eq("https://checkout.stripe.com/c/pay/cs_test_c1oxrH")
end
end

context "when Stripe returns an error" do
let(:stripe_api_status) { 400 }
let(:stripe_api_response) do
{
error: {
message: "The payment method `crypto` cannot be used in `setup` mode.",
request_log_url: "https://dashboard.stripe.com/test/logs/req_uOkDI6eikj7r51?t=1739911184",
type: "invalid_request_error"
}
}
end

it "returns a payment_provider error" do
subject
expect(response).to have_http_status(:bad_request)
body = JSON.parse(response.body)
expect(body.keys).to eq(%w[status error payment_provider payment_provider_code details])
expect(body["details"].keys.sort).to eq(%w[code error request_id].sort)
expect(body["details"]["error"]["message"]).to eq "The payment method `crypto` cannot be used in `setup` mode."
end
end
end
Expand Down
33 changes: 24 additions & 9 deletions spec/services/payment_provider_customers/stripe_service_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
subject(:stripe_service) { described_class.new(stripe_customer) }

let(:customer) { create(:customer, name: customer_name, organization:) }
let(:stripe_provider) { create(:stripe_provider) }
let(:stripe_provider) { create(:stripe_provider, code: "stripe_us") }
let(:organization) { stripe_provider.organization }
let(:customer_name) { nil }

Expand Down Expand Up @@ -478,17 +478,13 @@

before { allow(::Stripe::Checkout::Session).to receive(:create).and_raise(stripe_error) }

it "returns an error result" do
it do
result = described_class.new(stripe_customer).generate_checkout_url

aggregate_failures do
expect(result).not_to be_success
expect(result.error).to be_a(BaseService::UnauthorizedFailure)
expect(result.error.message).to eq("Stripe authentication failed. Expired API Key provided")
end
end
expect(result).not_to be_success
expect(result.error).to be_a(BaseService::UnauthorizedFailure)
expect(result.error.message).to eq("Stripe authentication failed. Expired API Key provided")

it "delivers an error webhook" do
expect { described_class.new(stripe_customer).generate_checkout_url }.to enqueue_job(SendWebhookJob)
.with(
"customer.payment_provider_error",
Expand All @@ -500,6 +496,25 @@
).on_queue(webhook_queue)
end
end

context "when stripe raises an invalid request error" do
before do
stub_request(:post, %r{/v1/checkout/sessions}).to_return(status: 400, body: {
error: {
message: "The payment method `crypto` cannot be used in `setup` mode.",
request_log_url: "https://dashboard.stripe.com/test/logs/req_uOkDI6eikj7r51?t=1739911184",
type: "invalid_request_error"
}
}.to_json)
end

it "returns an error result" do
result = stripe_service.generate_checkout_url
expect(result).not_to be_success
expect(result.error).to be_a(BaseService::PaymentProviderFailure)
expect(result.error.message).to eq("Stripe Account 1 (stripe_us): The payment method `crypto` cannot be used in `setup` mode.")
end
end
end

describe "#success_redirect_url" do
Expand Down

0 comments on commit 41c6850

Please sign in to comment.