dodil/erp-brick-timesheets
DODIL ERP brick: timesheets
src/brick_timesheets/application/inputs.py
"""The bounded inputs of this brick's use cases (BRICKS.md: an unbounded int or str is a promise the engine cannot keep).
Hours and rates are exact decimals as strings on the wire; the use case answers 422 {gate: bounds} for what a person's week cannot hold."""
from datetime import date
from decimal import Decimal, InvalidOperation
from typing import Annotated, Any, Optional
from erp_platform.types import text
from pydantic import BaseModel, BeforeValidator, Field, WithJsonSchema
from brick_timesheets.domain.sheet import RATE_MAX
Code = text(64, min_length=1)
Currency = text(3) # the use case says why a code is not an ISO 4217 code (after the permission check)
Reason = text(500)
Ref = text(140)
Note = text(500)
Descr = text(200)
Value = text(64, min_length=1)
# A date given as text, parsed by the use case AFTER the permission gate: a caller without the permission is told so (403), not that their date is
# malformed (422). Same for a week ("2026-W47") and for hours (a string like "7.50").
Day = Annotated[str, Field(max_length=32), WithJsonSchema({"type": "string", "format": "date", "description": "a date like 2026-11-20"})]
Week = Annotated[str, Field(max_length=16), WithJsonSchema({"type": "string", "description": "an ISO week like 2026-W47"})]
HoursText = Annotated[str, Field(max_length=20), WithJsonSchema({"type": "string", "description": "hours as an exact decimal like \"7.50\", zero to clear"})]
Instant = Annotated[str, Field(max_length=40), WithJsonSchema({"type": "string", "description": "an RFC 3339 instant like 2026-11-23T10:00:00Z"})]
class EntryIn(BaseModel):
"""The hours of one day on one line."""
work_date: Day
hours: HoursText
note: Optional[Note] = None
Entries = Annotated[list[EntryIn], Field(min_length=1, max_length=7)]
def parse_rate(v: Any) -> Decimal:
"""A cost rate per hour: an exact decimal above zero, at most four places, below 10 million."""
if isinstance(v, (bool, float)):
raise ValueError("a rate is an exact number: give it as a string such as \"52.0000\"")
try:
d = v if isinstance(v, Decimal) else Decimal(str(v).strip())
except (InvalidOperation, ValueError):
raise ValueError(f"{v!r} is not a rate") from None
if not d.is_finite():
raise ValueError("a rate is a finite number")
if d.as_tuple().exponent < -4:
raise ValueError("a rate has at most four decimal places")
if not (Decimal(0) < d < RATE_MAX):
raise ValueError("a rate is more than zero and below 10,000,000 an hour")
return d
Rate = Annotated[Decimal, BeforeValidator(parse_rate),
WithJsonSchema({"type": "string", "pattern": r"^[0-9]{1,7}(\.[0-9]{1,4})?$", "description": "an exact hourly rate as a string"})]
def day(v: Optional[str]) -> date:
return date.fromisoformat(str(v).strip())