Skip to content

Commit 3181262

Browse files
committed
add possibility to preview applied_credits application
1 parent 5b5bf7f commit 3181262

3 files changed

Lines changed: 136 additions & 54 deletions

File tree

app/services/credits/applied_prepaid_credits_service.rb

Lines changed: 58 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,9 @@
22

33
module Credits
44
class AppliedPrepaidCreditsService < BaseService
5-
def initialize(invoice:)
5+
def initialize(invoice:, preview: false)
66
@invoice = invoice
7+
@preview = preview
78

89
super(nil)
910
end
@@ -20,50 +21,21 @@ def call
2021

2122
ActiveRecord::Base.transaction do
2223
Customers::LockService.call(customer:, scope: :prepaid_credit) do
23-
ordered_remaining_amounts = calculate_amounts_for_fees_by_type_and_bm
24-
remaining_invoice_amount = invoice.total_amount_cents
25-
26-
wallets.each do |wallet|
27-
wallet.reload
28-
wallet_fee_transactions = []
29-
wallet_targets_array = wallet.wallet_targets.map do |wt|
30-
if wt&.billable_metric_id
31-
["charge", wt.billable_metric_id]
32-
end
33-
end
34-
wallet_types_array = wallet.allowed_fee_types
35-
36-
ordered_remaining_amounts.each do |fee_key, remaining_amount|
37-
next if remaining_amount <= 0
38-
39-
next unless applicable_fee?(fee_key:, targets: wallet_targets_array, types: wallet_types_array, wallet:)
40-
41-
used_amount = wallet_fee_transactions.sum { |t| t[:amount_cents] }
42-
remaining_wallet_balance = wallet.balance_cents - used_amount
43-
next if remaining_wallet_balance <= 0
44-
45-
transaction_amount = [remaining_amount, remaining_wallet_balance, remaining_invoice_amount].min
46-
next if transaction_amount <= 0
47-
48-
ordered_remaining_amounts[fee_key] -= transaction_amount
49-
remaining_invoice_amount -= transaction_amount
50-
wallet_fee_transactions << {
51-
fee_key: fee_key,
52-
amount_cents: transaction_amount
53-
}
54-
end
55-
total_amount_cents = wallet_fee_transactions.sum { |t| t[:amount_cents] }
56-
next if total_amount_cents <= 0
24+
# per each wallet we create a single wallet transaction. wallets_transactions is a hash with wallet and total transaction amount
25+
wallets_transactions = calculate_wallet_transactions
26+
if preview
27+
result.wallet_transactions = wallets_transactions
28+
return result
29+
end
5730

58-
wallet_transaction = create_wallet_transaction(wallet, total_amount_cents)
31+
wallets_transactions.each do |wallet, amount_cents|
32+
wallet_transaction = create_wallet_transaction(wallet, amount_cents)
33+
result.wallet_transactions << wallet_transaction
5934

6035
if wallet.traceable?
6136
WalletTransactions::TrackConsumptionService.call!(outbound_wallet_transaction: wallet_transaction)
6237
end
63-
6438
Wallets::Balance::DecreaseService.call(wallet:, wallet_transaction:, skip_refresh: true)
65-
66-
result.wallet_transactions << wallet_transaction
6739
end
6840

6941
update_prepaid_credit_amounts(result.wallet_transactions)
@@ -80,10 +52,53 @@ def call
8052

8153
private
8254

83-
attr_accessor :invoice
55+
attr_accessor :invoice, :preview
8456

8557
delegate :customer, to: :invoice
8658

59+
def calculate_wallet_transactions
60+
ordered_remaining_amounts = calculate_amounts_for_fees_by_type_and_bm
61+
remaining_invoice_amount = invoice.total_amount_cents
62+
wallets_transactions = {}
63+
64+
wallets.each do |wallet|
65+
wallet.reload
66+
wallet_fee_transactions = []
67+
wallet_targets_array = wallet.wallet_targets.map do |wt|
68+
if wt&.billable_metric_id
69+
["charge", wt.billable_metric_id]
70+
end
71+
end
72+
wallet_types_array = wallet.allowed_fee_types
73+
byebug
74+
75+
ordered_remaining_amounts.each do |fee_key, remaining_amount|
76+
next if remaining_amount <= 0
77+
byebug
78+
79+
next unless applicable_fee?(fee_key:, targets: wallet_targets_array, types: wallet_types_array, wallet:)
80+
81+
used_amount = wallet_fee_transactions.sum { |t| t[:amount_cents] }
82+
remaining_wallet_balance = wallet.balance_cents - used_amount
83+
next if remaining_wallet_balance <= 0
84+
85+
transaction_amount = [remaining_amount, remaining_wallet_balance, remaining_invoice_amount].min
86+
next if transaction_amount <= 0
87+
88+
ordered_remaining_amounts[fee_key] -= transaction_amount
89+
remaining_invoice_amount -= transaction_amount
90+
wallet_fee_transactions << {
91+
fee_key: fee_key,
92+
amount_cents: transaction_amount
93+
}
94+
end
95+
total_amount_cents = wallet_fee_transactions.sum { |t| t[:amount_cents] }
96+
next if total_amount_cents <= 0
97+
wallets_transactions[wallet] = total_amount_cents
98+
end
99+
wallets_transactions
100+
end
101+
87102
def schedule_webhook_notifications(wallet_transactions)
88103
wallet_transactions.each do |wt|
89104
Utils::ActivityLog.produce_after_commit(wt, "wallet_transaction.created")
@@ -125,8 +140,9 @@ def calculate_prepaid_credit_breakdown(wallet_transactions)
125140

126141
def calculate_amounts_for_fees_by_type_and_bm
127142
remaining = Hash.new(0)
143+
fees = invoice.persisted? ? invoice.fees.includes(:charge) : invoice.fees
128144

129-
invoice.fees.includes(:charge).find_each do |fee|
145+
fees.each do |fee|
130146
next if fee.sub_total_excluding_taxes_amount_cents == 0
131147

132148
cap = fee.sub_total_excluding_taxes_amount_cents +
@@ -162,6 +178,7 @@ def reconcile_remaining_amounts(ordered_remaining_amounts)
162178

163179
def wallets_already_applied?
164180
return false unless invoice
181+
return false unless invoice.persisted?
165182

166183
WalletTransaction.exists?(invoice_id: invoice.id, wallet_id: wallets.map(&:id))
167184
end

app/services/invoices/preview_service.rb

Lines changed: 4 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -264,23 +264,14 @@ def create_credit_note_credits
264264

265265
def create_applied_prepaid_credits
266266
return unless customer.persisted?
267-
return unless wallet
268267
return unless invoice.total_amount_cents&.positive?
269-
return unless wallet.balance.positive?
270268

271-
amount_cents = if wallet.balance_cents <= invoice.total_amount_cents
272-
wallet.balance_cents
273-
else
274-
invoice.total_amount_cents
275-
end
276-
invoice.prepaid_credit_amount_cents += amount_cents
277-
invoice.total_amount_cents -= amount_cents
278-
end
269+
wallets_transactions = Credits::AppliedPrepaidCreditsService.call!(invoice:, preview: true).wallet_transactions
270+
amount_cents = wallets_transactions.sum {|_k, v| v }
279271

280-
def wallet
281-
return @wallet if defined? @wallet
282272

283-
@wallet = customer.wallets.active.first
273+
invoice.prepaid_credit_amount_cents += amount_cents
274+
invoice.total_amount_cents -= amount_cents
284275
end
285276

286277
def apply_taxes

spec/scenarios/invoices/preview_spec.rb

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,4 +81,78 @@
8181
end
8282
end
8383
end
84+
85+
context "when wallet has allowed_fee_types restriction" do
86+
let(:customer) { create(:customer, organization:) }
87+
let(:plan) { create(:plan, organization:, amount_cents: 2_000) }
88+
89+
let(:preview_params) do
90+
{
91+
customer: {external_id: customer.external_id},
92+
plan_code: plan.code,
93+
billing_time: "calendar"
94+
}
95+
end
96+
97+
context "and the preview invoice contains only subscription fees" do
98+
before do
99+
create(
100+
:wallet,
101+
:with_inbound_transaction,
102+
customer:,
103+
organization:,
104+
balance_cents: 500,
105+
credits_balance: 5.0,
106+
allowed_fee_types: %w[charge]
107+
)
108+
end
109+
110+
it "does not apply wallet credit to subscription fees" do
111+
travel_to(DateTime.parse("2026-03-01 12:00:00")) do
112+
post_with_token(organization, "/api/v1/invoices/preview", preview_params)
113+
114+
expect(response).to have_http_status(:success)
115+
expect(json[:invoice]).to include(
116+
fees_amount_cents: 2_000,
117+
prepaid_credit_amount_cents: 0,
118+
total_amount_cents: 2_000
119+
)
120+
end
121+
end
122+
end
123+
124+
context "and the preview invoice contains a mix of matching and non-matching fees" do
125+
before do
126+
create(:fixed_charge, plan:, units: 1, charge_model: "standard", properties: {amount: "3"})
127+
128+
create(
129+
:wallet,
130+
:with_inbound_transaction,
131+
customer:,
132+
organization:,
133+
balance_cents: 500,
134+
credits_balance: 5.0,
135+
allowed_fee_types: %w[fixed_charge]
136+
)
137+
end
138+
139+
it "applies wallet credit only to matching fee types, capped at the fee amount" do
140+
travel_to(DateTime.parse("2026-03-01 12:00:00")) do
141+
# subscription fee = 2000 cents (full March, calendar billing)
142+
# fixed_charge fee = 1 unit * 3 EUR = 300 cents
143+
# wallet 500 cents is restricted to fixed_charge fees,
144+
# so it can only consume against the 300 cents fixed_charge fee
145+
# prepaid_credit_amount_cents = 300, NOT 500
146+
post_with_token(organization, "/api/v1/invoices/preview", preview_params)
147+
148+
expect(response).to have_http_status(:success)
149+
expect(json[:invoice]).to include(
150+
fees_amount_cents: 2_300,
151+
prepaid_credit_amount_cents: 300,
152+
total_amount_cents: 2_000
153+
)
154+
end
155+
end
156+
end
157+
end
84158
end

0 commit comments

Comments
 (0)