mirror of
https://github.com/nowcommunity/nowdatabase.git
synced 2026-10-12 19:13:52 +03:00
feat: validate locality pollen percentages and combined total
This commit is contained in:
@@ -0,0 +1,178 @@
|
||||
# Pollen Record Validation Plan (Locality → Climate)
|
||||
|
||||
## 1) Assumptions & Scope
|
||||
|
||||
- The feature applies to existing Locality climate fields `pers_pollen_ap`, `pers_pollen_nap`, and `pers_pollen_other`, which are already present in Prisma and shared frontend/backend locality types.
|
||||
- No database schema change is required for this feature because all required columns already exist and are typed as nullable integers.
|
||||
- Validation must be enforced in both layers:
|
||||
- Shared field-level validation (used by frontend forms and backend locality service validation flow).
|
||||
- Backend request-time validation to guarantee data integrity regardless of client behavior.
|
||||
- UI impact is limited to Locality Climate tab input behavior and validation feedback; no new routes/pages are needed.
|
||||
- Existing role-based access control for locality create/update remains unchanged; validation should run after permission checks and before persistence.
|
||||
- Error responses must stay compatible with current structured validation error patterns used by locality endpoints.
|
||||
|
||||
## 2) High-Level Plan
|
||||
|
||||
1. **Validation rules definition (shared contract)**
|
||||
- Define/extend locality validator behavior so each pollen field accepts only integers in `[0, 100]` when provided.
|
||||
- Add a cross-field rule: `AP + NAP + OP <= 100` (ignoring `null` values as zero or validating only when value exists, consistent with existing optional-field behavior).
|
||||
|
||||
2. **Backend enforcement**
|
||||
- Ensure locality create/update flows surface the shared validator errors and reject invalid payloads with `400` + structured field errors.
|
||||
- Confirm validation is applied for partial updates and full payload updates.
|
||||
|
||||
3. **Frontend UX validation**
|
||||
- Surface immediate field-level errors in the Locality Climate tab for non-integer/out-of-range input.
|
||||
- Surface cross-field combined-percentage error in a predictable location (field error or form-level message) before submit.
|
||||
|
||||
4. **Testing strategy**
|
||||
- Add/extend backend API tests for accepted and rejected cases.
|
||||
- Add/extend shared validator tests (or frontend validation tests) to cover integer/range/sum constraints.
|
||||
- Verify no regressions for locality create/update and no auth behavior changes.
|
||||
|
||||
5. **Docs/ops**
|
||||
- No migration, env, or deployment config changes expected.
|
||||
- Update any relevant validation documentation/changelog entry if maintained.
|
||||
|
||||
## 3) Tasks (JSON)
|
||||
|
||||
```json
|
||||
[
|
||||
{
|
||||
"id": "T1",
|
||||
"title": "Add pollen field constraints to shared locality validator",
|
||||
"summary": "Implement validation for pers_pollen_ap, pers_pollen_nap, and pers_pollen_other as optional integers between 0 and 100, plus a cross-field sum constraint AP+NAP+OP<=100.",
|
||||
"app": "shared",
|
||||
"files_touched": [
|
||||
"frontend/src/shared/validators/locality.ts"
|
||||
],
|
||||
"migrations": false,
|
||||
"permissions": [
|
||||
"No RBAC change",
|
||||
"Validation applies to any caller already authorized to create/update localities"
|
||||
],
|
||||
"acceptance_criteria": [
|
||||
"Each pollen field rejects non-integer values",
|
||||
"Each pollen field rejects values <0 or >100",
|
||||
"Combined AP/NAP/OP values above 100 return a clear validation error",
|
||||
"Null/empty pollen fields remain accepted where currently optional"
|
||||
],
|
||||
"test_plan": [
|
||||
"Add unit tests for per-field integer/range validation",
|
||||
"Add unit tests for cross-field sum rule with partial-null and full-value combinations"
|
||||
],
|
||||
"estimate_hours": 2.5,
|
||||
"priority": "high"
|
||||
},
|
||||
{
|
||||
"id": "T2",
|
||||
"title": "Enforce pollen validation in locality backend write flows",
|
||||
"summary": "Ensure locality create/update service paths reject invalid pollen payloads with structured 400 responses using existing locality validation pipeline.",
|
||||
"app": "backend",
|
||||
"files_touched": [
|
||||
"backend/src/services/locality.ts",
|
||||
"backend/src/api-tests/locality/create.test.ts",
|
||||
"backend/src/api-tests/locality/update.test.ts",
|
||||
"backend/src/api-tests/locality/data.ts"
|
||||
],
|
||||
"migrations": false,
|
||||
"permissions": [
|
||||
"Existing locality write permissions unchanged",
|
||||
"Unauthorized users remain blocked before data validation/persistence"
|
||||
],
|
||||
"acceptance_criteria": [
|
||||
"Create endpoint returns 400 when pollen values violate integer/range/sum rules",
|
||||
"Update endpoint returns 400 for invalid pollen changes",
|
||||
"Valid pollen payloads continue to persist successfully"
|
||||
],
|
||||
"test_plan": [
|
||||
"Add integration tests for invalid create payloads",
|
||||
"Add integration tests for invalid update payloads",
|
||||
"Assert response contains expected validation error keys/messages"
|
||||
],
|
||||
"estimate_hours": 3.0,
|
||||
"priority": "high"
|
||||
},
|
||||
{
|
||||
"id": "T3",
|
||||
"title": "Improve Locality Climate tab validation feedback",
|
||||
"summary": "Ensure Locality Climate tab shows actionable validation messages for pollen fields and cross-field total constraint before submit.",
|
||||
"app": "frontend",
|
||||
"files_touched": [
|
||||
"frontend/src/components/Locality/Tabs/ClimateTab.tsx",
|
||||
"frontend/src/shared/validators/locality.ts"
|
||||
],
|
||||
"migrations": false,
|
||||
"permissions": [
|
||||
"No RBAC change",
|
||||
"UI controls remain enabled/disabled according to existing locality edit permissions"
|
||||
],
|
||||
"acceptance_criteria": [
|
||||
"User sees immediate validation feedback for invalid pollen inputs",
|
||||
"User sees clear message when AP+NAP+OP exceeds 100",
|
||||
"Form submission is blocked client-side when pollen validation fails"
|
||||
],
|
||||
"test_plan": [
|
||||
"Add/update frontend validation tests for pollen input edge cases",
|
||||
"Add component-level tests for error rendering in climate tab"
|
||||
],
|
||||
"estimate_hours": 2.5,
|
||||
"priority": "medium"
|
||||
},
|
||||
{
|
||||
"id": "T4",
|
||||
"title": "Quality gates and release readiness",
|
||||
"summary": "Run lint, type-check, and impacted test suites; record feature notes for reviewers.",
|
||||
"app": "fullstack",
|
||||
"files_touched": [
|
||||
"README.md"
|
||||
],
|
||||
"migrations": false,
|
||||
"permissions": [
|
||||
"N/A"
|
||||
],
|
||||
"acceptance_criteria": [
|
||||
"Lint/type-check pass in affected scopes",
|
||||
"All new/updated tests pass",
|
||||
"Reviewer notes describe validation behavior and edge cases"
|
||||
],
|
||||
"test_plan": [
|
||||
"Run npm run lint",
|
||||
"Run npm run tsc",
|
||||
"Run locality backend API tests and relevant frontend tests"
|
||||
],
|
||||
"estimate_hours": 1.5,
|
||||
"priority": "medium"
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
## 4) Risks & Mitigations
|
||||
|
||||
- **Auth failures**: Validation changes might unintentionally alter create/update control flow.
|
||||
- *Mitigation*: Keep permission middleware/service checks unchanged and add unauthorized regression tests.
|
||||
- **Behavior ambiguity for null/partial fields**: Optional fields can conflict with sum validation semantics.
|
||||
- *Mitigation*: Document rule as "sum of provided values" and codify with tests for null/empty combinations.
|
||||
- **Inconsistent frontend/backend messaging**: Users may see different messages client vs server.
|
||||
- *Mitigation*: Reuse shared validator constants/messages where possible and assert text in tests.
|
||||
- **Regression risk on locality updates**: Existing locality test fixtures might fail due to stricter constraints.
|
||||
- *Mitigation*: Update fixtures explicitly and add targeted test cases to make constraints clear.
|
||||
- **Rollback complexity**: Stricter validation can block data entry workflows.
|
||||
- *Mitigation*: Keep change isolated to validators and endpoint checks; revertable in one release if needed.
|
||||
|
||||
## 5) Out of Scope
|
||||
|
||||
- Backfilling or correcting historical pollen data already stored in the database.
|
||||
- Redesigning Locality Climate UI layout beyond validation feedback.
|
||||
- Introducing i18n/localized validation messages.
|
||||
- Unrelated locality performance optimizations.
|
||||
|
||||
## 6) Definition of Done ✅
|
||||
|
||||
- [ ] Pollen fields accept only integers from 0 to 100.
|
||||
- [ ] Combined AP/NAP/OP rule (`<=100`) enforced client and server side.
|
||||
- [ ] Locality create/update API responses return structured validation errors for violations.
|
||||
- [ ] Frontend displays clear validation messages and blocks invalid submit.
|
||||
- [ ] Added/updated backend + frontend tests for all new rules.
|
||||
- [ ] Lint and TypeScript checks pass in CI.
|
||||
- [ ] Documentation/changelog entry updated for validation behavior.
|
||||
@@ -2,6 +2,28 @@ import { EditDataType, LocalityDetailsType } from '../types'
|
||||
import { Validators, validator } from './validator'
|
||||
import { validCountries } from './countryList'
|
||||
|
||||
const pollenFields = ['pers_pollen_ap', 'pers_pollen_nap', 'pers_pollen_other'] as const
|
||||
|
||||
const validatePollenPercentage = (name: string, num: number) => {
|
||||
if (!Number.isInteger(num)) return `${name} must be an integer value`
|
||||
if (num < 0 || num > 100) return `${name} must be between 0 and 100`
|
||||
return
|
||||
}
|
||||
|
||||
const validatePollenRecordTotal = (editData: Partial<EditDataType<LocalityDetailsType>>) => {
|
||||
const values = pollenFields
|
||||
.map(fieldName => editData[fieldName])
|
||||
.filter((value): value is number => typeof value === 'number')
|
||||
|
||||
const total = values.reduce((sum, value) => sum + value, 0)
|
||||
|
||||
if (total > 100) {
|
||||
return 'Combined Arboreal (AP%), Non-arboreal (NAP%), and Other pollen (OP%) must be less than or equal to 100'
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
export const validateLocality = (
|
||||
editData: EditDataType<LocalityDetailsType>,
|
||||
fieldName: keyof EditDataType<LocalityDetailsType>
|
||||
@@ -121,6 +143,42 @@ export const validateLocality = (
|
||||
return
|
||||
},
|
||||
},
|
||||
pers_pollen_ap: {
|
||||
name: 'Arboreal pollen (AP%)',
|
||||
asNumber: (num: number) => {
|
||||
const valueError = validatePollenPercentage('Arboreal pollen (AP%)', num)
|
||||
if (valueError) return valueError
|
||||
|
||||
const totalError = validatePollenRecordTotal(editData)
|
||||
if (totalError) return totalError
|
||||
|
||||
return
|
||||
},
|
||||
},
|
||||
pers_pollen_nap: {
|
||||
name: 'Non-arboreal pollen (NAP%)',
|
||||
asNumber: (num: number) => {
|
||||
const valueError = validatePollenPercentage('Non-arboreal pollen (NAP%)', num)
|
||||
if (valueError) return valueError
|
||||
|
||||
const totalError = validatePollenRecordTotal(editData)
|
||||
if (totalError) return totalError
|
||||
|
||||
return
|
||||
},
|
||||
},
|
||||
pers_pollen_other: {
|
||||
name: 'Other pollen (OP%)',
|
||||
asNumber: (num: number) => {
|
||||
const valueError = validatePollenPercentage('Other pollen (OP%)', num)
|
||||
if (valueError) return valueError
|
||||
|
||||
const totalError = validatePollenRecordTotal(editData)
|
||||
if (totalError) return totalError
|
||||
|
||||
return
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
return validator<EditDataType<LocalityDetailsType>>(validators, editData, fieldName)
|
||||
|
||||
Reference in New Issue
Block a user