• Mahmoud Aglan's avatar
    Fix pricing engine and rebuild discounts around recipes and a picker · b3f127c8
    Mahmoud Aglan authored
    Two bugs meant no pricing rule has ever applied correctly:
    
    1. Condition keys never matched. The engine reads min/max/values;
       the wizard wrote min_age/min_children/target_gender and the form
       blade wrote a third set. Ranges saw null bounds and list rules saw
       an empty allow-list, both of which passed, so every rule applied to
       every participant.
    2. Percentages were 100x too small. applyAdjustment divides by 10000
       (basis points) but both screens stored a plain percent, so "20%"
       discounted 0.2%.
    
    They masked each other, which is why the symptom looked like a broken
    engine rather than two bugs — and why everyone moved to the untyped
    super-admin price override instead.
    
    Engine
    - ConditionSchema is now the single owner of the conditions vocabulary;
      builder, engine, simulator and migration all read keys from it.
    - Percent handles all basis-point conversion; nothing else touches the
      raw column.
    - evaluateInList fails closed instead of treating an empty allow-list
      as "match everyone".
    - custom rules no longer auto-apply; they are picker-only.
    - enrollment_timing honours days_before_start (fails closed without a
      program start date instead of silently passing).
    - Global discount cap reads system_settings rather than a hardcoded
      constant with a TODO.
    - New: explain(), audience(), wouldApply(), and role-capped manual
      discounts.
    
    Per-branch
    - pricing_rule_branches pivot so one rule targets many branches,
      instead of one near-identical row per branch that drifts apart.
    
    Stacking
    - is_stackable now defaults to false; best-of-one is the normal case
      and stacking is an explicit opt-in.
    
    Authoring
    - The five-step column editor becomes a recipe gallery plus an Arabic
      sentence, with a live simulator on a real participant and an audience
      count that warns when a rule would hit everyone. Saving a
      conditionless rule is refused.
    
    Checkout
    - ManagesDiscounts trait plus <x-pricing.discount-picker>: branch-scoped,
      searchable, pinned favourites, replace-vs-stack inline, blocked rows
      show why. Wired into CollectPaymentWizard renewals; discount names are
      frozen onto invoice.metadata so receipts survive later rule changes.
    - NewRegistrationWizard now prices through the engine using a
      provisional context built from the form, since the participant row
      does not exist yet. The step-4 guard still checks the base price, so a
      100% discount is not mistaken for an unpriced program.
    
    Migration
    - Rewrites conditions onto the canonical keys and scales percentages to
      basis points. Rules whose conditions cannot be mapped confidently are
      deactivated rather than guessed, with the old JSON kept in
      metadata.legacy_conditions.
    
    Also fixes list and coupon views that rendered the raw column (a 20%
    rule would have displayed as 2000%), and adds the [x-cloak] CSS rule
    that was missing app-wide.
    Co-Authored-By: 's avatarClaude Opus 5 (1M context) <noreply@anthropic.com>
    b3f127c8
DiscountCandidate.php 2.52 KB