Skip to content

Commit

Permalink
feat (cascade-plan-updates): add logic for cascading charge removal (#…
Browse files Browse the repository at this point in the history
…2715)

## Context

Currently when plan is updated, these changes are not cascaded to all
the children plans.

## Description

This PR handles the case for cascading charge removal case
  • Loading branch information
lovrocolic authored Oct 21, 2024
1 parent faa7518 commit fb27588
Show file tree
Hide file tree
Showing 7 changed files with 191 additions and 10 deletions.
3 changes: 1 addition & 2 deletions app/jobs/charges/create_job.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,7 @@ class CreateJob < ApplicationJob
queue_as 'default'

def perform(plan:, params:)
create_result = Charges::CreateService.call(plan:, params:)
create_result.raise_if_error!
Charges::CreateService.call(plan:, params:).raise_if_error!
end
end
end
11 changes: 11 additions & 0 deletions app/jobs/charges/destroy_job.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
# frozen_string_literal: true

module Charges
class DestroyJob < ApplicationJob
queue_as 'default'

def perform(charge:)
Charges::DestroyService.call(charge:).raise_if_error!
end
end
end
33 changes: 33 additions & 0 deletions app/services/charges/destroy_service.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
# frozen_string_literal: true

module Charges
class DestroyService < BaseService
def initialize(charge:)
@charge = charge

super
end

def call
return result.not_found_failure!(resource: 'charge') unless charge

ActiveRecord::Base.transaction do
charge.discard!
charge.filter_values.discard_all
charge.filters.discard_all

result.charge = charge
end

result
rescue ActiveRecord::RecordInvalid => e
result.record_validation_failure!(record: e.record)
rescue BaseService::FailedResult => e
e.result
end

private

attr_reader :charge
end
end
23 changes: 15 additions & 8 deletions app/services/plans/update_service.rb
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,17 @@ def cascade_charge_creation(payload_charge)
end
end

def cascade_charge_removal(charge)
return unless cascade?
return if plan.children.empty?

plan.children.includes(:charges).find_each do |p|
child_charge = p.charges.find { |c| c.parent_id == charge.id }

Charges::DestroyJob.perform_later(charge: child_charge) if child_charge
end
end

def cascade?
ActiveModel::Type::Boolean.new.cast(params[:cascade_updates])
end
Expand Down Expand Up @@ -258,7 +269,10 @@ def process_charges(plan, params_charges)
def sanitize_charges(plan, args_charges, created_charges_ids)
args_charges_ids = args_charges.map { |c| c[:id] }.compact
charges_ids = plan.charges.pluck(:id) - args_charges_ids - created_charges_ids
plan.charges.where(id: charges_ids).find_each { |charge| discard_charge!(charge) }
plan.charges.where(id: charges_ids).find_each do |charge|
Charges::DestroyService.call(charge:)
cascade_charge_removal(charge)
end
end

def sanitize_thresholds(plan, args_thresholds, created_thresholds_ids)
Expand All @@ -267,13 +281,6 @@ def sanitize_thresholds(plan, args_thresholds, created_thresholds_ids)
plan.usage_thresholds.where(id: thresholds_ids).discard_all
end

def discard_charge!(charge)
charge.discard!

charge.filter_values.discard_all
charge.filters.discard_all
end

# NOTE: We should remove pending subscriptions
# if plan has been downgraded but amount cents became less than downgraded value. This pending subscription
# is not relevant in this case and downgrade should be ignored
Expand Down
17 changes: 17 additions & 0 deletions spec/jobs/charges/destroy_job_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# frozen_string_literal: true

require 'rails_helper'

RSpec.describe Charges::DestroyJob, type: :job do
let(:charge) { create(:standard_charge) }

before do
allow(Charges::DestroyService).to receive(:call).with(charge:).and_return(BaseService::Result.new)
end

it 'calls the service' do
described_class.perform_now(charge:)

expect(Charges::DestroyService).to have_received(:call)
end
end
54 changes: 54 additions & 0 deletions spec/services/charges/destroy_service_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
# frozen_string_literal: true

require 'rails_helper'

RSpec.describe Charges::DestroyService, type: :service do
subject(:destroy_service) { described_class.new(charge:) }

let(:membership) { create(:membership) }
let(:organization) { membership.organization }
let(:billable_metric) { create(:billable_metric, organization:) }
let(:subscription) { create(:subscription) }
let(:charge) { create(:standard_charge, plan: subscription.plan, billable_metric:) }

let(:filters) { create_list(:billable_metric_filter, 2, billable_metric:) }
let(:charge_filter) { create(:charge_filter, charge:) }
let(:filter_value) do
create(:charge_filter_value, charge_filter:, billable_metric_filter: filters.first)
end

before do
charge
filter_value
end

describe '#call' do
it 'soft deletes the charge' do
freeze_time do
expect { destroy_service.call }.to change(Charge, :count).by(-1)
.and change { charge.reload.deleted_at }.from(nil).to(Time.current)
end
end

it 'soft deletes all related filters' do
freeze_time do
expect { destroy_service.call }.to change { charge_filter.reload.deleted_at }.from(nil).to(Time.current)
end
end

it 'soft deletes all related filter values' do
freeze_time do
expect { destroy_service.call }.to change { filter_value.reload.deleted_at }.from(nil).to(Time.current)
end
end

context 'when charge is not found' do
it 'returns an error' do
result = described_class.new(charge: nil).call

expect(result).not_to be_success
expect(result.error.error_code).to eq('charge_not_found')
end
end
end
end
60 changes: 60 additions & 0 deletions spec/services/plans/update_service_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -932,6 +932,66 @@
.to change { charge.reload.deleted_at }.from(nil).to(Time.current)
end
end

context 'with cascade option' do
let(:child_plan) { create(:plan, organization:, parent_id:) }
let(:parent_id) { plan.id }
let(:charge_parent_id) { charge.id }
let(:child_charge) do
create(
:standard_charge,
plan_id: child_plan.id,
parent_id: charge_parent_id,
billable_metric_id: billable_metric.id,
properties: {amount: '300'}
)
end

before do
child_charge
update_args[:cascade_updates] = true
end

context 'when cascade is true and there is no children plans' do
let(:parent_id) { nil }

it 'does not enqueue the job for removing charge' do
expect do
plans_service.call
end.not_to have_enqueued_job(Charges::DestroyJob)
end
end

context 'when cascade is true and there are children plans' do
it 'enqueues the job for removing charge' do
expect do
plans_service.call
end.to have_enqueued_job(Charges::DestroyJob)
end
end

context 'when cascade is true and there are children plans without link to parent charge' do
let(:charge_parent_id) { nil }

it 'does not enqueue the job for removing charge' do
expect do
plans_service.call
end.not_to have_enqueued_job(Charges::DestroyJob)
end
end

context 'when cascade is false with children plans' do
before do
update_args[:cascade_updates] = false
end

it 'does not enqueue the job for removing charge' do
expect do
plans_service.call
end.not_to have_enqueued_job(Charges::DestroyJob)
end
end
end
end

context 'when attached to a subscription' do
Expand Down

0 comments on commit fb27588

Please sign in to comment.