-- Denní ekonomika za časové okno [p_ts_from, p_ts_to) bez joinu na vw_site_effective_price -- (ten dělá cross join mip × site_market_config a je extrémně drahý při agregaci přes vw_economics_daily). -- Ceny = fn_effective_buy_price / fn_effective_sell_price (stejně jako ve vw_site_effective_price). 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 slots as ( select ai.site_id, (date_trunc('day', ai.interval_start at time zone 'Europe/Prague'))::date as day_local, round( coalesce(ai.actual_grid_import_wh, greatest(ai.actual_grid_power_w, 0)::numeric / 4) / 1000, 4 ) as import_kwh, round( coalesce(ai.actual_grid_export_wh, abs(least(ai.actual_grid_power_w, 0))::numeric / 4) / 1000, 4 ) as export_kwh, round( coalesce(ai.actual_grid_import_wh, greatest(ai.actual_grid_power_w, 0)::numeric / 4) / 1000.0 * coalesce(pr.buy_p, 0), 4 ) as grid_import_cashflow_czk, round( coalesce(ai.actual_grid_export_wh, abs(least(ai.actual_grid_power_w, 0))::numeric / 4) / 1000.0 * coalesce(pr.sell_p, 0), 4 ) as grid_export_revenue_czk, round( coalesce(ai.actual_grid_import_wh, greatest(ai.actual_grid_power_w, 0)::numeric / 4) / 1000.0 * coalesce(pr.buy_p, 0) - coalesce(ai.actual_grid_export_wh, abs(least(ai.actual_grid_power_w, 0))::numeric / 4) / 1000.0 * coalesce(pr.sell_p, 0), 4 ) as dynamic_cost_czk, ai.green_bonus_czk, pi.expected_cost_czk as planned_cost_czk, ai.actual_pv_power_w, ai.actual_load_power_w, ai.actual_ev_power_w, ai.actual_heat_pump_power_w from ems.audit_interval ai left join ems.planning_interval pi on pi.run_id = ai.planning_run_id and pi.interval_start = ai.interval_start cross join lateral ( select ems.fn_effective_buy_price(ai.site_id, ai.interval_start) as buy_p, ems.fn_effective_sell_price(ai.site_id, ai.interval_start) as sell_p ) pr where ai.site_id = p_site_id and ai.interval_start >= p_ts_from and ai.interval_start < p_ts_to ), 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 pro site v polovičně otevřeném UTC okně [from, to); bez vw_site_effective_price.'; 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).';