From 41c68500b0284bf11a86f9ab3225b4becefea116 Mon Sep 17 00:00:00 2001 From: Julien Bourdeau Date: Tue, 18 Feb 2025 15:57:13 -0800 Subject: [PATCH] feat(api): Introduce new error --- app/controllers/concerns/api_errors.rb | 19 ++++++++-- app/services/base_service.rb | 14 ++++++++ .../stripe_service.rb | 17 ++++++++- .../api/v1/customers_controller_spec.rb | 35 +++++++++++++++++-- .../stripe_service_spec.rb | 33 ++++++++++++----- 5 files changed, 103 insertions(+), 15 deletions(-) diff --git a/app/controllers/concerns/api_errors.rb b/app/controllers/concerns/api_errors.rb index b853e161e4e..7b13cad99df 100644 --- a/app/controllers/concerns/api_errors.rb +++ b/app/controllers/concerns/api_errors.rb @@ -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, @@ -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 diff --git a/app/services/base_service.rb b/app/services/base_service.rb index ea0db751aa9..efcc7fddf77 100644 --- a/app/services/base_service.rb +++ b/app/services/base_service.rb @@ -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 diff --git a/app/services/payment_provider_customers/stripe_service.rb b/app/services/payment_provider_customers/stripe_service.rb index c1bd203161f..e83f5ae2399 100644 --- a/app/services/payment_provider_customers/stripe_service.rb +++ b/app/services/payment_provider_customers/stripe_service.rb @@ -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) @@ -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) diff --git a/spec/requests/api/v1/customers_controller_spec.rb b/spec/requests/api/v1/customers_controller_spec.rb index 61751373f5d..24dd5399dfa 100644 --- a/spec/requests/api/v1/customers_controller_spec.rb +++ b/spec/requests/api/v1/customers_controller_spec.rb @@ -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( @@ -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" @@ -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 diff --git a/spec/services/payment_provider_customers/stripe_service_spec.rb b/spec/services/payment_provider_customers/stripe_service_spec.rb index 5c0c9b21484..ca8aed3e789 100644 --- a/spec/services/payment_provider_customers/stripe_service_spec.rb +++ b/spec/services/payment_provider_customers/stripe_service_spec.rb @@ -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 } @@ -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", @@ -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