import { Injectable } from '@nestjs/common';
import { DatabaseService } from '../../infrastructure/database/database.service';
export interface DistributionOutcomeSummary {
recommendationCount: number;
allocatedUnits: number;
fulfilledUnits: number;
fulfillmentRate: number;
overrideCount: number;
fulfilledCount: number;
partialCount: number;
notFulfilledCount: number;
confidence: 'high' | 'medium' | 'low';
}
@Injectable()
export class DistributionOutcomeReadService {
constructor(private readonly database: DatabaseService) {}
async summarize(
organizationId: string,
limit = 100,
): Promise<DistributionOutcomeSummary> {
if (
typeof organizationId !== 'string' ||
!organizationId.trim() ||
!Number.isSafeInteger(limit) ||
limit < 1 ||
limit > 1000
) {
throw new Error('distribution_outcome.summary_invalid');
}
const rows = await this.database.query<{
requested_units: string | null;
allocated_units: string | null;
fulfilled_units: string | null;
outcome: string | null;
override_used: string | null;
count: number;
}>(
`select
metadata->>'requestedUnits' as requested_units,
metadata->>'allocatedUnits' as allocated_units,
metadata->>'fulfilledUnits' as fulfilled_units,
metadata->>'outcome' as outcome,
metadata->>'overrideUsed' as override_used,
count(*)::int as count
from (
select metadata
from audit_events
where organization_id=$1
and event_type='distribution.recommendation.outcome_recorded'
order by occurred_at desc
limit $2
) recent_outcomes
group by metadata->>'requestedUnits', metadata->>'allocatedUnits',
metadata->>'fulfilledUnits', metadata->>'outcome',
metadata->>'overrideUsed'`,
[organizationId.trim(), limit],
);
let recommendationCount = 0;
let allocatedUnits = 0;
let fulfilledUnits = 0;
let overrideCount = 0;
let fulfilledCount = 0;
let partialCount = 0;
let notFulfilledCount = 0;
for (const row of rows) {
const count = Number(row.count);
if (!Number.isSafeInteger(count) || count < 0) {
throw new Error('distribution_outcome.summary_invalid');
}
const allocated = Number(row.allocated_units ?? 0);
const fulfilled = Number(row.fulfilled_units ?? 0);
if (
!Number.isSafeInteger(allocated) ||
allocated < 0 ||
!Number.isSafeInteger(fulfilled) ||
fulfilled < 0
) {
throw new Error('distribution_outcome.summary_invalid');
}
recommendationCount += count;
allocatedUnits += allocated * count;
fulfilledUnits += fulfilled * count;
if (row.override_used === 'true') overrideCount += count;
if (row.outcome === 'fulfilled') fulfilledCount += count;
if (row.outcome === 'partial') partialCount += count;
if (row.outcome === 'not_fulfilled') notFulfilledCount += count;
}
if (
!Number.isSafeInteger(recommendationCount) ||
!Number.isSafeInteger(allocatedUnits) ||
!Number.isSafeInteger(fulfilledUnits) ||
!Number.isSafeInteger(overrideCount) ||
!Number.isSafeInteger(fulfilledCount) ||
!Number.isSafeInteger(partialCount) ||
!Number.isSafeInteger(notFulfilledCount)
) {
throw new Error('distribution_outcome.summary_invalid');
}
const fulfillmentRate = allocatedUnits === 0 ? 0 : fulfilledUnits / allocatedUnits;
return {
recommendationCount,
allocatedUnits,
fulfilledUnits,
fulfillmentRate,
overrideCount,
fulfilledCount,
partialCount,
notFulfilledCount,
confidence: recommendationCount >= 10
? 'high'
: recommendationCount >= 3
? 'medium'
: 'low',
};
}
}
