diff --git a/.env.template b/.env.template index bb5a55f..9413042 100644 --- a/.env.template +++ b/.env.template @@ -1,4 +1,4 @@ ENVIRONMENT= HOST= PORT= -AIRTABLE_API_KEY= \ No newline at end of file +AIRTABLE_API_KEY= diff --git a/src/controllers/FlightRequest.controller.ts b/src/controllers/FlightRequest.controller.ts index af1eecd..3c97a1e 100644 --- a/src/controllers/FlightRequest.controller.ts +++ b/src/controllers/FlightRequest.controller.ts @@ -1,10 +1,18 @@ /* eslint-disable no-irregular-whitespace */ import { createTestFlightLegData } from '../data/test-data'; +import logger from '../util/logger'; +import { trimFlightLeg, trimRequest } from '../util/trim'; import Airtable from 'airtable'; import dotenv from 'dotenv'; +import type { FlightLegData } from '../interfaces/legs/flight-leg.interface'; import type { Request, Response } from 'express'; +import type { FlightRequestData } from '../interfaces/requests/flight-request.interface'; dotenv.config(); +const base = new Airtable({ + apiKey: process.env.AIRTABLE_API_KEY || '', +}).base('appwPsfAb6U8CV3mf'); + /** * This function returns all flight requests for a given user * @@ -23,18 +31,54 @@ export const getAllFlightRequestsForUser = async ( req: Request, res: Response ) => { - // get the userId from the query parameters const { userId } = req.query; - if (userId === null) { + if (!userId) { return res.status(400).json({ error: 'Passenger ID missing' }); - } // create a fake array of flight requests + } + + try { + // query Airtable for flight requests for the user ID + const flightRequests = await base('Flight Requests (Trips)') + .select({ + filterByFormula: `{Patient AirTable Record ID} = "${userId}"`, + }) + .all(); - const flightRequests = Array.from({ length: 10 }, () => - createTestFlightLegData() - ); // return the flight requests for the user + if (flightRequests.length === 0) { + return res + .status(400) + .json({ error: 'No flight requests found for this user' }); + } + + const trimmedFlightRequests = flightRequests.map(request => + trimRequest(request as unknown as FlightRequestData) + ); - res.status(200).send(flightRequests); + // Retrieve flight legs for each flight request and format the data + const formattedFlightRequests = await Promise.all( + trimmedFlightRequests.map(async request => { + const flightLegs = await base('Flight Legs') + .select({ + filterByFormula: `{Request AirTable Record ID} = "${request.id}"`, + }) + .all(); + + // Format the flight request data and include the corresponding flight legs + return { + ...request, + flightLegs: flightLegs.map(leg => + trimFlightLeg(leg as unknown as FlightLegData) + ), + }; + }) + ); + + return res.status(200).json(formattedFlightRequests); + } catch (error) { + console.error(error); + return res.status(500).json({ error: 'Error fetching flight requests' }); + } }; /** @@ -68,7 +112,7 @@ export const getFlightRequestById = async (req: Request, res: Response) => { } ); } catch (err: any) { - console.error(err); + logger.error(err); return res.status(500).json({ error: 'Error fetching record' }); } }; @@ -102,7 +146,7 @@ export const getFlightLegsById = async (req: Request, res: Response) => { return res.status(400).json({ error: 'No record found' }); } } catch (err: any) { - console.error(err); + logger.error(err); return res.status(500).json({ error: 'Error fetching record' }); } diff --git a/src/controllers/Passenger.controller.ts b/src/controllers/Passenger.controller.ts index b8ad1c3..1b461d6 100644 --- a/src/controllers/Passenger.controller.ts +++ b/src/controllers/Passenger.controller.ts @@ -42,21 +42,23 @@ export const getAllPassengersForUser = async (req: Request, res: Response) => { return; } else { // get related passengers information - const accompPassengers = [] as Record
[]; - const accompanyingPassengersPromise = record._rawJson.fields[ - 'Related Accompanying Passenger(s)' - ].map(async (id: string) => { - // map through the related passengers and get the passenger information for each one - const passenger = await base('Passengers').find(id.toString()); - accompPassengers.push(passenger); - }); + const accompPassengersPromise = [] as Promise>[]; + + record._rawJson.fields['Related Accompanying Passenger(s)']?.map( + async (id: string) => { + // map through the related passengers and get the passenger information for each one + const passenger = base('Passengers').find(id.toString()); + accompPassengersPromise.push(passenger); + } + ); // Remove any unnecessary data from the passengers - await Promise.all(accompanyingPassengersPromise); - const trimmedPassengers = accompPassengers.map( - (passenger: Record
) => + const passengers = await Promise.all(accompPassengersPromise); + + const trimmedPassengers = + passengers?.map((passenger: Record
) => trimPassenger(passenger._rawJson as unknown as PassengerData) - ); + ) || []; // return the passengers for the user return res.send(trimmedPassengers); @@ -65,7 +67,7 @@ export const getAllPassengersForUser = async (req: Request, res: Response) => { ); } catch (err: any) { // if that fails return a 500 - console.error(err); + logger.error(err); return res.status(500).json({ error: 'Error fetching record' }); } }; @@ -84,7 +86,37 @@ export const getAllPassengersForUser = async (req: Request, res: Response) => { * @param res - the response object */ export const getPassengerById = async (req: Request, res: Response) => { - // const { userId } = req.query; + const { id: userId } = req.params; + + if (!userId) { + return res.status(400).json({ error: 'Passenger ID missing' }); + } + + const base = new Airtable({ + apiKey: process.env.AIRTABLE_API_KEY || '', + }).base('appwPsfAb6U8CV3mf'); + + try { + await base('Passengers').find( + userId.toString(), + async (err: any, record: any | undefined) => { + if (err) { + return res.status(400).send({ error: 'No passenger found' }); + } else { + // remove any unnecessary data from the passenger + const trimmedPassenger = trimPassenger( + record._rawJson as unknown as PassengerData + ); + + // return the passenger + return res.send(trimmedPassenger); + } + } + ); + } catch (err: any) { + logger.error(err); + return res.status(500).json({ error: 'Error fetching record' }); + } }; /** @@ -244,18 +276,56 @@ export const createPassenger = async (req: Request, res: Response) => { * @param res - the response object */ export const updatePassenger = async (req: Request, res: Response) => { - // get the passengerId from the query parameters - // const { passengerId } = req.query; + const { id } = req.params; + const passengerData = req.body; - // get the passenger data from the request body - // const data = req.body; + if (!id) { + return res.status(400).send({ error: 'User ID is required' }); + } + if (!passengerData) { + return res.status(400).send({ error: 'Passenger data is required' }); + } - // validate the passenger data using Joi - // ... + const schema = Joi.object({ + Street: Joi.string().optional(), + City: Joi.string().optional(), + State: Joi.string().optional(), + Country: Joi.string().optional(), + Email: Joi.string().email().optional(), + 'Cell Phone': Joi.string().optional(), + 'Home Phone': Joi.string().optional(), + Education: Joi.string().optional(), + 'Household Income': Joi.number().optional(), + 'Household Size': Joi.number().optional(), + 'Marital Status': Joi.string().optional(), + Employment: Joi.string().optional, + 'Military Service': Joi.string().optional(), + 'Military Member': Joi.array().optional(), + }); - // create a fake passenger - const passenger = createTestPassengerData(); + if (schema.validate(passengerData).error) { + return res.status(400).send({ error: 'Invalid passenger data' }); + } - // return the updated passenger - res.status(200).send(passenger); + const base = new Airtable({ + apiKey: process.env.AIRTABLE_API_KEY || '', + }).base('appwPsfAb6U8CV3mf'); + + try { + // make a call to AirTable to update the passenger + await base('Passengers').update( + [{ id, fields: passengerData }], + async (err, records) => { + if (err) { + logger.error(err); + return; + } + res.status(200).send(records); + } + ); + } catch (err: any) { + // if that fails return a 500 + logger.error(err); + return res.status(500).json({ error: 'Error updating' }); + } }; diff --git a/src/data/test-data.ts b/src/data/test-data.ts index bd8ba08..10a9aa2 100644 --- a/src/data/test-data.ts +++ b/src/data/test-data.ts @@ -191,6 +191,34 @@ export const createTestPassengerData = ( Created: faker.date.recent().toISOString(), 'Latest Trip': faker.date.recent().toISOString(), 'TS City, State (from Treatment Site Totals 2)': [], + 'Cell Phone': faker.phone.number(), + 'Home Phone': faker.phone.number(), + Education: faker.helpers.arrayElement([ + 'Less than high school degree', + 'High school degree or equivalent', + 'Some college, no degree', + 'Associate degree', + "Bachelor's degree", + 'Graduate or professional degree', + ]), + + 'Marital Status': faker.helpers.arrayElement([ + 'Single', + 'Married', + 'Divorced', + 'Widowed', + ]), + Employment: faker.helpers.arrayElement([ + 'Employed, working 40+ hours per week', + 'Employed, working 1-39 hours per week', + 'Unemployed, looking for work', + 'Unemployed, not looking for work', + 'Retired', + 'Student', + 'Homemaker', + 'Unable to work', + 'Other', + ]), ...manualData.fields, }, ...manualData, diff --git a/src/interfaces/passenger/passenger.interface.ts b/src/interfaces/passenger/passenger.interface.ts index 7ed2565..0d912cb 100644 --- a/src/interfaces/passenger/passenger.interface.ts +++ b/src/interfaces/passenger/passenger.interface.ts @@ -11,8 +11,13 @@ export interface PassengerData { City: string; Country: string; Email: string; + 'Cell Phone': string; + 'Home Phone': string; + Education: string; 'Household Income': number; 'Household Size': number; + 'Marital Status': string; + Employment: string; Ethnicity: string[]; 'Military Service': string; 'Military Member': string[]; diff --git a/src/interfaces/passenger/trimmed-passenger.interface.ts b/src/interfaces/passenger/trimmed-passenger.interface.ts index 2cf1964..0fb89de 100644 --- a/src/interfaces/passenger/trimmed-passenger.interface.ts +++ b/src/interfaces/passenger/trimmed-passenger.interface.ts @@ -13,6 +13,11 @@ export interface TrimmedPassenger { Email: string; 'Household Income': number; 'Household Size': number; + 'Cell Phone': string; + 'Home Phone': string; + Education: string; + 'Marital Status': string; + Employment: string; Ethnicity: string[]; 'Military Service': string; 'Military Member': string[]; @@ -24,6 +29,7 @@ export interface TrimmedPassenger { '# of Booked Flight Requests': number; 'Birth Month': string; 'Full Name': string; + 'Passenger Names (from All Flight Legs)': string[]; Age: number; 'Latest Trip': string; } diff --git a/src/routes/routes.ts b/src/routes/routes.ts index 6682241..c3a4bc2 100644 --- a/src/routes/routes.ts +++ b/src/routes/routes.ts @@ -32,7 +32,7 @@ const routes = (app: Express) => { app.get('/test/retrievePassengers', retrievePassengers); /* Passenger Controller Routes */ - app.get('/passenger/', getAllPassengersForUser); + app.get('/passenger/accompanying', getAllPassengersForUser); app.get('/passenger/:id', getPassengerById); app.post('/passenger/:id', createPassenger); app.put('/passenger/:id', updatePassenger); diff --git a/src/tests/FlightRequest.tests.ts b/src/tests/FlightRequest.tests.ts index 5942b96..a5eeb60 100644 --- a/src/tests/FlightRequest.tests.ts +++ b/src/tests/FlightRequest.tests.ts @@ -26,6 +26,49 @@ after(done => { done(); }); +// Test getAllFlightRequestsForUser +describe('GET /requests/', () => { + it('should return requests with the correct passenger name, flight request ID, and flight leg departure time for a given user', done => { + chai + .request(app) + .get('/requests/?userId=recV1y3bJr9eb2U5W') // Assuming userId is passed as a query parameter + .end((err, res) => { + expect(res).to.have.status(200); + const requests = res.body; + + // Assuming we're testing the first request in the array for simplicity + const firstRequest = requests[0]; + expect(firstRequest).to.have.property('id').that.is.a('string'); + expect(firstRequest) + .to.have.nested.property('Request ID') + .that.is.a('string'); + expect(firstRequest) + .to.have.nested.property('Request ID') + .that.equals( + '2022-12-08 | In Progress | Gilchrist, Stormie | 2014-06-21' + ); + + // Assuming flightLegs is an array and we're testing the first flight leg for simplicity + const firstFlightLeg = firstRequest.flightLegs[0]; + expect(firstFlightLeg) + .to.have.nested.property('Departure Date/Time') + .that.equals('2023-02-06'); + + done(); + }); + }); + + it('should return a 400 response for invalid or missing ID', done => { + chai + .request(app) + .get('/requests/?userId=blahblahblah') + .end((err, res) => { + expect(res).to.have.status(400); + done(); + }); + }); +}); + // Test getFlightRequestByID describe('GET /requests', () => { it('should return the correct request ID, origin airport, destination airport, and passenger name', done => { diff --git a/src/tests/Passenger.tests.ts b/src/tests/Passenger.tests.ts index bc4f87a..a9e2fdc 100644 --- a/src/tests/Passenger.tests.ts +++ b/src/tests/Passenger.tests.ts @@ -29,11 +29,11 @@ after(done => { // describe is group of tests // it is the actual test itself // Test case -describe('GET /passenger', () => { +describe('GET /passenger/accompanying', () => { it('should return a 400 response', done => { chai .request(app) - .get('/passenger') + .get('/passenger/accompanying') .query({ id: '' }) .end((err, res) => { expect(res).to.have.status(400); @@ -43,13 +43,14 @@ describe('GET /passenger', () => { it('should be an accompanying passenger', done => { chai .request(app) - .get('/passenger') + .get('/passenger/accompanying') .query({ id: 'recleNlsBm3dheZHy' }) .end((err, res) => { expect(res.body[0]['First Name']).to.be.oneOf([ 'Anakin', 'Bail', 'Jefferson', + 'Jefferson ', ]); expect(res).to.have.status(200); done(); @@ -58,7 +59,7 @@ describe('GET /passenger', () => { it('should be an accompanying passenger', done => { chai .request(app) - .get('/passenger') + .get('/passenger/accompanying') .query({ id: 'recleNlsBm3dheZHy' }) .end((err, res) => { expect(res.body[1]['Gender']).to.equal('Male'); @@ -69,7 +70,7 @@ describe('GET /passenger', () => { it('should be an accompanying passenger', done => { chai .request(app) - .get('/passenger') + .get('/passenger/accompanying') .query({ id: 'recleNlsBm3dheZHy' }) .end((err, res) => { expect(res.body[2]['Relationship']).to.be.oneOf(['Father', undefined]); @@ -157,7 +158,107 @@ describe('POST /createPassenger', () => { .end((err, res) => { expect(res).to.have.status(200); expect(res.body['First Name']).to.be.oneOf(['Testman']); + done(); }); }); + + describe('GET /passenger/:id', () => { + it('should return a 400 response', done => { + chai + .request(app) + .get('/passenger/blahblahblah') + + .end((err, res) => { + expect(res).to.have.status(400); + done(); + }); + }); + it('should return the correct passenger', done => { + chai + .request(app) + .get('/passenger/rec3Wv1VViXYv3t72') + .end((err, res) => { + expect(res).to.have.status(200); + expect(res.body['First Name'].toString()).to.equal('Anakin'); + expect(res.body['Last Name'].toString()).to.equal('Skywalker '); + expect(res.body['Email']).to.equal('zachmcmullen04@gmail.com'); + done(); + }); + }); + }); + + describe('PUT passenger/:id', () => { + it('should return a 400 response', done => { + chai + .request(app) + .put('/passenger/junk') + .send({ id: '' }) + .end((err, res) => { + expect(res).to.have.status(400); + done(); + }); + }); + + it('should return a 400 response', done => { + chai + .request(app) + .put('/passenger/junk') + .send({ passengerData: '' }) + .end((err, res) => { + expect(res).to.have.status(400); + done(); + }); + }); + it('should update street for anakin skywalker', done => { + chai + .request(app) + .put('/passenger/rec3Wv1VViXYv3t72') + .send({ Street: 'HELLOSTREET' }) + .end((err, res) => { + expect(res).to.have.status(200); + done(); + }); + }); + it('should update marital status for princess leia', done => { + chai + .request(app) + .put('/passenger/recaUmd14q3YOP3Uf') + .send({ 'Marital Status': 'Married' }) + .end((err, res) => { + expect(res).to.have.status(200); + done(); + }); + }); + it('should update household size for princess leia', done => { + chai + .request(app) + .put('/passenger/recaUmd14q3YOP3Uf') + .send({ 'Household Size': 3 }) + .end((err, res) => { + expect(res).to.have.status(200); + done(); + }); + }); + it('should return a 400 response', done => { + chai + .request(app) + .put('/passenger/recaUmd14q3YOP3Uf') + .send({ 'Household Size': 'test' }) + .end((err, res) => { + expect(res).to.have.status(400); + done(); + }); + }); + it('should update email for jefferson morales', done => { + chai + .request(app) + .put('/passenger/recLFdznCJOUPEx72') + .send({ Email: 'loser@weirdo.com' }) + .end((err, res) => { + expect(res).to.have.status(200); + done(); + }); + }); + }); }); diff --git a/src/tests/Trimming.tests.ts b/src/tests/Trimming.tests.ts index 2f7c7a3..d5d60de 100644 --- a/src/tests/Trimming.tests.ts +++ b/src/tests/Trimming.tests.ts @@ -258,6 +258,9 @@ describe('UTIL Trimming', () => { expect(passengerData.fields['Full Name']).to.equal( trimmedPassenger['Full Name'] ); + expect( + passengerData.fields['Passenger Names (from All Flight Legs)'] + ).to.equal(trimmedPassenger['Passenger Names (from All Flight Legs)']); expect(passengerData.fields.Age).to.equal(trimmedPassenger.Age); expect(passengerData.fields['Latest Trip']).to.equal( trimmedPassenger['Latest Trip'] diff --git a/src/util/trim.ts b/src/util/trim.ts index d945b68..cf102dc 100644 --- a/src/util/trim.ts +++ b/src/util/trim.ts @@ -46,8 +46,15 @@ export const trimPassenger = (passenger: PassengerData): TrimmedPassenger => { '# of Booked Flight Requests': fields['# of Booked Flight Requests'], 'Birth Month': fields['Birth Month'], 'Full Name': fields['Full Name'], + 'Passenger Names (from All Flight Legs)': + fields['Passenger Names (from All Flight Legs)'], Age: Age, 'Latest Trip': fields['Latest Trip'], + 'Cell Phone': fields['Cell Phone'], + 'Home Phone': fields['Home Phone'], + Education: fields['Education'], + 'Marital Status': fields['Marital Status'], + Employment: fields['Employment'], }; return trimmedPassenger; diff --git a/src/util/trimming/passengers.ts b/src/util/trimming/passengers.ts new file mode 100644 index 0000000..2d0d403 --- /dev/null +++ b/src/util/trimming/passengers.ts @@ -0,0 +1,56 @@ +import type { PassengerData } from '../../interfaces/passenger/passenger.interface'; +import type { TrimmedPassenger } from '../../interfaces/passenger/trimmed-passenger.interface'; + +export const trimPassenger = (passenger: PassengerData): TrimmedPassenger => { + const { id, createdTime, fields } = passenger; + + const { + Type, + Gender, + Street, + City, + Country, + Email, + Ethnicity, + Diagnosis, + Age, + } = fields; + + const trimmedPassenger: TrimmedPassenger = { + id: id, + createdTime: createdTime, + Type: Type, + 'First Name': fields['First Name'], + 'Last Name': fields['Last Name'], + 'Date of Birth': fields['Date of Birth'], + Gender: Gender, + Street: Street, + City: City, + Country: Country, + Email: Email, + 'Household Income': fields['Household Income'], + 'Household Size': fields['Household Size'], + Ethnicity: Ethnicity, + 'Military Service': fields['Military Service'], + 'Military Member': fields['Military Member'], + 'How did you hear about us': fields['How did you hear about us'], + 'All Flight Legs': fields['All Flight Legs'], + Diagnosis: Diagnosis, + 'AirTable Record ID': fields['AirTable Record ID'], + '# of Flight Legs': fields['# of Flight Legs'], + '# of Booked Flight Requests': fields['# of Booked Flight Requests'], + 'Birth Month': fields['Birth Month'], + 'Full Name': fields['Full Name'], + 'Passenger Names (from All Flight Legs)': + fields['Passenger Names (from All Flight Legs)'], + Age: Age, + 'Latest Trip': fields['Latest Trip'], + 'Cell Phone': fields['Cell Phone'], + 'Home Phone': fields['Home Phone'], + Education: fields['Education'], + 'Marital Status': fields['Marital Status'], + Employment: fields['Employment'], + }; + + return trimmedPassenger; +};