fix(loyalty): ProgramCreate accepts null for minimum_purchase_cents
All checks were successful
CI / ruff (push) Successful in 16s
CI / pytest (push) Successful in 2h53m13s
CI / validate (push) Successful in 35s
CI / dependency-scanning (push) Successful in 35s
CI / docs (push) Successful in 57s
CI / deploy (push) Successful in 1m18s

The admin program-edit form sends null for empty number inputs.
ProgramCreate had minimum_purchase_cents declared as int (default
0, ge=0), which rejected null with 422 — even though the DB column
is NOT NULL with default 0 and "0 means no minimum" is the
documented semantics.

Add a field_validator(mode="before") that coerces None to 0 so
the admin form (and any other client that sends null for an empty
optional number) goes through cleanly. The other tolerant fields
in the schema (stamps_reward_value_cents, points_expiration_days)
are already int | None; ProgramUpdate already accepts null here.

User hit this after a clean-DB reset prevented falling back to a
pre-existing program; the merchant area form happens to send 0
instead of null, masking the bug there.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-17 15:08:29 +02:00
parent 8d6830fc97
commit 120532e63f

View File

@@ -10,7 +10,7 @@ Merchant-based programs:
from datetime import datetime
from pydantic import BaseModel, ConfigDict, Field
from pydantic import BaseModel, ConfigDict, Field, field_validator
class PointsRewardConfig(BaseModel):
@@ -83,6 +83,13 @@ class ProgramCreate(BaseModel):
description="Minimum purchase amount to earn points (0 = no minimum)",
)
@field_validator("minimum_purchase_cents", mode="before")
@classmethod
def _coerce_purchase_cents_none_to_zero(cls, v):
# Form sends null for empty number inputs; DB column is NOT NULL
# with default 0, and 0 already means "no minimum" semantically.
return 0 if v is None else v
# Future: Tier configuration
tier_config: list[TierConfig] | None = Field(
None,