diff --git a/apps/backend/src/config/migrations.ts b/apps/backend/src/config/migrations.ts index 0385198cc..9e322b74a 100644 --- a/apps/backend/src/config/migrations.ts +++ b/apps/backend/src/config/migrations.ts @@ -30,6 +30,7 @@ import { AddFoodRescueToDonationItems1770679339809 } from '../migrations/1770679 import { UpdateManufacturerEntity1768680807820 } from '../migrations/1768680807820-UpdateManufacturerEntity'; import { AddUserPoolId1769189327767 } from '../migrations/1769189327767-AddUserPoolId'; import { UpdateOrderEntity1769990652833 } from '../migrations/1769990652833-UpdateOrderEntity'; +import { MoveRequestFieldsToOrders1770571145350 } from '../migrations/1770571145350-MoveRequestFieldsToOrders'; import { RenameDonationMatchingStatus1771260403657 } from '../migrations/1771260403657-RenameDonationMatchingStatus'; const schemaMigrations = [ @@ -65,6 +66,7 @@ const schemaMigrations = [ UpdateManufacturerEntity1768680807820, AddUserPoolId1769189327767, UpdateOrderEntity1769990652833, + MoveRequestFieldsToOrders1770571145350, RenameDonationMatchingStatus1771260403657, ]; diff --git a/apps/backend/src/foodRequests/request.controller.spec.ts b/apps/backend/src/foodRequests/request.controller.spec.ts index de62ab829..46772a4c7 100644 --- a/apps/backend/src/foodRequests/request.controller.spec.ts +++ b/apps/backend/src/foodRequests/request.controller.spec.ts @@ -2,19 +2,13 @@ import { RequestsService } from './request.service'; import { RequestsController } from './request.controller'; import { Test, TestingModule } from '@nestjs/testing'; import { mock } from 'jest-mock-extended'; -import { AWSS3Service } from '../aws/aws-s3.service'; -import { OrdersService } from '../orders/order.service'; -import { Readable } from 'stream'; import { FoodRequest } from './request.entity'; import { RequestSize } from './types'; import { OrderStatus } from '../orders/types'; import { FoodType } from '../donationItems/types'; import { OrderDetailsDto } from './dtos/order-details.dto'; -import { Order } from '../orders/order.entity'; const mockRequestsService = mock(); -const mockOrdersService = mock(); -const mockAWSS3Service = mock(); const foodRequest: Partial = { requestId: 1, @@ -28,10 +22,7 @@ describe('RequestsController', () => { mockRequestsService.findOne.mockReset(); mockRequestsService.find.mockReset(); mockRequestsService.create.mockReset(); - mockRequestsService.updateDeliveryDetails?.mockReset(); mockRequestsService.getOrderDetails.mockReset(); - mockAWSS3Service.upload.mockReset(); - mockOrdersService.updateStatus.mockReset(); const module: TestingModule = await Test.createTestingModule({ controllers: [RequestsController], @@ -40,14 +31,6 @@ describe('RequestsController', () => { provide: RequestsService, useValue: mockRequestsService, }, - { - provide: OrdersService, - useValue: mockOrdersService, - }, - { - provide: AWSS3Service, - useValue: mockAWSS3Service, - }, ], }).compile(); @@ -151,9 +134,6 @@ describe('RequestsController', () => { requestedSize: RequestSize.MEDIUM, requestedItems: ['Test item 1', 'Test item 2'], additionalInformation: 'Test information.', - dateReceived: null, - feedback: null, - photos: null, }; const createdRequest: Partial = { @@ -175,208 +155,7 @@ describe('RequestsController', () => { createBody.requestedSize, createBody.requestedItems, createBody.additionalInformation, - createBody.dateReceived, - createBody.feedback, - createBody.photos, - ); - }); - }); - - describe('POST /:requestId/confirm-delivery', () => { - it('should upload photos, update the order, then update the request', async () => { - const requestId = 1; - - const body = { - dateReceived: new Date().toISOString(), - feedback: 'Nice delivery!', - }; - - // Mock Photos - const mockStream = {} as Readable; - - const photos: Express.Multer.File[] = [ - { - fieldname: 'photos', - originalname: 'photo1.jpg', - encoding: '7bit', - mimetype: 'image/jpeg', - buffer: Buffer.from('image1'), - size: 1000, - destination: '', - filename: '', - path: '', - stream: mockStream, - }, - { - fieldname: 'photos', - originalname: 'photo2.jpg', - encoding: '7bit', - mimetype: 'image/jpeg', - buffer: Buffer.from('image2'), - size: 2000, - destination: '', - filename: '', - path: '', - stream: mockStream, - }, - ]; - - const uploadedUrls = [ - 'https://fake-s3/photo1.jpg', - 'https://fake-s3/photo2.jpg', - ]; - - // Mock AWS upload - mockAWSS3Service.upload.mockResolvedValue(uploadedUrls); - - // Mock RequestsService.findOne - mockRequestsService.findOne.mockResolvedValue({ - requestId, - pantryId: 1, - orders: [{ orderId: 99 }], - } as FoodRequest); - - mockOrdersService.updateStatus.mockResolvedValue(); - - const order = new Order(); - order.orderId = 99; - - const updatedRequest: Partial = { - requestId, - pantryId: 1, - dateReceived: new Date(body.dateReceived), - feedback: body.feedback, - photos: uploadedUrls, - orders: [order], - }; - - mockRequestsService.updateDeliveryDetails.mockResolvedValue( - updatedRequest as FoodRequest, ); - - const result = await controller.confirmDelivery(requestId, body, photos); - - expect(mockAWSS3Service.upload).toHaveBeenCalledWith(photos); - - expect(mockOrdersService.updateStatus).toHaveBeenCalledWith( - 99, - OrderStatus.DELIVERED, - ); - - expect(mockRequestsService.updateDeliveryDetails).toHaveBeenCalledWith( - requestId, - new Date(body.dateReceived), - body.feedback, - uploadedUrls, - ); - - expect(result).toEqual(updatedRequest); - }); - - it('should handle no photos being uploaded', async () => { - const requestId = 1; - - const body = { - dateReceived: new Date().toISOString(), - feedback: 'No photos delivery!', - }; - - mockRequestsService.findOne.mockResolvedValue({ - requestId, - pantryId: 1, - orders: [{ orderId: 100 }], - } as FoodRequest); - - mockOrdersService.updateStatus.mockResolvedValue(); - - const order = new Order(); - order.orderId = 100; - - const updatedRequest: Partial = { - requestId, - pantryId: 1, - dateReceived: new Date(body.dateReceived), - feedback: body.feedback, - photos: [], - orders: [order], - }; - - mockRequestsService.updateDeliveryDetails.mockResolvedValue( - updatedRequest as FoodRequest, - ); - - const result = await controller.confirmDelivery(requestId, body); - - expect(mockAWSS3Service.upload).not.toHaveBeenCalled(); - expect(mockOrdersService.updateStatus).toHaveBeenCalledWith( - 100, - OrderStatus.DELIVERED, - ); - expect(mockRequestsService.updateDeliveryDetails).toHaveBeenCalledWith( - requestId, - new Date(body.dateReceived), - body.feedback, - [], - ); - expect(result).toEqual(updatedRequest); - }); - - it('should handle empty photos array', async () => { - const requestId = 1; - - const body = { - dateReceived: new Date().toISOString(), - feedback: 'Empty photos array delivery!', - }; - - mockRequestsService.findOne.mockResolvedValue({ - requestId, - pantryId: 1, - orders: [{ orderId: 101 }], - } as FoodRequest); - - mockOrdersService.updateStatus.mockResolvedValue(); - - const order = new Order(); - order.orderId = 101; - - const updatedRequest: Partial = { - requestId, - pantryId: 1, - dateReceived: new Date(body.dateReceived), - feedback: body.feedback, - photos: [], - orders: [order], - }; - - mockRequestsService.updateDeliveryDetails.mockResolvedValue( - updatedRequest as FoodRequest, - ); - - const result = await controller.confirmDelivery(requestId, body, []); - - expect(mockAWSS3Service.upload).not.toHaveBeenCalled(); - expect(mockOrdersService.updateStatus).toHaveBeenCalledWith( - 101, - OrderStatus.DELIVERED, - ); - expect(mockRequestsService.updateDeliveryDetails).toHaveBeenCalledWith( - requestId, - new Date(body.dateReceived), - body.feedback, - [], - ); - expect(result).toEqual(updatedRequest); - }); - - it('should throw an error for invalid date', async () => { - await expect( - controller.confirmDelivery( - 1, - { dateReceived: 'bad-date', feedback: '' }, - [], - ), - ).rejects.toThrow('Invalid date format for deliveryDate'); }); }); }); diff --git a/apps/backend/src/foodRequests/request.controller.ts b/apps/backend/src/foodRequests/request.controller.ts index 230eb0c48..665f7e571 100644 --- a/apps/backend/src/foodRequests/request.controller.ts +++ b/apps/backend/src/foodRequests/request.controller.ts @@ -5,31 +5,19 @@ import { ParseIntPipe, Post, Body, - UploadedFiles, - UseInterceptors, BadRequestException, - NotFoundException, } from '@nestjs/common'; import { ApiBody } from '@nestjs/swagger'; import { RequestsService } from './request.service'; import { FoodRequest } from './request.entity'; -import { AWSS3Service } from '../aws/aws-s3.service'; -import { FilesInterceptor } from '@nestjs/platform-express'; -import * as multer from 'multer'; import { Roles } from '../auth/roles.decorator'; import { Role } from '../users/types'; -import { OrdersService } from '../orders/order.service'; import { RequestSize } from './types'; -import { OrderStatus } from '../orders/types'; import { OrderDetailsDto } from './dtos/order-details.dto'; @Controller('requests') export class RequestsController { - constructor( - private requestsService: RequestsService, - private awsS3Service: AWSS3Service, - private ordersService: OrdersService, - ) {} + constructor(private requestsService: RequestsService) {} @Roles(Role.PANTRY, Role.ADMIN) @Get('/:requestId') @@ -76,19 +64,6 @@ export class RequestsController { nullable: true, example: 'Urgent request', }, - dateReceived: { - type: 'string', - format: 'date-time', - nullable: true, - example: null, - }, - feedback: { type: 'string', nullable: true, example: null }, - photos: { - type: 'array', - items: { type: 'string' }, - nullable: true, - example: [], - }, }, }, }) @@ -99,9 +74,6 @@ export class RequestsController { requestedSize: RequestSize; requestedItems: string[]; additionalInformation: string; - dateReceived: Date; - feedback: string; - photos: string[]; }, ): Promise { if ( @@ -114,85 +86,6 @@ export class RequestsController { body.requestedSize, body.requestedItems, body.additionalInformation, - body.dateReceived, - body.feedback, - body.photos, ); } - - @Roles(Role.PANTRY, Role.ADMIN) - //TODO: delete endpoint, here temporarily as a logic reference for order status impl. - @Post('/:requestId/confirm-delivery') - @ApiBody({ - description: 'Details for a confirmation form', - schema: { - type: 'object', - properties: { - dateReceived: { - type: 'string', - format: 'date-time', - nullable: true, - example: new Date().toISOString(), - }, - feedback: { - type: 'string', - nullable: true, - example: 'Wonderful shipment!', - }, - photos: { - type: 'array', - items: { type: 'string' }, - nullable: true, - example: [], - }, - }, - }, - }) - @UseInterceptors( - FilesInterceptor('photos', 10, { storage: multer.memoryStorage() }), - ) - async confirmDelivery( - @Param('requestId', ParseIntPipe) requestId: number, - @Body() body: { dateReceived: string; feedback: string }, - @UploadedFiles() photos?: Express.Multer.File[], - ): Promise { - const formattedDate = new Date(body.dateReceived); - if (isNaN(formattedDate.getTime())) { - throw new Error('Invalid date format for deliveryDate'); - } - - const uploadedPhotoUrls = - photos && photos.length > 0 ? await this.awsS3Service.upload(photos) : []; - console.log( - 'Received photo files:', - photos?.map((p) => p.originalname), - '| Count:', - photos?.length, - ); - - const updatedRequest = await this.requestsService.updateDeliveryDetails( - requestId, - formattedDate, - body.feedback, - uploadedPhotoUrls, - ); - - if (!updatedRequest) { - throw new NotFoundException('Invalid request ID'); - } - - if (!updatedRequest.orders || updatedRequest.orders.length == 0) { - throw new NotFoundException( - 'No associated orders found for this request', - ); - } - - await Promise.all( - updatedRequest.orders.map((order) => - this.ordersService.updateStatus(order.orderId, OrderStatus.DELIVERED), - ), - ); - - return updatedRequest; - } } diff --git a/apps/backend/src/foodRequests/request.entity.ts b/apps/backend/src/foodRequests/request.entity.ts index 25ba8e66b..8014c04a7 100644 --- a/apps/backend/src/foodRequests/request.entity.ts +++ b/apps/backend/src/foodRequests/request.entity.ts @@ -8,7 +8,7 @@ import { JoinColumn, } from 'typeorm'; import { Order } from '../orders/order.entity'; -import { RequestSize } from './types'; +import { RequestSize, FoodRequestStatus } from './types'; import { Pantry } from '../pantries/pantries.entity'; @Entity('food_requests') @@ -44,14 +44,14 @@ export class FoodRequest { }) requestedAt: Date; - @Column({ name: 'date_received', type: 'timestamp', nullable: true }) - dateReceived: Date; - - @Column({ name: 'feedback', type: 'text', nullable: true }) - feedback: string; - - @Column({ name: 'photos', type: 'text', array: true, nullable: true }) - photos: string[]; + @Column({ + name: 'status', + type: 'enum', + enumName: 'food_requests_status_enum', + enum: FoodRequestStatus, + default: FoodRequestStatus.ACTIVE, + }) + status: FoodRequestStatus; @OneToMany(() => Order, (order) => order.request, { nullable: true }) orders: Order[]; diff --git a/apps/backend/src/foodRequests/request.module.ts b/apps/backend/src/foodRequests/request.module.ts index 0e5dc2803..9fdf03554 100644 --- a/apps/backend/src/foodRequests/request.module.ts +++ b/apps/backend/src/foodRequests/request.module.ts @@ -3,21 +3,14 @@ import { TypeOrmModule } from '@nestjs/typeorm'; import { RequestsController } from './request.controller'; import { FoodRequest } from './request.entity'; import { RequestsService } from './request.service'; -import { AWSS3Module } from '../aws/aws-s3.module'; -import { MulterModule } from '@nestjs/platform-express'; import { AuthModule } from '../auth/auth.module'; -import { OrdersService } from '../orders/order.service'; import { Order } from '../orders/order.entity'; import { Pantry } from '../pantries/pantries.entity'; @Module({ - imports: [ - AWSS3Module, - MulterModule.register({ dest: './uploads' }), - TypeOrmModule.forFeature([FoodRequest, Order, Pantry]), - AuthModule, - ], + imports: [TypeOrmModule.forFeature([FoodRequest, Order, Pantry]), AuthModule], controllers: [RequestsController], - providers: [RequestsService, OrdersService], + providers: [RequestsService], + exports: [RequestsService], }) export class RequestsModule {} diff --git a/apps/backend/src/foodRequests/request.service.spec.ts b/apps/backend/src/foodRequests/request.service.spec.ts index 1eb21b692..7072157b7 100644 --- a/apps/backend/src/foodRequests/request.service.spec.ts +++ b/apps/backend/src/foodRequests/request.service.spec.ts @@ -23,9 +23,6 @@ const mockRequest: Partial = { requestedItems: ['Canned Goods', 'Vegetables'], additionalInformation: 'No onions, please.', requestedAt: null, - dateReceived: null, - feedback: null, - photos: null, orders: null, }; @@ -250,9 +247,6 @@ describe('RequestsService', () => { mockRequest.requestedSize, mockRequest.requestedItems, mockRequest.additionalInformation, - mockRequest.dateReceived, - mockRequest.feedback, - mockRequest.photos, ); expect(result).toEqual(mockRequest); @@ -261,9 +255,6 @@ describe('RequestsService', () => { requestedSize: mockRequest.requestedSize, requestedItems: mockRequest.requestedItems, additionalInformation: mockRequest.additionalInformation, - dateReceived: mockRequest.dateReceived, - feedback: mockRequest.feedback, - photos: mockRequest.photos, }); expect(mockRequestsRepository.save).toHaveBeenCalledWith(mockRequest); }); @@ -277,9 +268,6 @@ describe('RequestsService', () => { RequestSize.MEDIUM, ['Canned Goods', 'Vegetables'], 'Additional info', - null, - null, - null, ), ).rejects.toThrow(`Pantry ${invalidPantryId} not found`); @@ -299,9 +287,6 @@ describe('RequestsService', () => { requestedItems: ['Rice', 'Beans'], additionalInformation: 'Gluten-free items only.', requestedAt: null, - dateReceived: null, - feedback: null, - photos: null, orders: null, }, { @@ -311,9 +296,6 @@ describe('RequestsService', () => { requestedItems: ['Fruits', 'Snacks'], additionalInformation: 'No nuts, please.', requestedAt: null, - dateReceived: null, - feedback: null, - photos: null, orders: null, }, ]; @@ -331,117 +313,4 @@ describe('RequestsService', () => { }); }); }); - - describe('updateDeliveryDetails', () => { - it('should update and return the food request with new delivery details', async () => { - const mockOrder: Partial = { - orderId: 1, - request: null, - status: OrderStatus.SHIPPED, - createdAt: new Date(), - shippedAt: new Date(), - deliveredAt: null, - }; - - const mockRequest2: Partial = { - ...mockRequest, - orders: [mockOrder] as Order[], - }; - - const requestId = 1; - const deliveryDate = new Date(); - const feedback = 'Good delivery!'; - const photos = ['photo1.jpg', 'photo2.jpg']; - - mockRequestsRepository.findOne.mockResolvedValueOnce( - mockRequest2 as FoodRequest, - ); - - const updatedOrder = { ...mockOrder, status: OrderStatus.DELIVERED }; - - mockRequestsRepository.save.mockResolvedValueOnce({ - ...mockRequest, - dateReceived: deliveryDate, - feedback, - photos, - orders: [updatedOrder], - } as FoodRequest); - - const result = await service.updateDeliveryDetails( - requestId, - deliveryDate, - feedback, - photos, - ); - - expect(result).toEqual({ - ...mockRequest, - dateReceived: deliveryDate, - feedback, - photos, - orders: [updatedOrder], - }); - - expect(mockRequestsRepository.findOne).toHaveBeenCalledWith({ - where: { requestId }, - relations: ['orders'], - }); - - expect(mockRequestsRepository.save).toHaveBeenCalledWith({ - ...mockRequest, - dateReceived: deliveryDate, - feedback, - photos, - orders: [mockOrder], - }); - }); - - it('should throw an error if the request ID is invalid', async () => { - const requestId = 999; - const deliveryDate = new Date(); - const feedback = 'Good delivery!'; - const photos = ['photo1.jpg', 'photo2.jpg']; - - mockRequestsRepository.findOne.mockResolvedValueOnce(null); - - await expect( - service.updateDeliveryDetails( - requestId, - deliveryDate, - feedback, - photos, - ), - ).rejects.toThrow('Invalid request ID'); - - expect(mockRequestsRepository.findOne).toHaveBeenCalledWith({ - where: { requestId }, - relations: ['orders'], - }); - }); - - it('should throw an error if there is no associated order', async () => { - const requestId = 1; - const deliveryDate = new Date(); - const feedback = 'Good delivery!'; - const photos = ['photo1.jpg', 'photo2.jpg']; - - mockRequestsRepository.findOne.mockResolvedValueOnce( - mockRequest as FoodRequest, - ); - - await expect( - service.updateDeliveryDetails( - requestId, - deliveryDate, - feedback, - photos, - ), - ).rejects.toThrow('No associated orders found for this request'); - - expect(mockRequestsRepository.findOne).toHaveBeenCalledWith({ - where: { requestId }, - relations: ['orders'], - }); - }); - }); }); diff --git a/apps/backend/src/foodRequests/request.service.ts b/apps/backend/src/foodRequests/request.service.ts index c74b2f878..0deee0108 100644 --- a/apps/backend/src/foodRequests/request.service.ts +++ b/apps/backend/src/foodRequests/request.service.ts @@ -3,10 +3,11 @@ import { InjectRepository } from '@nestjs/typeorm'; import { Repository } from 'typeorm'; import { FoodRequest } from './request.entity'; import { validateId } from '../utils/validation.utils'; -import { RequestSize } from './types'; +import { FoodRequestStatus, RequestSize } from './types'; import { Pantry } from '../pantries/pantries.entity'; import { Order } from '../orders/order.entity'; import { OrderDetailsDto } from './dtos/order-details.dto'; +import { OrderStatus } from '../orders/types'; @Injectable() export class RequestsService { @@ -78,9 +79,6 @@ export class RequestsService { requestedSize: RequestSize, requestedItems: string[], additionalInformation: string | undefined, - dateReceived: Date | undefined, - feedback: string | undefined, - photos: string[] | undefined, ): Promise { validateId(pantryId, 'Pantry'); @@ -94,9 +92,6 @@ export class RequestsService { requestedSize, requestedItems, additionalInformation, - dateReceived, - feedback, - photos, }); return await this.repo.save(foodRequest); @@ -111,12 +106,7 @@ export class RequestsService { }); } - async updateDeliveryDetails( - requestId: number, - deliveryDate: Date, - feedback: string, - photos: string[], - ): Promise { + async updateRequestStatus(requestId: number): Promise { validateId(requestId, 'Request'); const request = await this.repo.findOne({ @@ -125,19 +115,25 @@ export class RequestsService { }); if (!request) { - throw new NotFoundException('Invalid request ID'); + throw new NotFoundException(`Request ${requestId} not found`); } - if (!request.orders || request.orders.length == 0) { - throw new NotFoundException( - 'No associated orders found for this request', - ); + const orders = request.orders || []; + + if (!orders.length) { + request.status = FoodRequestStatus.ACTIVE; + await this.repo.save(request); + return; } - request.feedback = feedback; - request.dateReceived = deliveryDate; - request.photos = photos; + const allDelivered = orders.every( + (order) => order.status === OrderStatus.DELIVERED, + ); + + request.status = allDelivered + ? FoodRequestStatus.CLOSED + : FoodRequestStatus.ACTIVE; - return await this.repo.save(request); + await this.repo.save(request); } } diff --git a/apps/backend/src/foodRequests/types.ts b/apps/backend/src/foodRequests/types.ts index 1057eef84..ba0378451 100644 --- a/apps/backend/src/foodRequests/types.ts +++ b/apps/backend/src/foodRequests/types.ts @@ -4,3 +4,8 @@ export enum RequestSize { MEDIUM = 'Medium (5-10 boxes)', LARGE = 'Large (10+ boxes)', } + +export enum FoodRequestStatus { + ACTIVE = 'active', + CLOSED = 'closed', +} diff --git a/apps/backend/src/migrations/1770571145350-MoveRequestFieldsToOrders.ts b/apps/backend/src/migrations/1770571145350-MoveRequestFieldsToOrders.ts new file mode 100644 index 000000000..e35aafd13 --- /dev/null +++ b/apps/backend/src/migrations/1770571145350-MoveRequestFieldsToOrders.ts @@ -0,0 +1,96 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class MoveRequestFieldsToOrders1770571145350 + implements MigrationInterface +{ + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(` + CREATE TYPE food_requests_status_enum AS ENUM ('active', 'closed'); + `); + + await queryRunner.query(` + ALTER TABLE food_requests + ADD COLUMN status food_requests_status_enum NOT NULL DEFAULT 'active', + DROP COLUMN date_received, + DROP COLUMN feedback, + DROP COLUMN photos; + `); + + await queryRunner.query(` + ALTER TABLE orders + ADD COLUMN date_received TIMESTAMP, + ADD COLUMN feedback TEXT, + ADD COLUMN photos TEXT[]; + `); + + await queryRunner.query(` + INSERT INTO public.food_requests ( + pantry_id, requested_size, requested_items, additional_information, + requested_at, status + ) VALUES ( + (SELECT pantry_id FROM public.pantries WHERE pantry_name = 'Westside Community Kitchen' LIMIT 1), + 'Small (2-5 boxes)', + ARRAY['Dairy-Free Alternatives', 'Gluten-Free Bread'], + 'Second order for active request test', + '2024-02-05 10:00:00', + 'active' + ) + `); + + await queryRunner.query(` + INSERT INTO public.orders ( + request_id, food_manufacturer_id, status, created_at, shipped_at, delivered_at + ) VALUES + ( + (SELECT request_id FROM public.food_requests WHERE additional_information = 'Second order for active request test' LIMIT 1), + (SELECT food_manufacturer_id FROM public.food_manufacturers WHERE food_manufacturer_name = 'Healthy Foods Co' LIMIT 1), + 'shipped', + '2024-02-05 11:00:00', + '2024-02-06 08:00:00', + NULL + ), + ( + (SELECT request_id FROM public.food_requests WHERE additional_information = 'Second order for active request test' LIMIT 1), + (SELECT food_manufacturer_id FROM public.food_manufacturers WHERE food_manufacturer_name = 'FoodCorp Industries' LIMIT 1), + 'shipped', + '2024-02-05 12:00:00', + '2024-02-06 09:00:00', + NULL + ) + `); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(` + DELETE FROM public.orders + WHERE request_id = ( + SELECT request_id FROM public.food_requests + WHERE additional_information = 'Second order for active request test' LIMIT 1 + ) + `); + + await queryRunner.query(` + DELETE FROM public.food_requests + WHERE additional_information = 'Second order for active request test' + `); + + await queryRunner.query(` + ALTER TABLE orders + DROP COLUMN photos, + DROP COLUMN feedback, + DROP COLUMN date_received; + `); + + await queryRunner.query(` + ALTER TABLE food_requests + ADD COLUMN date_received TIMESTAMP, + ADD COLUMN feedback TEXT, + ADD COLUMN photos TEXT[], + DROP COLUMN status; + `); + + await queryRunner.query(` + DROP TYPE food_requests_status_enum; + `); + } +} diff --git a/apps/backend/src/migrations/1771260403657-RenameDonationMatchingStatus.ts b/apps/backend/src/migrations/1771260403657-RenameDonationMatchingStatus.ts index df25b6236..07ba1fe97 100644 --- a/apps/backend/src/migrations/1771260403657-RenameDonationMatchingStatus.ts +++ b/apps/backend/src/migrations/1771260403657-RenameDonationMatchingStatus.ts @@ -5,67 +5,67 @@ export class RenameDonationMatchingStatus1771260403657 { public async up(queryRunner: QueryRunner): Promise { await queryRunner.query(` - ALTER TABLE donations - ALTER COLUMN status DROP DEFAULT; + ALTER TABLE donations + ALTER COLUMN status DROP DEFAULT; - CREATE TYPE donations_status_enum_new AS ENUM ( - 'available', - 'matched', - 'fulfilled' - ); + CREATE TYPE donations_status_enum_new AS ENUM ( + 'available', + 'matched', + 'fulfilled' + ); - ALTER TABLE donations - ALTER COLUMN status - TYPE donations_status_enum_new - USING ( - CASE - WHEN status = 'matching' - THEN 'matched' - ELSE status::text - END - )::donations_status_enum_new; + ALTER TABLE donations + ALTER COLUMN status + TYPE donations_status_enum_new + USING ( + CASE + WHEN status = 'matching' + THEN 'matched' + ELSE status::text + END + )::donations_status_enum_new; - DROP TYPE donations_status_enum; + DROP TYPE donations_status_enum; - ALTER TYPE donations_status_enum_new - RENAME TO donations_status_enum; + ALTER TYPE donations_status_enum_new + RENAME TO donations_status_enum; - ALTER TABLE donations - ALTER COLUMN status - SET DEFAULT 'available'; - `); + ALTER TABLE donations + ALTER COLUMN status + SET DEFAULT 'available'; + `); } public async down(queryRunner: QueryRunner): Promise { await queryRunner.query(` - ALTER TABLE donations - ALTER COLUMN status DROP DEFAULT; + ALTER TABLE donations + ALTER COLUMN status DROP DEFAULT; - CREATE TYPE donations_status_enum_old AS ENUM ( - 'available', - 'matching', - 'fulfilled' - ); + CREATE TYPE donations_status_enum_old AS ENUM ( + 'available', + 'matching', + 'fulfilled' + ); - ALTER TABLE donations - ALTER COLUMN status - TYPE donations_status_enum_old - USING ( - CASE - WHEN status = 'matched' - THEN 'matching' - ELSE status::text - END - )::donations_status_enum_old; + ALTER TABLE donations + ALTER COLUMN status + TYPE donations_status_enum_old + USING ( + CASE + WHEN status = 'matched' + THEN 'matching' + ELSE status::text + END + )::donations_status_enum_old; - DROP TYPE donations_status_enum; + DROP TYPE donations_status_enum; - ALTER TYPE donations_status_enum_old - RENAME TO donations_status_enum; + ALTER TYPE donations_status_enum_old + RENAME TO donations_status_enum; - ALTER TABLE donations - ALTER COLUMN status - SET DEFAULT 'available'; - `); + ALTER TABLE donations + ALTER COLUMN status + SET DEFAULT 'available'; + `); } } diff --git a/apps/backend/src/orders/dtos/confirm-delivery.dto.ts b/apps/backend/src/orders/dtos/confirm-delivery.dto.ts new file mode 100644 index 000000000..3d2c48fac --- /dev/null +++ b/apps/backend/src/orders/dtos/confirm-delivery.dto.ts @@ -0,0 +1,16 @@ +import { + IsDateString, + IsNotEmpty, + IsOptional, + IsString, +} from 'class-validator'; + +export class ConfirmDeliveryDto { + @IsDateString() + dateReceived!: string; + + @IsOptional() + @IsString() + @IsNotEmpty() + feedback?: string; +} diff --git a/apps/backend/src/orders/order.controller.spec.ts b/apps/backend/src/orders/order.controller.spec.ts index 4706f4512..5267ceede 100644 --- a/apps/backend/src/orders/order.controller.spec.ts +++ b/apps/backend/src/orders/order.controller.spec.ts @@ -8,12 +8,15 @@ import { mock } from 'jest-mock-extended'; import { OrderStatus } from './types'; import { FoodRequest } from '../foodRequests/request.entity'; import { Pantry } from '../pantries/pantries.entity'; +import { AWSS3Service } from '../aws/aws-s3.service'; import { TrackingCostDto } from './dtos/tracking-cost.dto'; import { BadRequestException, NotFoundException } from '@nestjs/common'; import { FoodManufacturer } from '../foodManufacturers/manufacturers.entity'; +import { ConfirmDeliveryDto } from './dtos/confirm-delivery.dto'; const mockOrdersService = mock(); const mockAllocationsService = mock(); +const mockAWSS3Service = mock(); describe('OrdersController', () => { let controller: OrdersController; @@ -68,6 +71,7 @@ describe('OrdersController', () => { providers: [ { provide: OrdersService, useValue: mockOrdersService }, { provide: AllocationsService, useValue: mockAllocationsService }, + { provide: AWSS3Service, useValue: mockAWSS3Service }, ], }).compile(); @@ -160,7 +164,7 @@ describe('OrdersController', () => { const result = await controller.getRequestFromOrder(orderId); - expect(result).toEqual(mockRequests[0] as Pantry); + expect(result).toEqual(mockRequests[0] as FoodRequest); expect(mockOrdersService.findOrderFoodRequest).toHaveBeenCalledWith( orderId, ); @@ -282,6 +286,112 @@ describe('OrdersController', () => { ).toHaveBeenCalledWith(orderId); }); }); + describe('confirmDelivery', () => { + beforeEach(() => { + mockAWSS3Service.upload.mockReset(); + mockOrdersService.confirmDelivery.mockReset(); + }); + + it('should upload photos and confirm delivery with all fields', async () => { + const orderId = 1; + const body: ConfirmDeliveryDto = { + dateReceived: new Date().toISOString(), + feedback: 'Great delivery!', + }; + const mockFiles: Express.Multer.File[] = [ + { + fieldname: 'photos', + originalname: 'photo1.jpg', + encoding: '7bit', + mimetype: 'image/jpeg', + buffer: Buffer.from('photo1'), + size: 1000, + } as Express.Multer.File, + ]; + + const uploadedUrls = ['https://s3.example.com/photo1.jpg']; + mockAWSS3Service.upload.mockResolvedValueOnce(uploadedUrls); + + const confirmedOrder: Partial = { + orderId, + status: OrderStatus.DELIVERED, + dateReceived: new Date(body.dateReceived), + feedback: body.feedback, + photos: uploadedUrls, + }; + mockOrdersService.confirmDelivery.mockResolvedValueOnce( + confirmedOrder as Order, + ); + + const result = await controller.confirmDelivery(orderId, body, mockFiles); + + expect(mockAWSS3Service.upload).toHaveBeenCalledWith(mockFiles); + expect(mockOrdersService.confirmDelivery).toHaveBeenCalledWith( + orderId, + body, + uploadedUrls, + ); + expect(result).toEqual(confirmedOrder); + }); + + it('should handle no photos being uploaded', async () => { + const orderId = 2; + const body: ConfirmDeliveryDto = { + dateReceived: new Date().toISOString(), + feedback: 'Delivery without photos', + }; + + const confirmedOrder: Partial = { + orderId, + status: OrderStatus.DELIVERED, + dateReceived: new Date(body.dateReceived), + feedback: body.feedback, + photos: [], + }; + mockOrdersService.confirmDelivery.mockResolvedValueOnce( + confirmedOrder as Order, + ); + + const result = await controller.confirmDelivery(orderId, body); + + expect(mockAWSS3Service.upload).not.toHaveBeenCalled(); + expect(mockOrdersService.confirmDelivery).toHaveBeenCalledWith( + orderId, + body, + [], + ); + expect(result).toEqual(confirmedOrder); + }); + + it('should handle empty photos array', async () => { + const orderId = 3; + const body: ConfirmDeliveryDto = { + dateReceived: new Date().toISOString(), + feedback: 'Empty photos', + }; + + const confirmedOrder: Partial = { + orderId, + status: OrderStatus.DELIVERED, + dateReceived: new Date(body.dateReceived), + feedback: body.feedback, + photos: [], + }; + mockOrdersService.confirmDelivery.mockResolvedValueOnce( + confirmedOrder as Order, + ); + + const result = await controller.confirmDelivery(orderId, body, []); + + expect(mockAWSS3Service.upload).not.toHaveBeenCalled(); + expect(mockOrdersService.confirmDelivery).toHaveBeenCalledWith( + orderId, + body, + [], + ); + expect(result).toEqual(confirmedOrder); + }); + }); describe('updateStatus', () => { it('should call ordersService.updateStatus', async () => { diff --git a/apps/backend/src/orders/order.controller.ts b/apps/backend/src/orders/order.controller.ts index a3ec003aa..e075e9857 100644 --- a/apps/backend/src/orders/order.controller.ts +++ b/apps/backend/src/orders/order.controller.ts @@ -1,6 +1,7 @@ import { Controller, Get, + Post, Patch, Param, ParseIntPipe, @@ -8,7 +9,10 @@ import { Query, BadRequestException, ValidationPipe, + UploadedFiles, + UseInterceptors, } from '@nestjs/common'; +import { ApiBody } from '@nestjs/swagger'; import { OrdersService } from './order.service'; import { Order } from './order.entity'; import { Pantry } from '../pantries/pantries.entity'; @@ -17,12 +21,17 @@ import { FoodRequest } from '../foodRequests/request.entity'; import { AllocationsService } from '../allocations/allocations.service'; import { OrderStatus } from './types'; import { TrackingCostDto } from './dtos/tracking-cost.dto'; +import { AWSS3Service } from '../aws/aws-s3.service'; +import { FilesInterceptor } from '@nestjs/platform-express'; +import * as multer from 'multer'; +import { ConfirmDeliveryDto } from './dtos/confirm-delivery.dto'; @Controller('orders') export class OrdersController { constructor( private readonly ordersService: OrdersService, private readonly allocationsService: AllocationsService, + private readonly awsS3Service: AWSS3Service, ) {} // Called like: /?status=pending&pantryName=Test%20Pantry&pantryName=Test%20Pantry%202 @@ -110,4 +119,46 @@ export class OrdersController { ): Promise { return this.ordersService.updateTrackingCostInfo(orderId, dto); } + + @Patch('/:orderId/confirm-delivery') + @ApiBody({ + description: 'Details for a confirmation of order delivery form', + schema: { + type: 'object', + properties: { + dateReceived: { + type: 'string', + format: 'date-time', + example: new Date().toISOString(), + }, + feedback: { + type: 'string', + nullable: true, + example: 'Wonderful shipment!', + }, + photos: { + type: 'array', + items: { type: 'string' }, + nullable: true, + example: [ + 'https://s3.amazonaws.com/bucket/photo1.jpg', + 'https://s3.amazonaws.com/bucket/photo2.jpg', + ], + }, + }, + }, + }) + @UseInterceptors( + FilesInterceptor('photos', 10, { storage: multer.memoryStorage() }), + ) + async confirmDelivery( + @Param('orderId', ParseIntPipe) orderId: number, + @Body() body: ConfirmDeliveryDto, + @UploadedFiles() photos?: Express.Multer.File[], + ): Promise { + const uploadedPhotoUrls = + photos && photos.length > 0 ? await this.awsS3Service.upload(photos) : []; + + return this.ordersService.confirmDelivery(orderId, body, uploadedPhotoUrls); + } } diff --git a/apps/backend/src/orders/order.entity.ts b/apps/backend/src/orders/order.entity.ts index 2913ff150..6a7636bec 100644 --- a/apps/backend/src/orders/order.entity.ts +++ b/apps/backend/src/orders/order.entity.ts @@ -58,14 +58,23 @@ export class Order { type: 'timestamp', nullable: true, }) - shippedAt?: Date; + shippedAt!: Date | null; @Column({ name: 'delivered_at', type: 'timestamp', nullable: true, }) - deliveredAt?: Date; + deliveredAt!: Date | null; + + @Column({ name: 'date_received', type: 'timestamp', nullable: true }) + dateReceived!: Date | null; + + @Column({ name: 'feedback', type: 'text', nullable: true }) + feedback!: string | null; + + @Column({ name: 'photos', type: 'text', array: true, nullable: true }) + photos!: string[] | null; @OneToMany(() => Allocation, (allocation) => allocation.order) allocations!: Allocation[]; @@ -76,7 +85,7 @@ export class Order { length: 255, nullable: true, }) - trackingLink?: string; + trackingLink!: string | null; @Column({ name: 'shipping_cost', @@ -85,5 +94,5 @@ export class Order { scale: 2, nullable: true, }) - shippingCost?: number; + shippingCost!: number | null; } diff --git a/apps/backend/src/orders/order.module.ts b/apps/backend/src/orders/order.module.ts index 0734f8760..c312b0d18 100644 --- a/apps/backend/src/orders/order.module.ts +++ b/apps/backend/src/orders/order.module.ts @@ -3,17 +3,22 @@ import { TypeOrmModule } from '@nestjs/typeorm'; import { OrdersController } from './order.controller'; import { Order } from './order.entity'; import { OrdersService } from './order.service'; -import { JwtStrategy } from '../auth/jwt.strategy'; -import { AuthService } from '../auth/auth.service'; import { Pantry } from '../pantries/pantries.entity'; import { AllocationModule } from '../allocations/allocations.module'; import { AuthModule } from '../auth/auth.module'; +import { FoodRequest } from '../foodRequests/request.entity'; +import { AWSS3Module } from '../aws/aws-s3.module'; +import { MulterModule } from '@nestjs/platform-express'; +import { RequestsModule } from '../foodRequests/request.module'; @Module({ imports: [ - TypeOrmModule.forFeature([Order, Pantry]), + TypeOrmModule.forFeature([Order, Pantry, FoodRequest]), AllocationModule, forwardRef(() => AuthModule), + AWSS3Module, + MulterModule.register({ dest: './uploads' }), + forwardRef(() => RequestsModule), ], controllers: [OrdersController], providers: [OrdersService], diff --git a/apps/backend/src/orders/order.service.spec.ts b/apps/backend/src/orders/order.service.spec.ts index 8d89fe1cf..a8fa1e9c7 100644 --- a/apps/backend/src/orders/order.service.spec.ts +++ b/apps/backend/src/orders/order.service.spec.ts @@ -7,6 +7,10 @@ import { OrderStatus } from './types'; import { Pantry } from '../pantries/pantries.entity'; import { BadRequestException, NotFoundException } from '@nestjs/common'; import { TrackingCostDto } from './dtos/tracking-cost.dto'; +import { FoodRequest } from '../foodRequests/request.entity'; +import 'multer'; +import { FoodRequestStatus } from '../foodRequests/types'; +import { RequestsService } from '../foodRequests/request.service'; // Set 1 minute timeout for async DB operations jest.setTimeout(60000); @@ -27,6 +31,7 @@ describe('OrdersService', () => { const module: TestingModule = await Test.createTestingModule({ providers: [ OrdersService, + RequestsService, { provide: getRepositoryToken(Order), useValue: testDataSource.getRepository(Order), @@ -35,6 +40,10 @@ describe('OrdersService', () => { provide: getRepositoryToken(Pantry), useValue: testDataSource.getRepository(Pantry), }, + { + provide: getRepositoryToken(FoodRequest), + useValue: testDataSource.getRepository(FoodRequest), + }, ], }).compile(); @@ -127,7 +136,7 @@ describe('OrdersService', () => { describe('getCurrentOrders', () => { it(`returns only orders with status 'pending' or 'shipped'`, async () => { const orders = await service.getCurrentOrders(); - expect(orders).toHaveLength(2); + expect(orders).toHaveLength(4); expect( orders.every( (order) => @@ -394,4 +403,153 @@ describe('OrdersService', () => { expect(updatedOrder.shippedAt).toBeDefined(); }); }); + + describe('confirmDelivery', () => { + it('should throw BadRequestException for invalid date format', async () => { + await expect( + service.confirmDelivery( + 1, + { dateReceived: 'invalid-date', feedback: 'test feedback' }, + [], + ), + ).rejects.toThrow( + new BadRequestException('Invalid date format for dateReceived'), + ); + }); + + it('should update order with delivery details and set status to delivered and update request status to closed', async () => { + const orderRepo = testDataSource.getRepository(Order); + const requestRepo = testDataSource.getRepository(FoodRequest); + + const shippedOrder = await orderRepo.findOne({ + where: { status: OrderStatus.SHIPPED, orderId: 3 }, + relations: ['request'], + }); + + expect(shippedOrder).toBeDefined(); + + const dateReceived = new Date().toISOString(); + const feedback = 'Perfect delivery!'; + const photos = ['photo1.jpg', 'photo2.jpg']; + + const result = await service.confirmDelivery( + shippedOrder.orderId, + { dateReceived, feedback }, + photos, + ); + + expect(result.orderId).toBe(shippedOrder.orderId); + expect(result.status).toBe(OrderStatus.DELIVERED); + expect(result.dateReceived).toEqual(new Date(dateReceived)); + expect(result.feedback).toBe(feedback); + expect(result.photos).toEqual(photos); + expect(result.deliveredAt).toBeNull(); + + const updatedRequest = await requestRepo.findOne({ + where: { requestId: shippedOrder.requestId }, + relations: ['orders'], + }); + + expect(updatedRequest.status).toBe(FoodRequestStatus.CLOSED); + }); + + it('should update order with delivery details and set status to delivered but request remains active', async () => { + const orderRepo = testDataSource.getRepository(Order); + const requestRepo = testDataSource.getRepository(FoodRequest); + + // Get an existing shipped order + const existingShippedOrder = await orderRepo.findOne({ + where: { status: OrderStatus.SHIPPED }, + relations: ['request'], + }); + + expect(existingShippedOrder).toBeDefined(); + + // Add a second shipped order to the same request so it stays active after delivery + const secondOrder = orderRepo.create({ + requestId: existingShippedOrder.requestId, + foodManufacturerId: existingShippedOrder.foodManufacturerId, + status: OrderStatus.SHIPPED, + shippedAt: new Date(), + }); + await orderRepo.save(secondOrder); + + const dateReceived = new Date().toISOString(); + const feedback = 'Perfect delivery!'; + const photos = ['photo1.jpg', 'photo2.jpg']; + + const result = await service.confirmDelivery( + existingShippedOrder.orderId, + { dateReceived, feedback }, + photos, + ); + + expect(result.orderId).toBe(existingShippedOrder.orderId); + expect(result.status).toBe(OrderStatus.DELIVERED); + expect(result.dateReceived).toEqual(new Date(dateReceived)); + expect(result.feedback).toBe(feedback); + expect(result.photos).toEqual(photos); + + const updatedRequest = await requestRepo.findOne({ + where: { requestId: existingShippedOrder.requestId }, + relations: ['orders'], + }); + + expect(updatedRequest.status).toBe(FoodRequestStatus.ACTIVE); + }); + + it('should throw NotFoundException for invalid order id', async () => { + const invalidOrderId = 99999; + + await expect( + service.confirmDelivery( + invalidOrderId, + { dateReceived: new Date().toISOString(), feedback: 'test' }, + [], + ), + ).rejects.toThrow( + new NotFoundException(`Order ${invalidOrderId} not found`), + ); + }); + + it('should throw BadRequestException when order is not shipped', async () => { + const orderRepo = testDataSource.getRepository(Order); + + const pendingOrder = await orderRepo.findOne({ + where: { status: OrderStatus.PENDING }, + }); + + expect(pendingOrder).toBeDefined(); + + await expect( + service.confirmDelivery( + pendingOrder.orderId, + { dateReceived: new Date().toISOString(), feedback: 'test' }, + [], + ), + ).rejects.toThrow( + new BadRequestException('Can only confirm delivery for shipped orders'), + ); + }); + + it('should throw BadRequestException when order is already delivered', async () => { + const orderRepo = testDataSource.getRepository(Order); + + const deliveredOrder = await orderRepo.findOne({ + where: { status: OrderStatus.DELIVERED }, + }); + + expect(deliveredOrder).toBeDefined(); + + await expect( + service.confirmDelivery( + deliveredOrder.orderId, + { dateReceived: new Date().toISOString(), feedback: 'test' }, + [], + ), + ).rejects.toThrow( + new BadRequestException('Can only confirm delivery for shipped orders'), + ); + }); + }); }); diff --git a/apps/backend/src/orders/order.service.ts b/apps/backend/src/orders/order.service.ts index b7af3beb0..8b62a51af 100644 --- a/apps/backend/src/orders/order.service.ts +++ b/apps/backend/src/orders/order.service.ts @@ -12,12 +12,15 @@ import { FoodRequest } from '../foodRequests/request.entity'; import { validateId } from '../utils/validation.utils'; import { OrderStatus } from './types'; import { TrackingCostDto } from './dtos/tracking-cost.dto'; +import { ConfirmDeliveryDto } from './dtos/confirm-delivery.dto'; +import { RequestsService } from '../foodRequests/request.service'; @Injectable() export class OrdersService { constructor( @InjectRepository(Order) private repo: Repository, @InjectRepository(Pantry) private pantryRepo: Repository, + private requestsService: RequestsService, ) {} // TODO: when order is created, set FM @@ -150,6 +153,44 @@ export class OrdersService { .execute(); } + async confirmDelivery( + orderId: number, + dto: ConfirmDeliveryDto, + photos: string[], + ): Promise { + validateId(orderId, 'Order'); + + const formattedDate = new Date(dto.dateReceived); + if (isNaN(formattedDate.getTime())) { + throw new BadRequestException('Invalid date format for dateReceived'); + } + + const order = await this.repo.findOne({ + where: { orderId }, + }); + + if (!order) { + throw new NotFoundException(`Order ${orderId} not found`); + } + + if (order.status !== OrderStatus.SHIPPED) { + throw new BadRequestException( + 'Can only confirm delivery for shipped orders', + ); + } + + order.dateReceived = formattedDate; + order.feedback = dto.feedback; + order.photos = photos; + order.status = OrderStatus.DELIVERED; + + const updatedOrder = await this.repo.save(order); + + await this.requestsService.updateRequestStatus(order.requestId); + + return updatedOrder; + } + async getOrdersByPantry(pantryId: number): Promise { validateId(pantryId, 'Pantry'); diff --git a/apps/frontend/src/types/types.ts b/apps/frontend/src/types/types.ts index ce85f3b4a..481ba8965 100644 --- a/apps/frontend/src/types/types.ts +++ b/apps/frontend/src/types/types.ts @@ -196,13 +196,11 @@ export interface FoodRequest { requestId: number; pantryId: number; pantry: Pantry; - requestedSize: string; + requestedSize: RequestSize; requestedItems: string[]; additionalInformation: string | null; requestedAt: string; - dateReceived: string | null; - feedback: string | null; - photos: string[] | null; + status: FoodRequestStatus; orders?: Order[]; } @@ -219,6 +217,9 @@ export interface Order { allocations: Allocation[]; trackingLink?: string; shippingCost?: number; + dateReceived?: string; + feedback?: string; + photos?: string[]; } export interface OrderItemDetails { @@ -265,12 +266,9 @@ export interface ManufacturerApplicationDto { export interface CreateFoodRequestBody { pantryId: number; - requestedSize: string; + requestedSize: RequestSize; requestedItems: string[]; - additionalInformation: string | null | undefined; - dateReceived: string | null | undefined; - feedback: string | null | undefined; - photos: string[] | null | undefined; + additionalInformation?: string | null; } export interface CreateMultipleDonationItemsBody { @@ -316,6 +314,11 @@ export enum RequestSize { LARGE = 'Large (10+ boxes)', } +export enum FoodRequestStatus { + ACTIVE = 'active', + CLOSED = 'closed', +} + export enum DonationFrequency { YEARLY = 'yearly', BIWEEKLY = 'biweekly', diff --git a/package.json b/package.json index 2e7aebc87..2bc442e8c 100644 --- a/package.json +++ b/package.json @@ -79,7 +79,7 @@ "@nx/webpack": "22.5.1", "@testing-library/react": "16.1.0", "@types/jest": "30.0.0", - "@types/multer": "^1.4.12", + "@types/multer": "^2.0.0", "@types/node": "^18.14.2", "@types/nodemailer": "^6.4.17", "@types/passport-jwt": "^4.0.1", diff --git a/yarn.lock b/yarn.lock index 6ef08033f..7f23e2cf2 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5900,10 +5900,10 @@ resolved "https://registry.yarnpkg.com/@types/ms/-/ms-2.1.0.tgz#052aa67a48eccc4309d7f0191b7e41434b90bb78" integrity sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA== -"@types/multer@^1.4.12": - version "1.4.13" - resolved "https://registry.yarnpkg.com/@types/multer/-/multer-1.4.13.tgz#be483f909a77f13e0624cac3d001859eb12ae68b" - integrity sha512-bhhdtPw7JqCiEfC9Jimx5LqX9BDIPJEh2q/fQ4bqbBPtyEZYr3cvF22NwG0DmPZNYA0CAf2CnqDB4KIGGpJcaw== +"@types/multer@^2.0.0": + version "2.0.0" + resolved "https://registry.yarnpkg.com/@types/multer/-/multer-2.0.0.tgz#db5f82136b619f5ce4d923b00034eb466c13acf4" + integrity sha512-C3Z9v9Evij2yST3RSBktxP9STm6OdMc5uR1xF1SGr98uv8dUlAL2hqwrZ3GVB3uyMyiegnscEK6PGtYvNrjTjw== dependencies: "@types/express" "*"