mirror of
https://github.com/Zproger/Aurum.git
synced 2026-10-13 02:56:39 +07:00
`GoalContributionCreate.amount` used `Field(ne=0)`, but pydantic has no
"not equal" constraint — the keyword was accepted as unknown schema
metadata and never validated anything. `POST /api/goals/{id}/contributions`
with `"amount": "0"` returned 201 and wrote a contribution that moves the
goal's progress by nothing, and the comment right above the field says zero
is pointless.
Replaced with a field_validator so the rule is enforced, deliberately not
with `gt=0`: negative contributions are intentional (money taken back out of
a goal), and the added tests pin both halves of that.
This also clears the `PydanticDeprecatedSince20: Using extra keyword
arguments on Field (Extra keys: 'ne')` warning that the test suite printed
on every run — that warning was the symptom.
Claude-Session: https://claude.ai/code/session_01UmuMDFrwdvQkce885VaaTh
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
46 lines
1.4 KiB
Python
46 lines
1.4 KiB
Python
from datetime import date as date_
|
|
from decimal import Decimal
|
|
|
|
from pydantic import BaseModel, Field, field_validator
|
|
|
|
|
|
class GoalCreate(BaseModel):
|
|
name: str = Field(min_length=1, max_length=150)
|
|
target_amount: Decimal = Field(gt=0)
|
|
target_date: date_ | None = None
|
|
|
|
|
|
class GoalUpdate(BaseModel):
|
|
name: str | None = Field(default=None, min_length=1, max_length=150)
|
|
target_amount: Decimal | None = Field(default=None, gt=0)
|
|
target_date: date_ | None = None
|
|
|
|
|
|
class GoalContributionCreate(BaseModel):
|
|
# Negative allowed — a withdrawal from the goal is still a contribution
|
|
# to its running total, just in the other direction. Zero is pointless.
|
|
amount: Decimal
|
|
date: date_
|
|
note: str | None = Field(default=None, max_length=200)
|
|
|
|
@field_validator("amount")
|
|
@classmethod
|
|
def _reject_zero(cls, value: Decimal) -> Decimal:
|
|
# Has to be a validator: pydantic has no "not equal" constraint, so
|
|
# the Field(ne=0) this replaces was parsed as an unknown keyword,
|
|
# stored as schema metadata, and never actually checked anything.
|
|
if value == 0:
|
|
raise ValueError("amount must not be zero")
|
|
return value
|
|
|
|
|
|
class GoalRead(BaseModel):
|
|
id: int
|
|
name: str
|
|
target_amount: Decimal
|
|
target_date: date_ | None
|
|
current_amount: Decimal
|
|
remaining: Decimal
|
|
percent: float
|
|
is_reached: bool
|