import { Injectable } from '@nestjs/common';
import { AuditService } from '../../modules/audit/audit.service';
import {
RecommendationOutcomeService,
type RecommendationOutcome,
} from '../intelligence/recommendation-outcome.service';
export interface DistributionOutcomeInput {
organizationId: string;
decisionFingerprint: string;
requestedUnits: number;
allocatedUnits: number;
fulfilledUnits: number;
overrideUsed?: boolean;
userId?: string;
correlationId?: string;
}
export interface DistributionOutcomeResult extends RecommendationOutcome {
decisionFingerprint: string;
status: 'recorded';
mutatesOperationalTruth: false;
appendOnlyFeedback: true;
}
/**
* Records distribution performance as append-only learning evidence.
*
* The outcome is deliberately separate from channel activation and order
* truth. It gives AI-X5 a feedback loop without allowing an observation to
* silently reconfigure a provider or rewrite a canonical record.
*/
@Injectable()
export class DistributionOutcomeService {
constructor(
private readonly outcomes: RecommendationOutcomeService,
private readonly audit: AuditService,
) {}
async record(input: DistributionOutcomeInput): Promise<DistributionOutcomeResult> {
const fingerprint = typeof input?.decisionFingerprint === 'string'
? input.decisionFingerprint.trim().toLowerCase()
: '';
if (
!input ||
typeof input !== 'object' ||
typeof input.organizationId !== 'string' ||
!input.organizationId.trim() ||
!/^[a-f0-9]{24}$/.test(fingerprint)
) {
throw new Error('distribution_outcome.invalid');
}
const outcome = this.outcomes.evaluate({
recommendationId: fingerprint,
requestedUnits: input.requestedUnits,
allocatedUnits: input.allocatedUnits,
fulfilledUnits: input.fulfilledUnits,
overrideUsed: input.overrideUsed,
});
await this.audit.record({
organizationId: input.organizationId.trim(),
actorUserId: input.userId,
correlationId: input.correlationId,
eventType: 'distribution.recommendation.outcome_recorded',
entityType: 'DistributionRecommendationOutcome',
metadata: {
decisionFingerprint: fingerprint,
recommendationKind: 'distribution',
requestedUnits: outcome.requestedUnits,
allocatedUnits: outcome.allocatedUnits,
fulfilledUnits: outcome.fulfilledUnits,
fulfillmentRate: outcome.fulfillmentRate,
outcome: outcome.outcome,
overrideUsed: outcome.overrideUsed,
mutatesOperationalTruth: false,
appendOnlyFeedback: true,
},
});
return {
...outcome,
decisionFingerprint: fingerprint,
status: 'recorded',
mutatesOperationalTruth: false,
appendOnlyFeedback: true,
};
}
}
