dodil/erp-brick-payroll
DODIL ERP brick: payroll
src/brick_payroll/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).
Money and rates arrive as TEXT and are parsed by the use case after the permission check, so a caller without the
permission is told so (403) and a bad value is a 422 {gate: bounds} naming the field - never a 500."""
from datetime import date
from decimal import Decimal, InvalidOperation
from typing import Annotated, Any, Optional
from erp_platform.types import text
from pydantic import Field, WithJsonSchema
from brick_payroll.domain.money import MONEY_MAX
Code = text(32, min_length=1)
Company = text(4, min_length=1)
Ref = text(140, min_length=1)
Reason = text(500)
Note = text(500)
Day = Annotated[str, Field(max_length=32), WithJsonSchema({"type": "string", "format": "date", "description": "a date like 2026-11-30"})]
AmountText = Annotated[str, Field(max_length=40), WithJsonSchema({"type": "string", "description": "an exact amount like \"2354.24\""})]
PercentText = Annotated[str, Field(max_length=20), WithJsonSchema({"type": "string", "description": "a percentage like \"5.0000\""})]
Period = Annotated[str, Field(max_length=10), WithJsonSchema({"type": "string", "description": "a calendar month like 2026-11, or an ISO week like 2026-W46 for a weekly schedule"})]
def parse_money(v: Any, what: str, *, negative_ok: bool = False) -> Decimal:
if isinstance(v, (bool, float)):
raise ValueError(f"{what} is an exact number: give it as a string such as \"2354.24\"")
try:
d = v if isinstance(v, Decimal) else Decimal(str(v).strip())
except (InvalidOperation, ValueError):
raise ValueError(f"{what}: {v!r} is not an amount") from None
if not d.is_finite() or d.as_tuple().exponent < -2:
raise ValueError(f"{what} has at most two decimal places")
if abs(d) >= MONEY_MAX or (d < 0 and not negative_ok):
raise ValueError(f"{what} is {'an amount' if negative_ok else 'zero or more'} below 10,000,000,000,000 (NUMERIC(18,2))")
return d
def parse_percent(v: Any, what: str) -> Decimal:
try:
d = Decimal(str(v).strip())
except (InvalidOperation, ValueError):
raise ValueError(f"{what}: {v!r} is not a percentage") from None
if not d.is_finite() or d.as_tuple().exponent < -4 or not Decimal(0) <= d <= Decimal(100):
raise ValueError(f"{what} is a percentage from 0 to 100 with at most four places (NUMERIC(19,4))")
return d
def day(v: Optional[str]) -> date:
return date.fromisoformat(str(v).strip())