From 294c2e4af7fd7e68a218e3eaddec2ad1bc82081c Mon Sep 17 00:00:00 2001 From: Tiago Lupepic Date: Tue, 2 Jun 2026 14:18:42 +0200 Subject: [PATCH] feat(presentation_group_keys): Support presentation group keys feature - Change responses and fixtures to include the presentation_breakdowns - Update Fee to include presentation_breakdowns - Change the current_usage to send the filter filter_by_presentation as JSON --- lib/lago/api/resources/customer.rb | 11 +- spec/factories/plan.rb | 7 +- .../api/customer_projected_usage.json | 135 ++++++++++++++++++ spec/fixtures/api/customer_usage.json | 36 ++++- spec/fixtures/api/fee.json | 8 ++ spec/fixtures/api/plan.json | 10 +- spec/fixtures/api/plan_charge.json | 10 +- spec/fixtures/api/plan_charges.json | 10 +- spec/fixtures/api/plans.json | 10 +- spec/fixtures/api/subscription_charge.json | 10 +- spec/fixtures/api/subscription_charges.json | 10 +- spec/lago/api/resources/customer_spec.rb | 85 +++++++++++ spec/lago/api/resources/fee_spec.rb | 2 + spec/lago/api/resources/plan_spec.rb | 23 ++- spec/lago/api/resources/subscription_spec.rb | 14 +- 15 files changed, 368 insertions(+), 13 deletions(-) create mode 100644 spec/fixtures/api/customer_projected_usage.json diff --git a/lib/lago/api/resources/customer.rb b/lib/lago/api/resources/customer.rb index 1c468f60..2abba4e4 100644 --- a/lib/lago/api/resources/customer.rb +++ b/lib/lago/api/resources/customer.rb @@ -21,7 +21,8 @@ def wallets def current_usage( # rubocop:disable Metrics/ParameterLists external_customer_id, external_subscription_id, apply_taxes: nil, charge_id: nil, charge_code: nil, billable_metric_code: nil, group: nil, - filter_by_charge_id: nil, filter_by_charge_code: nil, filter_by_group: nil, full_usage: nil + filter_by_charge_id: nil, filter_by_charge_code: nil, filter_by_group: nil, full_usage: nil, + filter_by_presentation: nil ) query_params = { external_subscription_id: external_subscription_id } query_params[:apply_taxes] = apply_taxes unless apply_taxes.nil? @@ -33,6 +34,10 @@ def current_usage( # rubocop:disable Metrics/ParameterLists query_params[:filter_by_charge_code] = filter_by_charge_code unless filter_by_charge_code.nil? filter_by_group&.each { |k, v| query_params[:"filter_by_group[#{k}]"] = v } query_params[:full_usage] = full_usage unless full_usage.nil? + unless filter_by_presentation.nil? + query_params[:filter_by_presentation] = + filter_by_presentation_param(filter_by_presentation) + end query_string = URI.encode_www_form(query_params) uri = URI("#{client.base_api_url}#{api_resource}/#{external_customer_id}/current_usage?#{query_string}") @@ -126,6 +131,10 @@ def whitelist_params(params) { root_name => result_hash } end + def filter_by_presentation_param(filter_by_presentation) + filter_by_presentation.is_a?(String) ? filter_by_presentation : JSON.generate(filter_by_presentation) + end + def whitelist_billing_configuration(billing_params) (billing_params || {}).slice( :invoice_grace_period, diff --git a/spec/factories/plan.rb b/spec/factories/plan.rb index 3dec805e..8ad1abd3 100644 --- a/spec/factories/plan.rb +++ b/spec/factories/plan.rb @@ -24,7 +24,12 @@ invoice_display_name: 'Charge 1', min_amount_cents: 0, accepts_target_wallet: false, - properties: { amount: '0.22' }, + properties: { + amount: '0.22', + presentation_group_keys: [ + { value: 'region', options: { display_in_invoice: true } }, + ], + }, }, ] end diff --git a/spec/fixtures/api/customer_projected_usage.json b/spec/fixtures/api/customer_projected_usage.json new file mode 100644 index 00000000..a3dd1ae4 --- /dev/null +++ b/spec/fixtures/api/customer_projected_usage.json @@ -0,0 +1,135 @@ +{ + "customer_usage": { + "from_datetime": "2022-07-01T00:00:00Z", + "to_datetime": "2022-07-31T23:59:59Z", + "issuing_date": "2022-08-01", + "currency": "EUR", + "amount_cents": 123, + "projected_amount_cents": 246, + "total_amount_cents": 123, + "projected_total_amount_cents": 246, + "taxes_amount_cents": 0, + "projected_taxes_amount_cents": 0, + "charges_usage": [ + { + "units": "1.0", + "projected_units": "2.0", + "events_count": 1, + "amount_cents": 123, + "projected_amount_cents": 246, + "amount_currency": "EUR", + "charge": { + "lago_id": "5eb02857-a71e-4ea2-bcf9-57d3a41bc6ba", + "charge_model": "graduated", + "invoice_display_name": "add_on_invoice_display_name" + }, + "billable_metric": { + "lago_id": "99a6094e-199b-4101-896a-54e927ce7bd7", + "name": "Usage metric", + "code": "usage_metric", + "aggregation_type": "sum" + }, + "filters": [ + { + "units": "3.0", + "projected_units": "6.0", + "events_count": 1, + "amount_cents": 123, + "projected_amount_cents": 246, + "values": { + "country": ["france"] + }, + "presentation_breakdowns": [ + { + "presentation_by": { + "team": "engineering" + }, + "units": "2.0" + } + ], + "projected_presentation_breakdowns": [ + { + "presentation_by": { + "team": "engineering" + }, + "units": "4.0" + } + ] + } + ], + "presentation_breakdowns": [ + { + "presentation_by": { + "team": "engineering" + }, + "units": "2.0" + } + ], + "projected_presentation_breakdowns": [ + { + "presentation_by": { + "team": "engineering" + }, + "units": "4.0" + } + ], + "grouped_usage": [ + { + "amount_cents": 123, + "projected_amount_cents": 246, + "events_count": 1, + "units": "3.0", + "projected_units": "6.0", + "grouped_by": { + "agent_name": "aragorn" + }, + "filters": [ + { + "units": "3.0", + "projected_units": "6.0", + "events_count": 1, + "amount_cents": 123, + "projected_amount_cents": 246, + "values": { + "country": ["france"] + }, + "presentation_breakdowns": [ + { + "presentation_by": { + "team": "engineering" + }, + "units": "2.0" + } + ], + "projected_presentation_breakdowns": [ + { + "presentation_by": { + "team": "engineering" + }, + "units": "4.0" + } + ] + } + ], + "presentation_breakdowns": [ + { + "presentation_by": { + "team": "engineering" + }, + "units": "2.0" + } + ], + "projected_presentation_breakdowns": [ + { + "presentation_by": { + "team": "engineering" + }, + "units": "4.0" + } + ] + } + ] + } + ] + } +} diff --git a/spec/fixtures/api/customer_usage.json b/spec/fixtures/api/customer_usage.json index 420533eb..00b48bfa 100644 --- a/spec/fixtures/api/customer_usage.json +++ b/spec/fixtures/api/customer_usage.json @@ -34,7 +34,23 @@ "country": [ "france" ] - } + }, + "presentation_breakdowns": [ + { + "presentation_by": { + "team": "engineering" + }, + "units": "2.0" + } + ] + } + ], + "presentation_breakdowns": [ + { + "presentation_by": { + "team": "engineering" + }, + "units": "2.0" } ], "grouped_usage": [ @@ -54,7 +70,23 @@ "country": [ "france" ] - } + }, + "presentation_breakdowns": [ + { + "presentation_by": { + "team": "engineering" + }, + "units": "2.0" + } + ] + } + ], + "presentation_breakdowns": [ + { + "presentation_by": { + "team": "engineering" + }, + "units": "2.0" } ] } diff --git a/spec/fixtures/api/fee.json b/spec/fixtures/api/fee.json index 77564e4b..3fb07ecc 100644 --- a/spec/fixtures/api/fee.json +++ b/spec/fixtures/api/fee.json @@ -25,6 +25,14 @@ "total_amount_currency": "EUR", "units": "10.0", "events_count": 10, + "presentation_breakdowns": [ + { + "presentation_by": { + "team": "engineering" + }, + "units": "2.0" + } + ], "applied_taxes": [ { "lago_id": "1a901a90-1a90-1a90-1a90-1a901a901a90", diff --git a/spec/fixtures/api/plan.json b/spec/fixtures/api/plan.json index ea3a625c..72f51ecc 100644 --- a/spec/fixtures/api/plan.json +++ b/spec/fixtures/api/plan.json @@ -28,7 +28,15 @@ "min_amount_cents": 0, "properties": { "amount": "0.22", - "pricing_group_keys": ["agent_name"] + "pricing_group_keys": ["agent_name"], + "presentation_group_keys": [ + { + "value": "region", + "options": { + "display_in_invoice": true + } + } + ] }, "filters": [ { diff --git a/spec/fixtures/api/plan_charge.json b/spec/fixtures/api/plan_charge.json index 27d19f14..7c5a4ec3 100644 --- a/spec/fixtures/api/plan_charge.json +++ b/spec/fixtures/api/plan_charge.json @@ -12,7 +12,15 @@ "min_amount_cents": 0, "prorated": false, "properties": { - "amount": "0.22" + "amount": "0.22", + "presentation_group_keys": [ + { + "value": "region", + "options": { + "display_in_invoice": true + } + } + ] }, "filters": [], "taxes": [], diff --git a/spec/fixtures/api/plan_charges.json b/spec/fixtures/api/plan_charges.json index 7f592e9a..4e458857 100644 --- a/spec/fixtures/api/plan_charges.json +++ b/spec/fixtures/api/plan_charges.json @@ -13,7 +13,15 @@ "min_amount_cents": 0, "prorated": false, "properties": { - "amount": "0.22" + "amount": "0.22", + "presentation_group_keys": [ + { + "value": "region", + "options": { + "display_in_invoice": true + } + } + ] }, "filters": [], "taxes": [], diff --git a/spec/fixtures/api/plans.json b/spec/fixtures/api/plans.json index c00e600f..409abff3 100644 --- a/spec/fixtures/api/plans.json +++ b/spec/fixtures/api/plans.json @@ -29,7 +29,15 @@ "accepts_target_wallet": false, "properties": { "amount": "0.22", - "pricing_group_keys": ["agent_name"] + "pricing_group_keys": ["agent_name"], + "presentation_group_keys": [ + { + "value": "region", + "options": { + "display_in_invoice": true + } + } + ] }, "filters": [ { diff --git a/spec/fixtures/api/subscription_charge.json b/spec/fixtures/api/subscription_charge.json index 27d19f14..7c5a4ec3 100644 --- a/spec/fixtures/api/subscription_charge.json +++ b/spec/fixtures/api/subscription_charge.json @@ -12,7 +12,15 @@ "min_amount_cents": 0, "prorated": false, "properties": { - "amount": "0.22" + "amount": "0.22", + "presentation_group_keys": [ + { + "value": "region", + "options": { + "display_in_invoice": true + } + } + ] }, "filters": [], "taxes": [], diff --git a/spec/fixtures/api/subscription_charges.json b/spec/fixtures/api/subscription_charges.json index 7f592e9a..4e458857 100644 --- a/spec/fixtures/api/subscription_charges.json +++ b/spec/fixtures/api/subscription_charges.json @@ -13,7 +13,15 @@ "min_amount_cents": 0, "prorated": false, "properties": { - "amount": "0.22" + "amount": "0.22", + "presentation_group_keys": [ + { + "value": "region", + "options": { + "display_in_invoice": true + } + } + ] }, "filters": [], "taxes": [], diff --git a/spec/lago/api/resources/customer_spec.rb b/spec/lago/api/resources/customer_spec.rb index bd1593d9..9504aead 100644 --- a/spec/lago/api/resources/customer_spec.rb +++ b/spec/lago/api/resources/customer_spec.rb @@ -157,6 +157,71 @@ end end + context 'when filter_by_presentation parameter is provided' do + before do + stub_request(:get, "https://api.getlago.com/api/v1/customers/#{customer_external_id}/current_usage") + .with(query: { + external_subscription_id: subscription_external_id, + filter_by_presentation: '["engineering","operations"]', + }) + .to_return(body: customer_usage_response, status: 200) + end + + it 'returns the usage of the customer with presentation breakdowns filtered' do + response = resource.current_usage( + customer_external_id, + subscription_external_id, + filter_by_presentation: %w[engineering operations], + ) + + breakdown = response['customer_usage']['charges_usage'].first['presentation_breakdowns'].first + expect(breakdown['presentation_by']).to eq('team' => 'engineering') + expect(breakdown['units']).to eq('2.0') + end + end + + context 'when filter_by_presentation is empty' do + before do + stub_request(:get, "https://api.getlago.com/api/v1/customers/#{customer_external_id}/current_usage") + .with(query: { + external_subscription_id: subscription_external_id, + filter_by_presentation: '[]', + }) + .to_return(body: customer_usage_response, status: 200) + end + + it 'passes an empty presentation filter' do + response = resource.current_usage( + customer_external_id, + subscription_external_id, + filter_by_presentation: [], + ) + + expect(response['customer_usage']['from_datetime']).to eq('2022-07-01T00:00:00Z') + end + end + + context 'when filter_by_presentation is already JSON encoded' do + before do + stub_request(:get, "https://api.getlago.com/api/v1/customers/#{customer_external_id}/current_usage") + .with(query: { + external_subscription_id: subscription_external_id, + filter_by_presentation: '["engineering","operations"]', + }) + .to_return(body: customer_usage_response, status: 200) + end + + it 'passes the encoded presentation filter through' do + response = resource.current_usage( + customer_external_id, + subscription_external_id, + filter_by_presentation: '["engineering","operations"]', + ) + + expect(response['customer_usage']['from_datetime']).to eq('2022-07-01T00:00:00Z') + end + end + context 'when the customer does not exists' do let(:customer_external_id) { 'DOESNOTEXIST' } @@ -186,6 +251,26 @@ end end + describe '#projected_usage' do + let(:customer_usage_response) { load_fixture('customer_projected_usage') } + let(:subscription_external_id) { '123' } + + context 'when the customer exists' do + before do + stub_request(:get, "https://api.getlago.com/api/v1/customers/#{customer_external_id}/projected_usage?external_subscription_id=#{subscription_external_id}") + .to_return(body: customer_usage_response, status: 200) + end + + it 'returns projected presentation breakdowns' do + response = resource.projected_usage(customer_external_id, subscription_external_id) + charge_usage = response['customer_usage']['charges_usage'].first + + expect(charge_usage['presentation_breakdowns'].first['units']).to eq('2.0') + expect(charge_usage['projected_presentation_breakdowns'].first['units']).to eq('4.0') + end + end + end + describe '#past_usage' do let(:customer_usage_response) { load_fixture('customer_past_usage') } let(:subscription_external_id) { '123' } diff --git a/spec/lago/api/resources/fee_spec.rb b/spec/lago/api/resources/fee_spec.rb index 3f0b734e..d9f295a2 100644 --- a/spec/lago/api/resources/fee_spec.rb +++ b/spec/lago/api/resources/fee_spec.rb @@ -36,6 +36,7 @@ expect(fee.invoice_display_name).to eq(fee_invoice_display_name) expect(fee.item.invoice_display_name).to eq(fee_item_invoice_display_name) expect(fee.item.filter_invoice_display_name).to eq(fee_item_filter_invoice_display_name) + expect(fee.presentation_breakdowns.first.presentation_by.team).to eq('engineering') end end @@ -76,6 +77,7 @@ expect(response['fees'].first['lago_id']).to eq(fee_id) expect(response['fees'].first['invoice_display_name']).to eq(fee_invoice_display_name) + expect(response['fees'].first['presentation_breakdowns'].first['units']).to eq('2.0') end context 'when filters are present' do diff --git a/spec/lago/api/resources/plan_spec.rb b/spec/lago/api/resources/plan_spec.rb index aa117688..5125cc4f 100644 --- a/spec/lago/api/resources/plan_spec.rb +++ b/spec/lago/api/resources/plan_spec.rb @@ -181,6 +181,8 @@ expect(response['plans'].first['invoice_display_name']).to eq(plan_dsplay_name) expect(response['plans'].first['charges'].first['invoice_display_name']).to eq('Charge 1') expect(response['plans'].first['charges'].first['properties']['pricing_group_keys']).to eq(['agent_name']) + presentation_group_keys = response['plans'].first['charges'].first['properties']['presentation_group_keys'] + expect(presentation_group_keys.first['value']).to eq('region') expect(response['plans'].first['minimum_commitment']['invoice_display_name']).to eq('Minimum commitment (C1)') expect(response['meta']['current_page']).to eq(1) end @@ -436,6 +438,7 @@ expect(response['charges'].first['lago_id']).to eq('51c1e851-5be6-4343-a0ee-39a81d8b4ee1') expect(response['charges'].first['code']).to eq('charge_code') + expect(response['charges'].first['properties']['presentation_group_keys'].first['value']).to eq('region') expect(response['meta']['current_page']).to eq(1) end end @@ -482,6 +485,7 @@ expect(charge.lago_id).to eq('51c1e851-5be6-4343-a0ee-39a81d8b4ee1') expect(charge.code).to eq(charge_code) + expect(charge.properties.presentation_group_keys.first.value).to eq('region') end end @@ -504,7 +508,12 @@ billable_metric_id: 'a6947936-628f-4945-8857-db6858ee7941', code: 'charge_code', charge_model: 'standard', - properties: { amount: '0.22' }, + properties: { + amount: '0.22', + presentation_group_keys: [ + { value: 'region', options: { display_in_invoice: true } }, + ], + }, } end @@ -539,7 +548,17 @@ describe '#update_charge' do let(:json_response) { load_fixture('plan_charge') } let(:charge_code) { 'charge_code' } - let(:params) { { invoice_display_name: 'Updated Setup' } } + let(:params) do + { + invoice_display_name: 'Updated Setup', + properties: { + amount: '0.22', + presentation_group_keys: [ + { value: 'region', options: { display_in_invoice: true } }, + ], + }, + } + end context 'when charge is successfully updated' do before do diff --git a/spec/lago/api/resources/subscription_spec.rb b/spec/lago/api/resources/subscription_spec.rb index 28cf5de7..8c8ba42a 100644 --- a/spec/lago/api/resources/subscription_spec.rb +++ b/spec/lago/api/resources/subscription_spec.rb @@ -878,6 +878,7 @@ expect(response['charges'].first['lago_id']).to eq('51c1e851-5be6-4343-a0ee-39a81d8b4ee1') expect(response['charges'].first['code']).to eq('charge_code') + expect(response['charges'].first['properties']['presentation_group_keys'].first['value']).to eq('region') expect(response['meta']['current_page']).to eq(1) end end @@ -910,6 +911,7 @@ expect(charge.lago_id).to eq('51c1e851-5be6-4343-a0ee-39a81d8b4ee1') expect(charge.code).to eq(charge_code) + expect(charge.properties.presentation_group_keys.first.value).to eq('region') end end @@ -943,7 +945,17 @@ let(:json_response) { load_fixture('subscription_charge') } let(:external_subscription_id) { 'sub_123' } let(:charge_code) { 'charge_code' } - let(:params) { { invoice_display_name: 'Updated Setup' } } + let(:params) do + { + invoice_display_name: 'Updated Setup', + properties: { + amount: '0.22', + presentation_group_keys: [ + { value: 'region', options: { display_in_invoice: true } }, + ], + }, + } + end context 'when charge is successfully updated' do before do