import { Injectable } from '@nestjs/common';
import { randomUUID } from 'node:crypto';
import { DatabaseService } from '../../../infrastructure/database/database.service';
import { assertCostingScope } from './costing-input-validation';
@Injectable()
export class CostAlertService {
constructor(private readonly database: DatabaseService) {}
async refresh(input: {
organizationId: string;
locationId: string;
}) {
assertCostingScope(input, 'costing.alert_input_invalid');
const cfgRows = await this.database.query<any>(
`select target_food_cost_percent,minimum_gross_margin_percent
from costing_configurations
where organization_id=$1 and location_id=$2 and is_active=true
limit 1`,
[input.organizationId,input.locationId],
);
const cfg = cfgRows[0] ?? {};
await this.database.query(
`update cost_alerts
set status='resolved',resolved_at=now()
where organization_id=$1 and location_id=$2 and status='open'
and alert_type in ('food_cost_above_target','margin_below_target')`,
[input.organizationId,input.locationId],
);
let created = 0;
if (cfg.target_food_cost_percent != null) {
const rows = await this.database.query<any>(
`select product_id,food_cost_percent::text,currency_code
from product_margin_projections
where organization_id=$1 and location_id=$2
and quality_state<>'insufficient_data'
and food_cost_percent>$3::numeric`,
[input.organizationId,input.locationId,cfg.target_food_cost_percent],
);
for (const row of rows) {
await this.database.query(
`insert into cost_alerts(
id,organization_id,location_id,entity_type,entity_id,
alert_type,severity,new_value,currency_code,reason
) values($1,$2,$3,'product',$4,'food_cost_above_target','warning',$5,$6,$7)`,
[
randomUUID(),input.organizationId,input.locationId,row.product_id,
row.food_cost_percent,row.currency_code,'food_cost_target_exceeded',
],
);
created++;
}
}
if (cfg.minimum_gross_margin_percent != null) {
const rows = await this.database.query<any>(
`select product_id,gross_margin_percent::text,currency_code
from product_margin_projections
where organization_id=$1 and location_id=$2
and quality_state<>'insufficient_data'
and gross_margin_percent<$3::numeric`,
[input.organizationId,input.locationId,cfg.minimum_gross_margin_percent],
);
for (const row of rows) {
await this.database.query(
`insert into cost_alerts(
id,organization_id,location_id,entity_type,entity_id,
alert_type,severity,new_value,currency_code,reason
) values($1,$2,$3,'product',$4,'margin_below_target','warning',$5,$6,$7)`,
[
randomUUID(),input.organizationId,input.locationId,row.product_id,
row.gross_margin_percent,row.currency_code,'gross_margin_target_missed',
],
);
created++;
}
}
return { created };
}
}
