Imports name this capability’s declared dependencies, which fune builds next to it in your project; each one links to its page.
from typing import List, Optional, Sequence
from .dates_add_days import add_days ← from dates.add-days ^1.0.0 · built alongside by fune
from .dates_days_between import days_between ← from dates.days-between ^1.0.0 · built alongside by fune
from .money_amount import assert_same_currency, money ← from money.amount ^1.0.0 · built alongside by fune
from .money_sum import sum_money ← from money.sum ^1.0.0 · built alongside by fune
from .logistics_demurrage_types import DemurrageTier, DemurrageLine, DemurrageCharge
def _whole(value: object) -> bool:
return isinstance(value, int) and not isinstance(value, bool)
def demurrage(
start_date: str,
end_date: str,
free_days: int,
tiers: Sequence[DemurrageTier],
) -> DemurrageCharge:
"""Demurrage or detention for one container: calendar days from start_date
to end_date inclusive, the first free_days free, every later day at the
rate of the tier whose day numbers contain it."""
if not _whole(free_days):
raise TypeError("freeDays must be an integer, received %r" % (free_days,))
if free_days < 0:
raise ValueError("freeDays must not be negative, received %d" % free_days)
total_days = days_between(start_date, end_date) + 1
if total_days < 1:
raise ValueError("endDate must not be before startDate: %s to %s" % (start_date, end_date))
if len(tiers) == 0:
raise ValueError("tiers must not be empty")
for tier in tiers:
if not _whole(tier.from_day) or tier.from_day < 1:
raise ValueError("tier fromDay must be 1 or more, received %r" % (tier.from_day,))
if tier.to_day is not None and (not _whole(tier.to_day) or tier.to_day < tier.from_day):
raise ValueError(
"tier toDay must not be before fromDay, received %r to %r" % (tier.from_day, tier.to_day)
)
if tier.daily_rate.minor < 0:
raise ValueError("dailyRate must not be negative, received %d" % tier.daily_rate.minor)
assert_same_currency(tiers[0].daily_rate, tier.daily_rate)
ordered = sorted(tiers, key=lambda t: t.from_day)
for i in range(1, len(ordered)):
previous = ordered[i - 1]
if previous.to_day is None or previous.to_day >= ordered[i].from_day:
raise ValueError("tiers overlap at day %d" % ordered[i].from_day)
lines: List[DemurrageLine] = []
day = free_days + 1
for tier in ordered:
if day > total_days:
break
if tier.to_day is not None and tier.to_day < day:
continue # wholly inside free time
# A gap is a mistake in the tariff; charging nothing for it would hide it.
if tier.from_day > day:
raise ValueError("no rate for day %d: no tier covers it" % day)
last = total_days if tier.to_day is None else min(tier.to_day, total_days)
days = last - day + 1
lines.append(
DemurrageLine(
from_day=day,
to_day=last,
days=days,
daily_rate=tier.daily_rate,
amount=money(tier.daily_rate.minor * days, tier.daily_rate.currency),
)
)
day = last + 1
if day <= total_days:
raise ValueError("no rate for day %d: no tier covers it" % day)
free_time_ends: Optional[str] = add_days(start_date, free_days - 1) if free_days > 0 else None
return DemurrageCharge(
total_days=total_days,
free_days_used=min(free_days, total_days),
chargeable_days=max(total_days - free_days, 0),
free_time_ends=free_time_ends,
lines=lines,
total=sum_money([line.amount for line in lines], tiers[0].daily_rate.currency),
)