diff --git a/app/jobs/invoices/payments/adyen_create_job.rb b/app/jobs/invoices/payments/adyen_create_job.rb index d20810833ec..575aaf8b5d5 100644 --- a/app/jobs/invoices/payments/adyen_create_job.rb +++ b/app/jobs/invoices/payments/adyen_create_job.rb @@ -10,7 +10,7 @@ class AdyenCreateJob < ApplicationJob retry_on Faraday::ConnectionFailed, wait: :polynomially_longer, attempts: 6 def perform(invoice) - # NOTE: Legacy job, kept only to avoid existing jobs + # NOTE: Legacy job, kept only to avoid failure with existing jobs Invoices::Payments::CreateService.call!(invoice:, payment_provider: :adyen) end diff --git a/app/jobs/invoices/payments/gocardless_create_job.rb b/app/jobs/invoices/payments/gocardless_create_job.rb index 70f821ea797..915cbf7a489 100644 --- a/app/jobs/invoices/payments/gocardless_create_job.rb +++ b/app/jobs/invoices/payments/gocardless_create_job.rb @@ -8,7 +8,7 @@ class GocardlessCreateJob < ApplicationJob unique :until_executed, on_conflict: :log def perform(invoice) - # NOTE: Legacy job, kept only to avoid existing jobs + # NOTE: Legacy job, kept only to avoid faileure with existing jobs Invoices::Payments::CreateService.call!(invoice:, payment_provider: :gocardless) end diff --git a/app/jobs/invoices/payments/stripe_create_job.rb b/app/jobs/invoices/payments/stripe_create_job.rb index 413755e5374..3f2cc462046 100644 --- a/app/jobs/invoices/payments/stripe_create_job.rb +++ b/app/jobs/invoices/payments/stripe_create_job.rb @@ -13,7 +13,7 @@ class StripeCreateJob < ApplicationJob retry_on Invoices::Payments::RateLimitError, wait: :polynomially_longer, attempts: 6 def perform(invoice) - # NOTE: Legacy job, kept only to avoid existing jobs + # NOTE: Legacy job, kept only to avoid faileure with existing jobs Invoices::Payments::CreateService.call!(invoice:, payment_provider: :stripe) end diff --git a/app/jobs/payment_requests/payments/adyen_create_job.rb b/app/jobs/payment_requests/payments/adyen_create_job.rb index 872e2034760..51c7220cbaf 100644 --- a/app/jobs/payment_requests/payments/adyen_create_job.rb +++ b/app/jobs/payment_requests/payments/adyen_create_job.rb @@ -10,11 +10,9 @@ class AdyenCreateJob < ApplicationJob retry_on Faraday::ConnectionFailed, wait: :polynomially_longer, attempts: 6 def perform(payable) - result = PaymentRequests::Payments::AdyenService.new(payable).create + # NOTE: Legacy job, kept only to avoid faileure with existing jobs - PaymentRequestMailer.with(payment_request: payable).requested.deliver_later if result.payable&.payment_failed? - - result.raise_if_error! + PaymentRequests::Payments::CreateService.call!(payable:, payment_provider: 'adyen') end end end diff --git a/app/jobs/payment_requests/payments/create_job.rb b/app/jobs/payment_requests/payments/create_job.rb new file mode 100644 index 00000000000..aacef461a79 --- /dev/null +++ b/app/jobs/payment_requests/payments/create_job.rb @@ -0,0 +1,19 @@ +# frozen_string_literal: true + +module PaymentRequests + module Payments + class CreateJob < ApplicationJob + queue_as 'providers' + + unique :until_executed, on_conflict: :log + + retry_on Faraday::ConnectionFailed, wait: :polynomially_longer, attempts: 6 + retry_on ::Stripe::RateLimitError, wait: :polynomially_longer, attempts: 6 + retry_on ::Stripe::APIConnectionError, wait: :polynomially_longer, attempts: 6 + + def perform(payable:, payment_provider:) + PaymentRequests::Payments::CreateService.call!(payable:, payment_provider:) + end + end + end +end diff --git a/app/jobs/payment_requests/payments/gocardless_create_job.rb b/app/jobs/payment_requests/payments/gocardless_create_job.rb index ccca63cdf57..ef5fd23bbca 100644 --- a/app/jobs/payment_requests/payments/gocardless_create_job.rb +++ b/app/jobs/payment_requests/payments/gocardless_create_job.rb @@ -8,11 +8,9 @@ class GocardlessCreateJob < ApplicationJob unique :until_executed, on_conflict: :log def perform(payable) - result = PaymentRequests::Payments::GocardlessService.new(payable).create + # NOTE: Legacy job, kept only to avoid faileure with existing jobs - PaymentRequestMailer.with(payment_request: payable).requested.deliver_later if result.payable&.payment_failed? - - result.raise_if_error! + PaymentRequests::Payments::CreateService.call!(payable:, payment_provider: 'gocardless') end end end diff --git a/app/jobs/payment_requests/payments/stripe_create_job.rb b/app/jobs/payment_requests/payments/stripe_create_job.rb index 9bfcbc42cf3..6965f53dcc8 100644 --- a/app/jobs/payment_requests/payments/stripe_create_job.rb +++ b/app/jobs/payment_requests/payments/stripe_create_job.rb @@ -11,11 +11,9 @@ class StripeCreateJob < ApplicationJob retry_on ::Stripe::APIConnectionError, wait: :polynomially_longer, attempts: 6 def perform(payable) - result = PaymentRequests::Payments::StripeService.new(payable).create + # NOTE: Legacy job, kept only to avoid faileure with existing jobs - PaymentRequestMailer.with(payment_request: payable).requested.deliver_later if result.payable&.payment_failed? - - result.raise_if_error! + PaymentRequests::Payments::CreateService.call!(payable:, payment_provider: 'stripe') end end end diff --git a/app/models/payment_providers/adyen_provider.rb b/app/models/payment_providers/adyen_provider.rb index d45af2703eb..057c4414346 100644 --- a/app/models/payment_providers/adyen_provider.rb +++ b/app/models/payment_providers/adyen_provider.rb @@ -4,7 +4,7 @@ module PaymentProviders class AdyenProvider < BaseProvider SUCCESS_REDIRECT_URL = 'https://www.adyen.com/' - PENDING_STATUSES = %w[AuthorisedPending Received].freeze + PROCESSING_STATUSES = %w[AuthorisedPending Received].freeze SUCCESS_STATUSES = %w[Authorised SentForSettle SettleScheduled Settled Refunded].freeze FAILED_STATUSES = %w[Cancelled CaptureFailed Error Expired Refused].freeze diff --git a/app/models/payment_providers/base_provider.rb b/app/models/payment_providers/base_provider.rb index 813bb6435f0..6970ba9b2d3 100644 --- a/app/models/payment_providers/base_provider.rb +++ b/app/models/payment_providers/base_provider.rb @@ -28,7 +28,7 @@ class BaseProvider < ApplicationRecord settings_accessors :webhook_secret, :success_redirect_url def determine_payment_status(payment_status) - return :pending if self.class::PENDING_STATUSES.include?(payment_status) + return :processing if self.class::PROCESSING_STATUSES.include?(payment_status) return :succeeded if self.class::SUCCESS_STATUSES.include?(payment_status) return :failed if self.class::FAILED_STATUSES.include?(payment_status) diff --git a/app/models/payment_providers/gocardless_provider.rb b/app/models/payment_providers/gocardless_provider.rb index 8ee648424c3..69e725e5c33 100644 --- a/app/models/payment_providers/gocardless_provider.rb +++ b/app/models/payment_providers/gocardless_provider.rb @@ -4,7 +4,7 @@ module PaymentProviders class GocardlessProvider < BaseProvider SUCCESS_REDIRECT_URL = 'https://gocardless.com/' - PENDING_STATUSES = %w[pending_customer_approval pending_submission submitted confirmed].freeze + PROCESSING_STATUSES = %w[pending_customer_approval pending_submission submitted confirmed].freeze SUCCESS_STATUSES = %w[paid_out].freeze FAILED_STATUSES = %w[cancelled customer_approval_denied failed charged_back].freeze diff --git a/app/models/payment_providers/stripe_provider.rb b/app/models/payment_providers/stripe_provider.rb index 82a9b83778a..bd004568eca 100644 --- a/app/models/payment_providers/stripe_provider.rb +++ b/app/models/payment_providers/stripe_provider.rb @@ -18,7 +18,7 @@ class StripeProvider < BaseProvider charge.dispute.closed ].freeze - PENDING_STATUSES = %w[ + PROCESSING_STATUSES = %w[ processing requires_capture requires_action diff --git a/app/services/invoices/payments/adyen_service.rb b/app/services/invoices/payments/adyen_service.rb index 7444f33639d..84bccc04485 100644 --- a/app/services/invoices/payments/adyen_service.rb +++ b/app/services/invoices/payments/adyen_service.rb @@ -6,10 +6,6 @@ class AdyenService < BaseService include Lago::Adyen::ErrorHandlable include Customers::PaymentProviderFinder - PENDING_STATUSES = %w[AuthorisedPending Received].freeze - SUCCESS_STATUSES = %w[Authorised SentForSettle SettleScheduled Settled Refunded].freeze - FAILED_STATUSES = %w[Cancelled CaptureFailed Error Expired Refused].freeze - def initialize(invoice = nil) @invoice = invoice @@ -30,7 +26,7 @@ def update_payment_status(provider_payment_id:, status:, metadata: {}) payment.update!(status:) - invoice_payment_status = invoice_payment_status(status) + invoice_payment_status = payment.payment_provider&.determine_payment_status(status) update_invoice_payment_status(payment_status: invoice_payment_status) result @@ -175,14 +171,6 @@ def payment_url_params prms end - def invoice_payment_status(payment_status) - return :pending if PENDING_STATUSES.include?(payment_status) - return :succeeded if SUCCESS_STATUSES.include?(payment_status) - return :failed if FAILED_STATUSES.include?(payment_status) - - payment_status - end - def update_invoice_payment_status(payment_status:, deliver_webhook: true) result = Invoices::UpdateService.call( invoice:, diff --git a/app/services/invoices/payments/create_service.rb b/app/services/invoices/payments/create_service.rb index f7b11776088..8b31a1e9fef 100644 --- a/app/services/invoices/payments/create_service.rb +++ b/app/services/invoices/payments/create_service.rb @@ -43,13 +43,20 @@ def call result.payment = payment - payment_result = ::PaymentProviders::CreatePaymentFactory.new_instance(provider:, payment:).call! + payment_result = ::PaymentProviders::CreatePaymentFactory.new_instance( + provider:, + payment:, + reference: "#{invoice.organization.name} - Invoice #{invoice.number}", + metadata: { + lago_invoice_id: invoice.id, + lago_customer_id: invoice.customer_id, + invoice_issuing_date: invoice.issuing_date.iso8601, + invoice_type: invoice.invoice_type + } + ).call! payment_status = payment_result.payment.payable_payment_status - update_invoice_payment_status( - payment_status: (payment_status == "processing") ? :pending : payment_status, - processing: payment_status == "processing" - ) + update_invoice_payment_status(payment_status:) Integrations::Aggregator::Payments::CreateJob.perform_later(payment:) if result.payment.should_sync_payment? @@ -58,10 +65,12 @@ def call result.payment = e.result.payment if e.result.payment.payable_payment_status&.to_sym != :pending + # Avoid notification for amount_too_small errors deliver_error_webhook(e.result) - update_invoice_payment_status(payment_status: e.result.payment.payable_payment_status) end + update_invoice_payment_status(payment_status: e.result.payment.payable_payment_status) + # Some errors should be investigated and need to be raised raise if e.result.reraise @@ -103,13 +112,13 @@ def current_payment_provider_customer .find_by(payment_provider_id: current_payment_provider.id) end - def update_invoice_payment_status(payment_status:, processing: false) + def update_invoice_payment_status(payment_status:) Invoices::UpdateService.call!( invoice: invoice, params: { - payment_status:, # NOTE: A proper `processing` payment status should be introduced for invoices - ready_for_payment_processing: !processing && payment_status.to_sym != :succeeded + payment_status: (payment_status.to_s == "processing") ? :pending : payment_status, + ready_for_payment_processing: %w[pending failed].include?(payment_status.to_s) }, webhook_notification: payment_status.to_sym == :succeeded ) diff --git a/app/services/invoices/payments/gocardless_service.rb b/app/services/invoices/payments/gocardless_service.rb index 1304533b87c..cc825caa347 100644 --- a/app/services/invoices/payments/gocardless_service.rb +++ b/app/services/invoices/payments/gocardless_service.rb @@ -5,11 +5,6 @@ module Payments class GocardlessService < BaseService include Customers::PaymentProviderFinder - PENDING_STATUSES = %w[pending_customer_approval pending_submission submitted confirmed] - .freeze - SUCCESS_STATUSES = %w[paid_out].freeze - FAILED_STATUSES = %w[cancelled customer_approval_denied failed charged_back].freeze - def initialize(invoice = nil) @invoice = invoice @@ -26,7 +21,7 @@ def update_payment_status(provider_payment_id:, status:) payment.update!(status:) - invoice_payment_status = invoice_payment_status(status) + invoice_payment_status = payment.payment_provider&.determine_payment_status(status) update_invoice_payment_status(payment_status: invoice_payment_status) result @@ -40,14 +35,6 @@ def update_payment_status(provider_payment_id:, status:) delegate :organization, :customer, to: :invoice - def invoice_payment_status(payment_status) - return :pending if PENDING_STATUSES.include?(payment_status) - return :succeeded if SUCCESS_STATUSES.include?(payment_status) - return :failed if FAILED_STATUSES.include?(payment_status) - - payment_status - end - def update_invoice_payment_status(payment_status:, deliver_webhook: true) update_invoice_result = Invoices::UpdateService.call( invoice: result.invoice, diff --git a/app/services/invoices/payments/stripe_service.rb b/app/services/invoices/payments/stripe_service.rb index 22a0ea4dfaa..38f00ed4303 100644 --- a/app/services/invoices/payments/stripe_service.rb +++ b/app/services/invoices/payments/stripe_service.rb @@ -5,11 +5,6 @@ module Payments class StripeService < BaseService include Customers::PaymentProviderFinder - PENDING_STATUSES = %w[processing requires_capture requires_action requires_confirmation requires_payment_method] - .freeze - SUCCESS_STATUSES = %w[succeeded].freeze - FAILED_STATUSES = %w[canceled].freeze - def initialize(invoice = nil) @invoice = invoice @@ -37,7 +32,7 @@ def update_payment_status(organization_id:, status:, stripe_payment:) payment.update!(status:) update_invoice_payment_status( - payment_status: invoice_payment_status(status), + payment_status: payment.payment_provider&.determine_payment_status(status), processing: status == "processing" ) @@ -89,7 +84,7 @@ def create_payment(stripe_payment, invoice: nil) status: "pending" ) - status = invoice_payment_status(stripe_payment.status) + status = payment.payment_provider&.determine_payment_status(stripe_payment.status) status = (status.to_sym == :pending) ? :processing : status payment.provider_payment_id = stripe_payment.id @@ -150,14 +145,6 @@ def description "#{organization.name} - Invoice #{invoice.number}" end - def invoice_payment_status(payment_status) - return :pending if PENDING_STATUSES.include?(payment_status) - return :succeeded if SUCCESS_STATUSES.include?(payment_status) - return :failed if FAILED_STATUSES.include?(payment_status) - - payment_status&.to_sym - end - def update_invoice_payment_status(payment_status:, deliver_webhook: true, processing: false) result = Invoices::UpdateService.call( invoice: invoice.presence || @result.invoice, diff --git a/app/services/payment_providers/adyen/payments/create_service.rb b/app/services/payment_providers/adyen/payments/create_service.rb index 8fd5d854fd7..9b1ad515a4f 100644 --- a/app/services/payment_providers/adyen/payments/create_service.rb +++ b/app/services/payment_providers/adyen/payments/create_service.rb @@ -4,12 +4,10 @@ module PaymentProviders module Adyen module Payments class CreateService < BaseService - PROCESSING_STATUSES = %w[AuthorisedPending Received].freeze - SUCCESS_STATUSES = %w[Authorised SentForSettle SettleScheduled Settled Refunded].freeze - FAILED_STATUSES = %w[Cancelled CaptureFailed Error Expired Refused].freeze - - def initialize(payment:) + def initialize(payment:, reference:, metadata:) @payment = payment + @reference = reference + @metadata = metadata @invoice = payment.payable @provider_customer = payment.payment_provider_customer @@ -29,7 +27,7 @@ def call payment.provider_payment_id = adyen_result.response["pspReference"] payment.status = adyen_result.response["resultCode"] - payment.payable_payment_status = payment_status_mapping(payment.status) + payment.payable_payment_status = payment.payment_provider&.determine_payment_status(payment.status) payment.save! result.payment = payment @@ -45,7 +43,7 @@ def call private - attr_reader :payment, :invoice, :provider_customer + attr_reader :payment, :reference, :metadata, :invoice, :provider_customer delegate :payment_provider, :customer, to: :provider_customer @@ -92,7 +90,7 @@ def payment_params currency: payment.amount_currency.upcase, value: payment.amount_cents }, - reference: invoice.number, + reference: reference, paymentMethod: { type: "scheme", storedPaymentMethodId: provider_customer.payment_method_id @@ -106,14 +104,6 @@ def payment_params prms end - def payment_status_mapping(payment_status) - return :processing if PROCESSING_STATUSES.include?(payment_status) - return :succeeded if SUCCESS_STATUSES.include?(payment_status) - return :failed if FAILED_STATUSES.include?(payment_status) - - payment_status - end - def prepare_failed_result(error, reraise: false) result.error_message = error.msg result.error_code = error.code diff --git a/app/services/payment_providers/create_payment_factory.rb b/app/services/payment_providers/create_payment_factory.rb index 56f30805643..8f8e0ddb81d 100644 --- a/app/services/payment_providers/create_payment_factory.rb +++ b/app/services/payment_providers/create_payment_factory.rb @@ -2,8 +2,8 @@ module PaymentProviders class CreatePaymentFactory - def self.new_instance(provider:, payment:) - service_class(provider:).new(payment:) + def self.new_instance(provider:, payment:, reference:, metadata:) + service_class(provider:).new(payment:, reference:, metadata:) end def self.service_class(provider:) diff --git a/app/services/payment_providers/gocardless/payments/create_service.rb b/app/services/payment_providers/gocardless/payments/create_service.rb index 25fae2f193c..b9f07385367 100644 --- a/app/services/payment_providers/gocardless/payments/create_service.rb +++ b/app/services/payment_providers/gocardless/payments/create_service.rb @@ -17,19 +17,16 @@ def code end end - def initialize(payment:) + def initialize(payment:, reference:, metadata:) @payment = payment + @reference = reference + @metadata = metadata @invoice = payment.payable @provider_customer = payment.payment_provider_customer super end - PROCESSING_STATUSES = %w[pending_customer_approval pending_submission submitted confirmed] - .freeze - SUCCESS_STATUSES = %w[paid_out].freeze - FAILED_STATUSES = %w[cancelled customer_approval_denied failed charged_back].freeze - def call result.payment = payment @@ -37,7 +34,7 @@ def call payment.provider_payment_id = gocardless_result.id payment.status = gocardless_result.status - payment.payable_payment_status = payment_status_mapping(payment.status) + payment.payable_payment_status = payment.payment_provider&.determine_payment_status(payment.status) payment.save! result.payment = payment @@ -48,7 +45,7 @@ def call prepare_failed_result(e, reraise: true) end - attr_reader :payment, :invoice, :provider_customer + attr_reader :payment, :reference, :metadata, :invoice, :provider_customer delegate :payment_provider, :customer, to: :provider_customer @@ -83,11 +80,7 @@ def create_gocardless_payment amount: payment.amount_cents, currency: payment.amount_currency.upcase, retry_if_possible: false, - metadata: { - lago_customer_id: customer.id, - lago_invoice_id: invoice.id, - invoice_issuing_date: invoice.issuing_date.iso8601 - }, + metadata: metadata.except(:invoice_type), links: { mandate: mandate_id } @@ -98,14 +91,6 @@ def create_gocardless_payment ) end - def payment_status_mapping(payment_status) - return :processing if PROCESSING_STATUSES.include?(payment_status) - return :succeeded if SUCCESS_STATUSES.include?(payment_status) - return :failed if FAILED_STATUSES.include?(payment_status) - - payment_status - end - def prepare_failed_result(error, reraise: false) result.error_message = error.message result.error_code = error.code diff --git a/app/services/payment_providers/stripe/payments/create_service.rb b/app/services/payment_providers/stripe/payments/create_service.rb index 1072dd800e8..8b3e12ae645 100644 --- a/app/services/payment_providers/stripe/payments/create_service.rb +++ b/app/services/payment_providers/stripe/payments/create_service.rb @@ -4,13 +4,10 @@ module PaymentProviders module Stripe module Payments class CreateService < BaseService - PROCESSING_STATUSES = %w[processing requires_capture requires_action requires_confirmation requires_payment_method] - .freeze - SUCCESS_STATUSES = %w[succeeded].freeze - FAILED_STATUSES = %w[canceled].freeze - - def initialize(payment:) + def initialize(payment:, reference:, metadata:) @payment = payment + @reference = reference + @metadata = metadata @invoice = payment.payable @provider_customer = payment.payment_provider_customer @@ -24,7 +21,7 @@ def call payment.provider_payment_id = stripe_result.id payment.status = stripe_result.status - payment.payable_payment_status = payment_status_mapping(payment.status) + payment.payable_payment_status = payment.payment_provider&.determine_payment_status(payment.status) payment.provider_payment_data = stripe_result.next_action if stripe_result.status == "requires_action" payment.save! @@ -56,18 +53,10 @@ def call private - attr_reader :payment, :invoice, :provider_customer + attr_reader :payment, :reference, :metadata, :invoice, :provider_customer delegate :payment_provider, to: :provider_customer - def payment_status_mapping(payment_status) - return :processing if PROCESSING_STATUSES.include?(payment_status) - return :succeeded if SUCCESS_STATUSES.include?(payment_status) - return :failed if FAILED_STATUSES.include?(payment_status) - - payment_status - end - def handle_requires_action(payment) SendWebhookJob.perform_later("payment.requires_action", payment, { provider_customer_id: provider_customer.provider_customer_id @@ -133,13 +122,8 @@ def payment_intent_payload off_session: off_session?, return_url: success_redirect_url, error_on_requires_action: error_on_requires_action?, - description:, - metadata: { - lago_customer_id: provider_customer.customer_id, - lago_invoice_id: invoice.id, - invoice_issuing_date: invoice.issuing_date.iso8601, - invoice_type: invoice.invoice_type - } + description: reference, + metadata: metadata } end @@ -159,10 +143,6 @@ def error_on_requires_action? invoice.customer.country != "IN" end - def description - "#{invoice.organization.name} - Invoice #{invoice.number}" - end - def prepare_failed_result(error, reraise: false, payable_payment_status: :failed) result.error_message = error.message result.error_code = error.code diff --git a/app/services/payment_requests/create_service.rb b/app/services/payment_requests/create_service.rb index a349f3234eb..30e8649c0af 100644 --- a/app/services/payment_requests/create_service.rb +++ b/app/services/payment_requests/create_service.rb @@ -28,7 +28,7 @@ def call after_commit do SendWebhookJob.perform_later("payment_request.created", payment_request) - payment_result = Payments::CreateService.call(payment_request) + payment_result = PaymentRequests::Payments::CreateService.call_async(payable: payment_request) PaymentRequestMailer.with(payment_request:).requested.deliver_later unless payment_result.success? end diff --git a/app/services/payment_requests/payments/adyen_service.rb b/app/services/payment_requests/payments/adyen_service.rb index dc40208037b..37b115a09a4 100644 --- a/app/services/payment_requests/payments/adyen_service.rb +++ b/app/services/payment_requests/payments/adyen_service.rb @@ -12,48 +12,6 @@ def initialize(payable = nil) super(nil) end - def create - result.payable = payable - return result unless should_process_payment? - - unless payable.total_amount_cents.positive? - update_payable_payment_status(payment_status: :succeeded) - return result - end - - payable.increment_payment_attempts! - - res = create_adyen_payment - return result unless res - - adyen_success, _adyen_error = handle_adyen_response(res) - unless adyen_success - update_payable_payment_status(payment_status: :failed, deliver_webhook: false) - return result - end - - payment = Payment.new( - payable: 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: res.response["pspReference"], - status: res.response["resultCode"] - ) - - payment.save! - - payable_payment_status = adyen_payment_provider.determine_payment_status(payment.status) - update_payable_payment_status(payment_status: payable_payment_status) - update_invoices_payment_status(payment_status: payable_payment_status) - - Integrations::Aggregator::Payments::CreateJob.perform_later(payment:) if payment.should_sync_payment? - - result.payment = payment - result - end - def generate_payment_url return result unless should_process_payment? @@ -124,57 +82,6 @@ def adyen_payment_provider @adyen_payment_provider ||= payment_provider(customer) end - def update_payment_method_id - result = client.checkout.payments_api.payment_methods( - Lago::Adyen::Params.new(payment_method_params).to_h - ).response - - payment_method_id = result["storedPaymentMethods"]&.first&.dig("id") - customer.adyen_customer.update!(payment_method_id:) if payment_method_id - end - - def create_adyen_payment - update_payment_method_id - - client.checkout.payments_api.payments(Lago::Adyen::Params.new(payment_params).to_h) - rescue Adyen::AuthenticationError, Adyen::ValidationError => e - deliver_error_webhook(e) - update_payable_payment_status(payment_status: :failed, deliver_webhook: false) - nil - rescue Adyen::AdyenError => e - deliver_error_webhook(e) - update_payable_payment_status(payment_status: :failed, deliver_webhook: false) - result.service_failure!(code: e.code, message: e.message) - nil - end - - def payment_method_params - { - merchantAccount: adyen_payment_provider.merchant_account, - shopperReference: customer.adyen_customer.provider_customer_id - } - end - - def payment_params - prms = { - amount: { - currency: payable.currency.upcase, - value: payable.total_amount_cents - }, - reference: "Overdue invoices", - paymentMethod: { - 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" - } - prms[:shopperEmail] = customer.email if customer.email - prms - end - def payment_url_params prms = { reference: "Overdue invoices", diff --git a/app/services/payment_requests/payments/create_service.rb b/app/services/payment_requests/payments/create_service.rb index fad6476e3df..b0cbb8727eb 100644 --- a/app/services/payment_requests/payments/create_service.rb +++ b/app/services/payment_requests/payments/create_service.rb @@ -3,35 +3,156 @@ module PaymentRequests module Payments class CreateService < BaseService - def initialize(payable) + include Customers::PaymentProviderFinder + + def initialize(payable:, payment_provider: nil) @payable = payable + @provider = payment_provider&.to_sym super end def call - return result.not_found_failure!(resource: "payment_provider") unless payment_provider - - case payment_provider - when :adyen - PaymentRequests::Payments::AdyenCreateJob.perform_later(payable) - when :gocardless - PaymentRequests::Payments::GocardlessCreateJob.perform_later(payable) - when :stripe - PaymentRequests::Payments::StripeCreateJob.perform_later(payable) + return result.not_found_failure!(resource: "payment_provider") unless provider + + result.payable = payable + return result unless should_process_payment? + + unless payable.total_amount_cents.positive? + update_payable_payment_status(payment_status: :succeeded) + return result + end + + if processing_payment + # Payment is being processed, return the existing payment + # Status will be updated via webhooks + result.payment = processing_payment + return result end + payable.increment_payment_attempts! + + payment ||= Payment.create_with( + payment_provider_id: current_payment_provider.id, + payment_provider_customer_id: current_payment_provider_customer.id, + amount_cents: payable.total_amount_cents, + amount_currency: payable.currency, + status: "pending" + ).find_or_create_by!( + payable:, + payable_payment_status: "pending" + ) + + result.payment = payment + + payment_result = ::PaymentProviders::CreatePaymentFactory.new_instance( + provider:, + payment:, + reference: "#{organization.name} - Overdue invoices", + metadata: { + lago_customer_id: payable.customer_id, + lago_payable_id: payable.id, + lago_payable_type: payable.class.name + } + ).call! + + update_payable_payment_status(payment_status: payment_result.payment.payable_payment_status) + update_invoices_payment_status(payment_status: payment_result.payment.payable_payment_status) + + PaymentRequestMailer.with(payment_request: payable).requested.deliver_later if payable.payment_failed? + + result + rescue BaseService::ServiceFailure => e + result.payment = e.result.payment + deliver_error_webhook(e.result) + update_payable_payment_status(payment_status: e.result.payment.payable_payment_status) + + # Some errors should be investigated and need to be raised + raise if e.result.reraise + + result + end + + def call_async + return result.not_found_failure!(resource: "payment_provider") unless provider + + PaymentRequests::Payments::CreateJob.perform_later(payable:, payment_provider: provider) + + result.payment_provider = provider result - rescue ActiveJob::Uniqueness::JobNotUnique => e - Sentry.capture_exception(e) end private attr_reader :payable - def payment_provider - payable.customer.payment_provider&.to_sym + delegate :customer, :organization, to: :payable + + def provider + @provider ||= payable.customer.payment_provider&.to_sym + end + + def should_process_payment? + return false if payable.payment_succeeded? + return false if current_payment_provider.blank? + + current_payment_provider_customer&.provider_customer_id + end + + def current_payment_provider + @current_payment_provider ||= payment_provider(customer) + end + + def current_payment_provider_customer + @current_payment_provider_customer ||= customer.payment_provider_customers + .find_by(payment_provider_id: current_payment_provider.id) + end + + def update_payable_payment_status(payment_status:) + PaymentRequests::UpdateService.call!( + payable: payable, + params: { + # NOTE: A proper `processing` payment status should be introduced for invoices + payment_status: (payment_status.to_s == "processing") ? :pending : payment_status, + ready_for_payment_processing: payment_status.to_sym == :failed + }, + webhook_notification: payment_status.to_sym == :succeeded + ) + end + + def update_invoices_payment_status(payment_status:) + payable.invoices.each do |invoice| + Invoices::UpdateService.call!( + invoice:, + params: { + # NOTE: A proper `processing` payment status should be introduced for invoices + payment_status: (payment_status.to_s == "processing") ? :pending : payment_status, + ready_for_payment_processing: payment_status.to_sym == :failed + }, + webhook_notification: payment_status.to_sym == :succeeded + ) + end + end + + def deliver_error_webhook(payment_result) + DeliverErrorWebhookService.call_async(payable, { + provider_customer_id: current_payment_provider_customer.provider_customer_id, + provider_error: { + message: payment_result.error_message, + error_code: payment_result.error_code + } + }) + end + + def processing_payment + @processing_payment ||= Payment.find_by( + payable_id: payable.id, + payment_provider_id: current_payment_provider.id, + payment_provider_customer_id: current_payment_provider_customer.id, + amount_cents: payable.total_amount_cents, + amount_currency: payable.currency, + payable_payment_status: "processing" + ) end end end diff --git a/app/services/payment_requests/payments/gocardless_service.rb b/app/services/payment_requests/payments/gocardless_service.rb index 8577c58b412..43beab6dc0c 100644 --- a/app/services/payment_requests/payments/gocardless_service.rb +++ b/app/services/payment_requests/payments/gocardless_service.rb @@ -24,48 +24,6 @@ def initialize(payable = nil) super(nil) end - def create - result.payable = payable - return result unless should_process_payment? - - unless payable.total_amount_cents.positive? - update_payable_payment_status(payment_status: :succeeded) - return result - end - - payable.increment_payment_attempts! - - gocardless_result = create_gocardless_payment - return result unless gocardless_result - - payment = Payment.new( - payable: payable, - payment_provider_id: gocardless_payment_provider.id, - payment_provider_customer_id: customer.gocardless_customer.id, - amount_cents: gocardless_result.amount, - amount_currency: gocardless_result.currency&.upcase, - provider_payment_id: gocardless_result.id, - status: gocardless_result.status - ) - - payment.save! - - payable_payment_status = gocardless_payment_provider.determine_payment_status(payment.status) - update_payable_payment_status(payment_status: payable_payment_status) - update_invoices_payment_status(payment_status: payable_payment_status) - - Integrations::Aggregator::Payments::CreateJob.perform_later(payment:) if payment.should_sync_payment? - - result.payment = payment - result - rescue MandateNotFoundError => e - deliver_error_webhook(e) - update_payable_payment_status(payment_status: :failed, deliver_webhook: false) - - result.service_failure!(code: e.code, message: e.message) - result - end - def update_payment_status(provider_payment_id:, status:) payment = Payment.find_by(provider_payment_id:) return result.not_found_failure!(resource: 'gocardless_payment') unless payment @@ -94,13 +52,6 @@ def update_payment_status(provider_payment_id:, status:) delegate :organization, :customer, to: :payable - def should_process_payment? - return false if payable.payment_succeeded? - return false if gocardless_payment_provider.blank? - - !!customer&.gocardless_customer&.provider_customer_id - end - def client @client ||= GoCardlessPro::Client.new( access_token: gocardless_payment_provider.access_token, @@ -112,51 +63,6 @@ def gocardless_payment_provider @gocardless_payment_provider ||= payment_provider(customer) end - def mandate_id - result = client.mandates.list( - params: { - customer: customer.gocardless_customer.provider_customer_id, - status: %w[pending_customer_approval pending_submission submitted active] - } - ) - - mandate = result&.records&.first - - raise MandateNotFoundError unless mandate - - customer.gocardless_customer.provider_mandate_id = mandate.id - customer.gocardless_customer.save! - - mandate.id - end - - def create_gocardless_payment - client.payments.create( - params: { - amount: payable.total_amount_cents, - currency: payable.currency.upcase, - retry_if_possible: false, - metadata: { - lago_customer_id: customer.id, - lago_payable_id: payable.id, - lago_payable_type: payable.class.name - }, - links: { - mandate: mandate_id - } - }, - headers: { - 'Idempotency-Key' => "#{payable.id}/#{payable.payment_attempts}" - } - ) - rescue GoCardlessPro::Error => e - deliver_error_webhook(e) - update_payable_payment_status(payment_status: :failed, deliver_webhook: false) - - result.service_failure!(code: e.code, message: e.message) - nil - end - def update_payable_payment_status(payment_status:, deliver_webhook: true) UpdateService.call( payable: result.payable, @@ -181,16 +87,6 @@ def update_invoices_payment_status(payment_status:, deliver_webhook: true) end end - def deliver_error_webhook(gocardless_error) - DeliverErrorWebhookService.call_async(payable, { - provider_customer_id: customer.gocardless_customer.provider_customer_id, - provider_error: { - message: gocardless_error.message, - error_code: gocardless_error.code - } - }) - end - def payment_status_succeeded?(payment_status) payment_status.to_sym == :succeeded end diff --git a/app/services/payment_requests/payments/stripe_service.rb b/app/services/payment_requests/payments/stripe_service.rb index 96ecad72bd9..517842c73ad 100644 --- a/app/services/payment_requests/payments/stripe_service.rb +++ b/app/services/payment_requests/payments/stripe_service.rb @@ -11,60 +11,6 @@ def initialize(payable = nil) super(nil) end - def create - result.payable = payable - return result unless should_process_payment? - - unless payable.total_amount_cents.positive? - update_payable_payment_status(payment_status: :succeeded) - return result - end - - payable.increment_payment_attempts! - - stripe_result = create_stripe_payment - # NOTE: return if payment was not processed - return result unless stripe_result - - payment = Payment.new( - payable: payable, - payment_provider_id: stripe_payment_provider.id, - payment_provider_customer_id: customer.stripe_customer.id, - amount_cents: stripe_result.amount, - amount_currency: stripe_result.currency&.upcase, - provider_payment_id: stripe_result.id, - status: stripe_result.status - ) - - payment.save! - - payable_payment_status = stripe_payment_provider.determine_payment_status(payment.status) - update_payable_payment_status( - payment_status: payable_payment_status, - processing: payment.status == "processing" - ) - update_invoices_payment_status( - payment_status: payable_payment_status, - processing: payment.status == "processing" - ) - - result.payment = payment - result - rescue ::Stripe::AuthenticationError, ::Stripe::CardError, ::Stripe::InvalidRequestError, Stripe::PermissionError => e - # NOTE: Do not mark the payable as failed if the amount is too small for Stripe - # For now we keep it as pending. - return result if e.code == "amount_too_small" - - deliver_error_webhook(e) - update_payable_payment_status(payment_status: :failed, deliver_webhook: false) - result - rescue ::Stripe::RateLimitError, ::Stripe::APIConnectionError - raise # Let the auto-retry process do its own job - rescue ::Stripe::StripeError => e - deliver_error_webhook(e) - raise - end - def generate_payment_url return result unless should_process_payment? @@ -140,77 +86,6 @@ def stripe_api_key stripe_payment_provider.secret_key end - def stripe_payment_method - payment_method_id = customer.stripe_customer.payment_method_id - - if payment_method_id - # NOTE: Check if payment method still exists - customer_service = PaymentProviderCustomers::StripeService.new(customer.stripe_customer) - customer_service_result = customer_service.check_payment_method(payment_method_id) - return customer_service_result.payment_method.id if customer_service_result.success? - end - - # NOTE: Retrieve list of existing payment_methods - payment_method = Stripe::PaymentMethod.list( - { - customer: customer.stripe_customer.provider_customer_id - }, - { - api_key: stripe_api_key - } - ).first - customer.stripe_customer.payment_method_id = payment_method&.id - customer.stripe_customer.save! - - payment_method&.id - end - - def update_payment_method_id - result = Stripe::Customer.retrieve( - customer.stripe_customer.provider_customer_id, - { - api_key: stripe_api_key - } - ) - # TODO: stripe customer should be updated/deleted - return if result.deleted? - - if (payment_method_id = result.invoice_settings.default_payment_method || result.default_source) - customer.stripe_customer.update!(payment_method_id:) - end - end - - def create_stripe_payment - update_payment_method_id - - Stripe::PaymentIntent.create( - stripe_payment_payload, - { - api_key: stripe_api_key, - idempotency_key: "#{payable.id}/#{payable.payment_attempts}" - } - ) - end - - def stripe_payment_payload - { - amount: payable.total_amount_cents, - currency: payable.currency.downcase, - customer: customer.stripe_customer.provider_customer_id, - payment_method: stripe_payment_method, - payment_method_types: customer.stripe_customer.provider_payment_methods, - confirm: true, - off_session: true, - error_on_requires_action: true, - description:, - metadata: { - lago_customer_id: customer.id, - lago_payable_id: payable.id, - lago_payable_type: payable.class.name - } - } - end - def description "#{organization.name} - Overdue invoices" end diff --git a/spec/jobs/payment_requests/payments/adyen_create_job_spec.rb b/spec/jobs/payment_requests/payments/adyen_create_job_spec.rb index e02a78109ac..9c58d2372a6 100644 --- a/spec/jobs/payment_requests/payments/adyen_create_job_spec.rb +++ b/spec/jobs/payment_requests/payments/adyen_create_job_spec.rb @@ -5,40 +5,17 @@ RSpec.describe PaymentRequests::Payments::AdyenCreateJob, type: :job do let(:payment_request) { create(:payment_request) } - let(:adyen_service) { instance_double(PaymentRequests::Payments::AdyenService) } let(:service_result) { BaseService::Result.new } before do - allow(PaymentRequests::Payments::AdyenService).to receive(:new) - .with(payment_request) - .and_return(adyen_service) - allow(adyen_service).to receive(:create) + allow(PaymentRequests::Payments::CreateService).to receive(:call!) + .with(payable: payment_request, payment_provider: 'adyen') .and_return(service_result) end it 'calls the stripe create service' do described_class.perform_now(payment_request) - expect(PaymentRequests::Payments::AdyenService).to have_received(:new) - expect(adyen_service).to have_received(:create) - end - - it "does not send a payment requested email" do - expect { described_class.perform_now(payment_request) } - .not_to have_enqueued_mail(PaymentRequestMailer, :requested) - end - - context "when the payment fails" do - let(:service_result) do - BaseService::Result.new.tap do |result| - result.payable = instance_double(PaymentRequest, payment_failed?: true) - end - end - - it "sends a payment requested email" do - expect { described_class.perform_now(payment_request) } - .to have_enqueued_mail(PaymentRequestMailer, :requested) - .with(params: {payment_request:}, args: []) - end + expect(PaymentRequests::Payments::CreateService).to have_received(:call!) end end diff --git a/spec/jobs/payment_requests/payments/create_job_spec.rb b/spec/jobs/payment_requests/payments/create_job_spec.rb new file mode 100644 index 00000000000..c497849fa1f --- /dev/null +++ b/spec/jobs/payment_requests/payments/create_job_spec.rb @@ -0,0 +1,22 @@ +# frozen_string_literal: true + +require "rails_helper" + +RSpec.describe PaymentRequests::Payments::CreateJob, type: :job do + let(:payment_request) { create(:payment_request) } + + let(:service_result) { BaseService::Result.new } + let(:payment_provider) { "stripe" } + + before do + allow(PaymentRequests::Payments::CreateService).to receive(:call!) + .with(payable: payment_request, payment_provider:) + .and_return(service_result) + end + + it "calls the stripe create service" do + described_class.perform_now(payable: payment_request, payment_provider:) + + expect(PaymentRequests::Payments::CreateService).to have_received(:call!) + end +end diff --git a/spec/jobs/payment_requests/payments/gocardless_create_job_spec.rb b/spec/jobs/payment_requests/payments/gocardless_create_job_spec.rb index e61382348b4..608835eb312 100644 --- a/spec/jobs/payment_requests/payments/gocardless_create_job_spec.rb +++ b/spec/jobs/payment_requests/payments/gocardless_create_job_spec.rb @@ -5,40 +5,17 @@ RSpec.describe PaymentRequests::Payments::GocardlessCreateJob, type: :job do let(:payment_request) { create(:payment_request) } - let(:gocardless_service) { instance_double(PaymentRequests::Payments::GocardlessService) } let(:service_result) { BaseService::Result.new } before do - allow(PaymentRequests::Payments::GocardlessService).to receive(:new) - .with(payment_request) - .and_return(gocardless_service) - allow(gocardless_service).to receive(:create) + allow(PaymentRequests::Payments::CreateService).to receive(:call!) + .with(payable: payment_request, payment_provider: "gocardless") .and_return(service_result) end it "calls the stripe create service" do described_class.perform_now(payment_request) - expect(PaymentRequests::Payments::GocardlessService).to have_received(:new) - expect(gocardless_service).to have_received(:create) - end - - it "does not send a payment requested email" do - expect { described_class.perform_now(payment_request) } - .not_to have_enqueued_mail(PaymentRequestMailer, :requested) - end - - context "when the payment fails" do - let(:service_result) do - BaseService::Result.new.tap do |result| - result.payable = instance_double(PaymentRequest, payment_failed?: true) - end - end - - it "sends a payment requested email" do - expect { described_class.perform_now(payment_request) } - .to have_enqueued_mail(PaymentRequestMailer, :requested) - .with(params: {payment_request:}, args: []) - end + expect(PaymentRequests::Payments::CreateService).to have_received(:call!) end end diff --git a/spec/jobs/payment_requests/payments/stripe_create_job_spec.rb b/spec/jobs/payment_requests/payments/stripe_create_job_spec.rb index a4ce546932c..fb59fac099e 100644 --- a/spec/jobs/payment_requests/payments/stripe_create_job_spec.rb +++ b/spec/jobs/payment_requests/payments/stripe_create_job_spec.rb @@ -5,40 +5,17 @@ RSpec.describe PaymentRequests::Payments::StripeCreateJob, type: :job do let(:payment_request) { create(:payment_request) } - let(:stripe_service) { instance_double(PaymentRequests::Payments::StripeService) } let(:service_result) { BaseService::Result.new } before do - allow(PaymentRequests::Payments::StripeService).to receive(:new) - .with(payment_request) - .and_return(stripe_service) - allow(stripe_service).to receive(:create) + allow(PaymentRequests::Payments::CreateService).to receive(:call!) + .with(payable: payment_request, payment_provider: "stripe") .and_return(service_result) end it "calls the stripe create service" do described_class.perform_now(payment_request) - expect(PaymentRequests::Payments::StripeService).to have_received(:new) - expect(stripe_service).to have_received(:create) - end - - it "does not send a payment requested email" do - expect { described_class.perform_now(payment_request) } - .not_to have_enqueued_mail(PaymentRequestMailer, :requested) - end - - context "when the payment fails" do - let(:service_result) do - BaseService::Result.new.tap do |result| - result.payable = instance_double(PaymentRequest, payment_failed?: true) - end - end - - it "sends a payment requested email" do - expect { described_class.perform_now(payment_request) } - .to have_enqueued_mail(PaymentRequestMailer, :requested) - .with(params: {payment_request:}, args: []) - end + expect(PaymentRequests::Payments::CreateService).to have_received(:call!) end end diff --git a/spec/services/invoices/payments/adyen_service_spec.rb b/spec/services/invoices/payments/adyen_service_spec.rb index 3c9baea8c20..829e0f23c58 100644 --- a/spec/services/invoices/payments/adyen_service_spec.rb +++ b/spec/services/invoices/payments/adyen_service_spec.rb @@ -8,7 +8,7 @@ let(:customer) { create(:customer, payment_provider_code: code) } let(:organization) { customer.organization } let(:adyen_payment_provider) { create(:adyen_provider, organization:, code:) } - let(:adyen_customer) { create(:adyen_customer, customer:) } + let(:adyen_customer) { create(:adyen_customer, customer:, payment_provider: adyen_payment_provider) } let(:adyen_client) { instance_double(Adyen::Client) } let(:payments_api) { Adyen::PaymentsApi.new(adyen_client, 70) } let(:payment_links_api) { Adyen::PaymentLinksApi.new(adyen_client, 70) } @@ -55,7 +55,8 @@ :payment, payable: invoice, provider_payment_id: "ch_123456", - status: "Pending" + status: "Pending", + payment_provider: adyen_payment_provider ) end diff --git a/spec/services/invoices/payments/create_service_spec.rb b/spec/services/invoices/payments/create_service_spec.rb index 77cda59d23f..75bec167324 100644 --- a/spec/services/invoices/payments/create_service_spec.rb +++ b/spec/services/invoices/payments/create_service_spec.rb @@ -27,8 +27,17 @@ provider_customer allow(provider_class) - .to receive(:new).with(payment: an_instance_of(Payment)) - .and_return(provider_service) + .to receive(:new) + .with( + payment: an_instance_of(Payment), + reference: "#{invoice.organization.name} - Invoice #{invoice.number}", + metadata: { + lago_invoice_id: invoice.id, + lago_customer_id: customer.id, + invoice_issuing_date: invoice.issuing_date.iso8601, + invoice_type: invoice.invoice_type + } + ).and_return(provider_service) allow(provider_service).to receive(:call!) .and_return(result) end @@ -69,7 +78,7 @@ it "calls the gocardless service" do create_service.call - expect(provider_class).to have_received(:new).with(payment: an_instance_of(Payment)) + expect(provider_class).to have_received(:new) expect(provider_service).to have_received(:call!) end end @@ -215,16 +224,18 @@ end context "when invoice is credit? and open?" do + let(:invoice) { create(:invoice, :credit, :open, customer:, organization:, total_amount_cents: 100) } + let(:wallet_transaction) { create(:wallet_transaction) } + let(:fee) { create(:fee, fee_type: :credit, invoice: invoice, invoiceable: wallet_transaction) } + before do + fee + allow(Invoices::Payments::DeliverErrorWebhookService) .to receive(:call_async).and_call_original end it "delivers an error webhook" do - wallet_transaction = create(:wallet_transaction) - create(:fee, fee_type: :credit, invoice: invoice, invoiceable: wallet_transaction) - invoice.update!(status: :open, invoice_type: :credit) - expect { create_service.call }.to raise_error(BaseService::ServiceFailure) expect(Invoices::Payments::DeliverErrorWebhookService).to have_received(:call_async) diff --git a/spec/services/invoices/payments/gocardless_service_spec.rb b/spec/services/invoices/payments/gocardless_service_spec.rb index 2fee837f1f5..ea98cc61560 100644 --- a/spec/services/invoices/payments/gocardless_service_spec.rb +++ b/spec/services/invoices/payments/gocardless_service_spec.rb @@ -33,7 +33,8 @@ :payment, payable: invoice, provider_payment_id: "ch_123456", - status: "pending_submission" + status: "pending_submission", + payment_provider: gocardless_payment_provider ) end diff --git a/spec/services/payment_providers/adyen/payments/create_service_spec.rb b/spec/services/payment_providers/adyen/payments/create_service_spec.rb index 3a05b7c3722..1e85f95d16a 100644 --- a/spec/services/payment_providers/adyen/payments/create_service_spec.rb +++ b/spec/services/payment_providers/adyen/payments/create_service_spec.rb @@ -3,7 +3,7 @@ require "rails_helper" RSpec.describe PaymentProviders::Adyen::Payments::CreateService, type: :service do - subject(:create_service) { described_class.new(payment:) } + subject(:create_service) { described_class.new(payment:, reference:, metadata:) } let(:customer) { create(:customer, payment_provider_code: code) } let(:organization) { customer.organization } @@ -17,6 +17,15 @@ let(:payments_response) { generate(:adyen_payments_response) } let(:payment_methods_response) { generate(:adyen_payment_methods_response) } let(:code) { "adyen_1" } + let(:reference) { "organization.name - Invoice #{invoice.number}" } + let(:metadata) do + { + lago_customer_id: customer.id, + lago_invoice_id: invoice.id, + invoice_issuing_date: invoice.issuing_date.iso8601, + invoice_type: invoice.invoice_type + } + end let(:invoice) do create( diff --git a/spec/services/payment_providers/create_payment_factory_spec.rb b/spec/services/payment_providers/create_payment_factory_spec.rb index ad5f041b5cb..d237351d036 100644 --- a/spec/services/payment_providers/create_payment_factory_spec.rb +++ b/spec/services/payment_providers/create_payment_factory_spec.rb @@ -3,7 +3,7 @@ require "rails_helper" RSpec.describe PaymentProviders::CreatePaymentFactory, type: :service do - subject(:new_instance) { described_class.new_instance(provider:, payment:) } + subject(:new_instance) { described_class.new_instance(provider:, payment:, reference: '', metadata: {}) } let(:provider) { "stripe" } let(:payment) { create(:payment) } diff --git a/spec/services/payment_providers/gocardless/payments/create_service_spec.rb b/spec/services/payment_providers/gocardless/payments/create_service_spec.rb index 028bd7a7bea..19d9abaa2f2 100644 --- a/spec/services/payment_providers/gocardless/payments/create_service_spec.rb +++ b/spec/services/payment_providers/gocardless/payments/create_service_spec.rb @@ -3,7 +3,7 @@ require "rails_helper" RSpec.describe PaymentProviders::Gocardless::Payments::CreateService, type: :service do - subject(:create_service) { described_class.new(payment:) } + subject(:create_service) { described_class.new(payment:, reference:, metadata:) } let(:customer) { create(:customer, payment_provider_code: code) } let(:organization) { customer.organization } @@ -14,6 +14,15 @@ let(:gocardless_mandates_service) { instance_double(GoCardlessPro::Services::MandatesService) } let(:gocardless_list_response) { instance_double(GoCardlessPro::ListResponse) } let(:code) { "gocardless_1" } + let(:reference) { "organization.name - Invoice #{invoice.number}" } + let(:metadata) do + { + lago_customer_id: customer.id, + lago_invoice_id: invoice.id, + invoice_issuing_date: invoice.issuing_date.iso8601, + invoice_type: invoice.invoice_type + } + end let(:invoice) do create( diff --git a/spec/services/payment_providers/stripe/payments/create_service_spec.rb b/spec/services/payment_providers/stripe/payments/create_service_spec.rb index 9868828b511..ce2d9aa7d51 100644 --- a/spec/services/payment_providers/stripe/payments/create_service_spec.rb +++ b/spec/services/payment_providers/stripe/payments/create_service_spec.rb @@ -3,13 +3,22 @@ require "rails_helper" RSpec.describe PaymentProviders::Stripe::Payments::CreateService, type: :service do - subject(:create_service) { described_class.new(payment:) } + subject(:create_service) { described_class.new(payment:, reference:, metadata:) } let(:customer) { create(:customer, payment_provider_code: code) } let(:organization) { customer.organization } let(:stripe_payment_provider) { create(:stripe_provider, organization:, code:) } let(:stripe_customer) { create(:stripe_customer, customer:, payment_method_id: "pm_123456", payment_provider: stripe_payment_provider) } let(:code) { "stripe_1" } + let(:reference) { "organization.name - Invoice #{invoice.number}" } + let(:metadata) do + { + lago_customer_id: customer.id, + lago_invoice_id: invoice.id, + invoice_issuing_date: invoice.issuing_date.iso8601, + invoice_type: invoice.invoice_type + } + end let(:invoice) do create( @@ -311,13 +320,8 @@ off_session: true, return_url: create_service.__send__(:success_redirect_url), error_on_requires_action: true, - description: create_service.__send__(:description), - metadata: { - lago_customer_id: customer.id, - lago_invoice_id: invoice.id, - invoice_issuing_date: invoice.issuing_date.iso8601, - invoice_type: invoice.invoice_type - } + description: reference, + metadata: metadata } end @@ -337,14 +341,5 @@ end end end - - context "with #description" do - let(:description_call) { create_service.__send__(:description) } - let(:description) { "#{organization.name} - Invoice #{invoice.number}" } - - it "returns the description" do - expect(description_call).to eq(description) - end - end end end diff --git a/spec/services/payment_requests/create_service_spec.rb b/spec/services/payment_requests/create_service_spec.rb index 7587f15cdbb..a0523814634 100644 --- a/spec/services/payment_requests/create_service_spec.rb +++ b/spec/services/payment_requests/create_service_spec.rb @@ -111,11 +111,11 @@ end it "creates a payment for the payment request" do - allow(PaymentRequests::Payments::CreateService).to receive(:call).and_call_original + allow(PaymentRequests::Payments::CreateService).to receive(:call_async).and_call_original result = create_service.call - expect(PaymentRequests::Payments::CreateService).to have_received(:call).with(result.payment_request) + expect(PaymentRequests::Payments::CreateService).to have_received(:call_async).with(payable: result.payment_request) end context "when Payments::CreateService returns an error" do diff --git a/spec/services/payment_requests/payments/adyen_service_spec.rb b/spec/services/payment_requests/payments/adyen_service_spec.rb index f2738f9d93a..7f018c13d3d 100644 --- a/spec/services/payment_requests/payments/adyen_service_spec.rb +++ b/spec/services/payment_requests/payments/adyen_service_spec.rb @@ -49,289 +49,6 @@ ) end - describe "#create" do - before do - adyen_payment_provider - adyen_customer - - allow(Adyen::Client).to receive(:new) - .and_return(adyen_client) - allow(adyen_client).to receive(:checkout) - .and_return(checkout) - allow(checkout).to receive(:payments_api) - .and_return(payments_api) - allow(payments_api).to receive(:payments) - .and_return(payments_response) - allow(payments_api).to receive(:payment_methods) - .and_return(payment_methods_response) - end - - it "creates an adyen payment", :aggregate_failures do - result = adyen_service.create - - expect(result).to be_success - - expect(result.payable).to be_payment_succeeded - expect(result.payable.payment_attempts).to eq(1) - expect(result.payable.reload.ready_for_payment_processing).to eq(false) - - expect(result.payment.id).to be_present - expect(result.payment.payable).to eq(payment_request) - expect(result.payment.payment_provider).to eq(adyen_payment_provider) - expect(result.payment.payment_provider_customer).to eq(adyen_customer) - expect(result.payment.amount_cents).to eq(payment_request.total_amount_cents) - expect(result.payment.amount_currency).to eq(payment_request.currency) - expect(result.payment.status).to eq("Authorised") - - expect(adyen_customer.reload.payment_method_id) - .to eq(payment_methods_response.response["storedPaymentMethods"].first["id"]) - - expect(payments_api) - .to have_received(:payments) - .with( - { - amount: { - currency: "USD", - value: 799 - }, - applicationInfo: { - externalPlatform: {integrator: "Lago", name: "Lago"}, - merchantApplication: {name: "Lago"} - }, - merchantAccount: adyen_payment_provider.merchant_account, - paymentMethod: { - storedPaymentMethodId: adyen_customer.payment_method_id, - type: "scheme" - }, - recurringProcessingModel: "UnscheduledCardOnFile", - reference: "Overdue invoices", - shopperEmail: customer.email, - shopperInteraction: "ContAuth", - shopperReference: adyen_customer.provider_customer_id - } - ) - end - - it "updates invoice payment status to succeeded", :aggregate_failures do - adyen_service.create - - expect(invoice_1.reload).to be_payment_succeeded - expect(invoice_2.reload).to be_payment_succeeded - end - - context "when payment request payment status is already succeeded" do - let(:payment_request) do - create( - :payment_request, - organization:, - customer:, - payment_status: "succeeded", - amount_cents: 799, - amount_currency: "EUR" - ) - end - - it "does not creates a payment", :aggregate_failures do - result = adyen_service.create - - expect(result).to be_success - expect(result.payable).to be_payment_succeeded - expect(result.payment).to be_nil - - expect(payments_api).not_to have_received(:payments) - end - end - - context "with no payment provider" do - let(:adyen_payment_provider) { nil } - - it "does not creates a adyen payment", :aggregate_failures do - result = adyen_service.create - - expect(result).to be_success - - expect(result.payable).to eq(payment_request) - expect(result.payment).to be_nil - - expect(payments_api).not_to have_received(:payments) - end - end - - context "with 0 amount" do - let(:payment_request) do - create( - :payment_request, - organization:, - customer:, - amount_cents: 0, - amount_currency: "EUR", - invoices: [invoice] - ) - end - - let(:invoice) do - create( - :invoice, - organization:, - customer:, - total_amount_cents: 0, - currency: 'EUR' - ) - end - - it "does not creates a adyen payment", :aggregate_failures do - result = adyen_service.create - - expect(result).to be_success - expect(result.payable).to eq(payment_request) - expect(result.payment).to be_nil - expect(result.payable).to be_payment_succeeded - expect(payments_api).not_to have_received(:payments) - end - end - - context "when customer does not have a provider customer id" do - before { adyen_customer.update!(provider_customer_id: nil) } - - it "does not creates a adyen payment", :aggregate_failures do - result = adyen_service.create - - expect(result).to be_success - - expect(result.payable).to eq(payment_request) - expect(result.payment).to be_nil - - expect(payments_api).not_to have_received(:payments) - end - end - - context "with error response from adyen" do - let(:payments_error_response) { generate(:adyen_payments_error_response) } - - before do - allow(payments_api).to receive(:payments).and_return(payments_error_response) - end - - it "delivers an error webhook" do - expect { adyen_service.create }.to enqueue_job(SendWebhookJob) - .with( - "payment_request.payment_failure", - payment_request, - provider_customer_id: adyen_customer.provider_customer_id, - provider_error: { - message: "There are no payment methods available for the given parameters.", - error_code: "validation" - } - ).on_queue(:webhook) - end - - it "marks the payment request as payment failed" do - result = adyen_service.create - - expect(result).to be_success - expect(result.payable).to be_payment_failed - end - end - - context "with validation error on adyen" do - let(:customer) { create(:customer, organization:, payment_provider_code: code) } - - let(:organization) do - create(:organization, webhook_url: "https://webhook.com") - end - - context "when changing payment method fails with invalid card" do - before do - allow(payments_api).to receive(:payment_methods) - .and_raise(Adyen::ValidationError.new("Invalid card number", nil)) - end - - it "delivers an error webhook" do - expect { adyen_service.create }.to enqueue_job(SendWebhookJob) - .with( - "payment_request.payment_failure", - payment_request, - provider_customer_id: adyen_customer.provider_customer_id, - provider_error: { - message: "Invalid card number", - error_code: nil - } - ).on_queue(:webhook) - end - - it "marks the payment request as payment failed" do - result = adyen_service.create - - expect(result).to be_success - expect(result.payable).to be_payment_failed - end - end - - context "when payment fails with invalid card" do - before do - allow(payments_api).to receive(:payments) - .and_raise(Adyen::ValidationError.new("Invalid card number", nil)) - end - - it "delivers an error webhook" do - expect { adyen_service.create }.to enqueue_job(SendWebhookJob) - .with( - "payment_request.payment_failure", - payment_request, - provider_customer_id: adyen_customer.provider_customer_id, - provider_error: { - message: "Invalid card number", - error_code: nil - } - ).on_queue(:webhook) - end - - it "marks the payment request as payment failed" do - result = adyen_service.create - - expect(result).to be_success - expect(result.payable).to be_payment_failed - end - end - end - - context "with error on adyen" do - let(:customer) do - create(:customer, organization:, payment_provider_code: code) - end - - let(:organization) do - create(:organization, webhook_url: "https://webhook.com") - end - - before do - allow(payments_api).to receive(:payments) - .and_raise(Adyen::AdyenError.new(nil, nil, "error", "code")) - end - - it "delivers an error webhook" do - expect { adyen_service.create } - .to enqueue_job(SendWebhookJob) - .with( - "payment_request.payment_failure", - payment_request, - provider_customer_id: adyen_customer.provider_customer_id, - provider_error: { - message: "error", - error_code: "code" - } - ) - end - - it "updates payment request payment status to failed" do - result = adyen_service.create - - expect(result).not_to be_success - expect(result.payable).to be_payment_failed - end - end - end - describe "#generate_payment_url" do let(:payment_links_api) { Adyen::PaymentLinksApi.new(adyen_client, 70) } let(:payment_links_response) { generate(:adyen_payment_links_response) } @@ -396,26 +113,6 @@ end end - describe "#payment_method_params" do - subject(:payment_method_params) { adyen_service.__send__(:payment_method_params) } - - let(:params) do - { - merchantAccount: adyen_payment_provider.merchant_account, - shopperReference: adyen_customer.provider_customer_id - } - end - - before do - adyen_payment_provider - adyen_customer - end - - it "returns payment method params" do - expect(payment_method_params).to eq(params) - end - end - describe "#update_payment_status" do subject(:result) do adyen_service.update_payment_status(provider_payment_id:, status:) diff --git a/spec/services/payment_requests/payments/create_service_spec.rb b/spec/services/payment_requests/payments/create_service_spec.rb index 72b86354a04..de51c3dc5ba 100644 --- a/spec/services/payment_requests/payments/create_service_spec.rb +++ b/spec/services/payment_requests/payments/create_service_spec.rb @@ -3,21 +3,497 @@ require "rails_helper" RSpec.describe PaymentRequests::Payments::CreateService, type: :service do - subject(:create_service) { described_class.new(payment_request) } + subject(:create_service) { described_class.new(payable: payment_request, payment_provider: provider) } + + let(:organization) { create(:organization) } + let(:customer) { create(:customer, organization:, payment_provider: provider, payment_provider_code:) } + let(:provider) { "stripe" } + let(:payment_provider_code) { "stripe_1" } let(:payment_request) do - create(:payment_request, customer:, organization: customer.organization) + create( + :payment_request, + organization:, + customer:, + amount_cents: 799, + amount_currency: "USD", + invoices: [invoice_1, invoice_2] + ) + end + + let(:invoice_1) do + create( + :invoice, + organization:, + customer:, + total_amount_cents: 200, + currency: "USD", + ready_for_payment_processing: true + ) + end + + let(:invoice_2) do + create( + :invoice, + organization:, + customer:, + total_amount_cents: 599, + currency: "USD", + ready_for_payment_processing: true + ) end - let(:customer) { create(:customer, payment_provider:) } describe "#call" do + let(:payment_provider) { create(:stripe_provider, code: payment_provider_code, organization:) } + let(:provider_customer) { create(:stripe_customer, payment_provider:, customer:) } + let(:provider_class) { PaymentProviders::Stripe::Payments::CreateService } + let(:provider_service) { instance_double(provider_class) } + + let(:service_result) do + BaseService::Result.new.tap do |r| + r.payment = OpenStruct.new(payable_payment_status: "succeeded") + end + end + + before do + provider_customer + + allow(provider_class) + .to receive(:new) + .with( + payment: an_instance_of(Payment), + reference: "#{organization.name} - Overdue invoices", + metadata: { + lago_customer_id: customer.id, + lago_payable_id: payment_request.id, + lago_payable_type: "PaymentRequest" + } + ).and_return(provider_service) + allow(provider_service).to receive(:call!) + .and_return(service_result) + end + + context "with adyen payment provider" do + let(:provider) { "adyen" } + let(:payment_provider) { create(:adyen_provider, code: payment_provider_code, organization:) } + let(:provider_customer) { create(:adyen_customer, payment_provider:, customer:) } + + let(:provider_class) { PaymentProviders::Adyen::Payments::CreateService } + let(:provider_service) { instance_double(provider_class) } + + let(:service_result) do + BaseService::Result.new.tap do |r| + r.payment = OpenStruct.new(payable_payment_status: "succeeded") + end + end + + it 'creates a payment and calls the adyen service' do + result = create_service.call + + expect(result).to be_success + expect(result.payable).to eq(payment_request) + expect(result.payment).to be_present + + expect(result.payable).to be_payment_succeeded + expect(result.payable.payment_attempts).to eq(1) + expect(result.payable.ready_for_payment_processing).to eq(false) + + payment = result.payment + expect(payment.payment_provider).to eq(payment_provider) + expect(payment.payment_provider_customer).to eq(provider_customer) + expect(payment.amount_cents).to eq(payment_request.total_amount_cents) + expect(payment.amount_currency).to eq(payment_request.currency) + expect(payment.payable).to eq(payment_request) + + expect(provider_class).to have_received(:new) + expect(provider_service).to have_received(:call!) + end + + it "updates invoice payment status to succeeded" do + create_service.call + + expect(invoice_1.reload).to be_payment_succeeded + expect(invoice_2.reload).to be_payment_succeeded + end + + it "does not send a payment requested email" do + expect { create_service.call } + .not_to have_enqueued_mail(PaymentRequestMailer, :requested) + end + + context "when the payment fails" do + let(:service_result) do + BaseService::Result.new.tap do |r| + r.payment = OpenStruct.new(payable_payment_status: "failed") + end + end + + it "sends a payment requested email" do + expect { create_service.call } + .to have_enqueued_mail(PaymentRequestMailer, :requested) + .with(params: {payment_request:}, args: []) + end + end + end + + context "with gocardless payment provider" do + let(:provider) { "gocardless" } + let(:payment_provider) { create(:gocardless_provider, code: payment_provider_code, organization:) } + let(:provider_customer) { create(:gocardless_customer, payment_provider:, customer:) } + + let(:provider_class) { PaymentProviders::Gocardless::Payments::CreateService } + let(:provider_service) { instance_double(provider_class) } + + it 'creates a payment and calls the gocardless service' do + result = create_service.call + + expect(result).to be_success + expect(result.payable).to eq(payment_request) + expect(result.payment).to be_present + + expect(result.payable).to be_payment_succeeded + expect(result.payable.payment_attempts).to eq(1) + expect(result.payable.ready_for_payment_processing).to eq(false) + + payment = result.payment + expect(payment.payment_provider).to eq(payment_provider) + expect(payment.payment_provider_customer).to eq(provider_customer) + expect(payment.amount_cents).to eq(payment_request.total_amount_cents) + expect(payment.amount_currency).to eq(payment_request.currency) + expect(payment.payable).to eq(payment_request) + + expect(provider_class).to have_received(:new) + expect(provider_service).to have_received(:call!) + end + + it "updates invoice payment status to succeeded" do + create_service.call + + expect(invoice_1.reload).to be_payment_succeeded + expect(invoice_2.reload).to be_payment_succeeded + end + + it "does not send a payment requested email" do + expect { create_service.call } + .not_to have_enqueued_mail(PaymentRequestMailer, :requested) + end + + context "when the payment fails" do + let(:service_result) do + BaseService::Result.new.tap do |r| + r.payment = OpenStruct.new(payable_payment_status: "failed") + end + end + + it "sends a payment requested email" do + expect { create_service.call } + .to have_enqueued_mail(PaymentRequestMailer, :requested) + .with(params: {payment_request:}, args: []) + end + end + end + + context "with stripe payment provider" do + it 'creates a payment and calls the stripe service' do + result = create_service.call + + expect(result).to be_success + expect(result.payable).to eq(payment_request) + expect(result.payment).to be_present + + expect(result.payable).to be_payment_succeeded + expect(result.payable.payment_attempts).to eq(1) + expect(result.payable.ready_for_payment_processing).to eq(false) + + payment = result.payment + expect(payment.payment_provider).to eq(payment_provider) + expect(payment.payment_provider_customer).to eq(provider_customer) + expect(payment.amount_cents).to eq(payment_request.total_amount_cents) + expect(payment.amount_currency).to eq(payment_request.currency) + expect(payment.payable).to eq(payment_request) + + expect(provider_class).to have_received(:new) + expect(provider_service).to have_received(:call!) + end + + it "updates invoice payment status to succeeded" do + create_service.call + + expect(invoice_1.reload).to be_payment_succeeded + expect(invoice_2.reload).to be_payment_succeeded + end + + it "does not send a payment requested email" do + expect { create_service.call } + .not_to have_enqueued_mail(PaymentRequestMailer, :requested) + end + + context "when the payment fails" do + let(:service_result) do + BaseService::Result.new.tap do |r| + r.payment = OpenStruct.new(payable_payment_status: "failed") + end + end + + it "sends a payment requested email" do + expect { create_service.call } + .to have_enqueued_mail(PaymentRequestMailer, :requested) + .with(params: {payment_request:}, args: []) + end + end + end + + context "when payment request payment status is succeeded" do + let(:payment_request) do + create( + :payment_request, + organization:, + customer:, + payment_status: "succeeded", + amount_cents: 799, + amount_currency: "EUR", + invoices: [invoice_1, invoice_2] + ) + end + + it "does not creates a payment" do + result = create_service.call + + expect(result).to be_success + + expect(result.payable).to be_payment_succeeded + expect(result.payable.payment_attempts).to eq(0) + expect(result.payment).to be_nil + + expect(provider_class).not_to have_received(:new) + end + end + + context "with no payment provider" do + let(:payment_provider) { nil } + + it "does not creates a stripe payment", :aggregate_failures do + result = create_service.call + + expect(result).to be_success + + expect(result.payable).to eq(payment_request) + expect(result.payment).to be_nil + + expect(provider_class).not_to have_received(:new) + end + end + + context "with 0 amount" do + let(:payment_request) do + create( + :payment_request, + organization:, + customer:, + amount_cents: 0, + amount_currency: "EUR", + invoices: [invoice] + ) + end + + let(:invoice) do + create( + :invoice, + organization:, + customer:, + total_amount_cents: 0, + currency: "EUR" + ) + end + + it "does not creates a stripe payment", :aggregate_failures do + result = create_service.call + + expect(result).to be_success + expect(result.payable).to eq(payment_request) + expect(result.payment).to be_nil + expect(result.payable).to be_payment_succeeded + expect(provider_class).not_to have_received(:new) + end + end + + context "when customer does not have a provider customer id" do + before { provider_customer.update!(provider_customer_id: nil) } + + it "does not creates a stripe payment", :aggregate_failures do + result = create_service.call + + expect(result).to be_success + + expect(result.payable).to eq(payment_request) + expect(result.payment).to be_nil + expect(provider_class).not_to have_received(:new) + end + end + + context "when provider service raises a service failure" do + let(:service_result) do + BaseService::Result.new.tap do |r| + r.payment = OpenStruct.new(status: "pending", payable_payment_status: "pending") + r.error_message = "error" + r.error_code = "code" + r.reraise = true + end + end + + before do + allow(provider_service).to receive(:call!) + .and_raise(BaseService::ServiceFailure.new(service_result, code: "code", error_message: "error")) + end + + it "re-reaise the error and delivers an error webhook" do + expect { create_service.call } + .to raise_error(BaseService::ServiceFailure) + .and enqueue_job(SendWebhookJob) + .with( + "payment_request.payment_failure", + payment_request, + provider_customer_id: provider_customer.provider_customer_id, + provider_error: { + message: "error", + error_code: "code" + } + ).on_queue(:webhook) + end + + context "when payment has a payable_payment_status" do + let(:service_result) do + BaseService::Result.new.tap do |r| + r.payment = OpenStruct.new(payable_payment_status: "failed") + r.error_message = "error" + r.error_code = "code" + r.reraise = true + end + end + + it "updates the payment request payment status" do + expect { create_service.call } + .to raise_error(BaseService::ServiceFailure) + + expect(payment_request.reload).to be_payment_failed + end + end + + context "when payable_payment_status is pending" do + let(:service_result) do + BaseService::Result.new.tap do |r| + r.payment = OpenStruct.new(status: "failed", payable_payment_status: "pending") + r.error_message = "stripe_error" + r.error_code = "amount_too_small" + end + end + + it "re-reaise the error and delivers an error webhook" do + result = create_service.call + + expect(result).to be_success + expect(result.payable).to eq(payment_request) + expect(result.payment).to be_present + + expect(result.payment.status).to eq("failed") + expect(result.payment.payable_payment_status).to eq("pending") + + expect(provider_class).to have_received(:new) + expect(provider_service).to have_received(:call!) + end + end + end + + context "when payment status is processing" do + let(:service_result) do + BaseService::Result.new.tap do |r| + r.payment = OpenStruct.new(payable_payment_status: "pending", status: "processing") + end + end + + it "creates a payment" do + result = create_service.call + + expect(result).to be_success + expect(result.payable).to eq(payment_request) + expect(result.payment).to be_present + + expect(result.payable).to be_payment_pending + expect(result.payable.payment_attempts).to eq(1) + expect(result.payable.ready_for_payment_processing).to eq(false) + + payment = result.payment + expect(payment.payment_provider).to eq(payment_provider) + expect(payment.payment_provider_customer).to eq(provider_customer) + expect(payment.amount_cents).to eq(payment_request.total_amount_cents) + expect(payment.amount_currency).to eq(payment_request.currency) + expect(payment.payable_payment_status).to eq("pending") + expect(payment.payable).to eq(payment_request) + + expect(provider_class).to have_received(:new) + expect(provider_service).to have_received(:call!) + end + end + + context 'when a payment exits' do + let(:payment) do + create( + :payment, + payable: payment_request, + payment_provider:, + payment_provider_customer: provider_customer, + amount_cents: payment_request.total_amount_cents, + amount_currency: payment_request.currency, + status: "pending", + payable_payment_status: payment_status + ) + end + + let(:payment_status) { "pending" } + + before { payment } + + it "retrieves the payment for processing" do + result = create_service.call + + expect(result).to be_success + expect(result.payable).to eq(payment_request) + expect(result.payment).to eq(payment) + + expect(payment.payment_provider).to eq(payment_provider) + expect(payment.payment_provider_customer).to eq(provider_customer) + expect(payment.amount_cents).to eq(payment_request.total_amount_cents) + expect(payment.amount_currency).to eq(payment_request.currency) + + expect(provider_class).to have_received(:new) + expect(provider_service).to have_received(:call!) + end + + context "when payment is already processing" do + let(:payment_status) { "processing" } + + it "does not creates a payment" do + result = create_service.call + + expect(result).to be_success + expect(result.payable).to eq(payment_request) + expect(result.payment).to eq(payment) + + expect(provider_class).not_to have_received(:new) + expect(provider_service).not_to have_received(:call!) + end + end + end + end + + describe "#call_async" do context "with adyen payment provider" do let(:payment_provider) { "adyen" } it "enqueues a job to create a adyen payment" do expect do - create_service.call - end.to have_enqueued_job(PaymentRequests::Payments::AdyenCreateJob) + create_service.call_async + end.to have_enqueued_job(PaymentRequests::Payments::CreateJob) end end @@ -26,8 +502,8 @@ it "enqueues a job to create a gocardless payment" do expect do - create_service.call - end.to have_enqueued_job(PaymentRequests::Payments::GocardlessCreateJob) + create_service.call_async + end.to have_enqueued_job(PaymentRequests::Payments::CreateJob) end end @@ -36,8 +512,8 @@ it "enqueues a job to create a stripe payment" do expect do - create_service.call - end.to have_enqueued_job(PaymentRequests::Payments::StripeCreateJob) + create_service.call_async + end.to have_enqueued_job(PaymentRequests::Payments::CreateJob) end end end diff --git a/spec/services/payment_requests/payments/gocardless_service_spec.rb b/spec/services/payment_requests/payments/gocardless_service_spec.rb index 38b5a20a2e8..6ef66814978 100644 --- a/spec/services/payment_requests/payments/gocardless_service_spec.rb +++ b/spec/services/payment_requests/payments/gocardless_service_spec.rb @@ -48,233 +48,6 @@ ) end - describe "#create" do - before do - gocardless_payment_provider - gocardless_customer - - allow(GoCardlessPro::Client).to receive(:new) - .and_return(gocardless_client) - allow(gocardless_client).to receive(:mandates) - .and_return(gocardless_mandates_service) - allow(gocardless_mandates_service).to receive(:list) - .and_return(gocardless_list_response) - allow(gocardless_list_response).to receive(:records) - .and_return([GoCardlessPro::Resources::Mandate.new("id" => "mandate_id")]) - allow(gocardless_client).to receive(:payments) - .and_return(gocardless_payments_service) - allow(gocardless_payments_service).to receive(:create) - .and_return(GoCardlessPro::Resources::Payment.new( - "id" => "_ID_", - "amount" => payment_request.total_amount_cents, - "currency" => payment_request.currency, - "status" => "paid_out" - )) - allow(Invoices::PrepaidCreditJob).to receive(:perform_later) - end - - it "creates a gocardless payment", :aggregate_failures do - result = gocardless_service.create - - expect(result).to be_success - - expect(result.payable).to be_payment_succeeded - expect(result.payable.payment_attempts).to eq(1) - expect(result.payable.reload.ready_for_payment_processing).to eq(false) - - expect(result.payment.id).to be_present - expect(result.payment.payable).to eq(payment_request) - expect(result.payment.payment_provider).to eq(gocardless_payment_provider) - expect(result.payment.payment_provider_customer).to eq(gocardless_customer) - expect(result.payment.amount_cents).to eq(payment_request.total_amount_cents) - expect(result.payment.amount_currency).to eq(payment_request.currency) - expect(result.payment.status).to eq("paid_out") - expect(gocardless_customer.reload.provider_mandate_id).to eq("mandate_id") - - expect(gocardless_payments_service).to have_received(:create).with( - { - headers: { - "Idempotency-Key" => "#{payment_request.id}/1" - }, - params: - { - amount: 799, - currency: "USD", - links: {mandate: "mandate_id"}, - metadata: { - lago_customer_id: customer.id, - lago_payable_id: payment_request.id, - lago_payable_type: "PaymentRequest" - }, - retry_if_possible: false - } - } - ) - end - - it "updates invoice payment status to succeeded", :aggregate_failures do - gocardless_service.create - - 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 payment request payment status is already succeeded" do - let(:payment_request) do - create( - :payment_request, - organization:, - customer:, - payment_status: "succeeded", - amount_cents: 799, - amount_currency: "EUR" - ) - end - - it "does not creates a payment", :aggregate_failures do - result = gocardless_service.create - - expect(result).to be_success - expect(result.payable).to be_payment_succeeded - expect(result.payment).to be_nil - - expect(gocardless_payments_service).not_to have_received(:create) - end - end - - context "with no payment provider" do - let(:gocardless_payment_provider) { nil } - - it "does not creates a gocardless payment", :aggregate_failures do - result = gocardless_service.create - - expect(result).to be_success - expect(result.payable).to eq(payment_request) - expect(result.payment).to be_nil - expect(gocardless_payments_service).not_to have_received(:create) - end - end - - context "with 0 amount" do - let(:payment_request) do - create( - :payment_request, - organization:, - customer:, - amount_cents: 0, - amount_currency: "EUR", - invoices: [invoice] - ) - end - - let(:invoice) do - create( - :invoice, - organization:, - customer:, - total_amount_cents: 0, - currency: 'EUR' - ) - end - - it "does not creates a gocardless payment", :aggregate_failures do - result = gocardless_service.create - - expect(result).to be_success - expect(result.payable).to eq(payment_request) - expect(result.payment).to be_nil - expect(result.payable).to be_payment_succeeded - expect(gocardless_payments_service).not_to have_received(:create) - end - end - - context "when customer does not have a provider customer id" do - before { gocardless_customer.update!(provider_customer_id: nil) } - - it "does not creates a gocardless payment", :aggregate_failures do - result = gocardless_service.create - - expect(result).to be_success - expect(result.payable).to eq(payment_request) - expect(result.payment).to be_nil - expect(gocardless_payments_service).not_to have_received(:create) - end - end - - context "with error on gocardless" do - before do - allow(gocardless_payments_service).to receive(:create) - .and_raise(GoCardlessPro::Error.new("code" => "code", "message" => "error")) - end - - it "delivers an error webhook" do - gocardless_service.create - - expect(SendWebhookJob).to have_been_enqueued - .with( - "payment_request.payment_failure", - payment_request, - provider_customer_id: gocardless_customer.provider_customer_id, - provider_error: { - message: "error", - error_code: "code" - } - ) - end - - it "returns a service failure" do - result = gocardless_service.create - - expect(result).not_to be_success - expect(result.error.code).to eq("code") - expect(result.payable).to be_payment_failed - end - end - - context "when customer has no mandate to make a payment" do - before do - allow(gocardless_list_response).to receive(:records) - .and_return([]) - - allow(gocardless_payments_service).to receive(:create) - .and_raise(GoCardlessPro::Error.new("code" => "code", "message" => "error")) - end - - it "delivers an error webhook", :aggregate_failures do - result = gocardless_service.create - - expect(result).not_to be_success - expect(result.error).to be_a(BaseService::ServiceFailure) - expect(result.error.code).to eq("no_mandate_error") - expect(result.error.error_message).to eq("No mandate available for payment") - expect(result.payable.reload).to be_payment_failed - expect(result.payable.reload.ready_for_payment_processing).to eq(true) - - expect(SendWebhookJob).to have_been_enqueued - .with( - "payment_request.payment_failure", - payment_request, - provider_customer_id: gocardless_customer.provider_customer_id, - provider_error: { - message: "No mandate available for payment", - error_code: "no_mandate_error" - } - ) - end - - it "marks the payment request as payment failed" do - result = gocardless_service.create - - expect(result).not_to be_success - expect(result.error.code).to eq("no_mandate_error") - expect(payment_request.reload).to be_payment_failed - end - end - end - describe "#update_payment_status" do subject(:result) do gocardless_service.update_payment_status(provider_payment_id:, status:) diff --git a/spec/services/payment_requests/payments/stripe_service_spec.rb b/spec/services/payment_requests/payments/stripe_service_spec.rb index 17db961f8bd..df7bd59a8f0 100644 --- a/spec/services/payment_requests/payments/stripe_service_spec.rb +++ b/spec/services/payment_requests/payments/stripe_service_spec.rb @@ -47,355 +47,6 @@ ) end - describe "#create" do - let(:provider_customer_service) { instance_double(PaymentProviderCustomers::StripeService) } - - let(:provider_customer_service_result) do - BaseService::Result.new.tap do |result| - result.payment_method = Stripe::PaymentMethod.new(id: "pm_123456") - end - end - - let(:customer_response) do - File.read(Rails.root.join("spec/fixtures/stripe/customer_retrieve_response.json")) - end - - let(:payment_status) { "succeeded" } - - before do - stripe_payment_provider - stripe_customer - - allow(Stripe::PaymentIntent).to receive(:create) - .and_return( - Stripe::PaymentIntent.construct_from( - id: "ch_123456", - status: payment_status, - amount: payment_request.total_amount_cents, - currency: payment_request.currency - ) - ) - allow(SegmentTrackJob).to receive(:perform_later) - allow(PaymentProviderCustomers::StripeService).to receive(:new) - .and_return(provider_customer_service) - allow(provider_customer_service).to receive(:check_payment_method) - .and_return(provider_customer_service_result) - - stub_request(:get, "https://api.stripe.com/v1/customers/#{stripe_customer.provider_customer_id}") - .to_return(status: 200, body: customer_response, headers: {}) - end - - it "creates a stripe payment and a payment", :aggregate_failures do - result = stripe_service.create - - expect(result).to be_success - - expect(result.payable).to be_payment_succeeded - expect(result.payable.payment_attempts).to eq(1) - expect(result.payable.ready_for_payment_processing).to eq(false) - - expect(result.payment.id).to be_present - expect(result.payment.payable).to eq(payment_request) - expect(result.payment.payment_provider).to eq(stripe_payment_provider) - expect(result.payment.payment_provider_customer).to eq(stripe_customer) - expect(result.payment.amount_cents).to eq(payment_request.total_amount_cents) - expect(result.payment.amount_currency).to eq(payment_request.currency) - expect(result.payment.status).to eq("succeeded") - - expect(Stripe::PaymentIntent).to have_received(:create).with( - { - amount: payment_request.total_amount_cents, - currency: payment_request.currency.downcase, - customer: customer.stripe_customer.provider_customer_id, - payment_method: stripe_payment_method_id, - payment_method_types: customer.stripe_customer.provider_payment_methods, - confirm: true, - off_session: true, - error_on_requires_action: true, - description: "#{organization.name} - Overdue invoices", - metadata: { - lago_customer_id: customer.id, - lago_payable_id: payment_request.id, - lago_payable_type: "PaymentRequest" - } - }, - hash_including( - { - api_key: an_instance_of(String), - idempotency_key: "#{payment_request.id}/#{payment_request.reload.payment_attempts}" - } - ) - ) - end - - it "updates invoice payment status to succeeded", :aggregate_failures do - stripe_service.create - - expect(invoice_1.reload).to be_payment_succeeded - expect(invoice_2.reload).to be_payment_succeeded - end - - context "when payment request payment status is succeeded" do - let(:payment_request) do - create( - :payment_request, - organization:, - customer:, - payment_status: "succeeded", - amount_cents: 799, - amount_currency: "EUR", - invoices: [invoice_1, invoice_2] - ) - end - - it "does not creates a payment", :aggregate_failures do - result = stripe_service.create - - expect(result).to be_success - - expect(result.payable).to be_payment_succeeded - expect(result.payable.payment_attempts).to eq(0) - expect(result.payment).to be_nil - - expect(Stripe::PaymentIntent).not_to have_received(:create) - end - end - - context "with no payment provider" do - let(:stripe_payment_provider) { nil } - - it "does not creates a stripe payment", :aggregate_failures do - result = stripe_service.create - - expect(result).to be_success - - expect(result.payable).to eq(payment_request) - expect(result.payment).to be_nil - - expect(Stripe::PaymentIntent).not_to have_received(:create) - end - end - - context "with 0 amount" do - let(:payment_request) do - create( - :payment_request, - organization:, - customer:, - amount_cents: 0, - amount_currency: "EUR", - invoices: [invoice] - ) - end - - let(:invoice) do - create( - :invoice, - organization:, - customer:, - total_amount_cents: 0, - currency: "EUR" - ) - end - - it "does not creates a stripe payment", :aggregate_failures do - result = stripe_service.create - - expect(result).to be_success - expect(result.payable).to eq(payment_request) - expect(result.payment).to be_nil - expect(result.payable).to be_payment_succeeded - expect(Stripe::PaymentIntent).not_to have_received(:create) - end - end - - context "when customer does not have a provider customer id" do - before { stripe_customer.update!(provider_customer_id: nil) } - - it "does not creates a stripe payment", :aggregate_failures do - result = stripe_service.create - - expect(result).to be_success - - expect(result.payable).to eq(payment_request) - expect(result.payment).to be_nil - - expect(Stripe::PaymentIntent).not_to have_received(:create) - end - end - - context "when customer does not have a payment method" do - let(:stripe_customer) { create(:stripe_customer, customer:) } - - before do - allow(Stripe::Customer).to receive(:retrieve) - .and_return(Stripe::StripeObject.construct_from( - { - invoice_settings: { - default_payment_method: nil - }, - default_source: nil - } - )) - - allow(Stripe::PaymentMethod).to receive(:list) - .and_return(Stripe::ListObject.construct_from( - data: [ - { - id: "pm_123456", - object: "payment_method", - card: {brand: "visa"}, - created: 1_656_422_973, - customer: "cus_123456", - livemode: false, - metadata: {}, - type: "card" - } - ] - )) - end - - it "retrieves the payment method" do - result = stripe_service.create - - expect(result).to be_success - expect(customer.stripe_customer.reload).to be_present - expect(customer.stripe_customer.provider_customer_id).to eq(stripe_customer.provider_customer_id) - expect(customer.stripe_customer.payment_method_id).to eq("pm_123456") - - expect(Stripe::PaymentMethod).to have_received(:list) - expect(Stripe::PaymentIntent).to have_received(:create) - end - end - - context "with card error on stripe" do - let(:customer) { create(:customer, organization:, payment_provider_code: code) } - - let(:organization) do - create(:organization, webhook_url: "https://webhook.com") - end - - before do - allow(Stripe::PaymentIntent).to receive(:create) - .and_raise(::Stripe::CardError.new("error", {})) - allow(PaymentRequests::Payments::DeliverErrorWebhookService) - .to receive(:call_async) - .and_call_original - end - - it "delivers an error webhook" do - stripe_service.create - - expect(PaymentRequests::Payments::DeliverErrorWebhookService).to have_received(:call_async) - expect(SendWebhookJob).to have_been_enqueued - .with( - "payment_request.payment_failure", - payment_request, - provider_customer_id: stripe_customer.provider_customer_id, - provider_error: { - message: "error", - error_code: nil - } - ) - end - - it "marks the payment request as payment failed" do - result = stripe_service.create - - expect(result).to be_success - expect(result.payable).to be_payment_failed - end - end - - context "when payment request has a too small amount" do - let(:organization) { create(:organization) } - let(:customer) { create(:customer, organization:) } - - let(:payment_request) do - create( - :payment_request, - organization:, - customer:, - amount_cents: 20, - amount_currency: "EUR", - ready_for_payment_processing: true - ) - end - - before do - allow(Stripe::PaymentIntent).to receive(:create) - .and_raise(::Stripe::InvalidRequestError.new("amount_too_small", {}, code: "amount_too_small")) - end - - it "does not mark the payment request as failed" do - result = stripe_service.create - - expect(result).to be_success - expect(payment_request.reload).to be_payment_pending - end - end - - context "with random stripe error" do - let(:customer) { create(:customer, organization:, payment_provider_code: code) } - - let(:organization) do - create(:organization, webhook_url: "https://webhook.com") - end - - let(:stripe_error) { Stripe::StripeError.new("error") } - - before do - allow(Stripe::PaymentIntent).to receive(:create) - .and_raise(stripe_error) - allow(PaymentRequests::Payments::DeliverErrorWebhookService) - .to receive(:call_async) - .and_call_original - end - - it "delivers an error webhook and raises the error" do - expect { stripe_service.create } - .to raise_error(stripe_error) - .and not_change { payment_request.reload.payment_status } - - expect(PaymentRequests::Payments::DeliverErrorWebhookService).to have_received(:call_async) - expect(SendWebhookJob).to have_been_enqueued - .with( - "payment_request.payment_failure", - payment_request, - provider_customer_id: stripe_customer.provider_customer_id, - provider_error: { - message: "error", - error_code: nil - } - ) - end - end - - context "when payment status is processing" do - let(:payment_status) { "processing" } - - it "creates a stripe payment and a payment", :aggregate_failures do - result = stripe_service.create - - expect(result).to be_success - - expect(result.payable).to be_payment_pending - expect(result.payable.payment_attempts).to eq(1) - expect(result.payable.ready_for_payment_processing).to eq(false) - - expect(result.payment.id).to be_present - expect(result.payment.payable).to eq(payment_request) - expect(result.payment.payment_provider).to eq(stripe_payment_provider) - expect(result.payment.payment_provider_customer).to eq(stripe_customer) - expect(result.payment.amount_cents).to eq(payment_request.total_amount_cents) - expect(result.payment.amount_currency).to eq(payment_request.currency) - expect(result.payment.status).to eq("processing") - - expect(Stripe::PaymentIntent).to have_received(:create) - end - end - end - describe "#generate_payment_url" do before do stripe_payment_provider