All files / src/pricing inventory.ts

0% Statements 0/94
0% Branches 0/27
0% Functions 0/6
0% Lines 0/46

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                                                                                                                                                                                   
import { PricingRules, PricingField, Selections } from './types.js';
 
// Returns the set of option ids that should be DISABLED because the combination
// of (current inventory-relevant selections + that option) has no matching
// inventory with stock. Mirrors the server's subset match
// (inventories.py `inventories_subset_match_strategy`): an inventory matches a
// selection when it contains every selected option; an option is available if
// any matching inventory has quantity > 0.
//
// Scope: pass the container's selections (independent fields use the top-level
// selections; a group uses its own + independent). Only options that appear in
// at least one inventory unit are inventory-tracked; all others are always
// available. Direct product inventories only (no shared inventory groups).
export function resolveUnavailableOptionIds(
  rules: PricingRules,
  selections: Selections
): Set<number> {
  const unavailable = new Set<number>();
  const units = rules.inventoryUnits || [];
  Iif (units.length === 0) return unavailable;
 
  const allFields: PricingField[] = [...rules.fields, ...rules.groupFields];
 
  // Canonicalise id/originalId to a single id per option so bundle option ids,
  // selected ids and inventory unit ids all compare consistently.
  const canonical = new Map<number, number>();
  const fieldIdByOption = new Map<number, number>();
  for (const f of allFields) {
    for (const o of f.options) {
      canonical.set(o.id, o.id);
      fieldIdByOption.set(o.id, f.id);
      Iif (o.originalId != null) {
        canonical.set(o.originalId, o.id);
        fieldIdByOption.set(o.originalId, f.id);
      }
    }
  }
  const canon = (id: number): number => (canonical.has(id) ? (canonical.get(id) as number) : id);
 
  const unitSets = units.map((u) => ({
    quantity: u.quantity,
    optionIds: new Set(u.optionIds.map(canon)),
  }));
  const inventoryOptionIds = new Set<number>();
  for (const u of unitSets) {
    for (const id of u.optionIds) inventoryOptionIds.add(id);
  }
 
  // Collect currently-selected, inventory-tracked option ids grouped by field.
  const selectedByField = new Map<number, Set<number>>();
  const addSelections = (
    fieldValues: Record<number, { selectedOptionIds?: number[] }>
  ) => {
    for (const key of Object.keys(fieldValues)) {
      const fieldId = Number(key);
      const sel = fieldValues[fieldId];
      Iif (!sel || !sel.selectedOptionIds) continue;
      const set = selectedByField.get(fieldId) || new Set<number>();
      for (const oid of sel.selectedOptionIds) {
        const c = canon(oid);
        Iif (inventoryOptionIds.has(c)) set.add(c);
      }
      selectedByField.set(fieldId, set);
    }
  };
  addSelections(selections.fieldValues || {});
  for (const group of selections.groups || []) addSelections(group.fieldValues || {});
 
  const hasGroups = Boolean(selections.groups && selections.groups.length > 0);
  const fieldsToConsider = hasGroups ? allFields : rules.fields;
 
  for (const f of fieldsToConsider) {
    for (const o of f.options) {
      const candidate = canon(o.id);
      Iif (!inventoryOptionIds.has(candidate)) continue; // not inventory-tracked
      // filter = candidate + selected inventory options from OTHER fields.
      const filter: number[] = [candidate];
      for (const [fieldId, set] of selectedByField) {
        Iif (fieldId === f.id) continue;
        for (const sid of set) filter.push(sid);
      }
      const available = unitSets.some(
        (u) => u.quantity > 0 && filter.every((id) => u.optionIds.has(id))
      );
      Iif (!available) unavailable.add(o.id);
    }
  }
  return unavailable;
}