ADR-067: `request_type` vs `payload_type` Vocabulary
ADR-067: request_type vs payload_type Vocabulary
Status: Implemented Date: 2026-06-04 Sprint: 86 Version: 10.10.0
Context
ADR-066 shipped the unified feed but left one modelling seam
documented and untouched: RequestCardData.request_type was typed as the fine payload-subtype
union (transportation | moving_help | tech_help | …, what RequestPayloadRenderer switches on)
but at runtime carried the coarse 5-value request_type_enum (generic | ride | borrow | service | event, the filter dimension). One field was being asked to do two incompatible jobs:
- Filtering — "show me only
riderequests" needs the coarse enum. - Payload rendering — "render the pickup/dropoff detail" needs the fine subtype.
Because the card fed the enum value to a renderer that switches on the subtype, the polymorphic
payload detail never rendered on the canonical card (the switch always missed).
Complicating the fix, the fine subtype isn't stored cleanly anywhere. The help_requests.category
column is the only source, and it is mixed-vocabulary:
- On INSERT (
requests.ts),categoryis written the same value asrequest_type— so newer rows hold the coarse enum (generic,ride, …). - Older / seed / simulation rows hold skill tokens (
moving,tech_support,gardening,cooking, …) — the values the matching SQL keys off (r.category = 'moving' AND s.skill IN (…)).
So category is neither cleanly the enum nor cleanly the renderer's subtype. A raw category
passthrough to the renderer would be wrong (it would feed moving/generic/gardening, none of
which the renderer knows).
Decision
Separate the two concerns into two fields, with no database migration:
request_typestays the coarse 5-valuerequest_type_enum— the filter dimension. Unchanged.payload_typeis a new derived field (the fine subtype union) that drivesRequestPayloadRenderer. It is computed fromcategorythrough a single canonical adapter,categoryToPayloadType()(services/request-service/src/services/payloadType.ts):- Known aliases are translated to the renderer vocabulary:
moving → moving_help,tech_support → tech_help,cooking → food,ride → transportation, and the already-alignedtransportation/childcare/home_repair/pet_care/foodpass through. - Everything else returns
undefined— the coarse enum values (generic/borrow/service/event), unmapped skill tokens (gardening/tutoring/…), and null/empty.RequestPayloadRendereralready no-ops on an unknown type / empty payload, so an unmapped category is a safe fallback, never a regression.
- Known aliases are translated to the renderer vocabulary:
categoryToPayloadType is the single place this translation happens — routes and components never
inline a category map. The seam fix is applied in toRequestCardData, so both the Dashboard Home
(view=home) and Community (view=community) views light up payload detail.
Consequences
Easier:
- Payload detail (pickup/dropoff, moving floors, tech device, etc.) finally renders on the one
canonical
RequestCard, on both feed views. - Filtering (coarse enum) and payload rendering (fine subtype) are no longer entangled in one field.
- One audited translation point means the messy
categoryvocabulary is contained, not spread.
Harder / deferred:
- The map is alias-driven; a genuinely new
categoryvalue renders without payload detail until its alias is added tocategoryToPayloadType. This is intentional (safe degradation) and unit-tested (tests/unit/payload-type.test.tsasserts the alias cases and the unknown→undefined contract). - The longer-term cleanup — normalizing
categoryitself, or a dedicatedpayload_typecolumn — is still possible later; this ADR deliberately avoids a migration.
References
- ADR-066: Unified Feed Model — the seam this closes
services/request-service/src/services/payloadType.ts— the canonical adapterservices/request-service/tests/unit/payload-type.test.ts— the map + unknown→undefined guard