{
  "standard": "FSP",
  "module": "core",
  "version": "1.0",
  "registry": "derived_metrics",
  "title": "Расчётные метрики",
  "description": "Канонические формулы, входы, обработка null и правила округления для габаритного и ценового контекста.",
  "in_standard": true,
  "columns": [
    "Метрика",
    "Формула",
    "Входы",
    "Null policy",
    "Единица",
    "Округление",
    "Комментарий"
  ],
  "rows": [
    {
      "Метрика": "sum_sides_cm",
      "Формула": "length_cm + width_cm + height_cm",
      "Входы": "length_cm; width_cm; height_cm",
      "Null policy": "INPUT_MISSING",
      "Единица": "cm",
      "Округление": "none",
      "Комментарий": "Все размеры относятся к тарифицируемой упаковке"
    },
    {
      "Метрика": "max_side_cm",
      "Формула": "MAX(length_cm, width_cm, height_cm)",
      "Входы": "length_cm; width_cm; height_cm",
      "Null policy": "INPUT_MISSING",
      "Единица": "cm",
      "Округление": "none",
      "Комментарий": ""
    },
    {
      "Метрика": "volume_m3",
      "Формула": "length_cm * width_cm * height_cm / 1000000",
      "Входы": "length_cm; width_cm; height_cm",
      "Null policy": "INPUT_MISSING",
      "Единица": "m3",
      "Округление": "half_up:0.000001",
      "Комментарий": ""
    },
    {
      "Метрика": "volumetric_weight_kg",
      "Формула": "length_cm * width_cm * height_cm / volumetric_divisor_cm3_per_kg",
      "Входы": "length_cm; width_cm; height_cm; volumetric_divisor_cm3_per_kg",
      "Null policy": "INPUT_MISSING",
      "Единица": "kg",
      "Округление": "rate_card.rounding",
      "Комментарий": "Делитель задаётся тарифным планом"
    },
    {
      "Метрика": "chargeable_weight_kg",
      "Формула": "MAX(weight_kg, volumetric_weight_kg)",
      "Входы": "weight_kg; volumetric_weight_kg",
      "Null policy": "INPUT_MISSING",
      "Единица": "kg",
      "Округление": "rate_card.rounding",
      "Комментарий": "Иная формула требует новой версии тарифного плана"
    },
    {
      "Метрика": "storage_age_days",
      "Формула": "FLOOR((period_day_end - batch_accepted_at) / 1 сут)",
      "Входы": "batch_accepted_at; period_day_end; time_basis",
      "Null policy": "INPUT_MISSING",
      "Единица": "days",
      "Округление": "floor:1",
      "Комментарий": "Возраст считается от приёмки партии, а не от начала расчётного периода; граница суток по price_rules.time_basis"
    },
    {
      "Метрика": "tier_days",
      "Формула": "MAX(0, MIN(storage_age_days_end, quantity_to) - MAX(storage_age_days_start, quantity_from))",
      "Входы": "storage_age_days_start; storage_age_days_end; quantity_from; quantity_to",
      "Null policy": "INPUT_MISSING",
      "Единица": "days",
      "Округление": "none",
      "Комментарий": "Суток расчётного отрезка, попавших в диапазон возраста; отрезок задан возрастом партии на начало и конец, пустая quantity_to трактуется как бесконечность"
    },
    {
      "Метрика": "unit_sequence",
      "Формула": "ROW_NUMBER() внутри группы tier_scope, порядок — порядок строк документа",
      "Входы": "tier_scope; sku; порядок строк документа",
      "Null policy": "INPUT_MISSING",
      "Единица": "unit",
      "Округление": "none",
      "Комментарий": "Нумерация начинается с 1"
    },
    {
      "Метрика": "tier_units",
      "Формула": "MAX(0, MIN(units_in_scope + 1, quantity_to) - quantity_from)",
      "Входы": "units_in_scope; quantity_from; quantity_to",
      "Null policy": "INPUT_MISSING",
      "Единица": "unit",
      "Округление": "none",
      "Комментарий": "Единиц группы, попавших в диапазон по unit_sequence; units_in_scope — число единиц в группе tier_scope, нумерация с 1"
    }
  ]
}