diff --git a/app/services/payment_requests/payments/adyen_service.rb b/app/services/payment_requests/payments/adyen_service.rb index eda8ab6f4b2..e06cbcc5e7b 100644 --- a/app/services/payment_requests/payments/adyen_service.rb +++ b/app/services/payment_requests/payments/adyen_service.rb @@ -39,8 +39,8 @@ def create payment_provider_customer_id: customer.adyen_customer.id, amount_cents: payable.total_amount_cents, amount_currency: payable.currency.upcase, - provider_payment_id: res.response['pspReference'], - status: res.response['resultCode'] + provider_payment_id: res.response["pspReference"], + status: res.response["resultCode"] ) ActiveRecord::Base.transaction do @@ -67,7 +67,7 @@ def generate_payment_url adyen_success, adyen_error = handle_adyen_response(result_url) return result.service_failure!(code: adyen_error.code, message: adyen_error.msg) unless adyen_success - result.payment_url = result_url.response['url'] + result.payment_url = result_url.response["url"] result rescue Adyen::AdyenError => e @@ -76,6 +76,29 @@ def generate_payment_url result.service_failure!(code: e.code, message: e.msg) end + def update_payment_status(provider_payment_id:, status:, metadata: {}) + payment = if metadata[:payment_type] == "one-time" + create_payment(provider_payment_id:, metadata:) + else + Payment.find_by(provider_payment_id:) + end + return result.not_found_failure!(resource: "adyen_payment") unless payment + + result.payment = payment + result.payable = payment.payable + return result if payment.payable.payment_succeeded? + + payment.update!(status:) + + payable_payment_status = payable_payment_status(status) + update_payable_payment_status(payment_status: payable_payment_status) + update_invoices_payment_status(payment_status: payable_payment_status) + + result + rescue BaseService::FailedResult => e + result.fail_with_error!(e) + end + private attr_accessor :payable @@ -106,7 +129,7 @@ def update_payment_method_id Lago::Adyen::Params.new(payment_method_params).to_h ).response - payment_method_id = result['storedPaymentMethods']&.first&.dig('id') + payment_method_id = result["storedPaymentMethods"]&.first&.dig("id") customer.adyen_customer.update!(payment_method_id:) if payment_method_id end @@ -139,13 +162,13 @@ def payment_params }, reference: "Overdue invoices", paymentMethod: { - type: 'scheme', + type: "scheme", storedPaymentMethodId: customer.adyen_customer.payment_method_id }, shopperReference: customer.adyen_customer.provider_customer_id, merchantAccount: adyen_payment_provider.merchant_account, - shopperInteraction: 'ContAuth', - recurringProcessingModel: 'UnscheduledCardOnFile' + shopperInteraction: "ContAuth", + recurringProcessingModel: "UnscheduledCardOnFile" } prms[:shopperEmail] = customer.email if customer.email prms @@ -161,14 +184,14 @@ def payment_url_params merchantAccount: adyen_payment_provider.merchant_account, returnUrl: success_redirect_url, shopperReference: customer.external_id, - storePaymentMethodMode: 'enabled', - recurringProcessingModel: 'UnscheduledCardOnFile', + storePaymentMethodMode: "enabled", + recurringProcessingModel: "UnscheduledCardOnFile", expiresAt: Time.current + 70.days, # max link TTL metadata: { lago_customer_id: customer.id, lago_payment_request_id: payable.id, lago_invoice_ids: payable.invoice_ids, - payment_type: 'one-time' + payment_type: "one-time" } } prms[:shopperEmail] = customer.email if customer.email @@ -188,10 +211,14 @@ def payable_payment_status(payment_status) end def update_payable_payment_status(payment_status:, deliver_webhook: true) - payable.update!( - payment_status:, - ready_for_payment_processing: payment_status.to_sym != :succeeded - ) + UpdateService.call( + payable: result.payable, + params: { + payment_status:, + ready_for_payment_processing: payment_status.to_sym != :succeeded + }, + webhook_notification: deliver_webhook + ).raise_if_error! end def update_invoices_payment_status(payment_status:, deliver_webhook: true) @@ -207,6 +234,21 @@ def update_invoices_payment_status(payment_status:, deliver_webhook: true) end end + def create_payment(provider_payment_id:, metadata:) + @payable = PaymentRequest.find(metadata[:lago_payment_request_id]) + + payable.increment_payment_attempts! + + Payment.new( + payable:, + payment_provider_id: adyen_payment_provider.id, + payment_provider_customer_id: customer.adyen_customer.id, + amount_cents: payable.total_amount_cents, + amount_currency: payable.currency.upcase, + provider_payment_id: + ) + end + def deliver_error_webhook(adyen_error) DeliverErrorWebhookService.call_async(payable, { provider_customer_id: customer.adyen_customer.provider_customer_id, diff --git a/spec/services/payment_requests/payments/adyen_service_spec.rb b/spec/services/payment_requests/payments/adyen_service_spec.rb index 4c269741c36..92b6c7e42ee 100644 --- a/spec/services/payment_requests/payments/adyen_service_spec.rb +++ b/spec/services/payment_requests/payments/adyen_service_spec.rb @@ -359,4 +359,143 @@ expect(payment_method_params).to eq(params) end end + + describe "#update_payment_status" do + let(:payment) do + create( + :payment, + payable: payment_request, + provider_payment_id:, + status: "Pending" + ) + end + + let(:provider_payment_id) { "ch_123456" } + + before do + allow(SendWebhookJob).to receive(:perform_later) + allow(SegmentTrackJob).to receive(:perform_later) + payment + end + + it "updates the payment, payment_request and invoices payment_status", :aggregate_failures do + result = adyen_service.update_payment_status( + provider_payment_id:, + status: "Authorised" + ) + + expect(result).to be_success + expect(result.payment.status).to eq("Authorised") + + expect(result.payable.reload).to be_payment_succeeded + expect(result.payable.ready_for_payment_processing).to eq(false) + + expect(invoice_1.reload).to be_payment_succeeded + expect(invoice_1.ready_for_payment_processing).to eq(false) + expect(invoice_2.reload).to be_payment_succeeded + expect(invoice_2.ready_for_payment_processing).to eq(false) + end + + context "when status is failed" do + it "updates the payment, payment_request and invoices status", :aggregate_failures do + result = adyen_service.update_payment_status( + provider_payment_id:, + status: "Refused" + ) + + expect(result).to be_success + expect(result.payment.status).to eq("Refused") + + expect(result.payable.reload).to be_payment_failed + expect(result.payable.ready_for_payment_processing).to eq(true) + + expect(invoice_1.reload).to be_payment_failed + expect(invoice_1.ready_for_payment_processing).to eq(true) + + expect(invoice_2.reload).to be_payment_failed + expect(invoice_2.ready_for_payment_processing).to eq(true) + end + end + + context "when payment_request and invoices is already payment_succeeded" do + before do + payment_request.payment_succeeded! + invoice_1.payment_succeeded! + invoice_2.payment_succeeded! + end + + it "does not update the status of invoices, payment_request and payment" do + expect { + adyen_service.update_payment_status( + provider_payment_id:, + status: %w[Authorised SentForSettle SettleScheduled Settled Refunded].sample + ) + }.to not_change { invoice_1.reload.payment_status } + .and not_change { invoice_2.reload.payment_status } + .and not_change { payment_request.reload.payment_status } + .and not_change { payment.reload.status } + + result = adyen_service.update_payment_status( + provider_payment_id:, + status: %w[Authorised SentForSettle SettleScheduled Settled Refunded].sample + ) + + expect(result).to be_success + end + end + + context "with invalid status" do + let(:status) { "invalid-status" } + + it "does not update the payment_status of payment_request, invoices and payment" do + expect { + adyen_service.update_payment_status(provider_payment_id:, status:) + }.to not_change { payment_request.reload.payment_status } + .and not_change { invoice_1.reload.payment_status } + .and not_change { invoice_2.reload.payment_status } + .and change { payment.reload.status }.to(status) + end + + it "returns an error", :aggregate_failures do + result = adyen_service.update_payment_status(provider_payment_id:, status:) + + expect(result).not_to be_success + expect(result.error).to be_a(BaseService::ValidationFailure) + expect(result.error.messages.keys).to include(:payment_status) + expect(result.error.messages[:payment_status]).to include("value_is_invalid") + end + end + + context "when payment is not found and it is one time payment" do + let(:payment) { nil } + + before do + adyen_payment_provider + adyen_customer + end + + it "creates a payment and updates payment request and invoices payment status", :aggregate_failures do + result = adyen_service.update_payment_status( + provider_payment_id:, + status: "succeeded", + metadata: { + lago_payment_request_id: payment_request.id, + payment_type: "one-time" + } + ) + + expect(result).to be_success + expect(result.payment.status).to eq("succeeded") + + expect(result.payable).to be_payment_succeeded + expect(result.payable.ready_for_payment_processing).to eq(false) + + expect(invoice_1.reload).to be_payment_succeeded + expect(invoice_1.ready_for_payment_processing).to eq(false) + + expect(invoice_2.reload).to be_payment_succeeded + expect(invoice_2.ready_for_payment_processing).to eq(false) + end + end + end end