Skip to content

Commit

Permalink
Fix unique validator for vouche code
Browse files Browse the repository at this point in the history
Paranoia doesn't support unique validation including deleted records:
  rubysherpas/paranoia#333
We use a custom validator, ScopedUniquenessValidator to avoid the issue
  • Loading branch information
rioug authored and mkllnk committed Nov 26, 2024
1 parent bf7aaf3 commit 8a010bc
Show file tree
Hide file tree
Showing 6 changed files with 68 additions and 4 deletions.
2 changes: 1 addition & 1 deletion app/models/vouchers/flat_rate.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,6 @@ module Vouchers
class FlatRate < Voucher
include FlatRatable

validates :code, uniqueness: { scope: :enterprise_id }
validates_with ScopedUniquenessValidator
end
end
2 changes: 1 addition & 1 deletion app/models/vouchers/percentage_rate.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ class PercentageRate < Voucher
validates :amount,
presence: true,
numericality: { greater_than: 0, less_than_or_equal_to: 100 }
validates :code, uniqueness: { scope: :enterprise_id }
validates_with ScopedUniquenessValidator

def display_value
ActionController::Base.helpers.number_to_percentage(amount, precision: 2)
Expand Down
25 changes: 25 additions & 0 deletions app/validators/vouchers/scoped_uniqueness_validator.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
# frozen_string_literal: false

# paranoia doesn't support unique validation including deleted records:
# https://github.com/rubysherpas/paranoia/pull/333
# We use a custom validator to fix the issue, so we don't need to fork/patch the gem
module Vouchers
class ScopedUniquenessValidator < ActiveModel::Validator
def validate(record)
@record = record

return unless unique_voucher_code_per_enterprise?

record.errors.add :code, :taken, value: @record.code
end

private

def unique_voucher_code_per_enterprise?
query = Voucher.with_deleted.where(code: @record.code, enterprise_id: @record.enterprise_id)
query = query.where.not(id: @record.id) unless @record.id.nil?

query.present?
end
end
end
2 changes: 1 addition & 1 deletion spec/models/vouchers/flat_rate_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@

it { is_expected.to validate_presence_of(:amount) }
it { is_expected.to validate_numericality_of(:amount).is_greater_than(0) }
it { is_expected.to validate_uniqueness_of(:code).scoped_to(:enterprise_id) }
it_behaves_like 'has a unique code per enterprise', "voucher_flat_rate"
end

describe '#compute_amount' do
Expand Down
2 changes: 1 addition & 1 deletion spec/models/vouchers/percentage_rate_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
.is_greater_than(0)
.is_less_than_or_equal_to(100)
end
it { is_expected.to validate_uniqueness_of(:code).scoped_to(:enterprise_id) }
it_behaves_like 'has a unique code per enterprise', "voucher_percentage_rate"
end

describe '#compute_amount' do
Expand Down
39 changes: 39 additions & 0 deletions spec/support/voucher_uniqueness_helper.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
# frozen_string_literal: true

shared_examples_for 'has a unique code per enterprise' do |voucher_type|
describe "code" do
let(:code) { "super_code" }
let(:enterprise) { create(:enterprise) }

it "is unique per enterprise" do
voucher = create(voucher_type, code:, enterprise:)
expect(voucher).to be_valid

expect_voucher_with_same_enterprise_to_be_invalid(voucher_type)

expect_voucher_with_other_enterprise_to_be_valid(voucher_type)
end

context "with deleted voucher" do
it "is unique per enterprise" do
create(voucher_type, code:, enterprise:).destroy!

expect_voucher_with_same_enterprise_to_be_invalid(voucher_type)

expect_voucher_with_other_enterprise_to_be_valid(voucher_type)
end
end
end

def expect_voucher_with_same_enterprise_to_be_invalid(voucher_type)
new_voucher = build(voucher_type, code:, enterprise: )

expect(new_voucher).not_to be_valid
expect(new_voucher.errors.full_messages).to include("Code has already been taken")
end

def expect_voucher_with_other_enterprise_to_be_valid(voucher_type)
other_voucher = build(voucher_type, code:, enterprise: create(:enterprise) )
expect(other_voucher).to be_valid
end
end

0 comments on commit 8a010bc

Please sign in to comment.