import { parseUsagePriceForm, usagePriceForm, USAGE_PRICE_FIELDS, type UsagePriceForm, } from "./usagePriceForm"; import type { UsagePriceChange, UsagePriceTarget } from "./usagePriceTargets"; export type UsagePriceField = (typeof USAGE_PRICE_FIELDS)[number]["key"]; export interface UsagePriceDraft { readonly id: string; readonly model: string; readonly isNew: boolean; readonly values: Partial>; readonly removed?: boolean; } export function isEmptyUsagePriceDraft(draft: UsagePriceDraft) { return ( draft.isNew && draft.model.trim() === "" && Object.values(draft.values).every((value) => value.trim() === "") ); } function modelPrice(target: UsagePriceTarget, model: string) { return target.prices && Object.hasOwn(target.prices, model) ? target.prices[model] : undefined; } export function usagePriceCell( targets: readonly UsagePriceTarget[], model: string, field: UsagePriceField, ) { const optional = USAGE_PRICE_FIELDS.find((entry) => entry.key === field)!.optional; const values = targets.map((target) => modelPrice(target, model) ? usagePriceForm(model, modelPrice(target, model))[field] : null, ); if (targets.some((target) => target.prices === null)) return { value: "", placeholder: "Unavailable" }; if (values.some((value) => value !== values[0])) return { value: "", placeholder: "Mixed" }; return { value: values[0] ?? "", placeholder: values[0] === null ? "Automatic" : optional ? "Input rate" : "0.00", }; } /** Only edited cells replace rates; untouched cells retain each environment's own values. */ export function usagePriceTableChanges( target: UsagePriceTarget, drafts: readonly UsagePriceDraft[], ) { const changes: UsagePriceChange[] = []; const errors = new Map(); for (const draft of drafts) { if (isEmptyUsagePriceDraft(draft)) continue; const model = draft.model.trim(); if (draft.removed) { if (modelPrice(target, model)) changes.push({ model, price: null }); continue; } const original = usagePriceForm(model, modelPrice(target, model)); const form = { ...original, ...draft.values }; const parsed = parseUsagePriceForm(form); if (parsed === null) { const missing = USAGE_PRICE_FIELDS.find( (field) => !field.optional && form[field.key].trim() === "", ); errors.set( draft.id, model === "" ? "Enter a model ID." : missing ? `${missing.label} is required on ${target.label}.` : "Use non-negative numbers for prices.", ); continue; } const next = usagePriceForm(model, parsed.price); if (USAGE_PRICE_FIELDS.some((field) => original[field.key] !== next[field.key])) changes.push(parsed); } return { changes, errors }; } /** Unavailable destinations report a save failure without blocking writable environments. */ export function usagePriceTableErrors( targets: readonly UsagePriceTarget[], drafts: readonly UsagePriceDraft[], ) { return new Map( targets .filter((target) => target.unavailable === null && target.prices !== null) .flatMap((target) => [...usagePriceTableChanges(target, drafts).errors]), ); }