|
9 | 9 | describe 'validations' do |
10 | 10 | it { is_expected.to validate_presence_of(:price) } |
11 | 11 | it { is_expected.to validate_numericality_of(:price).is_greater_than_or_equal_to(0) } |
12 | | - |
13 | | - it do |
14 | | - is_expected.to validate_numericality_of(:discount).is_greater_than_or_equal_to(0).is_less_than_or_equal_to(100) |
15 | | - end |
16 | | - |
| 12 | + it { is_expected.to validate_numericality_of(:discount).is_greater_than_or_equal_to(0).is_less_than_or_equal_to(100) } |
17 | 13 | it { is_expected.to validate_presence_of(:final_price) } |
18 | 14 | it { is_expected.to validate_numericality_of(:final_price).is_greater_than_or_equal_to(0) } |
| 15 | + |
| 16 | + context 'boundary values' do |
| 17 | + subject(:quote_item) { build(:quote_item, price: 100, discount: 10) } |
| 18 | + |
| 19 | + it 'is valid with price = 0' do |
| 20 | + subject.price = 0 |
| 21 | + subject.valid? |
| 22 | + expect(subject).to be_valid |
| 23 | + end |
| 24 | + |
| 25 | + it 'is invalid with price < 0' do |
| 26 | + subject.price = -1 |
| 27 | + subject.valid? |
| 28 | + expect(subject).not_to be_valid |
| 29 | + expect(subject.errors[:price]).to include("must be greater than or equal to 0") |
| 30 | + end |
| 31 | + |
| 32 | + it 'is valid with discount = 0' do |
| 33 | + subject.discount = 0 |
| 34 | + expect(subject).to be_valid |
| 35 | + end |
| 36 | + |
| 37 | + it 'is valid with discount = 100' do |
| 38 | + subject.discount = 100 |
| 39 | + expect(subject).to be_valid |
| 40 | + end |
| 41 | + |
| 42 | + it 'is invalid with discount < 0' do |
| 43 | + subject.discount = -0.1 |
| 44 | + expect(subject).not_to be_valid |
| 45 | + expect(subject.errors[:discount]).to include("must be greater than or equal to 0") |
| 46 | + end |
| 47 | + |
| 48 | + it 'is invalid with discount > 100' do |
| 49 | + subject.discount = 100.1 |
| 50 | + expect(subject).not_to be_valid |
| 51 | + expect(subject.errors[:discount]).to include("must be less than or equal to 100") |
| 52 | + end |
| 53 | + |
| 54 | + it 'is valid with final_price = 0' do |
| 55 | + allow(subject).to receive(:calculate_final_price) |
| 56 | + subject.final_price = 0 |
| 57 | + expect(subject).to be_valid |
| 58 | + end |
| 59 | + |
| 60 | + it 'is invalid with final_price < 0' do |
| 61 | + allow(subject).to receive(:calculate_final_price) |
| 62 | + subject.final_price = -1 |
| 63 | + expect(subject).not_to be_valid |
| 64 | + expect(subject.errors[:final_price]).to include("must be greater than or equal to 0") |
| 65 | + end |
| 66 | + end |
19 | 67 | end |
20 | 68 |
|
21 | | - describe 'before_save callbacks' do |
| 69 | + describe 'before_validation callbacks' do |
22 | 70 | let(:quote) { create(:quote) } |
23 | 71 | let(:item) { create(:item) } |
24 | 72 | let(:price) { 1000 } |
25 | 73 | let(:discount) { 10 } |
26 | 74 |
|
27 | | - context 'when quote_item is created' do |
28 | | - let(:quote_item) do |
29 | | - QuoteItem.new(quote: quote, item: item, price: price, discount: discount) |
| 75 | + describe '#compile_pricing_parameters' do |
| 76 | + let(:quote_item) { build(:quote_item, quote: quote, item: item) } |
| 77 | + |
| 78 | + it 'combines fixed, open, and select parameters correctly' do |
| 79 | + item.update(fixed_parameters: { 'platform_fee' => '1000' }) |
| 80 | + quote_item.open_param_values = { 'setup' => '2500' } |
| 81 | + quote_item.select_param_values = { 'tier' => '500' } |
| 82 | + |
| 83 | + quote_item.valid? |
| 84 | + |
| 85 | + expect(quote_item.pricing_parameters).to eq({ |
| 86 | + 'platform_fee' => '1000', 'setup' => '2500', 'tier' => '500' |
| 87 | + }) |
30 | 88 | end |
31 | 89 |
|
32 | | - it 'calculates final_price before saving' do |
33 | | - quote_item.save |
34 | | - expect(quote_item.final_price).to eq(900) |
| 90 | + it 'handles nil open_param_values' do |
| 91 | + item.update(fixed_parameters: { 'platform_fee' => '1000' }) |
| 92 | + quote_item.open_param_values = nil |
| 93 | + quote_item.select_param_values = { 'tier' => '500' } |
| 94 | + |
| 95 | + quote_item.valid? |
| 96 | + |
| 97 | + expect(quote_item.pricing_parameters).to eq({ |
| 98 | + 'platform_fee' => '1000', 'tier' => '500' |
| 99 | + }) |
35 | 100 | end |
36 | 101 | end |
37 | 102 |
|
38 | | - context 'when quote_item is updated' do |
39 | | - let(:quote_item) do |
40 | | - create(:quote_item, quote: quote, item: item, price: price, discount: discount) |
41 | | - end |
| 103 | + describe '#calculate_price_from_formula' do |
| 104 | + let(:quote_item) { build(:quote_item, quote: quote, item: item) } |
42 | 105 |
|
43 | | - context 'when price changes' do |
44 | | - before do |
45 | | - quote_item.price = BigDecimal(1200) |
46 | | - quote_item.save |
47 | | - end |
| 106 | + it 'evaluates the formula using Dentaku' do |
| 107 | + item.update(calculation_formula: 'users * setup', fixed_parameters: { 'users' => 10 }) |
| 108 | + quote_item.open_param_values = { 'setup' => 500 } |
48 | 109 |
|
49 | | - it 'recalculates final_price' do |
50 | | - expect(quote_item.final_price).to eq(1080) |
51 | | - end |
| 110 | + quote_item.valid? |
52 | 111 |
|
53 | | - it 'calls calculate_final_price' do |
54 | | - expect_any_instance_of(QuoteItem).to_not receive(:calculate_final_price) |
55 | | - end |
| 112 | + expect(quote_item.price).to eq(5000) |
56 | 113 | end |
57 | 114 |
|
58 | | - context 'when discount changes' do |
59 | | - before do |
60 | | - quote_item.discount = BigDecimal(20) |
61 | | - quote_item.save |
62 | | - end |
| 115 | + it 'adds error for missing variables' do |
| 116 | + item.update(calculation_formula: 'users * setup', fixed_parameters: {}) |
| 117 | + quote_item.open_param_values = {} |
63 | 118 |
|
64 | | - it 'recalculates final_price' do |
65 | | - expect(quote_item.final_price).to eq(800) |
66 | | - end |
| 119 | + quote_item.valid? |
67 | 120 |
|
68 | | - it 'calls calculate_final_price' do |
69 | | - expect_any_instance_of(QuoteItem).to_not receive(:calculate_final_price) |
70 | | - end |
| 121 | + expect(quote_item.errors[:price]).to include("can't be blank") |
71 | 122 | end |
| 123 | + end |
72 | 124 |
|
73 | | - context 'when neither price nor discount changes' do |
74 | | - before do |
| 125 | + describe '#calculate_final_price' do |
| 126 | + let(:quote_item) { build(:quote_item, quote: quote, item: item, price: price, discount: discount) } |
| 127 | + |
| 128 | + context 'when price and discount are present' do |
| 129 | + it 'calculates final_price on create' do |
75 | 130 | quote_item.save |
| 131 | + expect(quote_item.final_price).to eq(900) |
76 | 132 | end |
77 | 133 |
|
78 | | - it 'does not recalculate final_price' do |
79 | | - expect(quote_item.final_price).to eq(900) |
| 134 | + it 'recalculates final_price when price changes' do |
| 135 | + quote_item.save |
| 136 | + quote_item.price = 1200 |
| 137 | + quote_item.save |
| 138 | + expect(quote_item.final_price).to eq(1080) |
80 | 139 | end |
81 | 140 |
|
82 | | - it 'does not recalculate final_price' do |
83 | | - expect_any_instance_of(QuoteItem).to_not receive(:calculate_final_price) |
| 141 | + it 'recalculates final_price when discount changes' do |
| 142 | + quote_item.save |
| 143 | + quote_item.discount = 20 |
| 144 | + quote_item.save |
| 145 | + expect(quote_item.final_price).to eq(800) |
84 | 146 | end |
85 | 147 | end |
86 | 148 | end |
87 | 149 | end |
| 150 | + |
| 151 | + describe '#item_requires_formula?' do |
| 152 | + let(:quote_item) { build(:quote_item, item: item) } |
| 153 | + let(:item) { build(:item) } |
| 154 | + |
| 155 | + it 'returns true when item has a calculation formula' do |
| 156 | + item.calculation_formula = 'users * setup' |
| 157 | + expect(quote_item.send(:item_requires_formula?)).to be true |
| 158 | + end |
| 159 | + |
| 160 | + it 'returns false when item has no calculation formula' do |
| 161 | + item.calculation_formula = nil |
| 162 | + expect(quote_item.send(:item_requires_formula?)).to be false |
| 163 | + end |
| 164 | + |
| 165 | + it 'returns false when item is nil' do |
| 166 | + quote_item.item = nil |
| 167 | + expect(quote_item.send(:item_requires_formula?)).to be false |
| 168 | + end |
| 169 | + end |
| 170 | + |
| 171 | + describe 'pricing_parameters storage' do |
| 172 | + let(:quote) { create(:quote) } |
| 173 | + let(:item) { create(:item, fixed_parameters: { 'platform_fee' => '1000' }) } |
| 174 | + let(:quote_item) { create(:quote_item, quote: quote, item: item, open_param_values: { 'setup' => '2500' }) } |
| 175 | + |
| 176 | + it 'persists pricing_parameters in the database' do |
| 177 | + quote_item.valid? |
| 178 | + quote_item.save |
| 179 | + expect(QuoteItem.find(quote_item.id).pricing_parameters).to eq({ |
| 180 | + 'platform_fee' => '1000', 'setup' => '2500' |
| 181 | + }) |
| 182 | + end |
| 183 | + end |
| 184 | + |
| 185 | + describe 'quote total price recalculation' do |
| 186 | + let(:quote) { create(:quote, total_price: 0) } |
| 187 | + let(:item) { create(:item) } |
| 188 | + |
| 189 | + it 'triggers after save and updates quote total_price' do |
| 190 | + quote_item = build(:quote_item, quote: quote, item: item, price: 100, discount: 0, final_price: 100) |
| 191 | + expect(quote).to receive(:recalculate_total_price).and_call_original |
| 192 | + quote_item.save |
| 193 | + expect(quote.reload.total_price).to eq(100) |
| 194 | + end |
| 195 | + |
| 196 | + it 'triggers after destroy and updates quote total_price' do |
| 197 | + quote_item = create(:quote_item, quote: quote, item: item, price: 100, discount: 0, final_price: 100) |
| 198 | + quote.update(total_price: 100) |
| 199 | + expect(quote).to receive(:recalculate_total_price).and_call_original |
| 200 | + quote_item.destroy |
| 201 | + expect(quote.reload.total_price).to eq(0) |
| 202 | + end |
| 203 | + end |
88 | 204 | end |
0 commit comments