Demand & Capacity-Based Pricing Conditions
Complete guide to demand and capacity-based pricing conditions.
Overview
Demand and capacity conditions define HOW MUCH to adjust price based on booking capacity metrics. Each condition specifies a condition_type that determines which capacity metric triggers the adjustment.
Window-based evaluation: In the implemented code, capacity metrics are aggregated across all 15-minute slots for the requested date (full-day aggregate) and the condition’s threshold is compared against that aggregate. Modifiers are then created for API slots whose
start_timelies inside the condition’s date/time eligibility window (defined bydemand_capacity_time_windows). For the authoritative real-implementation reference, see Demand & Capacity Rules — Implementation Notes.
Location:
- Model:
app/models/demand_capacity_condition.rb - Threshold items:
app/models/capacity_condition_item.rb - Date/time eligibility windows:
app/models/demand_capacity_time_window.rb
Key Difference from Date/Time Conditions:
- Date/Time Conditions: Answer “WHEN does the rule apply?” (temporal dimension)
- Demand/Capacity Conditions: Answer “WHAT capacity level triggers this rule?” (capacity dimension)
- Independent: Can coexist with date/time conditions as separate checks within the same rule; both must pass for the rule to fire
- Separate Tables: Own schema to avoid confusion and optimize performance
Database Schema
Parent Table: demand_capacity_conditions
Stores capacity-based condition configurations for each pricing rule.
CREATE TABLE demand_capacity_conditions (
id BIGINT PRIMARY KEY,
pricing_rule_id BIGINT NOT NULL,
condition_type VARCHAR NOT NULL, -- total_seats_booked, remaining_seats, pct_seats_booked, pct_seats_remaining
action_type VARCHAR NOT NULL, -- increase, decrease
value_type VARCHAR NOT NULL, -- percentage, fixed
value INTEGER NOT NULL, -- Amount (basis points or cents)
date_type VARCHAR NOT NULL, -- everyday, specific_date, specific_time, specific_date_time, day_of_week, day_time_of_week
period_mode VARCHAR, -- single, range (optional)
days_of_week INTEGER[], -- Array of day numbers 0-6 (optional)
created_at DATETIME NOT NULL,
updated_at DATETIME NOT NULL,
FOREIGN KEY (pricing_rule_id) REFERENCES pricing_rules(id) ON DELETE CASCADE,
INDEX idx_pricing_rule_id (pricing_rule_id)
)
Nested Table: capacity_condition_items
Stores threshold configurations for each demand condition.
CREATE TABLE capacity_condition_items (
id BIGINT PRIMARY KEY,
demand_capacity_condition_id BIGINT NOT NULL,
relation_type VARCHAR NOT NULL, -- gt, gte, eq, lt, lte, between
value_min INTEGER, -- Lower threshold value (required by validation unless is_anytime is true)
value_max INTEGER, -- Upper threshold
is_no_end_value BOOLEAN DEFAULT false, -- Open-ended upper threshold
is_anytime BOOLEAN DEFAULT false, -- All values apply
created_at DATETIME NOT NULL,
updated_at DATETIME NOT NULL,
FOREIGN KEY (demand_capacity_condition_id) REFERENCES demand_capacity_conditions(id) ON DELETE CASCADE,
INDEX idx_demand_capacity_condition_id (demand_capacity_condition_id)
)
Date/Time Eligibility Window Table: demand_capacity_time_windows
Stores the calendar/clock windows during which the condition is eligible to match. This mirrors the date_time_conditions / condition_items pattern used by Specific Date & Time rules.
CREATE TABLE demand_capacity_time_windows (
id BIGINT PRIMARY KEY,
demand_capacity_condition_id BIGINT NOT NULL,
start_date DATE,
end_date DATE,
start_time TIME,
end_time TIME,
is_no_end_date BOOLEAN DEFAULT false,
is_no_end_time BOOLEAN DEFAULT false,
is_anytime BOOLEAN DEFAULT false,
created_at DATETIME NOT NULL,
updated_at DATETIME NOT NULL,
FOREIGN KEY (demand_capacity_condition_id) REFERENCES demand_capacity_conditions(id) ON DELETE CASCADE,
INDEX idx_demand_capacity_condition_id (demand_capacity_condition_id)
)
Parallel Structure to Date/Time Conditions:
| Concept | Date/Time | Demand/Capacity |
|---|---|---|
| Parent type field | date_type | condition_type (plus its own date_type for eligibility windows) |
| Adjustment fields | action_type, value_type, value | action_type, value_type, value |
| Optional metadata | period_mode, days_of_week | period_mode, days_of_week |
| Range lower/upper | start_date/end_date, start_time/end_time | value_min, value_max for thresholds; demand_capacity_time_windows for eligibility |
| Open-ended flag | is_no_end_date, is_no_end_time | is_no_end_value; is_no_end_date/is_no_end_time on windows |
| All-values flag | is_anytime | is_anytime |
Condition Types
Defined in: CONDITION_TYPES constant
| condition_type | Description | Unit | Example |
|---|---|---|---|
total_seats_booked | Absolute number of seats booked across the rule’s time window | Count | “When 10 or more seats are booked in the window” |
remaining_seats | Available/unbooked seats across the rule’s time window | Count | “When less than 5 seats remain in the window” |
pct_seats_booked | Percentage of total capacity booked across the rule’s time window | Percentage (0-100) | “When 80% or more of the window’s capacity is booked” |
pct_seats_remaining | Percentage of total capacity available across the rule’s time window | Percentage (0-100) | “When 20% or less of the window’s capacity remains” |
Relation Types
Defined in: RELATION_TYPES constant
| relation_type | Symbol | Description | Parameters |
|---|---|---|---|
gt | > | Greater than | value_min only |
gte | ≥ | Greater than or equal | value_min only |
eq | = | Equal to | value_min only |
lt | < | Less than | value_min only |
lte | ≤ | Less than or equal | value_min only |
between | [min, max] | Between (inclusive) | value_min + value_max |
Model Attributes
DemandCapacityCondition (Parent)
| Field | Type | Description | Required |
|---|---|---|---|
pricing_rule_id | Integer | Parent rule (FK) | ✅ Yes |
condition_type | String | Capacity metric being evaluated (total_seats_booked, remaining_seats, pct_seats_booked, pct_seats_remaining) | ✅ Yes |
action_type | String | increase or decrease | ✅ Yes |
value_type | String | percentage or fixed | ✅ Yes |
value | Integer | Adjustment amount (basis points or cents) | ✅ Yes |
date_type | String | Eligibility window type: everyday, specific_date, specific_time, specific_date_time, day_of_week, day_time_of_week | ✅ Yes |
period_mode | String | single or range | ❌ No |
days_of_week | Integer[] | Array of day numbers 0-6 | ❌ No |
created_at | DateTime | Created timestamp | System |
updated_at | DateTime | Updated timestamp | System |
CapacityConditionItem (Nested)
| Field | Type | Description | Required |
|---|---|---|---|
demand_capacity_condition_id | Integer | Parent condition (FK) | ✅ Yes |
relation_type | String | Comparison operator (gt, gte, eq, lt, lte, between) | ✅ Yes |
value_min | Integer | Lower threshold value | Conditional (required unless is_anytime) |
value_max | Integer | Upper threshold | Conditional |
is_no_end_value | Boolean | Open-ended upper threshold | ❌ No (default: false) |
is_anytime | Boolean | All values apply | ❌ No (default: false) |
created_at | DateTime | Created timestamp | System |
updated_at | DateTime | Updated timestamp | System |
DemandCapacityTimeWindow (Nested)
| Field | Type | Description | Required |
|---|---|---|---|
demand_capacity_condition_id | Integer | Parent condition (FK) | ✅ Yes |
start_date | Date | Window start date | Conditional |
end_date | Date | Window end date | Conditional |
start_time | Time | Window start time | Conditional |
end_time | Time | Window end time | Conditional |
is_no_end_date | Boolean | Open-ended end date | ❌ No |
is_no_end_time | Boolean | Open-ended end time | ❌ No |
is_anytime | Boolean | All day / all dates | ❌ No |
created_at | DateTime | Created timestamp | System |
updated_at | DateTime | Updated timestamp | System |
Value Storage Format
IMPORTANT: Values are stored as integers:
- Percentage: Basis points (1% = 100 basis points)
- Fixed Amount: Cents (IDR)
Examples:
- 10% =
1000basis points - 5% =
500basis points - 25% =
2500basis points - IDR 5,000 =
500000cents - IDR 10,000 =
1000000cents
Threshold Value Constraints
| condition_type | value_min range | value_max range | Notes |
|---|---|---|---|
total_seats_booked | 0 - capacity | 0 - capacity | Use actual seat counts |
remaining_seats | 0 - capacity | 0 - capacity | Use actual seat counts |
pct_seats_booked | 0-100 | 0-100 | Percentage values, validated to ≤100 |
pct_seats_remaining | 0-100 | 0-100 | Percentage values, validated to ≤100 |
Model Implementation
DemandCapacityCondition
# app/models/demand_capacity_condition.rb
class DemandCapacityCondition < ApplicationRecord
# ===========================================================================
# ASSOCIATIONS
# ===========================================================================
belongs_to :pricing_rule
has_many :capacity_condition_items, dependent: :destroy
has_many :demand_capacity_time_windows, dependent: :destroy
accepts_nested_attributes_for :capacity_condition_items,
allow_destroy: true,
reject_if: :all_blank
accepts_nested_attributes_for :demand_capacity_time_windows,
allow_destroy: true,
reject_if: :all_blank
# ===========================================================================
# VALIDATIONS
# ===========================================================================
validates :condition_type, presence: true, inclusion: { in: CONDITION_TYPES }
validates :action_type, presence: true, inclusion: { in: ACTION_TYPES }
validates :value_type, presence: true, inclusion: { in: VALUE_TYPES }
validates :value, presence: true, numericality: { only_integer: true, greater_than: 0 }
validates :date_type, presence: true, inclusion: { in: DATE_TYPES }
validate :validate_condition_items_present
# ===========================================================================
# CALLBACKS
# ===========================================================================
after_commit :invalidate_pricing_rule_cache
# ===========================================================================
# SCOPES
# ===========================================================================
scope :by_type, ->(type) { where(condition_type: type) }
scope :active_conditions, -> { joins(:capacity_condition_items).distinct }
# ===========================================================================
# CONSTANTS
# ===========================================================================
CONDITION_TYPES = %w[
total_seats_booked
remaining_seats
pct_seats_booked
pct_seats_remaining
].freeze
ACTION_TYPES = %w[increase decrease].freeze
VALUE_TYPES = %w[percentage fixed].freeze
DATE_TYPES = %w[everyday specific_date specific_time specific_date_time day_of_week day_time_of_week].freeze
# ===========================================================================
# INSTANCE METHODS
# ===========================================================================
# Check if any condition item matches the aggregated capacity value
#
# @param capacity_value [Integer] the capacity metric value aggregated across the rule's time window
# @return [Boolean] true if any item threshold is met
def matches?(capacity_value)
capacity_condition_items.any? { |item| item.matches?(capacity_value) }
end
# Get all matching condition items
#
# @param capacity_value [Integer] the capacity metric value aggregated across the rule's time window
# @return [Array<CapacityConditionItem>]
def matching_items(capacity_value)
capacity_condition_items.select { |item| item.matches?(capacity_value) }
end
# Get human-readable description
#
# @return [String]
def description
items_desc = capacity_condition_items.map(&:description).join(" OR ")
"#{condition_type_label}: #{items_desc}"
end
# Get effect description
#
# @return [String]
def effect_description
value_amount = value_type == "percentage" ? "#{value / 100}%" : "IDR #{value / 100}"
"#{action_type.titleize} by #{value_amount}"
end
private
def condition_type_label
{
"total_seats_booked" => "Total Seats Booked",
"remaining_seats" => "Remaining Seats",
"pct_seats_booked" => "% Seats Booked",
"pct_seats_remaining" => "% Seats Remaining"
}[condition_type] || condition_type
end
def validate_condition_items_present
return if capacity_condition_items.any?
errors.add(:capacity_condition_items, "must have at least one item with threshold")
end
def invalidate_pricing_rule_cache
return if pricing_rule.blank?
restaurant_id = pricing_rule.restaurant_id
return if restaurant_id.blank?
PricingRuleRepository.invalidate_restaurant_cache(restaurant_id)
rescue StandardError => e
Rails.logger.error("[Cache Invalidation Error] demand_capacity_condition_id=#{id} error=#{e.class}: #{e.message}")
end
end
CapacityConditionItem
# app/models/capacity_condition_item.rb
class CapacityConditionItem < ApplicationRecord
# ===========================================================================
# ASSOCIATIONS
# ===========================================================================
belongs_to :demand_capacity_condition
# ===========================================================================
# VALIDATIONS
# ===========================================================================
validates :relation_type, presence: true, inclusion: { in: RELATION_TYPES }
validates :value_min, presence: true, numericality: { only_integer: true, greater_than_or_equal_to: 0 }
validates :value_max, numericality: { only_integer: true, greater_than_or_equal_to: 0 }, allow_nil: true
validates :is_no_end_value, inclusion: { in: [true, false] }
validates :is_anytime, inclusion: { in: [true, false] }
validate :value_max_greater_than_min_for_between
validate :threshold_values_within_range
# ===========================================================================
# CALLBACKS
# ===========================================================================
before_validation :clear_values_if_anytime
after_commit :invalidate_pricing_rule_cache
# ===========================================================================
# SCOPES
# ===========================================================================
scope :by_relation, ->(relation) { where(relation_type: relation) }
scope :for_value, ->(value) { where("? BETWEEN value_min AND COALESCE(value_max, ?)", value, value) }
# ===========================================================================
# CONSTANTS
# ===========================================================================
RELATION_TYPES = %w[gt gte eq lt lte between].freeze
# ===========================================================================
# INSTANCE METHODS
# ===========================================================================
# Check if this item's threshold is met for given value
#
# @param capacity_value [Integer] the capacity metric value
# @return [Boolean]
def matches?(capacity_value)
evaluate_threshold(capacity_value)
end
# Evaluate if a value meets the threshold
#
# @param value [Integer] the value to test
# @return [Boolean]
def evaluate_threshold(value)
return true if is_anytime?
case relation_type
when "gt"
value > value_min
when "gte"
value >= value_min
when "eq"
value == value_min
when "lt"
value < value_min
when "lte"
value <= value_min
when "between"
return value >= value_min if is_no_end_value? || value_max.nil?
value.between?(value_min, value_max)
else
false
end
end
# Get human-readable description of threshold
#
# @return [String]
def description
return "all values" if is_anytime?
case relation_type
when "gt"
"> #{value_min}"
when "gte"
"≥ #{value_min}"
when "eq"
"= #{value_min}"
when "lt"
"< #{value_min}"
when "lte"
"≤ #{value_min}"
when "between"
if is_no_end_value? || value_max.nil?
"≥ #{value_min}"
else
"#{value_min}-#{value_max}"
end
end
end
private
def value_max_greater_than_min_for_between
return unless relation_type == "between"
return if is_no_end_value?
return if value_max.blank?
errors.add(:value_max, "must be >= value_min for between relation") if value_max < value_min
end
def threshold_values_within_range
condition = demand_capacity_condition
return unless condition
case condition.condition_type
when "pct_seats_booked", "pct_seats_remaining"
errors.add(:value_min, "must be between 0-100 for percentage conditions") if value_min > 100
errors.add(:value_max, "must be between 0-100 for percentage conditions") if value_max && value_max > 100
end
end
def clear_values_if_anytime
return unless is_anytime?
self.value_min = nil
self.value_max = nil
end
def invalidate_pricing_rule_cache
return if demand_capacity_condition.blank?
pricing_rule = demand_capacity_condition.pricing_rule
return if pricing_rule.blank?
restaurant_id = pricing_rule.restaurant_id
return if restaurant_id.blank?
PricingRuleRepository.invalidate_restaurant_cache(restaurant_id)
rescue StandardError => e
Rails.logger.error("[Cache Invalidation Error] capacity_condition_item_id=#{id} error=#{e.class}: #{e.message}")
end
end
Integration with PricingRule
# In app/models/pricing_rule.rb
class PricingRule < ApplicationRecord
has_many :date_time_conditions, dependent: :destroy
has_many :demand_capacity_conditions, dependent: :destroy
accepts_nested_attributes_for :date_time_conditions, allow_destroy: true, reject_if: :all_blank
accepts_nested_attributes_for :demand_capacity_conditions, allow_destroy: true, reject_if: :all_blank
end
API Endpoints
Create/Update Pricing Rule with Demand Conditions
POST/PATCH /api/partner/v1/pricing_rules
Content-Type: application/json
{
"pricing_rule": {
"name": "Peak Occupancy Surcharge",
"restaurant_id": "123",
"rule_type": "dynamic",
"demand_capacity_conditions_attributes": [
{
"condition_type": "total_seats_booked",
"action_type": "increase",
"value_type": "percentage",
"value": 500,
"capacity_condition_items_attributes": [
{
"relation_type": "between",
"value_min": 10,
"value_max": 21
}
]
}
]
}
}
Example Payloads
1. High Demand Surcharge (From Screenshot)
“When 10-21 seats are booked, increase price by 5%”
{
"demand_capacity_condition": {
"condition_type": "total_seats_booked",
"action_type": "increase",
"value_type": "percentage",
"value": 500,
"capacity_condition_items_attributes": [
{
"relation_type": "between",
"value_min": 10,
"value_max": 21
}
]
}
}
Applied when: Total booked between 10-21 seats Effect: Price increases by 5% of base price
2. High Occupancy Surcharge
“When 80% or more seats are booked, increase price by 15%”
{
"demand_capacity_condition": {
"condition_type": "pct_seats_booked",
"action_type": "increase",
"value_type": "percentage",
"value": 1500,
"capacity_condition_items_attributes": [
{
"relation_type": "gte",
"value_min": 80
}
]
}
}
Applied when: Occupancy ≥ 80% Effect: Price increases by 15%
3. Low Occupancy Discount
“When fewer than 5 seats are booked, decrease price by 20%”
{
"demand_capacity_condition": {
"condition_type": "total_seats_booked",
"action_type": "decrease",
"value_type": "percentage",
"value": 2000,
"capacity_condition_items_attributes": [
{
"relation_type": "lt",
"value_min": 5
}
]
}
}
Applied when: Total booked < 5 seats Effect: Price decreases by 20%
4. Last-Minute Premium
“When fewer than 3 seats remain, increase price by IDR 75,000”
{
"demand_capacity_condition": {
"condition_type": "remaining_seats",
"action_type": "increase",
"value_type": "fixed",
"value": 7500000,
"capacity_condition_items_attributes": [
{
"relation_type": "lt",
"value_min": 3
}
]
}
}
Applied when: Remaining capacity < 3 Effect: Price increases by IDR 75,000
5. Multiple Thresholds (OR Logic)
“When 10+ seats OR 15+ seats are booked, increase price by 10%”
{
"demand_capacity_condition": {
"condition_type": "total_seats_booked",
"action_type": "increase",
"value_type": "percentage",
"value": 1000,
"capacity_condition_items_attributes": [
{
"relation_type": "gte",
"value_min": 10
},
{
"relation_type": "gte",
"value_min": 15
}
]
}
}
Applied when: Total booked ≥ 10 OR ≥ 15 (any match applies) Effect: Price increases by 10%
Multiple Conditions
Multiple Items on Same Condition (OR Logic)
A single demand condition can have multiple threshold items:
condition = DemandCapacityCondition.create!(
condition_type: "total_seats_booked",
action_type: "increase",
value_type: "percentage",
value: 1000,
capacity_condition_items_attributes: [
{ relation_type: "gte", value_min: 10 },
{ relation_type: "gte", value_min: 15 }
]
)
# Applied when: 10+ seats booked OR 15+ seats booked (any match applies)
Multiple Conditions on Same Rule (All Apply)
A pricing rule can have multiple demand conditions:
pricing_rule.demand_capacity_conditions = [
{
condition_type: "total_seats_booked",
action_type: "increase",
value_type: "percentage",
value: 500,
capacity_condition_items_attributes: [
{ relation_type: "gte", value_min: 5 }
]
},
{
condition_type: "pct_seats_booked",
action_type: "increase",
value_type: "percentage",
value: 1000,
capacity_condition_items_attributes: [
{ relation_type: "gte", value_min: 50 }
]
}
]
Evaluation:
- Condition 1: If ≥5 seats booked → +5%
- Condition 2: If ≥50% booked → +10% (additional)
- All matching conditions apply (cumulative effect)
Combining Date/Time Eligibility with Capacity Thresholds
A Demand & Capacity condition carries its own date/time eligibility via demand_capacity_time_windows. The condition only matches when:
- The requested date/time falls inside one of its windows, and
- The aggregate capacity for the date meets one of its threshold items.
{
"pricing_rule": {
"name": "Peak Hour Premium",
"restaurant_id": 123,
"rule_type": "demand_capacity",
"demand_capacity_conditions_attributes": [
{
"condition_type": "pct_seats_booked",
"action_type": "increase",
"value_type": "percentage",
"value": 1500,
"date_type": "day_time_of_week",
"days_of_week": [5, 6],
"capacity_condition_items_attributes": [
{
"relation_type": "gte",
"value_min": 80
}
],
"demand_capacity_time_windows_attributes": [
{
"start_time": "18:00",
"end_time": "22:00"
}
]
}
]
}
}
Scenario: Friday/Saturday 6-10 PM (eligibility window) AND 80%+ capacity for the date (threshold) Effect: Modifiers are created for API slots between 18:00 and 22:00 when the aggregate capacity condition is met.
Validations
DemandCapacityCondition Validations
condition_type: Must be one of CONDITION_TYPESaction_type: Must beincreaseordecreasevalue_type: Must bepercentageorfixedvalue: Must be integer > 0date_type: Must be one of DATE_TYPEScapacity_condition_items: Must have at least one itemdemand_capacity_time_windows: Required when thedate_typeuses slots or specific windows
CapacityConditionItem Validations
relation_type: Must be one of RELATION_TYPESvalue_min: Must be integer ≥ 0value_max: Optional, but if provided must be ≥ value_minis_no_end_value: Must be booleanis_anytime: Must be boolean
Custom Validations
- value_max_required_for_between: If
relation_typeis “between”,value_maxmust be present oris_no_end_valuemust be true - value_max_greater_than_min: If
relation_typeis “between” andvalue_maxis provided,value_max≥value_min - percentage_thresholds_valid: For percentage condition_types, values must be 0-100
- anytime_clears_values: If
is_anytimeis true,value_minandvalue_maxare cleared to null
Edge Cases & Rules
is_anytime Behavior:
- When
is_anytime = true: Condition matches ALL capacity values (regardless of value_min/value_max) - When
is_anytime = true: Stored value_min and value_max are cleared (set to null) - When
is_anytime = true+relation_type= “between”: Treated as “all values”, not requiring value_max
is_no_end_value Behavior:
- When
is_no_end_value = true+relation_type= “between”: Means “from value_min onwards” (open-ended upper bound) - Example:
value_min: 100,is_no_end_value: truemeans “≥ 100” with no upper limit value_maxshould be ignored whenis_no_end_value = true(but not validated for consistency)
period_mode & days_of_week (Future-Proofing):
- Currently optional and not enforced, but structure mirrors DATE_TIME_CONDITIONS for future extensibility
period_mode: single= condition applies to a single capacity occurrenceperiod_mode: range= condition applies across a range of capacities (future)days_of_week: Array like[5, 6]for Friday/Saturday (future use with temporal conditions)
Approval Workflow:
- Controlled at PricingRule level via
requires_approvalboolean field - When
requires_approval = true: All changes to conditions require management approval before activation - When
requires_approval = false: Conditions are applied immediately without approval - See 005-PRICING-RULES.md for approval workflow details
Scopes
DemandCapacityCondition Scopes
# Find conditions by capacity metric type
DemandCapacityCondition.by_type('total_seats_booked')
DemandCapacityCondition.by_type('remaining_seats')
DemandCapacityCondition.by_type('pct_seats_booked')
DemandCapacityCondition.by_type('pct_seats_remaining')
# Find conditions that have at least one item
DemandCapacityCondition.active_conditions
CapacityConditionItem Scopes
# Find items by relation type
CapacityConditionItem.by_relation('gte')
CapacityConditionItem.by_relation('between')
# Find items matching a specific value
CapacityConditionItem.for_value(80)
Database Migration
# db/migrate/[timestamp]_create_demand_capacity_conditions.rb
class CreateDemandCapacityConditions < ActiveRecord::Migration[6.0]
def change
create_table :demand_capacity_conditions do |t|
t.bigint :pricing_rule_id, null: false
t.string :condition_type, null: false
t.string :action_type, null: false
t.string :value_type, null: false
t.integer :value, null: false
t.string :period_mode
t.integer :days_of_week, array: true
t.timestamps
end
add_foreign_key :demand_capacity_conditions, :pricing_rules, on_delete: :cascade
add_index :demand_capacity_conditions, :pricing_rule_id
# Nested table for threshold items
create_table :capacity_condition_items do |t|
t.bigint :demand_capacity_condition_id, null: false
t.string :relation_type, null: false
t.integer :value_min, null: false
t.integer :value_max
t.boolean :is_no_end_value, default: false
t.boolean :is_anytime, default: false
t.timestamps
end
add_foreign_key :capacity_condition_items, :demand_capacity_conditions, on_delete: :cascade
add_index :capacity_condition_items, :demand_capacity_condition_id
add_index :capacity_condition_items, [:relation_type, :value_min]
end
end
Cache Invalidation
Callback: after_commit :invalidate_pricing_rule_cache
Automatically invalidates parent rule’s cache after:
- Create
- Update
- Destroy
Cache key: "restaurant:#{restaurant_id}:pricing_rules"
Global Pricing Limits
All conditions are validated against restaurant’s global pricing limits:
Enforced at: PricingRule model level
Validation: validate_global_pricing_limits
Limits check:
- Increase/decrease values don’t exceed global limits
- Value type matches global limit type
See: 007-GLOBAL-PRICING-SETTINGS.md
Performance Considerations
- Separate Table: Avoids polluting
date_time_conditionstable - Efficient Querying: Use
.includes(:demand_capacity_conditions, :capacity_condition_items)when loading rules - Indexed Lookups: Frequent queries on
pricing_rule_idandrelation_type - Lazy Evaluation: Only evaluate conditions that match current capacity state
- Cache Strategy: Invalidate rule cache on condition changes
Testing Strategy
Unit Tests
Location: spec/models/demand_capacity_condition_spec.rb
RSpec.describe DemandCapacityCondition do
describe '#matches?' do
it 'correctly evaluates gte relation' do
condition = build(:demand_capacity_condition, condition_type: 'pct_seats_booked')
condition.capacity_condition_items.build(relation_type: 'gte', value_min: 80)
expect(condition.matches?(80)).to be true
expect(condition.matches?(79)).to be false
end
it 'correctly evaluates between relation' do
condition = build(:demand_capacity_condition, condition_type: 'total_seats_booked')
condition.capacity_condition_items.build(relation_type: 'between', value_min: 10, value_max: 21)
expect(condition.matches?(15)).to be true
expect(condition.matches?(9)).to be false
expect(condition.matches?(22)).to be false
end
end
describe 'validations' do
it 'requires at least one capacity_condition_item' do
condition = build(:demand_capacity_condition)
expect(condition).not_to be_valid
end
it 'validates percentage thresholds are 0-100' do
condition = build(:demand_capacity_condition, condition_type: 'pct_seats_booked')
condition.capacity_condition_items.build(relation_type: 'gte', value_min: 150)
expect(condition).not_to be_valid
end
end
end
Integration Tests
Location: spec/requests/api/partner/v1/pricing_rules_spec.rb
RSpec.describe 'API::Partner::V1::PricingRules', type: :request do
describe 'POST/PATCH demand_capacity_conditions' do
it 'creates demand condition on rule' do
rule = create(:pricing_rule)
patch "/api/partner/v1/pricing_rules/#{rule.id}",
params: {
pricing_rule: {
demand_capacity_conditions_attributes: [
{
condition_type: 'total_seats_booked',
action_type: 'increase',
value_type: 'percentage',
value: 500,
capacity_condition_items_attributes: [
{
relation_type: 'between',
value_min: 10,
value_max: 21
}
]
}
]
}
}
expect(response).to have_http_status(:ok)
expect(rule.reload.demand_capacity_conditions.size).to eq(1)
expect(rule.demand_capacity_conditions.first.capacity_condition_items.size).to eq(1)
end
end
end
Structure Comparison to Date/Time Conditions
| Aspect | Date/Time | Demand/Capacity |
|---|---|---|
| Parent Field | date_type | condition_type (with its own date_type for windows) |
| Nested Items | Time/date slots | Threshold items and date/time eligibility windows |
| Slot Fields | start_date, end_date, start_time, end_time | demand_capacity_time_windows for eligibility; value_min, value_max for thresholds |
| Open-ended Flag | is_no_end_date, is_no_end_time | is_no_end_value; window-level is_no_end_date/is_no_end_time |
| All-value Flag | is_anytime | is_anytime |
| Relation Concept | Implicit (start/end) | Explicit (gt, gte, eq, lt, lte, between) |
| Use Case | “WHEN does rule apply?” | “WHAT capacity triggers rule?” |
Related Documentation
- 005-PRICING-RULES.md - Parent rule model
- 006-DATE-TIME-CONDITIONS.md - Date/time-based conditions
- 007-GLOBAL-PRICING-SETTINGS.md - Global limits
- 004-API-ENDPOINTS.md - API documentation
- 012-DEVELOPMENT-STANDARDS.md - Development standards
- Dynamic Pricing — Capacity & Demand PRD - Product PRD (scenarios, UX, business rules)
- Dynamic Pricing — Date & Time PRD - Product PRD (date/time rule configuration)
- Dynamic Pricing — AI (Rapidcanvas) PRD - Product PRD (AI-driven rule configuration)
- Dynamic Pricings 2.0 — Customer Booking Flow (HH Pegasus) - Product PRD (customer-facing browse → date → time → checkout flow)
- Dynamic Pricings 2.0 — HH Server Integration - HH Server ↔ pricing-engine integration
- Customer Booking API - pricing-engine endpoints consumed by the booking flow
- Capacity & Demand Implementation Plan - Step-by-step implementation plan
- Demand & Capacity Rules — Implementation Notes - Authoritative real-implementation reference