Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Global Pricing Settings

Restaurant-level pricing limits that constrain all pricing rules.

Overview

Global pricing settings define maximum price adjustments allowed for a restaurant. All pricing rules must respect these limits.

Key Concepts:

  • One setting per restaurant (1:1 relationship)
  • Auto-creates with defaults on first GET
  • Controls increase AND decrease limits separately
  • Enforced during pricing rule validation

Model

Location: app/models/global_pricing_setting.rb

Attributes

FieldTypeDescriptionDefault
restaurant_idStringRestaurant identifier (unique)Required
price_increase_limit_typeStringLimit type for increases"percentage"
price_increase_limit_valueDecimalMax increase (decimal % or cents)50.0 (50%)
price_decrease_limit_typeStringLimit type for decreases"percentage"
price_decrease_limit_valueDecimalMax decrease (decimal % or cents)50.0 (50%)

Limit Types

Defined in: LIMIT_TYPES constant

ValueDescriptionStorage Unit
percentagePercentage-based limitDecimal percentage (50.0 = 50%)
fixed_amountFixed amount limitDecimal amount (100.0 = 100 THB)

Value Storage Format

IMPORTANT: The API accepts basis points for percentages, but converts them to decimal format for storage:

API Input → Database Storage:

  • Percentage (API): Basis points (5000) → Stored as: Decimal (50.0)
  • Fixed Amount (API): Cents (10000) → Stored as: Decimal (100.0)

Conversion Rules:

  • Percentage values >= 100 are treated as basis points and divided by 100
  • Percentage values < 100 are treated as already in decimal format
  • Fixed amount values are stored as-is (in cents/minor currency units)

Examples:

API Input: 5000 (basis points) → Stored: 50.0 (decimal %)
API Input: 50.0 (decimal) → Stored: 50.0 (decimal %)
API Input: 2500 (basis points) → Stored: 25.0 (decimal %)
Fixed: 10000 (cents) → Stored: 10000.0 (cents)

Why this matters:

  • Ensures compatibility with internal percentage calculations
  • Prevents confusion when comparing with pricing rule values (which use basis points internally)
  • Automatic conversion in model setters handles both formats

Validations

  • restaurant_id: Must be present and unique
  • price_increase_limit_type: Must be percentage or fixed_amount (if present)
  • price_decrease_limit_type: Must be percentage or fixed_amount (if present)
  • price_increase_limit_value: Must be >= 0 (if present)
  • price_decrease_limit_value: Must be >= 0 (if present)

Auto-Create Behavior

Controller: app/controllers/api/partner/v1/global_pricing_settings_controller.rb

When you GET a setting that doesn’t exist, it’s automatically created with defaults:

GlobalPricingSetting.find_or_create_by!(restaurant_id: restaurant_id) do |setting|
  setting.price_increase_limit_type = "percentage"
  setting.price_increase_limit_value = 50.0  # 50% as decimal (or 5000 basis points - auto-converted)
  setting.price_decrease_limit_type = "percentage"
  setting.price_decrease_limit_value = 50.0  # 50% as decimal (or 5000 basis points - auto-converted)
end

Result: Every restaurant gets default 50% limits on first access.

Note: The model automatically converts basis points to decimal percentages when saving.

API Endpoints

Base Route: Singular resource pattern

resource :global_pricing_setting, only: [:show, :update]

GET Setting (with Auto-Create)

Endpoint: GET /api/partner/v1/global_pricing_setting

Headers:

  • Authorization: Bearer {token}
  • X-Restaurant-Id: {restaurant_id} (required)

Response: 200 OK

{
  "id": 1,
  "restaurant_id": "rest_123",
  "price_increase_limit_type": "percentage",
  "price_increase_limit_value": "50.0",
  "price_decrease_limit_type": "percentage",
  "price_decrease_limit_value": "50.0",
  "created_at": "2025-01-01T00:00:00Z",
  "updated_at": "2025-01-01T00:00:00Z"
}

Note:

  • If setting doesn’t exist, it’s created with defaults automatically
  • Values are returned as decimal strings (50.0 = 50%)

UPDATE Setting

Endpoint: PATCH /api/partner/v1/global_pricing_setting

Headers:

  • Authorization: Bearer {token}
  • X-Restaurant-Id: {restaurant_id} (required)

Request Body:

{
  "global_pricing_setting": {
    "price_increase_limit_type": "percentage",
    "price_increase_limit_value": 2500,
    "price_decrease_limit_type": "fixed_amount",
    "price_decrease_limit_value": 10000
  }
}

Value Formats:

  • For percentage type: Send basis points (2500 = 25%, will be converted to 25.0)
  • For fixed_amount type: Send cents (10000 = 100 THB)

Response: 200 OK (same format as GET)

Restaurant Context

Settings are accessed per restaurant via:

  1. Header (recommended):

    X-Restaurant-Id: rest_123
    
  2. Query parameter (fallback):

    ?restaurant_id=rest_123
    

Implementation: determine_target_restaurants(require_single: true) helper from BaseController

Enforcement

Limits are enforced when:

  1. Creating a pricing rule
  2. Updating a pricing rule

Validation Location: app/models/pricing_rule.rb

validate :validate_global_pricing_limits, 
         if: -> { restaurant_id.present? && date_time_conditions.any? }

Validation Logic

For each date_time_condition:

  • Check increase: If action_type = “increase”, value must not exceed increase limit
  • Check decrease: If action_type = “decrease”, value must not exceed decrease limit
  • Type matching: Condition’s value_type must match global limit type

Example Error:

{
  "errors": [
    "Condition 1: increase value 10000 exceeds global limit of 5000 basis points"
  ]
}

Class Methods

for_restaurant

Find or initialize settings for a restaurant:

setting = GlobalPricingSetting.for_restaurant('rest_123')

Returns existing or new (unsaved) instance.

Instance Methods

price_increase_within_limit?

Check if price increase is within global limit:

setting.price_increase_within_limit?(10000, 12000)
# => true if 20% increase is within limit

price_decrease_within_limit?

Check if price decrease is within global limit:

setting.price_decrease_within_limit?(10000, 8000)
# => true if 20% decrease is within limit

Example Usage

Get Setting (First Time)

curl -X GET \
  https://price-engine.hh-engineering.my.id/api/partner/v1/global_pricing_setting \
  -H "Authorization: Bearer {token}" \
  -H "X-Restaurant-Id: rest_123"

Response: Auto-created with 50% defaults

Update to 25% Limits

curl -X PATCH \
  https://price-engine.hh-engineering.my.id/api/partner/v1/global_pricing_setting \
  -H "Authorization: Bearer {token}" \
  -H "X-Restaurant-Id: rest_123" \
  -H "Content-Type: application/json" \
  -d '{
    "global_pricing_setting": {
      "price_increase_limit_value": 2500,
      "price_decrease_limit_value": 2500
    }
  }'

Update to Mixed Types

curl -X PATCH \
  https://price-engine.hh-engineering.my.id/api/partner/v1/global_pricing_setting \
  -H "Authorization: Bearer {token}" \
  -H "X-Restaurant-Id: rest_123" \
  -H "Content-Type: application/json" \
  -d '{
    "global_pricing_setting": {
      "price_increase_limit_type": "percentage",
      "price_increase_limit_value": 3000,
      "price_decrease_limit_type": "fixed_amount",
      "price_decrease_limit_value": 5000000
    }
  }'

Singular Resource Pattern

Route Definition:

resource :global_pricing_setting, only: [:show, :update]

URLs:

  • GET /api/partner/v1/global_pricing_setting (no ID in URL)
  • PATCH /api/partner/v1/global_pricing_setting (no ID in URL)

Rationale: One setting per restaurant, identified by X-Restaurant-Id header, not by URL parameter.