Appearance
Архитектура: ОДН / дельта ОДПУ−ИПУ, регистр дома, годовая корректировка
Целевая схема для учёта положительной и отрицательной разницы между общедомовым прибором и суммой индивидуальных потреблений (и нормативов), в терминах ПП РФ №354 (ОДН / КР на СОИ).
Размещение данных: billing-service (оперативный учёт периодов и распределения к ЛС) + чтение справочников и показаний из core-service.
1. Принципы
| Принцип | Содержание |
|---|---|
| Дом — отдельный контур объёма | Перенос «минуса» ОДН и учёт «плюса» до распределения — не в разрезе ЛС, а в регистре по дому и ресурсу (линии ОДН). |
| ЛС — только неотрицательные начисления ОДН в квитанции | При отрицательной сырой дельте жителям за месяц не выставляется отрицательная строка ОДН; дельта уходит в перенос (carry). |
| Идемпотентность периода | Одна запись «снимка месяца» на пару (дом, услуга ОДН, период); перерасчёт периода перезаписывает снимок и перевыставляет Charge (черновик → применение). |
| Связь с доменом core | house_id и house_service_id (общедомовая услуга с is_common_service = true, связь с индивидуальной через related_individual_service_id) уже есть в core; billing хранит ссылки по ID. |
2. Целевая схема таблиц (PostgreSQL)
2.1 Регистр переноса объёма — house_resource_carry_balances
Один текущий остаток объёма (в единицах учёта ресурса: м³, Гкал, кВт·ч) на пару дом + линия ОДН. Остаток знаковый: отрицательный = «дом «должен» жителям по объёму» после прошлых периодов; положительный = «есть нераспределённый плюс» после взаимозачёта.
sql
-- Регистр переноса дельты по дому (объём, не рубли)
CREATE TABLE house_resource_carry_balances (
id BIGSERIAL PRIMARY KEY,
house_id BIGINT NOT NULL, -- FK → core.houses
house_service_id BIGINT NOT NULL, -- FK → core.house_services (ОДН-линия)
resource_unit_code VARCHAR(32) NOT NULL, -- м3, gcal, kwh — денормализация для контроля
carry_volume NUMERIC(20, 6) NOT NULL
DEFAULT 0, -- знаковый остаток к зачёту в след. месяцах
last_reconciled_period VARCHAR(7), -- YYYY-MM последнего полного расчёта
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
UNIQUE (house_id, house_service_id)
);
CREATE INDEX idx_house_resource_carry_house ON house_resource_carry_balances (house_id);Семантика carry_volume после расчёта месяца:
- К существующему
carry_volumeприбавляется сырая дельта месяца (после нормативного ограничения «плюса», см. ниже), минус то, что фактически распределено на ЛС за месяц. - Упрощённо:
carry_volume_new = carry_volume_old + volume_available_for_distribution - distributed_sum
гдеvolume_available_for_distributionуже учитывает правило «отрицательная сырая дельта → к переносу, жителям 0».
Альтернатива: хранить только carry_volume и вести полную историю в house_odn_period_facts (рекомендуется).
2.2 Факты расчёта за период — house_odn_period_facts
Журнал по месяцу: входы (ОДПУ, ΣИПУ, нормативы), промежуточные величины, сколько ушло жителям, сколько в перенос, сверхнорматив УК.
sql
CREATE TABLE house_odn_period_facts (
id BIGSERIAL PRIMARY KEY,
house_id BIGINT NOT NULL,
house_service_id BIGINT NOT NULL, -- ОДН house_service
billing_period VARCHAR(7) NOT NULL, -- YYYY-MM
-- Входные объёмы за период (уже в единицах ресурса после методики расчёта месяца)
volume_odpu NUMERIC(20, 6) NOT NULL,
volume_sum_ipu NUMERIC(20, 6) NOT NULL DEFAULT 0,
volume_sum_normative NUMERIC(20, 6) NOT NULL DEFAULT 0,
-- Сырая дельта: V_odpu - Σ V_ipu - Σ V_norm
raw_delta_volume NUMERIC(20, 6) NOT NULL,
-- После правил ПП 354 для «плюса»: потолок по нормативу ОДН (если применимо)
normative_cap_volume NUMERIC(20, 6), -- NULL = не применялось
volume_after_normative_cap NUMERIC(20, 6) NOT NULL, -- сколько максимум можно отнести на жителей «плюсом»
uk_absorption_volume NUMERIC(20, 6) NOT NULL DEFAULT 0, -- сверхнорматив / убыток УК (объём)
-- Взаимозачёт с регистром на начало месяца
carry_opening_volume NUMERIC(20, 6) NOT NULL DEFAULT 0,
volume_net_before_distribution NUMERIC(20, 6) NOT NULL, -- что реально распределяем пропорционально S
-- Распределение
distributed_volume NUMERIC(20, 6) NOT NULL DEFAULT 0, -- Σ по ЛС (должно совпасть с суммой детализации)
carry_closing_volume NUMERIC(20, 6) NOT NULL, -- остаток регистра после месяца (= новый carry)
-- Управление и аудит
calculation_job_id BIGINT, -- FK → billing calculation_jobs
protocol_oss_full_fact_flag BOOLEAN NOT NULL DEFAULT FALSE, -- «100% факта» по ОСС / ТСЖ (упрощённо)
comment TEXT,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
UNIQUE (house_id, house_service_id, billing_period)
);
CREATE INDEX idx_house_odn_period_house_period ON house_odn_period_facts (house_id, billing_period);Заполнение volume_after_normative_cap и uk_absorption_volume (положительная сыря дельта):
- Если действует ограничение «не более норматива ОДН» и нет флага полного факта:
volume_after_normative_cap = min(raw_delta_volume, normative_cap_volume)uk_absorption_volume = max(0, raw_delta_volume - normative_cap_volume)(объём на УК). - Если протокол ОСС / ТСЖ:
volume_after_normative_cap = raw_delta_volume,uk_absorption_volume = 0.
Отрицательная сырая дельта:
volume_after_normative_cap = 0(на жителей в этом месяце не распределяем «минус» как начисление).- Весь
raw_delta_volume(отрицательный) участвует в переносе: увеличивает «долг дома перед жителями по объёму» вcarry_closing_volume(через формулу ниже).
Взаимозачёт с переносом (псевдокод):
text
opening = house_resource_carry_balances.carry_volume // на начало месяца
capped_plus = volume_after_normative_cap // >= 0
// Объём, который пойдёт в распределение по площади (не может быть отрицательным в квитанции)
to_distribute = max(0, capped_plus + opening) // «плюс» съедает накопленный «минус»
distributed = Σ по ЛС долей (Si/S_total) * to_distribute
carry_closing = opening + raw_delta_effective - distributed
// где raw_delta_effective для учёта: для отрицательной сырой дельты без распределения:
// фактически carry_closing = opening + raw_delta_volume (если capped_plus=0 и distributed=0)
// Унифицированно: carry_closing = opening + (capped_plus - distributed) + (raw_delta_volume - capped_plus - uk_absorption_volume)
// Рекомендуется в коде разбить на понятные шаги и покрыть тестами LS10 и положительный ОДН.Точную формулу фиксируйте в сервисе расчёта одним модульным блоком + golden tests.
2.3 Детализация по ЛС — house_odn_period_account_allocations
Сколько объёма ОДН получил каждый ЛС за период (для сверки с Charge и актами).
sql
CREATE TABLE house_odn_period_account_allocations (
id BIGSERIAL PRIMARY KEY,
house_odn_period_fact_id BIGINT NOT NULL
REFERENCES house_odn_period_facts(id) ON DELETE CASCADE,
personal_account_id BIGINT NOT NULL,
share_area_m2 NUMERIC(18, 4) NOT NULL,
allocated_volume NUMERIC(20, 6) NOT NULL,
amount_rub NUMERIC(18, 2), -- опционально, если дублируете из Charge
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
UNIQUE (house_odn_period_fact_id, personal_account_id)
);
CREATE INDEX idx_odn_alloc_pa ON house_odn_period_account_allocations (personal_account_id);2.4 Годовая корректировка ОДН — house_odn_annual_adjustments
Разовая операция за год (п. 44 / методика региона): сводка 12 месяцев, доначисление или возврат.
sql
CREATE TABLE house_odn_annual_adjustments (
id BIGSERIAL PRIMARY KEY,
house_id BIGINT NOT NULL,
house_service_id BIGINT NOT NULL,
calendar_year INT NOT NULL, -- например 2025 за периоды 2025-01 … 2025-12
total_volume_delta_year NUMERIC(20, 6) NOT NULL, -- итог по фактам/регистру (методика)
adjustment_volume NUMERIC(20, 6) NOT NULL, -- что разово доначислить (+) или сторнировать (-)
status VARCHAR(20) NOT NULL DEFAULT 'DRAFT', -- DRAFT | APPLIED
applied_at TIMESTAMPTZ,
billing_cycle_id BIGINT, -- связь с проводками при применении
comment TEXT,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
UNIQUE (house_id, house_service_id, calendar_year)
);Детализация по ЛС годовой корректировки — отдельная таблица или строки Charge с типом annual_odn_adjustment.
3. Рекомендуемая архитектура модулей (billing-service)
┌─────────────────────────────────────────────────────────────────┐
│ Calculation pipeline (месяц) │
└─────────────────────────────────────────────────────────────────┘
│
├─ 1. Load core: house, house_services (индивид. + ОДН), meters, readings
│
├─ 2. OdnVolumeEngine (дом × ОДН-услуга)
│ • Считает volume_odpu, volume_sum_ipu, volume_sum_normative
│ • raw_delta_volume
│ • normative_cap, OSS flag → volume_after_normative_cap, uk_absorption_volume
│ • FOR UPDATE house_resource_carry_balances
│ • to_distribute, distributed по Si/S_total
│ • UPSERT house_odn_period_facts + allocations
│ • UPDATE carry_volume = carry_closing
│
├─ 3. TariffEngine → amount на ЛС из allocated_volume (или один шаг с п.2)
│
├─ 4. Persist Charge (DRAFT) по house_service_id ОДН + personal_account_id
│
└─ 5. Apply billing cycle → Ledger (без отрицательных ОДН-строк; корректировки — отдельными типами проводок)- Идempotency: пересчёт месяца удаляет/перезаписывает
house_odn_period_facts+allocationsи связанныеChargeэтого job/периода. - Core не хранит перенос объёма — только счётчики и услуги; billing — источник истины по регистру и фактам ОДН.
4. Связь с существующими сущностями Nimbus
| Уже есть | Роль |
|---|---|
HouseService (ОДН + related_individual_service_id) | Идентификация линии ОДН и связи с индивидуальной услугой |
UtilityService.CalculationFormulaCode + FormulaFlags (OdnCalculationRule, …) | Правила норматива / метода; читает OdnVolumeEngine |
Meter + показания | Входы volume_odpu, volume_sum_ipu |
Charge | Итог в рублях по ЛС за период |
house_resource_carry_balances (новая) | Перенос знаковой дельты между месяцами |
5. Расширения (по мере зрелости)
- Отдельное поле на
housesилиhouse_services:odn_full_fact_oss_protocol_ref(ссылка на файл/номер протокола). - Версионирование норматива ОДН по региону и дате — справочник в core или billing.
- Отчёт «сверка ОДН по дому за год» из
house_odn_period_facts+annual_adjustments.
6. Миграция и внедрение
- Добавить таблицы в
billing-serviceчерез GORMAutoMigrate+ SQL-миграция для индексов/комментариев. - Реализовать
OdnVolumeEngineи тесты (в т.ч. сценарий LS10 — отрицательная дельта, распределение 0, ростcarry_volume). - Подключить к
processCalculationпосле базового контураCharge.
7. Реализация в коде (текущее состояние)
| Компонент | Расположение |
|---|---|
| Модели GORM | billing-service/internal/models/house_resource_carry_balance.go, house_odn_period_fact.go, house_odn_period_account_allocation.go, house_odn_annual_adjustment.go |
| Автомиграция | billing-service/internal/database/migrate.go |
| SQL-миграция (PostgreSQL) | billing-service/migrations/006_house_odn_tables.sql |
| Расчёт дельты, норматива, переноса, долей по площади | billing-service/internal/odn/calc.go (ComputePeriod), юнит-тесты calc_test.go |
| Сохранение снимка периода + обновление carry | billing-service/internal/odn/persist.go (SavePeriodResult, GetCarryVolume) |
Формула переноса в коде: CarryClosing = CarryOpening + RawDelta − Distributed − UKAbsorption (см. ComputePeriod).
Дальше: загрузка объёмов ОДПУ/ИПУ из core, вызов GetCarryVolume → ComputePeriod → SavePeriodResult из конвейера расчёта, затем формирование Charge по allocated_volume и тарифу. В processCalculation оставлен комментарий-ссылка на пакет internal/odn.
См. также
- ПП 354: общедомовые счётчики — формулы и п. 44 про отрицательный ОДН.
- Ядро биллинга —
Charge, Ledger. - Дорожная карта начислений — порядок внедрения.