@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.
- package/package.json +1 -1
- package/src/api/client.test.ts +76 -76
- package/src/api/client.ts +16 -16
- package/src/components/FilterRuleEditor/FilterRuleEditor.stories.tsx +24 -24
- package/src/components/FilterRuleEditor/FilterRuleEditor.test.tsx +39 -39
- package/src/components/FilterRuleEditor/FilterRuleEditor.tsx +1 -1
- package/src/components/FilterRuleEditor/RuleRow.tsx +8 -8
- package/src/components/FunnelCard/FunnelCard.stories.tsx +118 -118
- package/src/components/FunnelCard/FunnelCard.test.ts +48 -48
- package/src/components/FunnelCard/FunnelCard.test.tsx +40 -40
- package/src/components/FunnelCard/FunnelCard.tsx +3 -3
- package/src/components/FunnelPreview/FunnelPreview.test.tsx +63 -63
- package/src/components/FunnelPreview/FunnelPreview.tsx +11 -11
- package/src/components/FunnelPreview/StageBreakdown.tsx +5 -5
- package/src/components/FunnelPreview/example.tsx +43 -43
- package/src/components/FunnelRunHistory/FunnelRunHistory.stories.tsx +70 -70
- package/src/components/FunnelRunHistory/FunnelRunHistory.test.tsx +63 -63
- package/src/components/FunnelRunHistory/FunnelRunHistory.tsx +12 -12
- package/src/components/FunnelRunHistory/RunDetailsModal.tsx +5 -5
- package/src/components/FunnelRunHistory/RunFilters.tsx +8 -8
- package/src/components/FunnelRunHistory/RunRow.tsx +7 -7
- package/src/components/FunnelRunHistory/StageBreakdownList.tsx +9 -9
- package/src/components/FunnelRunHistory/types.ts +6 -6
- package/src/components/FunnelStageBuilder/FunnelStageBuilder.stories.tsx +41 -41
- package/src/components/FunnelStageBuilder/FunnelStageBuilder.test.tsx +6 -6
- package/src/components/FunnelStageBuilder/FunnelStageBuilder.tsx +5 -5
- package/src/components/FunnelStageBuilder/StageActions.test.tsx +7 -7
- package/src/components/FunnelStageBuilder/StageActions.tsx +4 -4
- package/src/components/FunnelStageBuilder/StageCard.tsx +3 -3
- package/src/components/FunnelStageBuilder/StageForm.tsx +14 -14
- package/src/components/FunnelVisualFlow/FunnelVisualFlow.stories.tsx +42 -42
- package/src/components/FunnelVisualFlow/FunnelVisualFlow.test.tsx +31 -31
- package/src/components/FunnelVisualFlow/FunnelVisualFlow.tsx +4 -4
- package/src/components/FunnelVisualFlow/StageNode.tsx +8 -8
- package/src/components/FunnelVisualFlow/example.tsx +7 -7
- package/src/core/engine.test.ts +271 -271
- package/src/core/engine.ts +60 -60
- package/src/core/evaluator.example.ts +31 -31
- package/src/core/evaluator.test.ts +92 -92
- package/src/core/evaluator.ts +23 -23
- package/src/core/field-resolver.example.ts +1 -1
- package/src/core/field-resolver.test.ts +4 -4
- package/src/store/create-funnel-store.test.ts +32 -32
- package/src/store/create-funnel-store.ts +3 -3
- package/src/store/types.ts +2 -2
- package/src/types/index.ts +64 -64
package/src/core/engine.ts
CHANGED
|
@@ -38,22 +38,22 @@ export interface ExecutionResult<T = any> {
|
|
|
38
38
|
excluded: FunnelResult<T>[];
|
|
39
39
|
|
|
40
40
|
/** Total entities processed */
|
|
41
|
-
|
|
41
|
+
totalInput: number;
|
|
42
42
|
|
|
43
43
|
/** Total entities that matched */
|
|
44
|
-
|
|
44
|
+
totalMatched: number;
|
|
45
45
|
|
|
46
46
|
/** Total entities excluded */
|
|
47
|
-
|
|
47
|
+
totalExcluded: number;
|
|
48
48
|
|
|
49
49
|
/** Total entities tagged */
|
|
50
|
-
|
|
50
|
+
totalTagged: number;
|
|
51
51
|
|
|
52
52
|
/** Per-stage statistics */
|
|
53
|
-
|
|
53
|
+
stageStats: Record<string, StageStats>;
|
|
54
54
|
|
|
55
55
|
/** Execution duration in milliseconds */
|
|
56
|
-
|
|
56
|
+
durationMs: number;
|
|
57
57
|
|
|
58
58
|
/** Errors encountered (non-fatal) */
|
|
59
59
|
errors?: string[];
|
|
@@ -68,10 +68,10 @@ export interface ExecutionResult<T = any> {
|
|
|
68
68
|
function evaluateRule<T>(_entity: T, _rule: FilterRule): RuleResult {
|
|
69
69
|
throw new Error('Not implemented - BEAD: fund-your-startup-a0b8. evaluateRule must import from rule evaluator.');
|
|
70
70
|
// return {
|
|
71
|
-
//
|
|
71
|
+
// fieldPath: _rule.fieldPath,
|
|
72
72
|
// operator: _rule.operator,
|
|
73
73
|
// value: _rule.value,
|
|
74
|
-
//
|
|
74
|
+
// actualValue: undefined,
|
|
75
75
|
// matched: false,
|
|
76
76
|
// error: 'Evaluator not implemented yet',
|
|
77
77
|
// };
|
|
@@ -115,9 +115,9 @@ export class FunnelEngine<T = any> {
|
|
|
115
115
|
const results: FunnelResult<T>[] = entities.map(entity => ({
|
|
116
116
|
entity,
|
|
117
117
|
matched: true, // Start as matched, exclude as needed
|
|
118
|
-
|
|
118
|
+
accumulatedTags: [],
|
|
119
119
|
context: {},
|
|
120
|
-
|
|
120
|
+
stageResults: [],
|
|
121
121
|
}));
|
|
122
122
|
|
|
123
123
|
const stageStats: Record<string, StageStats> = {};
|
|
@@ -131,18 +131,18 @@ export class FunnelEngine<T = any> {
|
|
|
131
131
|
const stageStartTime = Date.now();
|
|
132
132
|
|
|
133
133
|
// Track entities entering this stage
|
|
134
|
-
const inputEntities = results.filter(r => r.matched && !r.
|
|
134
|
+
const inputEntities = results.filter(r => r.matched && !r.excludedAtStage);
|
|
135
135
|
|
|
136
136
|
const stats: StageStats = {
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
137
|
+
stageId: stage.id,
|
|
138
|
+
stageName: stage.name,
|
|
139
|
+
inputCount: inputEntities.length,
|
|
140
|
+
matchedCount: 0,
|
|
141
|
+
notMatchedCount: 0,
|
|
142
|
+
excludedCount: 0,
|
|
143
|
+
taggedCount: 0,
|
|
144
|
+
continuedCount: 0,
|
|
145
|
+
errorCount: 0,
|
|
146
146
|
};
|
|
147
147
|
|
|
148
148
|
// Process each entity through this stage
|
|
@@ -151,46 +151,46 @@ export class FunnelEngine<T = any> {
|
|
|
151
151
|
const stageResult = this.processStage(stage, result.entity);
|
|
152
152
|
|
|
153
153
|
// Update entity result
|
|
154
|
-
result.
|
|
154
|
+
result.stageResults!.push(stageResult);
|
|
155
155
|
|
|
156
156
|
// Apply stage result to entity
|
|
157
157
|
if (stageResult.matched) {
|
|
158
|
-
stats.
|
|
158
|
+
stats.matchedCount++;
|
|
159
159
|
} else {
|
|
160
|
-
stats.
|
|
160
|
+
stats.notMatchedCount++;
|
|
161
161
|
}
|
|
162
162
|
|
|
163
|
-
if (stageResult.
|
|
164
|
-
result.
|
|
165
|
-
stats.
|
|
163
|
+
if (stageResult.tagsAdded && stageResult.tagsAdded.length > 0) {
|
|
164
|
+
result.accumulatedTags.push(...stageResult.tagsAdded);
|
|
165
|
+
stats.taggedCount++;
|
|
166
166
|
}
|
|
167
167
|
|
|
168
|
-
if (stageResult.
|
|
169
|
-
result.context = { ...result.context, ...stageResult.
|
|
168
|
+
if (stageResult.contextAdded) {
|
|
169
|
+
result.context = { ...result.context, ...stageResult.contextAdded };
|
|
170
170
|
}
|
|
171
171
|
|
|
172
172
|
if (stageResult.excluded) {
|
|
173
173
|
result.matched = false;
|
|
174
|
-
result.
|
|
175
|
-
stats.
|
|
174
|
+
result.excludedAtStage = stage.id;
|
|
175
|
+
stats.excludedCount++;
|
|
176
176
|
} else if (stageResult.continued) {
|
|
177
|
-
stats.
|
|
177
|
+
stats.continuedCount++;
|
|
178
178
|
}
|
|
179
179
|
} catch (error) {
|
|
180
|
-
stats.
|
|
180
|
+
stats.errorCount!++;
|
|
181
181
|
errors.push(`Stage ${stage.name}: ${error instanceof Error ? error.message : String(error)}`);
|
|
182
182
|
}
|
|
183
183
|
}
|
|
184
184
|
|
|
185
|
-
stats.
|
|
185
|
+
stats.durationMs = Date.now() - stageStartTime;
|
|
186
186
|
stageStats[stage.id] = stats;
|
|
187
187
|
}
|
|
188
188
|
|
|
189
189
|
// Apply completion tags to matched entities
|
|
190
|
-
if (funnel.
|
|
190
|
+
if (funnel.completionTags && funnel.completionTags.length > 0) {
|
|
191
191
|
for (const result of results) {
|
|
192
192
|
if (result.matched) {
|
|
193
|
-
result.
|
|
193
|
+
result.accumulatedTags.push(...funnel.completionTags);
|
|
194
194
|
}
|
|
195
195
|
}
|
|
196
196
|
}
|
|
@@ -200,17 +200,17 @@ export class FunnelEngine<T = any> {
|
|
|
200
200
|
const excluded = results.filter(r => !r.matched);
|
|
201
201
|
|
|
202
202
|
// Count unique tagged entities
|
|
203
|
-
const totalTagged = results.filter(r => r.
|
|
203
|
+
const totalTagged = results.filter(r => r.accumulatedTags.length > 0).length;
|
|
204
204
|
|
|
205
205
|
return {
|
|
206
206
|
matched,
|
|
207
207
|
excluded,
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
208
|
+
totalInput: entities.length,
|
|
209
|
+
totalMatched: matched.length,
|
|
210
|
+
totalExcluded: excluded.length,
|
|
211
|
+
totalTagged: totalTagged,
|
|
212
|
+
stageStats: stageStats,
|
|
213
|
+
durationMs: Date.now() - startTime,
|
|
214
214
|
errors: errors.length > 0 ? errors : undefined,
|
|
215
215
|
};
|
|
216
216
|
}
|
|
@@ -227,10 +227,10 @@ export class FunnelEngine<T = any> {
|
|
|
227
227
|
const ruleResults: RuleResult[] = [];
|
|
228
228
|
let matched = false;
|
|
229
229
|
|
|
230
|
-
if (stage.
|
|
230
|
+
if (stage.customEvaluator) {
|
|
231
231
|
// Use custom evaluator if provided (highest priority)
|
|
232
232
|
try {
|
|
233
|
-
matched = stage.
|
|
233
|
+
matched = stage.customEvaluator(entity);
|
|
234
234
|
} catch (error) {
|
|
235
235
|
matched = false;
|
|
236
236
|
}
|
|
@@ -245,7 +245,7 @@ export class FunnelEngine<T = any> {
|
|
|
245
245
|
}
|
|
246
246
|
|
|
247
247
|
// Apply filter logic (AND/OR)
|
|
248
|
-
if (stage.
|
|
248
|
+
if (stage.filterLogic === 'AND') {
|
|
249
249
|
// All rules must match
|
|
250
250
|
matched = ruleResults.every(r => r.matched);
|
|
251
251
|
} else {
|
|
@@ -262,20 +262,20 @@ export class FunnelEngine<T = any> {
|
|
|
262
262
|
let continued = false;
|
|
263
263
|
|
|
264
264
|
if (matched) {
|
|
265
|
-
action = stage.
|
|
265
|
+
action = stage.matchAction;
|
|
266
266
|
|
|
267
267
|
// Add match tags
|
|
268
|
-
if (stage.
|
|
269
|
-
tagsAdded = [...stage.
|
|
268
|
+
if (stage.matchTags && stage.matchTags.length > 0) {
|
|
269
|
+
tagsAdded = [...stage.matchTags];
|
|
270
270
|
}
|
|
271
271
|
|
|
272
272
|
// Add match context
|
|
273
|
-
if (stage.
|
|
274
|
-
contextAdded = stage.
|
|
273
|
+
if (stage.matchContext) {
|
|
274
|
+
contextAdded = stage.matchContext;
|
|
275
275
|
}
|
|
276
276
|
|
|
277
277
|
// Apply match action
|
|
278
|
-
switch (stage.
|
|
278
|
+
switch (stage.matchAction) {
|
|
279
279
|
case 'continue':
|
|
280
280
|
continued = true;
|
|
281
281
|
break;
|
|
@@ -293,15 +293,15 @@ export class FunnelEngine<T = any> {
|
|
|
293
293
|
break;
|
|
294
294
|
}
|
|
295
295
|
} else {
|
|
296
|
-
action = stage.
|
|
296
|
+
action = stage.noMatchAction;
|
|
297
297
|
|
|
298
298
|
// Add no-match tags
|
|
299
|
-
if (stage.
|
|
300
|
-
tagsAdded = [...stage.
|
|
299
|
+
if (stage.noMatchTags && stage.noMatchTags.length > 0) {
|
|
300
|
+
tagsAdded = [...stage.noMatchTags];
|
|
301
301
|
}
|
|
302
302
|
|
|
303
303
|
// Apply no-match action
|
|
304
|
-
switch (stage.
|
|
304
|
+
switch (stage.noMatchAction) {
|
|
305
305
|
case 'continue':
|
|
306
306
|
continued = true;
|
|
307
307
|
break;
|
|
@@ -315,13 +315,13 @@ export class FunnelEngine<T = any> {
|
|
|
315
315
|
}
|
|
316
316
|
|
|
317
317
|
return {
|
|
318
|
-
|
|
319
|
-
|
|
318
|
+
stageId: stage.id,
|
|
319
|
+
stageName: stage.name,
|
|
320
320
|
matched,
|
|
321
|
-
|
|
321
|
+
ruleResults: ruleResults.length > 0 ? ruleResults : undefined,
|
|
322
322
|
action,
|
|
323
|
-
|
|
324
|
-
|
|
323
|
+
tagsAdded: tagsAdded.length > 0 ? tagsAdded : undefined,
|
|
324
|
+
contextAdded: contextAdded,
|
|
325
325
|
excluded,
|
|
326
326
|
continued,
|
|
327
327
|
};
|
|
@@ -27,7 +27,7 @@ interface Investor {
|
|
|
27
27
|
geography: string[];
|
|
28
28
|
};
|
|
29
29
|
tags: string[];
|
|
30
|
-
|
|
30
|
+
createdAt: Date;
|
|
31
31
|
}
|
|
32
32
|
|
|
33
33
|
const investors: Investor[] = [
|
|
@@ -41,7 +41,7 @@ const investors: Investor[] = [
|
|
|
41
41
|
geography: ['US', 'Canada'],
|
|
42
42
|
},
|
|
43
43
|
tags: ['qualified', 'active'],
|
|
44
|
-
|
|
44
|
+
createdAt: new Date('2024-01-15'),
|
|
45
45
|
},
|
|
46
46
|
{
|
|
47
47
|
name: 'Jane Smith',
|
|
@@ -53,7 +53,7 @@ const investors: Investor[] = [
|
|
|
53
53
|
geography: ['US'],
|
|
54
54
|
},
|
|
55
55
|
tags: ['active'],
|
|
56
|
-
|
|
56
|
+
createdAt: new Date('2024-02-01'),
|
|
57
57
|
},
|
|
58
58
|
{
|
|
59
59
|
name: 'Bob Johnson',
|
|
@@ -65,14 +65,14 @@ const investors: Investor[] = [
|
|
|
65
65
|
geography: ['US', 'Europe', 'Asia'],
|
|
66
66
|
},
|
|
67
67
|
tags: ['qualified', 'premium'],
|
|
68
|
-
|
|
68
|
+
createdAt: new Date('2024-01-20'),
|
|
69
69
|
},
|
|
70
70
|
];
|
|
71
71
|
|
|
72
72
|
// Example 1a: Single rule evaluation
|
|
73
73
|
console.log('=== Example 1a: Single Rule ===');
|
|
74
74
|
const rule1: FilterRule = {
|
|
75
|
-
|
|
75
|
+
fieldPath: 'firm.aum',
|
|
76
76
|
operator: 'gte',
|
|
77
77
|
value: 100000000,
|
|
78
78
|
};
|
|
@@ -85,9 +85,9 @@ investors.forEach(investor => {
|
|
|
85
85
|
// Example 1b: Multiple rules with AND logic
|
|
86
86
|
console.log('\n=== Example 1b: Multiple Rules (AND) ===');
|
|
87
87
|
const rules1: FilterRule[] = [
|
|
88
|
-
{
|
|
89
|
-
{
|
|
90
|
-
{
|
|
88
|
+
{ fieldPath: 'firm.stage', operator: 'in', value: ['Series A', 'Series B'] },
|
|
89
|
+
{ fieldPath: 'firm.aum', operator: 'gte', value: 100000000 },
|
|
90
|
+
{ fieldPath: 'tags', operator: 'has_tag', value: 'qualified' },
|
|
91
91
|
];
|
|
92
92
|
|
|
93
93
|
const qualified = filterEntities(investors, rules1, 'AND');
|
|
@@ -99,9 +99,9 @@ const result1 = evaluateRulesWithResults(investors[0], rules1, 'AND');
|
|
|
99
99
|
console.log('Evaluation for John Doe:');
|
|
100
100
|
console.log('Overall match:', result1.matched);
|
|
101
101
|
console.log('Rule results:');
|
|
102
|
-
result1.
|
|
103
|
-
console.log(` - ${r.
|
|
104
|
-
console.log(` Actual: ${JSON.stringify(r.
|
|
102
|
+
result1.ruleResults.forEach(r => {
|
|
103
|
+
console.log(` - ${r.fieldPath} ${r.operator} ${JSON.stringify(r.value)}`);
|
|
104
|
+
console.log(` Actual: ${JSON.stringify(r.actualValue)}, Matched: ${r.matched}`);
|
|
105
105
|
});
|
|
106
106
|
|
|
107
107
|
// ============================================================================
|
|
@@ -151,7 +151,7 @@ const recipes: Recipe[] = [
|
|
|
151
151
|
// Example 2a: Filter by dietary restrictions
|
|
152
152
|
console.log('\n=== Example 2: Recipe Filtering ===');
|
|
153
153
|
const veganRules: FilterRule[] = [
|
|
154
|
-
{
|
|
154
|
+
{ fieldPath: 'dietary', operator: 'has_tag', value: 'vegan' },
|
|
155
155
|
];
|
|
156
156
|
|
|
157
157
|
const veganRecipes = filterEntities(recipes, veganRules);
|
|
@@ -159,9 +159,9 @@ console.log('Vegan recipes:', veganRecipes.map(r => r.name));
|
|
|
159
159
|
|
|
160
160
|
// Example 2b: Filter by multiple criteria (quick meals)
|
|
161
161
|
const quickMealRules: FilterRule[] = [
|
|
162
|
-
{
|
|
163
|
-
{
|
|
164
|
-
{
|
|
162
|
+
{ fieldPath: 'prep_time_minutes', operator: 'lte', value: 30 },
|
|
163
|
+
{ fieldPath: 'difficulty', operator: 'in', value: ['easy', 'medium'] },
|
|
164
|
+
{ fieldPath: 'rating', operator: 'gte', value: 4.0 },
|
|
165
165
|
];
|
|
166
166
|
|
|
167
167
|
const quickMeals = filterEntities(recipes, quickMealRules, 'AND');
|
|
@@ -222,9 +222,9 @@ const repos: GitHubRepo[] = [
|
|
|
222
222
|
// Example 3a: Find popular active repos
|
|
223
223
|
console.log('\n=== Example 3: GitHub Repos ===');
|
|
224
224
|
const popularRepoRules: FilterRule[] = [
|
|
225
|
-
{
|
|
226
|
-
{
|
|
227
|
-
{
|
|
225
|
+
{ fieldPath: 'stars', operator: 'gte', value: 100 },
|
|
226
|
+
{ fieldPath: 'is_archived', operator: 'is_false', value: null },
|
|
227
|
+
{ fieldPath: 'last_updated', operator: 'gte', value: new Date('2024-01-01') },
|
|
228
228
|
];
|
|
229
229
|
|
|
230
230
|
const popularRepos = filterEntities(repos, popularRepoRules, 'AND');
|
|
@@ -232,8 +232,8 @@ console.log('Popular active repos:', popularRepos.map(r => r.name));
|
|
|
232
232
|
|
|
233
233
|
// Example 3b: Find TypeScript projects with React
|
|
234
234
|
const tsReactRules: FilterRule[] = [
|
|
235
|
-
{
|
|
236
|
-
{
|
|
235
|
+
{ fieldPath: 'language', operator: 'eq', value: 'TypeScript' },
|
|
236
|
+
{ fieldPath: 'topics', operator: 'has_tag', value: 'react' },
|
|
237
237
|
];
|
|
238
238
|
|
|
239
239
|
const tsReactRepos = filterEntities(repos, tsReactRules, 'AND');
|
|
@@ -247,7 +247,7 @@ console.log('\n=== Example 4: Advanced Patterns ===');
|
|
|
247
247
|
|
|
248
248
|
// Pattern 1: Negation
|
|
249
249
|
const notArchivedRule: FilterRule = {
|
|
250
|
-
|
|
250
|
+
fieldPath: 'is_archived',
|
|
251
251
|
operator: 'is_true',
|
|
252
252
|
value: null,
|
|
253
253
|
negate: true, // Negate the result
|
|
@@ -258,7 +258,7 @@ console.log('Active repos (using negate):', activeRepos.map(r => r.name));
|
|
|
258
258
|
|
|
259
259
|
// Pattern 2: Complex string matching
|
|
260
260
|
const emailDomainRule: FilterRule = {
|
|
261
|
-
|
|
261
|
+
fieldPath: 'email',
|
|
262
262
|
operator: 'matches',
|
|
263
263
|
value: '@acme\\.vc$', // Matches emails ending with @acme.vc
|
|
264
264
|
};
|
|
@@ -268,8 +268,8 @@ console.log('Acme VC investors:', acmeInvestors.map(i => i.name));
|
|
|
268
268
|
|
|
269
269
|
// Pattern 3: OR logic (match any condition)
|
|
270
270
|
const flexibleStageRules: FilterRule[] = [
|
|
271
|
-
{
|
|
272
|
-
{
|
|
271
|
+
{ fieldPath: 'firm.stage', operator: 'eq', value: 'Series A' },
|
|
272
|
+
{ fieldPath: 'firm.aum', operator: 'gte', value: 500000000 }, // OR very large fund
|
|
273
273
|
];
|
|
274
274
|
|
|
275
275
|
const flexibleMatch = filterEntities(investors, flexibleStageRules, 'OR');
|
|
@@ -282,11 +282,11 @@ investorsWithoutEmail.push({
|
|
|
282
282
|
email: null as any,
|
|
283
283
|
firm: { name: 'Test', stage: 'Seed', aum: 0, geography: [] },
|
|
284
284
|
tags: [],
|
|
285
|
-
|
|
285
|
+
createdAt: new Date(),
|
|
286
286
|
});
|
|
287
287
|
|
|
288
288
|
const hasEmailRule: FilterRule = {
|
|
289
|
-
|
|
289
|
+
fieldPath: 'email',
|
|
290
290
|
operator: 'isnotnull',
|
|
291
291
|
value: null,
|
|
292
292
|
};
|
|
@@ -296,7 +296,7 @@ console.log('Investors with email:', withEmail.map(i => i.name));
|
|
|
296
296
|
|
|
297
297
|
// Pattern 5: Array operations
|
|
298
298
|
const multiGeographyRule: FilterRule = {
|
|
299
|
-
|
|
299
|
+
fieldPath: 'firm.geography',
|
|
300
300
|
operator: 'has_all',
|
|
301
301
|
value: ['US', 'Europe'],
|
|
302
302
|
};
|
|
@@ -329,22 +329,22 @@ function simulateFunnel<T>(entities: T[], stages: Array<{ name: string; rules: F
|
|
|
329
329
|
const investorFunnelStages = [
|
|
330
330
|
{
|
|
331
331
|
name: 'Active & Not Archived',
|
|
332
|
-
rules: [{
|
|
332
|
+
rules: [{ fieldPath: 'tags', operator: 'has_tag', value: 'active' } as FilterRule],
|
|
333
333
|
logic: 'AND' as const,
|
|
334
334
|
},
|
|
335
335
|
{
|
|
336
336
|
name: 'Series A+ Stage',
|
|
337
|
-
rules: [{
|
|
337
|
+
rules: [{ fieldPath: 'firm.stage', operator: 'in', value: ['Series A', 'Series B', 'Series C'] } as FilterRule],
|
|
338
338
|
logic: 'AND' as const,
|
|
339
339
|
},
|
|
340
340
|
{
|
|
341
341
|
name: 'Minimum AUM $100M',
|
|
342
|
-
rules: [{
|
|
342
|
+
rules: [{ fieldPath: 'firm.aum', operator: 'gte', value: 100000000 } as FilterRule],
|
|
343
343
|
logic: 'AND' as const,
|
|
344
344
|
},
|
|
345
345
|
{
|
|
346
346
|
name: 'Qualified Tag',
|
|
347
|
-
rules: [{
|
|
347
|
+
rules: [{ fieldPath: 'tags', operator: 'has_tag', value: 'qualified' } as FilterRule],
|
|
348
348
|
logic: 'AND' as const,
|
|
349
349
|
},
|
|
350
350
|
];
|