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;
}
|