Feature: Diaper Log
Version: 1.4.0 Last Reviewed: 2026-03-14 Status: Approved
User Story
As a caregiver, I can log a diaper change in 2 taps so we can track output for the pediatrician.
MVP Scope
- Timestamp (defaults to "now", editable)
- Type: pee, poop, or both (three buttons in bottom sheet)
- Consistency: hard, formed, loose, or watery (optional, poop/both only)
- Notes: optional free-text field per diaper change (max 1000 chars)
- EC (Elimination Communication) tracking: ec_attempt (was a potty/toilet offered?) and ec_success (was it caught?)
- List diapers with timestamp filtering
- Edit timestamp, type, consistency, and notes after logging
- Delete a logged diaper change
NOT in MVP
- Color tracking (yellow, green, brown, black) (Post-MVP #2)
- Diaper brand tracking
- Rash tracking (none, mild, severe)
- Photo uploads
- Volume/size indicators
API Contract
Create Diaper Change
POST /api/v1/children/:childId/diapers
Authorization: Bearer <token>
Request (pee):
{
"type": "pee" // required: "pee" | "poop" | "both" | "miss"
}
Request (poop with consistency):
{
"type": "poop",
"consistency": "formed",
"notes": "Seemed uncomfortable beforehand"
}
Request (pee with EC catch):
{
"type": "pee",
"ec_attempt": true, // optional, defaults to false
"ec_success": true // optional, defaults to false
}
Response 201:
{
"diaper": {
"id": "550e8400-e29b-41d4-a716-446655440000",
"child_id": "7c9e6679-7425-40de-944b-e07fc1f90ae7",
"timestamp": "2026-02-10T14:30:00.000Z",
"type": "pee",
"consistency": null,
"notes": null,
"ec_attempt": false,
"ec_success": false,
"created_by": "f47ac10b-58cc-4372-a567-0e02b2c3d479",
"created_at": "2026-02-10T14:30:15.000Z",
"updated_by": "f47ac10b-58cc-4372-a567-0e02b2c3d479",
"updated_at": "2026-02-10T14:30:15.000Z"
}
}
Get Diaper by ID
GET /api/v1/children/:childId/diapers/:id
Authorization: Bearer <token>
Response 200:
{
"diaper": { ... }
}
List Diaper Changes
GET /api/v1/children/:childId/diapers?since=2026-02-10T00:00:00.000Z
Authorization: Bearer <token>
Query Parameters (all optional):
- since: ISO8601 timestamp - return diapers after this time (exclusive)
- limit: integer - max number of results (default 100, max 500)
Response 200:
{
"diapers": [
{
"id": "...",
"child_id": "...",
"timestamp": "2026-02-10T14:30:00.000Z",
"type": "both",
"consistency": "formed",
"notes": null,
"ec_attempt": false,
"ec_success": false,
"created_by": "...",
"created_at": "...",
"updated_by": "...",
"updated_at": "..."
}
],
"count": 1
}
Note: Results ordered by timestamp DESC (newest first).
Empty array if no diapers match criteria.
Update Diaper Change
PUT /api/v1/children/:childId/diapers/:id
Authorization: Bearer <token>
Request (all fields optional - partial update):
{
"timestamp": "2026-02-10T14:35:00.000Z",
"type": "poop",
"consistency": "loose",
"notes": "Runny, possible dairy sensitivity?",
"ec_attempt": true,
"ec_success": false
}
Response 200:
{
"diaper": { ... }
}
Delete Diaper Change
DELETE /api/v1/children/:childId/diapers/:id
Authorization: Bearer <token>
Response 204: (No Content)
Error Responses
Standard error format:
{
"error": {
"code": "VALIDATION_ERROR",
"message": "Request body validation failed",
"details": [
{ "field": "type", "message": "Must be one of: pee, poop, both, miss", "code": "invalid_enum" }
]
}
}
Error codes: 400 VALIDATION_ERROR, 401 UNAUTHORIZED, 403 FORBIDDEN, 404 NOT_FOUND
Business Rules
Validation
- type: REQUIRED. Must be exactly "pee", "poop", "both", or "miss" (case-sensitive).
- consistency: Optional. Must be "hard", "formed", "loose", or "watery". Only meaningful for poop/both. If provided with type="pee" or type="miss", it is silently stripped (not an error).
- notes: Optional. Max 1000 characters. Empty string after trim is treated as null. Stored trimmed.
- timestamp: Optional, defaults to server's current time. Must be valid ISO8601 if provided.
- ec_attempt: Optional boolean, defaults to false. Indicates whether a potty/toilet was offered (Elimination Communication).
- ec_success: Optional boolean, defaults to false. Indicates whether the elimination was caught. Only meaningful when ec_attempt=true. When ec_attempt is false, ec_success is silently set to false (mirrors the consistency-stripping pattern for pee).
- Both EC fields are independent of type — you can EC-catch pee, poop, or both.
Dashboard Counting
- "Today" defined by user's
day_start_timepreference (default 07:00) in user's timezone - Count pee:
type = "pee"ORtype = "both" - Count poop:
type = "poop"ORtype = "both" - A diaper with type "both" increments BOTH the pee count AND the poop count
- Example: 2 "pee" + 1 "poop" + 1 "both" → displays "3 pee, 2 poop"
- Count ec_attempts: diapers where
ec_attempt = true(any type) - Count ec_catches: diapers where
ec_attempt = true AND ec_success = true - Count misses: diapers where
type = 'miss'
Access Control
- All operations require authentication
- User must have ChildAccess record (owner or caregiver) for the child
- No access → 403 FORBIDDEN (all endpoints)
- Child not found → 404 NOT_FOUND
Audit Trail
- All CUD operations create an AuditLog entry
- Create: changes = full entity
- Update: changes = { before: {...}, after: {...} }
- Delete: changes = full entity being deleted
Query Behavior
- List orders by timestamp descending
- "since" filters exclusively (timestamp > since)
- Default limit 100, max 500
- Responses wrapped:
{ "diapers": [...], "count": N }
Acceptance Criteria
- Can log pee/poop/both with 2 taps from dashboard
- Can edit timestamp and type after logging
- Can delete a diaper change (returns 204)
- Timestamp defaults to now if not provided
- List returns diapers wrapped in object with count
- List orders by timestamp descending
- Since filter works correctly (exclusive)
- "both" counts as 1 pee AND 1 poop for dashboard totals
- Today's count uses day_start_time boundary (not midnight)
- Validation errors return 400 with details array
- No access returns 403, not found returns 404
- All CUD operations create AuditLog entries
- Cascade delete removes diapers when child is deleted
- Can create diaper with ec_attempt=true, ec_success=true (caught)
- Can create diaper with ec_attempt=true, ec_success=false (missed)
- Creating without EC fields defaults both to false (backward compatible)
- Updating ec_attempt to false clears ec_success to false
- EC fields appear in GET responses
Test Cases
Create - Happy Path
- Pee with default timestamp: type=pee → 201, timestamp = server time
- Poop with explicit timestamp: type=poop, timestamp=... → 201
- Both: type=both → 201
- Audit log created: AuditLog with action=create
Create - EC (Elimination Communication)
- EC caught (pee): type=pee, ec_attempt=true, ec_success=true → 201, both EC fields true
- EC missed (poop): type=poop, ec_attempt=true, ec_success=false → 201, ec_attempt=true, ec_success=false
- No EC fields: type=pee → 201, ec_attempt=false, ec_success=false (defaults)
- EC success without attempt: ec_attempt=false, ec_success=true → 201, ec_success silently set to false
- GET returns EC fields: Create with EC, GET by ID → ec_attempt and ec_success present in response
Create - Validation
- Missing type: {} → 400, details: [{field: "type"}]
- Invalid type: type=wet → 400
- Case sensitive: type=Pee → 400
- Invalid timestamp: timestamp=not-a-date → 400
Create - Access Control
- No auth: missing token → 401
- No access: → 403
- Caregiver access: caregiver role → 201
- Child not found: → 404
List
- List all: → 200, { diapers: [...], count: N }
- List with since: diapers at T1, T2, T3; since=T1 → returns T2, T3 only
- List empty: no diapers → 200, { diapers: [], count: 0 }
- Since is exclusive: diaper at T1, since=T1 → empty
- List with limit: 5 diapers, limit=2 → 2 most recent
- List no access: → 403
Update
- Update timestamp: → 200
- Update type: type=both → 200
- Update both fields: → 200
- Updated_by reflects editor: User B updates → updated_by = User B
- Audit log: action=update, before/after
- Update not found: → 404
- Update no access: → 403
- Update EC attempt off clears success: ec_attempt=true, ec_success=true → PUT ec_attempt=false → ec_success=false
Delete
- Delete diaper: → 204
- Delete then list: diaper gone
- Delete not found: → 404
- Delete no access: → 403
- Audit log: action=delete
Dashboard Counting
- Pee count: 3 pee → 3 pee, 0 poop
- Poop count: 2 poop → 0 pee, 2 poop
- Both counted correctly: 2 pee + 1 poop + 1 both → 3 pee, 2 poop
- Multiple both: 3 both → 3 pee, 3 poop
- Day boundary: diaper at 06:59 (before day_start_time 07:00) → previous day's count
- Day boundary: diaper at 07:00 → today's count
- Empty today: no diapers in window → 0 pee, 0 poop
Cascade
- Child delete cascades: child with 5 diapers → delete child → all diapers gone
- Audit logs survive: diaper audit logs remain after child deletion
Boundaries
- No pagination beyond limit parameter (single query returns up to 500)
- No filtering by type in list endpoint (client filters)
- No bulk operations
- No soft deletes
- Color/consistency fields deferred to Post-MVP #2 (will be added to this same entity)
- Dashboard display format is an iOS concern, not specified here