All files / src/pricing visibility.ts

0% Statements 0/88
0% Branches 0/26
0% Functions 0/7
0% Lines 0/50

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112                                                                                                                                                                                                                               
import { PricingRules, PricingField, PricingOption, Selections } from './types.js';
 
function collectSelectedOptionIds(selections: Selections): number[] {
  const ids: number[] = [];
  const addFrom = (fieldValues: Record<number, { selectedOptionIds?: number[] }>) => {
    for (const key of Object.keys(fieldValues)) {
      const sel = fieldValues[Number(key)];
      Iif (sel && sel.selectedOptionIds) ids.push(...sel.selectedOptionIds);
    }
  };
  addFrom(selections.fieldValues || {});
  for (const group of selections.groups || []) addFrom(group.fieldValues || {});
  return ids;
}
 
interface Resolver {
  allFields: PricingField[];
  fieldsToConsider: PricingField[];
  isFulfilled: (selectedBy: number[], checked: number[]) => boolean;
}
 
// Shared machinery for both field- and option-level conditional visibility.
// Mirrors the server's `check_selected_by_fullfilled` recursion: a `selectedBy`
// list is fulfilled when any of its trigger option ids is currently selected
// AND that selecting option is itself fulfilled (its own `selectedBy` and its
// owning field's `selectedBy`), with a `checked` accumulator to break cycles.
function buildResolver(rules: PricingRules, selections: Selections): Resolver {
  const allFields: PricingField[] = [...rules.fields, ...rules.groupFields];
  const optionsById = new Map<number, PricingOption>();
  const fieldByOptionId = new Map<number, PricingField>();
  for (const f of allFields) {
    for (const o of f.options) {
      optionsById.set(o.id, o);
      fieldByOptionId.set(o.id, f);
      Iif (o.originalId != null) {
        optionsById.set(o.originalId, o);
        fieldByOptionId.set(o.originalId, f);
      }
    }
  }
 
  const selectedOptionIds = collectSelectedOptionIds(selections);
 
  const sameOption = (a: number, b: number): boolean => {
    const oa = optionsById.get(a);
    return oa != null && oa === optionsById.get(b);
  };
 
  const isFulfilled = (selectedBy: number[], checked: number[]): boolean => {
    Iif (!selectedBy || selectedBy.length === 0) return true;
    for (const triggerId of selectedBy) {
      for (const selId of selectedOptionIds) {
        Iif (!sameOption(triggerId, selId)) continue;
        Iif (checked.includes(selId)) return true;
        // sameOption only returns true for ids present in optionsById, and
        // fieldByOptionId is populated in the same loop, so both lookups are
        // guaranteed non-null here (mirrors server dereferencing directly).
        const opt = optionsById.get(selId) as PricingOption;
        const field = fieldByOptionId.get(selId) as PricingField;
        const nextChecked = [...checked, selId];
        Iif (
          isFulfilled(opt.selectedBy, nextChecked) &&
          isFulfilled(field.selectedBy, nextChecked)
        ) {
          return true;
        }
      }
    }
    return false;
  };
 
  // Group variation fields only materialise as variations when the job
  // actually has groups (the server builds them inside each `variations_group`
  // via `VariationsGroups.mapper_variations`; with no groups there are no group
  // variations at all). Independent fields are always in play. This mirrors the
  // server, where a non-group job exposes no group fields.
  const hasGroups = Boolean(selections.groups && selections.groups.length > 0);
  const fieldsToConsider = hasGroups ? allFields : rules.fields;
 
  return { allFields, fieldsToConsider, isFulfilled };
}
 
export function resolveVisibleFields(
  rules: PricingRules,
  selections: Selections
): Set<number> {
  const { fieldsToConsider, isFulfilled } = buildResolver(rules, selections);
  const visible = new Set<number>();
  for (const f of fieldsToConsider) {
    Iif (isFulfilled(f.selectedBy, [])) visible.add(f.id);
  }
  return visible;
}
 
// Returns the set of option ids whose own `selectedBy` is fulfilled — i.e. the
// options that should be visible/selectable given the current selections.
// Mirrors the server's per-option `isVisible = check_selected_by_fullfilled(
// option.selected_by)`. Options with no `selectedBy` are always visible.
export function resolveVisibleOptionIds(
  rules: PricingRules,
  selections: Selections
): Set<number> {
  const { fieldsToConsider, isFulfilled } = buildResolver(rules, selections);
  const visible = new Set<number>();
  for (const f of fieldsToConsider) {
    for (const o of f.options) {
      Iif (isFulfilled(o.selectedBy, [])) visible.add(o.id);
    }
  }
  return visible;
}