1+ /**
2+ * ride.ts — Express router for ride CRUD operations.
3+ *
4+ * Handles creating, reading, updating, and deleting rides, including support
5+ * for recurring ride series. Recurring rides share a recurrenceId and are
6+ * generated up to 4 months out from the series start.
7+ *
8+ * Routes:
9+ * GET / — query all rides (filterable)
10+ * GET /repeating — get all active recurring rides
11+ * GET /download — export rides for a given date as CSV
12+ * GET /:id — get a single ride by ID
13+ * GET /rider/:id — get all rides for a specific rider
14+ * POST / — create a ride (single or recurring)
15+ * PUT /:id — update a ride (?scope=single|future)
16+ * DELETE /:id — delete a ride (?scope=single|future)
17+ */
18+
119import express from 'express' ;
220import { v4 as uuid } from 'uuid' ;
321import * as csv from '@fast-csv/format' ;
@@ -72,17 +90,21 @@ function generateRecurringRides(
7290 return rides ;
7391}
7492
75- // Transform Prisma ride (uppercase enums) to frontend-expected format (lowercase enums)
93+ /**
94+ * Convert prisma ride object to the format the frontend expects
95+ * Prisma stores enums in uppercase (e.g. "UPCOMING", "NOT_STARTED")
96+ * but frontend uses lowercase strings throughout.
97+ */
7698const formatRide = ( ride : any ) => ( {
7799 ...ride ,
78100 type : ride . type ?. toLowerCase ( ) ,
79101 status : ride . status ?. toLowerCase ( ) ,
80102 schedulingState : ride . schedulingState ?. toLowerCase ( ) ,
81103} ) ;
82104
83- // Build a Prisma update payload from request body fields.
105+ // Build prisma update payload from request body fields
84106// Used for single-ride edits — includes schedulingState since admins manage that per-ride.
85- // Do NOT use this for bulk future-ride regeneration.
107+ // not to be used for bulk future-ride regeneration.
86108function buildUpdateData ( body : any ) : any {
87109 const updateData : any = { } ;
88110
@@ -156,6 +178,9 @@ router.get('/diagnose', async (_req, res) => {
156178 }
157179} ) ;
158180
181+ // Export all non-cancelled rides for a given date as a csv
182+ // Expects a `date` query param
183+ // Each row is one rider ... rides with multiple riders expand into multiple rows
159184router . get ( '/download' , async ( req , res ) => {
160185 try {
161186 const dateStart = moment ( req . query . date as string ) . toDate ( ) ;
@@ -367,16 +392,24 @@ router.post('/', validateUser('User'), async (req, res) => {
367392 const { recurrenceDays, recurrenceEndDate } = body ;
368393 const timezone = body . timezone || 'America/New_York' ;
369394
370- if ( ! recurrenceDays || ! Array . isArray ( recurrenceDays ) || recurrenceDays . length === 0 ) {
395+ if (
396+ ! recurrenceDays ||
397+ ! Array . isArray ( recurrenceDays ) ||
398+ recurrenceDays . length === 0
399+ ) {
371400 return res . status ( 400 ) . send ( {
372401 err : 'recurrenceDays is required for recurring rides (array of 0–6, where 0=Sun).' ,
373402 } ) ;
374403 }
375404 if ( ! recurrenceEndDate ) {
376- return res . status ( 400 ) . send ( { err : 'recurrenceEndDate is required for recurring rides.' } ) ;
405+ return res
406+ . status ( 400 )
407+ . send ( { err : 'recurrenceEndDate is required for recurring rides.' } ) ;
377408 }
378409 if ( ! body . startTime || ! body . endTime ) {
379- return res . status ( 400 ) . send ( { err : 'startTime and endTime are required.' } ) ;
410+ return res
411+ . status ( 400 )
412+ . send ( { err : 'startTime and endTime are required.' } ) ;
380413 }
381414 const maxEnd = moment ( ) . tz ( timezone ) . add ( 4 , 'months' ) . endOf ( 'day' ) ;
382415 if ( moment . tz ( recurrenceEndDate , timezone ) . isAfter ( maxEnd ) ) {
@@ -391,25 +424,29 @@ router.post('/', validateUser('User'), async (req, res) => {
391424 return res . status ( 400 ) . send ( { err : 'At least one rider is required.' } ) ;
392425 }
393426
394- const riderIds : string [ ] =
395- hasRiders
396- ? body . riders . map ( ( r : any ) => ( typeof r === 'string' ? r : r . id ) )
397- : [ typeof body . rider === 'string' ? body . rider : body . rider . id ] ;
427+ const riderIds : string [ ] = hasRiders
428+ ? body . riders . map ( ( r : any ) => ( typeof r === 'string' ? r : r . id ) )
429+ : [ typeof body . rider === 'string' ? body . rider : body . rider . id ] ;
398430
399431 const startLocationId =
400432 typeof startLocation === 'string' ? startLocation : startLocation . id ;
401433 const endLocationId =
402434 typeof endLocation === 'string' ? endLocation : endLocation . id ;
403435 const driverId = body . driver
404- ? typeof body . driver === 'string' ? body . driver : body . driver . id
436+ ? typeof body . driver === 'string'
437+ ? body . driver
438+ : body . driver . id
405439 : null ;
406440 const schedulingState : SchedulingState = driverId
407441 ? SchedulingState . SCHEDULED
408442 : SchedulingState . UNSCHEDULED ;
409443
410444 const recurrenceId = uuid ( ) ;
411445 const ridesData = generateRecurringRides (
412- { startTime : new Date ( body . startTime ) , endTime : new Date ( body . endTime ) } ,
446+ {
447+ startTime : new Date ( body . startTime ) ,
448+ endTime : new Date ( body . endTime ) ,
449+ } ,
413450 recurrenceDays ,
414451 new Date ( recurrenceEndDate ) ,
415452 timezone ,
@@ -446,7 +483,9 @@ router.post('/', validateUser('User'), async (req, res) => {
446483 )
447484 ) ;
448485
449- return res . status ( 200 ) . send ( { data : { recurrenceId, count : ridesData . length } } ) ;
486+ return res
487+ . status ( 200 )
488+ . send ( { data : { recurrenceId, count : ridesData . length } } ) ;
450489 }
451490
452491 const hasRiders = body . riders && body . riders . length > 0 ;
@@ -600,33 +639,49 @@ router.put('/:id', validateUser('User'), async (req, res) => {
600639 // --- edit all future rides in the series ---
601640 if ( scope === 'future' && ride . recurrenceId ) {
602641 const timezone = body . timezone ?? ride . timezone ?? 'America/New_York' ;
603- const recurrenceDays : number [ ] = body . recurrenceDays ?? ride . recurrenceDays ?? [ ] ;
642+ const recurrenceDays : number [ ] =
643+ body . recurrenceDays ?? ride . recurrenceDays ?? [ ] ;
604644 const recurrenceEndDate = body . recurrenceEndDate
605645 ? new Date ( body . recurrenceEndDate )
606646 : ride . recurrenceEndDate ;
607647
608648 if ( ! recurrenceEndDate ) {
609- return res . status ( 400 ) . send ( { err : 'recurrenceEndDate is missing from the series.' } ) ;
649+ return res
650+ . status ( 400 )
651+ . send ( { err : 'recurrenceEndDate is missing from the series.' } ) ;
610652 }
611653
612654 const startLocationId = body . startLocation
613- ? typeof body . startLocation === 'string' ? body . startLocation : body . startLocation . id
655+ ? typeof body . startLocation === 'string'
656+ ? body . startLocation
657+ : body . startLocation . id
614658 : ride . startLocationId ;
615659 const endLocationId = body . endLocation
616- ? typeof body . endLocation === 'string' ? body . endLocation : body . endLocation . id
660+ ? typeof body . endLocation === 'string'
661+ ? body . endLocation
662+ : body . endLocation . id
617663 : ride . endLocationId ;
618664 const driverId = Object . prototype . hasOwnProperty . call ( body , 'driver' )
619- ? body . driver ? ( typeof body . driver === 'string' ? body . driver : body . driver . id ) : null
665+ ? body . driver
666+ ? typeof body . driver === 'string'
667+ ? body . driver
668+ : body . driver . id
669+ : null
620670 : ride . driverId ;
621671 const riderIds : string [ ] = body . riders
622672 ? body . riders . map ( ( r : any ) => ( typeof r === 'string' ? r : r . id ) )
623673 : ride . riders . map ( ( r ) => r . id ) ;
624- const startTime = body . startTime ? new Date ( body . startTime ) : ride . startTime ;
674+ const startTime = body . startTime
675+ ? new Date ( body . startTime )
676+ : ride . startTime ;
625677 const endTime = body . endTime ? new Date ( body . endTime ) : ride . endTime ;
626678
627679 // delete this ride and all future rides in the series
628680 await prisma . ride . deleteMany ( {
629- where : { recurrenceId : ride . recurrenceId , startTime : { gte : ride . startTime } } ,
681+ where : {
682+ recurrenceId : ride . recurrenceId ,
683+ startTime : { gte : ride . startTime } ,
684+ } ,
630685 } ) ;
631686
632687 // regenerate from this point forward with the new parameters
@@ -643,7 +698,9 @@ router.put('/:id', validateUser('User'), async (req, res) => {
643698 }
644699
645700 // schedulingState is intentionally NOT carried over — admins manage that per-ride
646- const schedulingState = driverId ? SchedulingState . SCHEDULED : SchedulingState . UNSCHEDULED ;
701+ const schedulingState = driverId
702+ ? SchedulingState . SCHEDULED
703+ : SchedulingState . UNSCHEDULED ;
647704
648705 await prisma . $transaction (
649706 ridesData . map ( ( r ) =>
@@ -707,7 +764,6 @@ router.put('/:id', validateUser('User'), async (req, res) => {
707764 }
708765} ) ;
709766
710-
711767// Delete an existing ride
712768// ?scope=single (default) — delete only this ride
713769// ?scope=future — delete this ride and all future rides in the series
@@ -760,7 +816,10 @@ router.delete('/:id', validateUser('User'), async (req, res) => {
760816 // delete this ride and all future rides in the series
761817 if ( scope === 'future' && ride . recurrenceId ) {
762818 const { count } = await prisma . ride . deleteMany ( {
763- where : { recurrenceId : ride . recurrenceId , startTime : { gte : ride . startTime } } ,
819+ where : {
820+ recurrenceId : ride . recurrenceId ,
821+ startTime : { gte : ride . startTime } ,
822+ } ,
764823 } ) ;
765824 return res . status ( 200 ) . send ( { deleted : count } ) ;
766825 }
0 commit comments