import { Injectable } from '@nestjs/common';
import { AuditService } from '../../modules/audit/audit.service';
export type DistributionReviewDecision = 'accepted' | 'rejected';
export type IntelligenceRecommendationKind = 'distribution' | 'place_event';
export interface DistributionReviewInput {
organizationId: string;
decisionFingerprint: string;
decision: DistributionReviewDecision;
reason?: string;
userId?: string;
correlationId?: string;
}
export interface IntelligenceReviewInput extends DistributionReviewInput {
recommendationKind: IntelligenceRecommendationKind;
}
export interface DistributionReviewResult {
decisionFingerprint: string;
decision: DistributionReviewDecision;
status: 'recorded';
activatesChannels: false;
mutatesOperationalTruth: false;
reasonProvided: boolean;
}
@Injectable()
export class IntelligenceRecommendationReviewService {
constructor(private readonly audit: AuditService) {}
async record(input: IntelligenceReviewInput): Promise<DistributionReviewResult> {
const rawFingerprint = typeof input?.decisionFingerprint === 'string'
? input.decisionFingerprint.trim()
: '';
const errorCode = input?.recommendationKind === 'place_event'
? 'place_event_review.invalid'
: input?.recommendationKind === 'distribution'
? 'distribution_review.invalid'
: 'intelligence_review.invalid';
if (!input || typeof input !== 'object' || !['distribution', 'place_event'].includes(input.recommendationKind) || typeof input.organizationId !== 'string' || !input.organizationId.trim() ||
!/^[a-f0-9]{24}$/i.test(rawFingerprint) ||
!['accepted', 'rejected'].includes(input.decision) ||
(input.reason !== undefined && (typeof input.reason !== 'string' || !input.reason.trim() || input.reason.trim().length > 1000)) ||
(input.decision === 'rejected' && !input.reason?.trim())) {
throw new Error(errorCode);
}
const decisionFingerprint = rawFingerprint.toLowerCase();
const reasonProvided = Boolean(input.reason?.trim());
const isDistribution = input.recommendationKind === 'distribution';
await this.audit.record({
organizationId: input.organizationId.trim(),
actorUserId: input.userId,
correlationId: input.correlationId,
eventType: isDistribution
? 'distribution.recommendation.reviewed'
: 'place_event.recommendation.reviewed',
entityType: isDistribution
? 'DistributionRecommendation'
: 'PlaceEventRecommendation',
metadata: {
decisionFingerprint,
recommendationKind: input.recommendationKind,
decision: input.decision,
reasonProvided,
activatesChannels: false,
mutatesOperationalTruth: false,
},
});
return {
decisionFingerprint,
decision: input.decision,
status: 'recorded',
activatesChannels: false,
mutatesOperationalTruth: false,
reasonProvided,
};
}
}
@Injectable()
export class DistributionReviewService {
constructor(private readonly review: IntelligenceRecommendationReviewService) {}
record(input: DistributionReviewInput): Promise<DistributionReviewResult> {
return this.review.record({ ...input, recommendationKind: 'distribution' });
}
}
@Injectable()
export class PlaceEventReviewService {
constructor(private readonly review: IntelligenceRecommendationReviewService) {}
record(input: DistributionReviewInput): Promise<DistributionReviewResult> {
return this.review.record({ ...input, recommendationKind: 'place_event' });
}
}
