OpenSPP Registry: Reject Future Birthdates

Platform: OpenSPP (OpenSPP2) · Module: spp_registry · Type: Backend / Data integrity · Status: Pull request open — my first OpenSPP contribution · Issue: #362 · PR: #397

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:

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

Fix Process

I followed the standard OpenSPP contribution flow so the change lands cleanly:

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.