import { Injectable } from '@nestjs/common';
import { DatabaseService } from '../../../infrastructure/database/database.service';
import {
assertCostingCurrency,
assertCostingId,
assertCostingScope,
} from './costing-input-validation';
@Injectable()
export class ModifierCostService {
constructor(private readonly database: DatabaseService) {}
async recalculate(input: {
organizationId: string;
locationId: string;
modifierId: string;
currencyCode: string;
}) {
assertCostingScope(input, 'costing.projection_input_invalid');
assertCostingId(input.modifierId, 'costing.projection_input_invalid');
assertCostingCurrency(input.currencyCode, 'costing.projection_input_invalid');
const rows = await this.database.query<any>(
`select
m.id,
m.recipe_id,
coalesce(m.price_delta_minor,0)::bigint as price_delta_minor,
rcp.total_cost_minor::text as recipe_cost_minor,
rcp.quality_state
from modifiers m
left join recipes r
on r.id=m.recipe_id and r.organization_id=m.organization_id
left join recipe_versions rv
on rv.recipe_id=r.id and rv.status='published'
left join recipe_cost_projections rcp
on rcp.recipe_version_id=rv.id and rcp.location_id=$2
where m.id=$1 and m.organization_id=$3
order by rv.version_number desc nulls last
limit 1`,
[input.modifierId,input.locationId,input.organizationId],
);
const row = rows[0];
if (!row) return null;
const price = String(row.price_delta_minor ?? '0');
const cost = row.recipe_cost_minor;
let margin = null;
let pct = null;
if (cost != null) {
const calc = await this.database.query<any>(
`select
($1::numeric-$2::numeric)::numeric(24,8)::text as margin,
case when $1::numeric>0
then ((($1::numeric-$2::numeric)/$1::numeric)*100)::numeric(12,6)::text
else null end as pct`,
[price,cost],
);
margin = calc[0].margin;
pct = calc[0].pct;
}
const quality = cost == null
? 'insufficient_data'
: row.quality_state ?? 'estimated';
await this.database.query(
`insert into modifier_cost_projections(
organization_id,location_id,modifier_id,recipe_id,
cost_delta_minor,price_delta_minor,margin_delta_minor,
margin_percent,currency_code,quality_state
) values($1,$2,$3,$4,$5,$6,$7,$8,$9,$10)
on conflict(location_id,modifier_id)
do update set
recipe_id=excluded.recipe_id,
cost_delta_minor=excluded.cost_delta_minor,
price_delta_minor=excluded.price_delta_minor,
margin_delta_minor=excluded.margin_delta_minor,
margin_percent=excluded.margin_percent,
currency_code=excluded.currency_code,
quality_state=excluded.quality_state,
calculated_at=now(),
stale_at=null,
projection_version=modifier_cost_projections.projection_version+1`,
[
input.organizationId,input.locationId,input.modifierId,row.recipe_id,
cost,price,margin,pct,input.currencyCode,quality,
],
);
return {
modifierId: input.modifierId,
costDeltaMinor: cost,
priceDeltaMinor: price,
marginDeltaMinor: margin,
marginPercent: pct,
qualityState: quality,
};
}
}
