-- Denní ekonomika za [p_ts_from, p_ts_to): jen audit_interval v okně + joiny (žádný vw_site_effective_price, -- žádné volání fn_effective_* per řádek — musí zůstat v souladu s R__011_fn_effective_price.sql). create or replace function ems.fn_economics_daily_for_window( p_site_id int, p_ts_from timestamptz, p_ts_to timestamptz ) returns table ( site_id int, day_local date, interval_count int, import_kwh numeric, export_kwh numeric, pv_kwh numeric, load_kwh numeric, pv_self_consumption_kwh numeric, ev_kwh numeric, hp_kwh numeric, grid_import_cashflow_czk numeric, grid_export_revenue_czk numeric, import_cost_czk numeric, export_revenue_czk numeric, net_cost_czk numeric, green_bonus_czk numeric, total_balance_czk numeric, planned_net_cost_czk numeric, planned_balance_czk numeric, deviation_cost_czk numeric ) language sql stable as $fn$ with slot0 as ( select ai.site_id, ai.interval_start, (date_trunc('day', ai.interval_start at time zone 'Europe/Prague'))::date as day_local, ai.actual_grid_import_wh, ai.actual_grid_power_w, ai.actual_grid_export_wh, ai.green_bonus_czk, ai.actual_pv_power_w, ai.actual_load_power_w, ai.actual_ev_power_w, ai.actual_heat_pump_power_w, smc.id as market_config_id, smc.purchase_pricing_mode, smc.buy_fixed_energy_nt_czk_kwh, smc.buy_fixed_vt_surcharge_czk_kwh, smc.buy_margin_fixed_czk, smc.buy_margin_percent, smc.sell_margin_fixed_czk, smc.sell_margin_percent, smc.system_services_czk_kwh, smc.ote_fee_czk_kwh, smc.hdo_code_id, smc.tariff_id, coalesce(dt.vat_rate, 0.21) as vat_rate, mip.buy_raw_price_czk_kwh as buy_spot, mip.sell_raw_price_czk_kwh as sell_spot, pi.expected_cost_czk as planned_cost_czk from ems.audit_interval ai left join lateral ( select smc_inner.* from ems.site_market_config smc_inner where smc_inner.site_id = ai.site_id and smc_inner.valid_from <= ai.interval_start and (smc_inner.valid_to is null or smc_inner.valid_to > ai.interval_start) order by smc_inner.valid_from desc limit 1 ) smc on true left join ems.distribution_tariff dt on dt.id = smc.tariff_id left join lateral ( select mip_inner.buy_raw_price_czk_kwh, mip_inner.sell_raw_price_czk_kwh from ems.market_interval_price mip_inner where mip_inner.interval_start = ai.interval_start and mip_inner.market_source in ('OTE_CZ', 'OTE_CZ_DAM') limit 1 ) mip on true left join ems.planning_interval pi on pi.run_id = ai.planning_run_id and pi.interval_start = ai.interval_start where ai.site_id = p_site_id and ai.interval_start >= p_ts_from and ai.interval_start < p_ts_to ), slot1 as ( select s0.*, case when s0.hdo_code_id is null then false else exists ( select 1 from ems.hdo_code_window w where w.hdo_code_id = s0.hdo_code_id and ( w.day_type = 'all' or ( w.day_type = 'workday' and extract(dow from s0.interval_start at time zone 'Europe/Prague') between 1 and 5 ) or ( w.day_type = 'weekend' and extract(dow from s0.interval_start at time zone 'Europe/Prague') in (0, 6) ) ) and w.rate_type = 'VT' and (s0.interval_start at time zone 'Europe/Prague')::time >= w.window_from and (s0.interval_start at time zone 'Europe/Prague')::time < w.window_to ) end as is_vt from slot0 s0 ), slot2 as ( select s1.*, dr.dist_rate from slot1 s1 left join lateral ( select dtr.price_czk_kwh as dist_rate from ems.distribution_tariff_rate dtr where dtr.tariff_id = s1.tariff_id and dtr.rate_type = case when s1.is_vt then 'VT'::text else 'NT'::text end and dtr.valid_from <= s1.interval_start::date and (dtr.valid_to is null or dtr.valid_to > s1.interval_start::date) order by dtr.valid_from desc limit 1 ) dr on s1.tariff_id is not null ), slot3 as ( select s2.*, case when upper(trim(coalesce(s2.purchase_pricing_mode, ''))) = 'FIXED' and s2.buy_fixed_energy_nt_czk_kwh is not null then s2.buy_fixed_energy_nt_czk_kwh + case when s2.is_vt then coalesce(s2.buy_fixed_vt_surcharge_czk_kwh, 0) else 0::numeric end when s2.buy_spot is null then null::numeric else s2.buy_spot end as energy_czk from slot2 s2 ), slot4 as ( select s3.*, case when s3.market_config_id is null then null::numeric when s3.energy_czk is null then null::numeric else coalesce(s3.buy_margin_fixed_czk, 0) + (s3.energy_czk * coalesce(s3.buy_margin_percent, 0) / 100.0) end as buy_margin_part from slot3 s3 ), slot5 as ( select s4.*, case when s4.market_config_id is null then null::numeric when s4.energy_czk is null then null::numeric else round( ( s4.energy_czk + coalesce(s4.dist_rate, 0) + coalesce(s4.system_services_czk_kwh, 0) + coalesce(s4.ote_fee_czk_kwh, 0) + s4.buy_margin_part ) * (1 + coalesce(s4.vat_rate, 0.21)), 6 ) end as buy_p, case when s4.market_config_id is null then null::numeric when s4.sell_spot is null then null::numeric else round( s4.sell_spot + coalesce(s4.sell_margin_fixed_czk, 0) + (s4.sell_spot * coalesce(s4.sell_margin_percent, 0) / 100.0), 6 ) end as sell_p from slot4 s4 ), slots as ( select s5.site_id, s5.day_local, round( coalesce(s5.actual_grid_import_wh, greatest(s5.actual_grid_power_w, 0)::numeric / 4) / 1000, 4 ) as import_kwh, round( coalesce(s5.actual_grid_export_wh, abs(least(s5.actual_grid_power_w, 0))::numeric / 4) / 1000, 4 ) as export_kwh, round( coalesce(s5.actual_grid_import_wh, greatest(s5.actual_grid_power_w, 0)::numeric / 4) / 1000.0 * coalesce(s5.buy_p, 0), 4 ) as grid_import_cashflow_czk, round( coalesce(s5.actual_grid_export_wh, abs(least(s5.actual_grid_power_w, 0))::numeric / 4) / 1000.0 * coalesce(s5.sell_p, 0), 4 ) as grid_export_revenue_czk, round( coalesce(s5.actual_grid_import_wh, greatest(s5.actual_grid_power_w, 0)::numeric / 4) / 1000.0 * coalesce(s5.buy_p, 0) - coalesce(s5.actual_grid_export_wh, abs(least(s5.actual_grid_power_w, 0))::numeric / 4) / 1000.0 * coalesce(s5.sell_p, 0), 4 ) as dynamic_cost_czk, s5.green_bonus_czk, s5.planned_cost_czk, s5.actual_pv_power_w, s5.actual_load_power_w, s5.actual_ev_power_w, s5.actual_heat_pump_power_w from slot5 s5 ), daily_agg as ( select s.site_id, s.day_local, count(*)::int as interval_count, round(sum(s.import_kwh), 3) as import_kwh, round(sum(s.export_kwh), 3) as export_kwh, round(sum(greatest(s.actual_pv_power_w, 0)::numeric / 4000), 3) as pv_kwh, round(sum(greatest(s.actual_load_power_w, 0)::numeric / 4000), 3) as load_kwh, round(sum(greatest(s.actual_ev_power_w, 0)::numeric / 4000), 3) as ev_kwh, round(sum(greatest(s.actual_heat_pump_power_w, 0)::numeric / 4000), 3) as hp_kwh, round( sum(greatest(s.actual_pv_power_w, 0)::numeric / 4000) - sum(s.export_kwh), 3 ) as pv_self_consumption_kwh, round(sum(s.grid_import_cashflow_czk), 2) as grid_import_cashflow_czk, round(sum(s.grid_export_revenue_czk), 2) as grid_export_revenue_czk, round(sum(case when s.dynamic_cost_czk > 0 then s.dynamic_cost_czk else 0 end), 2) as import_cost_czk, round(sum(case when s.dynamic_cost_czk < 0 then abs(s.dynamic_cost_czk) else 0 end), 2) as export_revenue_czk, round(sum(s.dynamic_cost_czk), 2) as net_cost_czk, round(coalesce(sum(s.green_bonus_czk), 0), 2) as green_bonus_czk, round(-sum(s.dynamic_cost_czk) + coalesce(sum(s.green_bonus_czk), 0), 2) as total_balance_czk, round(sum(s.planned_cost_czk), 2) as planned_net_cost_czk, round(-coalesce(sum(s.planned_cost_czk), 0), 2) as planned_balance_czk, round(sum(s.dynamic_cost_czk) - coalesce(sum(s.planned_cost_czk), 0), 2) as deviation_cost_czk from slots s group by s.site_id, s.day_local ) select d.site_id, d.day_local, d.interval_count, d.import_kwh, d.export_kwh, d.pv_kwh, d.load_kwh, d.pv_self_consumption_kwh, d.ev_kwh, d.hp_kwh, d.grid_import_cashflow_czk, d.grid_export_revenue_czk, d.import_cost_czk, d.export_revenue_czk, d.net_cost_czk, d.green_bonus_czk, d.total_balance_czk, d.planned_net_cost_czk, d.planned_balance_czk, d.deviation_cost_czk from daily_agg d order by d.day_local; $fn$; comment on function ems.fn_economics_daily_for_window is 'Denní souhrn ekonomiky [from, to); ceny inline dle fn_effective_buy/sell (R__011), bez PL volání per slot.'; create or replace function ems.fn_economics_daily_month( p_site_id int, p_month_start date, p_month_end date ) returns jsonb language sql stable as $fn$ select jsonb_build_object( 'has_green_bonus', exists ( select 1 from ems.asset_pv_array apv where apv.site_id = p_site_id and apv.green_bonus_czk_kwh is not null ), 'days', coalesce( ( select jsonb_agg(sub.day_row order by sub.day_local) from ( select r.day_local, jsonb_build_object( 'day', r.day_local, 'interval_count', r.interval_count, 'import_kwh', r.import_kwh, 'export_kwh', r.export_kwh, 'pv_kwh', r.pv_kwh, 'load_kwh', r.load_kwh, 'pv_self_consumption_kwh', r.pv_self_consumption_kwh, 'ev_kwh', r.ev_kwh, 'hp_kwh', r.hp_kwh, 'import_cost_czk', case when l.site_id is not null then l.import_cost_czk else r.import_cost_czk end, 'export_revenue_czk', case when l.site_id is not null then l.export_revenue_czk else r.export_revenue_czk end, 'grid_import_cashflow_czk', coalesce(l.grid_import_cashflow_czk, r.grid_import_cashflow_czk), 'grid_export_revenue_czk', coalesce(l.grid_export_revenue_czk, r.grid_export_revenue_czk), 'net_cost_czk', case when l.site_id is not null then l.net_cost_czk else r.net_cost_czk end, 'green_bonus_czk', case when l.site_id is not null then l.green_bonus_czk else r.green_bonus_czk end, 'total_balance_czk', case when l.site_id is not null then l.total_balance_czk else r.total_balance_czk end, 'planned_balance_czk', r.planned_balance_czk, 'deviation_cost_czk', r.deviation_cost_czk, 'is_locked', (l.site_id is not null) ) as day_row from ems.fn_economics_daily_for_window( p_site_id, (p_month_start::timestamp at time zone 'Europe/Prague'), (p_month_end::timestamp at time zone 'Europe/Prague') ) r left join ems.audit_day_lock l on l.site_id = r.site_id and l.day_local = r.day_local order by r.day_local ) sub ), '[]'::jsonb ) ); $fn$; comment on function ems.fn_economics_daily_month(int, date, date) is 'Měsíční denní ekonomika + lock merge jako JSON (GET /economics/daily).';