FUE-T01: count billing window start day in full for fixed-fee/credit accrual (symmetric with end day)
This commit is contained in:
+57
-23
@@ -678,15 +678,33 @@ def summarize(session: Session, start: datetime, end: datetime) -> dict[str, Any
|
|||||||
+ fixed_costs -- per-day standing charges, cross-version
|
+ fixed_costs -- per-day standing charges, cross-version
|
||||||
- credits -- per-day heffingskorting, cross-version
|
- credits -- per-day heffingskorting, cross-version
|
||||||
|
|
||||||
**Fixed-cost / credit counting — Principle C**:
|
**Fixed-cost / credit counting — Principle C (symmetric begin/end)**:
|
||||||
Only *already-elapsed* whole local calendar days are counted. For each
|
Both the local calendar day in which *start* falls and the local calendar
|
||||||
local calendar date D in the window ``[start_local_date, min(end_local_date,
|
day in which *end* falls are counted as full days. Fixed charges
|
||||||
tomorrow_local))``, the contract version whose rate covers D (the one whose
|
(network_fee, management_fee) and the energy-tax credit (heffingskorting)
|
||||||
effective_from local-date ≤ D < next version's effective_from local-date) is
|
are assessed on a "service-is-active" basis — if the meter was online on a
|
||||||
used. Days that have not yet started in local time (D > today_local) are
|
given calendar day, the full day's charge/credit applies, regardless of
|
||||||
never counted. This is cross-version: if the active contract has V1 from
|
whether the window starts at midnight or mid-morning.
|
||||||
June 1 and V2 from June 25, querying June 1–30 uses V1 for days 1-24 and V2
|
|
||||||
for day 25. Switching versions never resets the counter.
|
For each local calendar date D in the range
|
||||||
|
``[local_date(start), min(local_date(end), today_local)]``:
|
||||||
|
- D ≤ today_local (only elapsed / today days count as "whole days").
|
||||||
|
- The contract version whose effective_from local-date ≤ D is used.
|
||||||
|
- This is cross-version: if V1 is from June 1 and V2 from June 25,
|
||||||
|
querying June 1–30 uses V1 for days 1-24 and V2 for day 25.
|
||||||
|
|
||||||
|
The end-day (last_counted) is included when the end's local midnight falls
|
||||||
|
strictly before end_utc; combined with the always-counted start day this
|
||||||
|
makes the begin/end handling symmetric. A short same-day window therefore
|
||||||
|
counts its single local day. A window contributes 0 days only when the
|
||||||
|
counted range is empty (first_counted > last_counted) — e.g. a window lying
|
||||||
|
entirely in the future, since last_counted is capped at today_local.
|
||||||
|
|
||||||
|
Daily getters (``*_today``) use windows exactly aligned to local midnight,
|
||||||
|
so their ``first_counted`` is always today — unaffected by this fix.
|
||||||
|
|
||||||
|
Days that have not yet started in local time (D > today_local) are
|
||||||
|
never counted. Switching versions never resets the counter.
|
||||||
|
|
||||||
**Timezone note**: the ``days`` field in the returned dict still represents
|
**Timezone note**: the ``days`` field in the returned dict still represents
|
||||||
the window length in calendar days (total_seconds / 86400), for backward
|
the window length in calendar days (total_seconds / 86400), for backward
|
||||||
@@ -750,25 +768,41 @@ def summarize(session: Session, start: datetime, end: datetime) -> dict[str, Any
|
|||||||
|
|
||||||
# --- Compute [first_counted, last_counted] local date range (inclusive) ---
|
# --- Compute [first_counted, last_counted] local date range (inclusive) ---
|
||||||
#
|
#
|
||||||
# We count local calendar day D if its LOCAL MIDNIGHT falls within [start_utc, end_utc)
|
# Both the window-start day and the window-end day are counted as complete
|
||||||
# AND D ≤ today_local (only elapsed / today days count as "whole days").
|
# local calendar days, regardless of whether the window starts/ends at midnight.
|
||||||
#
|
#
|
||||||
# Semantics: "A day D is counted when its local midnight has arrived (D ≤ today)
|
# Principle C (symmetric begin/end):
|
||||||
# AND the local midnight is within the billing window."
|
# • first_counted = local calendar date of start_utc (start day always counted)
|
||||||
|
# • last_counted = local calendar date of end_utc (end day counted if its
|
||||||
|
# local midnight is strictly before end_utc)
|
||||||
|
# • Both are then capped at today_local (only elapsed / today days count).
|
||||||
#
|
#
|
||||||
# This means a sub-day window [10:00, 10:30) UTC that doesn't contain any
|
# Why symmetric? Fixed charges (network_fee, management_fee) and energy-tax credits
|
||||||
# local midnight contributes 0 days, while a window [22:00 UTC, 23:00 UTC) that
|
# (heffingskorting) are assessed on a "service-is-active" basis, not on how many
|
||||||
# contains CEST midnight (= 22:00 UTC) contributes 1 day.
|
# hours the service was actually running within that calendar day. If the meter
|
||||||
|
# anchor (started_at) falls at 09:18 on June 24, the full June 24 standing charge
|
||||||
|
# and credit still apply because the service was online for that day.
|
||||||
#
|
#
|
||||||
# Implementation: compute the first and last local date whose midnight is in range.
|
# Previous asymmetric behaviour: a start_utc that was *later* than the local
|
||||||
|
# midnight of local_start_date caused first_counted to be bumped to the *next*
|
||||||
|
# day, silently dropping the anchor day's charges/credits. This was incorrect
|
||||||
|
# for cumulative entities (import_cost_total / export_revenue_total) whose anchor
|
||||||
|
# is often an above-midnight started_at. The end-side had always been symmetric
|
||||||
|
# (counted if local midnight < end_utc), creating an inconsistency.
|
||||||
|
#
|
||||||
|
# Daily getters (window = [local today 00:00, local tomorrow 00:00)) are
|
||||||
|
# unaffected: local_date(local_midnight_utc(today)) == today, so first_counted
|
||||||
|
# is still today regardless of the fix.
|
||||||
|
#
|
||||||
|
# A short same-day window now counts its single local day (the start day is
|
||||||
|
# always counted, symmetric with the end side). A window contributes 0 days
|
||||||
|
# only when the counted range is empty (first_counted > last_counted) — e.g. a
|
||||||
|
# window lying entirely in the future, where last_counted is capped at
|
||||||
|
# today_local while first_counted is later.
|
||||||
from app.services.timezone import local_midnight_utc as _lmu
|
from app.services.timezone import local_midnight_utc as _lmu
|
||||||
|
|
||||||
local_start_date: _date = local_date(start_utc)
|
# Start side: always count the local calendar day in which start_utc falls.
|
||||||
# Is the midnight of local_start_date ≥ start_utc? If not, first midnight is next day.
|
first_counted: _date = local_date(start_utc)
|
||||||
if _lmu(local_start_date) >= start_utc:
|
|
||||||
first_counted: _date = local_start_date
|
|
||||||
else:
|
|
||||||
first_counted = local_start_date + _td(days=1)
|
|
||||||
|
|
||||||
local_end_date: _date = local_date(end_utc)
|
local_end_date: _date = local_date(end_utc)
|
||||||
# Is the midnight of local_end_date < end_utc? If yes, that day's midnight is in range.
|
# Is the midnight of local_end_date < end_utc? If yes, that day's midnight is in range.
|
||||||
|
|||||||
+175
-29
@@ -996,35 +996,52 @@ class TestSummarize:
|
|||||||
assert result["period_count"] == 2
|
assert result["period_count"] == 2
|
||||||
assert result["degraded_count"] == 0
|
assert result["degraded_count"] == 0
|
||||||
|
|
||||||
def test_fixed_costs_zero_for_sub_day_window(self, energy_db: Session) -> None:
|
def test_fixed_costs_one_day_for_sub_day_window(self, energy_db: Session) -> None:
|
||||||
"""Principle C: a 30-minute window within the same local day counts 0 whole days.
|
"""FUE-T01 (symmetric begin/end): a 30-minute window within one local day counts 1 day.
|
||||||
|
|
||||||
Fixed costs are only added for elapsed whole local calendar days.
|
Under the symmetric-begin/end fix, fixed charges are assessed on a
|
||||||
A window [10:00, 10:30) UTC stays within the same local calendar date
|
"service-is-active" basis: if the window falls within a local calendar day,
|
||||||
regardless of timezone offset (any UTC-11..UTC+14 range), so no whole
|
that entire day's charge applies. The begin side no longer requires a local
|
||||||
day is covered → fixed_costs = 0.
|
midnight to fall *inside* the window — the local date of start_utc itself is
|
||||||
|
always counted as the first day.
|
||||||
|
|
||||||
|
Window [10:00, 10:30) UTC in Europe/Amsterdam (CEST = UTC+2):
|
||||||
|
local date of start: June 23 (12:00 CEST)
|
||||||
|
local date of end: June 23 (12:30 CEST)
|
||||||
|
→ first_counted = last_counted = June 23 → 1 day
|
||||||
|
→ fixed_costs = (9.87 + 9.87) / 30 × 1
|
||||||
"""
|
"""
|
||||||
from zoneinfo import ZoneInfo
|
from zoneinfo import ZoneInfo
|
||||||
|
from unittest.mock import patch
|
||||||
|
|
||||||
self._setup_two_periods(energy_db)
|
self._setup_two_periods(energy_db)
|
||||||
# Pin the local timezone so the test is deterministic on any CI host.
|
# Pin the local timezone so the test is deterministic on any CI host.
|
||||||
with __import__("unittest.mock", fromlist=["patch"]).patch.object(
|
with patch.object(tz_module, "local_tz", return_value=ZoneInfo("Europe/Amsterdam")):
|
||||||
tz_module, "local_tz", return_value=ZoneInfo("Europe/Amsterdam")
|
|
||||||
):
|
|
||||||
result = summarize(energy_db, _ts(10, 0), _ts(10, 30))
|
result = summarize(energy_db, _ts(10, 0), _ts(10, 30))
|
||||||
assert result["fixed_costs"] == 0.0, (
|
|
||||||
"Sub-day window should contribute 0 whole-day fixed costs under Principle C"
|
expected_fixed = (9.87 + 9.87) / 30 # 1 day
|
||||||
|
assert abs(result["fixed_costs"] - expected_fixed) < 1e-9, (
|
||||||
|
f"FUE-T01: sub-day window in one local day should count 1 day of fixed costs; "
|
||||||
|
f"expected {expected_fixed:.6f}, got {result['fixed_costs']}"
|
||||||
)
|
)
|
||||||
|
|
||||||
def test_credits_zero_for_sub_day_window(self, energy_db: Session) -> None:
|
def test_credits_one_day_for_sub_day_window(self, energy_db: Session) -> None:
|
||||||
"""Principle C: a 30-minute window within the same local day counts 0 whole days for credits."""
|
"""FUE-T01 (symmetric begin/end): a 30-minute window within one local day counts 1 day of credits.
|
||||||
|
|
||||||
|
Same rationale as test_fixed_costs_one_day_for_sub_day_window.
|
||||||
|
credits = 600.0 / 365 × 1 day.
|
||||||
|
"""
|
||||||
from zoneinfo import ZoneInfo
|
from zoneinfo import ZoneInfo
|
||||||
|
from unittest.mock import patch
|
||||||
|
|
||||||
self._setup_two_periods(energy_db)
|
self._setup_two_periods(energy_db)
|
||||||
with __import__("unittest.mock", fromlist=["patch"]).patch.object(
|
with patch.object(tz_module, "local_tz", return_value=ZoneInfo("Europe/Amsterdam")):
|
||||||
tz_module, "local_tz", return_value=ZoneInfo("Europe/Amsterdam")
|
|
||||||
):
|
|
||||||
result = summarize(energy_db, _ts(10, 0), _ts(10, 30))
|
result = summarize(energy_db, _ts(10, 0), _ts(10, 30))
|
||||||
assert result["credits"] == 0.0, (
|
|
||||||
"Sub-day window should contribute 0 whole-day credits under Principle C"
|
expected_credits = 600.0 / 365 # 1 day
|
||||||
|
assert abs(result["credits"] - expected_credits) < 1e-9, (
|
||||||
|
f"FUE-T01: sub-day window in one local day should count 1 day of credits; "
|
||||||
|
f"expected {expected_credits:.6f}, got {result['credits']}"
|
||||||
)
|
)
|
||||||
|
|
||||||
def test_total_payable_formula(self, energy_db: Session) -> None:
|
def test_total_payable_formula(self, energy_db: Session) -> None:
|
||||||
@@ -1068,12 +1085,20 @@ class TestSummarize:
|
|||||||
assert result["period_count"] == 0
|
assert result["period_count"] == 0
|
||||||
|
|
||||||
def test_one_day_summarize_hand_calc(self, energy_db: Session) -> None:
|
def test_one_day_summarize_hand_calc(self, energy_db: Session) -> None:
|
||||||
"""Full 1-day hand-calculation: fixed_costs/30 and credits/365 with 1-day interval.
|
"""Full 1-day hand-calculation: fixed_costs/30 and credits/365 with 1-day UTC interval.
|
||||||
|
|
||||||
Principle C: the window [2026-06-23 00:00 UTC, 2026-06-24 00:00 UTC) spans
|
FUE-T01 (symmetric begin/end): the window [2026-06-23 00:00 UTC, 2026-06-24 00:00 UTC)
|
||||||
exactly one local calendar day in Europe/Amsterdam (CEST=UTC+2: 02:00→02:00).
|
in Europe/Amsterdam (CEST=UTC+2):
|
||||||
effective_from = June 23 00:00 UTC = June 23 02:00 CEST = local date June 23.
|
- local date of start (June 23 00:00 UTC = June 23 02:00 CEST) = June 23
|
||||||
Today (2026-06-25) ≥ June 23, so the day is elapsed → 1 day counted.
|
→ first_counted = June 23 (anchor day always counted)
|
||||||
|
- local midnight of June 24 = June 23 22:00 UTC, which is < end_utc (June 24 00:00 UTC)
|
||||||
|
→ last_counted = June 24
|
||||||
|
- today (2026-06-25) ≥ June 24 → cap doesn't apply
|
||||||
|
→ 2 local calendar days counted (June 23 + June 24)
|
||||||
|
|
||||||
|
Note: a 1-day UTC window centered at non-midnight UTC spans **two** local days
|
||||||
|
in Amsterdam (CEST=UTC+2) because local midnight (June 23 22:00 UTC) falls
|
||||||
|
inside the window. This is correct under the symmetric begin/end principle.
|
||||||
"""
|
"""
|
||||||
from zoneinfo import ZoneInfo
|
from zoneinfo import ZoneInfo
|
||||||
from unittest.mock import patch
|
from unittest.mock import patch
|
||||||
@@ -1091,12 +1116,18 @@ class TestSummarize:
|
|||||||
with patch.object(tz_module, "local_tz", return_value=ZoneInfo("Europe/Amsterdam")):
|
with patch.object(tz_module, "local_tz", return_value=ZoneInfo("Europe/Amsterdam")):
|
||||||
result = summarize(energy_db, start, end)
|
result = summarize(energy_db, start, end)
|
||||||
|
|
||||||
# fixed_costs = (9.87 + 9.87) / 30 × 1.0 = 0.658
|
# 2 local days (June 23 start day + June 24 because local midnight of June 24
|
||||||
assert abs(result["fixed_costs"] - (9.87 + 9.87) / 30) < 1e-9
|
# = June 23 22:00 UTC falls within [start, end)).
|
||||||
# credits = 600 / 365
|
n_days = 2
|
||||||
assert abs(result["credits"] - 600.0 / 365) < 1e-9
|
assert abs(result["fixed_costs"] - (9.87 + 9.87) / 30 * n_days) < 1e-9, (
|
||||||
# total = 0 + 0.658 − (600/365)
|
f"FUE-T01: 1-day UTC window = 2 local days in Amsterdam; "
|
||||||
expected_total = (9.87 + 9.87) / 30 - 600.0 / 365
|
f"expected fixed={((9.87 + 9.87) / 30 * n_days):.6f}, got {result['fixed_costs']}"
|
||||||
|
)
|
||||||
|
assert abs(result["credits"] - 600.0 / 365 * n_days) < 1e-9, (
|
||||||
|
f"FUE-T01: expected credits={600.0 / 365 * n_days:.6f}, got {result['credits']}"
|
||||||
|
)
|
||||||
|
# total = 0 + fixed − credits
|
||||||
|
expected_total = (9.87 + 9.87) / 30 * n_days - 600.0 / 365 * n_days
|
||||||
assert abs(result["total_payable"] - expected_total) < 1e-9
|
assert abs(result["total_payable"] - expected_total) < 1e-9
|
||||||
|
|
||||||
|
|
||||||
@@ -1390,6 +1421,121 @@ class TestSummarizePrincipleC:
|
|||||||
assert abs(result["fixed_costs"] - expected_fixed) < 1e-9
|
assert abs(result["fixed_costs"] - expected_fixed) < 1e-9
|
||||||
assert abs(result["credits"] - expected_credits) < 1e-9
|
assert abs(result["credits"] - expected_credits) < 1e-9
|
||||||
|
|
||||||
|
def test_morning_anchor_first_day_counted(self, energy_db: Session) -> None:
|
||||||
|
"""FUE-T01: anchor falling above local midnight counts its local day as day 1.
|
||||||
|
|
||||||
|
Bug scenario: meter.started_at = June 24 09:18 local time (07:18 UTC, CEST=UTC+2).
|
||||||
|
Old code: local_date(07:18 UTC) = June 24; _lmu(June 24) = June 23 22:00 UTC;
|
||||||
|
June 23 22:00 UTC < June 24 07:18 UTC → False (22:00 < 07:18? No — 22:00 UTC June 23 is
|
||||||
|
*before* 07:18 UTC June 24; so the condition >= start_utc was False);
|
||||||
|
first_counted bumped to June 25. June 24's charges were silently dropped.
|
||||||
|
|
||||||
|
Fix: first_counted = local_date(start_utc) = June 24 always, regardless of where
|
||||||
|
within the day start_utc falls. June 24 is the anchor day → its charge is counted.
|
||||||
|
|
||||||
|
Window: [June 24 07:18 UTC (= 09:18 CEST), June 26 22:00 UTC (= June 27 00:00 CEST)).
|
||||||
|
Today local = June 25 (2026-06-25).
|
||||||
|
first_counted = June 24 (anchor day, previously dropped).
|
||||||
|
last_counted = min(June 26, June 25) = June 25.
|
||||||
|
n_days = June 25 - June 24 + 1 = 2.
|
||||||
|
"""
|
||||||
|
from unittest.mock import patch
|
||||||
|
|
||||||
|
eff_utc = _ams_midnight(2026, 6, 1)
|
||||||
|
self._make_single_version_contract(energy_db, effective_from_utc=eff_utc)
|
||||||
|
|
||||||
|
# anchor = June 24 07:18 UTC = June 24 09:18 CEST (above local midnight)
|
||||||
|
anchor_utc = datetime(2026, 6, 24, 7, 18, 0, tzinfo=_UTC)
|
||||||
|
# end = June 26 22:00 UTC = June 27 00:00 CEST (past today June 25, capped)
|
||||||
|
end_utc = datetime(2026, 6, 26, 22, 0, 0, tzinfo=_UTC)
|
||||||
|
|
||||||
|
with patch.object(tz_module, "local_tz", return_value=_ams()):
|
||||||
|
result = self._run_summarize_ams(energy_db, anchor_utc, end_utc)
|
||||||
|
|
||||||
|
daily_fixed = (9.87 + 9.87) / 30
|
||||||
|
daily_credit = 600.0 / 365
|
||||||
|
# June 24 (anchor day) + June 25 (today) = 2 days
|
||||||
|
assert abs(result["fixed_costs"] - daily_fixed * 2) < 1e-9, (
|
||||||
|
f"FUE-T01: morning anchor should count anchor day + today = 2 days of fixed costs; "
|
||||||
|
f"expected {daily_fixed * 2:.6f}, got {result['fixed_costs']}"
|
||||||
|
)
|
||||||
|
assert abs(result["credits"] - daily_credit * 2) < 1e-9, (
|
||||||
|
f"FUE-T01: morning anchor should count anchor day + today = 2 days of credits; "
|
||||||
|
f"expected {daily_credit * 2:.6f}, got {result['credits']}"
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_daily_window_midnight_aligned_unaffected(self, energy_db: Session) -> None:
|
||||||
|
"""FUE-T01: the daily getter window [today midnight, tomorrow midnight) is not affected.
|
||||||
|
|
||||||
|
The *_today getters use windows exactly aligned to local midnight:
|
||||||
|
start = local_midnight_utc(today) = June 24 22:00 UTC (for June 25 local)
|
||||||
|
end = local_midnight_utc(tomorrow) = June 25 22:00 UTC
|
||||||
|
|
||||||
|
With either old or new logic, first_counted = June 25 (today):
|
||||||
|
- Old: local_date(June 24 22:00 UTC in Amsterdam) = June 25;
|
||||||
|
_lmu(June 25) = June 24 22:00 UTC >= June 24 22:00 UTC → True → first = June 25.
|
||||||
|
- New: first_counted = local_date(June 24 22:00 UTC) = June 25.
|
||||||
|
Both give the same result: 1 day counted.
|
||||||
|
|
||||||
|
This test explicitly verifies daily window behaviour is unchanged.
|
||||||
|
"""
|
||||||
|
from unittest.mock import patch
|
||||||
|
|
||||||
|
eff_utc = _ams_midnight(2026, 6, 1)
|
||||||
|
self._make_single_version_contract(energy_db, effective_from_utc=eff_utc)
|
||||||
|
|
||||||
|
# Today's window aligned to local midnight (June 25 CEST = June 24 22:00 UTC).
|
||||||
|
today_start_utc = _ams_midnight(2026, 6, 25) # June 24 22:00 UTC
|
||||||
|
tomorrow_start_utc = _ams_midnight(2026, 6, 26) # June 25 22:00 UTC
|
||||||
|
|
||||||
|
with patch.object(tz_module, "local_tz", return_value=_ams()):
|
||||||
|
result = self._run_summarize_ams(energy_db, today_start_utc, tomorrow_start_utc)
|
||||||
|
|
||||||
|
daily_fixed = (9.87 + 9.87) / 30
|
||||||
|
daily_credit = 600.0 / 365
|
||||||
|
# Exactly 1 day counted (June 25 only; June 26 is future, capped).
|
||||||
|
assert abs(result["fixed_costs"] - daily_fixed * 1) < 1e-9, (
|
||||||
|
f"FUE-T01: daily window should count exactly 1 day; "
|
||||||
|
f"expected {daily_fixed:.6f}, got {result['fixed_costs']}"
|
||||||
|
)
|
||||||
|
assert abs(result["credits"] - daily_credit * 1) < 1e-9, (
|
||||||
|
f"FUE-T01: daily window should count exactly 1 day of credits; "
|
||||||
|
f"expected {daily_credit:.6f}, got {result['credits']}"
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_single_day_morning_anchor_counts_one_day(self, energy_db: Session) -> None:
|
||||||
|
"""FUE-T01: a window starting mid-morning on a single local day counts that 1 day.
|
||||||
|
|
||||||
|
Window: [June 24 08:00 UTC (= 10:00 CEST), June 24 22:00 UTC (= June 25 00:00 CEST)).
|
||||||
|
Both endpoints resolve to June 24 local (end is exactly midnight of June 25,
|
||||||
|
which is June 24 22:00 UTC, so _lmu(June 25) = June 24 22:00 UTC is NOT < end_utc
|
||||||
|
but equal → last_counted = June 24).
|
||||||
|
first_counted = June 24.
|
||||||
|
n_days = 1. Today (June 25) is past June 24 → elapsed.
|
||||||
|
"""
|
||||||
|
from unittest.mock import patch
|
||||||
|
|
||||||
|
eff_utc = _ams_midnight(2026, 6, 1)
|
||||||
|
self._make_single_version_contract(energy_db, effective_from_utc=eff_utc)
|
||||||
|
|
||||||
|
start_utc = datetime(2026, 6, 24, 8, 0, 0, tzinfo=_UTC) # 10:00 CEST = mid-morning June 24
|
||||||
|
end_utc = datetime(2026, 6, 24, 22, 0, 0, tzinfo=_UTC) # = June 25 00:00 CEST midnight
|
||||||
|
|
||||||
|
with patch.object(tz_module, "local_tz", return_value=_ams()):
|
||||||
|
result = self._run_summarize_ams(energy_db, start_utc, end_utc)
|
||||||
|
|
||||||
|
daily_fixed = (9.87 + 9.87) / 30
|
||||||
|
daily_credit = 600.0 / 365
|
||||||
|
# Exactly 1 day: June 24 (start day, counted as full day; elapsed since today is June 25).
|
||||||
|
assert abs(result["fixed_costs"] - daily_fixed * 1) < 1e-9, (
|
||||||
|
f"FUE-T01: mid-morning to midnight window on one day should count 1 day; "
|
||||||
|
f"expected {daily_fixed:.6f}, got {result['fixed_costs']}"
|
||||||
|
)
|
||||||
|
assert abs(result["credits"] - daily_credit * 1) < 1e-9, (
|
||||||
|
f"FUE-T01: mid-morning to midnight window on one day should count 1 day of credits; "
|
||||||
|
f"expected {daily_credit:.6f}, got {result['credits']}"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# 8. compute_closed_periods
|
# 8. compute_closed_periods
|
||||||
|
|||||||
Reference in New Issue
Block a user