Skip to main content

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

  1. type: REQUIRED. Must be exactly "pee", "poop", "both", or "miss" (case-sensitive).
  2. 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).
  3. notes: Optional. Max 1000 characters. Empty string after trim is treated as null. Stored trimmed.
  4. timestamp: Optional, defaults to server's current time. Must be valid ISO8601 if provided.
  5. ec_attempt: Optional boolean, defaults to false. Indicates whether a potty/toilet was offered (Elimination Communication).
  6. 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).
  7. Both EC fields are independent of type — you can EC-catch pee, poop, or both.

Dashboard Counting

  • "Today" defined by user's day_start_time preference (default 07:00) in user's timezone
  • Count pee: type = "pee" OR type = "both"
  • Count poop: type = "poop" OR type = "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

  1. Pee with default timestamp: type=pee → 201, timestamp = server time
  2. Poop with explicit timestamp: type=poop, timestamp=... → 201
  3. Both: type=both → 201
  4. Audit log created: AuditLog with action=create

Create - EC (Elimination Communication)

  1. EC caught (pee): type=pee, ec_attempt=true, ec_success=true → 201, both EC fields true
  2. EC missed (poop): type=poop, ec_attempt=true, ec_success=false → 201, ec_attempt=true, ec_success=false
  3. No EC fields: type=pee → 201, ec_attempt=false, ec_success=false (defaults)
  4. EC success without attempt: ec_attempt=false, ec_success=true → 201, ec_success silently set to false
  5. GET returns EC fields: Create with EC, GET by ID → ec_attempt and ec_success present in response

Create - Validation

  1. Missing type: {} → 400, details: [{field: "type"}]
  2. Invalid type: type=wet → 400
  3. Case sensitive: type=Pee → 400
  4. Invalid timestamp: timestamp=not-a-date → 400

Create - Access Control

  1. No auth: missing token → 401
  2. No access: → 403
  3. Caregiver access: caregiver role → 201
  4. Child not found: → 404

List

  1. List all: → 200, { diapers: [...], count: N }
  2. List with since: diapers at T1, T2, T3; since=T1 → returns T2, T3 only
  3. List empty: no diapers → 200, { diapers: [], count: 0 }
  4. Since is exclusive: diaper at T1, since=T1 → empty
  5. List with limit: 5 diapers, limit=2 → 2 most recent
  6. List no access: → 403

Update

  1. Update timestamp: → 200
  2. Update type: type=both → 200
  3. Update both fields: → 200
  4. Updated_by reflects editor: User B updates → updated_by = User B
  5. Audit log: action=update, before/after
  6. Update not found: → 404
  7. Update no access: → 403
  8. Update EC attempt off clears success: ec_attempt=true, ec_success=true → PUT ec_attempt=false → ec_success=false

Delete

  1. Delete diaper: → 204
  2. Delete then list: diaper gone
  3. Delete not found: → 404
  4. Delete no access: → 403
  5. Audit log: action=delete

Dashboard Counting

  1. Pee count: 3 pee → 3 pee, 0 poop
  2. Poop count: 2 poop → 0 pee, 2 poop
  3. Both counted correctly: 2 pee + 1 poop + 1 both → 3 pee, 2 poop
  4. Multiple both: 3 both → 3 pee, 3 poop
  5. Day boundary: diaper at 06:59 (before day_start_time 07:00) → previous day's count
  6. Day boundary: diaper at 07:00 → today's count
  7. Empty today: no diapers in window → 0 pee, 0 poop

Cascade

  1. Child delete cascades: child with 5 diapers → delete child → all diapers gone
  2. 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