Skip to content

Commit

Permalink
feat(boleto): Introduce Boleto as a valid Stripe Payment Method (#3111)
Browse files Browse the repository at this point in the history
## Context

We're adding support for Boleto payment method through Stripe 🇧🇷

## Description

Boleto requires 2 step. First the payment is initiated on Stripe and the
customer receive a barcode to go pay IRL.

This PR has very little change because most of it already works and is
already tested 😅 but it requires a fair amount of manual QA.

To test the integration, you can use [different email address as
described in Stripe
docs](https://docs.stripe.com/payments/boleto/accept-a-payment#test-integration).
By default, boleto payment are completed after 3 minutes.


## TODO

- [x] Add `boleto` to
`PaymentProviderCustomers::StripeCustomer::PAYMENT_METHODS`
    - [x]  GQL: `boleto` is a valid payment method for a customer
    - [x]  API: `boleto` is a valid payment method for a customer
- [x]  When Lago creates a invoice and tries to collect it via stripe
    - [x]  Boleto is a valid payment method
    - [x]  A Lago  `payment.requires_action` webhook is sent
- [x] Once the invoice is paid, Stripe sends payment_intent.succeded
webhook and the invoice is marked as paid
- [x]  When using a stripe payment URL to pay an invoice
    - [x]  Boleto is a valid payment method
- [x] No webhook is sent for `next_action` since the customer is “on
session”
- [x] Boleto is saved as a customer payment method (See
#3107)
- [x] Once the invoice is paid, Stripe sends payment_intent.succeded
webhook and the invoice is marked as paid
- [x] When setting up payment method (`POST
/customers/:id/checkout_url`)
    - [x]  Boleto is a valid payment method
- [x] No webhook is sent for `next_action` because there is no payment
at this stage, just setup
- [x]  When creating a PaymentRequest (`POST /payment_requests`)
    - [x]  If Boleto is setup, we try to collect payment with Boleto
    - [x]  A Lago  `payment.requires_action` webhook is sent
    - [x]  No `PaymentRequestMailer` email is sent

## Screenshots

### Create new customer, without payment method, create invoice and use
invoice payment_url to pay

![CleanShot 2025-01-29 at 13 46
15@2x](https://github.com/user-attachments/assets/449f526e-d61b-4318-9803-a902ba8bee3d)
![CleanShot 2025-01-29 at 13 46
39@2x](https://github.com/user-attachments/assets/cc129306-ad42-4620-a730-1b5dd89077da)
![CleanShot 2025-01-29 at 13 46
59@2x](https://github.com/user-attachments/assets/2aa3bc56-910b-4c7b-bc66-10e0388c3ec8)
![CleanShot 2025-01-29 at 13 47
37@2x](https://github.com/user-attachments/assets/2dd885b4-f766-493b-88c6-276c22767c6d)
![CleanShot 2025-01-29 at 13 47
47@2x](https://github.com/user-attachments/assets/512bfb7c-4152-4f81-a480-f5f47818fe5a)
![CleanShot 2025-01-29 at 13 51
05@2x](https://github.com/user-attachments/assets/80c0776e-a6c4-4659-94ca-539ca7eb6466)
![CleanShot 2025-01-29 at 13 54
52@2x](https://github.com/user-attachments/assets/ea2b56fe-5e60-4842-8ba6-3ef11cef473a)



### Create new customer and set up payment method

![CleanShot 2025-01-29 at 13 10
14@2x](https://github.com/user-attachments/assets/415f02cb-fa4c-4f78-ad74-d1dd7b8a17a3)
![CleanShot 2025-01-29 at 13 10
33@2x](https://github.com/user-attachments/assets/7204c141-d571-4cbd-8110-be66c938e674)
![CleanShot 2025-01-29 at 13 11
01@2x](https://github.com/user-attachments/assets/dec9cc16-e4e9-49e2-a81b-54b485f4c912)
![CleanShot 2025-01-29 at 13 11
47@2x](https://github.com/user-attachments/assets/4c6a1778-2cdb-4017-938c-6a38546fbdce)
![CleanShot 2025-01-29 at 13 14
24@2x](https://github.com/user-attachments/assets/f472f4a2-f3c1-430d-8dce-4d096d31bb68)

### Invoice collection with Boleto set up

![CleanShot 2025-01-29 at 13 35
32@2x](https://github.com/user-attachments/assets/8fe1158a-b100-4a6c-b6f3-c6eb591cc677)
![CleanShot 2025-01-29 at 13 35
59@2x](https://github.com/user-attachments/assets/301773a7-30b3-4b2a-919b-7d30d1df416d)
![CleanShot 2025-01-29 at 13 38
58@2x](https://github.com/user-attachments/assets/c097af4d-0851-4298-a59f-95b06909fda4)
![CleanShot 2025-01-29 at 13 39
59@2x](https://github.com/user-attachments/assets/0ec502fa-0870-4d4b-a7ea-8a353a31ed7e)
  • Loading branch information
julienbourdeau authored Jan 31, 2025
1 parent bca5d60 commit 04c838f
Show file tree
Hide file tree
Showing 6 changed files with 80 additions and 12 deletions.
2 changes: 1 addition & 1 deletion app/models/payment_provider_customers/stripe_customer.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

module PaymentProviderCustomers
class StripeCustomer < BaseCustomer
PAYMENT_METHODS = %w[card sepa_debit us_bank_account bacs_debit link].freeze
PAYMENT_METHODS = %w[card sepa_debit us_bank_account bacs_debit link boleto].freeze

validates :provider_payment_methods, presence: true
validate :allowed_provider_payment_methods
Expand Down
1 change: 1 addition & 0 deletions schema.graphql

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 6 additions & 0 deletions schema.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion spec/graphql/types/customers/customer_type_enum_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
require 'rails_helper'

RSpec.describe Types::Customers::CustomerTypeEnum do
it 'enumerizes the correct values' do
it 'enumerates the correct values' do
expect(described_class.values.keys).to match_array(%w[company individual])
end
end
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
# frozen_string_literal: true

require 'rails_helper'

RSpec.describe Types::PaymentProviderCustomers::ProviderPaymentMethodsEnum do
it 'enumerates the correct values' do
expect(described_class.values.keys).to match_array(%w[card sepa_debit us_bank_account bacs_debit link boleto])
end
end
Original file line number Diff line number Diff line change
Expand Up @@ -56,13 +56,13 @@
File.read(Rails.root.join("spec/fixtures/stripe/customer_retrieve_response.json"))
end

let(:stripe_payment_intent) do
Stripe::PaymentIntent.construct_from(
id: "ch_123456",
let(:stripe_payment_intent_data) do
{
id: "pi_123456",
status: payment_status,
amount: invoice.total_amount_cents,
currency: invoice.currency
)
}
end

let(:payment_status) { "succeeded" }
Expand All @@ -71,8 +71,10 @@
stripe_payment_provider
stripe_customer

allow(Stripe::PaymentIntent).to receive(:create)
.and_return(stripe_payment_intent)
allow(Stripe::PaymentIntent).to receive(:create).and_call_original
stub_request(:post, "https://api.stripe.com/v1/payment_intents")
.to_return(body: stripe_payment_intent_data.to_json)

allow(SegmentTrackJob).to receive(:perform_later)
allow(Invoices::PrepaidCreditJob).to receive(:perform_later)

Expand Down Expand Up @@ -206,6 +208,9 @@

expect(result.error_message).to eq("error")
expect(result.error_code).to be_nil

expect(result.payment.status).to eq("failed")
expect(result.payment.payable_payment_status).to eq("failed")
end
end

Expand Down Expand Up @@ -247,6 +252,53 @@
end
end

context 'when invoice amount is too big to pay with Boleto' do
let(:organization) { create(:organization) }
let(:customer) { create(:customer, organization:) }
let(:subscription) { create(:subscription, organization:, customer:) }

let(:invoice) do
create(
:invoice,
organization:,
customer:,
total_amount_cents: 100_000_00,
currency: "BRL",
ready_for_payment_processing: true
)
end

before do
subscription

WebMock.stub_request(:post, "https://api.stripe.com/v1/payment_intents")
.to_return(status: 400, body: {
error: {
code: "amount_too_large",
doc_url: "https://stripe.com/docs/error-codes/amount-too-large",
message: "Amount must be no more than R$ 49,999.99 brl",
param: "amount",
request_log_url: "https://dashboard.stripe.com/test/logs/req_WAmkqXs7ajMNAU?t=1738144303",
type: "invalid_request_error"
}
}.to_json)
end

it "returns an empty result" do
result = create_service.call

expect(result).not_to be_success
expect(result.error).to be_a(BaseService::ServiceFailure)
expect(result.error.code).to eq("stripe_error")
expect(result.error.error_message).to eq("Amount must be no more than R$ 49,999.99 brl")

expect(result.error_message).to eq("Amount must be no more than R$ 49,999.99 brl")
expect(result.error_code).to eq("amount_too_large")
expect(result.payment.status).to eq("failed")
expect(result.payment.payable_payment_status).to eq("failed")
end
end

context "when payment status is processing" do
let(:payment_status) { "processing" }

Expand All @@ -271,16 +323,16 @@
context "when customers country is IN" do
let(:payment_status) { "requires_action" }

let(:stripe_payment_intent) do
Stripe::PaymentIntent.construct_from(
id: "ch_123456",
let(:stripe_payment_intent_data) do
{
id: "pi_123456",
status: payment_status,
amount: invoice.total_amount_cents,
currency: invoice.currency,
next_action: {
redirect_to_url: {url: "https://foo.bar"}
}
)
}
end

before do
Expand Down

0 comments on commit 04c838f

Please sign in to comment.