OpenSPP Registry: Reject Future Birthdates
Context
OpenSPP stores a beneficiary’s date of birth on
res.partner.birthdate, defined in
spp_registry/models/individual.py. A non-stored
computed field, age, derives years of age from that
date using relativedelta(now, dob).
In the web form, a small guard called
_birthdate_onchange already reacts when someone
types a future date: it clears the field and shows a friendly
warning.
Problem
That onchange guard only runs in the form UI. Every other write path skips it entirely, so a birthdate in the future is accepted through:
-
direct ORM calls —
partner.write({"birthdate": date(2030, 1, 1)}) create()values- CSV / Excel import
- API calls (XML-RPC, API v2, DCI endpoints)
Once a future birthdate is stored, the computed
age goes negative and renders as a string such as
"-3" in views, exports, and anywhere else that reads
the field — a confusing and invalid value for downstream program
logic.
Background
This is pre-existing behavior that was explicitly flagged as out
of scope in PR
#357. That PR removed a dead @api.constrains("age")
guard which looked like it covered this case but never actually
ran — constrains hooks do not fire on non-stored computed fields,
so the removal was behavior-preserving.
No @api.constrains on birthdate existed
anywhere in the repository. The only related check is
registration_date >= birthdate in
spp_registry/models/registrant.py, which does not
reject a future date of birth on its own.
My Approach
The fix is a stored-field constraint that fires on every create/write path, giving a reliable server-side backstop while the friendly form onchange stays in place for a gentle in-UI experience.
@api.constrains("birthdate")
def _check_birthdate_not_future(self):
today = fields.Date.today()
for record in self:
if record.birthdate and record.birthdate > today:
raise ValidationError(_("Date of birth cannot be in the future."))
Because birthdate is a stored, writeable field, the
constraint runs on create, write, import, and API paths alike —
and, unlike the removed age guard, produces no
registry-load warning. I kept it consistent with the existing
_check_registration_date constraint already living in
registrant.py.
Tests
Following the project’s test-first workflow, I added a
TestBirthdateNotFutureConstraint class in
spp_registry/tests/test_constraints.py. It exercises
the write paths that skip the form onchange —
create, write, and
load() (the ORM path CSV/Excel import uses under the
hood) — plus the boundary cases that must still pass: a birthdate
of today, and an ordinary past date. A final test asserts that an
approximate date of birth still can’t be set in the future.
Considerations
- Existing bad data: a constraint only validates on write, so records already holding a future birthdate stay invalid until touched — and could then block otherwise unrelated writes. I raised this with the maintainers in the pull request, suggesting a paired data-quality check or a migration note for deployed databases.
-
Approximate birthdates: I assumed the
birthdate_not_exactflag should still not allow a future date, and added a test asserting that — while flagging it in the pull request as a product decision for the maintainer to confirm rather than deciding it silently. - Keep the onchange: the silent-reset UX in the form is friendlier than a raised error, so it stays as the first line of defense with the constraint behind it.
Fix Process
I followed the standard OpenSPP contribution flow so the change lands cleanly:
- wrote failing tests first (TDD), then added the constraint
- covered the create, write, and import paths in the test suite
-
version bump plus a
readme/HISTORY.mdfragment - flagged the module README regeneration for a maintainer, since it needs the repo toolchain I couldn’t run on a disk-constrained machine
Current Status
Pull request
#397
is open against the 19.0 branch, with no merge
conflicts, awaiting CI and maintainer review. This was the first
issue I picked up on OpenSPP: I worked through the reproduction
across each write path, then a test-first implementation, before
opening the pull request.
Reflection
I like this as a first contribution because it’s small in code but careful in reasoning: the interesting part isn’t the constraint itself, it’s understanding why the old guard never fired, where every write path enters the model, and how to protect existing data without breaking unrelated saves.
For a social protection platform, a clean date of birth is not a cosmetic detail — it feeds eligibility and program logic, so getting the validation right genuinely matters for the people the system serves.