@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.test.ts
CHANGED
|
@@ -116,20 +116,20 @@ describe('FunnelEngine - Empty Funnel', () => {
|
|
|
116
116
|
id: 'funnel-1',
|
|
117
117
|
name: 'Empty Funnel',
|
|
118
118
|
status: 'active',
|
|
119
|
-
|
|
119
|
+
inputType: 'any',
|
|
120
120
|
stages: [],
|
|
121
|
-
|
|
122
|
-
|
|
121
|
+
createdAt: new Date().toISOString(),
|
|
122
|
+
updatedAt: new Date().toISOString(),
|
|
123
123
|
};
|
|
124
124
|
|
|
125
125
|
const result = engine.execute(funnel, entities);
|
|
126
126
|
|
|
127
|
-
expect(result.
|
|
128
|
-
expect(result.
|
|
129
|
-
expect(result.
|
|
127
|
+
expect(result.totalInput).toBe(3);
|
|
128
|
+
expect(result.totalMatched).toBe(3); // No exclusions
|
|
129
|
+
expect(result.totalExcluded).toBe(0);
|
|
130
130
|
expect(result.matched.length).toBe(3);
|
|
131
131
|
expect(result.excluded.length).toBe(0);
|
|
132
|
-
expect(Object.keys(result.
|
|
132
|
+
expect(Object.keys(result.stageStats).length).toBe(0);
|
|
133
133
|
});
|
|
134
134
|
|
|
135
135
|
it('should handle empty entity array', () => {
|
|
@@ -139,27 +139,27 @@ describe('FunnelEngine - Empty Funnel', () => {
|
|
|
139
139
|
id: 'funnel-1',
|
|
140
140
|
name: 'Test Funnel',
|
|
141
141
|
status: 'active',
|
|
142
|
-
|
|
142
|
+
inputType: 'any',
|
|
143
143
|
stages: [
|
|
144
144
|
{
|
|
145
145
|
id: 'stage-1',
|
|
146
146
|
order: 0,
|
|
147
147
|
name: 'Filter Stage',
|
|
148
|
-
|
|
148
|
+
filterLogic: 'AND',
|
|
149
149
|
rules: [],
|
|
150
|
-
|
|
151
|
-
|
|
150
|
+
matchAction: 'continue',
|
|
151
|
+
noMatchAction: 'exclude',
|
|
152
152
|
},
|
|
153
153
|
],
|
|
154
|
-
|
|
155
|
-
|
|
154
|
+
createdAt: new Date().toISOString(),
|
|
155
|
+
updatedAt: new Date().toISOString(),
|
|
156
156
|
};
|
|
157
157
|
|
|
158
158
|
const result = engine.execute(funnel, []);
|
|
159
159
|
|
|
160
|
-
expect(result.
|
|
161
|
-
expect(result.
|
|
162
|
-
expect(result.
|
|
160
|
+
expect(result.totalInput).toBe(0);
|
|
161
|
+
expect(result.totalMatched).toBe(0);
|
|
162
|
+
expect(result.totalExcluded).toBe(0);
|
|
163
163
|
});
|
|
164
164
|
});
|
|
165
165
|
|
|
@@ -176,31 +176,31 @@ describe('FunnelEngine - Single Stage', () => {
|
|
|
176
176
|
id: 'funnel-1',
|
|
177
177
|
name: 'Single Stage Funnel',
|
|
178
178
|
status: 'active',
|
|
179
|
-
|
|
179
|
+
inputType: 'any',
|
|
180
180
|
stages: [
|
|
181
181
|
{
|
|
182
182
|
id: 'stage-1',
|
|
183
183
|
order: 0,
|
|
184
184
|
name: 'Pass All',
|
|
185
|
-
|
|
185
|
+
filterLogic: 'AND',
|
|
186
186
|
rules: [], // No rules = always match
|
|
187
|
-
|
|
188
|
-
|
|
187
|
+
matchAction: 'continue',
|
|
188
|
+
noMatchAction: 'exclude',
|
|
189
189
|
},
|
|
190
190
|
],
|
|
191
|
-
|
|
192
|
-
|
|
191
|
+
createdAt: new Date().toISOString(),
|
|
192
|
+
updatedAt: new Date().toISOString(),
|
|
193
193
|
};
|
|
194
194
|
|
|
195
195
|
const result = engine.execute(funnel, entities);
|
|
196
196
|
|
|
197
|
-
expect(result.
|
|
198
|
-
expect(result.
|
|
199
|
-
expect(result.
|
|
200
|
-
expect(result.
|
|
197
|
+
expect(result.totalMatched).toBe(3);
|
|
198
|
+
expect(result.stageStats['stage-1'].inputCount).toBe(3);
|
|
199
|
+
expect(result.stageStats['stage-1'].matchedCount).toBe(3);
|
|
200
|
+
expect(result.stageStats['stage-1'].notMatchedCount).toBe(0);
|
|
201
201
|
});
|
|
202
202
|
|
|
203
|
-
it('should apply
|
|
203
|
+
it('should apply matchAction: output (stop processing)', () => {
|
|
204
204
|
const engine = new FunnelEngine<MockInvestor>();
|
|
205
205
|
const entities = createMockInvestors();
|
|
206
206
|
|
|
@@ -208,30 +208,30 @@ describe('FunnelEngine - Single Stage', () => {
|
|
|
208
208
|
id: 'funnel-1',
|
|
209
209
|
name: 'Output Stage',
|
|
210
210
|
status: 'active',
|
|
211
|
-
|
|
211
|
+
inputType: 'any',
|
|
212
212
|
stages: [
|
|
213
213
|
{
|
|
214
214
|
id: 'stage-1',
|
|
215
215
|
order: 0,
|
|
216
216
|
name: 'Output All',
|
|
217
|
-
|
|
217
|
+
filterLogic: 'AND',
|
|
218
218
|
rules: [],
|
|
219
|
-
|
|
220
|
-
|
|
219
|
+
matchAction: 'output', // Stop processing
|
|
220
|
+
noMatchAction: 'exclude',
|
|
221
221
|
},
|
|
222
222
|
],
|
|
223
|
-
|
|
224
|
-
|
|
223
|
+
createdAt: new Date().toISOString(),
|
|
224
|
+
updatedAt: new Date().toISOString(),
|
|
225
225
|
};
|
|
226
226
|
|
|
227
227
|
const result = engine.execute(funnel, entities);
|
|
228
228
|
|
|
229
|
-
expect(result.
|
|
230
|
-
expect(result.matched.every(r => r.
|
|
231
|
-
expect(result.matched.every(r => r.
|
|
229
|
+
expect(result.totalMatched).toBe(3);
|
|
230
|
+
expect(result.matched.every(r => r.stageResults?.length === 1)).toBe(true);
|
|
231
|
+
expect(result.matched.every(r => r.stageResults![0].continued === false)).toBe(true);
|
|
232
232
|
});
|
|
233
233
|
|
|
234
|
-
it('should apply
|
|
234
|
+
it('should apply matchAction: tag (add tags and exclude)', () => {
|
|
235
235
|
const engine = new FunnelEngine<MockInvestor>();
|
|
236
236
|
const entities = createMockInvestors();
|
|
237
237
|
|
|
@@ -239,31 +239,31 @@ describe('FunnelEngine - Single Stage', () => {
|
|
|
239
239
|
id: 'funnel-1',
|
|
240
240
|
name: 'Tag Stage',
|
|
241
241
|
status: 'active',
|
|
242
|
-
|
|
242
|
+
inputType: 'any',
|
|
243
243
|
stages: [
|
|
244
244
|
{
|
|
245
245
|
id: 'stage-1',
|
|
246
246
|
order: 0,
|
|
247
247
|
name: 'Tag All',
|
|
248
|
-
|
|
248
|
+
filterLogic: 'AND',
|
|
249
249
|
rules: [],
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
250
|
+
matchAction: 'tag',
|
|
251
|
+
noMatchAction: 'exclude',
|
|
252
|
+
matchTags: ['qualified'],
|
|
253
253
|
},
|
|
254
254
|
],
|
|
255
|
-
|
|
256
|
-
|
|
255
|
+
createdAt: new Date().toISOString(),
|
|
256
|
+
updatedAt: new Date().toISOString(),
|
|
257
257
|
};
|
|
258
258
|
|
|
259
259
|
const result = engine.execute(funnel, entities);
|
|
260
260
|
|
|
261
|
-
// Entities are tagged but excluded (
|
|
262
|
-
expect(result.
|
|
263
|
-
expect(result.excluded.every(r => r.
|
|
261
|
+
// Entities are tagged but excluded (matchAction: tag stops processing)
|
|
262
|
+
expect(result.totalExcluded).toBe(3);
|
|
263
|
+
expect(result.excluded.every(r => r.accumulatedTags.includes('qualified'))).toBe(true);
|
|
264
264
|
});
|
|
265
265
|
|
|
266
|
-
it('should apply
|
|
266
|
+
it('should apply matchAction: tag_continue (add tags and continue)', () => {
|
|
267
267
|
const engine = new FunnelEngine<MockInvestor>();
|
|
268
268
|
const entities = createMockInvestors();
|
|
269
269
|
|
|
@@ -271,31 +271,31 @@ describe('FunnelEngine - Single Stage', () => {
|
|
|
271
271
|
id: 'funnel-1',
|
|
272
272
|
name: 'Tag Continue Stage',
|
|
273
273
|
status: 'active',
|
|
274
|
-
|
|
274
|
+
inputType: 'any',
|
|
275
275
|
stages: [
|
|
276
276
|
{
|
|
277
277
|
id: 'stage-1',
|
|
278
278
|
order: 0,
|
|
279
279
|
name: 'Tag and Continue',
|
|
280
|
-
|
|
280
|
+
filterLogic: 'AND',
|
|
281
281
|
rules: [],
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
282
|
+
matchAction: 'tag_continue',
|
|
283
|
+
noMatchAction: 'exclude',
|
|
284
|
+
matchTags: ['processed'],
|
|
285
285
|
},
|
|
286
286
|
],
|
|
287
|
-
|
|
288
|
-
|
|
287
|
+
createdAt: new Date().toISOString(),
|
|
288
|
+
updatedAt: new Date().toISOString(),
|
|
289
289
|
};
|
|
290
290
|
|
|
291
291
|
const result = engine.execute(funnel, entities);
|
|
292
292
|
|
|
293
|
-
expect(result.
|
|
294
|
-
expect(result.matched.every(r => r.
|
|
295
|
-
expect(result.matched.every(r => r.
|
|
293
|
+
expect(result.totalMatched).toBe(3);
|
|
294
|
+
expect(result.matched.every(r => r.accumulatedTags.includes('processed'))).toBe(true);
|
|
295
|
+
expect(result.matched.every(r => r.stageResults![0].continued === true)).toBe(true);
|
|
296
296
|
});
|
|
297
297
|
|
|
298
|
-
it('should apply
|
|
298
|
+
it('should apply noMatchAction: exclude', () => {
|
|
299
299
|
const engine = new FunnelEngine<MockInvestor>();
|
|
300
300
|
const entities = createMockInvestors();
|
|
301
301
|
|
|
@@ -303,30 +303,30 @@ describe('FunnelEngine - Single Stage', () => {
|
|
|
303
303
|
id: 'funnel-1',
|
|
304
304
|
name: 'Exclude Stage',
|
|
305
305
|
status: 'active',
|
|
306
|
-
|
|
306
|
+
inputType: 'any',
|
|
307
307
|
stages: [
|
|
308
308
|
{
|
|
309
309
|
id: 'stage-1',
|
|
310
310
|
order: 0,
|
|
311
311
|
name: 'Never Match',
|
|
312
|
-
|
|
312
|
+
filterLogic: 'AND',
|
|
313
313
|
rules: [],
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
314
|
+
matchAction: 'continue',
|
|
315
|
+
noMatchAction: 'exclude',
|
|
316
|
+
customEvaluator: () => false, // Always false
|
|
317
317
|
},
|
|
318
318
|
],
|
|
319
|
-
|
|
320
|
-
|
|
319
|
+
createdAt: new Date().toISOString(),
|
|
320
|
+
updatedAt: new Date().toISOString(),
|
|
321
321
|
};
|
|
322
322
|
|
|
323
323
|
const result = engine.execute(funnel, entities);
|
|
324
324
|
|
|
325
|
-
expect(result.
|
|
326
|
-
expect(result.excluded.every(r => r.
|
|
325
|
+
expect(result.totalExcluded).toBe(3);
|
|
326
|
+
expect(result.excluded.every(r => r.excludedAtStage === 'stage-1')).toBe(true);
|
|
327
327
|
});
|
|
328
328
|
|
|
329
|
-
it('should apply
|
|
329
|
+
it('should apply noMatchAction: tag_exclude', () => {
|
|
330
330
|
const engine = new FunnelEngine<MockInvestor>();
|
|
331
331
|
const entities = createMockInvestors();
|
|
332
332
|
|
|
@@ -334,31 +334,31 @@ describe('FunnelEngine - Single Stage', () => {
|
|
|
334
334
|
id: 'funnel-1',
|
|
335
335
|
name: 'Tag Exclude Stage',
|
|
336
336
|
status: 'active',
|
|
337
|
-
|
|
337
|
+
inputType: 'any',
|
|
338
338
|
stages: [
|
|
339
339
|
{
|
|
340
340
|
id: 'stage-1',
|
|
341
341
|
order: 0,
|
|
342
342
|
name: 'Tag on No Match',
|
|
343
|
-
|
|
343
|
+
filterLogic: 'AND',
|
|
344
344
|
rules: [],
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
345
|
+
matchAction: 'continue',
|
|
346
|
+
noMatchAction: 'tag_exclude',
|
|
347
|
+
noMatchTags: ['rejected'],
|
|
348
|
+
customEvaluator: () => false,
|
|
349
349
|
},
|
|
350
350
|
],
|
|
351
|
-
|
|
352
|
-
|
|
351
|
+
createdAt: new Date().toISOString(),
|
|
352
|
+
updatedAt: new Date().toISOString(),
|
|
353
353
|
};
|
|
354
354
|
|
|
355
355
|
const result = engine.execute(funnel, entities);
|
|
356
356
|
|
|
357
|
-
expect(result.
|
|
358
|
-
expect(result.excluded.every(r => r.
|
|
357
|
+
expect(result.totalExcluded).toBe(3);
|
|
358
|
+
expect(result.excluded.every(r => r.accumulatedTags.includes('rejected'))).toBe(true);
|
|
359
359
|
});
|
|
360
360
|
|
|
361
|
-
it('should add
|
|
361
|
+
it('should add matchContext when rules match', () => {
|
|
362
362
|
const engine = new FunnelEngine<MockInvestor>();
|
|
363
363
|
const entities = createMockInvestors();
|
|
364
364
|
|
|
@@ -366,21 +366,21 @@ describe('FunnelEngine - Single Stage', () => {
|
|
|
366
366
|
id: 'funnel-1',
|
|
367
367
|
name: 'Context Stage',
|
|
368
368
|
status: 'active',
|
|
369
|
-
|
|
369
|
+
inputType: 'any',
|
|
370
370
|
stages: [
|
|
371
371
|
{
|
|
372
372
|
id: 'stage-1',
|
|
373
373
|
order: 0,
|
|
374
374
|
name: 'Add Context',
|
|
375
|
-
|
|
375
|
+
filterLogic: 'AND',
|
|
376
376
|
rules: [],
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
377
|
+
matchAction: 'continue',
|
|
378
|
+
noMatchAction: 'exclude',
|
|
379
|
+
matchContext: { tier: 'premium', score: 100 },
|
|
380
380
|
},
|
|
381
381
|
],
|
|
382
|
-
|
|
383
|
-
|
|
382
|
+
createdAt: new Date().toISOString(),
|
|
383
|
+
updatedAt: new Date().toISOString(),
|
|
384
384
|
};
|
|
385
385
|
|
|
386
386
|
const result = engine.execute(funnel, entities);
|
|
@@ -403,51 +403,51 @@ describe('FunnelEngine - Multi-Stage', () => {
|
|
|
403
403
|
id: 'funnel-1',
|
|
404
404
|
name: 'Multi-Stage Funnel',
|
|
405
405
|
status: 'active',
|
|
406
|
-
|
|
406
|
+
inputType: 'any',
|
|
407
407
|
stages: [
|
|
408
408
|
{
|
|
409
409
|
id: 'stage-1',
|
|
410
410
|
order: 0,
|
|
411
411
|
name: 'Stage 1',
|
|
412
|
-
|
|
412
|
+
filterLogic: 'AND',
|
|
413
413
|
rules: [],
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
414
|
+
matchAction: 'continue',
|
|
415
|
+
noMatchAction: 'exclude',
|
|
416
|
+
matchTags: ['stage-1'],
|
|
417
417
|
},
|
|
418
418
|
{
|
|
419
419
|
id: 'stage-2',
|
|
420
420
|
order: 1,
|
|
421
421
|
name: 'Stage 2',
|
|
422
|
-
|
|
422
|
+
filterLogic: 'AND',
|
|
423
423
|
rules: [],
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
424
|
+
matchAction: 'continue',
|
|
425
|
+
noMatchAction: 'exclude',
|
|
426
|
+
matchTags: ['stage-2'],
|
|
427
427
|
},
|
|
428
428
|
{
|
|
429
429
|
id: 'stage-3',
|
|
430
430
|
order: 2,
|
|
431
431
|
name: 'Stage 3',
|
|
432
|
-
|
|
432
|
+
filterLogic: 'AND',
|
|
433
433
|
rules: [],
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
434
|
+
matchAction: 'output',
|
|
435
|
+
noMatchAction: 'exclude',
|
|
436
|
+
matchTags: ['stage-3'],
|
|
437
437
|
},
|
|
438
438
|
],
|
|
439
|
-
|
|
440
|
-
|
|
439
|
+
createdAt: new Date().toISOString(),
|
|
440
|
+
updatedAt: new Date().toISOString(),
|
|
441
441
|
};
|
|
442
442
|
|
|
443
443
|
const result = engine.execute(funnel, entities);
|
|
444
444
|
|
|
445
|
-
expect(result.
|
|
446
|
-
expect(result.matched.every(r => r.
|
|
445
|
+
expect(result.totalMatched).toBe(3);
|
|
446
|
+
expect(result.matched.every(r => r.stageResults?.length === 3)).toBe(true);
|
|
447
447
|
expect(result.matched.every(r =>
|
|
448
|
-
r.
|
|
449
|
-
r.
|
|
450
|
-
r.
|
|
448
|
+
r.accumulatedTags.includes('stage-1') &&
|
|
449
|
+
r.accumulatedTags.includes('stage-2') &&
|
|
450
|
+
r.accumulatedTags.includes('stage-3')
|
|
451
451
|
)).toBe(true);
|
|
452
452
|
});
|
|
453
453
|
|
|
@@ -459,51 +459,51 @@ describe('FunnelEngine - Multi-Stage', () => {
|
|
|
459
459
|
id: 'funnel-1',
|
|
460
460
|
name: 'Filtering Funnel',
|
|
461
461
|
status: 'active',
|
|
462
|
-
|
|
462
|
+
inputType: 'any',
|
|
463
463
|
stages: [
|
|
464
464
|
{
|
|
465
465
|
id: 'stage-1',
|
|
466
466
|
order: 0,
|
|
467
467
|
name: 'Pass All',
|
|
468
|
-
|
|
468
|
+
filterLogic: 'AND',
|
|
469
469
|
rules: [],
|
|
470
|
-
|
|
471
|
-
|
|
470
|
+
matchAction: 'continue',
|
|
471
|
+
noMatchAction: 'exclude',
|
|
472
472
|
},
|
|
473
473
|
{
|
|
474
474
|
id: 'stage-2',
|
|
475
475
|
order: 1,
|
|
476
476
|
name: 'Filter Some',
|
|
477
|
-
|
|
477
|
+
filterLogic: 'AND',
|
|
478
478
|
rules: [],
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
479
|
+
matchAction: 'continue',
|
|
480
|
+
noMatchAction: 'exclude',
|
|
481
|
+
customEvaluator: (entity) => entity.id === '1' || entity.id === '2',
|
|
482
482
|
},
|
|
483
483
|
{
|
|
484
484
|
id: 'stage-3',
|
|
485
485
|
order: 2,
|
|
486
486
|
name: 'Final Filter',
|
|
487
|
-
|
|
487
|
+
filterLogic: 'AND',
|
|
488
488
|
rules: [],
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
489
|
+
matchAction: 'output',
|
|
490
|
+
noMatchAction: 'exclude',
|
|
491
|
+
customEvaluator: (entity) => entity.id === '1',
|
|
492
492
|
},
|
|
493
493
|
],
|
|
494
|
-
|
|
495
|
-
|
|
494
|
+
createdAt: new Date().toISOString(),
|
|
495
|
+
updatedAt: new Date().toISOString(),
|
|
496
496
|
};
|
|
497
497
|
|
|
498
498
|
const result = engine.execute(funnel, entities);
|
|
499
499
|
|
|
500
|
-
expect(result.
|
|
500
|
+
expect(result.totalMatched).toBe(1);
|
|
501
501
|
expect(result.matched[0].entity.id).toBe('1');
|
|
502
|
-
expect(result.
|
|
502
|
+
expect(result.totalExcluded).toBe(2);
|
|
503
503
|
|
|
504
504
|
// Check exclusion stages
|
|
505
|
-
const excludedAtStage2 = result.excluded.find(r => r.
|
|
506
|
-
const excludedAtStage3 = result.excluded.find(r => r.
|
|
505
|
+
const excludedAtStage2 = result.excluded.find(r => r.excludedAtStage === 'stage-2');
|
|
506
|
+
const excludedAtStage3 = result.excluded.find(r => r.excludedAtStage === 'stage-3');
|
|
507
507
|
|
|
508
508
|
expect(excludedAtStage2?.entity.id).toBe('3');
|
|
509
509
|
expect(excludedAtStage3?.entity.id).toBe('2');
|
|
@@ -520,47 +520,47 @@ describe('FunnelEngine - Multi-Stage', () => {
|
|
|
520
520
|
id: 'funnel-1',
|
|
521
521
|
name: 'Unsorted Stages',
|
|
522
522
|
status: 'active',
|
|
523
|
-
|
|
523
|
+
inputType: 'any',
|
|
524
524
|
stages: [
|
|
525
525
|
{
|
|
526
526
|
id: 'stage-3',
|
|
527
527
|
order: 2,
|
|
528
528
|
name: 'Third Stage',
|
|
529
|
-
|
|
529
|
+
filterLogic: 'AND',
|
|
530
530
|
rules: [],
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
531
|
+
matchAction: 'continue',
|
|
532
|
+
noMatchAction: 'exclude',
|
|
533
|
+
matchTags: ['third'],
|
|
534
534
|
},
|
|
535
535
|
{
|
|
536
536
|
id: 'stage-1',
|
|
537
537
|
order: 0,
|
|
538
538
|
name: 'First Stage',
|
|
539
|
-
|
|
539
|
+
filterLogic: 'AND',
|
|
540
540
|
rules: [],
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
541
|
+
matchAction: 'continue',
|
|
542
|
+
noMatchAction: 'exclude',
|
|
543
|
+
matchTags: ['first'],
|
|
544
544
|
},
|
|
545
545
|
{
|
|
546
546
|
id: 'stage-2',
|
|
547
547
|
order: 1,
|
|
548
548
|
name: 'Second Stage',
|
|
549
|
-
|
|
549
|
+
filterLogic: 'AND',
|
|
550
550
|
rules: [],
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
551
|
+
matchAction: 'continue',
|
|
552
|
+
noMatchAction: 'exclude',
|
|
553
|
+
matchTags: ['second'],
|
|
554
554
|
},
|
|
555
555
|
],
|
|
556
|
-
|
|
557
|
-
|
|
556
|
+
createdAt: new Date().toISOString(),
|
|
557
|
+
updatedAt: new Date().toISOString(),
|
|
558
558
|
};
|
|
559
559
|
|
|
560
560
|
const result = engine.execute(funnel, entities);
|
|
561
561
|
|
|
562
562
|
// Tags should be added in order: first, second, third
|
|
563
|
-
expect(result.matched[0].
|
|
563
|
+
expect(result.matched[0].accumulatedTags).toEqual(['first', 'second', 'third']);
|
|
564
564
|
});
|
|
565
565
|
});
|
|
566
566
|
|
|
@@ -581,29 +581,29 @@ describe('FunnelEngine - AND Logic', () => {
|
|
|
581
581
|
id: 'funnel-1',
|
|
582
582
|
name: 'AND Logic Funnel',
|
|
583
583
|
status: 'active',
|
|
584
|
-
|
|
584
|
+
inputType: 'any',
|
|
585
585
|
stages: [
|
|
586
586
|
{
|
|
587
587
|
id: 'stage-1',
|
|
588
588
|
order: 0,
|
|
589
589
|
name: 'AND Stage',
|
|
590
|
-
|
|
590
|
+
filterLogic: 'AND',
|
|
591
591
|
rules: [
|
|
592
|
-
{
|
|
593
|
-
{
|
|
592
|
+
{ fieldPath: 'value', operator: 'gte', value: 20 },
|
|
593
|
+
{ fieldPath: 'active', operator: 'is_true', value: null },
|
|
594
594
|
],
|
|
595
|
-
|
|
596
|
-
|
|
595
|
+
matchAction: 'output',
|
|
596
|
+
noMatchAction: 'exclude',
|
|
597
597
|
},
|
|
598
598
|
],
|
|
599
|
-
|
|
600
|
-
|
|
599
|
+
createdAt: new Date().toISOString(),
|
|
600
|
+
updatedAt: new Date().toISOString(),
|
|
601
601
|
};
|
|
602
602
|
|
|
603
|
-
// Mock rule evaluator to use
|
|
603
|
+
// Mock rule evaluator to use customEvaluator
|
|
604
604
|
const stageWithEvaluator: FunnelStage<GenericEntity> = {
|
|
605
605
|
...funnel.stages[0],
|
|
606
|
-
|
|
606
|
+
customEvaluator: (entity) => entity.value >= 20 && entity.active === true,
|
|
607
607
|
};
|
|
608
608
|
|
|
609
609
|
funnel.stages[0] = stageWithEvaluator;
|
|
@@ -611,7 +611,7 @@ describe('FunnelEngine - AND Logic', () => {
|
|
|
611
611
|
const result = engine.execute(funnel, entities);
|
|
612
612
|
|
|
613
613
|
// Only entity 3 matches both conditions
|
|
614
|
-
expect(result.
|
|
614
|
+
expect(result.totalMatched).toBe(1);
|
|
615
615
|
expect(result.matched[0].entity.id).toBe('3');
|
|
616
616
|
});
|
|
617
617
|
});
|
|
@@ -633,29 +633,29 @@ describe('FunnelEngine - OR Logic', () => {
|
|
|
633
633
|
id: 'funnel-1',
|
|
634
634
|
name: 'OR Logic Funnel',
|
|
635
635
|
status: 'active',
|
|
636
|
-
|
|
636
|
+
inputType: 'any',
|
|
637
637
|
stages: [
|
|
638
638
|
{
|
|
639
639
|
id: 'stage-1',
|
|
640
640
|
order: 0,
|
|
641
641
|
name: 'OR Stage',
|
|
642
|
-
|
|
642
|
+
filterLogic: 'OR',
|
|
643
643
|
rules: [
|
|
644
|
-
{
|
|
645
|
-
{
|
|
644
|
+
{ fieldPath: 'value', operator: 'gte', value: 20 },
|
|
645
|
+
{ fieldPath: 'active', operator: 'is_true', value: null },
|
|
646
646
|
],
|
|
647
|
-
|
|
648
|
-
|
|
647
|
+
matchAction: 'output',
|
|
648
|
+
noMatchAction: 'exclude',
|
|
649
649
|
},
|
|
650
650
|
],
|
|
651
|
-
|
|
652
|
-
|
|
651
|
+
createdAt: new Date().toISOString(),
|
|
652
|
+
updatedAt: new Date().toISOString(),
|
|
653
653
|
};
|
|
654
654
|
|
|
655
655
|
// Mock rule evaluator
|
|
656
656
|
const stageWithEvaluator: FunnelStage<GenericEntity> = {
|
|
657
657
|
...funnel.stages[0],
|
|
658
|
-
|
|
658
|
+
customEvaluator: (entity) => entity.value >= 20 || entity.active === true,
|
|
659
659
|
};
|
|
660
660
|
|
|
661
661
|
funnel.stages[0] = stageWithEvaluator;
|
|
@@ -663,7 +663,7 @@ describe('FunnelEngine - OR Logic', () => {
|
|
|
663
663
|
const result = engine.execute(funnel, entities);
|
|
664
664
|
|
|
665
665
|
// Entities 1 and 2 match (1 has active=true, 2 has value>=20)
|
|
666
|
-
expect(result.
|
|
666
|
+
expect(result.totalMatched).toBe(2);
|
|
667
667
|
expect(result.matched.map(r => r.entity.id).sort()).toEqual(['1', '2']);
|
|
668
668
|
});
|
|
669
669
|
});
|
|
@@ -681,36 +681,36 @@ describe('FunnelEngine - Tags', () => {
|
|
|
681
681
|
id: 'funnel-1',
|
|
682
682
|
name: 'Tag Accumulation',
|
|
683
683
|
status: 'active',
|
|
684
|
-
|
|
684
|
+
inputType: 'any',
|
|
685
685
|
stages: [
|
|
686
686
|
{
|
|
687
687
|
id: 'stage-1',
|
|
688
688
|
order: 0,
|
|
689
689
|
name: 'Stage 1',
|
|
690
|
-
|
|
690
|
+
filterLogic: 'AND',
|
|
691
691
|
rules: [],
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
692
|
+
matchAction: 'continue',
|
|
693
|
+
noMatchAction: 'exclude',
|
|
694
|
+
matchTags: ['tag-1', 'tag-2'],
|
|
695
695
|
},
|
|
696
696
|
{
|
|
697
697
|
id: 'stage-2',
|
|
698
698
|
order: 1,
|
|
699
699
|
name: 'Stage 2',
|
|
700
|
-
|
|
700
|
+
filterLogic: 'AND',
|
|
701
701
|
rules: [],
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
702
|
+
matchAction: 'continue',
|
|
703
|
+
noMatchAction: 'exclude',
|
|
704
|
+
matchTags: ['tag-3'],
|
|
705
705
|
},
|
|
706
706
|
],
|
|
707
|
-
|
|
708
|
-
|
|
707
|
+
createdAt: new Date().toISOString(),
|
|
708
|
+
updatedAt: new Date().toISOString(),
|
|
709
709
|
};
|
|
710
710
|
|
|
711
711
|
const result = engine.execute(funnel, entities);
|
|
712
712
|
|
|
713
|
-
expect(result.matched[0].
|
|
713
|
+
expect(result.matched[0].accumulatedTags).toEqual(['tag-1', 'tag-2', 'tag-3']);
|
|
714
714
|
});
|
|
715
715
|
|
|
716
716
|
it('should apply completion tags to matched entities', () => {
|
|
@@ -724,29 +724,29 @@ describe('FunnelEngine - Tags', () => {
|
|
|
724
724
|
id: 'funnel-1',
|
|
725
725
|
name: 'Completion Tags',
|
|
726
726
|
status: 'active',
|
|
727
|
-
|
|
727
|
+
inputType: 'any',
|
|
728
728
|
stages: [
|
|
729
729
|
{
|
|
730
730
|
id: 'stage-1',
|
|
731
731
|
order: 0,
|
|
732
732
|
name: 'Filter',
|
|
733
|
-
|
|
733
|
+
filterLogic: 'AND',
|
|
734
734
|
rules: [],
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
735
|
+
matchAction: 'output',
|
|
736
|
+
noMatchAction: 'exclude',
|
|
737
|
+
customEvaluator: (entity) => entity.value >= 20,
|
|
738
738
|
},
|
|
739
739
|
],
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
740
|
+
completionTags: ['completed', 'qualified'],
|
|
741
|
+
createdAt: new Date().toISOString(),
|
|
742
|
+
updatedAt: new Date().toISOString(),
|
|
743
743
|
};
|
|
744
744
|
|
|
745
745
|
const result = engine.execute(funnel, entities);
|
|
746
746
|
|
|
747
|
-
expect(result.
|
|
748
|
-
expect(result.matched[0].
|
|
749
|
-
expect(result.matched[0].
|
|
747
|
+
expect(result.totalMatched).toBe(1);
|
|
748
|
+
expect(result.matched[0].accumulatedTags).toContain('completed');
|
|
749
|
+
expect(result.matched[0].accumulatedTags).toContain('qualified');
|
|
750
750
|
});
|
|
751
751
|
|
|
752
752
|
it('should count tagged entities correctly', () => {
|
|
@@ -761,27 +761,27 @@ describe('FunnelEngine - Tags', () => {
|
|
|
761
761
|
id: 'funnel-1',
|
|
762
762
|
name: 'Tag Counting',
|
|
763
763
|
status: 'active',
|
|
764
|
-
|
|
764
|
+
inputType: 'any',
|
|
765
765
|
stages: [
|
|
766
766
|
{
|
|
767
767
|
id: 'stage-1',
|
|
768
768
|
order: 0,
|
|
769
769
|
name: 'Tag Some',
|
|
770
|
-
|
|
770
|
+
filterLogic: 'AND',
|
|
771
771
|
rules: [],
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
772
|
+
matchAction: 'continue',
|
|
773
|
+
noMatchAction: 'continue',
|
|
774
|
+
matchTags: ['tagged'],
|
|
775
|
+
customEvaluator: (entity) => entity.value >= 20,
|
|
776
776
|
},
|
|
777
777
|
],
|
|
778
|
-
|
|
779
|
-
|
|
778
|
+
createdAt: new Date().toISOString(),
|
|
779
|
+
updatedAt: new Date().toISOString(),
|
|
780
780
|
};
|
|
781
781
|
|
|
782
782
|
const result = engine.execute(funnel, entities);
|
|
783
783
|
|
|
784
|
-
expect(result.
|
|
784
|
+
expect(result.totalTagged).toBe(2); // Entities 2 and 3
|
|
785
785
|
});
|
|
786
786
|
});
|
|
787
787
|
|
|
@@ -802,40 +802,40 @@ describe('FunnelEngine - Exclusions', () => {
|
|
|
802
802
|
id: 'funnel-1',
|
|
803
803
|
name: 'Exclusion Tracking',
|
|
804
804
|
status: 'active',
|
|
805
|
-
|
|
805
|
+
inputType: 'any',
|
|
806
806
|
stages: [
|
|
807
807
|
{
|
|
808
808
|
id: 'stage-1',
|
|
809
809
|
order: 0,
|
|
810
810
|
name: 'Stage 1',
|
|
811
|
-
|
|
811
|
+
filterLogic: 'AND',
|
|
812
812
|
rules: [],
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
813
|
+
matchAction: 'continue',
|
|
814
|
+
noMatchAction: 'exclude',
|
|
815
|
+
customEvaluator: (entity) => entity.value >= 20,
|
|
816
816
|
},
|
|
817
817
|
{
|
|
818
818
|
id: 'stage-2',
|
|
819
819
|
order: 1,
|
|
820
820
|
name: 'Stage 2',
|
|
821
|
-
|
|
821
|
+
filterLogic: 'AND',
|
|
822
822
|
rules: [],
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
823
|
+
matchAction: 'output',
|
|
824
|
+
noMatchAction: 'exclude',
|
|
825
|
+
customEvaluator: (entity) => entity.value >= 30,
|
|
826
826
|
},
|
|
827
827
|
],
|
|
828
|
-
|
|
829
|
-
|
|
828
|
+
createdAt: new Date().toISOString(),
|
|
829
|
+
updatedAt: new Date().toISOString(),
|
|
830
830
|
};
|
|
831
831
|
|
|
832
832
|
const result = engine.execute(funnel, entities);
|
|
833
833
|
|
|
834
|
-
expect(result.
|
|
834
|
+
expect(result.totalMatched).toBe(1);
|
|
835
835
|
expect(result.matched[0].entity.id).toBe('3');
|
|
836
836
|
|
|
837
|
-
const excludedAtStage1 = result.excluded.find(r => r.
|
|
838
|
-
const excludedAtStage2 = result.excluded.find(r => r.
|
|
837
|
+
const excludedAtStage1 = result.excluded.find(r => r.excludedAtStage === 'stage-1');
|
|
838
|
+
const excludedAtStage2 = result.excluded.find(r => r.excludedAtStage === 'stage-2');
|
|
839
839
|
|
|
840
840
|
expect(excludedAtStage1?.entity.id).toBe('1');
|
|
841
841
|
expect(excludedAtStage2?.entity.id).toBe('2');
|
|
@@ -849,37 +849,37 @@ describe('FunnelEngine - Exclusions', () => {
|
|
|
849
849
|
id: 'funnel-1',
|
|
850
850
|
name: 'Stop After Exclusion',
|
|
851
851
|
status: 'active',
|
|
852
|
-
|
|
852
|
+
inputType: 'any',
|
|
853
853
|
stages: [
|
|
854
854
|
{
|
|
855
855
|
id: 'stage-1',
|
|
856
856
|
order: 0,
|
|
857
857
|
name: 'Exclude',
|
|
858
|
-
|
|
858
|
+
filterLogic: 'AND',
|
|
859
859
|
rules: [],
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
860
|
+
matchAction: 'continue',
|
|
861
|
+
noMatchAction: 'exclude',
|
|
862
|
+
customEvaluator: () => false,
|
|
863
863
|
},
|
|
864
864
|
{
|
|
865
865
|
id: 'stage-2',
|
|
866
866
|
order: 1,
|
|
867
867
|
name: 'Should Not Reach',
|
|
868
|
-
|
|
868
|
+
filterLogic: 'AND',
|
|
869
869
|
rules: [],
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
870
|
+
matchAction: 'output',
|
|
871
|
+
noMatchAction: 'exclude',
|
|
872
|
+
matchTags: ['unreachable'],
|
|
873
873
|
},
|
|
874
874
|
],
|
|
875
|
-
|
|
876
|
-
|
|
875
|
+
createdAt: new Date().toISOString(),
|
|
876
|
+
updatedAt: new Date().toISOString(),
|
|
877
877
|
};
|
|
878
878
|
|
|
879
879
|
const result = engine.execute(funnel, entities);
|
|
880
880
|
|
|
881
|
-
expect(result.excluded[0].
|
|
882
|
-
expect(result.excluded[0].
|
|
881
|
+
expect(result.excluded[0].stageResults?.length).toBe(1); // Only stage-1
|
|
882
|
+
expect(result.excluded[0].accumulatedTags).not.toContain('unreachable');
|
|
883
883
|
});
|
|
884
884
|
});
|
|
885
885
|
|
|
@@ -900,35 +900,35 @@ describe('FunnelEngine - Statistics', () => {
|
|
|
900
900
|
id: 'funnel-1',
|
|
901
901
|
name: 'Stats Funnel',
|
|
902
902
|
status: 'active',
|
|
903
|
-
|
|
903
|
+
inputType: 'any',
|
|
904
904
|
stages: [
|
|
905
905
|
{
|
|
906
906
|
id: 'stage-1',
|
|
907
907
|
order: 0,
|
|
908
908
|
name: 'Filter Stage',
|
|
909
|
-
|
|
909
|
+
filterLogic: 'AND',
|
|
910
910
|
rules: [],
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
911
|
+
matchAction: 'continue',
|
|
912
|
+
noMatchAction: 'exclude',
|
|
913
|
+
matchTags: ['qualified'],
|
|
914
|
+
customEvaluator: (entity) => entity.value >= 20,
|
|
915
915
|
},
|
|
916
916
|
],
|
|
917
|
-
|
|
918
|
-
|
|
917
|
+
createdAt: new Date().toISOString(),
|
|
918
|
+
updatedAt: new Date().toISOString(),
|
|
919
919
|
};
|
|
920
920
|
|
|
921
921
|
const result = engine.execute(funnel, entities);
|
|
922
922
|
|
|
923
|
-
const stats = result.
|
|
923
|
+
const stats = result.stageStats['stage-1'];
|
|
924
924
|
|
|
925
|
-
expect(stats.
|
|
926
|
-
expect(stats.
|
|
927
|
-
expect(stats.
|
|
928
|
-
expect(stats.
|
|
929
|
-
expect(stats.
|
|
930
|
-
expect(stats.
|
|
931
|
-
expect(stats.
|
|
925
|
+
expect(stats.inputCount).toBe(3);
|
|
926
|
+
expect(stats.matchedCount).toBe(2);
|
|
927
|
+
expect(stats.notMatchedCount).toBe(1);
|
|
928
|
+
expect(stats.excludedCount).toBe(1);
|
|
929
|
+
expect(stats.taggedCount).toBe(2);
|
|
930
|
+
expect(stats.continuedCount).toBe(2);
|
|
931
|
+
expect(stats.durationMs).toBeGreaterThanOrEqual(0);
|
|
932
932
|
});
|
|
933
933
|
|
|
934
934
|
it('should track execution duration', () => {
|
|
@@ -939,26 +939,26 @@ describe('FunnelEngine - Statistics', () => {
|
|
|
939
939
|
id: 'funnel-1',
|
|
940
940
|
name: 'Duration Test',
|
|
941
941
|
status: 'active',
|
|
942
|
-
|
|
942
|
+
inputType: 'any',
|
|
943
943
|
stages: [
|
|
944
944
|
{
|
|
945
945
|
id: 'stage-1',
|
|
946
946
|
order: 0,
|
|
947
947
|
name: 'Test Stage',
|
|
948
|
-
|
|
948
|
+
filterLogic: 'AND',
|
|
949
949
|
rules: [],
|
|
950
|
-
|
|
951
|
-
|
|
950
|
+
matchAction: 'output',
|
|
951
|
+
noMatchAction: 'exclude',
|
|
952
952
|
},
|
|
953
953
|
],
|
|
954
|
-
|
|
955
|
-
|
|
954
|
+
createdAt: new Date().toISOString(),
|
|
955
|
+
updatedAt: new Date().toISOString(),
|
|
956
956
|
};
|
|
957
957
|
|
|
958
958
|
const result = engine.execute(funnel, entities);
|
|
959
959
|
|
|
960
|
-
expect(result.
|
|
961
|
-
expect(typeof result.
|
|
960
|
+
expect(result.durationMs).toBeGreaterThanOrEqual(0);
|
|
961
|
+
expect(typeof result.durationMs).toBe('number');
|
|
962
962
|
});
|
|
963
963
|
});
|
|
964
964
|
|
|
@@ -967,7 +967,7 @@ describe('FunnelEngine - Statistics', () => {
|
|
|
967
967
|
// ============================================================================
|
|
968
968
|
|
|
969
969
|
describe('FunnelEngine - Custom Evaluators', () => {
|
|
970
|
-
it('should use
|
|
970
|
+
it('should use customEvaluator when provided', () => {
|
|
971
971
|
const engine = new FunnelEngine<MockRecipe>();
|
|
972
972
|
const entities = createMockRecipes();
|
|
973
973
|
|
|
@@ -975,30 +975,30 @@ describe('FunnelEngine - Custom Evaluators', () => {
|
|
|
975
975
|
id: 'funnel-1',
|
|
976
976
|
name: 'Custom Eval Funnel',
|
|
977
977
|
status: 'active',
|
|
978
|
-
|
|
978
|
+
inputType: 'any',
|
|
979
979
|
stages: [
|
|
980
980
|
{
|
|
981
981
|
id: 'stage-1',
|
|
982
982
|
order: 0,
|
|
983
983
|
name: 'Quick Recipes',
|
|
984
|
-
|
|
984
|
+
filterLogic: 'AND',
|
|
985
985
|
rules: [],
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
986
|
+
matchAction: 'output',
|
|
987
|
+
noMatchAction: 'exclude',
|
|
988
|
+
customEvaluator: (recipe) => recipe.prep_time_minutes <= 45,
|
|
989
989
|
},
|
|
990
990
|
],
|
|
991
|
-
|
|
992
|
-
|
|
991
|
+
createdAt: new Date().toISOString(),
|
|
992
|
+
updatedAt: new Date().toISOString(),
|
|
993
993
|
};
|
|
994
994
|
|
|
995
995
|
const result = engine.execute(funnel, entities);
|
|
996
996
|
|
|
997
|
-
expect(result.
|
|
997
|
+
expect(result.totalMatched).toBe(2); // Pasta and Curry
|
|
998
998
|
expect(result.matched.every(r => r.entity.prep_time_minutes <= 45)).toBe(true);
|
|
999
999
|
});
|
|
1000
1000
|
|
|
1001
|
-
it('should handle
|
|
1001
|
+
it('should handle customEvaluator errors gracefully', () => {
|
|
1002
1002
|
const engine = new FunnelEngine<GenericEntity>();
|
|
1003
1003
|
const entities: GenericEntity[] = [{ id: '1', value: 10 }];
|
|
1004
1004
|
|
|
@@ -1006,30 +1006,30 @@ describe('FunnelEngine - Custom Evaluators', () => {
|
|
|
1006
1006
|
id: 'funnel-1',
|
|
1007
1007
|
name: 'Error Handling',
|
|
1008
1008
|
status: 'active',
|
|
1009
|
-
|
|
1009
|
+
inputType: 'any',
|
|
1010
1010
|
stages: [
|
|
1011
1011
|
{
|
|
1012
1012
|
id: 'stage-1',
|
|
1013
1013
|
order: 0,
|
|
1014
1014
|
name: 'Throw Error',
|
|
1015
|
-
|
|
1015
|
+
filterLogic: 'AND',
|
|
1016
1016
|
rules: [],
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1017
|
+
matchAction: 'output',
|
|
1018
|
+
noMatchAction: 'exclude',
|
|
1019
|
+
customEvaluator: () => {
|
|
1020
1020
|
throw new Error('Evaluator failed');
|
|
1021
1021
|
},
|
|
1022
1022
|
},
|
|
1023
1023
|
],
|
|
1024
|
-
|
|
1025
|
-
|
|
1024
|
+
createdAt: new Date().toISOString(),
|
|
1025
|
+
updatedAt: new Date().toISOString(),
|
|
1026
1026
|
};
|
|
1027
1027
|
|
|
1028
1028
|
const result = engine.execute(funnel, entities);
|
|
1029
1029
|
|
|
1030
1030
|
// Should treat error as no match
|
|
1031
|
-
expect(result.
|
|
1032
|
-
expect(result.
|
|
1031
|
+
expect(result.totalMatched).toBe(0);
|
|
1032
|
+
expect(result.totalExcluded).toBe(1);
|
|
1033
1033
|
});
|
|
1034
1034
|
});
|
|
1035
1035
|
|
|
@@ -1062,25 +1062,25 @@ describe('FunnelEngine - Generic Entity Types', () => {
|
|
|
1062
1062
|
id: 'funnel-1',
|
|
1063
1063
|
name: 'Arbitrary Funnel',
|
|
1064
1064
|
status: 'active',
|
|
1065
|
-
|
|
1065
|
+
inputType: 'any',
|
|
1066
1066
|
stages: [
|
|
1067
1067
|
{
|
|
1068
1068
|
id: 'stage-1',
|
|
1069
1069
|
order: 0,
|
|
1070
1070
|
name: 'Test Stage',
|
|
1071
|
-
|
|
1071
|
+
filterLogic: 'AND',
|
|
1072
1072
|
rules: [],
|
|
1073
|
-
|
|
1074
|
-
|
|
1073
|
+
matchAction: 'output',
|
|
1074
|
+
noMatchAction: 'exclude',
|
|
1075
1075
|
},
|
|
1076
1076
|
],
|
|
1077
|
-
|
|
1078
|
-
|
|
1077
|
+
createdAt: new Date().toISOString(),
|
|
1078
|
+
updatedAt: new Date().toISOString(),
|
|
1079
1079
|
};
|
|
1080
1080
|
|
|
1081
1081
|
const result = engine.execute(funnel, entities);
|
|
1082
1082
|
|
|
1083
|
-
expect(result.
|
|
1083
|
+
expect(result.totalMatched).toBe(1);
|
|
1084
1084
|
expect(result.matched[0].entity.uuid).toBe('abc-123');
|
|
1085
1085
|
expect(result.matched[0].entity.nested.deep.value).toBe(42);
|
|
1086
1086
|
});
|