---
description: Pricing engine resolution. No fixed prices. Snapshot principle.
globs: ["app/Domain/Pricing/**", "app/Domain/Financial/Services/**"]
---

# Pricing Engine

## Core Principle

There are NO fixed prices. Every price is calculated at sale/enrollment time.

## Resolution Order (10 steps, no shortcuts)

1. Find base price (fail if none exists)
2. Gather applicable rules (active, in date range, matching branch/target)
3. Evaluate conditions against participant context
4. Filter non-stackable conflicts (highest priority wins)
5. Apply rules in priority order (ASC)
6. Apply rule-level caps (max_discount_percent per rule)
7. Apply promotion/coupon (if any)
8. Apply global maximum discount (academy setting, default 50%)
9. Floor check (never negative)
10. Return PriceResult

## Rule Types (exact 13)

`age`, `membership_duration`, `family_size`, `sibling_order`, `classification`,
`enrollment_timing`, `enrollment_volume`, `seasonal`, `gender`, `branch`,
`day_time`, `loyalty`, `custom`

## Adjustment Types

- `percentage_discount`: subtract X% from current price
- `fixed_discount`: subtract X piasters
- `fixed_price`: override to exact amount
- `percentage_increase`: add X%

## Snapshot Principle

Once a price is calculated and placed on an invoice:
- It is FROZEN forever on that invoice
- Changing rules/base prices does NOT affect existing invoices
- Next renewal/purchase recalculates with current rules

## No Base Price = HARD FAIL

If PricingService cannot find an active base_price for the item + branch + date:
- Throw exception
- Block the sale/enrollment
- Show error: "لا يوجد سعر محدد"
- NEVER default to 0 or guess

## Stackable vs Non-Stackable

- `stackable = true`: rule applies alongside others (cumulative discounts)
- `stackable = false`: if multiple non-stackable rules match, ONLY highest priority applies
- Stackable and non-stackable rules CAN coexist (stackable applies on top of the winning non-stackable)
