Module: Menuconform::Rules::PriceRules
- Defined in:
- lib/menuconform/rules/price_rules.rb
Overview
PRICE-001..003. PRICE-003 uses a conservative static under-approximation of the minimum reachable total (depth-1 groups only, included_quantity ignored, cheapest conditional price assumed): it can miss negative totals hidden in nested groups, but a total it reports as negative genuinely is reachable- negative under those assumptions. The day 7-8 cart solver supersedes it for exact analysis.
Class Method Summary collapse
- .call(menu) ⇒ Object
- .incomplete_matrices(menu) ⇒ Object
-
.min_group_contribution(menu, group) ⇒ Object
Cheapest satisfying contribution of one group: forced picks take the cheapest options; beyond the minimum, only negative-priced options are added, within max_select / max_total_units / per-option max_quantity.
- .negative_prices(menu) ⇒ Object
- .negative_totals(menu) ⇒ Object
Class Method Details
.call(menu) ⇒ Object
14 15 16 |
# File 'lib/menuconform/rules/price_rules.rb', line 14 def call() negative_prices() + incomplete_matrices() + negative_totals() end |
.incomplete_matrices(menu) ⇒ Object
25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 |
# File 'lib/menuconform/rules/price_rules.rb', line 25 def incomplete_matrices() out = [] .each_conditional_context do |g, o, trigger| next unless trigger[:status] == :valid covered = (o["conditional_prices"] || []).map { |cp| cp["when_modifier_id"] } all = (trigger[:group]["options"] || []).map { |t| t["modifier_id"] } missing = all - covered next if missing.empty? out << Finding.new("PRICE-002", "modifier_group", g["id"], "option #{o['modifier_id']} conditional-price matrix does not cover " \ "#{missing.join(', ')} in trigger group #{trigger[:group]['id']} " \ "(falls back to base price)") end out end |
.min_group_contribution(menu, group) ⇒ Object
Cheapest satisfying contribution of one group: forced picks take the cheapest options; beyond the minimum, only negative-priced options are added, within max_select / max_total_units / per-option max_quantity.
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 |
# File 'lib/menuconform/rules/price_rules.rb', line 57 def min_group_contribution(, group) = (group["options"] || []).select { |o| .modifiers_by_id.key?(o["modifier_id"]) } min_sel = [group["min_select"] || 0, 0].max max_sel = group["max_select"] || .size return 0 if .empty? || min_sel > max_sel || min_sel > .size budget = group["max_total_units"] || Float::INFINITY candidates = .map do |o| unit_price = .resolved_min_price(o) units = if unit_price.negative? o["max_quantity"] || 1 else [o["min_quantity"] || 0, 1].max end { price: unit_price, units: units, cost: unit_price * units } end.sort_by { |c| c[:cost] } total = 0 used_units = 0.0 selected = 0 candidates.each do |c| forced = selected < min_sel break if !forced && selected >= max_sel next unless forced || c[:cost].negative? units = [c[:units], budget - used_units].min units = 1 if forced && units < 1 next if units < 1 total += c[:price] * units used_units += units selected += 1 end total end |
.negative_prices(menu) ⇒ Object
18 19 20 21 22 23 |
# File 'lib/menuconform/rules/price_rules.rb', line 18 def negative_prices() .items_by_id.each_value.filter_map do |it| next unless it["price"].negative? Finding.new("PRICE-001", "item", it["id"], "item price #{it['price']} is negative") end end |
.negative_totals(menu) ⇒ Object
41 42 43 44 45 46 47 48 49 50 51 52 |
# File 'lib/menuconform/rules/price_rules.rb', line 41 def negative_totals() .items_by_id.each_value.filter_map do |it| next if it["price"].negative? # PRICE-001 already anchors this item min_total = it["price"] + .attached_group_ids(it).uniq.sum do |gid| g = .groups_by_id[gid] g ? min_group_contribution(, g) : 0 end next unless min_total.negative? Finding.new("PRICE-003", "item", it["id"], "a valid selection can drive the total to #{min_total}") end end |