Skip to content

Commit

Permalink
feat(dunning): Add deleted_at to dunning campaign threshold (#2831)
Browse files Browse the repository at this point in the history
## Roadmap Task

👉 https://getlago.canny.io/feature-requests/p/set-up-payment-retry-logic

👉
https://getlago.canny.io/feature-requests/p/send-reminders-for-overdue-invoices

 ## Context

We want to automate dunning process so that our users don't have to look
at each customer to maximize their chances of being paid retrying
payments of overdue balances and sending email reminders.

We are extending dunning campaigns management to edit and delete
campaigns.

 ## Description

This change adds capabilities to soft delete dunning campaign thresholds
  • Loading branch information
ancorcruz authored Nov 19, 2024
1 parent 26d0d24 commit cdeac9a
Show file tree
Hide file tree
Showing 5 changed files with 68 additions and 4 deletions.
10 changes: 8 additions & 2 deletions app/models/dunning_campaign_threshold.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,16 @@
class DunningCampaignThreshold < ApplicationRecord
include Currencies
include PaperTrailTraceable
include Discard::Model
self.discard_column = :deleted_at

belongs_to :dunning_campaign

validates :amount_cents, numericality: {greater_than_or_equal_to: 0}
validates :currency, inclusion: {in: currency_list}
validates :currency, uniqueness: {scope: :dunning_campaign_id}
validates :currency, uniqueness: {scope: :dunning_campaign_id}, unless: :deleted_at

default_scope -> { kept }
end

# == Schema Information
Expand All @@ -18,13 +22,15 @@ class DunningCampaignThreshold < ApplicationRecord
# id :uuid not null, primary key
# amount_cents :bigint not null
# currency :string not null
# deleted_at :datetime
# created_at :datetime not null
# updated_at :datetime not null
# dunning_campaign_id :uuid not null
#
# Indexes
#
# idx_on_dunning_campaign_id_currency_fbf233b2ae (dunning_campaign_id,currency) UNIQUE
# idx_on_dunning_campaign_id_currency_fbf233b2ae (dunning_campaign_id,currency) UNIQUE WHERE (deleted_at IS NULL)
# index_dunning_campaign_thresholds_on_deleted_at (deleted_at)
# index_dunning_campaign_thresholds_on_dunning_campaign_id (dunning_campaign_id)
#
# Foreign Keys
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
# frozen_string_literal: true

class AddDeletedAtToDunningCampaignThresholds < ActiveRecord::Migration[7.1]
def change
add_column :dunning_campaign_thresholds, :deleted_at, :timestamp

safety_assured do
add_index :dunning_campaign_thresholds, :deleted_at
end
end
end
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
# frozen_string_literal: true

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

def change
remove_index :dunning_campaign_thresholds, %i[dunning_campaign_id currency], unique: true, algorithm: :concurrently

add_index :dunning_campaign_thresholds,
[:dunning_campaign_id, :currency],
unique: true,
where: "deleted_at IS NULL",
algorithm: :concurrently
end
end
6 changes: 4 additions & 2 deletions db/schema.rb

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

30 changes: 30 additions & 0 deletions spec/models/dunning_campaign_threshold_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -12,4 +12,34 @@
it { is_expected.to validate_numericality_of(:amount_cents).is_greater_than_or_equal_to(0) }
it { is_expected.to validate_inclusion_of(:currency).in_array(described_class.currency_list) }
it { is_expected.to validate_uniqueness_of(:currency).scoped_to(:dunning_campaign_id) }

describe "currency validation" do
let(:currency) { "EUR" }
let(:dunning_campaign) { create(:dunning_campaign) }

it "validates uniqueness of currency scoped to dunning_campaign_id excluding deleted records" do
create(:dunning_campaign_threshold, currency:, dunning_campaign:)
new_record = build(:dunning_campaign_threshold, currency:, dunning_campaign:)

expect(new_record).not_to be_valid
expect(new_record.errors[:currency]).to include("value_already_exist")

# Records with deleted_at set should not conflict
deleted_record = create(:dunning_campaign_threshold, :deleted, currency:, dunning_campaign:)
expect(deleted_record).to be_valid
end
end

describe "default scope" do
let(:deleted_dunning_campaign_threshold) do
create(:dunning_campaign_threshold, :deleted)
end

before { deleted_dunning_campaign_threshold }

it "only returns non-deleted dunning_campaign_threshold objects" do
expect(described_class.all).to eq([])
expect(described_class.with_discarded).to eq([deleted_dunning_campaign_threshold])
end
end
end

0 comments on commit cdeac9a

Please sign in to comment.