diff --git a/app/models/payment_providers/stripe_provider.rb b/app/models/payment_providers/stripe_provider.rb index b0ce8edea93a..426ed50e0632 100644 --- a/app/models/payment_providers/stripe_provider.rb +++ b/app/models/payment_providers/stripe_provider.rb @@ -2,6 +2,8 @@ module PaymentProviders class StripeProvider < BaseProvider + StripePayment = Data.define(:id, :status, :metadata) + SUCCESS_REDIRECT_URL = 'https://stripe.com/' # NOTE: find the complete list of event types at https://stripe.com/docs/api/events/types diff --git a/app/services/invoices/payments/stripe_service.rb b/app/services/invoices/payments/stripe_service.rb index 0bdbfdcfe160..3a0ba0d2688b 100644 --- a/app/services/invoices/payments/stripe_service.rb +++ b/app/services/invoices/payments/stripe_service.rb @@ -71,13 +71,19 @@ def call raise end - def update_payment_status(organization_id:, provider_payment_id:, status:, metadata: {}) - payment = if metadata[:payment_type] == 'one-time' - create_payment(provider_payment_id:, metadata:) + def update_payment_status(organization_id:, status:, stripe_payment:) + payment = if stripe_payment.metadata[:payment_type] == 'one-time' + create_payment(stripe_payment) else - Payment.find_by(provider_payment_id:) + Payment.find_by(provider_payment_id: stripe_payment.id) + end + + unless payment + handle_missing_payment(organization_id, stripe_payment) + return result unless result.payment + + payment = result.payment end - return handle_missing_payment(organization_id, metadata) unless payment result.payment = payment result.invoice = payment.payable @@ -120,8 +126,8 @@ def generate_payment_url delegate :organization, :customer, to: :invoice - def create_payment(provider_payment_id:, metadata:) - @invoice = Invoice.find_by(id: metadata[:lago_invoice_id]) + def create_payment(stripe_payment, invoice: nil) + @invoice = invoice || Invoice.find_by(id: stripe_payment.metadata[:lago_invoice_id]) unless @invoice result.not_found_failure!(resource: 'invoice') return @@ -130,12 +136,12 @@ def create_payment(provider_payment_id:, metadata:) increment_payment_attempts Payment.new( - payable: invoice, + payable: @invoice, payment_provider_id: stripe_payment_provider.id, payment_provider_customer_id: customer.stripe_customer.id, - amount_cents: invoice.total_amount_cents, - amount_currency: invoice.currency&.upcase, - provider_payment_id: + amount_cents: @invoice.total_amount_cents, + amount_currency: @invoice.currency, + provider_payment_id: stripe_payment.id ) end @@ -298,19 +304,22 @@ def deliver_error_webhook(stripe_error) }) end - def handle_missing_payment(organization_id, metadata) + def handle_missing_payment(organization_id, stripe_payment) # NOTE: Payment was not initiated by lago - return result unless metadata&.key?(:lago_invoice_id) + return result unless stripe_payment.metadata&.key?(:lago_invoice_id) # NOTE: Invoice does not belong to this lago organization # It means the same Stripe secret key is used for multiple organizations - invoice = Invoice.find_by(id: metadata[:lago_invoice_id], organization_id:) + invoice = Invoice.find_by(id: stripe_payment.metadata[:lago_invoice_id], organization_id:) return result if invoice.nil? # NOTE: Invoice exists but payment status is failed return result if invoice.payment_failed? - result.not_found_failure!(resource: 'stripe_payment') + # NOTE: For some reason payment is missing in the database... (killed sidekiq job, etc.) + # We have to recreate it from the received data + result.payment = create_payment(stripe_payment, invoice:) + result end # NOTE: Due to RBI limitation, all indians payment should be off_session diff --git a/app/services/payment_providers/stripe_service.rb b/app/services/payment_providers/stripe_service.rb index 1b00b1426af7..d922f1c8aee4 100644 --- a/app/services/payment_providers/stripe_service.rb +++ b/app/services/payment_providers/stripe_service.rb @@ -106,9 +106,12 @@ def handle_event(organization:, event_json:) payment_service_klass(event) .new.update_payment_status( organization_id: organization.id, - provider_payment_id: event.data.object.payment_intent, status: 'succeeded', - metadata: event.data.object.metadata.to_h.symbolize_keys + stripe_payment: PaymentProviders::StripeProvider::StripePayment.new( + id: event.data.object.payment_intent, + status: event.data.object.status, + metadata: event.data.object.metadata.to_h.symbolize_keys + ) ).raise_if_error! when 'charge.dispute.closed' PaymentProviders::Webhooks::Stripe::ChargeDisputeClosedService.call( @@ -120,9 +123,12 @@ def handle_event(organization:, event_json:) payment_service_klass(event) .new.update_payment_status( organization_id: organization.id, - provider_payment_id: event.data.object.id, status:, - metadata: event.data.object.metadata.to_h.symbolize_keys + stripe_payment: PaymentProviders::StripeProvider::StripePayment.new( + id: event.data.object.id, + status: event.data.object.status, + metadata: event.data.object.metadata.to_h.symbolize_keys + ) ).raise_if_error! when 'payment_method.detached' PaymentProviderCustomers::StripeService diff --git a/app/services/payment_requests/payments/stripe_service.rb b/app/services/payment_requests/payments/stripe_service.rb index 0e8a8942c853..b277d2e0c1c3 100644 --- a/app/services/payment_requests/payments/stripe_service.rb +++ b/app/services/payment_requests/payments/stripe_service.rb @@ -89,15 +89,20 @@ def generate_payment_url result.single_validation_failure!(error_code: "payment_provider_error") end - def update_payment_status(organization_id:, provider_payment_id:, status:, metadata: {}) + def update_payment_status(organization_id:, status:, stripe_payment:) # TODO: do we have one-time payments for payment requests? - payment = if metadata[:payment_type] == "one-time" - create_payment(provider_payment_id:, metadata:) + payment = if stripe_payment.metadata[:payment_type] == "one-time" + create_payment(stripe_payment) else - Payment.find_by(provider_payment_id:) + Payment.find_by(provider_payment_id: stripe_payment.id) end - return handle_missing_payment(organization_id, metadata) unless payment + unless payment + handle_missing_payment(organization_id, stripe_payment) + return result unless result.payment + + payment = result.payment + end result.payment = payment result.payable = payment.payable @@ -278,38 +283,41 @@ def payment_url_payload } end - def handle_missing_payment(organization_id, metadata) + def handle_missing_payment(organization_id, stripe_payment) # NOTE: Payment was not initiated by lago - return result unless metadata&.key?(:lago_payable_id) + return result unless stripe_payment.metadata&.key?(:lago_payable_id) # NOTE: Payment Request does not belong to this lago organization # It means the same Stripe secret key is used for multiple organizations - payment_request = PaymentRequest.find_by(id: metadata[:lago_payable_id], organization_id:) + payment_request = PaymentRequest.find_by(id: stripe_payment.metadata[:lago_payable_id], organization_id:) return result unless payment_request # NOTE: Payment Request exists but payment status is failed return result if payment_request.payment_failed? - result.not_found_failure!(resource: "stripe_payment") + # NOTE: For some reason payment is missing in the database... (killed sidekiq job, etc.) + # We have to recreate it from the received data + result.payment = create_payment(stripe_payment, payable: payment_request) + result end - def create_payment(provider_payment_id:, metadata:) - @payable = PaymentRequest.find_by(id: metadata[:lago_payable_id]) + def create_payment(stripe_payment, payable: nil) + @payable = payable || PaymentRequest.find_by(id: stripe_payment.metadata[:lago_payable_id]) - unless payable + unless @payable result.not_found_failure!(resource: "payment_request") return end - payable.increment_payment_attempts! + @payable.increment_payment_attempts! Payment.new( - payable:, + payable: @payable, payment_provider_id: stripe_payment_provider.id, payment_provider_customer_id: customer.stripe_customer.id, - amount_cents: payable.total_amount_cents, - amount_currency: payable.currency&.upcase, - provider_payment_id: + amount_cents: @payable.total_amount_cents, + amount_currency: @payable.currency, + provider_payment_id: stripe_payment.id ) end diff --git a/spec/services/invoices/payments/stripe_service_spec.rb b/spec/services/invoices/payments/stripe_service_spec.rb index 8365701659ee..dd26e249741d 100644 --- a/spec/services/invoices/payments/stripe_service_spec.rb +++ b/spec/services/invoices/payments/stripe_service_spec.rb @@ -491,6 +491,14 @@ ) end + let(:stripe_payment) do + PaymentProviders::StripeProvider::StripePayment.new( + id: 'ch_123456', + status: 'succeeded', + metadata: {} + ) + end + before do allow(SegmentTrackJob).to receive(:perform_later) allow(SendWebhookJob).to receive(:perform_later) @@ -500,8 +508,8 @@ it 'updates the payment and invoice status' do result = stripe_service.update_payment_status( organization_id: organization.id, - provider_payment_id: 'ch_123456', - status: 'succeeded' + status: 'succeeded', + stripe_payment: ) expect(result).to be_success @@ -513,11 +521,19 @@ end context 'when status is failed' do + let(:stripe_payment) do + PaymentProviders::StripeProvider::StripePayment.new( + id: 'ch_123456', + status: 'canceled', + metadata: {} + ) + end + it 'updates the payment and invoice status' do result = stripe_service.update_payment_status( organization_id: organization.id, - provider_payment_id: 'ch_123456', - status: 'failed' + status: 'failed', + stripe_payment: ) expect(result).to be_success @@ -535,8 +551,8 @@ it 'does not update the status of invoice and payment' do result = stripe_service.update_payment_status( organization_id: organization.id, - provider_payment_id: 'ch_123456', - status: 'succeeded' + status: 'succeeded', + stripe_payment: ) expect(result).to be_success @@ -548,8 +564,8 @@ it 'does not update the status of invoice and payment' do result = stripe_service.update_payment_status( organization_id: organization.id, - provider_payment_id: 'ch_123456', - status: 'foo-bar' + status: 'foo-bar', + stripe_payment: ) aggregate_failures do @@ -564,6 +580,14 @@ context 'when payment is not found and it is one time payment' do let(:payment) { nil } + let(:stripe_payment) do + PaymentProviders::StripeProvider::StripePayment.new( + id: 'ch_123456', + status: 'succeeded', + metadata: {lago_invoice_id: invoice.id, payment_type: 'one-time'} + ) + end + before do stripe_payment_provider stripe_customer @@ -572,9 +596,8 @@ it 'creates a payment and updates invoice payment status' do result = stripe_service.update_payment_status( organization_id: organization.id, - provider_payment_id: 'ch_123456', status: 'succeeded', - metadata: {lago_invoice_id: invoice.id, payment_type: 'one-time'} + stripe_payment: ) aggregate_failures do @@ -588,12 +611,19 @@ end context 'when invoice is not found' do + let(:stripe_payment) do + PaymentProviders::StripeProvider::StripePayment.new( + id: 'ch_123456', + status: 'succeeded', + metadata: {lago_invoice_id: 'invalid', payment_type: 'one-time'} + ) + end + it 'raises a not found failure' do result = stripe_service.update_payment_status( organization_id: organization.id, - provider_payment_id: 'ch_123456', status: 'succeeded', - metadata: {lago_invoice_id: 'invalid', payment_type: 'one-time'} + stripe_payment: ) aggregate_failures do @@ -611,8 +641,8 @@ it 'returns an empty result' do result = stripe_service.update_payment_status( organization_id: organization.id, - provider_payment_id: 'ch_123456', - status: 'succeeded' + status: 'succeeded', + stripe_payment: ) aggregate_failures do @@ -622,12 +652,19 @@ end context 'with invoice id in metadata' do + let(:stripe_payment) do + PaymentProviders::StripeProvider::StripePayment.new( + id: 'ch_123456', + status: 'succeeded', + metadata: {lago_invoice_id: SecureRandom.uuid} + ) + end + it 'returns an empty result' do result = stripe_service.update_payment_status( organization_id: organization.id, - provider_payment_id: 'ch_123456', status: 'succeeded', - metadata: {lago_invoice_id: SecureRandom.uuid} + stripe_payment: ) aggregate_failures do @@ -637,19 +674,44 @@ end context 'when the invoice is found for organization' do - it 'returns a not found failure' do + let(:stripe_payment) do + PaymentProviders::StripeProvider::StripePayment.new( + id: 'ch_123456', + status: 'succeeded', + metadata: {lago_invoice_id: invoice.id} + ) + end + + before do + stripe_customer + stripe_payment_provider + end + + it 'creates the missing payment and updates invoice status' do result = stripe_service.update_payment_status( organization_id: organization.id, - provider_payment_id: 'ch_123456', status: 'succeeded', - metadata: {lago_invoice_id: invoice.id} + stripe_payment: ) - aggregate_failures do - expect(result).not_to be_success - expect(result.error).to be_a(BaseService::NotFoundFailure) - expect(result.error.message).to eq('stripe_payment_not_found') - end + expect(result).to be_success + expect(result.payment.status).to eq('succeeded') + expect(result.invoice.reload).to have_attributes( + payment_status: 'succeeded', + ready_for_payment_processing: false + ) + + expect(invoice.payments.count).to eq(1) + payment = invoice.payments.first + expect(payment).to have_attributes( + payable: invoice, + payment_provider_id: stripe_payment_provider.id, + payment_provider_customer_id: stripe_customer.id, + amount_cents: invoice.total_amount_cents, + amount_currency: invoice.currency, + provider_payment_id: 'ch_123456', + status: 'succeeded' + ) end end end diff --git a/spec/services/payment_providers/stripe_service_spec.rb b/spec/services/payment_providers/stripe_service_spec.rb index 3074546f0511..0fe13077c7f2 100644 --- a/spec/services/payment_providers/stripe_service_spec.rb +++ b/spec/services/payment_providers/stripe_service_spec.rb @@ -256,11 +256,14 @@ expect(payment_service).to have_received(:update_payment_status) .with( organization_id: organization.id, - provider_payment_id: 'pi_1JKS2Y2VYugoKSBzNHPFBNj9', status: 'succeeded', - metadata: { - lago_invoice_id: 'a587e552-36bc-4334-81f2-abcbf034ad3f' - } + stripe_payment: PaymentProviders::StripeProvider::StripePayment.new( + id: 'pi_1JKS2Y2VYugoKSBzNHPFBNj9', + status: 'success', + metadata: { + lago_invoice_id: 'a587e552-36bc-4334-81f2-abcbf034ad3f' + } + ) ) end end @@ -292,12 +295,15 @@ expect(payment_service).to have_received(:update_payment_status) .with( organization_id: organization.id, - provider_payment_id: "pi_1JKS2Y2VYugoKSBzNHPFBNj9", status: "succeeded", - metadata: { - lago_payment_request_id: "a587e552-36bc-4334-81f2-abcbf034ad3f", - lago_payable_type: "PaymentRequest" - } + stripe_payment: PaymentProviders::StripeProvider::StripePayment.new( + id: 'pi_1JKS2Y2VYugoKSBzNHPFBNj9', + status: 'success', + metadata: { + lago_payment_request_id: "a587e552-36bc-4334-81f2-abcbf034ad3f", + lago_payable_type: "PaymentRequest" + } + ) ) end end @@ -333,9 +339,12 @@ expect(payment_service).to have_received(:update_payment_status) .with( organization_id: organization.id, - provider_payment_id: 'pi_123456', status: 'succeeded', - metadata: {} + stripe_payment: PaymentProviders::StripeProvider::StripePayment.new( + id: 'pi_123456', + status: 'succeeded', + metadata: {} + ) ) end end @@ -367,12 +376,15 @@ expect(payment_service).to have_received(:update_payment_status) .with( organization_id: organization.id, - provider_payment_id: 'pi_123456', status: "succeeded", - metadata: { - lago_payment_request_id: "a587e552-36bc-4334-81f2-abcbf034ad3f", - lago_payable_type: "PaymentRequest" - } + stripe_payment: PaymentProviders::StripeProvider::StripePayment.new( + id: 'pi_123456', + status: 'succeeded', + metadata: { + lago_payment_request_id: "a587e552-36bc-4334-81f2-abcbf034ad3f", + lago_payable_type: "PaymentRequest" + } + ) ) end end diff --git a/spec/services/payment_requests/payments/stripe_service_spec.rb b/spec/services/payment_requests/payments/stripe_service_spec.rb index 07b9b44a6ca4..63bf4b1e6dae 100644 --- a/spec/services/payment_requests/payments/stripe_service_spec.rb +++ b/spec/services/payment_requests/payments/stripe_service_spec.rb @@ -457,7 +457,7 @@ subject(:result) do stripe_service.update_payment_status( organization_id: organization.id, - provider_payment_id:, + stripe_payment:, status: ) end @@ -468,11 +468,17 @@ create( :payment, payable: payment_request, - provider_payment_id: + provider_payment_id: stripe_payment.id ) end - let(:provider_payment_id) { "ch_123456" } + let(:stripe_payment) do + PaymentProviders::StripeProvider::StripePayment.new( + id: "ch_123456", + status: "succeeded", + metadata: {} + ) + end before do allow(SegmentTrackJob).to receive(:perform_later) @@ -569,23 +575,24 @@ let(:payment) { nil } let(:status) { "succeeded" } - before do - stripe_payment_provider - stripe_customer - end - - it "creates a payment and updates payment request and invoice payment status", :aggregate_failures do - result = stripe_service.update_payment_status( - organization_id: organization.id, - provider_payment_id:, - status:, + let(:stripe_payment) do + PaymentProviders::StripeProvider::StripePayment.new( + id: "ch_123456", + status: "succeeded", metadata: { lago_payable_id: payment_request.id, lago_payable_type: "PaymentRequest", payment_type: "one-time" } ) + end + + before do + stripe_payment_provider + stripe_customer + end + it "creates a payment and updates payment request and invoice payment status", :aggregate_failures do expect(result).to be_success expect(result.payment.status).to eq(status) @@ -599,18 +606,19 @@ end context "when payment request is not found" do - it "raises a not found failure", :aggregate_failures do - result = stripe_service.update_payment_status( - organization_id: organization.id, - provider_payment_id:, - status:, + let(:stripe_payment) do + PaymentProviders::StripeProvider::StripePayment.new( + id: "ch_123456", + status: "succeeded", metadata: { lago_payable_id: "invalid", lago_payable_type: "PaymentRequest", payment_type: "one-time" } ) + end + it "raises a not found failure", :aggregate_failures do expect(result).not_to be_success expect(result.error).to be_a(BaseService::NotFoundFailure) expect(result.error.message).to eq("payment_request_not_found") @@ -628,30 +636,62 @@ end context "with payment request id in metadata" do - it "returns an empty result", :aggregate_failures do - result = stripe_service.update_payment_status( - organization_id: organization.id, - provider_payment_id:, - status:, - metadata: {lago_payable_id: SecureRandom.uuid, lago_payable_type: "PaymentRequest"} + let(:stripe_payment) do + PaymentProviders::StripeProvider::StripePayment.new( + id: "ch_123456", + status: "succeeded", + metadata: { + lago_payable_id: SecureRandom.uuid, + lago_payable_type: "PaymentRequest" + } ) + end + it "returns an empty result", :aggregate_failures do expect(result).to be_success expect(result.payment).to be_nil end context "when the payment request is found for organization" do - it "returns a not found failure", :aggregate_failures do - result = stripe_service.update_payment_status( - organization_id: organization.id, - provider_payment_id:, - status:, - metadata: {lago_payable_id: payment_request.id, lago_payable_type: "PaymentRequest"} + let(:stripe_payment) do + PaymentProviders::StripeProvider::StripePayment.new( + id: "ch_123456", + status: "succeeded", + metadata: { + lago_payable_id: payment_request.id, + lago_payable_type: "PaymentRequest" + } ) + end + + before do + stripe_customer + stripe_payment_provider + end - expect(result).not_to be_success - expect(result.error).to be_a(BaseService::NotFoundFailure) - expect(result.error.message).to eq("stripe_payment_not_found") + it "creates the missing payment and updates payment_request status", :aggregate_failures do + expect(result).to be_success + expect(result.payment.status).to eq(status) + + expect(result.payable.reload).to be_payment_succeeded + expect(result.payable.ready_for_payment_processing).to eq(false) + + expect(invoice_1.reload).to be_payment_succeeded + expect(invoice_1.ready_for_payment_processing).to eq(false) + expect(invoice_2.reload).to be_payment_succeeded + expect(invoice_2.ready_for_payment_processing).to eq(false) + + expect(payment_request.payments.count).to eq(1) + payment = payment_request.payments.first + expect(payment).to have_attributes( + payable: payment_request, + payment_provider_id: stripe_payment_provider.id, + payment_provider_customer_id: stripe_customer.id, + amount_cents: payment_request.total_amount_cents, + amount_currency: payment_request.currency, + provider_payment_id: 'ch_123456', + status: 'succeeded' + ) end end end