bharat_filter_engine 0.1.1

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.
@@ -0,0 +1,894 @@
1
+ require "spec_helper"
2
+
3
+ RSpec.describe BharatFilterEngine::Engine do
4
+
5
+ # `let` (not `let!`) on purpose: these fixtures are only referenced
6
+ # by name in the first few examples below ("applies string filter",
7
+ # etc). Every other example in this file builds its own local
8
+ # `sale`/`sale_one`/`sale_two` records. If these were `let!` they'd
9
+ # be created unconditionally before *every* example in the file,
10
+ # and this record (stage: "qualified", approval_status: "approved",
11
+ # active: true, amount: 500) would leak into unrelated examples'
12
+ # results whenever their filter happens to match it.
13
+ let(:client) do
14
+ Client.create!(
15
+ name: "John Doe",
16
+ email: "john@example.com"
17
+ )
18
+ end
19
+
20
+ let(:lead) do
21
+ Lead.create!(
22
+ source: "google",
23
+ client: client,
24
+ created_at: Time.utc(2026, 7, 20)
25
+ )
26
+ end
27
+
28
+ let(:sale) do
29
+ Sale.create!(
30
+ stage: "qualified",
31
+ approval_status: "approved",
32
+ project_id: 10,
33
+ active: true,
34
+ amount: 500,
35
+ actual_sale_date: Time.utc(2026, 7, 25),
36
+ lead: lead
37
+ )
38
+ end
39
+
40
+ describe ".apply" do
41
+
42
+ it "applies string filter" do
43
+
44
+ config = {
45
+ stage: {
46
+ type: :single,
47
+ filter_type: :string,
48
+ dbcolumn: :stage
49
+ }
50
+ }
51
+
52
+ result =
53
+ described_class.apply(
54
+ scope: Sale.all,
55
+ config: config,
56
+ params: {
57
+ stage: "qualified"
58
+ }
59
+ )
60
+
61
+ expect(result).to contain_exactly(sale)
62
+ end
63
+
64
+ it "applies array filter" do
65
+
66
+ config = {
67
+ status: {
68
+ type: :single,
69
+ filter_type: :array,
70
+ dbcolumn: :approval_status
71
+ }
72
+ }
73
+
74
+ result =
75
+ described_class.apply(
76
+ scope: Sale.all,
77
+ config: config,
78
+ params: {
79
+ status: [
80
+ "approved",
81
+ "pending"
82
+ ]
83
+ }
84
+ )
85
+
86
+ expect(result).to contain_exactly(sale)
87
+ end
88
+
89
+ it "applies boolean filter" do
90
+
91
+ config = {
92
+ active: {
93
+ type: :single,
94
+ filter_type: :boolean,
95
+ dbcolumn: :active
96
+ }
97
+ }
98
+
99
+ result =
100
+ described_class.apply(
101
+ scope: Sale.all,
102
+ config: config,
103
+ params: {
104
+ active: "true"
105
+ }
106
+ )
107
+
108
+ expect(result).to contain_exactly(sale)
109
+ end
110
+
111
+ it "applies integer gte filter" do
112
+
113
+ config = {
114
+ amount: {
115
+ type: :single,
116
+ filter_type: :integer,
117
+ dbcolumn: :amount,
118
+ range_type: :gte
119
+ }
120
+ }
121
+
122
+ result =
123
+ described_class.apply(
124
+ scope: Sale.all,
125
+ config: config,
126
+ params: {
127
+ amount: 400
128
+ }
129
+ )
130
+
131
+ expect(result).to contain_exactly(sale)
132
+ end
133
+
134
+ it "applies date range" do
135
+
136
+ config = {
137
+ actual_sale_date: {
138
+ type: :single,
139
+ filter_type: :daterange,
140
+ dbcolumn: :actual_sale_date
141
+ }
142
+ }
143
+
144
+ result =
145
+ described_class.apply(
146
+ scope: Sale.all,
147
+ config: config,
148
+ params: {
149
+ actual_sale_date: {
150
+ from: "2026-07-15",
151
+ to: "2026-07-30"
152
+ }
153
+ }
154
+ )
155
+
156
+ expect(result).to contain_exactly(sale)
157
+ end
158
+
159
+ it "applies nested filter" do
160
+
161
+ config = {
162
+ lead_source: {
163
+ type: :nested,
164
+ filter_type: :array,
165
+ association: :lead,
166
+ dbcolumn: :source
167
+ }
168
+ }
169
+
170
+ result =
171
+ described_class.apply(
172
+ scope: Sale.all,
173
+ config: config,
174
+ params: {
175
+ lead_source: ["google"]
176
+ }
177
+ )
178
+
179
+ expect(result).to contain_exactly(sale)
180
+ end
181
+
182
+ it "returns no records when string value does not match" do
183
+ sale = Sale.create!(
184
+ stage: "qualified"
185
+ )
186
+
187
+ config = {
188
+ stage: {
189
+ type: :single,
190
+ filter_type: :string,
191
+ dbcolumn: :stage
192
+ }
193
+ }
194
+
195
+ result = described_class.apply(
196
+ scope: Sale.all,
197
+ config: config,
198
+ params: {
199
+ stage: "closed"
200
+ }
201
+ )
202
+
203
+ expect(result).to be_empty
204
+ end
205
+
206
+ it "ignores blank string values" do
207
+ sale = Sale.create!(
208
+ stage: "qualified"
209
+ )
210
+
211
+ config = {
212
+ stage: {
213
+ type: :single,
214
+ filter_type: :string,
215
+ dbcolumn: :stage
216
+ }
217
+ }
218
+
219
+ result = described_class.apply(
220
+ scope: Sale.all,
221
+ config: config,
222
+ params: {
223
+ stage: ""
224
+ }
225
+ )
226
+
227
+ expect(result).to contain_exactly(sale)
228
+ end
229
+
230
+ it "supports multiple array values" do
231
+ sale_one = Sale.create!(
232
+ approval_status: "approved"
233
+ )
234
+
235
+ sale_two = Sale.create!(
236
+ approval_status: "pending"
237
+ )
238
+
239
+ Sale.create!(
240
+ approval_status: "rejected"
241
+ )
242
+
243
+ config = {
244
+ status: {
245
+ type: :single,
246
+ filter_type: :array,
247
+ dbcolumn: :approval_status
248
+ }
249
+ }
250
+
251
+ result = described_class.apply(
252
+ scope: Sale.all,
253
+ config: config,
254
+ params: {
255
+ status: ["approved", "pending"]
256
+ }
257
+ )
258
+
259
+ expect(result).to contain_exactly(
260
+ sale_one,
261
+ sale_two
262
+ )
263
+ end
264
+
265
+ it "normalizes comma separated values into an array" do
266
+ sale_one = Sale.create!(
267
+ approval_status: "approved"
268
+ )
269
+
270
+ sale_two = Sale.create!(
271
+ approval_status: "pending"
272
+ )
273
+
274
+ config = {
275
+ status: {
276
+ type: :single,
277
+ filter_type: :array,
278
+ dbcolumn: :approval_status
279
+ }
280
+ }
281
+
282
+ result = described_class.apply(
283
+ scope: Sale.all,
284
+ config: config,
285
+ params: {
286
+ status: "approved,pending"
287
+ }
288
+ )
289
+
290
+ expect(result).to contain_exactly(
291
+ sale_one,
292
+ sale_two
293
+ )
294
+ end
295
+
296
+ it "filters boolean true values" do
297
+ sale = Sale.create!(
298
+ active: true
299
+ )
300
+
301
+ Sale.create!(
302
+ active: false
303
+ )
304
+
305
+ config = {
306
+ active: {
307
+ type: :single,
308
+ filter_type: :boolean,
309
+ dbcolumn: :active
310
+ }
311
+ }
312
+
313
+ result = described_class.apply(
314
+ scope: Sale.all,
315
+ config: config,
316
+ params: {
317
+ active: "true"
318
+ }
319
+ )
320
+
321
+ expect(result).to contain_exactly(sale)
322
+ end
323
+
324
+ it "filters boolean false values" do
325
+ Sale.create!(
326
+ active: true
327
+ )
328
+
329
+ sale = Sale.create!(
330
+ active: false
331
+ )
332
+
333
+ config = {
334
+ active: {
335
+ type: :single,
336
+ filter_type: :boolean,
337
+ dbcolumn: :active
338
+ }
339
+ }
340
+
341
+ result = described_class.apply(
342
+ scope: Sale.all,
343
+ config: config,
344
+ params: {
345
+ active: false
346
+ }
347
+ )
348
+
349
+ expect(result).to contain_exactly(sale)
350
+ end
351
+
352
+ it "filters exact integer values" do
353
+ sale = Sale.create!(
354
+ amount: 500
355
+ )
356
+
357
+ Sale.create!(
358
+ amount: 1000
359
+ )
360
+
361
+ config = {
362
+ amount: {
363
+ type: :single,
364
+ filter_type: :integer,
365
+ dbcolumn: :amount
366
+ }
367
+ }
368
+
369
+ result = described_class.apply(
370
+ scope: Sale.all,
371
+ config: config,
372
+ params: {
373
+ amount: "500"
374
+ }
375
+ )
376
+
377
+ expect(result).to contain_exactly(sale)
378
+ end
379
+
380
+ it "supports integer greater than or equal filter" do
381
+ sale_one = Sale.create!(
382
+ amount: 500
383
+ )
384
+
385
+ sale_two = Sale.create!(
386
+ amount: 1000
387
+ )
388
+
389
+ Sale.create!(
390
+ amount: 200
391
+ )
392
+
393
+ config = {
394
+ amount: {
395
+ type: :single,
396
+ filter_type: :integer,
397
+ dbcolumn: :amount,
398
+ range_type: :gte
399
+ }
400
+ }
401
+
402
+ result = described_class.apply(
403
+ scope: Sale.all,
404
+ config: config,
405
+ params: {
406
+ amount: 500
407
+ }
408
+ )
409
+
410
+ expect(result).to contain_exactly(
411
+ sale_one,
412
+ sale_two
413
+ )
414
+ end
415
+
416
+ it "supports integer less than or equal filter" do
417
+ sale_one = Sale.create!(
418
+ amount: 500
419
+ )
420
+
421
+ sale_two = Sale.create!(
422
+ amount: 200
423
+ )
424
+
425
+ Sale.create!(
426
+ amount: 1000
427
+ )
428
+
429
+ config = {
430
+ amount: {
431
+ type: :single,
432
+ filter_type: :integer,
433
+ dbcolumn: :amount,
434
+ range_type: :lte
435
+ }
436
+ }
437
+
438
+ result = described_class.apply(
439
+ scope: Sale.all,
440
+ config: config,
441
+ params: {
442
+ amount: 500
443
+ }
444
+ )
445
+
446
+ expect(result).to contain_exactly(
447
+ sale_one,
448
+ sale_two
449
+ )
450
+ end
451
+
452
+ it "supports float filters" do
453
+ sale = Sale.create!(
454
+ conversion_rate: 12.5
455
+ )
456
+
457
+ Sale.create!(
458
+ conversion_rate: 20.5
459
+ )
460
+
461
+ config = {
462
+ conversion_rate: {
463
+ type: :single,
464
+ filter_type: :float,
465
+ dbcolumn: :conversion_rate
466
+ }
467
+ }
468
+
469
+ result = described_class.apply(
470
+ scope: Sale.all,
471
+ config: config,
472
+ params: {
473
+ conversion_rate: "12.5"
474
+ }
475
+ )
476
+
477
+ expect(result).to contain_exactly(sale)
478
+ end
479
+
480
+ it "supports float filters" do
481
+ sale = Sale.create!(
482
+ conversion_rate: 12.5
483
+ )
484
+
485
+ Sale.create!(
486
+ conversion_rate: 20.5
487
+ )
488
+
489
+ config = {
490
+ conversion_rate: {
491
+ type: :single,
492
+ filter_type: :float,
493
+ dbcolumn: :conversion_rate
494
+ }
495
+ }
496
+
497
+ result = described_class.apply(
498
+ scope: Sale.all,
499
+ config: config,
500
+ params: {
501
+ conversion_rate: "12.5"
502
+ }
503
+ )
504
+
505
+ expect(result).to contain_exactly(sale)
506
+ end
507
+
508
+ it "supports float greater than or equal filter" do
509
+ sale_one = Sale.create!(
510
+ conversion_rate: 10.5
511
+ )
512
+
513
+ sale_two = Sale.create!(
514
+ conversion_rate: 20.5
515
+ )
516
+
517
+ Sale.create!(
518
+ conversion_rate: 5.5
519
+ )
520
+
521
+ config = {
522
+ conversion_rate: {
523
+ type: :single,
524
+ filter_type: :float,
525
+ dbcolumn: :conversion_rate,
526
+ range_type: :gte
527
+ }
528
+ }
529
+
530
+ result = described_class.apply(
531
+ scope: Sale.all,
532
+ config: config,
533
+ params: {
534
+ conversion_rate: 10.5
535
+ }
536
+ )
537
+
538
+ expect(result).to contain_exactly(
539
+ sale_one,
540
+ sale_two
541
+ )
542
+ end
543
+
544
+ it "filters between two dates" do
545
+ sale = Sale.create!(
546
+ actual_sale_date: Time.utc(2026, 7, 20)
547
+ )
548
+
549
+ Sale.create!(
550
+ actual_sale_date: Time.utc(2026, 8, 20)
551
+ )
552
+
553
+ config = {
554
+ actual_sale_date: {
555
+ type: :single,
556
+ filter_type: :daterange,
557
+ dbcolumn: :actual_sale_date
558
+ }
559
+ }
560
+
561
+ result = described_class.apply(
562
+ scope: Sale.all,
563
+ config: config,
564
+ params: {
565
+ actual_sale_date: {
566
+ from: "2026-07-01",
567
+ to: "2026-07-31"
568
+ }
569
+ }
570
+ )
571
+
572
+ expect(result).to contain_exactly(sale)
573
+ end
574
+
575
+ it "supports date range with only from date" do
576
+ sale = Sale.create!(
577
+ actual_sale_date: Time.utc(2026, 8, 10)
578
+ )
579
+
580
+ Sale.create!(
581
+ actual_sale_date: Time.utc(2026, 7, 10)
582
+ )
583
+
584
+ config = {
585
+ actual_sale_date: {
586
+ type: :single,
587
+ filter_type: :daterange,
588
+ dbcolumn: :actual_sale_date
589
+ }
590
+ }
591
+
592
+ result = described_class.apply(
593
+ scope: Sale.all,
594
+ config: config,
595
+ params: {
596
+ actual_sale_date: {
597
+ from: "2026-08-01"
598
+ }
599
+ }
600
+ )
601
+
602
+ expect(result).to contain_exactly(sale)
603
+ end
604
+
605
+ it "supports date range with only to date" do
606
+ sale = Sale.create!(
607
+ actual_sale_date: Time.utc(2026, 7, 10)
608
+ )
609
+
610
+ Sale.create!(
611
+ actual_sale_date: Time.utc(2026, 8, 10)
612
+ )
613
+
614
+ config = {
615
+ actual_sale_date: {
616
+ type: :single,
617
+ filter_type: :daterange,
618
+ dbcolumn: :actual_sale_date
619
+ }
620
+ }
621
+
622
+ result = described_class.apply(
623
+ scope: Sale.all,
624
+ config: config,
625
+ params: {
626
+ actual_sale_date: {
627
+ to: "2026-07-31"
628
+ }
629
+ }
630
+ )
631
+
632
+ expect(result).to contain_exactly(sale)
633
+ end
634
+
635
+ it "supports date range array format" do
636
+ sale = Sale.create!(
637
+ actual_sale_date: Time.utc(2026, 7, 20)
638
+ )
639
+
640
+ config = {
641
+ actual_sale_date: {
642
+ type: :single,
643
+ filter_type: :daterange,
644
+ dbcolumn: :actual_sale_date
645
+ }
646
+ }
647
+
648
+ result = described_class.apply(
649
+ scope: Sale.all,
650
+ config: config,
651
+ params: {
652
+ actual_sale_date: [
653
+ "2026-07-01",
654
+ "2026-07-31"
655
+ ]
656
+ }
657
+ )
658
+
659
+ expect(result).to contain_exactly(sale)
660
+ end
661
+
662
+ it "supports nested string filter" do
663
+ client = Client.create!(
664
+ name: "John"
665
+ )
666
+
667
+ lead = Lead.create!(
668
+ source: "google",
669
+ client: client
670
+ )
671
+
672
+ sale = Sale.create!(
673
+ lead: lead
674
+ )
675
+
676
+ config = {
677
+ lead_source: {
678
+ type: :nested,
679
+ filter_type: :string,
680
+ association: :lead,
681
+ dbcolumn: :source
682
+ }
683
+ }
684
+
685
+ result = described_class.apply(
686
+ scope: Sale.all,
687
+ config: config,
688
+ params: {
689
+ lead_source: "google"
690
+ }
691
+ )
692
+
693
+ expect(result).to contain_exactly(sale)
694
+ end
695
+
696
+ it "supports nested array filter" do
697
+ client = Client.create!(
698
+ name: "John"
699
+ )
700
+
701
+ lead_one = Lead.create!(
702
+ source: "google",
703
+ client: client
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)
713
+
714
+ config = {
715
+ lead_source: {
716
+ type: :nested,
717
+ filter_type: :array,
718
+ association: :lead,
719
+ dbcolumn: :source
720
+ }
721
+ }
722
+
723
+ result = described_class.apply(
724
+ scope: Sale.all,
725
+ config: config,
726
+ params: {
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
742
+ )
743
+
744
+ Sale.create!(
745
+ stage: "qualified",
746
+ approval_status: "pending",
747
+ amount: 500
748
+ )
749
+
750
+ Sale.create!(
751
+ stage: "new",
752
+ approval_status: "approved",
753
+ amount: 500
754
+ )
755
+
756
+ config = {
757
+ stage: {
758
+ type: :single,
759
+ filter_type: :string,
760
+ dbcolumn: :stage
761
+ },
762
+
763
+ status: {
764
+ type: :single,
765
+ filter_type: :array,
766
+ dbcolumn: :approval_status
767
+ }
768
+ }
769
+
770
+ result = described_class.apply(
771
+ scope: Sale.all,
772
+ config: config,
773
+ params: {
774
+ stage: "qualified",
775
+ status: ["approved"]
776
+ }
777
+ )
778
+
779
+ expect(result).to contain_exactly(sale_one)
780
+ end
781
+
782
+ it "preserves existing scope conditions" do
783
+ sale_one = Sale.create!(
784
+ stage: "qualified",
785
+ active: true
786
+ )
787
+
788
+ Sale.create!(
789
+ stage: "qualified",
790
+ active: false
791
+ )
792
+
793
+ base_scope =
794
+ Sale.where(active: true)
795
+
796
+ config = {
797
+ stage: {
798
+ type: :single,
799
+ filter_type: :string,
800
+ dbcolumn: :stage
801
+ }
802
+ }
803
+
804
+ result = described_class.apply(
805
+ scope: base_scope,
806
+ config: config,
807
+ params: {
808
+ stage: "qualified"
809
+ }
810
+ )
811
+
812
+ expect(result).to contain_exactly(
813
+ sale_one
814
+ )
815
+ end
816
+
817
+ it "returns the original scope when no filters are provided" do
818
+ sale = Sale.create!(
819
+ stage: "qualified"
820
+ )
821
+
822
+ config = {
823
+ stage: {
824
+ type: :single,
825
+ filter_type: :string,
826
+ dbcolumn: :stage
827
+ }
828
+ }
829
+
830
+ result = described_class.apply(
831
+ scope: Sale.all,
832
+ config: config,
833
+ params: {}
834
+ )
835
+
836
+ expect(result).to contain_exactly(sale)
837
+ end
838
+
839
+ it "ignores unknown params that are not present in config" do
840
+ sale = Sale.create!(
841
+ stage: "qualified"
842
+ )
843
+
844
+ config = {
845
+ stage: {
846
+ type: :single,
847
+ filter_type: :string,
848
+ dbcolumn: :stage
849
+ }
850
+ }
851
+
852
+ result = described_class.apply(
853
+ scope: Sale.all,
854
+ config: config,
855
+ params: {
856
+ unknown_field: "something"
857
+ }
858
+ )
859
+
860
+ expect(result).to contain_exactly(sale)
861
+ end
862
+
863
+ it "parses JSON array values" do
864
+ sale_one = Sale.create!(
865
+ approval_status: "approved"
866
+ )
867
+
868
+ sale_two = Sale.create!(
869
+ approval_status: "pending"
870
+ )
871
+
872
+ config = {
873
+ status: {
874
+ type: :single,
875
+ filter_type: :array,
876
+ dbcolumn: :approval_status
877
+ }
878
+ }
879
+
880
+ result = described_class.apply(
881
+ scope: Sale.all,
882
+ config: config,
883
+ params: {
884
+ status: '["approved","pending"]'
885
+ }
886
+ )
887
+
888
+ expect(result).to contain_exactly(
889
+ sale_one,
890
+ sale_two
891
+ )
892
+ end
893
+ end
894
+ end