Skip to content

Commit 2f950a2

Browse files
authored
feat(presentation_group_keys): Update serializers to include presentation breakdowns (#5411)
This commit updates the serializers of usage, fee and invoices to include the breakdowns. We're including all presentation breakdowns associated to fees in invoice, in order to do that, this commit introduces a presentation builder, this class is responsible to group the breakdowns based on `presentation_by`. We avoid displaying the same `presentation_by` twice and instead we're grouping and sum them together. Serializers are being updated: - Charge ( Usage / Projected ) - Fee - Invoice
1 parent 88c8acd commit 2f950a2

20 files changed

Lines changed: 455 additions & 56 deletions

app/controllers/api/v1/fees_controller.rb

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,8 @@ def index
5050
:fixed_charge_add_on,
5151
:invoice,
5252
:invoiceable,
53-
:true_up_fee
53+
:true_up_fee,
54+
:presentation_breakdowns
5455
),
5556
::V1::FeeSerializer,
5657
collection_name: "fees",

app/queries/past_usage_query.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ def query
4646
end
4747

4848
def fees_query(invoice)
49-
query = invoice.fees.joins(:subscription).where(subscription: {external_id: filters.external_subscription_id}).charge.includes(:charge_filter)
49+
query = invoice.fees.joins(:subscription).where(subscription: {external_id: filters.external_subscription_id}).charge.includes(:charge_filter, :presentation_breakdowns)
5050
return query unless filters.billable_metric_code
5151

5252
query.joins(:charge).where(charges: {billable_metric_id: billable_metric.id})

app/serializers/v1/customers/charge_usage_serializer.rb

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,8 @@ def serialize
1313
charge: charge_data(fee),
1414
billable_metric: billable_metric_data(fee),
1515
filters: filters(fees),
16-
grouped_usage: grouped_usage(fees)
16+
grouped_usage: grouped_usage(fees),
17+
presentation_breakdowns: PresentationBreakdownBuilder.call(fees, filter: PresentationBreakdownBuilder::UNGROUPED)
1718
}
1819
end
1920
end
@@ -110,7 +111,8 @@ def build_grouped_usage_data(grouped_fees)
110111
{
111112
**usage_data.except(:amount_currency),
112113
grouped_by: grouped_fees.first.grouped_by,
113-
filters: filters(grouped_fees)
114+
filters: filters(grouped_fees),
115+
presentation_breakdowns: PresentationBreakdownBuilder.call(grouped_fees, filter: PresentationBreakdownBuilder::GROUPED)
114116
}
115117
end
116118
end
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
# frozen_string_literal: true
2+
3+
module V1
4+
module Customers
5+
class PresentationBreakdownBuilder
6+
ALL = :all
7+
UNGROUPED = :ungrouped
8+
GROUPED = :grouped
9+
10+
def self.call(fees, filter:)
11+
new(fees, filter:).call
12+
end
13+
14+
def initialize(fees, filter:)
15+
@fees = fees
16+
@filter = filter
17+
end
18+
19+
def call
20+
Array(fees).flat_map do |fee|
21+
next [] if filter == UNGROUPED && fee.grouped_by.present?
22+
next [] if filter == GROUPED && fee.grouped_by.blank?
23+
24+
fee.presentation_breakdowns.map do |breakdown|
25+
::V1::PresentationBreakdownSerializer.new(breakdown).serialize
26+
end
27+
end
28+
end
29+
30+
private
31+
32+
attr_reader :fees, :filter
33+
end
34+
end
35+
end

app/serializers/v1/customers/projected_charge_usage_serializer.rb

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,8 @@ def serialize
1515
charge: charge_data(fee),
1616
billable_metric: billable_metric_data(fee),
1717
filters: cached_filters(fees),
18-
grouped_usage: cached_grouped_usage(fees)
18+
grouped_usage: cached_grouped_usage(fees),
19+
presentation_breakdowns: PresentationBreakdownBuilder.call(fees, filter: PresentationBreakdownBuilder::UNGROUPED)
1920
}
2021
end
2122
end
@@ -32,7 +33,7 @@ def calculate_usage_data(fees)
3233

3334
def current_usage_data(fees)
3435
totals = fees.each_with_object({
35-
units: BigDecimal("0"),
36+
units: BigDecimal(0),
3637
events_count: 0,
3738
amount_cents: 0
3839
}) do |fee, acc|
@@ -208,7 +209,8 @@ def build_grouped_usage_data(grouped_fees)
208209
{
209210
**usage_data.except(:amount_currency),
210211
grouped_by: grouped_fees.first.grouped_by,
211-
filters: filters(grouped_fees)
212+
filters: filters(grouped_fees),
213+
presentation_breakdowns: PresentationBreakdownBuilder.call(grouped_fees, filter: PresentationBreakdownBuilder::GROUPED)
212214
}
213215
end
214216

app/serializers/v1/fee_serializer.rb

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,8 @@ def serialize
5656
refunded_at: model.refunded_at&.iso8601,
5757
amount_details: model.amount_details,
5858
self_billed: model.invoice&.self_billed || false,
59-
pricing_unit_details:
59+
pricing_unit_details:,
60+
presentation_breakdowns: model.presentation_breakdowns.map { |breakdown| PresentationBreakdownSerializer.new(breakdown).serialize }
6061
}
6162

6263
payload.merge!(model.date_boundaries) if model.charge? || model.subscription? || model.add_on? || model.fixed_charge?

app/serializers/v1/invoice_serializer.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,7 @@ def fees
9393
:customer,
9494
:charge,
9595
:billable_metric,
96+
:presentation_breakdowns,
9697
{charge_filter: {values: :billable_metric_filter}}
9798
]
9899
),
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
# frozen_string_literal: true
2+
3+
module V1
4+
class PresentationBreakdownSerializer < ModelSerializer
5+
def serialize
6+
{
7+
presentation_by: model.presentation_by,
8+
units: model.units.to_s
9+
}
10+
end
11+
end
12+
end

spec/scenarios/current_usage/by_charge_model/dynamic_spec.rb

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -123,8 +123,8 @@
123123

124124
expect(json[:customer_usage][:charges_usage][0][:grouped_usage]).to match_array(
125125
[
126-
{amount_cents: 902, events_count: 1, total_aggregated_units: "10.0", units: "10.0", grouped_by: {group_key: "value 2"}, filters: [], pricing_unit_details: nil},
127-
{amount_cents: 10, events_count: 1, total_aggregated_units: "10.0", units: "10.0", grouped_by: {group_key: "value 1"}, filters: [], pricing_unit_details: nil}
126+
{amount_cents: 902, events_count: 1, total_aggregated_units: "10.0", units: "10.0", grouped_by: {group_key: "value 2"}, filters: [], pricing_unit_details: nil, presentation_breakdowns: []},
127+
{amount_cents: 10, events_count: 1, total_aggregated_units: "10.0", units: "10.0", grouped_by: {group_key: "value 1"}, filters: [], pricing_unit_details: nil, presentation_breakdowns: []}
128128
]
129129
)
130130
end

spec/serializers/v1/customers/charge_usage_serializer_spec.rb

Lines changed: 163 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
let(:ratio) { days_passed.to_f / charges_duration }
2020

2121
let(:pricing_unit_usage) { nil }
22+
let(:presentation_breakdowns) { [] }
2223

2324
let(:usage) do
2425
[
@@ -44,7 +45,8 @@
4445
aggregation_type: billable_metric.aggregation_type,
4546
grouped_by: {"card_type" => "visa"},
4647
charge_filter: nil,
47-
pricing_unit_usage:
48+
pricing_unit_usage:,
49+
presentation_breakdowns:
4850
)
4951
]
5052
end
@@ -73,6 +75,7 @@
7375
"aggregation_type" => billable_metric.aggregation_type
7476
},
7577
"filters" => [],
78+
"presentation_breakdowns" => [],
7679
"grouped_usage" => [
7780
{
7881
"amount_cents" => 100,
@@ -81,12 +84,165 @@
8184
"units" => "10.0",
8285
"total_aggregated_units" => "10.0",
8386
"grouped_by" => {"card_type" => "visa"},
84-
"filters" => []
87+
"filters" => [],
88+
"presentation_breakdowns" => []
8589
}
8690
]
8791
)
8892
end
8993

94+
context "when contains presentation breakdowns" do
95+
let(:presentation_breakdowns) do
96+
[
97+
build(:presentation_breakdown, presentation_by: {"card_type" => "visa"}, units: "8"),
98+
build(:presentation_breakdown, presentation_by: {"card_type" => "mastercard"}, units: "1"),
99+
build(:presentation_breakdown, presentation_by: {"country" => "pt"}, units: "3")
100+
]
101+
end
102+
103+
it "serializes the breakdowns" do
104+
expect(result["charges"].first["presentation_breakdowns"]).to eq([])
105+
expect(result["charges"].first["grouped_usage"].first["presentation_breakdowns"]).to match_array(
106+
[
107+
{"presentation_by" => {"card_type" => "visa"}, "units" => "8.0"},
108+
{"presentation_by" => {"card_type" => "mastercard"}, "units" => "1.0"},
109+
{"presentation_by" => {"country" => "pt"}, "units" => "3.0"}
110+
]
111+
)
112+
end
113+
end
114+
115+
context "when usage contains two objects, one with grouped_by and other without grouped_by" do
116+
let(:other_charge) { create(:standard_charge, plan: charge.plan) }
117+
let(:other_billable_metric) { other_charge.billable_metric }
118+
let(:empty_group_presentation_breakdowns) { [] }
119+
let(:visa_group_presentation_breakdowns) { [] }
120+
121+
let(:usage) do
122+
[
123+
OpenStruct.new(
124+
charge_id: charge.id,
125+
subscription:,
126+
billable_metric: billable_metric,
127+
charge: charge,
128+
units: "2",
129+
total_aggregated_units: "2",
130+
events_count: 2,
131+
amount_cents: 20,
132+
amount_currency: "EUR",
133+
properties: {
134+
"from_datetime" => from_datetime.to_s,
135+
"to_datetime" => to_datetime.to_s,
136+
"charges_duration" => charges_duration
137+
},
138+
invoice_display_name: charge.invoice_display_name,
139+
lago_id: billable_metric.id,
140+
name: billable_metric.name,
141+
code: billable_metric.code,
142+
aggregation_type: billable_metric.aggregation_type,
143+
grouped_by: {},
144+
charge_filter: nil,
145+
pricing_unit_usage: pricing_unit_usage,
146+
presentation_breakdowns: empty_group_presentation_breakdowns
147+
),
148+
OpenStruct.new(
149+
charge_id: other_charge.id,
150+
subscription:,
151+
billable_metric: other_billable_metric,
152+
charge: other_charge,
153+
units: "8",
154+
total_aggregated_units: "8",
155+
events_count: 10,
156+
amount_cents: 80,
157+
amount_currency: "EUR",
158+
properties: {
159+
"from_datetime" => from_datetime.to_s,
160+
"to_datetime" => to_datetime.to_s,
161+
"charges_duration" => charges_duration
162+
},
163+
invoice_display_name: other_charge.invoice_display_name,
164+
lago_id: other_billable_metric.id,
165+
name: other_billable_metric.name,
166+
code: other_billable_metric.code,
167+
aggregation_type: other_billable_metric.aggregation_type,
168+
grouped_by: {"card_type" => "visa"},
169+
charge_filter: nil,
170+
pricing_unit_usage: pricing_unit_usage,
171+
presentation_breakdowns: visa_group_presentation_breakdowns
172+
)
173+
]
174+
end
175+
176+
it "serializes grouped usage including empty group" do
177+
expect(result["charges"].length).to eq(2)
178+
179+
empty_group_charge = result["charges"].find { |c| c.dig("charge", "lago_id") == charge.id }
180+
visa_group_charge = result["charges"].find { |c| c.dig("charge", "lago_id") == other_charge.id }
181+
182+
expect(empty_group_charge).to include(
183+
"units" => "2.0",
184+
"total_aggregated_units" => "2.0",
185+
"events_count" => 2,
186+
"amount_cents" => 20,
187+
"grouped_usage" => [],
188+
"presentation_breakdowns" => []
189+
)
190+
191+
expect(visa_group_charge).to include(
192+
"units" => "8.0",
193+
"total_aggregated_units" => "8.0",
194+
"events_count" => 10,
195+
"amount_cents" => 80,
196+
"grouped_usage" => [
197+
{
198+
"amount_cents" => 80,
199+
"pricing_unit_details" => nil,
200+
"events_count" => 10,
201+
"units" => "8.0",
202+
"total_aggregated_units" => "8.0",
203+
"grouped_by" => {"card_type" => "visa"},
204+
"filters" => [],
205+
"presentation_breakdowns" => []
206+
}
207+
]
208+
)
209+
end
210+
211+
context "when contains presentation breakdowns" do
212+
let(:empty_group_presentation_breakdowns) do
213+
[
214+
build(:presentation_breakdown, presentation_by: {"card_type" => "visa"}, units: "8"),
215+
build(:presentation_breakdown, presentation_by: {"country" => "pt"}, units: "3")
216+
]
217+
end
218+
let(:visa_group_presentation_breakdowns) do
219+
[
220+
build(:presentation_breakdown, presentation_by: {"card_type" => "mastercard"}, units: "1")
221+
]
222+
end
223+
224+
it "serializes breakdowns for ungrouped and grouped usage" do
225+
empty_group_charge = result["charges"].find { |c| c.dig("charge", "lago_id") == charge.id }
226+
visa_group_charge = result["charges"].find { |c| c.dig("charge", "lago_id") == other_charge.id }
227+
228+
expect(empty_group_charge["grouped_usage"]).to eq([])
229+
expect(empty_group_charge["presentation_breakdowns"]).to match_array(
230+
[
231+
{"presentation_by" => {"card_type" => "visa"}, "units" => "8.0"},
232+
{"presentation_by" => {"country" => "pt"}, "units" => "3.0"}
233+
]
234+
)
235+
236+
expect(visa_group_charge["presentation_breakdowns"]).to eq([])
237+
expect(visa_group_charge["grouped_usage"].first["presentation_breakdowns"]).to match_array(
238+
[
239+
{"presentation_by" => {"card_type" => "mastercard"}, "units" => "1.0"}
240+
]
241+
)
242+
end
243+
end
244+
end
245+
90246
context "when charge configured to use pricing units" do
91247
let(:pricing_unit_usage) do
92248
PricingUnitUsage.new(amount_cents: 200, conversion_rate: 0.5, short_name: "CR")
@@ -116,6 +272,7 @@
116272
"aggregation_type" => billable_metric.aggregation_type
117273
},
118274
"filters" => [],
275+
"presentation_breakdowns" => [],
119276
"grouped_usage" => [
120277
{
121278
"amount_cents" => 100,
@@ -128,7 +285,8 @@
128285
"units" => "10.0",
129286
"total_aggregated_units" => "10.0",
130287
"grouped_by" => {"card_type" => "visa"},
131-
"filters" => []
288+
"filters" => [],
289+
"presentation_breakdowns" => []
132290
}
133291
]
134292
)
@@ -157,7 +315,8 @@
157315
grouped_by: {"card_type" => "visa"},
158316
charge_filter:,
159317
charge_filter_id: charge_filter.id,
160-
pricing_unit_usage:
318+
pricing_unit_usage:,
319+
presentation_breakdowns:
161320
)
162321
end
163322
end

0 commit comments

Comments
 (0)