Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(wallet): Improve how we're refreshing wallet's ongoing balance #2682

Merged
merged 1 commit into from
Oct 14, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion app/jobs/clock/refresh_wallets_ongoing_balance_job.rb
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ class RefreshWalletsOngoingBalanceJob < ApplicationJob
def perform
return unless License.premium?

Wallet.active.find_each do |wallet|
Wallet.active.ready_to_be_refreshed.find_each do |wallet|
Wallets::RefreshOngoingBalanceJob.perform_later(wallet)
end
end
Expand Down
2 changes: 2 additions & 0 deletions app/jobs/wallets/refresh_ongoing_balance_job.rb
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ class RefreshOngoingBalanceJob < ApplicationJob
unique :until_executed, on_conflict: :log

def perform(wallet)
return unless wallet.ready_to_be_refreshed?

Wallets::Balance::RefreshOngoingService.call(wallet:)
end
end
Expand Down
5 changes: 4 additions & 1 deletion app/models/wallet.rb
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ def mark_as_terminated!(timestamp = Time.zone.now)
end

scope :expired, -> { where('wallets.expiration_at::timestamp(0) <= ?', Time.current) }
scope :ready_to_be_refreshed, -> { where(ready_to_be_refreshed: true) }

def currency=(currency)
self.balance_currency = currency
Expand Down Expand Up @@ -62,6 +63,7 @@ def currency
# ongoing_balance_cents :bigint default(0), not null
# ongoing_usage_balance_cents :bigint default(0), not null
# rate_amount :decimal(30, 5) default(0.0), not null
# ready_to_be_refreshed :boolean default(FALSE), not null
# status :integer not null
# terminated_at :datetime
# created_at :datetime not null
Expand All @@ -70,7 +72,8 @@ def currency
#
# Indexes
#
# index_wallets_on_customer_id (customer_id)
# index_wallets_on_customer_id (customer_id)
# index_wallets_on_ready_to_be_refreshed (ready_to_be_refreshed) WHERE ready_to_be_refreshed
#
# Foreign Keys
#
Expand Down
8 changes: 8 additions & 0 deletions app/services/events/post_process_service.rb
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ def call

expire_cached_charges(subscriptions)
flag_refresh_from_subscription
flag_wallets_for_refresh

handle_pay_in_advance

Expand Down Expand Up @@ -95,6 +96,13 @@ def flag_refresh_from_subscription
subscriptions.select(&:active?).each { |s| LifetimeUsages::FlagRefreshFromSubscriptionService.new(subscription: s).call }
end

def flag_wallets_for_refresh
return unless customer
return unless customer.wallets.active.any?

customer.wallets.active.update_all(ready_to_be_refreshed: true) # rubocop:disable Rails/SkipsModelValidations
end

def handle_pay_in_advance
return unless billable_metric
return unless charges.any?
Expand Down
3 changes: 2 additions & 1 deletion app/services/wallets/balance/update_ongoing_service.rb
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,8 @@ def compute_update_params
ongoing_usage_balance_cents: total_usage_amount_cents,
credits_ongoing_usage_balance:,
ongoing_balance_cents:,
credits_ongoing_balance:
credits_ongoing_balance:,
ready_to_be_refreshed: false
}

if !wallet.depleted_ongoing_balance? && ongoing_balance_cents <= 0
Expand Down
10 changes: 10 additions & 0 deletions db/migrate/20241014093451_add_ready_to_be_refreshed_to_wallets.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
# frozen_string_literal: true

class AddReadyToBeRefreshedToWallets < ActiveRecord::Migration[7.1]
disable_ddl_transaction!

def change
add_column :wallets, :ready_to_be_refreshed, :boolean, default: false, null: false
add_index :wallets, :ready_to_be_refreshed, where: "ready_to_be_refreshed", algorithm: :concurrently
end
end
4 changes: 3 additions & 1 deletion db/schema.rb

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

6 changes: 3 additions & 3 deletions spec/jobs/clock/refresh_wallets_ongoing_balance_job_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
describe '.perform' do
let(:organization) { create(:organization) }
let(:customer) { create(:customer, organization:) }
let(:wallet) { create(:wallet, customer:) }
let(:wallet) { create(:wallet, customer:, ready_to_be_refreshed: true) }

before do
wallet
Expand All @@ -30,8 +30,8 @@
expect(Wallets::RefreshOngoingBalanceJob).to have_been_enqueued.with(wallet)
end

context 'when not active' do
let(:wallet) { create(:wallet, :terminated) }
context 'when not ready to be refreshed' do
let(:wallet) { create(:wallet, customer:, ready_to_be_refreshed: false) }

it 'does not call the refresh service' do
described_class.perform_now
Expand Down
16 changes: 13 additions & 3 deletions spec/jobs/wallets/refresh_ongoing_balance_job_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,19 +3,29 @@
require 'rails_helper'

RSpec.describe Wallets::RefreshOngoingBalanceJob, type: :job do
let(:wallet) { create(:wallet) }
let(:wallet) { create(:wallet, ready_to_be_refreshed: true) }
let(:result) { BaseService::Result.new }

let(:refresh_service) do
instance_double(Wallets::Balance::RefreshOngoingService)
end

it 'delegates to the RefreshOngoingBalance service' do
before do
allow(Wallets::Balance::RefreshOngoingService).to receive(:new).with(wallet:).and_return(refresh_service)
allow(refresh_service).to receive(:call).and_return(result)
end

described_class.perform_now(wallet)
context 'when wallet is ready to be refreshed' do
let(:wallet) { create(:wallet, ready_to_be_refreshed: false) }

it 'does not call the RefreshOngoingBalance service' do
described_class.perform_now(wallet)
expect(Wallets::Balance::RefreshOngoingService).not_to have_received(:new)
end
end

it 'delegates to the RefreshOngoingBalance service' do
described_class.perform_now(wallet)
expect(Wallets::Balance::RefreshOngoingService).to have_received(:new)
expect(refresh_service).to have_received(:call)
end
Expand Down
6 changes: 6 additions & 0 deletions spec/services/events/post_process_service_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,12 @@
expect(subscription.lifetime_usage.recalculate_current_usage).to be(true)
end

it 'flags wallets for refresh' do
wallet = create(:wallet, customer:)

expect { process_service.call }.to change { wallet.reload.ready_to_be_refreshed }.from(false).to(true)
end

context 'when event matches an pay_in_advance charge' do
let(:charge) { create(:standard_charge, :pay_in_advance, plan:, billable_metric:, invoiceable: false) }
let(:billable_metric) { create(:billable_metric, organization:, aggregation_type: 'sum_agg', field_name: 'item_id') }
Expand Down
4 changes: 3 additions & 1 deletion spec/services/wallets/balance/update_ongoing_service_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,8 @@
ongoing_usage_balance_cents: 200,
credits_balance: 10.0,
credits_ongoing_balance: 8.0,
credits_ongoing_usage_balance: 2.0
credits_ongoing_usage_balance: 2.0,
ready_to_be_refreshed: true
)
end
let(:total_usage_amount_cents) { 450 }
Expand All @@ -28,6 +29,7 @@
.and change(wallet, :credits_ongoing_usage_balance).from(2.0).to(4.5)
.and change(wallet, :ongoing_balance_cents).from(800).to(550)
.and change(wallet, :credits_ongoing_balance).from(8.0).to(5.5)
.and change(wallet, :ready_to_be_refreshed).from(true).to(false)

expect(wallet).not_to be_depleted_ongoing_balance
end
Expand Down