bharat_filter_engine 0.1.2 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/Gemfile +12 -9
- data/README.md +1 -1
- data/Rakefile +5 -3
- data/lib/bharat_filter_engine/association_resolver.rb +18 -37
- data/lib/bharat_filter_engine/column_helper.rb +28 -0
- data/lib/bharat_filter_engine/dynamic_filter_values.rb +17 -18
- data/lib/bharat_filter_engine/engine.rb +249 -91
- data/lib/bharat_filter_engine/errors.rb +1 -1
- data/lib/bharat_filter_engine/join_tracker.rb +37 -0
- data/lib/bharat_filter_engine/rule_normalizer.rb +88 -30
- data/lib/bharat_filter_engine/search_builder.rb +48 -24
- data/lib/bharat_filter_engine/version.rb +1 -1
- data/lib/bharat_filter_engine.rb +14 -12
- data/spec/association_resolver_spec.rb +60 -41
- data/spec/dynamic_filter_values_spec.rb +161 -29
- data/spec/engine_spec.rb +1144 -538
- data/spec/join_tracker_spec.rb +70 -0
- data/spec/rule_normalizer_spec.rb +181 -32
- data/spec/same_table_join_spec.rb +199 -0
- data/spec/search_builder_spec.rb +222 -42
- data/spec/spec_helper.rb +11 -12
- metadata +14 -6
data/spec/engine_spec.rb
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
|
|
1
|
+
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
require 'spec_helper'
|
|
4
4
|
|
|
5
|
+
RSpec.describe BharatFilterEngine::Engine do
|
|
5
6
|
# `let` (not `let!`) on purpose: these fixtures are only referenced
|
|
6
7
|
# by name in the first few examples below ("applies string filter",
|
|
7
8
|
# etc). Every other example in this file builds its own local
|
|
@@ -12,14 +13,14 @@ RSpec.describe BharatFilterEngine::Engine do
|
|
|
12
13
|
# results whenever their filter happens to match it.
|
|
13
14
|
let(:client) do
|
|
14
15
|
Client.create!(
|
|
15
|
-
name:
|
|
16
|
-
email:
|
|
16
|
+
name: 'John Doe',
|
|
17
|
+
email: 'john@example.com'
|
|
17
18
|
)
|
|
18
19
|
end
|
|
19
20
|
|
|
20
21
|
let(:lead) do
|
|
21
22
|
Lead.create!(
|
|
22
|
-
source:
|
|
23
|
+
source: 'google',
|
|
23
24
|
client: client,
|
|
24
25
|
created_at: Time.utc(2026, 7, 20)
|
|
25
26
|
)
|
|
@@ -27,8 +28,8 @@ RSpec.describe BharatFilterEngine::Engine do
|
|
|
27
28
|
|
|
28
29
|
let(:sale) do
|
|
29
30
|
Sale.create!(
|
|
30
|
-
stage:
|
|
31
|
-
approval_status:
|
|
31
|
+
stage: 'qualified',
|
|
32
|
+
approval_status: 'approved',
|
|
32
33
|
project_id: 10,
|
|
33
34
|
active: true,
|
|
34
35
|
amount: 500,
|
|
@@ -37,10 +38,8 @@ RSpec.describe BharatFilterEngine::Engine do
|
|
|
37
38
|
)
|
|
38
39
|
end
|
|
39
40
|
|
|
40
|
-
describe
|
|
41
|
-
|
|
42
|
-
it "applies string filter" do
|
|
43
|
-
|
|
41
|
+
describe '.apply' do
|
|
42
|
+
it 'applies string filter' do
|
|
44
43
|
config = {
|
|
45
44
|
stage: {
|
|
46
45
|
type: :single,
|
|
@@ -54,15 +53,14 @@ RSpec.describe BharatFilterEngine::Engine do
|
|
|
54
53
|
scope: Sale.all,
|
|
55
54
|
config: config,
|
|
56
55
|
params: {
|
|
57
|
-
stage:
|
|
56
|
+
stage: 'qualified'
|
|
58
57
|
}
|
|
59
58
|
)
|
|
60
59
|
|
|
61
60
|
expect(result).to contain_exactly(sale)
|
|
62
61
|
end
|
|
63
62
|
|
|
64
|
-
it
|
|
65
|
-
|
|
63
|
+
it 'applies array filter' do
|
|
66
64
|
config = {
|
|
67
65
|
status: {
|
|
68
66
|
type: :single,
|
|
@@ -76,9 +74,9 @@ RSpec.describe BharatFilterEngine::Engine do
|
|
|
76
74
|
scope: Sale.all,
|
|
77
75
|
config: config,
|
|
78
76
|
params: {
|
|
79
|
-
status: [
|
|
80
|
-
|
|
81
|
-
|
|
77
|
+
status: %w[
|
|
78
|
+
approved
|
|
79
|
+
pending
|
|
82
80
|
]
|
|
83
81
|
}
|
|
84
82
|
)
|
|
@@ -86,8 +84,7 @@ RSpec.describe BharatFilterEngine::Engine do
|
|
|
86
84
|
expect(result).to contain_exactly(sale)
|
|
87
85
|
end
|
|
88
86
|
|
|
89
|
-
it
|
|
90
|
-
|
|
87
|
+
it 'applies boolean filter' do
|
|
91
88
|
config = {
|
|
92
89
|
active: {
|
|
93
90
|
type: :single,
|
|
@@ -101,15 +98,14 @@ RSpec.describe BharatFilterEngine::Engine do
|
|
|
101
98
|
scope: Sale.all,
|
|
102
99
|
config: config,
|
|
103
100
|
params: {
|
|
104
|
-
active:
|
|
101
|
+
active: 'true'
|
|
105
102
|
}
|
|
106
103
|
)
|
|
107
104
|
|
|
108
105
|
expect(result).to contain_exactly(sale)
|
|
109
106
|
end
|
|
110
107
|
|
|
111
|
-
it
|
|
112
|
-
|
|
108
|
+
it 'applies integer gte filter' do
|
|
113
109
|
config = {
|
|
114
110
|
amount: {
|
|
115
111
|
type: :single,
|
|
@@ -131,8 +127,7 @@ RSpec.describe BharatFilterEngine::Engine do
|
|
|
131
127
|
expect(result).to contain_exactly(sale)
|
|
132
128
|
end
|
|
133
129
|
|
|
134
|
-
it
|
|
135
|
-
|
|
130
|
+
it 'applies date range' do
|
|
136
131
|
config = {
|
|
137
132
|
actual_sale_date: {
|
|
138
133
|
type: :single,
|
|
@@ -147,8 +142,8 @@ RSpec.describe BharatFilterEngine::Engine do
|
|
|
147
142
|
config: config,
|
|
148
143
|
params: {
|
|
149
144
|
actual_sale_date: {
|
|
150
|
-
from:
|
|
151
|
-
to:
|
|
145
|
+
from: '2026-07-15',
|
|
146
|
+
to: '2026-07-30'
|
|
152
147
|
}
|
|
153
148
|
}
|
|
154
149
|
)
|
|
@@ -156,8 +151,7 @@ RSpec.describe BharatFilterEngine::Engine do
|
|
|
156
151
|
expect(result).to contain_exactly(sale)
|
|
157
152
|
end
|
|
158
153
|
|
|
159
|
-
it
|
|
160
|
-
|
|
154
|
+
it 'applies nested filter' do
|
|
161
155
|
config = {
|
|
162
156
|
lead_source: {
|
|
163
157
|
type: :nested,
|
|
@@ -172,723 +166,1335 @@ RSpec.describe BharatFilterEngine::Engine do
|
|
|
172
166
|
scope: Sale.all,
|
|
173
167
|
config: config,
|
|
174
168
|
params: {
|
|
175
|
-
lead_source: [
|
|
169
|
+
lead_source: ['google']
|
|
176
170
|
}
|
|
177
171
|
)
|
|
178
172
|
|
|
179
173
|
expect(result).to contain_exactly(sale)
|
|
180
174
|
end
|
|
181
175
|
|
|
182
|
-
it
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
176
|
+
it 'returns no records when string value does not match' do
|
|
177
|
+
Sale.create!(
|
|
178
|
+
stage: 'qualified'
|
|
179
|
+
)
|
|
186
180
|
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
}
|
|
181
|
+
config = {
|
|
182
|
+
stage: {
|
|
183
|
+
type: :single,
|
|
184
|
+
filter_type: :string,
|
|
185
|
+
dbcolumn: :stage
|
|
193
186
|
}
|
|
187
|
+
}
|
|
194
188
|
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
189
|
+
result = described_class.apply(
|
|
190
|
+
scope: Sale.all,
|
|
191
|
+
config: config,
|
|
192
|
+
params: {
|
|
193
|
+
stage: 'closed'
|
|
194
|
+
}
|
|
195
|
+
)
|
|
202
196
|
|
|
203
|
-
|
|
197
|
+
expect(result).to be_empty
|
|
204
198
|
end
|
|
205
199
|
|
|
206
|
-
it
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
200
|
+
it 'ignores blank string values' do
|
|
201
|
+
sale = Sale.create!(
|
|
202
|
+
stage: 'qualified'
|
|
203
|
+
)
|
|
210
204
|
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
}
|
|
205
|
+
config = {
|
|
206
|
+
stage: {
|
|
207
|
+
type: :single,
|
|
208
|
+
filter_type: :string,
|
|
209
|
+
dbcolumn: :stage
|
|
217
210
|
}
|
|
211
|
+
}
|
|
218
212
|
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
213
|
+
result = described_class.apply(
|
|
214
|
+
scope: Sale.all,
|
|
215
|
+
config: config,
|
|
216
|
+
params: {
|
|
217
|
+
stage: ''
|
|
218
|
+
}
|
|
219
|
+
)
|
|
226
220
|
|
|
227
|
-
|
|
221
|
+
expect(result).to contain_exactly(sale)
|
|
228
222
|
end
|
|
229
223
|
|
|
230
|
-
it
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
sale_two = Sale.create!(
|
|
236
|
-
approval_status: "pending"
|
|
237
|
-
)
|
|
238
|
-
|
|
239
|
-
Sale.create!(
|
|
240
|
-
approval_status: "rejected"
|
|
241
|
-
)
|
|
224
|
+
it 'ignores a nil filter value' do
|
|
225
|
+
sale = Sale.create!(
|
|
226
|
+
stage: 'qualified'
|
|
227
|
+
)
|
|
242
228
|
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
}
|
|
229
|
+
config = {
|
|
230
|
+
stage: {
|
|
231
|
+
type: :single,
|
|
232
|
+
filter_type: :string,
|
|
233
|
+
dbcolumn: :stage
|
|
249
234
|
}
|
|
235
|
+
}
|
|
250
236
|
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
237
|
+
result = described_class.apply(
|
|
238
|
+
scope: Sale.all,
|
|
239
|
+
config: config,
|
|
240
|
+
params: {
|
|
241
|
+
stage: nil
|
|
242
|
+
}
|
|
243
|
+
)
|
|
258
244
|
|
|
259
|
-
|
|
260
|
-
sale_one,
|
|
261
|
-
sale_two
|
|
262
|
-
)
|
|
245
|
+
expect(result).to contain_exactly(sale)
|
|
263
246
|
end
|
|
264
247
|
|
|
265
|
-
it
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
sale_two = Sale.create!(
|
|
271
|
-
approval_status: "pending"
|
|
272
|
-
)
|
|
248
|
+
it 'ignores an empty array filter value' do
|
|
249
|
+
sale = Sale.create!(
|
|
250
|
+
approval_status: 'approved'
|
|
251
|
+
)
|
|
273
252
|
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
}
|
|
253
|
+
config = {
|
|
254
|
+
status: {
|
|
255
|
+
type: :single,
|
|
256
|
+
filter_type: :array,
|
|
257
|
+
dbcolumn: :approval_status
|
|
280
258
|
}
|
|
259
|
+
}
|
|
281
260
|
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
261
|
+
result = described_class.apply(
|
|
262
|
+
scope: Sale.all,
|
|
263
|
+
config: config,
|
|
264
|
+
params: {
|
|
265
|
+
status: []
|
|
266
|
+
}
|
|
267
|
+
)
|
|
289
268
|
|
|
290
|
-
|
|
291
|
-
sale_one,
|
|
292
|
-
sale_two
|
|
293
|
-
)
|
|
269
|
+
expect(result).to contain_exactly(sale)
|
|
294
270
|
end
|
|
295
271
|
|
|
296
|
-
it
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
Sale.create!(
|
|
302
|
-
active: false
|
|
303
|
-
)
|
|
272
|
+
it 'returns the original scope unchanged for an empty config' do
|
|
273
|
+
sale = Sale.create!(
|
|
274
|
+
stage: 'qualified'
|
|
275
|
+
)
|
|
304
276
|
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
}
|
|
277
|
+
result = described_class.apply(
|
|
278
|
+
scope: Sale.all,
|
|
279
|
+
config: {},
|
|
280
|
+
params: {
|
|
281
|
+
stage: 'qualified'
|
|
311
282
|
}
|
|
283
|
+
)
|
|
312
284
|
|
|
313
|
-
|
|
314
|
-
scope: Sale.all,
|
|
315
|
-
config: config,
|
|
316
|
-
params: {
|
|
317
|
-
active: "true"
|
|
318
|
-
}
|
|
319
|
-
)
|
|
320
|
-
|
|
321
|
-
expect(result).to contain_exactly(sale)
|
|
285
|
+
expect(result).to contain_exactly(sale)
|
|
322
286
|
end
|
|
323
287
|
|
|
324
|
-
it
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
288
|
+
it 'supports multiple array values' do
|
|
289
|
+
sale_one = Sale.create!(
|
|
290
|
+
approval_status: 'approved'
|
|
291
|
+
)
|
|
328
292
|
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
293
|
+
sale_two = Sale.create!(
|
|
294
|
+
approval_status: 'pending'
|
|
295
|
+
)
|
|
332
296
|
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
297
|
+
Sale.create!(
|
|
298
|
+
approval_status: 'rejected'
|
|
299
|
+
)
|
|
300
|
+
|
|
301
|
+
config = {
|
|
302
|
+
status: {
|
|
303
|
+
type: :single,
|
|
304
|
+
filter_type: :array,
|
|
305
|
+
dbcolumn: :approval_status
|
|
339
306
|
}
|
|
307
|
+
}
|
|
340
308
|
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
309
|
+
result = described_class.apply(
|
|
310
|
+
scope: Sale.all,
|
|
311
|
+
config: config,
|
|
312
|
+
params: {
|
|
313
|
+
status: %w[approved pending]
|
|
314
|
+
}
|
|
315
|
+
)
|
|
348
316
|
|
|
349
|
-
|
|
317
|
+
expect(result).to contain_exactly(
|
|
318
|
+
sale_one,
|
|
319
|
+
sale_two
|
|
320
|
+
)
|
|
350
321
|
end
|
|
351
322
|
|
|
352
|
-
it
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
323
|
+
it 'normalizes comma separated values into an array' do
|
|
324
|
+
sale_one = Sale.create!(
|
|
325
|
+
approval_status: 'approved'
|
|
326
|
+
)
|
|
356
327
|
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
328
|
+
sale_two = Sale.create!(
|
|
329
|
+
approval_status: 'pending'
|
|
330
|
+
)
|
|
360
331
|
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
}
|
|
332
|
+
config = {
|
|
333
|
+
status: {
|
|
334
|
+
type: :single,
|
|
335
|
+
filter_type: :array,
|
|
336
|
+
dbcolumn: :approval_status
|
|
367
337
|
}
|
|
338
|
+
}
|
|
368
339
|
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
340
|
+
result = described_class.apply(
|
|
341
|
+
scope: Sale.all,
|
|
342
|
+
config: config,
|
|
343
|
+
params: {
|
|
344
|
+
status: 'approved,pending'
|
|
345
|
+
}
|
|
346
|
+
)
|
|
376
347
|
|
|
377
|
-
|
|
348
|
+
expect(result).to contain_exactly(
|
|
349
|
+
sale_one,
|
|
350
|
+
sale_two
|
|
351
|
+
)
|
|
378
352
|
end
|
|
379
353
|
|
|
380
|
-
it
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
sale_two = Sale.create!(
|
|
386
|
-
amount: 1000
|
|
387
|
-
)
|
|
354
|
+
it 'filters boolean true values' do
|
|
355
|
+
sale = Sale.create!(
|
|
356
|
+
active: true
|
|
357
|
+
)
|
|
388
358
|
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
359
|
+
Sale.create!(
|
|
360
|
+
active: false
|
|
361
|
+
)
|
|
392
362
|
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
range_type: :gte
|
|
399
|
-
}
|
|
363
|
+
config = {
|
|
364
|
+
active: {
|
|
365
|
+
type: :single,
|
|
366
|
+
filter_type: :boolean,
|
|
367
|
+
dbcolumn: :active
|
|
400
368
|
}
|
|
369
|
+
}
|
|
401
370
|
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
371
|
+
result = described_class.apply(
|
|
372
|
+
scope: Sale.all,
|
|
373
|
+
config: config,
|
|
374
|
+
params: {
|
|
375
|
+
active: 'true'
|
|
376
|
+
}
|
|
377
|
+
)
|
|
409
378
|
|
|
410
|
-
|
|
411
|
-
sale_one,
|
|
412
|
-
sale_two
|
|
413
|
-
)
|
|
379
|
+
expect(result).to contain_exactly(sale)
|
|
414
380
|
end
|
|
415
381
|
|
|
416
|
-
it
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
sale_two = Sale.create!(
|
|
422
|
-
amount: 200
|
|
423
|
-
)
|
|
382
|
+
it 'filters boolean false values' do
|
|
383
|
+
Sale.create!(
|
|
384
|
+
active: true
|
|
385
|
+
)
|
|
424
386
|
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
387
|
+
sale = Sale.create!(
|
|
388
|
+
active: false
|
|
389
|
+
)
|
|
428
390
|
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
range_type: :lte
|
|
435
|
-
}
|
|
391
|
+
config = {
|
|
392
|
+
active: {
|
|
393
|
+
type: :single,
|
|
394
|
+
filter_type: :boolean,
|
|
395
|
+
dbcolumn: :active
|
|
436
396
|
}
|
|
397
|
+
}
|
|
437
398
|
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
399
|
+
result = described_class.apply(
|
|
400
|
+
scope: Sale.all,
|
|
401
|
+
config: config,
|
|
402
|
+
params: {
|
|
403
|
+
active: false
|
|
404
|
+
}
|
|
405
|
+
)
|
|
445
406
|
|
|
446
|
-
|
|
447
|
-
sale_one,
|
|
448
|
-
sale_two
|
|
449
|
-
)
|
|
407
|
+
expect(result).to contain_exactly(sale)
|
|
450
408
|
end
|
|
451
409
|
|
|
452
|
-
it
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
410
|
+
it 'filters exact integer values' do
|
|
411
|
+
sale = Sale.create!(
|
|
412
|
+
amount: 500
|
|
413
|
+
)
|
|
456
414
|
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
415
|
+
Sale.create!(
|
|
416
|
+
amount: 1000
|
|
417
|
+
)
|
|
460
418
|
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
}
|
|
419
|
+
config = {
|
|
420
|
+
amount: {
|
|
421
|
+
type: :single,
|
|
422
|
+
filter_type: :integer,
|
|
423
|
+
dbcolumn: :amount
|
|
467
424
|
}
|
|
425
|
+
}
|
|
468
426
|
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
427
|
+
result = described_class.apply(
|
|
428
|
+
scope: Sale.all,
|
|
429
|
+
config: config,
|
|
430
|
+
params: {
|
|
431
|
+
amount: '500'
|
|
432
|
+
}
|
|
433
|
+
)
|
|
476
434
|
|
|
477
|
-
|
|
435
|
+
expect(result).to contain_exactly(sale)
|
|
478
436
|
end
|
|
479
437
|
|
|
480
|
-
it
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
438
|
+
it 'filters an exact integer value of zero' do
|
|
439
|
+
sale = Sale.create!(
|
|
440
|
+
amount: 0
|
|
441
|
+
)
|
|
484
442
|
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
443
|
+
Sale.create!(
|
|
444
|
+
amount: 500
|
|
445
|
+
)
|
|
488
446
|
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
}
|
|
447
|
+
config = {
|
|
448
|
+
amount: {
|
|
449
|
+
type: :single,
|
|
450
|
+
filter_type: :integer,
|
|
451
|
+
dbcolumn: :amount
|
|
495
452
|
}
|
|
453
|
+
}
|
|
496
454
|
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
455
|
+
result = described_class.apply(
|
|
456
|
+
scope: Sale.all,
|
|
457
|
+
config: config,
|
|
458
|
+
params: {
|
|
459
|
+
amount: 0
|
|
460
|
+
}
|
|
461
|
+
)
|
|
504
462
|
|
|
505
|
-
|
|
463
|
+
expect(result).to contain_exactly(sale)
|
|
506
464
|
end
|
|
507
465
|
|
|
508
|
-
it
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
466
|
+
it 'supports integer greater than or equal filter' do
|
|
467
|
+
sale_one = Sale.create!(
|
|
468
|
+
amount: 500
|
|
469
|
+
)
|
|
512
470
|
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
471
|
+
sale_two = Sale.create!(
|
|
472
|
+
amount: 1000
|
|
473
|
+
)
|
|
516
474
|
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
475
|
+
Sale.create!(
|
|
476
|
+
amount: 200
|
|
477
|
+
)
|
|
520
478
|
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
}
|
|
479
|
+
config = {
|
|
480
|
+
amount: {
|
|
481
|
+
type: :single,
|
|
482
|
+
filter_type: :integer,
|
|
483
|
+
dbcolumn: :amount,
|
|
484
|
+
range_type: :gte
|
|
528
485
|
}
|
|
486
|
+
}
|
|
529
487
|
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
488
|
+
result = described_class.apply(
|
|
489
|
+
scope: Sale.all,
|
|
490
|
+
config: config,
|
|
491
|
+
params: {
|
|
492
|
+
amount: 500
|
|
493
|
+
}
|
|
494
|
+
)
|
|
537
495
|
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
496
|
+
expect(result).to contain_exactly(
|
|
497
|
+
sale_one,
|
|
498
|
+
sale_two
|
|
499
|
+
)
|
|
542
500
|
end
|
|
543
501
|
|
|
544
|
-
it
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
502
|
+
it 'supports integer less than or equal filter' do
|
|
503
|
+
sale_one = Sale.create!(
|
|
504
|
+
amount: 500
|
|
505
|
+
)
|
|
548
506
|
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
507
|
+
sale_two = Sale.create!(
|
|
508
|
+
amount: 200
|
|
509
|
+
)
|
|
552
510
|
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
511
|
+
Sale.create!(
|
|
512
|
+
amount: 1000
|
|
513
|
+
)
|
|
514
|
+
|
|
515
|
+
config = {
|
|
516
|
+
amount: {
|
|
517
|
+
type: :single,
|
|
518
|
+
filter_type: :integer,
|
|
519
|
+
dbcolumn: :amount,
|
|
520
|
+
range_type: :lte
|
|
559
521
|
}
|
|
522
|
+
}
|
|
560
523
|
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
}
|
|
569
|
-
}
|
|
570
|
-
)
|
|
524
|
+
result = described_class.apply(
|
|
525
|
+
scope: Sale.all,
|
|
526
|
+
config: config,
|
|
527
|
+
params: {
|
|
528
|
+
amount: 500
|
|
529
|
+
}
|
|
530
|
+
)
|
|
571
531
|
|
|
572
|
-
|
|
532
|
+
expect(result).to contain_exactly(
|
|
533
|
+
sale_one,
|
|
534
|
+
sale_two
|
|
535
|
+
)
|
|
573
536
|
end
|
|
574
537
|
|
|
575
|
-
it
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
)
|
|
538
|
+
it 'supports integer between filter with a hash' do
|
|
539
|
+
sale_one = Sale.create!(amount: 500)
|
|
540
|
+
sale_two = Sale.create!(amount: 800)
|
|
579
541
|
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
)
|
|
542
|
+
Sale.create!(amount: 100)
|
|
543
|
+
Sale.create!(amount: 1500)
|
|
583
544
|
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
545
|
+
config = {
|
|
546
|
+
amount: {
|
|
547
|
+
type: :single,
|
|
548
|
+
filter_type: :integer,
|
|
549
|
+
dbcolumn: :amount,
|
|
550
|
+
range_type: :between
|
|
590
551
|
}
|
|
552
|
+
}
|
|
591
553
|
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
}
|
|
600
|
-
)
|
|
554
|
+
result = described_class.apply(
|
|
555
|
+
scope: Sale.all,
|
|
556
|
+
config: config,
|
|
557
|
+
params: {
|
|
558
|
+
amount: { from: 300, to: 1000 }
|
|
559
|
+
}
|
|
560
|
+
)
|
|
601
561
|
|
|
602
|
-
|
|
562
|
+
expect(result).to contain_exactly(
|
|
563
|
+
sale_one,
|
|
564
|
+
sale_two
|
|
565
|
+
)
|
|
603
566
|
end
|
|
604
567
|
|
|
605
|
-
it
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
568
|
+
it 'supports integer between filter with an array' do
|
|
569
|
+
sale_one = Sale.create!(amount: 500)
|
|
570
|
+
sale_two = Sale.create!(amount: 800)
|
|
571
|
+
|
|
572
|
+
Sale.create!(amount: 100)
|
|
573
|
+
|
|
574
|
+
config = {
|
|
575
|
+
amount: {
|
|
576
|
+
type: :single,
|
|
577
|
+
filter_type: :integer,
|
|
578
|
+
dbcolumn: :amount,
|
|
579
|
+
range_type: :between
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
result = described_class.apply(
|
|
584
|
+
scope: Sale.all,
|
|
585
|
+
config: config,
|
|
586
|
+
params: {
|
|
587
|
+
amount: [300, 1000]
|
|
588
|
+
}
|
|
589
|
+
)
|
|
590
|
+
|
|
591
|
+
expect(result).to contain_exactly(
|
|
592
|
+
sale_one,
|
|
593
|
+
sale_two
|
|
594
|
+
)
|
|
595
|
+
end
|
|
596
|
+
|
|
597
|
+
it 'supports integer between filter with only a lower bound' do
|
|
598
|
+
sale_one = Sale.create!(amount: 500)
|
|
599
|
+
sale_two = Sale.create!(amount: 800)
|
|
600
|
+
|
|
601
|
+
Sale.create!(amount: 100)
|
|
602
|
+
|
|
603
|
+
config = {
|
|
604
|
+
amount: {
|
|
605
|
+
type: :single,
|
|
606
|
+
filter_type: :integer,
|
|
607
|
+
dbcolumn: :amount,
|
|
608
|
+
range_type: :between
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
result = described_class.apply(
|
|
613
|
+
scope: Sale.all,
|
|
614
|
+
config: config,
|
|
615
|
+
params: {
|
|
616
|
+
amount: { from: 300 }
|
|
617
|
+
}
|
|
618
|
+
)
|
|
619
|
+
|
|
620
|
+
expect(result).to contain_exactly(
|
|
621
|
+
sale_one,
|
|
622
|
+
sale_two
|
|
623
|
+
)
|
|
624
|
+
end
|
|
625
|
+
|
|
626
|
+
it 'supports float between filter' do
|
|
627
|
+
sale = Sale.create!(conversion_rate: 12.5)
|
|
628
|
+
|
|
629
|
+
Sale.create!(conversion_rate: 1.0)
|
|
630
|
+
Sale.create!(conversion_rate: 99.0)
|
|
631
|
+
|
|
632
|
+
config = {
|
|
633
|
+
conversion_rate: {
|
|
634
|
+
type: :single,
|
|
635
|
+
filter_type: :float,
|
|
636
|
+
dbcolumn: :conversion_rate,
|
|
637
|
+
range_type: :between
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
result = described_class.apply(
|
|
642
|
+
scope: Sale.all,
|
|
643
|
+
config: config,
|
|
644
|
+
params: {
|
|
645
|
+
conversion_rate: { from: 10.0, to: 15.0 }
|
|
646
|
+
}
|
|
647
|
+
)
|
|
648
|
+
|
|
649
|
+
expect(result).to contain_exactly(sale)
|
|
650
|
+
end
|
|
651
|
+
|
|
652
|
+
it 'supports float filters' do
|
|
653
|
+
sale = Sale.create!(
|
|
654
|
+
conversion_rate: 12.5
|
|
655
|
+
)
|
|
609
656
|
|
|
610
|
-
|
|
611
|
-
|
|
657
|
+
Sale.create!(
|
|
658
|
+
conversion_rate: 20.5
|
|
659
|
+
)
|
|
660
|
+
|
|
661
|
+
config = {
|
|
662
|
+
conversion_rate: {
|
|
663
|
+
type: :single,
|
|
664
|
+
filter_type: :float,
|
|
665
|
+
dbcolumn: :conversion_rate
|
|
666
|
+
}
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
result = described_class.apply(
|
|
670
|
+
scope: Sale.all,
|
|
671
|
+
config: config,
|
|
672
|
+
params: {
|
|
673
|
+
conversion_rate: '12.5'
|
|
674
|
+
}
|
|
675
|
+
)
|
|
676
|
+
|
|
677
|
+
expect(result).to contain_exactly(sale)
|
|
678
|
+
end
|
|
679
|
+
|
|
680
|
+
it 'supports float greater than or equal filter' do
|
|
681
|
+
sale_one = Sale.create!(
|
|
682
|
+
conversion_rate: 10.5
|
|
683
|
+
)
|
|
684
|
+
|
|
685
|
+
sale_two = Sale.create!(
|
|
686
|
+
conversion_rate: 20.5
|
|
687
|
+
)
|
|
688
|
+
|
|
689
|
+
Sale.create!(
|
|
690
|
+
conversion_rate: 5.5
|
|
691
|
+
)
|
|
692
|
+
|
|
693
|
+
config = {
|
|
694
|
+
conversion_rate: {
|
|
695
|
+
type: :single,
|
|
696
|
+
filter_type: :float,
|
|
697
|
+
dbcolumn: :conversion_rate,
|
|
698
|
+
range_type: :gte
|
|
699
|
+
}
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
result = described_class.apply(
|
|
703
|
+
scope: Sale.all,
|
|
704
|
+
config: config,
|
|
705
|
+
params: {
|
|
706
|
+
conversion_rate: 10.5
|
|
707
|
+
}
|
|
708
|
+
)
|
|
709
|
+
|
|
710
|
+
expect(result).to contain_exactly(
|
|
711
|
+
sale_one,
|
|
712
|
+
sale_two
|
|
713
|
+
)
|
|
714
|
+
end
|
|
715
|
+
|
|
716
|
+
it 'filters between two dates' do
|
|
717
|
+
sale = Sale.create!(
|
|
718
|
+
actual_sale_date: Time.utc(2026, 7, 20)
|
|
719
|
+
)
|
|
720
|
+
|
|
721
|
+
Sale.create!(
|
|
722
|
+
actual_sale_date: Time.utc(2026, 8, 20)
|
|
723
|
+
)
|
|
724
|
+
|
|
725
|
+
config = {
|
|
726
|
+
actual_sale_date: {
|
|
727
|
+
type: :single,
|
|
728
|
+
filter_type: :daterange,
|
|
729
|
+
dbcolumn: :actual_sale_date
|
|
730
|
+
}
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
result = described_class.apply(
|
|
734
|
+
scope: Sale.all,
|
|
735
|
+
config: config,
|
|
736
|
+
params: {
|
|
737
|
+
actual_sale_date: {
|
|
738
|
+
from: '2026-07-01',
|
|
739
|
+
to: '2026-07-31'
|
|
740
|
+
}
|
|
741
|
+
}
|
|
742
|
+
)
|
|
743
|
+
|
|
744
|
+
expect(result).to contain_exactly(sale)
|
|
745
|
+
end
|
|
746
|
+
|
|
747
|
+
it 'supports date range with only from date' do
|
|
748
|
+
sale = Sale.create!(
|
|
749
|
+
actual_sale_date: Time.utc(2026, 8, 10)
|
|
750
|
+
)
|
|
751
|
+
|
|
752
|
+
Sale.create!(
|
|
753
|
+
actual_sale_date: Time.utc(2026, 7, 10)
|
|
754
|
+
)
|
|
755
|
+
|
|
756
|
+
config = {
|
|
757
|
+
actual_sale_date: {
|
|
758
|
+
type: :single,
|
|
759
|
+
filter_type: :daterange,
|
|
760
|
+
dbcolumn: :actual_sale_date
|
|
761
|
+
}
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
result = described_class.apply(
|
|
765
|
+
scope: Sale.all,
|
|
766
|
+
config: config,
|
|
767
|
+
params: {
|
|
768
|
+
actual_sale_date: {
|
|
769
|
+
from: '2026-08-01'
|
|
770
|
+
}
|
|
771
|
+
}
|
|
772
|
+
)
|
|
773
|
+
|
|
774
|
+
expect(result).to contain_exactly(sale)
|
|
775
|
+
end
|
|
776
|
+
|
|
777
|
+
it 'supports date range with only to date' do
|
|
778
|
+
sale = Sale.create!(
|
|
779
|
+
actual_sale_date: Time.utc(2026, 7, 10)
|
|
780
|
+
)
|
|
781
|
+
|
|
782
|
+
Sale.create!(
|
|
783
|
+
actual_sale_date: Time.utc(2026, 8, 10)
|
|
784
|
+
)
|
|
785
|
+
|
|
786
|
+
config = {
|
|
787
|
+
actual_sale_date: {
|
|
788
|
+
type: :single,
|
|
789
|
+
filter_type: :daterange,
|
|
790
|
+
dbcolumn: :actual_sale_date
|
|
791
|
+
}
|
|
792
|
+
}
|
|
793
|
+
|
|
794
|
+
result = described_class.apply(
|
|
795
|
+
scope: Sale.all,
|
|
796
|
+
config: config,
|
|
797
|
+
params: {
|
|
798
|
+
actual_sale_date: {
|
|
799
|
+
to: '2026-07-31'
|
|
800
|
+
}
|
|
801
|
+
}
|
|
802
|
+
)
|
|
803
|
+
|
|
804
|
+
expect(result).to contain_exactly(sale)
|
|
805
|
+
end
|
|
806
|
+
|
|
807
|
+
it 'supports date range array format' do
|
|
808
|
+
sale = Sale.create!(
|
|
809
|
+
actual_sale_date: Time.utc(2026, 7, 20)
|
|
810
|
+
)
|
|
811
|
+
|
|
812
|
+
config = {
|
|
813
|
+
actual_sale_date: {
|
|
814
|
+
type: :single,
|
|
815
|
+
filter_type: :daterange,
|
|
816
|
+
dbcolumn: :actual_sale_date
|
|
817
|
+
}
|
|
818
|
+
}
|
|
819
|
+
|
|
820
|
+
result = described_class.apply(
|
|
821
|
+
scope: Sale.all,
|
|
822
|
+
config: config,
|
|
823
|
+
params: {
|
|
824
|
+
actual_sale_date: %w[
|
|
825
|
+
2026-07-01
|
|
826
|
+
2026-07-31
|
|
827
|
+
]
|
|
828
|
+
}
|
|
829
|
+
)
|
|
830
|
+
|
|
831
|
+
expect(result).to contain_exactly(sale)
|
|
832
|
+
end
|
|
833
|
+
|
|
834
|
+
it 'supports nested string filter' do
|
|
835
|
+
client = Client.create!(
|
|
836
|
+
name: 'John'
|
|
837
|
+
)
|
|
838
|
+
|
|
839
|
+
lead = Lead.create!(
|
|
840
|
+
source: 'google',
|
|
841
|
+
client: client
|
|
842
|
+
)
|
|
843
|
+
|
|
844
|
+
sale = Sale.create!(
|
|
845
|
+
lead: lead
|
|
846
|
+
)
|
|
847
|
+
|
|
848
|
+
config = {
|
|
849
|
+
lead_source: {
|
|
850
|
+
type: :nested,
|
|
851
|
+
filter_type: :string,
|
|
852
|
+
association: :lead,
|
|
853
|
+
dbcolumn: :source
|
|
854
|
+
}
|
|
855
|
+
}
|
|
856
|
+
|
|
857
|
+
result = described_class.apply(
|
|
858
|
+
scope: Sale.all,
|
|
859
|
+
config: config,
|
|
860
|
+
params: {
|
|
861
|
+
lead_source: 'google'
|
|
862
|
+
}
|
|
863
|
+
)
|
|
864
|
+
|
|
865
|
+
expect(result).to contain_exactly(sale)
|
|
866
|
+
end
|
|
867
|
+
|
|
868
|
+
it 'supports nested array filter' do
|
|
869
|
+
client = Client.create!(
|
|
870
|
+
name: 'John'
|
|
871
|
+
)
|
|
872
|
+
|
|
873
|
+
lead_one = Lead.create!(
|
|
874
|
+
source: 'google',
|
|
875
|
+
client: client
|
|
876
|
+
)
|
|
877
|
+
|
|
878
|
+
lead_two = Lead.create!(
|
|
879
|
+
source: 'referral',
|
|
880
|
+
client: client
|
|
881
|
+
)
|
|
882
|
+
|
|
883
|
+
sale_one = Sale.create!(lead: lead_one)
|
|
884
|
+
sale_two = Sale.create!(lead: lead_two)
|
|
885
|
+
|
|
886
|
+
config = {
|
|
887
|
+
lead_source: {
|
|
888
|
+
type: :nested,
|
|
889
|
+
filter_type: :array,
|
|
890
|
+
association: :lead,
|
|
891
|
+
dbcolumn: :source
|
|
892
|
+
}
|
|
893
|
+
}
|
|
894
|
+
|
|
895
|
+
result = described_class.apply(
|
|
896
|
+
scope: Sale.all,
|
|
897
|
+
config: config,
|
|
898
|
+
params: {
|
|
899
|
+
lead_source: %w[google referral]
|
|
900
|
+
}
|
|
901
|
+
)
|
|
902
|
+
|
|
903
|
+
expect(result).to contain_exactly(
|
|
904
|
+
sale_one,
|
|
905
|
+
sale_two
|
|
906
|
+
)
|
|
907
|
+
end
|
|
908
|
+
|
|
909
|
+
it "raises InvalidAssociationError when a nested rule's association cannot be resolved" do
|
|
910
|
+
config = {
|
|
911
|
+
lead_source: {
|
|
912
|
+
type: :nested,
|
|
913
|
+
filter_type: :string,
|
|
914
|
+
association: :not_a_real_association,
|
|
915
|
+
dbcolumn: :source
|
|
916
|
+
}
|
|
917
|
+
}
|
|
918
|
+
|
|
919
|
+
expect do
|
|
920
|
+
described_class.apply(
|
|
921
|
+
scope: Sale.all,
|
|
922
|
+
config: config,
|
|
923
|
+
params: { lead_source: 'google' }
|
|
612
924
|
)
|
|
925
|
+
end.to raise_error(BharatFilterEngine::InvalidAssociationError)
|
|
926
|
+
end
|
|
927
|
+
|
|
928
|
+
it 'applies multiple filters together' do
|
|
929
|
+
sale_one = Sale.create!(
|
|
930
|
+
stage: 'qualified',
|
|
931
|
+
approval_status: 'approved',
|
|
932
|
+
amount: 500
|
|
933
|
+
)
|
|
934
|
+
|
|
935
|
+
Sale.create!(
|
|
936
|
+
stage: 'qualified',
|
|
937
|
+
approval_status: 'pending',
|
|
938
|
+
amount: 500
|
|
939
|
+
)
|
|
940
|
+
|
|
941
|
+
Sale.create!(
|
|
942
|
+
stage: 'new',
|
|
943
|
+
approval_status: 'approved',
|
|
944
|
+
amount: 500
|
|
945
|
+
)
|
|
946
|
+
|
|
947
|
+
config = {
|
|
948
|
+
stage: {
|
|
949
|
+
type: :single,
|
|
950
|
+
filter_type: :string,
|
|
951
|
+
dbcolumn: :stage
|
|
952
|
+
},
|
|
953
|
+
|
|
954
|
+
status: {
|
|
955
|
+
type: :single,
|
|
956
|
+
filter_type: :array,
|
|
957
|
+
dbcolumn: :approval_status
|
|
958
|
+
}
|
|
959
|
+
}
|
|
960
|
+
|
|
961
|
+
result = described_class.apply(
|
|
962
|
+
scope: Sale.all,
|
|
963
|
+
config: config,
|
|
964
|
+
params: {
|
|
965
|
+
stage: 'qualified',
|
|
966
|
+
status: ['approved']
|
|
967
|
+
}
|
|
968
|
+
)
|
|
969
|
+
|
|
970
|
+
expect(result).to contain_exactly(sale_one)
|
|
971
|
+
end
|
|
972
|
+
|
|
973
|
+
it 'preserves existing scope conditions' do
|
|
974
|
+
sale_one = Sale.create!(
|
|
975
|
+
stage: 'qualified',
|
|
976
|
+
active: true
|
|
977
|
+
)
|
|
978
|
+
|
|
979
|
+
Sale.create!(
|
|
980
|
+
stage: 'qualified',
|
|
981
|
+
active: false
|
|
982
|
+
)
|
|
983
|
+
|
|
984
|
+
base_scope =
|
|
985
|
+
Sale.where(active: true)
|
|
986
|
+
|
|
987
|
+
config = {
|
|
988
|
+
stage: {
|
|
989
|
+
type: :single,
|
|
990
|
+
filter_type: :string,
|
|
991
|
+
dbcolumn: :stage
|
|
992
|
+
}
|
|
993
|
+
}
|
|
994
|
+
|
|
995
|
+
result = described_class.apply(
|
|
996
|
+
scope: base_scope,
|
|
997
|
+
config: config,
|
|
998
|
+
params: {
|
|
999
|
+
stage: 'qualified'
|
|
1000
|
+
}
|
|
1001
|
+
)
|
|
1002
|
+
|
|
1003
|
+
expect(result).to contain_exactly(
|
|
1004
|
+
sale_one
|
|
1005
|
+
)
|
|
1006
|
+
end
|
|
1007
|
+
|
|
1008
|
+
it 'returns the original scope when no filters are provided' do
|
|
1009
|
+
sale = Sale.create!(
|
|
1010
|
+
stage: 'qualified'
|
|
1011
|
+
)
|
|
1012
|
+
|
|
1013
|
+
config = {
|
|
1014
|
+
stage: {
|
|
1015
|
+
type: :single,
|
|
1016
|
+
filter_type: :string,
|
|
1017
|
+
dbcolumn: :stage
|
|
1018
|
+
}
|
|
1019
|
+
}
|
|
1020
|
+
|
|
1021
|
+
result = described_class.apply(
|
|
1022
|
+
scope: Sale.all,
|
|
1023
|
+
config: config,
|
|
1024
|
+
params: {}
|
|
1025
|
+
)
|
|
1026
|
+
|
|
1027
|
+
expect(result).to contain_exactly(sale)
|
|
1028
|
+
end
|
|
1029
|
+
|
|
1030
|
+
it 'ignores unknown params that are not present in config' do
|
|
1031
|
+
sale = Sale.create!(
|
|
1032
|
+
stage: 'qualified'
|
|
1033
|
+
)
|
|
1034
|
+
|
|
1035
|
+
config = {
|
|
1036
|
+
stage: {
|
|
1037
|
+
type: :single,
|
|
1038
|
+
filter_type: :string,
|
|
1039
|
+
dbcolumn: :stage
|
|
1040
|
+
}
|
|
1041
|
+
}
|
|
1042
|
+
|
|
1043
|
+
result = described_class.apply(
|
|
1044
|
+
scope: Sale.all,
|
|
1045
|
+
config: config,
|
|
1046
|
+
params: {
|
|
1047
|
+
unknown_field: 'something'
|
|
1048
|
+
}
|
|
1049
|
+
)
|
|
1050
|
+
|
|
1051
|
+
expect(result).to contain_exactly(sale)
|
|
1052
|
+
end
|
|
1053
|
+
|
|
1054
|
+
it 'parses JSON array values' do
|
|
1055
|
+
sale_one = Sale.create!(
|
|
1056
|
+
approval_status: 'approved'
|
|
1057
|
+
)
|
|
1058
|
+
|
|
1059
|
+
sale_two = Sale.create!(
|
|
1060
|
+
approval_status: 'pending'
|
|
1061
|
+
)
|
|
1062
|
+
|
|
1063
|
+
config = {
|
|
1064
|
+
status: {
|
|
1065
|
+
type: :single,
|
|
1066
|
+
filter_type: :array,
|
|
1067
|
+
dbcolumn: :approval_status
|
|
1068
|
+
}
|
|
1069
|
+
}
|
|
1070
|
+
|
|
1071
|
+
result = described_class.apply(
|
|
1072
|
+
scope: Sale.all,
|
|
1073
|
+
config: config,
|
|
1074
|
+
params: {
|
|
1075
|
+
status: '["approved","pending"]'
|
|
1076
|
+
}
|
|
1077
|
+
)
|
|
1078
|
+
|
|
1079
|
+
expect(result).to contain_exactly(
|
|
1080
|
+
sale_one,
|
|
1081
|
+
sale_two
|
|
1082
|
+
)
|
|
1083
|
+
end
|
|
1084
|
+
|
|
1085
|
+
it 'supports a config-driven search filter' do
|
|
1086
|
+
client = Client.create!(name: 'John')
|
|
1087
|
+
lead = Lead.create!(source: 'google', client: client)
|
|
1088
|
+
sale = Sale.create!(stage: 'qualified', lead: lead)
|
|
1089
|
+
|
|
1090
|
+
Sale.create!(stage: 'new')
|
|
1091
|
+
|
|
1092
|
+
config = {
|
|
1093
|
+
q: {
|
|
1094
|
+
type: :single,
|
|
1095
|
+
filter_type: :search,
|
|
1096
|
+
dbcolumns: %i[stage lead__source]
|
|
1097
|
+
}
|
|
1098
|
+
}
|
|
1099
|
+
|
|
1100
|
+
result = described_class.apply(
|
|
1101
|
+
scope: Sale.all,
|
|
1102
|
+
config: config,
|
|
1103
|
+
params: { q: 'google' }
|
|
1104
|
+
)
|
|
1105
|
+
|
|
1106
|
+
expect(result).to contain_exactly(sale)
|
|
1107
|
+
end
|
|
1108
|
+
|
|
1109
|
+
describe 'negate' do
|
|
1110
|
+
it 'excludes matching string values' do
|
|
1111
|
+
sale = Sale.create!(stage: 'new')
|
|
1112
|
+
Sale.create!(stage: 'qualified')
|
|
613
1113
|
|
|
614
1114
|
config = {
|
|
615
|
-
|
|
1115
|
+
stage: {
|
|
616
1116
|
type: :single,
|
|
617
|
-
filter_type: :
|
|
618
|
-
dbcolumn: :
|
|
619
|
-
|
|
1117
|
+
filter_type: :string,
|
|
1118
|
+
dbcolumn: :stage,
|
|
1119
|
+
negate: true
|
|
1120
|
+
}
|
|
620
1121
|
}
|
|
621
1122
|
|
|
622
1123
|
result = described_class.apply(
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
actual_sale_date: {
|
|
627
|
-
to: "2026-07-31"
|
|
628
|
-
}
|
|
629
|
-
}
|
|
1124
|
+
scope: Sale.all,
|
|
1125
|
+
config: config,
|
|
1126
|
+
params: { stage: 'qualified' }
|
|
630
1127
|
)
|
|
631
1128
|
|
|
632
1129
|
expect(result).to contain_exactly(sale)
|
|
633
|
-
|
|
1130
|
+
end
|
|
634
1131
|
|
|
635
|
-
|
|
636
|
-
sale = Sale.create!(
|
|
637
|
-
|
|
638
|
-
)
|
|
1132
|
+
it 'excludes matching array values' do
|
|
1133
|
+
sale = Sale.create!(approval_status: 'rejected')
|
|
1134
|
+
Sale.create!(approval_status: 'approved')
|
|
1135
|
+
Sale.create!(approval_status: 'pending')
|
|
639
1136
|
|
|
640
1137
|
config = {
|
|
641
|
-
|
|
1138
|
+
status: {
|
|
642
1139
|
type: :single,
|
|
643
|
-
filter_type: :
|
|
644
|
-
dbcolumn: :
|
|
645
|
-
|
|
1140
|
+
filter_type: :array,
|
|
1141
|
+
dbcolumn: :approval_status,
|
|
1142
|
+
negate: true
|
|
1143
|
+
}
|
|
646
1144
|
}
|
|
647
1145
|
|
|
648
1146
|
result = described_class.apply(
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
actual_sale_date: [
|
|
653
|
-
"2026-07-01",
|
|
654
|
-
"2026-07-31"
|
|
655
|
-
]
|
|
656
|
-
}
|
|
1147
|
+
scope: Sale.all,
|
|
1148
|
+
config: config,
|
|
1149
|
+
params: { status: %w[approved pending] }
|
|
657
1150
|
)
|
|
658
1151
|
|
|
659
1152
|
expect(result).to contain_exactly(sale)
|
|
660
|
-
|
|
1153
|
+
end
|
|
661
1154
|
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
1155
|
+
it 'excludes matching boolean values' do
|
|
1156
|
+
sale = Sale.create!(active: false)
|
|
1157
|
+
Sale.create!(active: true)
|
|
1158
|
+
|
|
1159
|
+
config = {
|
|
1160
|
+
active: {
|
|
1161
|
+
type: :single,
|
|
1162
|
+
filter_type: :boolean,
|
|
1163
|
+
dbcolumn: :active,
|
|
1164
|
+
negate: true
|
|
1165
|
+
}
|
|
1166
|
+
}
|
|
666
1167
|
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
1168
|
+
result = described_class.apply(
|
|
1169
|
+
scope: Sale.all,
|
|
1170
|
+
config: config,
|
|
1171
|
+
params: { active: 'true' }
|
|
670
1172
|
)
|
|
671
1173
|
|
|
672
|
-
|
|
673
|
-
|
|
1174
|
+
expect(result).to contain_exactly(sale)
|
|
1175
|
+
end
|
|
1176
|
+
|
|
1177
|
+
it 'excludes an exact integer match' do
|
|
1178
|
+
sale = Sale.create!(amount: 1000)
|
|
1179
|
+
Sale.create!(amount: 500)
|
|
1180
|
+
|
|
1181
|
+
config = {
|
|
1182
|
+
amount: {
|
|
1183
|
+
type: :single,
|
|
1184
|
+
filter_type: :integer,
|
|
1185
|
+
dbcolumn: :amount,
|
|
1186
|
+
negate: true
|
|
1187
|
+
}
|
|
1188
|
+
}
|
|
1189
|
+
|
|
1190
|
+
result = described_class.apply(
|
|
1191
|
+
scope: Sale.all,
|
|
1192
|
+
config: config,
|
|
1193
|
+
params: { amount: 500 }
|
|
674
1194
|
)
|
|
675
1195
|
|
|
1196
|
+
expect(result).to contain_exactly(sale)
|
|
1197
|
+
end
|
|
1198
|
+
|
|
1199
|
+
it 'works on a nested (associated) column' do
|
|
1200
|
+
client = Client.create!(name: 'John')
|
|
1201
|
+
lead_google = Lead.create!(source: 'google', client: client)
|
|
1202
|
+
lead_referral = Lead.create!(source: 'referral', client: client)
|
|
1203
|
+
|
|
1204
|
+
sale = Sale.create!(lead: lead_referral)
|
|
1205
|
+
Sale.create!(lead: lead_google)
|
|
1206
|
+
|
|
676
1207
|
config = {
|
|
677
|
-
|
|
1208
|
+
lead_source: {
|
|
678
1209
|
type: :nested,
|
|
679
1210
|
filter_type: :string,
|
|
680
1211
|
association: :lead,
|
|
681
|
-
dbcolumn: :source
|
|
682
|
-
|
|
1212
|
+
dbcolumn: :source,
|
|
1213
|
+
negate: true
|
|
1214
|
+
}
|
|
683
1215
|
}
|
|
684
1216
|
|
|
685
1217
|
result = described_class.apply(
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
lead_source: "google"
|
|
690
|
-
}
|
|
1218
|
+
scope: Sale.all,
|
|
1219
|
+
config: config,
|
|
1220
|
+
params: { lead_source: 'google' }
|
|
691
1221
|
)
|
|
692
1222
|
|
|
693
1223
|
expect(result).to contain_exactly(sale)
|
|
694
|
-
|
|
1224
|
+
end
|
|
695
1225
|
|
|
696
|
-
|
|
697
|
-
client = Client.create!(
|
|
698
|
-
|
|
699
|
-
)
|
|
1226
|
+
it 'excludes matching array values on a nested (associated) column' do
|
|
1227
|
+
client = Client.create!(name: 'John')
|
|
1228
|
+
lead_google = Lead.create!(source: 'google', client: client)
|
|
1229
|
+
lead_referral = Lead.create!(source: 'referral', client: client)
|
|
1230
|
+
lead_direct = Lead.create!(source: 'direct', client: client)
|
|
700
1231
|
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
)
|
|
705
|
-
|
|
706
|
-
lead_two = Lead.create!(
|
|
707
|
-
source: "referral",
|
|
708
|
-
client: client
|
|
709
|
-
)
|
|
710
|
-
|
|
711
|
-
sale_one = Sale.create!(lead: lead_one)
|
|
712
|
-
sale_two = Sale.create!(lead: lead_two)
|
|
1232
|
+
sale = Sale.create!(lead: lead_direct)
|
|
1233
|
+
Sale.create!(lead: lead_google)
|
|
1234
|
+
Sale.create!(lead: lead_referral)
|
|
713
1235
|
|
|
714
1236
|
config = {
|
|
715
|
-
|
|
1237
|
+
lead_source: {
|
|
716
1238
|
type: :nested,
|
|
717
1239
|
filter_type: :array,
|
|
718
1240
|
association: :lead,
|
|
719
|
-
dbcolumn: :source
|
|
720
|
-
|
|
1241
|
+
dbcolumn: :source,
|
|
1242
|
+
negate: true
|
|
1243
|
+
}
|
|
721
1244
|
}
|
|
722
1245
|
|
|
723
1246
|
result = described_class.apply(
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
lead_source: ["google", "referral"]
|
|
728
|
-
}
|
|
729
|
-
)
|
|
730
|
-
|
|
731
|
-
expect(result).to contain_exactly(
|
|
732
|
-
sale_one,
|
|
733
|
-
sale_two
|
|
734
|
-
)
|
|
735
|
-
end
|
|
736
|
-
|
|
737
|
-
it "applies multiple filters together" do
|
|
738
|
-
sale_one = Sale.create!(
|
|
739
|
-
stage: "qualified",
|
|
740
|
-
approval_status: "approved",
|
|
741
|
-
amount: 500
|
|
1247
|
+
scope: Sale.all,
|
|
1248
|
+
config: config,
|
|
1249
|
+
params: { lead_source: %w[google referral] }
|
|
742
1250
|
)
|
|
743
1251
|
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
approval_status: "pending",
|
|
747
|
-
amount: 500
|
|
748
|
-
)
|
|
1252
|
+
expect(result).to contain_exactly(sale)
|
|
1253
|
+
end
|
|
749
1254
|
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
)
|
|
1255
|
+
it 'excludes an array value that itself contains nil' do
|
|
1256
|
+
sale = Sale.create!(approval_status: 'rejected')
|
|
1257
|
+
Sale.create!(approval_status: 'approved')
|
|
1258
|
+
Sale.create!(approval_status: nil)
|
|
755
1259
|
|
|
756
1260
|
config = {
|
|
757
|
-
|
|
758
|
-
type: :single,
|
|
759
|
-
filter_type: :string,
|
|
760
|
-
dbcolumn: :stage
|
|
761
|
-
},
|
|
762
|
-
|
|
763
|
-
status: {
|
|
1261
|
+
status: {
|
|
764
1262
|
type: :single,
|
|
765
1263
|
filter_type: :array,
|
|
766
|
-
dbcolumn: :approval_status
|
|
767
|
-
|
|
1264
|
+
dbcolumn: :approval_status,
|
|
1265
|
+
negate: true
|
|
1266
|
+
}
|
|
768
1267
|
}
|
|
769
1268
|
|
|
770
1269
|
result = described_class.apply(
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
stage: "qualified",
|
|
775
|
-
status: ["approved"]
|
|
776
|
-
}
|
|
1270
|
+
scope: Sale.all,
|
|
1271
|
+
config: config,
|
|
1272
|
+
params: { status: [nil, 'approved'] }
|
|
777
1273
|
)
|
|
778
1274
|
|
|
779
|
-
expect(result).to contain_exactly(
|
|
1275
|
+
expect(result).to contain_exactly(sale)
|
|
1276
|
+
end
|
|
780
1277
|
end
|
|
781
1278
|
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
)
|
|
1279
|
+
describe 'presence filter' do
|
|
1280
|
+
it 'returns records where the column is present when value is true' do
|
|
1281
|
+
sale = Sale.create!(stage: 'qualified')
|
|
1282
|
+
Sale.create!(stage: nil)
|
|
1283
|
+
Sale.create!(stage: '')
|
|
1284
|
+
|
|
1285
|
+
config = {
|
|
1286
|
+
has_stage: {
|
|
1287
|
+
type: :single,
|
|
1288
|
+
filter_type: :presence,
|
|
1289
|
+
dbcolumn: :stage
|
|
1290
|
+
}
|
|
1291
|
+
}
|
|
787
1292
|
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
1293
|
+
result = described_class.apply(
|
|
1294
|
+
scope: Sale.all,
|
|
1295
|
+
config: config,
|
|
1296
|
+
params: { has_stage: 'true' }
|
|
791
1297
|
)
|
|
792
1298
|
|
|
793
|
-
|
|
794
|
-
|
|
1299
|
+
expect(result).to contain_exactly(sale)
|
|
1300
|
+
end
|
|
1301
|
+
|
|
1302
|
+
it 'returns records where the column is blank when value is false' do
|
|
1303
|
+
Sale.create!(stage: 'qualified')
|
|
1304
|
+
sale_nil = Sale.create!(stage: nil)
|
|
1305
|
+
sale_blank = Sale.create!(stage: '')
|
|
795
1306
|
|
|
796
1307
|
config = {
|
|
797
|
-
|
|
1308
|
+
has_stage: {
|
|
798
1309
|
type: :single,
|
|
799
|
-
filter_type: :
|
|
1310
|
+
filter_type: :presence,
|
|
800
1311
|
dbcolumn: :stage
|
|
801
|
-
|
|
1312
|
+
}
|
|
802
1313
|
}
|
|
803
1314
|
|
|
804
1315
|
result = described_class.apply(
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
stage: "qualified"
|
|
809
|
-
}
|
|
1316
|
+
scope: Sale.all,
|
|
1317
|
+
config: config,
|
|
1318
|
+
params: { has_stage: 'false' }
|
|
810
1319
|
)
|
|
811
1320
|
|
|
812
1321
|
expect(result).to contain_exactly(
|
|
813
|
-
|
|
1322
|
+
sale_nil,
|
|
1323
|
+
sale_blank
|
|
814
1324
|
)
|
|
815
|
-
|
|
1325
|
+
end
|
|
816
1326
|
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
1327
|
+
it 'works on a nested (associated) column' do
|
|
1328
|
+
client_with_org = Client.create!(
|
|
1329
|
+
name: 'John',
|
|
1330
|
+
organization: Organization.create!(name: 'Acme')
|
|
820
1331
|
)
|
|
821
1332
|
|
|
1333
|
+
client_without_org = Client.create!(name: 'Jane')
|
|
1334
|
+
|
|
1335
|
+
lead_one = Lead.create!(source: 'google', client: client_with_org)
|
|
1336
|
+
lead_two = Lead.create!(source: 'referral', client: client_without_org)
|
|
1337
|
+
|
|
1338
|
+
sale_with_org = Sale.create!(lead: lead_one)
|
|
1339
|
+
Sale.create!(lead: lead_two)
|
|
1340
|
+
|
|
822
1341
|
config = {
|
|
823
|
-
|
|
824
|
-
type: :
|
|
825
|
-
filter_type: :
|
|
826
|
-
|
|
827
|
-
|
|
1342
|
+
has_organization: {
|
|
1343
|
+
type: :nested,
|
|
1344
|
+
filter_type: :presence,
|
|
1345
|
+
association: :lead__client,
|
|
1346
|
+
dbcolumn: :organization_id
|
|
1347
|
+
}
|
|
828
1348
|
}
|
|
829
1349
|
|
|
830
1350
|
result = described_class.apply(
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
1351
|
+
scope: Sale.all,
|
|
1352
|
+
config: config,
|
|
1353
|
+
params: { has_organization: 'true' }
|
|
834
1354
|
)
|
|
835
1355
|
|
|
836
|
-
expect(result).to contain_exactly(
|
|
1356
|
+
expect(result).to contain_exactly(sale_with_org)
|
|
1357
|
+
end
|
|
837
1358
|
end
|
|
838
1359
|
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
1360
|
+
describe 'presence filter on non-string columns' do
|
|
1361
|
+
def presence_config(column, **extra)
|
|
1362
|
+
{ has_value: { type: :single, filter_type: :presence, dbcolumn: column }.merge(extra) }
|
|
1363
|
+
end
|
|
1364
|
+
|
|
1365
|
+
{
|
|
1366
|
+
project_id: [1, 7],
|
|
1367
|
+
amount: [0, 500],
|
|
1368
|
+
conversion_rate: [0.0, 2.5],
|
|
1369
|
+
actual_sale_date: [Time.utc(2013, 1, 1), Time.utc(2013, 6, 1)],
|
|
1370
|
+
active: [false, true]
|
|
1371
|
+
}.each do |column, (value_one, value_two)|
|
|
1372
|
+
context "on the #{column} column" do
|
|
1373
|
+
let!(:with_one) { Sale.create!(column => value_one) }
|
|
1374
|
+
let!(:with_two) { Sale.create!(column => value_two) }
|
|
1375
|
+
let!(:without) { Sale.create!(column => nil) }
|
|
1376
|
+
|
|
1377
|
+
it 'returns only non-NULL rows when value is true' do
|
|
1378
|
+
result = described_class.apply(
|
|
1379
|
+
scope: Sale.all, config: presence_config(column), params: { has_value: 'true' }
|
|
1380
|
+
)
|
|
1381
|
+
expect(result).to contain_exactly(with_one, with_two)
|
|
1382
|
+
end
|
|
1383
|
+
|
|
1384
|
+
it 'returns only NULL rows when value is false' do
|
|
1385
|
+
result = described_class.apply(
|
|
1386
|
+
scope: Sale.all, config: presence_config(column), params: { has_value: 'false' }
|
|
1387
|
+
)
|
|
1388
|
+
expect(result).to contain_exactly(without)
|
|
1389
|
+
end
|
|
1390
|
+
end
|
|
1391
|
+
end
|
|
1392
|
+
|
|
1393
|
+
it 'treats a zero integer as present, not blank' do
|
|
1394
|
+
zero = Sale.create!(amount: 0)
|
|
1395
|
+
Sale.create!(amount: nil)
|
|
1396
|
+
|
|
1397
|
+
result = described_class.apply(
|
|
1398
|
+
scope: Sale.all, config: presence_config(:amount), params: { has_value: 'true' }
|
|
842
1399
|
)
|
|
1400
|
+
expect(result).to contain_exactly(zero)
|
|
1401
|
+
end
|
|
1402
|
+
|
|
1403
|
+
it 'works on a nested non-string (integer foreign key) column' do
|
|
1404
|
+
client = Client.create!(name: 'John')
|
|
1405
|
+
lead_with_client = Lead.create!(source: 'google', client: client)
|
|
1406
|
+
lead_without_client = Lead.create!(source: 'referral', client: nil)
|
|
1407
|
+
sale_with = Sale.create!(lead: lead_with_client)
|
|
1408
|
+
sale_without = Sale.create!(lead: lead_without_client)
|
|
843
1409
|
|
|
844
1410
|
config = {
|
|
845
|
-
|
|
846
|
-
type: :single,
|
|
847
|
-
filter_type: :string,
|
|
848
|
-
dbcolumn: :stage
|
|
849
|
-
}
|
|
1411
|
+
has_client: { type: :nested, filter_type: :presence, association: :lead, dbcolumn: :client_id }
|
|
850
1412
|
}
|
|
1413
|
+
present = described_class.apply(scope: Sale.all, config: config, params: { has_client: 'true' })
|
|
1414
|
+
blank = described_class.apply(scope: Sale.all, config: config, params: { has_client: 'false' })
|
|
1415
|
+
|
|
1416
|
+
expect(present).to contain_exactly(sale_with)
|
|
1417
|
+
expect(blank).to contain_exactly(sale_without)
|
|
1418
|
+
end
|
|
1419
|
+
|
|
1420
|
+
it 'still treats an empty string as blank on a string column' do
|
|
1421
|
+
sale = Sale.create!(stage: 'qualified')
|
|
1422
|
+
Sale.create!(stage: '')
|
|
851
1423
|
|
|
852
1424
|
result = described_class.apply(
|
|
853
|
-
|
|
854
|
-
config: config,
|
|
855
|
-
params: {
|
|
856
|
-
unknown_field: "something"
|
|
857
|
-
}
|
|
1425
|
+
scope: Sale.all, config: presence_config(:stage), params: { has_value: true }
|
|
858
1426
|
)
|
|
859
|
-
|
|
860
1427
|
expect(result).to contain_exactly(sale)
|
|
1428
|
+
end
|
|
861
1429
|
end
|
|
862
1430
|
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
1431
|
+
describe 'invalid numeric input' do
|
|
1432
|
+
it 'ignores a non-numeric exact integer filter value' do
|
|
1433
|
+
sale = Sale.create!(amount: 500)
|
|
1434
|
+
|
|
1435
|
+
config = {
|
|
1436
|
+
amount: {
|
|
1437
|
+
type: :single,
|
|
1438
|
+
filter_type: :integer,
|
|
1439
|
+
dbcolumn: :amount
|
|
1440
|
+
}
|
|
1441
|
+
}
|
|
1442
|
+
|
|
1443
|
+
result = described_class.apply(
|
|
1444
|
+
scope: Sale.all,
|
|
1445
|
+
config: config,
|
|
1446
|
+
params: { amount: 'abc' }
|
|
866
1447
|
)
|
|
867
1448
|
|
|
868
|
-
|
|
869
|
-
|
|
1449
|
+
expect(result).to contain_exactly(sale)
|
|
1450
|
+
end
|
|
1451
|
+
|
|
1452
|
+
it 'ignores a non-numeric gte filter value' do
|
|
1453
|
+
sale = Sale.create!(amount: 500)
|
|
1454
|
+
|
|
1455
|
+
config = {
|
|
1456
|
+
amount: {
|
|
1457
|
+
type: :single,
|
|
1458
|
+
filter_type: :integer,
|
|
1459
|
+
dbcolumn: :amount,
|
|
1460
|
+
range_type: :gte
|
|
1461
|
+
}
|
|
1462
|
+
}
|
|
1463
|
+
|
|
1464
|
+
result = described_class.apply(
|
|
1465
|
+
scope: Sale.all,
|
|
1466
|
+
config: config,
|
|
1467
|
+
params: { amount: 'not-a-number' }
|
|
870
1468
|
)
|
|
871
1469
|
|
|
1470
|
+
expect(result).to contain_exactly(sale)
|
|
1471
|
+
end
|
|
1472
|
+
|
|
1473
|
+
it 'honors the valid bound and ignores an invalid bound in a between filter' do
|
|
1474
|
+
sale_one = Sale.create!(amount: 100)
|
|
1475
|
+
sale_two = Sale.create!(amount: 900)
|
|
1476
|
+
Sale.create!(amount: 2000)
|
|
1477
|
+
|
|
872
1478
|
config = {
|
|
873
|
-
|
|
1479
|
+
amount: {
|
|
874
1480
|
type: :single,
|
|
875
|
-
filter_type: :
|
|
876
|
-
dbcolumn: :
|
|
877
|
-
|
|
1481
|
+
filter_type: :integer,
|
|
1482
|
+
dbcolumn: :amount,
|
|
1483
|
+
range_type: :between
|
|
1484
|
+
}
|
|
878
1485
|
}
|
|
879
1486
|
|
|
880
1487
|
result = described_class.apply(
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
status: '["approved","pending"]'
|
|
885
|
-
}
|
|
1488
|
+
scope: Sale.all,
|
|
1489
|
+
config: config,
|
|
1490
|
+
params: { amount: { from: 'not-a-number', to: 1000 } }
|
|
886
1491
|
)
|
|
887
1492
|
|
|
888
1493
|
expect(result).to contain_exactly(
|
|
889
|
-
|
|
890
|
-
|
|
1494
|
+
sale_one,
|
|
1495
|
+
sale_two
|
|
891
1496
|
)
|
|
1497
|
+
end
|
|
892
1498
|
end
|
|
893
1499
|
end
|
|
894
|
-
end
|
|
1500
|
+
end
|