From d12a15b36f1ddaacdc552f71145f674f9bc86332 Mon Sep 17 00:00:00 2001 From: Vincent Pochet Date: Tue, 12 Nov 2024 17:40:59 +0100 Subject: [PATCH] misc(stripe): Refact payment method update webhooks --- .../invoices/payments/stripe_create_job.rb | 4 +- .../stripe_checkout_url_job.rb | 6 +- .../stripe_create_job.rb | 6 +- .../payments/stripe_create_job.rb | 4 +- .../credit_notes/refunds/stripe_service.rb | 2 +- .../invoices/payments/stripe_service.rb | 10 +- .../stripe/update_payment_method_service.rb | 46 ++++ .../stripe_service.rb | 79 +----- .../stripe/register_webhook_service.rb | 2 +- .../payment_providers/stripe_service.rb | 36 +-- .../webhooks/base_service.rb | 20 ++ .../stripe/charge_dispute_closed_service.rb | 4 - .../stripe/customer_updated_service.rb | 37 +++ .../stripe/setup_intent_succeeded_service.rb | 58 ++++ .../payments/stripe_service.rb | 10 +- .../stripe/customer_updated_event.json | 9 +- .../customer_updated_event_with_metadata.json | 54 ++++ .../setup_intent_event_with_metadata.json | 50 ++++ .../setup_intent_event_without_customer.json | 48 ++++ .../api/v1/customers_controller_spec.rb | 4 +- .../api/v1/invoices_controller_spec.rb | 2 +- .../refunds/stripe_service_spec.rb | 4 +- .../generate_payment_url_service_spec.rb | 2 +- .../invoices/payments/stripe_service_spec.rb | 12 +- .../update_payment_method_service_spec.rb | 72 +++++ .../stripe_service_spec.rb | 253 +----------------- .../stripe/register_webhook_service_spec.rb | 2 +- .../payment_providers/stripe_service_spec.rb | 26 +- .../stripe/customer_updated_service_spec.rb | 66 +++++ .../setup_intent_succeeded_service_spec.rb | 88 ++++++ .../generate_payment_url_service_spec.rb | 2 +- .../payments/stripe_service_spec.rb | 10 +- 32 files changed, 615 insertions(+), 413 deletions(-) create mode 100644 app/services/payment_provider_customers/stripe/update_payment_method_service.rb create mode 100644 app/services/payment_providers/webhooks/stripe/customer_updated_service.rb create mode 100644 app/services/payment_providers/webhooks/stripe/setup_intent_succeeded_service.rb create mode 100644 spec/fixtures/stripe/customer_updated_event_with_metadata.json create mode 100644 spec/fixtures/stripe/setup_intent_event_with_metadata.json create mode 100644 spec/fixtures/stripe/setup_intent_event_without_customer.json create mode 100644 spec/services/payment_provider_customers/stripe/update_payment_method_service_spec.rb create mode 100644 spec/services/payment_providers/webhooks/stripe/customer_updated_service_spec.rb create mode 100644 spec/services/payment_providers/webhooks/stripe/setup_intent_succeeded_service_spec.rb diff --git a/app/jobs/invoices/payments/stripe_create_job.rb b/app/jobs/invoices/payments/stripe_create_job.rb index e7c87bfd88d..d64e5788091 100644 --- a/app/jobs/invoices/payments/stripe_create_job.rb +++ b/app/jobs/invoices/payments/stripe_create_job.rb @@ -7,8 +7,8 @@ class StripeCreateJob < ApplicationJob unique :until_executed, on_conflict: :log - retry_on Stripe::RateLimitError, wait: :polynomially_longer, attempts: 6 - retry_on Stripe::APIConnectionError, 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(invoice) result = Invoices::Payments::StripeService.new(invoice).create diff --git a/app/jobs/payment_provider_customers/stripe_checkout_url_job.rb b/app/jobs/payment_provider_customers/stripe_checkout_url_job.rb index a744fec9199..37fe21a8d05 100644 --- a/app/jobs/payment_provider_customers/stripe_checkout_url_job.rb +++ b/app/jobs/payment_provider_customers/stripe_checkout_url_job.rb @@ -4,9 +4,9 @@ module PaymentProviderCustomers class StripeCheckoutUrlJob < ApplicationJob queue_as :providers - retry_on Stripe::APIConnectionError, wait: :polynomially_longer, attempts: 6 - retry_on Stripe::APIError, wait: :polynomially_longer, attempts: 6 - retry_on Stripe::RateLimitError, wait: :polynomially_longer, attempts: 6 + retry_on ::Stripe::APIConnectionError, wait: :polynomially_longer, attempts: 6 + retry_on ::Stripe::APIError, wait: :polynomially_longer, attempts: 6 + retry_on ::Stripe::RateLimitError, wait: :polynomially_longer, attempts: 6 retry_on ActiveJob::DeserializationError def perform(stripe_customer) diff --git a/app/jobs/payment_provider_customers/stripe_create_job.rb b/app/jobs/payment_provider_customers/stripe_create_job.rb index e7410a6b57b..8dbb11c7e48 100644 --- a/app/jobs/payment_provider_customers/stripe_create_job.rb +++ b/app/jobs/payment_provider_customers/stripe_create_job.rb @@ -4,9 +4,9 @@ module PaymentProviderCustomers class StripeCreateJob < ApplicationJob queue_as :providers - retry_on Stripe::APIConnectionError, wait: :polynomially_longer, attempts: 6 - retry_on Stripe::APIError, wait: :polynomially_longer, attempts: 6 - retry_on Stripe::RateLimitError, wait: :polynomially_longer, attempts: 6 + retry_on ::Stripe::APIConnectionError, wait: :polynomially_longer, attempts: 6 + retry_on ::Stripe::APIError, wait: :polynomially_longer, attempts: 6 + retry_on ::Stripe::RateLimitError, wait: :polynomially_longer, attempts: 6 retry_on ActiveJob::DeserializationError def perform(stripe_customer) diff --git a/app/jobs/payment_requests/payments/stripe_create_job.rb b/app/jobs/payment_requests/payments/stripe_create_job.rb index fa183a1febd..9bfcbc42cf3 100644 --- a/app/jobs/payment_requests/payments/stripe_create_job.rb +++ b/app/jobs/payment_requests/payments/stripe_create_job.rb @@ -7,8 +7,8 @@ class StripeCreateJob < ApplicationJob unique :until_executed, on_conflict: :log - retry_on Stripe::RateLimitError, wait: :polynomially_longer, attempts: 6 - retry_on Stripe::APIConnectionError, 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) result = PaymentRequests::Payments::StripeService.new(payable).create diff --git a/app/services/credit_notes/refunds/stripe_service.rb b/app/services/credit_notes/refunds/stripe_service.rb index 4e70c7e55c1..6ad579008b0 100644 --- a/app/services/credit_notes/refunds/stripe_service.rb +++ b/app/services/credit_notes/refunds/stripe_service.rb @@ -86,7 +86,7 @@ def create_stripe_refund idempotency_key: credit_note.id } ) - rescue Stripe::InvalidRequestError => e + rescue ::Stripe::InvalidRequestError => e deliver_error_webhook(message: e.message, code: e.code) update_credit_note_status(:failed) diff --git a/app/services/invoices/payments/stripe_service.rb b/app/services/invoices/payments/stripe_service.rb index 68606da16a7..fdad206e114 100644 --- a/app/services/invoices/payments/stripe_service.rb +++ b/app/services/invoices/payments/stripe_service.rb @@ -56,7 +56,7 @@ def create result.payment = payment result - rescue Stripe::AuthenticationError, Stripe::CardError, Stripe::InvalidRequestError, Stripe::PermissionError => e + rescue ::Stripe::AuthenticationError, ::Stripe::CardError, ::Stripe::InvalidRequestError, Stripe::PermissionError => e # NOTE: Do not mark the invoice as failed if the amount is too small for Stripe # For now we keep it as pending, the user can still update it manually return result if e.code == 'amount_too_small' @@ -64,9 +64,9 @@ def create deliver_error_webhook(e) update_invoice_payment_status(payment_status: :failed, deliver_webhook: false) result - rescue Stripe::RateLimitError, Stripe::APIConnectionError + rescue ::Stripe::RateLimitError, Stripe::APIConnectionError raise # Let the auto-retry process do its own job - rescue Stripe::StripeError => e + rescue ::Stripe::StripeError => e deliver_error_webhook(e) raise end @@ -98,7 +98,7 @@ def update_payment_status(organization_id:, provider_payment_id:, status:, metad def generate_payment_url return result unless should_process_payment? - res = Stripe::Checkout::Session.create( + res = ::Stripe::Checkout::Session.create( payment_url_payload, { api_key: stripe_api_key @@ -108,7 +108,7 @@ def generate_payment_url result.payment_url = res['url'] result - rescue Stripe::CardError, Stripe::InvalidRequestError, Stripe::AuthenticationError, Stripe::PermissionError => e + rescue ::Stripe::CardError, ::Stripe::InvalidRequestError, ::Stripe::AuthenticationError, Stripe::PermissionError => e deliver_error_webhook(e) result.single_validation_failure!(error_code: 'payment_provider_error') diff --git a/app/services/payment_provider_customers/stripe/update_payment_method_service.rb b/app/services/payment_provider_customers/stripe/update_payment_method_service.rb new file mode 100644 index 00000000000..09fb478a161 --- /dev/null +++ b/app/services/payment_provider_customers/stripe/update_payment_method_service.rb @@ -0,0 +1,46 @@ +# frozen_String_literal: true + +module PaymentProviderCustomers + module Stripe + class UpdatePaymentMethodService < BaseService + def initialize(stripe_customer:, payment_method_id:) + @stripe_customer = stripe_customer + @payment_method_id = payment_method_id + + super + end + + def call + return result.not_found_failure!(resource: 'stripe_customer') unless stripe_customer + + stripe_customer.payment_method_id = payment_method_id + stripe_customer.save! + + reprocess_pending_invoices + + result.stripe_customer = stripe_customer + result + end + + private + + attr_reader :stripe_customer, :payment_method_id + + delegate :customer, to: :stripe_customer + + def reprocess_pending_invoices + invoices = customer.invoices + .payment_pending + .where(ready_for_payment_processing: true) + .where(status: 'finalized') + + invoices.find_each do |invoice| + Invoices::Payments::StripeCreateJob.perform_later(invoice) + rescue ActiveJob::Uniqueness::JobNotUnique + # NOTE: Payment is already enqueued for processing + Rails.logger.warn("Duplicated payment attempt for invoice #{invoice.id}") + end + end + end + end +end diff --git a/app/services/payment_provider_customers/stripe_service.rb b/app/services/payment_provider_customers/stripe_service.rb index a9ed291de89..875ffa920f6 100644 --- a/app/services/payment_provider_customers/stripe_service.rb +++ b/app/services/payment_provider_customers/stripe_service.rb @@ -31,58 +31,19 @@ def create def update return result if !stripe_payment_provider || stripe_customer.provider_customer_id.blank? - Stripe::Customer.update(stripe_customer.provider_customer_id, stripe_update_payload, {api_key:}) + ::Stripe::Customer.update(stripe_customer.provider_customer_id, stripe_update_payload, {api_key:}) result - rescue Stripe::InvalidRequestError, Stripe::PermissionError => e + rescue ::Stripe::InvalidRequestError, ::Stripe::PermissionError => e deliver_error_webhook(e) result.service_failure!(code: 'stripe_error', message: e.message) - rescue Stripe::AuthenticationError => e + rescue ::Stripe::AuthenticationError => e deliver_error_webhook(e) message = ['Stripe authentication failed.', e.message.presence].compact.join(' ') result.unauthorized_failure!(message:) end - def update_payment_method(organization_id:, stripe_customer_id:, payment_method_id:, metadata: {}) - @stripe_customer = PaymentProviderCustomers::StripeCustomer - .joins(:customer) - .where(customers: {organization_id:}) - .find_by(provider_customer_id: stripe_customer_id) - return handle_missing_customer(organization_id, metadata) unless stripe_customer - - stripe_customer.payment_method_id = payment_method_id - stripe_customer.save! - - reprocess_pending_invoices(customer) - - result.stripe_customer = stripe_customer - result - rescue ActiveRecord::RecordInvalid => e - result.record_validation_failure!(record: e.record) - end - - def update_provider_default_payment_method(organization_id:, stripe_customer_id:, payment_method_id:, metadata: {}) - return result.not_found_failure!(resource: 'stripe_customer') unless stripe_customer_id - - @stripe_customer = PaymentProviderCustomers::StripeCustomer - .joins(:customer) - .where(customers: {organization_id:}) - .find_by(provider_customer_id: stripe_customer_id) - return handle_missing_customer(organization_id, metadata) unless stripe_customer - - Stripe::Customer.update( - stripe_customer_id, - {invoice_settings: {default_payment_method: payment_method_id}}, - {api_key:} - ) - - result.payment_method = payment_method_id - result - rescue Stripe::InvalidRequestError => e - result.service_failure!(code: 'stripe_error', message: e.message) - end - def delete_payment_method(organization_id:, stripe_customer_id:, payment_method_id:, metadata: {}) @stripe_customer = PaymentProviderCustomers::StripeCustomer .joins(:customer) @@ -100,12 +61,12 @@ def delete_payment_method(organization_id:, stripe_customer_id:, payment_method_ end def check_payment_method(payment_method_id) - payment_method = Stripe::Customer.new(id: stripe_customer.provider_customer_id) + payment_method = ::Stripe::Customer.new(id: stripe_customer.provider_customer_id) .retrieve_payment_method(payment_method_id, {}, {api_key:}) result.payment_method = payment_method result - rescue Stripe::InvalidRequestError + rescue ::Stripe::InvalidRequestError # NOTE: The payment method is no longer valid stripe_customer.update!(payment_method_id: nil) @@ -116,7 +77,7 @@ def generate_checkout_url(send_webhook: true) return result unless customer # NOTE: Customer is nil when deleted. return result if customer.organization.webhook_endpoints.none? && send_webhook && payment_provider(customer) - res = Stripe::Checkout::Session.create( + res = ::Stripe::Checkout::Session.create( checkout_link_params, { api_key: @@ -134,10 +95,10 @@ def generate_checkout_url(send_webhook: true) end result - rescue Stripe::InvalidRequestError, Stripe::PermissionError => e + rescue ::Stripe::InvalidRequestError, ::Stripe::PermissionError => e deliver_error_webhook(e) result - rescue Stripe::AuthenticationError => e + rescue ::Stripe::AuthenticationError => e deliver_error_webhook(e) message = ['Stripe authentication failed.', e.message.presence].compact.join(' ') @@ -177,23 +138,23 @@ def success_redirect_url end def create_stripe_customer - Stripe::Customer.create( + ::Stripe::Customer.create( stripe_create_payload, { api_key:, idempotency_key: [customer.id, customer.updated_at.to_i].join('-') } ) - rescue Stripe::InvalidRequestError, Stripe::PermissionError => e + rescue ::Stripe::InvalidRequestError, ::Stripe::PermissionError => e deliver_error_webhook(e) nil - rescue Stripe::AuthenticationError => e + rescue ::Stripe::AuthenticationError => e deliver_error_webhook(e) message = ['Stripe authentication failed.', e.message.presence].compact.join(' ') result.unauthorized_failure!(message:) - rescue Stripe::IdempotencyError - stripe_customers = Stripe::Customer.list({email: customer.email}, {api_key:}) + rescue ::Stripe::IdempotencyError + stripe_customers = ::Stripe::Customer.list({email: customer.email}, {api_key:}) return stripe_customers.first if stripe_customers.count == 1 # NOTE: Multiple stripe customers with the same email, @@ -255,20 +216,6 @@ def deliver_error_webhook(stripe_error) ) end - def reprocess_pending_invoices(customer) - invoices = customer.invoices - .payment_pending - .where(ready_for_payment_processing: true) - .where(status: 'finalized') - - invoices.find_each do |invoice| - Invoices::Payments::StripeCreateJob.perform_later(invoice) - rescue ActiveJob::Uniqueness::JobNotUnique - # NOTE: Payment is already enqueued for processing - Rails.logger.warn("Duplicated payment attempt for invoice #{invoice.id}") - end - end - def handle_missing_customer(organization_id, metadata) # NOTE: Stripe customer was not created from lago return result unless metadata&.key?(:lago_customer_id) diff --git a/app/services/payment_providers/stripe/register_webhook_service.rb b/app/services/payment_providers/stripe/register_webhook_service.rb index 8b60a3c26b0..4fa6fcd2c88 100644 --- a/app/services/payment_providers/stripe/register_webhook_service.rb +++ b/app/services/payment_providers/stripe/register_webhook_service.rb @@ -24,7 +24,7 @@ def call rescue ::Stripe::AuthenticationError => e deliver_error_webhook(action: 'payment_provider.register_webhook', error: e) result - rescue ::Stripe::InvalidRequestError => e + rescue ::Stripe::PermissionError => e raise if e.message != "You have reached the maximum of 16 test webhook endpoints." deliver_error_webhook(action: 'payment_provider.register_webhook', error: e) diff --git a/app/services/payment_providers/stripe_service.rb b/app/services/payment_providers/stripe_service.rb index f26fd75248a..1b00b1426af 100644 --- a/app/services/payment_providers/stripe_service.rb +++ b/app/services/payment_providers/stripe_service.rb @@ -93,35 +93,15 @@ def handle_event(organization:, event_json:) case event.type when 'setup_intent.succeeded' - service = PaymentProviderCustomers::StripeService.new - - service - .update_provider_default_payment_method( - organization_id: organization.id, - stripe_customer_id: event.data.object.customer, - payment_method_id: event.data.object.payment_method, - metadata: event.data.object.metadata.to_h.symbolize_keys - ).raise_if_error! - - service - .update_payment_method( - organization_id: organization.id, - stripe_customer_id: event.data.object.customer, - payment_method_id: event.data.object.payment_method, - metadata: event.data.object.metadata.to_h.symbolize_keys - ).raise_if_error! + PaymentProviders::Webhooks::Stripe::SetupIntentSucceededService.call( + organization_id: organization.id, + event_json: + ).raise_if_error! when 'customer.updated' - payment_method_id = event.data.object.invoice_settings.default_payment_method || - event.data.object.default_source - - PaymentProviderCustomers::StripeService - .new - .update_payment_method( - organization_id: organization.id, - stripe_customer_id: event.data.object.id, - payment_method_id:, - metadata: event.data.object.metadata.to_h.symbolize_keys - ).raise_if_error! + PaymentProviders::Webhooks::Stripe::CustomerUpdatedService.call( + organization_id: organization.id, + event_json: + ).raise_if_error! when 'charge.succeeded' payment_service_klass(event) .new.update_payment_status( diff --git a/app/services/payment_providers/webhooks/base_service.rb b/app/services/payment_providers/webhooks/base_service.rb index 2db4bdeddce..9e0268f822f 100644 --- a/app/services/payment_providers/webhooks/base_service.rb +++ b/app/services/payment_providers/webhooks/base_service.rb @@ -13,6 +13,26 @@ def initialize(organization_id:, event_json:) private attr_reader :organization, :event_json + + def event + @event ||= ::Stripe::Event.construct_from(JSON.parse(event_json)) + end + + def metadata + @metadata ||= event.data.object.metadata.to_h.symbolize_keys + end + + def handle_missing_customer + # NOTE: Stripe customer was not created from lago + return result unless metadata&.key?(:lago_customer_id) + + # NOTE: Customer does not belong to this lago instance or + # exists but does not belong to the organizations + # (Happens when the Stripe API key is shared between organizations) + return result if Customer.find_by(id: metadata[:lago_customer_id], organization_id: organization.id).nil? + + result.not_found_failure!(resource: 'stripe_customer') + end end end end diff --git a/app/services/payment_providers/webhooks/stripe/charge_dispute_closed_service.rb b/app/services/payment_providers/webhooks/stripe/charge_dispute_closed_service.rb index dc21a75a5bc..bd4e5086392 100644 --- a/app/services/payment_providers/webhooks/stripe/charge_dispute_closed_service.rb +++ b/app/services/payment_providers/webhooks/stripe/charge_dispute_closed_service.rb @@ -21,10 +21,6 @@ def call private - def event - @event ||= ::Stripe::Event.construct_from(JSON.parse(event_json)) - end - def payment_dispute_lost_at Time.zone.at(event.created) end diff --git a/app/services/payment_providers/webhooks/stripe/customer_updated_service.rb b/app/services/payment_providers/webhooks/stripe/customer_updated_service.rb new file mode 100644 index 00000000000..913bedfd3b2 --- /dev/null +++ b/app/services/payment_providers/webhooks/stripe/customer_updated_service.rb @@ -0,0 +1,37 @@ +# frozen_string_literal: true + +module PaymentProviders + module Webhooks + module Stripe + class CustomerUpdatedService < BaseService + def call + return handle_missing_customer unless stripe_customer + + PaymentProviderCustomers::Stripe::UpdatePaymentMethodService.call( + stripe_customer:, + payment_method_id: payment_method_id + ) + rescue ActiveRecord::RecordInvalid => e + result.record_validation_failure!(record: e.record) + end + + private + + def stripe_customer_id + event.data.object.id + end + + def stripe_customer + @stripe_customer ||= PaymentProviderCustomers::StripeCustomer + .joins(:customer) + .where(customers: {organization_id: organization.id}) + .find_by(provider_customer_id: stripe_customer_id) + end + + def payment_method_id + event.data.object.invoice_settings.default_payment_method || event.data.object.default_source + end + end + end + end +end diff --git a/app/services/payment_providers/webhooks/stripe/setup_intent_succeeded_service.rb b/app/services/payment_providers/webhooks/stripe/setup_intent_succeeded_service.rb new file mode 100644 index 00000000000..15003b93069 --- /dev/null +++ b/app/services/payment_providers/webhooks/stripe/setup_intent_succeeded_service.rb @@ -0,0 +1,58 @@ +# frozen_string_literal: true + +module PaymentProviders + module Webhooks + module Stripe + class SetupIntentSucceededService < BaseService + include Customers::PaymentProviderFinder + + def call + return result if stripe_customer_id.nil? + return handle_missing_customer unless stripe_customer + + update_stripe_customer_default_payment_method + result.payment_method_id = payment_method_id + + PaymentProviderCustomers::Stripe::UpdatePaymentMethodService.call( + stripe_customer:, + payment_method_id: payment_method_id + ).raise_if_error! + + result.stripe_customer = stripe_customer + result + rescue ::Stripe::PermissionError => e + result.service_failure!(code: 'stripe_error', message: e.message) + end + + private + + def stripe_customer + @stripe_customer ||= PaymentProviderCustomers::StripeCustomer + .joins(:customer) + .where(customers: {organization_id: organization.id}) + .find_by(provider_customer_id: stripe_customer_id) + end + + def stripe_customer_id + event.data.object.customer + end + + def payment_method_id + event.data.object.payment_method + end + + def update_stripe_customer_default_payment_method + ::Stripe::Customer.update( + stripe_customer_id, + {invoice_settings: {default_payment_method: payment_method_id}}, + {api_key: stripe_payment_provider.secret_key} + ) + end + + def stripe_payment_provider + @stripe_payment_provider ||= payment_provider(stripe_customer.customer) + end + end + end + end +end diff --git a/app/services/payment_requests/payments/stripe_service.rb b/app/services/payment_requests/payments/stripe_service.rb index 77c30a8ef19..0e8a8942c85 100644 --- a/app/services/payment_requests/payments/stripe_service.rb +++ b/app/services/payment_requests/payments/stripe_service.rb @@ -55,7 +55,7 @@ def create result.payment = payment result - rescue Stripe::AuthenticationError, Stripe::CardError, Stripe::InvalidRequestError, Stripe::PermissionError => e + 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" @@ -63,9 +63,9 @@ def create deliver_error_webhook(e) update_payable_payment_status(payment_status: :failed, deliver_webhook: false) result - rescue Stripe::RateLimitError, Stripe::APIConnectionError + rescue ::Stripe::RateLimitError, ::Stripe::APIConnectionError raise # Let the auto-retry process do its own job - rescue Stripe::StripeError => e + rescue ::Stripe::StripeError => e deliver_error_webhook(e) raise end @@ -73,7 +73,7 @@ def create def generate_payment_url return result unless should_process_payment? - result_url = Stripe::Checkout::Session.create( + result_url = ::Stripe::Checkout::Session.create( payment_url_payload, { api_key: stripe_api_key @@ -83,7 +83,7 @@ def generate_payment_url result.payment_url = result_url["url"] result - rescue Stripe::CardError, Stripe::InvalidRequestError, Stripe::AuthenticationError, Stripe::PermissionError => e + rescue ::Stripe::CardError, ::Stripe::InvalidRequestError, ::Stripe::AuthenticationError, Stripe::PermissionError => e deliver_error_webhook(e) result.single_validation_failure!(error_code: "payment_provider_error") diff --git a/spec/fixtures/stripe/customer_updated_event.json b/spec/fixtures/stripe/customer_updated_event.json index f25f296b151..5ab21c7172e 100644 --- a/spec/fixtures/stripe/customer_updated_event.json +++ b/spec/fixtures/stripe/customer_updated_event.json @@ -25,15 +25,10 @@ "rendering_options": null }, "livemode": false, - "metadata": { - "lago_customer_id": "123456-1234-1234-1234-1234567890", - "customer_id": "test_5" - }, + "metadata": {}, "name": "Test 5", "phone": null, - "preferred_locales": [ - - ], + "preferred_locales": [], "shipping": null, "tax_exempt": "none", "test_clock": null diff --git a/spec/fixtures/stripe/customer_updated_event_with_metadata.json b/spec/fixtures/stripe/customer_updated_event_with_metadata.json new file mode 100644 index 00000000000..f25f296b151 --- /dev/null +++ b/spec/fixtures/stripe/customer_updated_event_with_metadata.json @@ -0,0 +1,54 @@ +{ + "id": "evt_123456789", + "object": "event", + "api_version": "2020-08-27", + "created": 1688816058, + "data": { + "object": { + "id": "cus_123456789", + "object": "customer", + "address": null, + "balance": 0, + "created": 1688679325, + "currency": null, + "default_currency": null, + "default_source": "card_123456789", + "delinquent": false, + "description": null, + "discount": null, + "email": "test@getlago.com", + "invoice_prefix": "123456", + "invoice_settings": { + "custom_fields": null, + "default_payment_method": null, + "footer": null, + "rendering_options": null + }, + "livemode": false, + "metadata": { + "lago_customer_id": "123456-1234-1234-1234-1234567890", + "customer_id": "test_5" + }, + "name": "Test 5", + "phone": null, + "preferred_locales": [ + + ], + "shipping": null, + "tax_exempt": "none", + "test_clock": null + }, + "previous_attributes": { + "invoice_settings": { + "default_payment_method": "pm_123456789" + } + } + }, + "livemode": false, + "pending_webhooks": 3, + "request": { + "id": "req_123456789", + "idempotency_key": "123456-1234-1234-1234-1234567890" + }, + "type": "customer.updated" +} diff --git a/spec/fixtures/stripe/setup_intent_event_with_metadata.json b/spec/fixtures/stripe/setup_intent_event_with_metadata.json new file mode 100644 index 00000000000..80088a37fba --- /dev/null +++ b/spec/fixtures/stripe/setup_intent_event_with_metadata.json @@ -0,0 +1,50 @@ +{ + "id": "evt_1LEBtZDu4p87RxPwbHKmfr13", + "object": "event", + "api_version": "2020-08-27", + "created": 1656074757, + "data": { + "object": { + "id": "seti_1LFeh5Du4p87RxPwCKBRSNzO", + "object": "setup_intent", + "application": null, + "cancellation_reason": null, + "client_secret": "seti_1LFeh5Du4p87RxPwCKBRSNzO_secret_LxZqWjIzFT06Hyqw9wfMcUbtnhmcTNC", + "created": 1656423787, + "customer": "cus_LxZqfhNCKS6abv", + "description": null, + "flow_directions": null, + "last_setup_error": null, + "latest_attempt": null, + "livemode": false, + "mandate": null, + "metadata": { + "lago_customer_id": "879541c8-63ac-4bd0-bcad-06c3004afc6c" + }, + "next_action": null, + "on_behalf_of": null, + "payment_method": "pm_1LFeTwDu4p87RxPwh3lMmhvu", + "payment_method_options": { + "card": { + "mandate_options": null, + "network": null, + "request_three_d_secure": "automatic" + } + }, + "payment_method_types": ["card"], + "single_use_mandate": null, + "status": "requires_payment_method", + "usage": "off_session" + }, + "previous_attributes": { + "default_source": null + } + }, + "livemode": false, + "pending_webhooks": 0, + "request": { + "id": null, + "idempotency_key": null + }, + "type": "setup_intent.succeeded" +} diff --git a/spec/fixtures/stripe/setup_intent_event_without_customer.json b/spec/fixtures/stripe/setup_intent_event_without_customer.json new file mode 100644 index 00000000000..8acd6cd56e2 --- /dev/null +++ b/spec/fixtures/stripe/setup_intent_event_without_customer.json @@ -0,0 +1,48 @@ +{ + "id": "evt_1LEBtZDu4p87RxPwbHKmfr13", + "object": "event", + "api_version": "2020-08-27", + "created": 1656074757, + "data": { + "object": { + "id": "seti_1LFeh5Du4p87RxPwCKBRSNzO", + "object": "setup_intent", + "application": null, + "cancellation_reason": null, + "client_secret": "seti_1LFeh5Du4p87RxPwCKBRSNzO_secret_LxZqWjIzFT06Hyqw9wfMcUbtnhmcTNC", + "created": 1656423787, + "customer": null, + "description": null, + "flow_directions": null, + "last_setup_error": null, + "latest_attempt": null, + "livemode": false, + "mandate": null, + "metadata": {}, + "next_action": null, + "on_behalf_of": null, + "payment_method": "pm_1LFeTwDu4p87RxPwh3lMmhvu", + "payment_method_options": { + "card": { + "mandate_options": null, + "network": null, + "request_three_d_secure": "automatic" + } + }, + "payment_method_types": ["card"], + "single_use_mandate": null, + "status": "requires_payment_method", + "usage": "off_session" + }, + "previous_attributes": { + "default_source": null + } + }, + "livemode": false, + "pending_webhooks": 0, + "request": { + "id": null, + "idempotency_key": null + }, + "type": "setup_intent.succeeded" +} diff --git a/spec/requests/api/v1/customers_controller_spec.rb b/spec/requests/api/v1/customers_controller_spec.rb index dc603e75eec..ff66484c31e 100644 --- a/spec/requests/api/v1/customers_controller_spec.rb +++ b/spec/requests/api/v1/customers_controller_spec.rb @@ -96,7 +96,7 @@ stub_request(:post, 'https://api.stripe.com/v1/checkout/sessions') .to_return(status: 200, body: body.to_json, headers: {}) - allow(Stripe::Checkout::Session).to receive(:create) + allow(::Stripe::Checkout::Session).to receive(:create) .and_return({'url' => 'https://example.com'}) post_with_token(organization, '/api/v1/customers', {customer: create_params}) @@ -360,7 +360,7 @@ customer.update(payment_provider: 'stripe', payment_provider_code: stripe_provider.code) - allow(Stripe::Checkout::Session).to receive(:create) + allow(::Stripe::Checkout::Session).to receive(:create) .and_return({'url' => 'https://example.com'}) end diff --git a/spec/requests/api/v1/invoices_controller_spec.rb b/spec/requests/api/v1/invoices_controller_spec.rb index 393e1a4588a..e993c0499a1 100644 --- a/spec/requests/api/v1/invoices_controller_spec.rb +++ b/spec/requests/api/v1/invoices_controller_spec.rb @@ -725,7 +725,7 @@ customer.update(payment_provider: 'stripe') - allow(Stripe::Checkout::Session).to receive(:create) + allow(::Stripe::Checkout::Session).to receive(:create) .and_return({'url' => 'https://example.com'}) end diff --git a/spec/services/credit_notes/refunds/stripe_service_spec.rb b/spec/services/credit_notes/refunds/stripe_service_spec.rb index e5365cda40f..7912769d821 100644 --- a/spec/services/credit_notes/refunds/stripe_service_spec.rb +++ b/spec/services/credit_notes/refunds/stripe_service_spec.rb @@ -88,12 +88,12 @@ context 'with an error on stripe' do before do allow(Stripe::Refund).to receive(:create) - .and_raise(Stripe::InvalidRequestError.new('error', {})) + .and_raise(::Stripe::InvalidRequestError.new('error', {})) end it 'delivers an error webhook' do expect { stripe_service.create } - .to raise_error(Stripe::InvalidRequestError) + .to raise_error(::Stripe::InvalidRequestError) expect(SendWebhookJob).to have_been_enqueued .with( diff --git a/spec/services/invoices/payments/generate_payment_url_service_spec.rb b/spec/services/invoices/payments/generate_payment_url_service_spec.rb index 2a2f2a1861d..7d26d8f26c6 100644 --- a/spec/services/invoices/payments/generate_payment_url_service_spec.rb +++ b/spec/services/invoices/payments/generate_payment_url_service_spec.rb @@ -24,7 +24,7 @@ customer.update(payment_provider: 'stripe') - allow(Stripe::Checkout::Session).to receive(:create) + allow(::Stripe::Checkout::Session).to receive(:create) .and_return({'url' => 'https://example55.com'}) end diff --git a/spec/services/invoices/payments/stripe_service_spec.rb b/spec/services/invoices/payments/stripe_service_spec.rb index 8418acf93cf..e10833d1d48 100644 --- a/spec/services/invoices/payments/stripe_service_spec.rb +++ b/spec/services/invoices/payments/stripe_service_spec.rb @@ -210,7 +210,7 @@ subscription allow(Stripe::PaymentIntent).to receive(:create) - .and_raise(Stripe::CardError.new('error', {})) + .and_raise(::Stripe::CardError.new('error', {})) end it 'delivers an error webhook' do @@ -279,7 +279,7 @@ subscription allow(Stripe::PaymentIntent).to receive(:create) - .and_raise(Stripe::InvalidRequestError.new('amount_too_small', {}, code: 'amount_too_small')) + .and_raise(::Stripe::InvalidRequestError.new('amount_too_small', {}, code: 'amount_too_small')) end it 'does not send mark the invoice as failed' do @@ -413,14 +413,14 @@ stripe_payment_provider stripe_customer - allow(Stripe::Checkout::Session).to receive(:create) + allow(::Stripe::Checkout::Session).to receive(:create) .and_return({'url' => 'https://example.com'}) end it 'generates payment url' do stripe_service.generate_payment_url - expect(Stripe::Checkout::Session).to have_received(:create) + expect(::Stripe::Checkout::Session).to have_received(:create) end context 'when invoice is payment_succeeded' do @@ -429,7 +429,7 @@ it 'does not generate payment url' do stripe_service.generate_payment_url - expect(Stripe::Checkout::Session).not_to have_received(:create) + expect(::Stripe::Checkout::Session).not_to have_received(:create) end end @@ -439,7 +439,7 @@ it 'does not generate payment url' do stripe_service.generate_payment_url - expect(Stripe::Checkout::Session).not_to have_received(:create) + expect(::Stripe::Checkout::Session).not_to have_received(:create) end end diff --git a/spec/services/payment_provider_customers/stripe/update_payment_method_service_spec.rb b/spec/services/payment_provider_customers/stripe/update_payment_method_service_spec.rb new file mode 100644 index 00000000000..16fbc73ceaa --- /dev/null +++ b/spec/services/payment_provider_customers/stripe/update_payment_method_service_spec.rb @@ -0,0 +1,72 @@ +# frozen_string_literal: true + +require 'rails_helper' + +RSpec.describe PaymentProviderCustomers::Stripe::UpdatePaymentMethodService, type: :service do + subject(:update_service) { described_class.new(stripe_customer:, payment_method_id:) } + + let(:organization) { create(:organization) } + let(:customer) { create(:customer, organization:) } + + let(:stripe_customer) { create(:stripe_customer, customer:) } + let(:payment_method_id) { 'pm_123456' } + + describe '#call' do + it 'updates the customer payment method', aggregate_failures: true do + result = update_service.call + + expect(result).to be_success + expect(result.stripe_customer.payment_method_id).to eq(payment_method_id) + end + + context 'with pending invoices' do + let(:invoice) do + create( + :invoice, + customer:, + total_amount_cents: 200, + currency: 'EUR', + status:, + ready_for_payment_processing: + ) + end + + let(:status) { 'finalized' } + let(:ready_for_payment_processing) { true } + + before { invoice } + + it 'enqueues jobs to reprocess the pending payment', aggregate_failure: true do + result = update_service.call + + expect(result).to be_success + expect(Invoices::Payments::StripeCreateJob).to have_been_enqueued + .with(invoice) + end + + context 'when invoices are not finalized' do + let(:status) { 'draft' } + + it 'does not enqueue jobs to reprocess pending payment', aggregate_failure: true do + result = update_service.call + + expect(result).to be_success + expect(Invoices::Payments::StripeCreateJob).not_to have_been_enqueued + .with(invoice) + end + end + + context 'when invoices are not ready for payment processing' do + let(:ready_for_payment_processing) { 'false' } + + it 'does not enqueue jobs to reprocess pending payment', aggregate_failure: true do + result = update_service.call + + expect(result).to be_success + expect(Invoices::Payments::StripeCreateJob).not_to have_been_enqueued + .with(invoice) + end + end + end + end +end diff --git a/spec/services/payment_provider_customers/stripe_service_spec.rb b/spec/services/payment_provider_customers/stripe_service_spec.rb index 3c8501080c4..d2d954b8ecd 100644 --- a/spec/services/payment_provider_customers/stripe_service_spec.rb +++ b/spec/services/payment_provider_customers/stripe_service_spec.rb @@ -102,7 +102,7 @@ context 'when payment provider has incorrect API key' do before do allow(Stripe::Customer).to receive(:create) - .and_raise(Stripe::AuthenticationError.new('API key invalid.')) + .and_raise(::Stripe::AuthenticationError.new('API key invalid.')) end it 'returns an unauthorized error' do @@ -131,7 +131,7 @@ context 'when failing to create the customer' do it 'delivers an error webhook' do allow(Stripe::Customer).to receive(:create) - .and_raise(Stripe::InvalidRequestError.new('error', {})) + .and_raise(::Stripe::InvalidRequestError.new('error', {})) stripe_service.create @@ -182,7 +182,7 @@ end context 'when stripe raises an invalid request error' do - let(:stripe_error) { Stripe::InvalidRequestError.new('Invalid request', nil) } + let(:stripe_error) { ::Stripe::InvalidRequestError.new('Invalid request', nil) } it 'returns an error result' do result = stripe_service.update @@ -236,7 +236,7 @@ end context 'when stripe raises an authentication error' do - let(:stripe_error) { Stripe::AuthenticationError.new('Invalid username.') } + let(:stripe_error) { ::Stripe::AuthenticationError.new('Invalid username.') } it 'returns an error result' do result = stripe_service.update @@ -354,243 +354,6 @@ end end - describe '#update_provider_default_payment_method' do - subject(:stripe_service) { described_class.new } - - let(:stripe_customer) do - create(:stripe_customer, customer:, provider_customer_id: 'cus_123456') - end - - before do - allow(Stripe::Customer).to receive(:update).and_return(true) - end - - it 'updates provider default payment method' do - result = stripe_service.update_provider_default_payment_method( - organization_id: organization.id, - stripe_customer_id: stripe_customer.provider_customer_id, - payment_method_id: 'pm_123456' - ) - - aggregate_failures do - expect(result).to be_success - expect(result.payment_method).to eq('pm_123456') - end - end - - context 'when customer is not found' do - it 'returns an empty result' do - result = stripe_service.update_provider_default_payment_method( - organization_id: organization.id, - stripe_customer_id: 'cus_InvaLid', - payment_method_id: 'pm_123456' - ) - - aggregate_failures do - expect(result).to be_success - expect(result.payment_method).to be_nil - end - end - - context 'when customer in metadata is not found' do - it 'returns an empty response' do - result = stripe_service.update_provider_default_payment_method( - organization_id: organization.id, - stripe_customer_id: 'cus_InvaLid', - payment_method_id: 'pm_123456', - metadata: { - lago_customer_id: SecureRandom.uuid - } - ) - - aggregate_failures do - expect(result).to be_success - expect(result.payment_method).to be_nil - end - end - end - - context 'when customer in metadata exists' do - it 'returns a not found error' do - result = stripe_service.update_payment_method( - organization_id: organization.id, - stripe_customer_id: 'cus_InvaLid', - payment_method_id: 'pm_123456', - metadata: { - lago_customer_id: customer.id - } - ) - - aggregate_failures do - expect(result).not_to be_success - expect(result.error).to be_a(BaseService::NotFoundFailure) - expect(result.error.message).to eq('stripe_customer_not_found') - end - end - end - end - - context 'when stripe customer id is missing' do - it 'returns a not found error' do - result = stripe_service.update_provider_default_payment_method( - organization_id: organization.id, - stripe_customer_id: nil, - payment_method_id: 'pm_123456' - ) - - aggregate_failures do - expect(result).not_to be_success - expect(result.error).to be_a(BaseService::NotFoundFailure) - expect(result.error.message).to eq('stripe_customer_not_found') - end - end - end - end - - describe '#update_payment_method' do - subject(:stripe_service) { described_class.new } - - let(:stripe_customer) do - create(:stripe_customer, customer:, provider_customer_id: 'cus_123456') - end - - it 'updates the customer payment method' do - result = stripe_service.update_payment_method( - organization_id: organization.id, - stripe_customer_id: stripe_customer.provider_customer_id, - payment_method_id: 'pm_123456' - ) - - aggregate_failures do - expect(result).to be_success - expect(result.stripe_customer.payment_method_id).to eq('pm_123456') - end - end - - context 'with pending invoices' do - let(:invoice) do - create( - :invoice, - customer:, - total_amount_cents: 200, - currency: 'EUR', - status:, - ready_for_payment_processing: - ) - end - - let(:status) { 'finalized' } - let(:ready_for_payment_processing) { true } - - before { invoice } - - it 'enqueues jobs to reprocess the pending payment' do - result = stripe_service.update_payment_method( - organization_id: organization.id, - stripe_customer_id: stripe_customer.provider_customer_id, - payment_method_id: 'pm_123456' - ) - - aggregate_failures do - expect(result).to be_success - - expect(Invoices::Payments::StripeCreateJob).to have_been_enqueued - .with(invoice) - end - end - - context 'when invoices are not finalized' do - let(:status) { 'draft' } - - it 'does not enqueue jobs to reprocess pending payment' do - result = stripe_service.update_payment_method( - organization_id: organization.id, - stripe_customer_id: stripe_customer.provider_customer_id, - payment_method_id: 'pm_123456' - ) - - aggregate_failures do - expect(result).to be_success - - expect(Invoices::Payments::StripeCreateJob).not_to have_been_enqueued - .with(invoice) - end - end - end - - context 'when invoices are not ready for payment processing' do - let(:ready_for_payment_processing) { 'false' } - - it 'does not enqueue jobs to reprocess pending payment' do - result = stripe_service.update_payment_method( - organization_id: organization.id, - stripe_customer_id: stripe_customer.provider_customer_id, - payment_method_id: 'pm_123456' - ) - - aggregate_failures do - expect(result).to be_success - - expect(Invoices::Payments::StripeCreateJob).not_to have_been_enqueued - .with(invoice) - end - end - end - end - - context 'when customer is not found' do - it 'returns an empty result' do - result = stripe_service.update_payment_method( - organization_id: organization.id, - stripe_customer_id: 'cus_InvaLid', - payment_method_id: 'pm_123456' - ) - - aggregate_failures do - expect(result).to be_success - expect(result.stripe_customer).to be_nil - end - end - - context 'when customer in metadata is not found' do - it 'returns an empty response' do - result = stripe_service.update_payment_method( - organization_id: organization.id, - stripe_customer_id: 'cus_InvaLid', - payment_method_id: 'pm_123456', - metadata: { - lago_customer_id: SecureRandom.uuid - } - ) - - aggregate_failures do - expect(result).to be_success - expect(result.stripe_customer).to be_nil - end - end - end - - context 'when customer in metadata exists' do - it 'returns a not found error' do - result = stripe_service.update_payment_method( - organization_id: organization.id, - stripe_customer_id: 'cus_InvaLid', - payment_method_id: 'pm_123456', - metadata: { - lago_customer_id: customer.id - } - ) - - aggregate_failures do - expect(result).not_to be_success - expect(result.error).to be_a(BaseService::NotFoundFailure) - expect(result.error.message).to eq('stripe_customer_not_found') - end - end - end - end - end - describe '#delete_payment_method' do subject(:stripe_service) { described_class.new } @@ -727,7 +490,7 @@ before do allow(stripe_api_customer) .to receive(:retrieve_payment_method) - .and_raise(Stripe::InvalidRequestError.new('error', {})) + .and_raise(::Stripe::InvalidRequestError.new('error', {})) end it 'returns a failed result' do @@ -745,7 +508,7 @@ describe '#generate_checkout_url' do it 'delivers a webhook with checkout url' do - allow(Stripe::Checkout::Session).to receive(:create) + allow(::Stripe::Checkout::Session).to receive(:create) .and_return({'url' => 'https://example.com'}) stripe_service.generate_checkout_url @@ -768,9 +531,9 @@ end context 'when stripe raises an authentication error' do - let(:stripe_error) { Stripe::AuthenticationError.new('Expired API Key provided') } + let(:stripe_error) { ::Stripe::AuthenticationError.new('Expired API Key provided') } - before { allow(Stripe::Checkout::Session).to receive(:create).and_raise(stripe_error) } + before { allow(::Stripe::Checkout::Session).to receive(:create).and_raise(stripe_error) } it 'returns an error result' do result = described_class.new(stripe_customer).generate_checkout_url diff --git a/spec/services/payment_providers/stripe/register_webhook_service_spec.rb b/spec/services/payment_providers/stripe/register_webhook_service_spec.rb index b60c8a96f48..fc3c4608257 100644 --- a/spec/services/payment_providers/stripe/register_webhook_service_spec.rb +++ b/spec/services/payment_providers/stripe/register_webhook_service_spec.rb @@ -65,7 +65,7 @@ before do allow(::Stripe::WebhookEndpoint) .to receive(:create) - .and_raise(::Stripe::InvalidRequestError.new( + .and_raise(::Stripe::PermissionError.new( 'You have reached the maximum of 16 test webhook endpoints.', {} )) end diff --git a/spec/services/payment_providers/stripe_service_spec.rb b/spec/services/payment_providers/stripe_service_spec.rb index 6210d9738ed..3074546f051 100644 --- a/spec/services/payment_providers/stripe_service_spec.rb +++ b/spec/services/payment_providers/stripe_service_spec.rb @@ -397,11 +397,7 @@ end before do - allow(PaymentProviderCustomers::StripeService).to receive(:new) - .and_return(provider_customer_service) - allow(provider_customer_service).to receive(:update_payment_method) - .and_return(service_result) - allow(provider_customer_service).to receive(:update_provider_default_payment_method) + allow(PaymentProviders::Webhooks::Stripe::SetupIntentSucceededService).to receive(:call) .and_return(service_result) end @@ -413,9 +409,7 @@ expect(result).to be_success - expect(PaymentProviderCustomers::StripeService).to have_received(:new) - expect(provider_customer_service).to have_received(:update_payment_method) - expect(provider_customer_service).to have_received(:update_provider_default_payment_method) + expect(PaymentProviders::Webhooks::Stripe::SetupIntentSucceededService).to have_received(:call) end end @@ -426,9 +420,7 @@ end before do - allow(PaymentProviderCustomers::StripeService).to receive(:new) - .and_return(provider_customer_service) - allow(provider_customer_service).to receive(:update_payment_method) + allow(PaymentProviders::Webhooks::Stripe::CustomerUpdatedService).to receive(:call) .and_return(service_result) end @@ -440,17 +432,7 @@ expect(result).to be_success - expect(PaymentProviderCustomers::StripeService).to have_received(:new) - expect(provider_customer_service).to have_received(:update_payment_method) - .with( - metadata: { - customer_id: 'test_5', - lago_customer_id: '123456-1234-1234-1234-1234567890' - }, - organization_id: organization.id, - stripe_customer_id: 'cus_123456789', - payment_method_id: 'card_123456789' - ) + expect(PaymentProviders::Webhooks::Stripe::CustomerUpdatedService).to have_received(:call) end end diff --git a/spec/services/payment_providers/webhooks/stripe/customer_updated_service_spec.rb b/spec/services/payment_providers/webhooks/stripe/customer_updated_service_spec.rb new file mode 100644 index 00000000000..2b327fee3be --- /dev/null +++ b/spec/services/payment_providers/webhooks/stripe/customer_updated_service_spec.rb @@ -0,0 +1,66 @@ +# frozen_string_literal: true + +require 'rails_helper' + +RSpec.describe PaymentProviders::Webhooks::Stripe::CustomerUpdatedService, type: :service do + subject(:webhook_service) { described_class.new(organization_id: organization.id, event_json:) } + + let(:organization) { create(:organization) } + let(:customer) { create(:customer, organization:) } + let(:event_json) { File.read('spec/fixtures/stripe/customer_updated_event.json') } + + let(:event) { Stripe::Event.construct_from(JSON.parse(event_json)) } + let(:provider_customer_id) { event.data.object.id } + let(:payment_method_id) { event.data.object.default_source } + + let(:stripe_provider) { create(:stripe_provider, organization:) } + let(:stripe_customer) do + create(:stripe_customer, payment_provider: stripe_provider, customer:, provider_customer_id:) + end + + before { stripe_customer } + + describe '#call' do + it 'updates the customer payment method', aggregate_failures: true do + result = webhook_service.call + + expect(result).to be_success + expect(result.stripe_customer.payment_method_id).to eq(payment_method_id) + end + + context 'when customer is not found' do + let(:provider_customer_id) { 'cus_InvaLid' } + + it 'returns an empty result', aggregate_failures: true do + result = webhook_service.call + + expect(result).to be_success + expect(result.stripe_customer).to be_nil + end + + context 'when customer in metadata is not found' do + let(:event_json) { File.read('spec/fixtures/stripe/customer_updated_event_with_metadata.json') } + + it 'returns an empty response', aggregate_failures: true do + result = webhook_service.call + + expect(result).to be_success + expect(result.stripe_customer).to be_nil + end + end + + context 'when customer in metadata exists' do + let(:event_json) { File.read('spec/fixtures/stripe/setup_intent_event_with_metadata.json') } + let(:customer) { create(:customer, id: event.data.object.metadata['lago_customer_id'], organization:) } + + it 'returns a not found error', aggregate_failures: true do + result = webhook_service.call + + expect(result).not_to be_success + expect(result.error).to be_a(BaseService::NotFoundFailure) + expect(result.error.message).to eq('stripe_customer_not_found') + end + end + end + end +end diff --git a/spec/services/payment_providers/webhooks/stripe/setup_intent_succeeded_service_spec.rb b/spec/services/payment_providers/webhooks/stripe/setup_intent_succeeded_service_spec.rb new file mode 100644 index 00000000000..d3023b51d6c --- /dev/null +++ b/spec/services/payment_providers/webhooks/stripe/setup_intent_succeeded_service_spec.rb @@ -0,0 +1,88 @@ +# frozen_string_literal: true + +require 'rails_helper' + +RSpec.describe PaymentProviders::Webhooks::Stripe::SetupIntentSucceededService, type: :service do + subject(:webhook_service) { described_class.new(organization_id: organization.id, event_json:) } + + let(:organization) { create(:organization) } + let(:customer) { create(:customer, organization:) } + let(:event_json) { File.read('spec/fixtures/stripe/setup_intent_event.json') } + + let(:event) { Stripe::Event.construct_from(JSON.parse(event_json)) } + let(:provider_customer_id) { event.data.object.customer } + let(:payment_method_id) { event.data.object.payment_method } + + let(:stripe_provider) { create(:stripe_provider, organization:) } + let(:stripe_customer) do + create(:stripe_customer, payment_provider: stripe_provider, customer:, provider_customer_id:) + end + + before { stripe_customer } + + describe '#call' do + it 'updates provider default payment method', aggregate_failures: true do + allow(Stripe::Customer).to receive(:update).and_return(true) + + result = webhook_service.call + + expect(result).to be_success + expect(result.payment_method_id).to eq(payment_method_id) + expect(result.stripe_customer).to eq(stripe_customer) + expect(result.stripe_customer.payment_method_id).to eq(payment_method_id) + + expect(Stripe::Customer).to have_received(:update).with( + provider_customer_id, + {invoice_settings: {default_payment_method: payment_method_id}}, + {api_key: stripe_provider.secret_key} + ) + end + + context 'when stripe customer is not found', aggregate_failures: true do + let(:provider_customer_id) { 'cus_InvaLid' } + + it 'returns an empty result' do + result = webhook_service.call + + expect(result).to be_success + expect(result.payment_method).to be_nil + end + + context 'when customer in metadata is not found' do + let(:event_json) { File.read('spec/fixtures/stripe/setup_intent_event_with_metadata.json') } + + it 'returns an empty response', aggregate_failures: true do + result = webhook_service.call + + expect(result).to be_success + expect(result.payment_method).to be_nil + end + end + + context 'when customer in metadata exists' do + let(:event_json) { File.read('spec/fixtures/stripe/setup_intent_event_with_metadata.json') } + let(:customer) { create(:customer, id: event.data.object.metadata['lago_customer_id'], organization:) } + + it 'returns a not found error', aggregate_failures: true do + result = webhook_service.call + + expect(result).not_to be_success + expect(result.error).to be_a(BaseService::NotFoundFailure) + expect(result.error.message).to eq('stripe_customer_not_found') + end + end + end + + context 'when stripe customer id is nil' do + let(:event_json) { File.read('spec/fixtures/stripe/setup_intent_event_without_customer.json') } + let(:provider_customer_id) { 'cus_InvaLid' } + + it 'returns an empty result', aggregate_failures: true do + result = webhook_service.call + + expect(result).to be_success + expect(result.payment_method).to be_nil + end + end + end +end diff --git a/spec/services/payment_requests/payments/generate_payment_url_service_spec.rb b/spec/services/payment_requests/payments/generate_payment_url_service_spec.rb index 5cc74517e9f..950ac80d0d1 100644 --- a/spec/services/payment_requests/payments/generate_payment_url_service_spec.rb +++ b/spec/services/payment_requests/payments/generate_payment_url_service_spec.rb @@ -21,7 +21,7 @@ payment_provider: stripe_provider ) - allow(Stripe::Checkout::Session).to receive(:create) + allow(::Stripe::Checkout::Session).to receive(:create) .and_return({"url" => "https://example55.com"}) end diff --git a/spec/services/payment_requests/payments/stripe_service_spec.rb b/spec/services/payment_requests/payments/stripe_service_spec.rb index 802175d1ae0..07b9b44a6ca 100644 --- a/spec/services/payment_requests/payments/stripe_service_spec.rb +++ b/spec/services/payment_requests/payments/stripe_service_spec.rb @@ -277,7 +277,7 @@ before do allow(Stripe::PaymentIntent).to receive(:create) - .and_raise(Stripe::CardError.new("error", {})) + .and_raise(::Stripe::CardError.new("error", {})) allow(PaymentRequests::Payments::DeliverErrorWebhookService) .to receive(:call_async) .and_call_original @@ -324,7 +324,7 @@ before do allow(Stripe::PaymentIntent).to receive(:create) - .and_raise(Stripe::InvalidRequestError.new("amount_too_small", {}, code: "amount_too_small")) + .and_raise(::Stripe::InvalidRequestError.new("amount_too_small", {}, code: "amount_too_small")) end it "does not mark the payment request as failed" do @@ -401,14 +401,14 @@ stripe_payment_provider stripe_customer - allow(Stripe::Checkout::Session).to receive(:create) + allow(::Stripe::Checkout::Session).to receive(:create) .and_return({"url" => "https://example.com"}) end it "generates payment url" do stripe_service.generate_payment_url - expect(Stripe::Checkout::Session) + expect(::Stripe::Checkout::Session) .to have_received(:create) .with( { @@ -448,7 +448,7 @@ it "does not generate payment url" do stripe_service.generate_payment_url - expect(Stripe::Checkout::Session).not_to have_received(:create) + expect(::Stripe::Checkout::Session).not_to have_received(:create) end end end