From 4f4087817ccfad53e803c3c1ae56f499a0c50eaf Mon Sep 17 00:00:00 2001
From: Vincent Pochet <vincent@getlago.com>
Date: Fri, 10 Jan 2025 14:15:15 +0100
Subject: [PATCH] fix(cashfree): Improve thirdparty provider error handling

---
 app/controllers/concerns/api_errors.rb        | 16 +++++++++
 app/services/base_service.rb                  | 15 ++++++++
 .../invoices/payments/cashfree_service.rb     |  3 +-
 .../payments/generate_payment_url_service.rb  |  1 +
 .../payments/cashfree_service_spec.rb         | 33 +++++++++++++++++-
 .../generate_payment_url_service_spec.rb      | 34 +++++++++++++++++++
 6 files changed, 100 insertions(+), 2 deletions(-)

diff --git a/app/controllers/concerns/api_errors.rb b/app/controllers/concerns/api_errors.rb
index 102245148bb..b0f9463eff7 100644
--- a/app/controllers/concerns/api_errors.rb
+++ b/app/controllers/concerns/api_errors.rb
@@ -57,6 +57,20 @@ def method_not_allowed_error(code:)
     )
   end
 
+  def thirdpary_error(error:)
+    render(
+      json: {
+        status: 422,
+        error: 'Unprocessable Entity',
+        code: 'third_party_error',
+        error_details: {
+          third_party: error.third_party,
+          thirdparty_error: error.error_message
+        }
+      }
+    )
+  end
+
   def render_error_response(error_result)
     case error_result.error
     when BaseService::NotFoundFailure
@@ -69,6 +83,8 @@ 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::ThirdPartyFailure
+      thirdpary_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 44a7de04999..33a3756e148 100644
--- a/app/services/base_service.rb
+++ b/app/services/base_service.rb
@@ -93,6 +93,17 @@ def initialize(result, message:)
     end
   end
 
+  class ThirdPartyFailure < FailedResult
+    attr_reader :third_party, :error_message
+
+    def initialize(result, third_party:, error_message:)
+      @third_party = third_party
+      @error_message = error_message
+
+      super(result, "#{third_party}: #{error_message}")
+    end
+  end
+
   class Result < OpenStruct
     attr_reader :error
 
@@ -150,6 +161,10 @@ def unauthorized_failure!(message: "unauthorized")
       fail_with_error!(UnauthorizedFailure.new(self, message:))
     end
 
+    def third_party_failure!(third_party:, error_message:)
+      fail_with_error!(ThirdPartyFailure.new(self, third_party:, error_message:))
+    end
+
     def raise_if_error!
       return self if success?
 
diff --git a/app/services/invoices/payments/cashfree_service.rb b/app/services/invoices/payments/cashfree_service.rb
index c7d9331a727..09982b65566 100644
--- a/app/services/invoices/payments/cashfree_service.rb
+++ b/app/services/invoices/payments/cashfree_service.rb
@@ -44,7 +44,8 @@ def generate_payment_url
         result
       rescue LagoHttpClient::HttpError => e
         deliver_error_webhook(e)
-        result.service_failure!(code: e.error_code, message: e.error_body)
+
+        result.third_party_failure!(third_party: "Cashfree", error_message: e.error_body)
       end
 
       private
diff --git a/app/services/invoices/payments/generate_payment_url_service.rb b/app/services/invoices/payments/generate_payment_url_service.rb
index 03abd0d08bd..bd4d7254cc2 100644
--- a/app/services/invoices/payments/generate_payment_url_service.rb
+++ b/app/services/invoices/payments/generate_payment_url_service.rb
@@ -22,6 +22,7 @@ def call
         payment_url_result = Invoices::Payments::PaymentProviders::Factory.new_instance(invoice:).generate_payment_url
 
         return payment_url_result unless payment_url_result.success?
+        return payment_url_result if payment_url_result.error.is_a?(BaseService::ThirdPartyFailure)
 
         if payment_url_result.payment_url.blank?
           return result.single_validation_failure!(error_code: 'payment_provider_error')
diff --git a/spec/services/invoices/payments/cashfree_service_spec.rb b/spec/services/invoices/payments/cashfree_service_spec.rb
index 07c45ef7cc4..dd9706e4e39 100644
--- a/spec/services/invoices/payments/cashfree_service_spec.rb
+++ b/spec/services/invoices/payments/cashfree_service_spec.rb
@@ -168,6 +168,7 @@
 
   describe ".generate_payment_url" do
     let(:payment_links_response) { Net::HTTPResponse.new("1.0", "200", "OK") }
+    let(:payment_links_body) { {link_url: "https://payments-test.cashfree.com/links//U1mgll3c0e9g"}.to_json }
 
     before do
       cashfree_payment_provider
@@ -178,7 +179,7 @@
       allow(cashfree_client).to receive(:post_with_response)
         .and_return(payment_links_response)
       allow(payment_links_response).to receive(:body)
-        .and_return({link_url: "https://payments-test.cashfree.com/links//U1mgll3c0e9g"}.to_json)
+        .and_return(payment_links_body)
     end
 
     it "generates payment url" do
@@ -206,5 +207,35 @@
         expect(result.payment_url).to be_nil
       end
     end
+
+    context 'when payment url failed to generate' do
+      let(:payment_links_response) { Net::HTTPResponse.new("1.0", "400", "Bad Request") }
+      let(:payment_links_body) do
+        {
+          message: "Currency USD is not enabled",
+          code: "link_post_failed",
+          type: "invalid_request_error"
+        }.to_json
+      end
+
+      before do
+        cashfree_payment_provider
+        cashfree_customer
+
+        allow(LagoHttpClient::Client).to receive(:new)
+          .and_return(cashfree_client)
+        allow(cashfree_client).to receive(:post_with_response)
+          .and_raise(::LagoHttpClient::HttpError.new(payment_links_response.code, payment_links_body, nil))
+      end
+
+      it 'returns a third party error' do
+        result = cashfree_service.generate_payment_url
+
+        expect(result).not_to be_success
+        expect(result.error).to be_a(BaseService::ThirdPartyFailure)
+        expect(result.error.third_party).to eq('Cashfree')
+        expect(result.error.error_message).to eq(payment_links_body)
+      end
+    end
   end
 end
diff --git a/spec/services/invoices/payments/generate_payment_url_service_spec.rb b/spec/services/invoices/payments/generate_payment_url_service_spec.rb
index 7d26d8f26c6..d47a9d726be 100644
--- a/spec/services/invoices/payments/generate_payment_url_service_spec.rb
+++ b/spec/services/invoices/payments/generate_payment_url_service_spec.rb
@@ -87,5 +87,39 @@
         end
       end
     end
+
+    context 'when provider service return a third party error' do
+      let(:payment_provider) { 'cashfree' }
+      let(:code) { 'cashfree_1' }
+
+      let(:payment_provider_service) { instance_double(PaymentRequests::Payments::CashfreeService) }
+
+      let(:error_result) do
+        BaseService::Result.new.tap do |result|
+          result.fail_with_error!(
+            BaseService::ThirdPartyFailure.new(
+              result,
+              third_party: 'Cashfree',
+              error_message: '{"code: "link_post_failed", "type": "invalid_request_error"}'
+            )
+          )
+        end
+      end
+
+      before do
+        allow(PaymentRequests::Payments::CashfreeService)
+          .to receive(:new)
+          .and_return(payment_provider_service)
+
+        allow(payment_provider_service).to receive(:generate_payment_url)
+          .and_return(error_result)
+      end
+
+      it 'returns a third party error' do
+        result = generate_payment_url_service.call
+
+        expect(result).to eq(error_result)
+      end
+    end
   end
 end