From 33ee1197d45ced329153c47f34d1b7fdfdba7b3a Mon Sep 17 00:00:00 2001 From: Ancor Cruz Date: Thu, 31 Oct 2024 13:37:17 +0000 Subject: [PATCH] feat(dunning): validate dunning campaign has at least one threshold MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 👉 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're first automating the overdue balance payment request, before looking at individual invoices. ## Description This change introduces validation to ensure dunning campaign at least has one threshold --- app/models/dunning_campaign.rb | 8 ++++++++ spec/factories/dunning_campaigns.rb | 4 ++++ spec/models/dunning_campaign_spec.rb | 7 +++++++ .../dunning_campaigns/create_service_spec.rb | 14 ++++++++++++++ 4 files changed, 33 insertions(+) diff --git a/app/models/dunning_campaign.rb b/app/models/dunning_campaign.rb index ee30d11796a..3a3fe7678b7 100644 --- a/app/models/dunning_campaign.rb +++ b/app/models/dunning_campaign.rb @@ -16,12 +16,20 @@ class DunningCampaign < ApplicationRecord validates :days_between_attempts, numericality: {greater_than: 0} validates :max_attempts, numericality: {greater_than: 0} validates :code, uniqueness: {scope: :organization_id} + validate :must_have_at_least_one_threshold scope :applied_to_organization, -> { where(applied_to_organization: true) } def self.ransackable_attributes(_auth_object = nil) %w[name code] end + + private + + def must_have_at_least_one_threshold + errors.add(:thresholds, "must have at least one") if thresholds.empty? + end + end # == Schema Information diff --git a/spec/factories/dunning_campaigns.rb b/spec/factories/dunning_campaigns.rb index 2ffe4d9860d..e563c7b91cb 100644 --- a/spec/factories/dunning_campaigns.rb +++ b/spec/factories/dunning_campaigns.rb @@ -8,5 +8,9 @@ days_between_attempts { Faker::Number.number(digits: 2) } max_attempts { Faker::Number.number(digits: 2) } applied_to_organization { false } + + after(:build) do |dunning_campaign| + dunning_campaign.thresholds << build(:dunning_campaign_threshold, dunning_campaign: dunning_campaign) if dunning_campaign.thresholds.empty? + end end end diff --git a/spec/models/dunning_campaign_spec.rb b/spec/models/dunning_campaign_spec.rb index 36b8e2f25ef..9ffc1d41181 100644 --- a/spec/models/dunning_campaign_spec.rb +++ b/spec/models/dunning_campaign_spec.rb @@ -17,4 +17,11 @@ it { is_expected.to validate_numericality_of(:max_attempts).is_greater_than(0) } it { is_expected.to validate_uniqueness_of(:code).scoped_to(:organization_id) } + + it "is invalid without any thresholds" do + dunning_campaign = DunningCampaign.new + + expect(dunning_campaign).not_to be_valid + expect(dunning_campaign.errors[:thresholds]).to include("must have at least one") + end end diff --git a/spec/services/dunning_campaigns/create_service_spec.rb b/spec/services/dunning_campaigns/create_service_spec.rb index e24a9e7c304..ad725546fd3 100644 --- a/spec/services/dunning_campaigns/create_service_spec.rb +++ b/spec/services/dunning_campaigns/create_service_spec.rb @@ -51,5 +51,19 @@ end end end + + context "without thresholds" do + let(:thresholds) { [] } + + it "returns an error" do + result = create_service.call + + aggregate_failures do + expect(result).not_to be_success + expect(result.error).to be_a(BaseService::ValidationFailure) + expect(result.error.messages[:thresholds]).to eq(["must have at least one"]) + end + end + end end end