From 81befc2925a66fe23e7aee1ce3e69bf17304589d Mon Sep 17 00:00:00 2001 From: Mario Diniz Date: Thu, 11 Jun 2026 15:23:34 -0300 Subject: [PATCH 1/2] [ING-70] feat(multi-entity): support billing_entity_code ## Context Multi-Entity Billing lets a customer's resources be scoped to a billing entity other than their default. The Ruby client needs to forward the optional billing_entity_code request param on writes and accept the billing_entity_codes filter on list endpoints so callers can target a specific entity. ## Description Adds billing_entity_code to subscription create/update, wallet create, one-off invoice create, and invoice preview payloads. When omitted, the API falls back to the customer's billing entity, so existing callers are unaffected. Subscription and wallet responses surface the field automatically through the client's OpenStruct deserialization (invoices and credit notes already exposed it). Adds the billing_entity_codes[] filter to the subscriptions, wallets and payment_requests list specs; the existing get_all already forwards options to the query string, so the list filters are test-only changes on the client side. --- .../customers/wallets/whitelist_params.rb | 1 + lib/lago/api/resources/invoice.rb | 7 +-- lib/lago/api/resources/subscription.rb | 1 + spec/factories/subscription.rb | 1 + spec/lago/api/resources/invoice_spec.rb | 52 +++++++++++++++++++ .../api/resources/payment_request_spec.rb | 16 ++++++ spec/lago/api/resources/subscription_spec.rb | 50 ++++++++++++++++++ spec/lago/api/resources/wallet_spec.rb | 52 +++++++++++++++++++ 8 files changed, 177 insertions(+), 3 deletions(-) diff --git a/lib/lago/api/resources/customers/wallets/whitelist_params.rb b/lib/lago/api/resources/customers/wallets/whitelist_params.rb index 4f8c3ca6..488dedc0 100644 --- a/lib/lago/api/resources/customers/wallets/whitelist_params.rb +++ b/lib/lago/api/resources/customers/wallets/whitelist_params.rb @@ -23,6 +23,7 @@ def wallet(params) :transaction_name, :paid_top_up_min_amount_cents, :paid_top_up_max_amount_cents, + :billing_entity_code, ) recurring_rules = recurring_rules_params(params[:recurring_transaction_rules]) diff --git a/lib/lago/api/resources/invoice.rb b/lib/lago/api/resources/invoice.rb index a9227db1..2ed6b5bf 100644 --- a/lib/lago/api/resources/invoice.rb +++ b/lib/lago/api/resources/invoice.rb @@ -86,7 +86,7 @@ def payment_url(invoice_id) def preview(params) path = "/api/v1/invoices/preview" payload = params.slice( - :customer, :plan_code, :subscription_at, :billing_time, :coupons, :subscriptions + :customer, :plan_code, :subscription_at, :billing_time, :coupons, :subscriptions, :billing_entity_code ) response = connection.post(payload, path)[root_name] @@ -125,8 +125,9 @@ def one_off_params(params) external_customer_id: params[:external_customer_id], currency: params[:currency], net_payment_term: params[:net_payment_term], - skip_psp: params[:skip_psp] - } + skip_psp: params[:skip_psp], + billing_entity_code: params[:billing_entity_code] + }.compact fees = whitelist_fees(params[:fees]) result[:fees] = fees unless fees.empty? diff --git a/lib/lago/api/resources/subscription.rb b/lib/lago/api/resources/subscription.rb index 005dc319..72782100 100644 --- a/lib/lago/api/resources/subscription.rb +++ b/lib/lago/api/resources/subscription.rb @@ -213,6 +213,7 @@ def whitelist_params(params) ending_at: params[:ending_at], plan_overrides: params[:plan_overrides], consolidate_invoice: params[:consolidate_invoice], + billing_entity_code: params[:billing_entity_code], }.compact payment_method_params = whitelist_payment_method_params(params[:payment_method]) diff --git a/spec/factories/subscription.rb b/spec/factories/subscription.rb index 853cffb7..7054085c 100644 --- a/spec/factories/subscription.rb +++ b/spec/factories/subscription.rb @@ -16,5 +16,6 @@ canceled_at { nil } created_at { '2022-05-05T12:27:30Z' } consolidate_invoice { true } + billing_entity_code { 'acme_corp' } end end diff --git a/spec/lago/api/resources/invoice_spec.rb b/spec/lago/api/resources/invoice_spec.rb index 6a1ebc09..0d0390fe 100644 --- a/spec/lago/api/resources/invoice_spec.rb +++ b/spec/lago/api/resources/invoice_spec.rb @@ -160,6 +160,37 @@ end end end + + context 'when billing_entity_code is provided' do + let(:params) do + { + external_customer_id: '_ID_', + currency: 'EUR', + net_payment_term: 0, + skip_psp: true, + billing_entity_code: 'eu_entity', + fees: [ + { + add_on_code: '123', + description: 'desc', + tax_codes: [tax.code], + } + ] + } + end + + before do + stub_request(:post, 'https://api.getlago.com/api/v1/invoices') + .with(body: { invoice: params }) + .to_return(body: invoice_response, status: 200) + end + + it 'returns invoice' do + invoice = resource.create(params) + + expect(invoice.lago_id).to eq(invoice_id) + end + end end describe '#update' do @@ -525,6 +556,27 @@ expect { subject }.to raise_error(Lago::Api::HttpError) end end + + context 'when billing_entity_code is provided' do + let(:invoice_response) { load_fixture('invoice_preview') } + let(:params) do + { + customer: { external_id: '_ID_' }, + plan_code: 'plan_code', + billing_entity_code: 'eu_entity' + } + end + + before do + stub_request(:post, 'https://api.getlago.com/api/v1/invoices/preview') + .with(body: params) + .to_return(body: invoice_response, status: 200) + end + + it 'forwards billing_entity_code to the preview endpoint' do + expect(subject).to have_attributes(lago_id: nil) + end + end end describe '#void (with params)' do diff --git a/spec/lago/api/resources/payment_request_spec.rb b/spec/lago/api/resources/payment_request_spec.rb index 536a17ff..162f8302 100644 --- a/spec/lago/api/resources/payment_request_spec.rb +++ b/spec/lago/api/resources/payment_request_spec.rb @@ -96,6 +96,22 @@ end end + context 'when billing_entity_codes is given' do + before do + stub_request( + :get, + 'https://api.getlago.com/api/v1/payment_requests?billing_entity_codes%5B%5D=eu_entity&billing_entity_codes%5B%5D=us_entity', + ).to_return(body: payment_requests_response, status: 200) + end + + it 'returns payment requests filtered by billing_entity_codes' do + response = resource.get_all({ 'billing_entity_codes[]': %w[eu_entity us_entity] }) + + expect(response['payment_requests'].first['lago_id']).to eq(payment_request_id) + expect(response['meta']['current_page']).to eq(1) + end + end + context 'when there is an issue' do before do stub_request(:get, 'https://api.getlago.com/api/v1/payment_requests') diff --git a/spec/lago/api/resources/subscription_spec.rb b/spec/lago/api/resources/subscription_spec.rb index 8c8ba42a..ac5ce1c6 100644 --- a/spec/lago/api/resources/subscription_spec.rb +++ b/spec/lago/api/resources/subscription_spec.rb @@ -196,6 +196,23 @@ expect { resource.create(params_with_invalid_pm) }.to raise_error Lago::Api::HttpError end end + + context 'when billing_entity_code is provided' do + let(:params_with_billing_entity) { params.merge(billing_entity_code: 'eu_entity') } + let(:body_with_billing_entity) { { 'subscription' => params_with_billing_entity } } + + before do + stub_request(:post, 'https://api.getlago.com/api/v1/subscriptions') + .with(body: body_with_billing_entity) + .to_return(body: response, status: 200) + end + + it 'returns subscription with billing_entity_code' do + subscription = resource.create(params_with_billing_entity) + + expect(subscription.billing_entity_code).to eq(factory_subscription.billing_entity_code) + end + end end describe '#delete' do @@ -356,6 +373,23 @@ expect { resource.update(params_with_invalid_pm, '123') }.to raise_error Lago::Api::HttpError end end + + context 'when billing_entity_code is provided' do + let(:params_with_billing_entity) { { name: 'new name', billing_entity_code: 'us_entity' } } + let(:body_with_billing_entity) { { 'subscription' => params_with_billing_entity } } + + before do + stub_request(:put, 'https://api.getlago.com/api/v1/subscriptions/123') + .with(body: body_with_billing_entity) + .to_return(body: response, status: 200) + end + + it 'returns subscription with billing_entity_code' do + subscription = resource.update(params_with_billing_entity, '123') + + expect(subscription.billing_entity_code).to eq(factory_subscription.billing_entity_code) + end + end end describe '#get' do @@ -432,6 +466,22 @@ end end + context 'when billing_entity_codes is given' do + before do + stub_request( + :get, + 'https://api.getlago.com/api/v1/subscriptions?billing_entity_codes%5B%5D=eu_entity&billing_entity_codes%5B%5D=us_entity', + ).to_return(body: response, status: 200) + end + + it 'returns subscriptions filtered by billing_entity_codes' do + response = resource.get_all({ 'billing_entity_codes[]': %w[eu_entity us_entity] }) + + expect(response['subscriptions'].first['lago_id']).to eq(factory_subscription.lago_id) + expect(response['meta']['current_page']).to eq(1) + end + end + context 'when there is an issue' do before do stub_request(:get, 'https://api.getlago.com/api/v1/subscriptions') diff --git a/spec/lago/api/resources/wallet_spec.rb b/spec/lago/api/resources/wallet_spec.rb index 734d4bd8..d79abd75 100644 --- a/spec/lago/api/resources/wallet_spec.rb +++ b/spec/lago/api/resources/wallet_spec.rb @@ -333,6 +333,42 @@ end end end + + context 'when billing_entity_code is provided' do + let(:params_with_billing_entity) { params.merge(billing_entity_code: 'eu_entity') } + let(:body_with_billing_entity) do + body['wallet']['billing_entity_code'] = 'eu_entity' + body + end + let(:response_with_billing_entity) do + { + 'wallet' => { + 'lago_id' => 'this-is-lago-id', + 'lago_customer_id' => factory_wallet.id, + 'name' => factory_wallet.name, + 'billing_entity_code' => 'eu_entity', + 'expiration_at' => factory_wallet.expiration_at, + 'balance_cents' => 10_000, + 'rate_amount' => factory_wallet.rate_amount, + 'created_at' => '2022-04-29T08:59:51Z', + 'recurring_transaction_rules' => factory_wallet.recurring_transaction_rules, + 'applies_to' => factory_wallet.applies_to, + } + }.to_json + end + + before do + stub_request(:post, 'https://api.getlago.com/api/v1/wallets') + .with(body: body_with_billing_entity) + .to_return(body: response_with_billing_entity, status: 200) + end + + it 'returns a wallet with billing_entity_code' do + wallet = resource.create(params_with_billing_entity) + + expect(wallet.billing_entity_code).to eq('eu_entity') + end + end end describe '#update' do @@ -675,6 +711,22 @@ end end + context 'when billing_entity_codes is given' do + before do + stub_request( + :get, + 'https://api.getlago.com/api/v1/wallets?billing_entity_codes%5B%5D=eu_entity&billing_entity_codes%5B%5D=us_entity', + ).to_return(body: response, status: 200) + end + + it 'returns wallets filtered by billing_entity_codes' do + response = resource.get_all({ 'billing_entity_codes[]': %w[eu_entity us_entity] }) + + expect(response['wallets'].first['lago_id']).to eq('this-is-lago-id') + expect(response['meta']['current_page']).to eq(1) + end + end + context 'when there is an issue' do before do stub_request(:get, 'https://api.getlago.com/api/v1/wallets') From 2247088c6535caf277f3d8198201f9a7f0966d0a Mon Sep 17 00:00:00 2001 From: Mario Diniz Date: Fri, 12 Jun 2026 09:24:27 -0300 Subject: [PATCH 2/2] fixing race condition tests --- spec/integration/customers/wallets_spec.rb | 5 ++++- spec/integration/wallets_spec.rb | 5 ++++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/spec/integration/customers/wallets_spec.rb b/spec/integration/customers/wallets_spec.rb index 940e32c8..1f3586c0 100644 --- a/spec/integration/customers/wallets_spec.rb +++ b/spec/integration/customers/wallets_spec.rb @@ -73,10 +73,13 @@ def assert_wallet_attributes_with_updated_balance(wallet, **attributes) end def wait_for_balance_update(wallet) + # balance_cents is synced synchronously when the wallet transaction settles, but the ongoing + # balance and pending transactions are reconciled by RefreshWalletJob. Wait for both so any + # subsequent destroy/assertion sees a fully settled wallet. new_wallet = nil wait_until do new_wallet = client.customers.wallets.get(wallet.external_customer_id, wallet.code) - new_wallet.balance_cents == 2000 + new_wallet.balance_cents == 2000 && new_wallet.ongoing_balance_cents == 2000 end new_wallet end diff --git a/spec/integration/wallets_spec.rb b/spec/integration/wallets_spec.rb index b5f3da32..93b7eb76 100644 --- a/spec/integration/wallets_spec.rb +++ b/spec/integration/wallets_spec.rb @@ -71,10 +71,13 @@ def assert_wallet_attributes_with_updated_balance(wallet, **attributes) end def wait_for_balance_update(wallet_id) + # balance_cents is synced synchronously when the wallet transaction settles, but the ongoing + # balance and pending transactions are reconciled by RefreshWalletJob. Wait for both so any + # subsequent destroy/assertion sees a fully settled wallet. wallet = nil wait_until do wallet = client.wallets.get(wallet_id) - wallet.balance_cents == 2000 + wallet.balance_cents == 2000 && wallet.ongoing_balance_cents == 2000 end wallet end