@startsimpli/funnels 0.4.6 → 0.4.8

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.
Files changed (46) hide show
  1. package/package.json +1 -1
  2. package/src/api/client.test.ts +76 -76
  3. package/src/api/client.ts +16 -16
  4. package/src/components/FilterRuleEditor/FilterRuleEditor.stories.tsx +24 -24
  5. package/src/components/FilterRuleEditor/FilterRuleEditor.test.tsx +39 -39
  6. package/src/components/FilterRuleEditor/FilterRuleEditor.tsx +1 -1
  7. package/src/components/FilterRuleEditor/RuleRow.tsx +8 -8
  8. package/src/components/FunnelCard/FunnelCard.stories.tsx +118 -118
  9. package/src/components/FunnelCard/FunnelCard.test.ts +48 -48
  10. package/src/components/FunnelCard/FunnelCard.test.tsx +40 -40
  11. package/src/components/FunnelCard/FunnelCard.tsx +3 -3
  12. package/src/components/FunnelPreview/FunnelPreview.test.tsx +63 -63
  13. package/src/components/FunnelPreview/FunnelPreview.tsx +11 -11
  14. package/src/components/FunnelPreview/StageBreakdown.tsx +5 -5
  15. package/src/components/FunnelPreview/example.tsx +43 -43
  16. package/src/components/FunnelRunHistory/FunnelRunHistory.stories.tsx +70 -70
  17. package/src/components/FunnelRunHistory/FunnelRunHistory.test.tsx +63 -63
  18. package/src/components/FunnelRunHistory/FunnelRunHistory.tsx +12 -12
  19. package/src/components/FunnelRunHistory/RunDetailsModal.tsx +5 -5
  20. package/src/components/FunnelRunHistory/RunFilters.tsx +8 -8
  21. package/src/components/FunnelRunHistory/RunRow.tsx +7 -7
  22. package/src/components/FunnelRunHistory/StageBreakdownList.tsx +9 -9
  23. package/src/components/FunnelRunHistory/types.ts +6 -6
  24. package/src/components/FunnelStageBuilder/FunnelStageBuilder.stories.tsx +41 -41
  25. package/src/components/FunnelStageBuilder/FunnelStageBuilder.test.tsx +6 -6
  26. package/src/components/FunnelStageBuilder/FunnelStageBuilder.tsx +5 -5
  27. package/src/components/FunnelStageBuilder/StageActions.test.tsx +7 -7
  28. package/src/components/FunnelStageBuilder/StageActions.tsx +4 -4
  29. package/src/components/FunnelStageBuilder/StageCard.tsx +3 -3
  30. package/src/components/FunnelStageBuilder/StageForm.tsx +14 -14
  31. package/src/components/FunnelVisualFlow/FunnelVisualFlow.stories.tsx +42 -42
  32. package/src/components/FunnelVisualFlow/FunnelVisualFlow.test.tsx +31 -31
  33. package/src/components/FunnelVisualFlow/FunnelVisualFlow.tsx +4 -4
  34. package/src/components/FunnelVisualFlow/StageNode.tsx +8 -8
  35. package/src/components/FunnelVisualFlow/example.tsx +7 -7
  36. package/src/core/engine.test.ts +271 -271
  37. package/src/core/engine.ts +60 -60
  38. package/src/core/evaluator.example.ts +31 -31
  39. package/src/core/evaluator.test.ts +92 -92
  40. package/src/core/evaluator.ts +23 -23
  41. package/src/core/field-resolver.example.ts +1 -1
  42. package/src/core/field-resolver.test.ts +4 -4
  43. package/src/store/create-funnel-store.test.ts +32 -32
  44. package/src/store/create-funnel-store.ts +3 -3
  45. package/src/store/types.ts +2 -2
  46. package/src/types/index.ts +64 -64
@@ -36,88 +36,88 @@ const baseFunnel: Funnel = {
36
36
  name: 'Series A Investor Qualification',
37
37
  description: 'Identifies qualified Series A investors based on stage focus, check size, and geography.',
38
38
  status: 'active',
39
- input_type: 'contacts',
39
+ inputType: 'contacts',
40
40
  stages: [
41
41
  {
42
42
  id: 'stage-1',
43
43
  order: 0,
44
44
  name: 'Stage Filter',
45
- filter_logic: 'AND',
45
+ filterLogic: 'AND',
46
46
  rules: [
47
- { field_path: 'firm.stage', operator: 'in', value: ['Series A', 'Multi-Stage'] },
47
+ { fieldPath: 'firm.stage', operator: 'in', value: ['Series A', 'Multi-Stage'] },
48
48
  ],
49
- match_action: 'tag_continue',
50
- no_match_action: 'exclude',
51
- match_tags: ['qualified_stage'],
49
+ matchAction: 'tag_continue',
50
+ noMatchAction: 'exclude',
51
+ matchTags: ['qualified_stage'],
52
52
  },
53
53
  {
54
54
  id: 'stage-2',
55
55
  order: 1,
56
56
  name: 'Check Size Filter',
57
- filter_logic: 'AND',
57
+ filterLogic: 'AND',
58
58
  rules: [
59
- { field_path: 'firm.check_size_min', operator: 'lte', value: 5000000 },
60
- { field_path: 'firm.check_size_max', operator: 'gte', value: 3000000 },
61
- { field_path: 'firm.active', operator: 'is_true', value: null },
59
+ { fieldPath: 'firm.check_size_min', operator: 'lte', value: 5000000 },
60
+ { fieldPath: 'firm.check_size_max', operator: 'gte', value: 3000000 },
61
+ { fieldPath: 'firm.active', operator: 'is_true', value: null },
62
62
  ],
63
- match_action: 'tag_continue',
64
- no_match_action: 'tag_exclude',
65
- match_tags: ['qualified_check_size'],
66
- no_match_tags: ['excluded_check_size'],
63
+ matchAction: 'tag_continue',
64
+ noMatchAction: 'tag_exclude',
65
+ matchTags: ['qualified_check_size'],
66
+ noMatchTags: ['excluded_check_size'],
67
67
  },
68
68
  {
69
69
  id: 'stage-3',
70
70
  order: 2,
71
71
  name: 'Geography Filter',
72
- filter_logic: 'OR',
72
+ filterLogic: 'OR',
73
73
  rules: [
74
- { field_path: 'firm.geography', operator: 'has_any', value: ['US', 'North America'] },
74
+ { fieldPath: 'firm.geography', operator: 'has_any', value: ['US', 'North America'] },
75
75
  ],
76
- match_action: 'tag_continue',
77
- no_match_action: 'exclude',
78
- match_tags: ['qualified_geography'],
76
+ matchAction: 'tag_continue',
77
+ noMatchAction: 'exclude',
78
+ matchTags: ['qualified_geography'],
79
79
  },
80
80
  {
81
81
  id: 'stage-4',
82
82
  order: 3,
83
83
  name: 'Industry Filter',
84
- filter_logic: 'OR',
84
+ filterLogic: 'OR',
85
85
  rules: [
86
- { field_path: 'firm.industries', operator: 'has_any', value: ['SaaS', 'Enterprise'] },
86
+ { fieldPath: 'firm.industries', operator: 'has_any', value: ['SaaS', 'Enterprise'] },
87
87
  ],
88
- match_action: 'tag_continue',
89
- no_match_action: 'exclude',
90
- match_tags: ['qualified_industry'],
88
+ matchAction: 'tag_continue',
89
+ noMatchAction: 'exclude',
90
+ matchTags: ['qualified_industry'],
91
91
  },
92
92
  {
93
93
  id: 'stage-5',
94
94
  order: 4,
95
95
  name: 'Final Qualification',
96
- filter_logic: 'AND',
96
+ filterLogic: 'AND',
97
97
  rules: [
98
- { field_path: 'tags', operator: 'has_all', value: ['qualified_stage', 'qualified_check_size', 'qualified_geography'] },
98
+ { fieldPath: 'tags', operator: 'has_all', value: ['qualified_stage', 'qualified_check_size', 'qualified_geography'] },
99
99
  ],
100
- match_action: 'output',
101
- no_match_action: 'exclude',
100
+ matchAction: 'output',
101
+ noMatchAction: 'exclude',
102
102
  },
103
103
  ],
104
- created_at: '2024-01-01T00:00:00Z',
105
- updated_at: '2024-01-15T10:30:00Z',
104
+ createdAt: '2024-01-01T00:00:00Z',
105
+ updatedAt: '2024-01-15T10:30:00Z',
106
106
  };
107
107
 
108
108
  const completedRun: FunnelRun = {
109
109
  id: 'run-1',
110
- funnel_id: 'funnel-1',
110
+ funnelId: 'funnel-1',
111
111
  status: 'completed',
112
- trigger_type: 'manual',
113
- started_at: '2024-01-15T10:00:00Z',
114
- completed_at: '2024-01-15T10:05:00Z',
115
- duration_ms: 300000,
116
- total_input: 83061,
117
- total_matched: 235,
118
- total_excluded: 82826,
119
- total_tagged: 235,
120
- stage_stats: {},
112
+ triggerType: 'manual',
113
+ startedAt: '2024-01-15T10:00:00Z',
114
+ completedAt: '2024-01-15T10:05:00Z',
115
+ durationMs: 300000,
116
+ totalInput: 83061,
117
+ totalMatched: 235,
118
+ totalExcluded: 82826,
119
+ totalTagged: 235,
120
+ stageStats: {},
121
121
  };
122
122
 
123
123
  // Stories
@@ -178,7 +178,7 @@ export const Running: Story = {
178
178
  latestRun: {
179
179
  ...completedRun,
180
180
  status: 'running',
181
- completed_at: undefined,
181
+ completedAt: undefined,
182
182
  },
183
183
  },
184
184
  };
@@ -214,9 +214,9 @@ export const OneStage: Story = {
214
214
  },
215
215
  latestRun: {
216
216
  ...completedRun,
217
- total_input: 1000,
218
- total_matched: 450,
219
- total_excluded: 550,
217
+ totalInput: 1000,
218
+ totalMatched: 450,
219
+ totalExcluded: 550,
220
220
  },
221
221
  },
222
222
  };
@@ -229,12 +229,12 @@ export const TenStages: Story = {
229
229
  id: `stage-${i}`,
230
230
  order: i,
231
231
  name: `Stage ${i + 1}: Filter ${i + 1}`,
232
- filter_logic: 'AND' as const,
232
+ filterLogic: 'AND' as const,
233
233
  rules: [
234
- { field_path: `field${i}`, operator: 'eq' as const, value: i },
234
+ { fieldPath: `field${i}`, operator: 'eq' as const, value: i },
235
235
  ],
236
- match_action: 'continue' as const,
237
- no_match_action: 'exclude' as const,
236
+ matchAction: 'continue' as const,
237
+ noMatchAction: 'exclude' as const,
238
238
  })),
239
239
  name: 'Complex Multi-Stage Funnel',
240
240
  description: 'Demonstrates card with many stages.',
@@ -248,9 +248,9 @@ export const HighMatchRate: Story = {
248
248
  funnel: baseFunnel,
249
249
  latestRun: {
250
250
  ...completedRun,
251
- total_input: 1000,
252
- total_matched: 950,
253
- total_excluded: 50,
251
+ totalInput: 1000,
252
+ totalMatched: 950,
253
+ totalExcluded: 50,
254
254
  },
255
255
  },
256
256
  };
@@ -260,9 +260,9 @@ export const LowMatchRate: Story = {
260
260
  funnel: baseFunnel,
261
261
  latestRun: {
262
262
  ...completedRun,
263
- total_input: 100000,
264
- total_matched: 12,
265
- total_excluded: 99988,
263
+ totalInput: 100000,
264
+ totalMatched: 12,
265
+ totalExcluded: 99988,
266
266
  },
267
267
  },
268
268
  };
@@ -272,8 +272,8 @@ export const ZeroMatches: Story = {
272
272
  funnel: baseFunnel,
273
273
  latestRun: {
274
274
  ...completedRun,
275
- total_matched: 0,
276
- total_excluded: 83061,
275
+ totalMatched: 0,
276
+ totalExcluded: 83061,
277
277
  },
278
278
  },
279
279
  };
@@ -317,52 +317,52 @@ export const RecipeFunnel: Story = {
317
317
  name: 'Quick Easy Dinner Recommendations',
318
318
  description: 'Filters recipes for vegetarian, gluten-free, quick prep, easy difficulty.',
319
319
  status: 'active',
320
- input_type: 'any',
320
+ inputType: 'any',
321
321
  stages: [
322
322
  {
323
323
  id: 'stage-1',
324
324
  order: 0,
325
325
  name: 'Dietary Restrictions',
326
- filter_logic: 'AND',
326
+ filterLogic: 'AND',
327
327
  rules: [
328
- { field_path: 'dietary_restrictions', operator: 'has_all', value: ['vegetarian', 'gluten-free'] },
328
+ { fieldPath: 'dietary_restrictions', operator: 'has_all', value: ['vegetarian', 'gluten-free'] },
329
329
  ],
330
- match_action: 'tag_continue',
331
- no_match_action: 'exclude',
332
- match_tags: ['meets_dietary_needs'],
330
+ matchAction: 'tag_continue',
331
+ noMatchAction: 'exclude',
332
+ matchTags: ['meets_dietary_needs'],
333
333
  },
334
334
  {
335
335
  id: 'stage-2',
336
336
  order: 1,
337
337
  name: 'Time Constraint',
338
- filter_logic: 'AND',
338
+ filterLogic: 'AND',
339
339
  rules: [
340
- { field_path: 'prep_time_minutes', operator: 'lte', value: 30 },
340
+ { fieldPath: 'prep_time_minutes', operator: 'lte', value: 30 },
341
341
  ],
342
- match_action: 'tag_continue',
343
- no_match_action: 'exclude',
344
- match_tags: ['quick_prep'],
342
+ matchAction: 'tag_continue',
343
+ noMatchAction: 'exclude',
344
+ matchTags: ['quick_prep'],
345
345
  },
346
346
  {
347
347
  id: 'stage-3',
348
348
  order: 2,
349
349
  name: 'Difficulty Level',
350
- filter_logic: 'OR',
350
+ filterLogic: 'OR',
351
351
  rules: [
352
- { field_path: 'difficulty', operator: 'in', value: ['easy', 'medium'] },
352
+ { fieldPath: 'difficulty', operator: 'in', value: ['easy', 'medium'] },
353
353
  ],
354
- match_action: 'output',
355
- no_match_action: 'exclude',
354
+ matchAction: 'output',
355
+ noMatchAction: 'exclude',
356
356
  },
357
357
  ],
358
- created_at: '2024-01-01T00:00:00Z',
359
- updated_at: '2024-01-15T10:30:00Z',
358
+ createdAt: '2024-01-01T00:00:00Z',
359
+ updatedAt: '2024-01-15T10:30:00Z',
360
360
  },
361
361
  latestRun: {
362
362
  ...completedRun,
363
- total_input: 5420,
364
- total_matched: 87,
365
- total_excluded: 5333,
363
+ totalInput: 5420,
364
+ totalMatched: 87,
365
+ totalExcluded: 5333,
366
366
  },
367
367
  },
368
368
  };
@@ -374,55 +374,55 @@ export const LeadScoringFunnel: Story = {
374
374
  name: 'Enterprise Lead Scoring',
375
375
  description: 'Scores and qualifies enterprise leads based on company size, engagement, and fit.',
376
376
  status: 'active',
377
- input_type: 'any',
377
+ inputType: 'any',
378
378
  stages: [
379
379
  {
380
380
  id: 'stage-1',
381
381
  order: 0,
382
382
  name: 'Company Size',
383
- filter_logic: 'AND',
383
+ filterLogic: 'AND',
384
384
  rules: [
385
- { field_path: 'company.employee_count', operator: 'gte', value: 100 },
385
+ { fieldPath: 'company.employee_count', operator: 'gte', value: 100 },
386
386
  ],
387
- match_action: 'tag_continue',
388
- no_match_action: 'tag_continue',
389
- match_tags: ['enterprise_size'],
390
- no_match_tags: ['smb_size'],
387
+ matchAction: 'tag_continue',
388
+ noMatchAction: 'tag_continue',
389
+ matchTags: ['enterprise_size'],
390
+ noMatchTags: ['smb_size'],
391
391
  },
392
392
  {
393
393
  id: 'stage-2',
394
394
  order: 1,
395
395
  name: 'Engagement Score',
396
- filter_logic: 'OR',
396
+ filterLogic: 'OR',
397
397
  rules: [
398
- { field_path: 'engagement.email_opens', operator: 'gte', value: 5 },
399
- { field_path: 'engagement.demo_requested', operator: 'is_true', value: null },
398
+ { fieldPath: 'engagement.email_opens', operator: 'gte', value: 5 },
399
+ { fieldPath: 'engagement.demo_requested', operator: 'is_true', value: null },
400
400
  ],
401
- match_action: 'tag_continue',
402
- no_match_action: 'tag_continue',
403
- match_tags: ['engaged'],
401
+ matchAction: 'tag_continue',
402
+ noMatchAction: 'tag_continue',
403
+ matchTags: ['engaged'],
404
404
  },
405
405
  {
406
406
  id: 'stage-3',
407
407
  order: 2,
408
408
  name: 'Final Qualification',
409
- filter_logic: 'AND',
409
+ filterLogic: 'AND',
410
410
  rules: [
411
- { field_path: 'tags', operator: 'has_all', value: ['enterprise_size', 'engaged'] },
411
+ { fieldPath: 'tags', operator: 'has_all', value: ['enterprise_size', 'engaged'] },
412
412
  ],
413
- match_action: 'output',
414
- no_match_action: 'continue',
415
- match_tags: ['hot_lead'],
413
+ matchAction: 'output',
414
+ noMatchAction: 'continue',
415
+ matchTags: ['hot_lead'],
416
416
  },
417
417
  ],
418
- created_at: '2024-01-01T00:00:00Z',
419
- updated_at: '2024-01-15T10:30:00Z',
418
+ createdAt: '2024-01-01T00:00:00Z',
419
+ updatedAt: '2024-01-15T10:30:00Z',
420
420
  },
421
421
  latestRun: {
422
422
  ...completedRun,
423
- total_input: 12500,
424
- total_matched: 342,
425
- total_excluded: 12158,
423
+ totalInput: 12500,
424
+ totalMatched: 342,
425
+ totalExcluded: 12158,
426
426
  },
427
427
  },
428
428
  };
@@ -434,51 +434,51 @@ export const GitHubIssueFunnel: Story = {
434
434
  name: 'GitHub Issue Priority Triage',
435
435
  description: 'Filters GitHub issues to identify high-priority items requiring immediate attention.',
436
436
  status: 'active',
437
- input_type: 'any',
437
+ inputType: 'any',
438
438
  stages: [
439
439
  {
440
440
  id: 'stage-1',
441
441
  order: 0,
442
442
  name: 'Filter PRs',
443
- filter_logic: 'AND',
443
+ filterLogic: 'AND',
444
444
  rules: [
445
- { field_path: 'is_pull_request', operator: 'is_false', value: null },
445
+ { fieldPath: 'is_pull_request', operator: 'is_false', value: null },
446
446
  ],
447
- match_action: 'continue',
448
- no_match_action: 'exclude',
447
+ matchAction: 'continue',
448
+ noMatchAction: 'exclude',
449
449
  },
450
450
  {
451
451
  id: 'stage-2',
452
452
  order: 1,
453
453
  name: 'Priority Labels',
454
- filter_logic: 'OR',
454
+ filterLogic: 'OR',
455
455
  rules: [
456
- { field_path: 'labels', operator: 'has_any', value: ['bug', 'security'] },
456
+ { fieldPath: 'labels', operator: 'has_any', value: ['bug', 'security'] },
457
457
  ],
458
- match_action: 'tag_continue',
459
- no_match_action: 'continue',
460
- match_tags: ['high_priority'],
458
+ matchAction: 'tag_continue',
459
+ noMatchAction: 'continue',
460
+ matchTags: ['high_priority'],
461
461
  },
462
462
  {
463
463
  id: 'stage-3',
464
464
  order: 2,
465
465
  name: 'Community Interest',
466
- filter_logic: 'AND',
466
+ filterLogic: 'AND',
467
467
  rules: [
468
- { field_path: 'comments_count', operator: 'gte', value: 5 },
468
+ { fieldPath: 'comments_count', operator: 'gte', value: 5 },
469
469
  ],
470
- match_action: 'output',
471
- no_match_action: 'output',
470
+ matchAction: 'output',
471
+ noMatchAction: 'output',
472
472
  },
473
473
  ],
474
- created_at: '2024-01-01T00:00:00Z',
475
- updated_at: '2024-01-15T10:30:00Z',
474
+ createdAt: '2024-01-01T00:00:00Z',
475
+ updatedAt: '2024-01-15T10:30:00Z',
476
476
  },
477
477
  latestRun: {
478
478
  ...completedRun,
479
- total_input: 1847,
480
- total_matched: 92,
481
- total_excluded: 1755,
479
+ totalInput: 1847,
480
+ totalMatched: 92,
481
+ totalExcluded: 1755,
482
482
  },
483
483
  },
484
484
  };
@@ -23,37 +23,37 @@ function createFunnel(overrides?: Partial<Funnel>): Funnel {
23
23
  name: 'Test Funnel',
24
24
  description: 'Test funnel description',
25
25
  status: 'active',
26
- input_type: 'any',
26
+ inputType: 'any',
27
27
  stages: [
28
28
  {
29
29
  id: 'stage-1',
30
30
  order: 0,
31
31
  name: 'Stage 1',
32
32
  description: 'First stage',
33
- filter_logic: 'AND',
33
+ filterLogic: 'AND',
34
34
  rules: [
35
- { field_path: 'name', operator: 'eq', value: 'test' }
35
+ { fieldPath: 'name', operator: 'eq', value: 'test' }
36
36
  ],
37
- match_action: 'continue',
38
- no_match_action: 'exclude',
37
+ matchAction: 'continue',
38
+ noMatchAction: 'exclude',
39
39
  },
40
40
  {
41
41
  id: 'stage-2',
42
42
  order: 1,
43
43
  name: 'Stage 2',
44
44
  description: 'Second stage',
45
- filter_logic: 'OR',
45
+ filterLogic: 'OR',
46
46
  rules: [
47
- { field_path: 'value', operator: 'gt', value: 100 },
48
- { field_path: 'type', operator: 'in', value: ['A', 'B'] },
49
- { field_path: 'active', operator: 'is_true', value: null },
47
+ { fieldPath: 'value', operator: 'gt', value: 100 },
48
+ { fieldPath: 'type', operator: 'in', value: ['A', 'B'] },
49
+ { fieldPath: 'active', operator: 'is_true', value: null },
50
50
  ],
51
- match_action: 'output',
52
- no_match_action: 'exclude',
51
+ matchAction: 'output',
52
+ noMatchAction: 'exclude',
53
53
  },
54
54
  ],
55
- created_at: '2024-01-01T00:00:00Z',
56
- updated_at: '2024-01-01T00:00:00Z',
55
+ createdAt: '2024-01-01T00:00:00Z',
56
+ updatedAt: '2024-01-01T00:00:00Z',
57
57
  ...overrides,
58
58
  };
59
59
  }
@@ -61,17 +61,17 @@ function createFunnel(overrides?: Partial<Funnel>): Funnel {
61
61
  function createFunnelRun(overrides?: Partial<FunnelRun>): FunnelRun {
62
62
  return {
63
63
  id: 'run-1',
64
- funnel_id: 'funnel-1',
64
+ funnelId: 'funnel-1',
65
65
  status: 'completed',
66
- trigger_type: 'manual',
67
- started_at: '2024-01-01T00:00:00Z',
68
- completed_at: '2024-01-01T00:01:00Z',
69
- duration_ms: 60000,
70
- total_input: 83061,
71
- total_matched: 235,
72
- total_excluded: 82826,
73
- total_tagged: 235,
74
- stage_stats: {},
66
+ triggerType: 'manual',
67
+ startedAt: '2024-01-01T00:00:00Z',
68
+ completedAt: '2024-01-01T00:01:00Z',
69
+ durationMs: 60000,
70
+ totalInput: 83061,
71
+ totalMatched: 235,
72
+ totalExcluded: 82826,
73
+ totalTagged: 235,
74
+ stageStats: {},
75
75
  ...overrides,
76
76
  };
77
77
  }
@@ -99,14 +99,14 @@ describe('FunnelCard', () => {
99
99
 
100
100
  expect(run.id).toBe('run-1');
101
101
  expect(run.status).toBe('completed');
102
- expect(run.total_input).toBe(83061);
103
- expect(run.total_matched).toBe(235);
102
+ expect(run.totalInput).toBe(83061);
103
+ expect(run.totalMatched).toBe(235);
104
104
  });
105
105
 
106
106
  it('creates funnel run with overrides', () => {
107
- const run = createFunnelRun({ total_matched: 1000 });
107
+ const run = createFunnelRun({ totalMatched: 1000 });
108
108
 
109
- expect(run.total_matched).toBe(1000);
109
+ expect(run.totalMatched).toBe(1000);
110
110
  });
111
111
  });
112
112
 
@@ -122,10 +122,10 @@ describe('FunnelCard', () => {
122
122
  id: `stage-${i}`,
123
123
  order: i,
124
124
  name: `Stage ${i + 1}`,
125
- filter_logic: 'AND' as const,
126
- rules: [{ field_path: 'x', operator: 'eq' as const, value: i }],
127
- match_action: 'continue' as const,
128
- no_match_action: 'exclude' as const,
125
+ filterLogic: 'AND' as const,
126
+ rules: [{ fieldPath: 'x', operator: 'eq' as const, value: i }],
127
+ matchAction: 'continue' as const,
128
+ noMatchAction: 'exclude' as const,
129
129
  }));
130
130
  const funnel = createFunnel({ stages });
131
131
 
@@ -147,21 +147,21 @@ describe('FunnelCard', () => {
147
147
  describe('Run Stats Calculation', () => {
148
148
  it('calculates match percentage correctly', () => {
149
149
  const run = createFunnelRun();
150
- const percentage = (run.total_matched / run.total_input) * 100;
150
+ const percentage = (run.totalMatched / run.totalInput) * 100;
151
151
 
152
152
  expect(percentage).toBeCloseTo(0.28, 2); // 235/83061 ≈ 0.28%
153
153
  });
154
154
 
155
155
  it('handles zero input', () => {
156
- const run = createFunnelRun({ total_input: 0, total_matched: 0 });
157
- const percentage = run.total_input > 0 ? (run.total_matched / run.total_input) * 100 : 0;
156
+ const run = createFunnelRun({ totalInput: 0, totalMatched: 0 });
157
+ const percentage = run.totalInput > 0 ? (run.totalMatched / run.totalInput) * 100 : 0;
158
158
 
159
159
  expect(percentage).toBe(0);
160
160
  });
161
161
 
162
162
  it('handles 100% match rate', () => {
163
- const run = createFunnelRun({ total_input: 1000, total_matched: 1000, total_excluded: 0 });
164
- const percentage = (run.total_matched / run.total_input) * 100;
163
+ const run = createFunnelRun({ totalInput: 1000, totalMatched: 1000, totalExcluded: 0 });
164
+ const percentage = (run.totalMatched / run.totalInput) * 100;
165
165
 
166
166
  expect(percentage).toBe(100);
167
167
  });
@@ -182,10 +182,10 @@ describe('FunnelCard', () => {
182
182
  id: 'stage-1',
183
183
  order: 0,
184
184
  name: 'Empty Stage',
185
- filter_logic: 'AND',
185
+ filterLogic: 'AND',
186
186
  rules: [],
187
- match_action: 'continue',
188
- no_match_action: 'exclude',
187
+ matchAction: 'continue',
188
+ noMatchAction: 'exclude',
189
189
  },
190
190
  ],
191
191
  });
@@ -199,14 +199,14 @@ describe('FunnelCard', () => {
199
199
  const run = createFunnelRun({ status: 'completed' });
200
200
 
201
201
  expect(run.status).toBe('completed');
202
- expect(run.completed_at).toBeDefined();
202
+ expect(run.completedAt).toBeDefined();
203
203
  });
204
204
 
205
205
  it('validates running state', () => {
206
- const run = createFunnelRun({ status: 'running', completed_at: undefined });
206
+ const run = createFunnelRun({ status: 'running', completedAt: undefined });
207
207
 
208
208
  expect(run.status).toBe('running');
209
- expect(run.completed_at).toBeUndefined();
209
+ expect(run.completedAt).toBeUndefined();
210
210
  });
211
211
 
212
212
  it('validates failed run', () => {
@@ -234,19 +234,19 @@ describe('FunnelCard', () => {
234
234
 
235
235
  it('handles large numbers in stats', () => {
236
236
  const run = createFunnelRun({
237
- total_input: 10000000,
238
- total_matched: 500000,
239
- total_excluded: 9500000,
237
+ totalInput: 10000000,
238
+ totalMatched: 500000,
239
+ totalExcluded: 9500000,
240
240
  });
241
241
 
242
- expect(run.total_input).toBe(10000000);
243
- expect(run.total_matched + run.total_excluded).toBe(run.total_input);
242
+ expect(run.totalInput).toBe(10000000);
243
+ expect(run.totalMatched + run.totalExcluded).toBe(run.totalInput);
244
244
  });
245
245
 
246
246
  it('validates stats add up correctly', () => {
247
247
  const run = createFunnelRun();
248
248
 
249
- expect(run.total_matched + run.total_excluded).toBe(run.total_input);
249
+ expect(run.totalMatched + run.totalExcluded).toBe(run.totalInput);
250
250
  });
251
251
  });
252
252
  });