1
import { Injectable } from '@nestjs/common';
2
3
export interface RecommendationOutcomeInput {
4
recommendationId: string;
5
requestedUnits: number;
6
allocatedUnits: number;
7
fulfilledUnits: number;
8
overrideUsed?: boolean;
9
}
10
11
export interface RecommendationOutcome {
12
recommendationId: string;
13
requestedUnits: number;
14
allocatedUnits: number;
15
fulfilledUnits: number;
16
fulfillmentRate: number;
17
outcome: 'fulfilled' | 'partial' | 'not_fulfilled';
18
overrideUsed: boolean;
19
explanation: string;
20
}
21
22
export interface RecommendationOutcomeSummary {
23
recommendationCount: number;
24
allocatedUnits: number;
25
fulfilledUnits: number;
26
fulfillmentRate: number;
27
overrideCount: number;
28
confidence: 'high' | 'medium' | 'low';
29
}
30
31
@Injectable()
32
export class RecommendationOutcomeService {
33
evaluate(input: RecommendationOutcomeInput): RecommendationOutcome {
34
if (!input || typeof input !== 'object' || typeof input.recommendationId !== 'string') throw new Error('recommendation_outcome.invalid');
35
const recommendationId = input.recommendationId.trim();
36
if (!recommendationId || !Number.isSafeInteger(input.requestedUnits) || input.requestedUnits < 0 || !Number.isSafeInteger(input.allocatedUnits) || input.allocatedUnits < 0 || input.allocatedUnits > input.requestedUnits || !Number.isSafeInteger(input.fulfilledUnits) || input.fulfilledUnits < 0 || input.fulfilledUnits > input.allocatedUnits || (input.overrideUsed !== undefined && typeof input.overrideUsed !== 'boolean')) {
37
throw new Error('recommendation_outcome.invalid');
38
}
39
const fulfillmentRate = input.allocatedUnits === 0 ? 0 : input.fulfilledUnits / input.allocatedUnits;
40
const outcome = fulfillmentRate === 1 ? 'fulfilled' : fulfillmentRate > 0 ? 'partial' : 'not_fulfilled';
41
return {recommendationId, requestedUnits: input.requestedUnits, allocatedUnits: input.allocatedUnits, fulfilledUnits: input.fulfilledUnits, fulfillmentRate, outcome, overrideUsed: input.overrideUsed === true, explanation: outcome === 'fulfilled' ? 'Allocated demand was fulfilled.' : outcome === 'partial' ? 'Only part of the allocated demand was fulfilled.' : 'No allocated demand was fulfilled.'};
42
}
43
44
summarize(outcomes: RecommendationOutcome[]): RecommendationOutcomeSummary {
45
if (!Array.isArray(outcomes) || outcomes.length === 0 || outcomes.some((outcome) => {
46
if (!outcome || typeof outcome.recommendationId !== 'string' || !outcome.recommendationId.trim() || !Number.isSafeInteger(outcome.requestedUnits) || outcome.requestedUnits < 0 || !Number.isSafeInteger(outcome.allocatedUnits) || outcome.allocatedUnits < 0 || outcome.allocatedUnits > outcome.requestedUnits || !Number.isSafeInteger(outcome.fulfilledUnits) || outcome.fulfilledUnits < 0 || outcome.fulfilledUnits > outcome.allocatedUnits || !Number.isFinite(outcome.fulfillmentRate) || outcome.fulfillmentRate < 0 || outcome.fulfillmentRate > 1 || typeof outcome.overrideUsed !== 'boolean' || typeof outcome.explanation !== 'string' || !outcome.explanation.trim()) return true;
47
const expectedOutcome = outcome.fulfillmentRate === 1 ? 'fulfilled' : outcome.fulfillmentRate > 0 ? 'partial' : 'not_fulfilled';
48
return outcome.outcome !== expectedOutcome;
49
})) throw new Error('recommendation_outcome.summary_invalid');
50
const ids = outcomes.map((outcome) => outcome.recommendationId.trim().toLowerCase());
51
if (new Set(ids).size !== ids.length) throw new Error('recommendation_outcome.duplicate');
52
const allocatedUnits = outcomes.reduce((total, outcome) => total + outcome.allocatedUnits, 0);
53
const fulfilledUnits = outcomes.reduce((total, outcome) => total + outcome.fulfilledUnits, 0);
54
if (!Number.isSafeInteger(allocatedUnits) || !Number.isSafeInteger(fulfilledUnits)) throw new Error('recommendation_outcome.summary_invalid');
55
const overrideCount = outcomes.filter((outcome) => outcome.overrideUsed).length;
56
const fulfillmentRate = allocatedUnits === 0 ? 0 : fulfilledUnits / allocatedUnits;
57
return {recommendationCount: outcomes.length, allocatedUnits, fulfilledUnits, fulfillmentRate, overrideCount, confidence: outcomes.length >= 10 ? 'high' : outcomes.length >= 3 ? 'medium' : 'low'};
58
}
59
}
60