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())