97 lines · 3.2 KB · TypeScript
RawDownload
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<Pick<UsagePriceForm, UsagePriceField>>;  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<string, string>();  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]),  );}