From a3ec50ae8e2cb582d47c2887cbb2276d6b9ab1e8 Mon Sep 17 00:00:00 2001 From: Tarun-Nagesh Date: Sun, 8 Feb 2026 12:43:51 -0500 Subject: [PATCH 01/17] Updated logic for moving fields --- apps/backend/src/config/migrations.ts | 2 + .../src/foodRequests/request.controller.ts | 108 +----------------- .../src/foodRequests/request.entity.ts | 18 +-- .../src/foodRequests/request.service.ts | 46 -------- apps/backend/src/foodRequests/types.ts | 5 + ...1770571145350-MoveRequestFieldsToOrders.ts | 46 ++++++++ apps/backend/src/orders/order.controller.ts | 58 ++++++++++ apps/backend/src/orders/order.entity.ts | 9 ++ apps/backend/src/orders/order.module.ts | 10 +- apps/backend/src/orders/order.service.ts | 54 +++++++++ 10 files changed, 193 insertions(+), 163 deletions(-) create mode 100644 apps/backend/src/migrations/1770571145350-MoveRequestFieldsToOrders.ts diff --git a/apps/backend/src/config/migrations.ts b/apps/backend/src/config/migrations.ts index 9573736c8..e7a9739ce 100644 --- a/apps/backend/src/config/migrations.ts +++ b/apps/backend/src/config/migrations.ts @@ -25,6 +25,7 @@ import { RemoveUnusedStatuses1764816885341 } from '../migrations/1764816885341-R import { UpdatePantryFields1763762628431 } from '../migrations/1763762628431-UpdatePantryFields'; import { PopulateDummyData1768501812134 } from '../migrations/1768501812134-populateDummyData'; import { RemovePantryFromOrders1769316004958 } from '../migrations/1769316004958-RemovePantryFromOrders'; +import { MoveRequestFieldsToOrders1770571145350 } from '../migrations/1770571145350-MoveRequestFieldsToOrders'; const schemaMigrations = [ User1725726359198, @@ -54,6 +55,7 @@ const schemaMigrations = [ RemoveUnusedStatuses1764816885341, PopulateDummyData1768501812134, RemovePantryFromOrders1769316004958, + MoveRequestFieldsToOrders1770571145350, ]; export default schemaMigrations; diff --git a/apps/backend/src/foodRequests/request.controller.ts b/apps/backend/src/foodRequests/request.controller.ts index ec6dc0f04..167734ea0 100644 --- a/apps/backend/src/foodRequests/request.controller.ts +++ b/apps/backend/src/foodRequests/request.controller.ts @@ -5,30 +5,18 @@ 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 { OrdersService } from '../orders/order.service'; import { RequestSize } from './types'; -import { OrderStatus } from '../orders/types'; import { OrderDetailsDto } from './dtos/order-details.dto'; @Controller('requests') // @UseInterceptors() export class RequestsController { - constructor( - private requestsService: RequestsService, - private awsS3Service: AWSS3Service, - private ordersService: OrdersService, - ) {} + constructor(private requestsService: RequestsService) {} @Get('/:requestId') async getRequest( @@ -73,19 +61,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: [], - }, }, }, }) @@ -96,9 +71,6 @@ export class RequestsController { requestedSize: RequestSize; requestedItems: string[]; additionalInformation: string; - dateReceived: Date; - feedback: string; - photos: string[]; }, ): Promise { if ( @@ -111,84 +83,6 @@ export class RequestsController { body.requestedSize, body.requestedItems, body.additionalInformation, - body.dateReceived, - body.feedback, - body.photos, - ); - } - - //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.service.ts b/apps/backend/src/foodRequests/request.service.ts index 80093c583..c3ebcb729 100644 --- a/apps/backend/src/foodRequests/request.service.ts +++ b/apps/backend/src/foodRequests/request.service.ts @@ -78,9 +78,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 +91,6 @@ export class RequestsService { requestedSize, requestedItems, additionalInformation, - dateReceived, - feedback, - photos, }); return await this.repo.save(foodRequest); @@ -110,44 +104,4 @@ export class RequestsService { relations: ['orders'], }); } - - async updateDeliveryDetails( - requestId: number, - deliveryDate: Date, - feedback: string, - photos: string[], - ): Promise { - validateId(requestId, 'Request'); - - const request = await this.repo.findOne({ - where: { requestId }, - relations: ['orders'], - }); - - if (!request) { - throw new NotFoundException('Invalid request ID'); - } - - if (!request.orders || request.orders.length == 0) { - throw new NotFoundException( - 'No associated orders found for this request', - ); - } - - const orders = request.orders; - - for (const order of orders) { - if (!order.shippedBy) { - throw new NotFoundException( - 'No associated food manufacturer found for an associated order', - ); - } - } - - request.feedback = feedback; - request.dateReceived = deliveryDate; - request.photos = photos; - - return await this.repo.save(request); - } } diff --git a/apps/backend/src/foodRequests/types.ts b/apps/backend/src/foodRequests/types.ts index 1057eef84..670faa346 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..4e24fad60 --- /dev/null +++ b/apps/backend/src/migrations/1770571145350-MoveRequestFieldsToOrders.ts @@ -0,0 +1,46 @@ +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 NULL, + ADD COLUMN feedback TEXT NULL, + ADD COLUMN photos TEXT[] NULL; + `); + } + + public async down(queryRunner: QueryRunner): Promise { + 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 NULL, + ADD COLUMN feedback TEXT NULL, + ADD COLUMN photos TEXT[] NULL, + DROP COLUMN status; + `); + + await queryRunner.query(` + DROP TYPE food_requests_status_enum; + `); + } +} diff --git a/apps/backend/src/orders/order.controller.ts b/apps/backend/src/orders/order.controller.ts index 870dc1eff..8d414e622 100644 --- a/apps/backend/src/orders/order.controller.ts +++ b/apps/backend/src/orders/order.controller.ts @@ -1,13 +1,17 @@ import { Controller, Get, + Post, Patch, Param, ParseIntPipe, Body, Query, BadRequestException, + 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'; @@ -15,12 +19,16 @@ import { FoodManufacturer } from '../foodManufacturers/manufacturer.entity'; import { FoodRequest } from '../foodRequests/request.entity'; import { AllocationsService } from '../allocations/allocations.service'; import { OrderStatus } from './types'; +import { AWSS3Service } from '../aws/aws-s3.service'; +import { FilesInterceptor } from '@nestjs/platform-express'; +import * as multer from 'multer'; @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 @@ -99,4 +107,54 @@ export class OrdersController { } return this.ordersService.updateStatus(orderId, newStatus as OrderStatus); } + + @Post('/:orderId/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('orderId', ParseIntPipe) orderId: 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 BadRequestException('Invalid date format for dateReceived'); + } + + const uploadedPhotoUrls = + photos && photos.length > 0 ? await this.awsS3Service.upload(photos) : []; + + return this.ordersService.confirmDelivery( + orderId, + formattedDate, + body.feedback, + uploadedPhotoUrls, + ); + } } diff --git a/apps/backend/src/orders/order.entity.ts b/apps/backend/src/orders/order.entity.ts index 9a246d0c9..616b807aa 100644 --- a/apps/backend/src/orders/order.entity.ts +++ b/apps/backend/src/orders/order.entity.ts @@ -67,6 +67,15 @@ export class Order { }) 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[]; } diff --git a/apps/backend/src/orders/order.module.ts b/apps/backend/src/orders/order.module.ts index 4937eced7..1bd82d9c9 100644 --- a/apps/backend/src/orders/order.module.ts +++ b/apps/backend/src/orders/order.module.ts @@ -7,9 +7,17 @@ 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 { FoodRequest } from '../foodRequests/request.entity'; +import { AWSS3Module } from '../aws/aws-s3.module'; +import { MulterModule } from '@nestjs/platform-express'; @Module({ - imports: [TypeOrmModule.forFeature([Order, Pantry]), AllocationModule], + imports: [ + TypeOrmModule.forFeature([Order, Pantry, FoodRequest]), + AllocationModule, + AWSS3Module, + MulterModule.register({ dest: './uploads' }), + ], controllers: [OrdersController], providers: [OrdersService, AuthService, JwtStrategy], exports: [OrdersService], diff --git a/apps/backend/src/orders/order.service.ts b/apps/backend/src/orders/order.service.ts index fdce0ab38..20c984996 100644 --- a/apps/backend/src/orders/order.service.ts +++ b/apps/backend/src/orders/order.service.ts @@ -5,6 +5,7 @@ import { Order } from './order.entity'; import { Pantry } from '../pantries/pantries.entity'; import { FoodManufacturer } from '../foodManufacturers/manufacturer.entity'; import { FoodRequest } from '../foodRequests/request.entity'; +import { FoodRequestStatus } from '../foodRequests/types'; import { validateId } from '../utils/validation.utils'; import { OrderStatus } from './types'; @@ -13,6 +14,8 @@ export class OrdersService { constructor( @InjectRepository(Order) private repo: Repository, @InjectRepository(Pantry) private pantryRepo: Repository, + @InjectRepository(FoodRequest) + private requestRepo: Repository, ) {} async getAll(filters?: { status?: string; pantryNames?: string[] }) { @@ -144,6 +147,57 @@ export class OrdersService { .execute(); } + async confirmDelivery( + orderId: number, + dateReceived: Date, + feedback: string, + photos: string[], + ): Promise { + validateId(orderId, 'Order'); + + const order = await this.repo.findOne({ + where: { orderId }, + }); + + if (!order) { + throw new NotFoundException(`Order ${orderId} not found`); + } + + order.dateReceived = dateReceived; + order.feedback = feedback; + order.photos = photos; + order.status = OrderStatus.DELIVERED; + order.deliveredAt = dateReceived; + + const updatedOrder = await this.repo.save(order); + + await this.updateRequestStatus(order.requestId); + + return updatedOrder; + } + + private async updateRequestStatus(requestId: number): Promise { + const request = await this.requestRepo.findOne({ + where: { requestId }, + relations: ['orders'], + }); + + if (!request) { + throw new NotFoundException(`Request ${requestId} not found`); + } + + const orders = request.orders || []; + const allDelivered = + orders.length > 0 && + orders.every((order) => order.status === OrderStatus.DELIVERED); + + request.status = allDelivered + ? FoodRequestStatus.CLOSED + : FoodRequestStatus.ACTIVE; + + await this.requestRepo.save(request); + } + async getOrdersByPantry(pantryId: number): Promise { validateId(pantryId, 'Pantry'); From f4a22c219bddf3426eae7abcaba75f3889db6441 Mon Sep 17 00:00:00 2001 From: Tarun-Nagesh Date: Sun, 8 Feb 2026 13:16:51 -0500 Subject: [PATCH 02/17] Added/changed tests --- .../foodRequests/request.controller.spec.ts | 16 +- .../src/foodRequests/request.service.spec.ts | 32 ++-- .../src/orders/order.controller.spec.ts | 128 +++++++++++++++ apps/backend/src/orders/order.service.spec.ts | 152 ++++++++++++++++++ 4 files changed, 299 insertions(+), 29 deletions(-) diff --git a/apps/backend/src/foodRequests/request.controller.spec.ts b/apps/backend/src/foodRequests/request.controller.spec.ts index 45b0d2d28..b06977793 100644 --- a/apps/backend/src/foodRequests/request.controller.spec.ts +++ b/apps/backend/src/foodRequests/request.controller.spec.ts @@ -28,7 +28,7 @@ describe('RequestsController', () => { mockRequestsService.findOne.mockReset(); mockRequestsService.find.mockReset(); mockRequestsService.create.mockReset(); - mockRequestsService.updateDeliveryDetails?.mockReset(); + // mockRequestsService.updateDeliveryDetails?.mockReset(); // Removed - method no longer exists mockRequestsService.getOrderDetails.mockReset(); mockAWSS3Service.upload.mockReset(); mockOrdersService.updateStatus.mockReset(); @@ -151,9 +151,9 @@ describe('RequestsController', () => { requestedSize: RequestSize.MEDIUM, requestedItems: ['Test item 1', 'Test item 2'], additionalInformation: 'Test information.', - dateReceived: null, - feedback: null, - photos: null, + // dateReceived: null, // Removed - no longer on FoodRequest + // feedback: null, // Removed - no longer on FoodRequest + // photos: null, // Removed - no longer on FoodRequest }; const createdRequest: Partial = { @@ -175,14 +175,12 @@ describe('RequestsController', () => { createBody.requestedSize, createBody.requestedItems, createBody.additionalInformation, - createBody.dateReceived, - createBody.feedback, - createBody.photos, ); }); }); - describe('POST /:requestId/confirm-delivery', () => { + // COMMENTED OUT: This endpoint was moved to orders controller + /* describe('POST /:requestId/confirm-delivery', () => { it('should upload photos, update the order, then update the request', async () => { const requestId = 1; @@ -379,5 +377,5 @@ describe('RequestsController', () => { ), ).rejects.toThrow('Invalid date format for deliveryDate'); }); - }); + }); */ }); diff --git a/apps/backend/src/foodRequests/request.service.spec.ts b/apps/backend/src/foodRequests/request.service.spec.ts index 23e07c877..c61c32cd4 100644 --- a/apps/backend/src/foodRequests/request.service.spec.ts +++ b/apps/backend/src/foodRequests/request.service.spec.ts @@ -23,9 +23,9 @@ const mockRequest: Partial = { requestedItems: ['Canned Goods', 'Vegetables'], additionalInformation: 'No onions, please.', requestedAt: null, - dateReceived: null, - feedback: null, - photos: null, + // dateReceived: null, // Removed - no longer on FoodRequest + // feedback: null, // Removed - no longer on FoodRequest + // photos: null, // Removed - no longer on FoodRequest orders: null, }; @@ -250,9 +250,6 @@ describe('RequestsService', () => { mockRequest.requestedSize, mockRequest.requestedItems, mockRequest.additionalInformation, - mockRequest.dateReceived, - mockRequest.feedback, - mockRequest.photos, ); expect(result).toEqual(mockRequest); @@ -261,9 +258,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 +271,6 @@ describe('RequestsService', () => { RequestSize.MEDIUM, ['Canned Goods', 'Vegetables'], 'Additional info', - null, - null, - null, ), ).rejects.toThrow(`Pantry ${invalidPantryId} not found`); @@ -299,9 +290,9 @@ describe('RequestsService', () => { requestedItems: ['Rice', 'Beans'], additionalInformation: 'Gluten-free items only.', requestedAt: null, - dateReceived: null, - feedback: null, - photos: null, + // dateReceived: null, // Removed + // feedback: null, // Removed + // photos: null, // Removed orders: null, }, { @@ -311,9 +302,9 @@ describe('RequestsService', () => { requestedItems: ['Fruits', 'Snacks'], additionalInformation: 'No nuts, please.', requestedAt: null, - dateReceived: null, - feedback: null, - photos: null, + // dateReceived: null, // Removed + // feedback: null, // Removed + // photos: null, // Removed orders: null, }, ]; @@ -332,7 +323,8 @@ describe('RequestsService', () => { }); }); - describe('updateDeliveryDetails', () => { + // COMMENTED OUT: updateDeliveryDetails method was removed, functionality moved to orders + /* describe('updateDeliveryDetails', () => { it('should update and return the food request with new delivery details', async () => { const mockOrder: Partial = { orderId: 1, @@ -489,5 +481,5 @@ describe('RequestsService', () => { relations: ['orders'], }); }); - }); + }); */ }); diff --git a/apps/backend/src/orders/order.controller.spec.ts b/apps/backend/src/orders/order.controller.spec.ts index e2e448185..30a20b175 100644 --- a/apps/backend/src/orders/order.controller.spec.ts +++ b/apps/backend/src/orders/order.controller.spec.ts @@ -8,9 +8,11 @@ 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'; const mockOrdersService = mock(); const mockAllocationsService = mock(); +const mockAWSS3Service = mock(); describe('OrdersController', () => { let controller: OrdersController; @@ -57,6 +59,7 @@ describe('OrdersController', () => { providers: [ { provide: OrdersService, useValue: mockOrdersService }, { provide: AllocationsService, useValue: mockAllocationsService }, + { provide: AWSS3Service, useValue: mockAWSS3Service }, ], }).compile(); @@ -100,4 +103,129 @@ 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 = { + dateReceived: new Date().toISOString(), + feedback: 'Great delivery!', + }; + const mockFiles = [ + { + 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, + new Date(body.dateReceived), + body.feedback, + uploadedUrls, + ); + expect(result).toEqual(confirmedOrder); + }); + + it('should handle no photos being uploaded', async () => { + const orderId = 2; + const body = { + 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, + new Date(body.dateReceived), + body.feedback, + [], + ); + expect(result).toEqual(confirmedOrder); + }); + + it('should handle empty photos array', async () => { + const orderId = 3; + const body = { + 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, + new Date(body.dateReceived), + body.feedback, + [], + ); + expect(result).toEqual(confirmedOrder); + }); + + it('should throw BadRequestException for invalid date format', async () => { + const orderId = 1; + const body = { + dateReceived: 'invalid-date', + feedback: 'test', + }; + + await expect(controller.confirmDelivery(orderId, body)).rejects.toThrow( + 'Invalid date format for dateReceived', + ); + + expect(mockAWSS3Service.upload).not.toHaveBeenCalled(); + expect(mockOrdersService.confirmDelivery).not.toHaveBeenCalled(); + }); + }); }); diff --git a/apps/backend/src/orders/order.service.spec.ts b/apps/backend/src/orders/order.service.spec.ts index e8e41949e..ab7a81358 100644 --- a/apps/backend/src/orders/order.service.spec.ts +++ b/apps/backend/src/orders/order.service.spec.ts @@ -5,6 +5,9 @@ import { Order } from './order.entity'; import { testDataSource } from '../config/typeormTestDataSource'; import { OrderStatus } from './types'; import { Pantry } from '../pantries/pantries.entity'; +import { FoodRequest } from '../foodRequests/request.entity'; +import { Repository } from 'typeorm'; +import { FoodRequestStatus } from '../foodRequests/types'; // Set 1 minute timeout for async DB operations jest.setTimeout(60000); @@ -33,6 +36,10 @@ describe('OrdersService', () => { provide: getRepositoryToken(Pantry), useValue: testDataSource.getRepository(Pantry), }, + { + provide: getRepositoryToken(FoodRequest), + useValue: testDataSource.getRepository(FoodRequest), + }, ], }).compile(); @@ -121,4 +128,149 @@ describe('OrdersService', () => { expect(orders[0].status).toBe(OrderStatus.DELIVERED); }); }); + + describe('confirmDelivery', () => { + it('should update order with delivery details and set status to delivered', async () => { + // Get an existing shipped order from dummy data + const orderRepo = testDataSource.getRepository(Order); + const requestRepo = testDataSource.getRepository(FoodRequest); + + const shippedOrder = await orderRepo.findOne({ + where: { status: OrderStatus.SHIPPED }, + relations: ['request'], + }); + + expect(shippedOrder).toBeDefined(); + + const dateReceived = new Date(); + 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(dateReceived); + expect(result.feedback).toBe(feedback); + expect(result.photos).toEqual(photos); + expect(result.deliveredAt).toBeDefined(); + + // Verify request status was updated + const updatedRequest = await requestRepo.findOne({ + where: { requestId: shippedOrder.requestId }, + relations: ['orders'], + }); + + // Check if all orders for this request are delivered + const allDelivered = updatedRequest.orders.every( + (order) => order.status === OrderStatus.DELIVERED, + ); + + if (allDelivered) { + expect(updatedRequest.status).toBe(FoodRequestStatus.CLOSED); + } else { + expect(updatedRequest.status).toBe(FoodRequestStatus.ACTIVE); + } + }); + + it('should set request status to CLOSED when all orders are delivered', async () => { + const orderRepo = testDataSource.getRepository(Order); + const requestRepo = testDataSource.getRepository(FoodRequest); + + // Find a request with only one order that's shipped + const request = await requestRepo.findOne({ + where: { status: FoodRequestStatus.ACTIVE }, + relations: ['orders'], + }); + + // Find a shipped order for this request + const shippedOrder = request.orders.find( + (order) => order.status === OrderStatus.SHIPPED, + ); + + if (shippedOrder) { + // Mark all other orders as delivered first + for (const order of request.orders) { + if (order.orderId !== shippedOrder.orderId) { + order.status = OrderStatus.DELIVERED; + await orderRepo.save(order); + } + } + + // Now confirm the last shipped order + await service.confirmDelivery( + shippedOrder.orderId, + new Date(), + 'Final delivery', + [], + ); + + // Verify request is now closed + const updatedRequest = await requestRepo.findOne({ + where: { requestId: request.requestId }, + relations: ['orders'], + }); + + expect( + updatedRequest.orders.every( + (o) => o.status === OrderStatus.DELIVERED, + ), + ).toBe(true); + expect(updatedRequest.status).toBe(FoodRequestStatus.CLOSED); + } + }); + + it('should set request status to ACTIVE when not all orders are delivered', async () => { + const orderRepo = testDataSource.getRepository(Order); + const requestRepo = testDataSource.getRepository(FoodRequest); + + // Find a request with multiple orders + const request = await requestRepo.findOne({ + where: { status: FoodRequestStatus.ACTIVE }, + relations: ['orders'], + }); + + if (request && request.orders.length > 1) { + const shippedOrder = request.orders.find( + (order) => order.status === OrderStatus.SHIPPED, + ); + + if (shippedOrder) { + // Confirm only one order, leaving others undelivered + await service.confirmDelivery( + shippedOrder.orderId, + new Date(), + 'Partial delivery', + [], + ); + + // Verify request is still active + const updatedRequest = await requestRepo.findOne({ + where: { requestId: request.requestId }, + relations: ['orders'], + }); + + expect( + updatedRequest.orders.some( + (o) => o.status !== OrderStatus.DELIVERED, + ), + ).toBe(true); + expect(updatedRequest.status).toBe(FoodRequestStatus.ACTIVE); + } + } + }); + + it('should throw NotFoundException for invalid order id', async () => { + const invalidOrderId = 99999; + + await expect( + service.confirmDelivery(invalidOrderId, new Date(), 'test', []), + ).rejects.toThrow(`Order ${invalidOrderId} not found`); + }); + }); }); From bb71e3e500b55cda63d76943ed89b196e6ccc338 Mon Sep 17 00:00:00 2001 From: Tarun-Nagesh Date: Sun, 8 Feb 2026 13:36:12 -0500 Subject: [PATCH 03/17] Some scuffed workaround, unsure if it has any other implications --- apps/backend/src/aws/aws-s3.service.ts | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/apps/backend/src/aws/aws-s3.service.ts b/apps/backend/src/aws/aws-s3.service.ts index 045038d52..c588a52f7 100644 --- a/apps/backend/src/aws/aws-s3.service.ts +++ b/apps/backend/src/aws/aws-s3.service.ts @@ -1,6 +1,15 @@ import { Injectable } from '@nestjs/common'; import { S3Client, PutObjectCommand } from '@aws-sdk/client-s3'; +interface MulterFile { + fieldname: string; + originalname: string; + encoding: string; + mimetype: string; + size: number; + buffer: Buffer; +} + @Injectable() export class AWSS3Service { private client: S3Client; @@ -22,7 +31,7 @@ export class AWSS3Service { }); } - async upload(files: Express.Multer.File[]): Promise { + async upload(files: MulterFile[]): Promise { const uploadedFileUrls: string[] = []; try { for (const file of files) { From 4dd7a44d14c38a85067ebe0a88994e9de94cba27 Mon Sep 17 00:00:00 2001 From: Tarun-Nagesh Date: Wed, 18 Feb 2026 23:10:38 -0500 Subject: [PATCH 04/17] cleanup and comments --- .../foodRequests/request.controller.spec.ts | 220 ------------------ .../src/foodRequests/request.module.ts | 11 +- .../src/foodRequests/request.service.spec.ts | 169 -------------- apps/backend/src/foodRequests/types.ts | 4 +- ...1770571145350-MoveRequestFieldsToOrders.ts | 16 +- .../src/orders/dtos/confirm-delivery.dto.ts | 5 + apps/backend/src/orders/order.controller.ts | 5 +- apps/backend/src/orders/order.entity.ts | 6 +- apps/backend/src/orders/order.service.spec.ts | 2 +- apps/backend/src/orders/order.service.ts | 13 +- apps/frontend/src/types/types.ts | 23 +- 11 files changed, 46 insertions(+), 428 deletions(-) create mode 100644 apps/backend/src/orders/dtos/confirm-delivery.dto.ts diff --git a/apps/backend/src/foodRequests/request.controller.spec.ts b/apps/backend/src/foodRequests/request.controller.spec.ts index b06977793..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(); // Removed - method no longer exists 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, // Removed - no longer on FoodRequest - // feedback: null, // Removed - no longer on FoodRequest - // photos: null, // Removed - no longer on FoodRequest }; const createdRequest: Partial = { @@ -178,204 +158,4 @@ describe('RequestsController', () => { ); }); }); - - // COMMENTED OUT: This endpoint was moved to orders controller - /* 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 = new Readable(); - mockStream._read = () => {}; - - 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.module.ts b/apps/backend/src/foodRequests/request.module.ts index 14a605d80..756c63d9a 100644 --- a/apps/backend/src/foodRequests/request.module.ts +++ b/apps/backend/src/foodRequests/request.module.ts @@ -5,19 +5,12 @@ import { FoodRequest } from './request.entity'; import { RequestsService } from './request.service'; import { JwtStrategy } from '../auth/jwt.strategy'; import { AuthService } from '../auth/auth.service'; -import { AWSS3Module } from '../aws/aws-s3.module'; -import { MulterModule } from '@nestjs/platform-express'; -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]), - ], + imports: [TypeOrmModule.forFeature([FoodRequest, Order, Pantry])], controllers: [RequestsController], - providers: [RequestsService, OrdersService, AuthService, JwtStrategy], + providers: [RequestsService, AuthService, JwtStrategy], }) export class RequestsModule {} diff --git a/apps/backend/src/foodRequests/request.service.spec.ts b/apps/backend/src/foodRequests/request.service.spec.ts index c61c32cd4..077e98961 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, // Removed - no longer on FoodRequest - // feedback: null, // Removed - no longer on FoodRequest - // photos: null, // Removed - no longer on FoodRequest orders: null, }; @@ -290,9 +287,6 @@ describe('RequestsService', () => { requestedItems: ['Rice', 'Beans'], additionalInformation: 'Gluten-free items only.', requestedAt: null, - // dateReceived: null, // Removed - // feedback: null, // Removed - // photos: null, // Removed orders: null, }, { @@ -302,9 +296,6 @@ describe('RequestsService', () => { requestedItems: ['Fruits', 'Snacks'], additionalInformation: 'No nuts, please.', requestedAt: null, - // dateReceived: null, // Removed - // feedback: null, // Removed - // photos: null, // Removed orders: null, }, ]; @@ -322,164 +313,4 @@ describe('RequestsService', () => { }); }); }); - - // COMMENTED OUT: updateDeliveryDetails method was removed, functionality moved to orders - /* describe('updateDeliveryDetails', () => { - it('should update and return the food request with new delivery details', async () => { - const mockOrder: Partial = { - orderId: 1, - request: null, - requestId: 1, - foodManufacturer: null, - shippedBy: 1, - 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'], - }); - }); - - it('should throw an error if the order does not have a food manufacturer', async () => { - const mockOrder: Partial = { - orderId: 1, - request: null, - requestId: 1, - foodManufacturer: null, - shippedBy: 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, - ); - - await expect( - service.updateDeliveryDetails( - requestId, - deliveryDate, - feedback, - photos, - ), - ).rejects.toThrow( - 'No associated food manufacturer found for an associated order', - ); - - expect(mockRequestsRepository.findOne).toHaveBeenCalledWith({ - where: { requestId }, - relations: ['orders'], - }); - }); - }); */ }); diff --git a/apps/backend/src/foodRequests/types.ts b/apps/backend/src/foodRequests/types.ts index 670faa346..ba0378451 100644 --- a/apps/backend/src/foodRequests/types.ts +++ b/apps/backend/src/foodRequests/types.ts @@ -6,6 +6,6 @@ export enum RequestSize { } export enum FoodRequestStatus { - ACTIVE = 'Active', - CLOSED = 'Closed', + ACTIVE = 'active', + CLOSED = 'closed', } diff --git a/apps/backend/src/migrations/1770571145350-MoveRequestFieldsToOrders.ts b/apps/backend/src/migrations/1770571145350-MoveRequestFieldsToOrders.ts index 4e24fad60..566edd093 100644 --- a/apps/backend/src/migrations/1770571145350-MoveRequestFieldsToOrders.ts +++ b/apps/backend/src/migrations/1770571145350-MoveRequestFieldsToOrders.ts @@ -4,12 +4,12 @@ export class MoveRequestFieldsToOrders1770571145350 implements MigrationInterfac public async up(queryRunner: QueryRunner): Promise { await queryRunner.query(` - CREATE TYPE food_requests_status_enum AS ENUM ('Active', 'Closed'); + 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', + ADD COLUMN status food_requests_status_enum NOT NULL DEFAULT 'active', DROP COLUMN date_received, DROP COLUMN feedback, DROP COLUMN photos; @@ -17,9 +17,9 @@ export class MoveRequestFieldsToOrders1770571145350 implements MigrationInterfac await queryRunner.query(` ALTER TABLE orders - ADD COLUMN date_received TIMESTAMP NULL, - ADD COLUMN feedback TEXT NULL, - ADD COLUMN photos TEXT[] NULL; + ADD COLUMN date_received TIMESTAMP, + ADD COLUMN feedback TEXT, + ADD COLUMN photos TEXT[]; `); } @@ -33,9 +33,9 @@ export class MoveRequestFieldsToOrders1770571145350 implements MigrationInterfac await queryRunner.query(` ALTER TABLE food_requests - ADD COLUMN date_received TIMESTAMP NULL, - ADD COLUMN feedback TEXT NULL, - ADD COLUMN photos TEXT[] NULL, + ADD COLUMN date_received TIMESTAMP, + ADD COLUMN feedback TEXT, + ADD COLUMN photos TEXT[], DROP COLUMN status; `); 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..e54703c66 --- /dev/null +++ b/apps/backend/src/orders/dtos/confirm-delivery.dto.ts @@ -0,0 +1,5 @@ +export class ConfirmDeliveryDto { + dateReceived: string; + feedback: string; + photos?: Express.Multer.File[]; +} diff --git a/apps/backend/src/orders/order.controller.ts b/apps/backend/src/orders/order.controller.ts index 8d414e622..0fb27bc77 100644 --- a/apps/backend/src/orders/order.controller.ts +++ b/apps/backend/src/orders/order.controller.ts @@ -22,6 +22,7 @@ import { OrderStatus } from './types'; 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 { @@ -139,9 +140,11 @@ export class OrdersController { ) async confirmDelivery( @Param('orderId', ParseIntPipe) orderId: number, - @Body() body: { dateReceived: string; feedback: string }, + @Body() body: ConfirmDeliveryDto, @UploadedFiles() photos?: Express.Multer.File[], ): Promise { + body.photos = photos; + const formattedDate = new Date(body.dateReceived); if (isNaN(formattedDate.getTime())) { throw new BadRequestException('Invalid date format for dateReceived'); diff --git a/apps/backend/src/orders/order.entity.ts b/apps/backend/src/orders/order.entity.ts index 616b807aa..b3604ad5a 100644 --- a/apps/backend/src/orders/order.entity.ts +++ b/apps/backend/src/orders/order.entity.ts @@ -68,13 +68,13 @@ export class Order { deliveredAt: Date | null; @Column({ name: 'date_received', type: 'timestamp', nullable: true }) - dateReceived: Date | null; + dateReceived?: Date | null; @Column({ name: 'feedback', type: 'text', nullable: true }) - feedback: string | null; + feedback?: string | null; @Column({ name: 'photos', type: 'text', array: true, nullable: true }) - photos: string[] | null; + photos?: string[] | null; @OneToMany(() => Allocation, (allocation) => allocation.order) allocations: Allocation[]; diff --git a/apps/backend/src/orders/order.service.spec.ts b/apps/backend/src/orders/order.service.spec.ts index ab7a81358..1652a4eec 100644 --- a/apps/backend/src/orders/order.service.spec.ts +++ b/apps/backend/src/orders/order.service.spec.ts @@ -158,7 +158,7 @@ describe('OrdersService', () => { expect(result.dateReceived).toEqual(dateReceived); expect(result.feedback).toBe(feedback); expect(result.photos).toEqual(photos); - expect(result.deliveredAt).toBeDefined(); + expect(result.deliveredAt).toBeNull(); // Verify request status was updated const updatedRequest = await requestRepo.findOne({ diff --git a/apps/backend/src/orders/order.service.ts b/apps/backend/src/orders/order.service.ts index 20c984996..4ad6833ed 100644 --- a/apps/backend/src/orders/order.service.ts +++ b/apps/backend/src/orders/order.service.ts @@ -167,7 +167,6 @@ export class OrdersService { order.feedback = feedback; order.photos = photos; order.status = OrderStatus.DELIVERED; - order.deliveredAt = dateReceived; const updatedOrder = await this.repo.save(order); @@ -177,6 +176,8 @@ export class OrdersService { } private async updateRequestStatus(requestId: number): Promise { + validateId(requestId, 'Request'); + const request = await this.requestRepo.findOne({ where: { requestId }, relations: ['orders'], @@ -187,9 +188,13 @@ export class OrdersService { } const orders = request.orders || []; - const allDelivered = - orders.length > 0 && - orders.every((order) => order.status === OrderStatus.DELIVERED); + if (!orders.length) { + throw new NotFoundException(`No orders found for request ${requestId}`); + } + + const allDelivered = orders.every( + (order) => order.status === OrderStatus.DELIVERED, + ); request.status = allDelivered ? FoodRequestStatus.CLOSED diff --git a/apps/frontend/src/types/types.ts b/apps/frontend/src/types/types.ts index 12fd07980..b4e33ec4c 100644 --- a/apps/frontend/src/types/types.ts +++ b/apps/frontend/src/types/types.ts @@ -180,13 +180,11 @@ export interface FoodRequest { requestId: number; pantryId: number; pantry: Pantry; - requestedSize: string; + requestedSize: RequestSize; requestedItems: string[]; additionalInformation: string | null; requestedAt: Date; - dateReceived: Date | null; - feedback: string | null; - photos: string[] | null; + status: FoodRequestStatus; orders?: Order[]; } @@ -200,6 +198,9 @@ export interface Order { createdAt: string; shippedAt: string | null; deliveredAt: string | null; + dateReceived?: string | null; + feedback?: string | null; + photos?: string[] | null; } export interface OrderItemDetails { @@ -223,14 +224,9 @@ export interface FoodManufacturer { export interface CreateFoodRequestBody { pantryId: number; - requestedSize: string; + requestedSize: RequestSize; requestedItems: string[]; - additionalInformation: string | null | undefined; - status: string; - fulfilledBy: number | null | undefined; - dateReceived: Date | null | undefined; - feedback: string | null | undefined; - photos: string[] | null | undefined; + additionalInformation?: string | null; } export interface CreateMultipleDonationItemsBody { @@ -275,6 +271,11 @@ export enum RequestSize { LARGE = 'Large (10+ boxes)', } +export enum FoodRequestStatus { + ACTIVE = 'active', + CLOSED = 'closed', +} + export enum DonationFrequency { YEARLY = 'yearly', BIWEEKLY = 'biweekly', From fda0ece3b4164a9b1951c0d6e5c6351fdcffdada Mon Sep 17 00:00:00 2001 From: Tarun-Nagesh Date: Thu, 19 Feb 2026 20:35:57 -0500 Subject: [PATCH 05/17] linting --- apps/backend/src/foodRequests/request.module.ts | 2 -- apps/backend/src/orders/order.module.ts | 5 ----- 2 files changed, 7 deletions(-) diff --git a/apps/backend/src/foodRequests/request.module.ts b/apps/backend/src/foodRequests/request.module.ts index 1d1d4ef38..2ef7e1d33 100644 --- a/apps/backend/src/foodRequests/request.module.ts +++ b/apps/backend/src/foodRequests/request.module.ts @@ -4,8 +4,6 @@ import { RequestsController } from './request.controller'; import { FoodRequest } from './request.entity'; import { RequestsService } from './request.service'; import { AuthModule } from '../auth/auth.module'; -import { JwtStrategy } from '../auth/jwt.strategy'; -import { AuthService } from '../auth/auth.service'; import { Order } from '../orders/order.entity'; import { Pantry } from '../pantries/pantries.entity'; diff --git a/apps/backend/src/orders/order.module.ts b/apps/backend/src/orders/order.module.ts index 8827e66a5..12fb40f39 100644 --- a/apps/backend/src/orders/order.module.ts +++ b/apps/backend/src/orders/order.module.ts @@ -3,8 +3,6 @@ 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'; @@ -14,12 +12,9 @@ import { MulterModule } from '@nestjs/platform-express'; @Module({ imports: [ - TypeOrmModule.forFeature([Order, Pantry, FoodRequest]), - AllocationModule, forwardRef(() => AuthModule), - , AWSS3Module, MulterModule.register({ dest: './uploads' }), ], From 4b16f2193e08dca493f4f73255e70d24a4966acf Mon Sep 17 00:00:00 2001 From: Tarun-Nagesh Date: Thu, 19 Feb 2026 20:41:09 -0500 Subject: [PATCH 06/17] fixed test --- apps/backend/src/orders/order.controller.spec.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/backend/src/orders/order.controller.spec.ts b/apps/backend/src/orders/order.controller.spec.ts index 241762119..29a7ae61d 100644 --- a/apps/backend/src/orders/order.controller.spec.ts +++ b/apps/backend/src/orders/order.controller.spec.ts @@ -163,7 +163,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, ); From e1dc7c05876783fb3fa54aa9e00423792dfac831 Mon Sep 17 00:00:00 2001 From: Tarun-Nagesh Date: Fri, 20 Feb 2026 18:21:54 -0500 Subject: [PATCH 07/17] Added status back to FoodRequest --- apps/frontend/src/types/types.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/apps/frontend/src/types/types.ts b/apps/frontend/src/types/types.ts index a0e284c20..ab29e97b1 100644 --- a/apps/frontend/src/types/types.ts +++ b/apps/frontend/src/types/types.ts @@ -194,6 +194,7 @@ export interface FoodRequest { requestedItems: string[]; additionalInformation: string | null; requestedAt: string; + status: FoodRequestStatus; orders?: Order[]; } From 66805a87d7b97fa1b534d625e414e233cb482892 Mon Sep 17 00:00:00 2001 From: Max Norman <27299284+maxn990@users.noreply.github.com> Date: Mon, 23 Feb 2026 13:25:47 -0500 Subject: [PATCH 08/17] resolve comments --- .../src/foodRequests/request.module.ts | 5 +- ...1770571145350-MoveRequestFieldsToOrders.ts | 54 ++++++++- .../src/orders/dtos/confirm-delivery.dto.ts | 2 +- .../src/orders/order.controller.spec.ts | 58 +++------ apps/backend/src/orders/order.controller.ts | 20 ++-- apps/backend/src/orders/order.service.spec.ts | 113 ++++++++++++------ apps/backend/src/orders/order.service.ts | 16 ++- 7 files changed, 165 insertions(+), 103 deletions(-) diff --git a/apps/backend/src/foodRequests/request.module.ts b/apps/backend/src/foodRequests/request.module.ts index 2ef7e1d33..df1bf80c0 100644 --- a/apps/backend/src/foodRequests/request.module.ts +++ b/apps/backend/src/foodRequests/request.module.ts @@ -8,10 +8,7 @@ import { Order } from '../orders/order.entity'; import { Pantry } from '../pantries/pantries.entity'; @Module({ - imports: [ - TypeOrmModule.forFeature([FoodRequest, Order, Pantry]), - AuthModule, - ], + imports: [TypeOrmModule.forFeature([FoodRequest, Order, Pantry]), AuthModule], controllers: [RequestsController], providers: [RequestsService], }) diff --git a/apps/backend/src/migrations/1770571145350-MoveRequestFieldsToOrders.ts b/apps/backend/src/migrations/1770571145350-MoveRequestFieldsToOrders.ts index 566edd093..e35aafd13 100644 --- a/apps/backend/src/migrations/1770571145350-MoveRequestFieldsToOrders.ts +++ b/apps/backend/src/migrations/1770571145350-MoveRequestFieldsToOrders.ts @@ -1,7 +1,8 @@ import { MigrationInterface, QueryRunner } from 'typeorm'; -export class MoveRequestFieldsToOrders1770571145350 implements MigrationInterface { - +export class MoveRequestFieldsToOrders1770571145350 + implements MigrationInterface +{ public async up(queryRunner: QueryRunner): Promise { await queryRunner.query(` CREATE TYPE food_requests_status_enum AS ENUM ('active', 'closed'); @@ -21,9 +22,58 @@ export class MoveRequestFieldsToOrders1770571145350 implements MigrationInterfac 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, diff --git a/apps/backend/src/orders/dtos/confirm-delivery.dto.ts b/apps/backend/src/orders/dtos/confirm-delivery.dto.ts index e54703c66..a34ea2576 100644 --- a/apps/backend/src/orders/dtos/confirm-delivery.dto.ts +++ b/apps/backend/src/orders/dtos/confirm-delivery.dto.ts @@ -1,5 +1,5 @@ export class ConfirmDeliveryDto { dateReceived: string; - feedback: string; + feedback?: string; photos?: Express.Multer.File[]; } diff --git a/apps/backend/src/orders/order.controller.spec.ts b/apps/backend/src/orders/order.controller.spec.ts index 29a7ae61d..46a75f910 100644 --- a/apps/backend/src/orders/order.controller.spec.ts +++ b/apps/backend/src/orders/order.controller.spec.ts @@ -12,6 +12,7 @@ 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(); @@ -294,11 +295,11 @@ describe('OrdersController', () => { it('should upload photos and confirm delivery with all fields', async () => { const orderId = 1; - const body = { + const body: ConfirmDeliveryDto = { dateReceived: new Date().toISOString(), feedback: 'Great delivery!', }; - const mockFiles = [ + const mockFiles: Express.Multer.File[] = [ { fieldname: 'photos', originalname: 'photo1.jpg', @@ -306,8 +307,8 @@ describe('OrdersController', () => { mimetype: 'image/jpeg', buffer: Buffer.from('photo1'), size: 1000, - }, - ] as Express.Multer.File[]; + } as Express.Multer.File, + ]; const uploadedUrls = ['https://s3.example.com/photo1.jpg']; mockAWSS3Service.upload.mockResolvedValueOnce(uploadedUrls); @@ -326,18 +327,16 @@ describe('OrdersController', () => { const result = await controller.confirmDelivery(orderId, body, mockFiles); expect(mockAWSS3Service.upload).toHaveBeenCalledWith(mockFiles); - expect(mockOrdersService.confirmDelivery).toHaveBeenCalledWith( - orderId, - new Date(body.dateReceived), - body.feedback, - uploadedUrls, - ); + expect(mockOrdersService.confirmDelivery).toHaveBeenCalledWith(orderId, { + ...body, + photos: uploadedUrls, + }); expect(result).toEqual(confirmedOrder); }); it('should handle no photos being uploaded', async () => { const orderId = 2; - const body = { + const body: ConfirmDeliveryDto = { dateReceived: new Date().toISOString(), feedback: 'Delivery without photos', }; @@ -356,18 +355,16 @@ describe('OrdersController', () => { const result = await controller.confirmDelivery(orderId, body); expect(mockAWSS3Service.upload).not.toHaveBeenCalled(); - expect(mockOrdersService.confirmDelivery).toHaveBeenCalledWith( - orderId, - new Date(body.dateReceived), - body.feedback, - [], - ); + expect(mockOrdersService.confirmDelivery).toHaveBeenCalledWith(orderId, { + ...body, + photos: [], + }); expect(result).toEqual(confirmedOrder); }); it('should handle empty photos array', async () => { const orderId = 3; - const body = { + const body: ConfirmDeliveryDto = { dateReceived: new Date().toISOString(), feedback: 'Empty photos', }; @@ -386,29 +383,12 @@ describe('OrdersController', () => { const result = await controller.confirmDelivery(orderId, body, []); expect(mockAWSS3Service.upload).not.toHaveBeenCalled(); - expect(mockOrdersService.confirmDelivery).toHaveBeenCalledWith( - orderId, - new Date(body.dateReceived), - body.feedback, - [], - ); + expect(mockOrdersService.confirmDelivery).toHaveBeenCalledWith(orderId, { + ...body, + photos: [], + }); expect(result).toEqual(confirmedOrder); }); - - it('should throw BadRequestException for invalid date format', async () => { - const orderId = 1; - const body = { - dateReceived: 'invalid-date', - feedback: 'test', - }; - - await expect(controller.confirmDelivery(orderId, body)).rejects.toThrow( - 'Invalid date format for dateReceived', - ); - - expect(mockAWSS3Service.upload).not.toHaveBeenCalled(); - expect(mockOrdersService.confirmDelivery).not.toHaveBeenCalled(); - }); }); describe('updateStatus', () => { diff --git a/apps/backend/src/orders/order.controller.ts b/apps/backend/src/orders/order.controller.ts index 04bac6a29..86351b7c7 100644 --- a/apps/backend/src/orders/order.controller.ts +++ b/apps/backend/src/orders/order.controller.ts @@ -154,21 +154,15 @@ export class OrdersController { @Body() body: ConfirmDeliveryDto, @UploadedFiles() photos?: Express.Multer.File[], ): Promise { - body.photos = photos; - - const formattedDate = new Date(body.dateReceived); - if (isNaN(formattedDate.getTime())) { - throw new BadRequestException('Invalid date format for dateReceived'); - } - const uploadedPhotoUrls = photos && photos.length > 0 ? await this.awsS3Service.upload(photos) : []; - return this.ordersService.confirmDelivery( - orderId, - formattedDate, - body.feedback, - uploadedPhotoUrls, - ); + body.photos = uploadedPhotoUrls as unknown as Express.Multer.File[]; + + return this.ordersService.confirmDelivery(orderId, { + dateReceived: body.dateReceived, + feedback: body.feedback, + photos: body.photos, + }); } } diff --git a/apps/backend/src/orders/order.service.spec.ts b/apps/backend/src/orders/order.service.spec.ts index 6c629d3a9..d82ef105e 100644 --- a/apps/backend/src/orders/order.service.spec.ts +++ b/apps/backend/src/orders/order.service.spec.ts @@ -134,7 +134,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) => @@ -403,8 +403,17 @@ describe('OrdersService', () => { }); describe('confirmDelivery', () => { + it('should throw BadRequestException for invalid date format', async () => { + await expect( + service.confirmDelivery(1, { + dateReceived: 'invalid-date', + feedback: 'test feedback', + photos: [], + }), + ).rejects.toThrow('Invalid date format for dateReceived'); + }); + it('should update order with delivery details and set status to delivered', async () => { - // Get an existing shipped order from dummy data const orderRepo = testDataSource.getRepository(Order); const requestRepo = testDataSource.getRepository(FoodRequest); @@ -415,59 +424,90 @@ describe('OrdersService', () => { expect(shippedOrder).toBeDefined(); - const dateReceived = new Date(); + const dateReceived = new Date().toISOString(); const feedback = 'Perfect delivery!'; const photos = ['photo1.jpg', 'photo2.jpg']; - const result = await service.confirmDelivery( - shippedOrder.orderId, + 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(dateReceived); + expect(result.dateReceived).toEqual(new Date(dateReceived)); expect(result.feedback).toBe(feedback); expect(result.photos).toEqual(photos); expect(result.deliveredAt).toBeNull(); - // Verify request status was updated const updatedRequest = await requestRepo.findOne({ where: { requestId: shippedOrder.requestId }, relations: ['orders'], }); - // Check if all orders for this request are delivered - const allDelivered = updatedRequest.orders.every( - (order) => order.status === OrderStatus.DELIVERED, + 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); + + const shippedOrder = await orderRepo.findOne({ + where: { + status: OrderStatus.SHIPPED, + shippedAt: new Date('2024-02-06T08:00:00'), + }, + relations: ['request', 'request.orders'], + }); + + expect(shippedOrder).toBeDefined(); + + const hasOtherUndeliveredOrders = shippedOrder.request.orders.some( + (o) => + o.orderId !== shippedOrder.orderId && + o.status !== OrderStatus.DELIVERED, ); + expect(hasOtherUndeliveredOrders).toBe(true); - if (allDelivered) { - expect(updatedRequest.status).toBe(FoodRequestStatus.CLOSED); - } else { - expect(updatedRequest.status).toBe(FoodRequestStatus.ACTIVE); - } + 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); + + const updatedRequest = await requestRepo.findOne({ + where: { requestId: shippedOrder.requestId }, + relations: ['orders'], + }); + + expect(updatedRequest.status).toBe(FoodRequestStatus.ACTIVE); }); it('should set request status to CLOSED when all orders are delivered', async () => { const orderRepo = testDataSource.getRepository(Order); const requestRepo = testDataSource.getRepository(FoodRequest); - // Find a request with only one order that's shipped const request = await requestRepo.findOne({ where: { status: FoodRequestStatus.ACTIVE }, relations: ['orders'], }); - // Find a shipped order for this request const shippedOrder = request.orders.find( (order) => order.status === OrderStatus.SHIPPED, ); if (shippedOrder) { - // Mark all other orders as delivered first for (const order of request.orders) { if (order.orderId !== shippedOrder.orderId) { order.status = OrderStatus.DELIVERED; @@ -475,15 +515,12 @@ describe('OrdersService', () => { } } - // Now confirm the last shipped order - await service.confirmDelivery( - shippedOrder.orderId, - new Date(), - 'Final delivery', - [], - ); + await service.confirmDelivery(shippedOrder.orderId, { + dateReceived: new Date().toISOString(), + feedback: 'Final delivery', + photos: [], + }); - // Verify request is now closed const updatedRequest = await requestRepo.findOne({ where: { requestId: request.requestId }, relations: ['orders'], @@ -502,7 +539,6 @@ describe('OrdersService', () => { const orderRepo = testDataSource.getRepository(Order); const requestRepo = testDataSource.getRepository(FoodRequest); - // Find a request with multiple orders const request = await requestRepo.findOne({ where: { status: FoodRequestStatus.ACTIVE }, relations: ['orders'], @@ -514,15 +550,12 @@ describe('OrdersService', () => { ); if (shippedOrder) { - // Confirm only one order, leaving others undelivered - await service.confirmDelivery( - shippedOrder.orderId, - new Date(), - 'Partial delivery', - [], - ); - - // Verify request is still active + await service.confirmDelivery(shippedOrder.orderId, { + dateReceived: new Date().toISOString(), + feedback: 'Partial delivery', + photos: [], + }); + const updatedRequest = await requestRepo.findOne({ where: { requestId: request.requestId }, relations: ['orders'], @@ -542,7 +575,11 @@ describe('OrdersService', () => { const invalidOrderId = 99999; await expect( - service.confirmDelivery(invalidOrderId, new Date(), 'test', []), + service.confirmDelivery(invalidOrderId, { + dateReceived: new Date().toISOString(), + feedback: 'test', + photos: [], + }), ).rejects.toThrow(`Order ${invalidOrderId} not found`); }); }); diff --git a/apps/backend/src/orders/order.service.ts b/apps/backend/src/orders/order.service.ts index 6b45bc20b..5f0ae5bde 100644 --- a/apps/backend/src/orders/order.service.ts +++ b/apps/backend/src/orders/order.service.ts @@ -13,6 +13,7 @@ import { FoodRequestStatus } from '../foodRequests/types'; import { validateId } from '../utils/validation.utils'; import { OrderStatus } from './types'; import { TrackingCostDto } from './dtos/tracking-cost.dto'; +import { ConfirmDeliveryDto } from './dtos/confirm-delivery.dto'; @Injectable() export class OrdersService { @@ -155,12 +156,15 @@ export class OrdersService { async confirmDelivery( orderId: number, - dateReceived: Date, - feedback: string, - photos: string[], + dto: ConfirmDeliveryDto, ): 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 }, }); @@ -169,9 +173,9 @@ export class OrdersService { throw new NotFoundException(`Order ${orderId} not found`); } - order.dateReceived = dateReceived; - order.feedback = feedback; - order.photos = photos; + order.dateReceived = formattedDate; + order.feedback = dto.feedback; + order.photos = dto.photos as unknown as string[]; order.status = OrderStatus.DELIVERED; const updatedOrder = await this.repo.save(order); From 9f2a664bf02a9eba4638901b159352eaabe3d13c Mon Sep 17 00:00:00 2001 From: Max Norman <27299284+maxn990@users.noreply.github.com> Date: Mon, 23 Feb 2026 13:32:51 -0500 Subject: [PATCH 09/17] fix test --- apps/backend/src/orders/order.service.spec.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/backend/src/orders/order.service.spec.ts b/apps/backend/src/orders/order.service.spec.ts index d82ef105e..3532d3748 100644 --- a/apps/backend/src/orders/order.service.spec.ts +++ b/apps/backend/src/orders/order.service.spec.ts @@ -431,7 +431,7 @@ describe('OrdersService', () => { const result = await service.confirmDelivery(shippedOrder.orderId, { dateReceived, feedback, - photos, + photos: [] as unknown as Express.Multer.File[], }); expect(result.orderId).toBe(shippedOrder.orderId); @@ -477,7 +477,7 @@ describe('OrdersService', () => { const result = await service.confirmDelivery(shippedOrder.orderId, { dateReceived, feedback, - photos, + photos: [] as unknown as Express.Multer.File[], }); expect(result.orderId).toBe(shippedOrder.orderId); From 699b0a5d94ae39b4e062b49cc38e1c20e69a95eb Mon Sep 17 00:00:00 2001 From: Max Norman <27299284+maxn990@users.noreply.github.com> Date: Mon, 23 Feb 2026 13:38:23 -0500 Subject: [PATCH 10/17] fix test --- package.json | 2 +- yarn.lock | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package.json b/package.json index d5f75f466..ae8e6fafc 100644 --- a/package.json +++ b/package.json @@ -74,7 +74,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/passport-jwt": "^4.0.1", "@types/react": "^18.2.14", diff --git a/yarn.lock b/yarn.lock index 295bbb82a..a0b22c8ba 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5854,10 +5854,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" "*" From 4d5bba1752d703bd66d608d1cbdb5a617dad212b Mon Sep 17 00:00:00 2001 From: Max Norman <27299284+maxn990@users.noreply.github.com> Date: Mon, 23 Feb 2026 13:45:10 -0500 Subject: [PATCH 11/17] fix tests --- apps/backend/src/orders/order.service.spec.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/apps/backend/src/orders/order.service.spec.ts b/apps/backend/src/orders/order.service.spec.ts index 3532d3748..d0ace0947 100644 --- a/apps/backend/src/orders/order.service.spec.ts +++ b/apps/backend/src/orders/order.service.spec.ts @@ -8,7 +8,7 @@ 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 { Repository } from 'typeorm'; +import 'multer'; import { FoodRequestStatus } from '../foodRequests/types'; // Set 1 minute timeout for async DB operations @@ -431,7 +431,7 @@ describe('OrdersService', () => { const result = await service.confirmDelivery(shippedOrder.orderId, { dateReceived, feedback, - photos: [] as unknown as Express.Multer.File[], + photos: photos as unknown as Express.Multer.File[], }); expect(result.orderId).toBe(shippedOrder.orderId); @@ -477,7 +477,7 @@ describe('OrdersService', () => { const result = await service.confirmDelivery(shippedOrder.orderId, { dateReceived, feedback, - photos: [] as unknown as Express.Multer.File[], + photos: photos as unknown as Express.Multer.File[], }); expect(result.orderId).toBe(shippedOrder.orderId); From 6ed004729eedfb085ed245adbc73c5a36a94df6e Mon Sep 17 00:00:00 2001 From: Max Norman <27299284+maxn990@users.noreply.github.com> Date: Mon, 23 Feb 2026 20:45:21 -0500 Subject: [PATCH 12/17] resolve comments --- apps/backend/src/aws/aws-s3.service.ts | 12 +-- .../src/foodRequests/request.service.ts | 31 +++++- ...1260403657-RenameDonationMatchingStatus.ts | 96 +++++++++---------- .../src/orders/dtos/confirm-delivery.dto.ts | 1 - .../src/orders/order.controller.spec.ts | 28 +++--- apps/backend/src/orders/order.controller.ts | 10 +- apps/backend/src/orders/order.entity.ts | 2 +- apps/backend/src/orders/order.service.spec.ts | 90 +++++++++++------ apps/backend/src/orders/order.service.ts | 44 +++------ 9 files changed, 168 insertions(+), 146 deletions(-) diff --git a/apps/backend/src/aws/aws-s3.service.ts b/apps/backend/src/aws/aws-s3.service.ts index c588a52f7..4121bc8f8 100644 --- a/apps/backend/src/aws/aws-s3.service.ts +++ b/apps/backend/src/aws/aws-s3.service.ts @@ -1,14 +1,6 @@ import { Injectable } from '@nestjs/common'; import { S3Client, PutObjectCommand } from '@aws-sdk/client-s3'; - -interface MulterFile { - fieldname: string; - originalname: string; - encoding: string; - mimetype: string; - size: number; - buffer: Buffer; -} +import 'multer'; @Injectable() export class AWSS3Service { @@ -31,7 +23,7 @@ export class AWSS3Service { }); } - async upload(files: MulterFile[]): Promise { + async upload(files: Express.Multer.File[]): Promise { const uploadedFileUrls: string[] = []; try { for (const file of files) { diff --git a/apps/backend/src/foodRequests/request.service.ts b/apps/backend/src/foodRequests/request.service.ts index c3ebcb729..5a0a5c4c1 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 { @@ -104,4 +105,32 @@ export class RequestsService { relations: ['orders'], }); } + + async updateRequestStatus(requestId: number): Promise { + validateId(requestId, 'Request'); + + const request = await this.repo.findOne({ + where: { requestId }, + relations: ['orders'], + }); + + if (!request) { + throw new NotFoundException(`Request ${requestId} not found`); + } + + const orders = request.orders || []; + if (!orders.length) { + throw new NotFoundException(`No orders found for request ${requestId}`); + } + + const allDelivered = orders.every( + (order) => order.status === OrderStatus.DELIVERED, + ); + + request.status = allDelivered + ? FoodRequestStatus.CLOSED + : FoodRequestStatus.ACTIVE; + + await this.repo.save(request); + } } 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 index a34ea2576..c34561eed 100644 --- a/apps/backend/src/orders/dtos/confirm-delivery.dto.ts +++ b/apps/backend/src/orders/dtos/confirm-delivery.dto.ts @@ -1,5 +1,4 @@ export class ConfirmDeliveryDto { dateReceived: string; feedback?: string; - photos?: Express.Multer.File[]; } diff --git a/apps/backend/src/orders/order.controller.spec.ts b/apps/backend/src/orders/order.controller.spec.ts index 46a75f910..5267ceede 100644 --- a/apps/backend/src/orders/order.controller.spec.ts +++ b/apps/backend/src/orders/order.controller.spec.ts @@ -286,7 +286,6 @@ describe('OrdersController', () => { ).toHaveBeenCalledWith(orderId); }); }); - describe('confirmDelivery', () => { beforeEach(() => { mockAWSS3Service.upload.mockReset(); @@ -327,10 +326,11 @@ describe('OrdersController', () => { const result = await controller.confirmDelivery(orderId, body, mockFiles); expect(mockAWSS3Service.upload).toHaveBeenCalledWith(mockFiles); - expect(mockOrdersService.confirmDelivery).toHaveBeenCalledWith(orderId, { - ...body, - photos: uploadedUrls, - }); + expect(mockOrdersService.confirmDelivery).toHaveBeenCalledWith( + orderId, + body, + uploadedUrls, + ); expect(result).toEqual(confirmedOrder); }); @@ -355,10 +355,11 @@ describe('OrdersController', () => { const result = await controller.confirmDelivery(orderId, body); expect(mockAWSS3Service.upload).not.toHaveBeenCalled(); - expect(mockOrdersService.confirmDelivery).toHaveBeenCalledWith(orderId, { - ...body, - photos: [], - }); + expect(mockOrdersService.confirmDelivery).toHaveBeenCalledWith( + orderId, + body, + [], + ); expect(result).toEqual(confirmedOrder); }); @@ -383,10 +384,11 @@ describe('OrdersController', () => { const result = await controller.confirmDelivery(orderId, body, []); expect(mockAWSS3Service.upload).not.toHaveBeenCalled(); - expect(mockOrdersService.confirmDelivery).toHaveBeenCalledWith(orderId, { - ...body, - photos: [], - }); + expect(mockOrdersService.confirmDelivery).toHaveBeenCalledWith( + orderId, + body, + [], + ); expect(result).toEqual(confirmedOrder); }); }); diff --git a/apps/backend/src/orders/order.controller.ts b/apps/backend/src/orders/order.controller.ts index 86351b7c7..6249b11da 100644 --- a/apps/backend/src/orders/order.controller.ts +++ b/apps/backend/src/orders/order.controller.ts @@ -120,7 +120,7 @@ export class OrdersController { return this.ordersService.updateTrackingCostInfo(orderId, dto); } - @Post('/:orderId/confirm-delivery') + @Patch('/:orderId/confirm-delivery') @ApiBody({ description: 'Details for a confirmation form', schema: { @@ -157,12 +157,6 @@ export class OrdersController { const uploadedPhotoUrls = photos && photos.length > 0 ? await this.awsS3Service.upload(photos) : []; - body.photos = uploadedPhotoUrls as unknown as Express.Multer.File[]; - - return this.ordersService.confirmDelivery(orderId, { - dateReceived: body.dateReceived, - feedback: body.feedback, - photos: body.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 88d01c400..f77c31368 100644 --- a/apps/backend/src/orders/order.entity.ts +++ b/apps/backend/src/orders/order.entity.ts @@ -74,7 +74,7 @@ export class Order { feedback?: string | null; @Column({ name: 'photos', type: 'text', array: true, nullable: true }) - photos?: string[] | null; + photos?: string[]; @OneToMany(() => Allocation, (allocation) => allocation.order) allocations!: Allocation[]; diff --git a/apps/backend/src/orders/order.service.spec.ts b/apps/backend/src/orders/order.service.spec.ts index dc2607a5f..5fc822805 100644 --- a/apps/backend/src/orders/order.service.spec.ts +++ b/apps/backend/src/orders/order.service.spec.ts @@ -10,6 +10,7 @@ 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); @@ -30,6 +31,7 @@ describe('OrdersService', () => { const module: TestingModule = await Test.createTestingModule({ providers: [ OrdersService, + RequestsService, { provide: getRepositoryToken(Order), useValue: testDataSource.getRepository(Order), @@ -405,15 +407,15 @@ describe('OrdersService', () => { describe('confirmDelivery', () => { it('should throw BadRequestException for invalid date format', async () => { await expect( - service.confirmDelivery(1, { - dateReceived: 'invalid-date', - feedback: 'test feedback', - photos: [], - }), + service.confirmDelivery( + 1, + { dateReceived: 'invalid-date', feedback: 'test feedback' }, + [], + ), ).rejects.toThrow('Invalid date format for dateReceived'); }); - it('should update order with delivery details and set status to delivered', async () => { + 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); @@ -428,11 +430,11 @@ describe('OrdersService', () => { const feedback = 'Perfect delivery!'; const photos = ['photo1.jpg', 'photo2.jpg']; - const result = await service.confirmDelivery(shippedOrder.orderId, { - dateReceived, - feedback, - photos: photos as unknown as Express.Multer.File[], - }); + const result = await service.confirmDelivery( + shippedOrder.orderId, + { dateReceived, feedback }, + photos, + ); expect(result.orderId).toBe(shippedOrder.orderId); expect(result.status).toBe(OrderStatus.DELIVERED); @@ -474,11 +476,11 @@ describe('OrdersService', () => { const feedback = 'Perfect delivery!'; const photos = ['photo1.jpg', 'photo2.jpg']; - const result = await service.confirmDelivery(shippedOrder.orderId, { - dateReceived, - feedback, - photos: photos as unknown as Express.Multer.File[], - }); + const result = await service.confirmDelivery( + shippedOrder.orderId, + { dateReceived, feedback }, + photos, + ); expect(result.orderId).toBe(shippedOrder.orderId); expect(result.status).toBe(OrderStatus.DELIVERED); @@ -515,11 +517,14 @@ describe('OrdersService', () => { } } - await service.confirmDelivery(shippedOrder.orderId, { - dateReceived: new Date().toISOString(), - feedback: 'Final delivery', - photos: [], - }); + await service.confirmDelivery( + shippedOrder.orderId, + { + dateReceived: new Date().toISOString(), + feedback: 'Final delivery', + }, + [], + ); const updatedRequest = await requestRepo.findOne({ where: { requestId: request.requestId }, @@ -550,11 +555,14 @@ describe('OrdersService', () => { ); if (shippedOrder) { - await service.confirmDelivery(shippedOrder.orderId, { - dateReceived: new Date().toISOString(), - feedback: 'Partial delivery', - photos: [], - }); + await service.confirmDelivery( + shippedOrder.orderId, + { + dateReceived: new Date().toISOString(), + feedback: 'Partial delivery', + }, + [], + ); const updatedRequest = await requestRepo.findOne({ where: { requestId: request.requestId }, @@ -575,12 +583,32 @@ describe('OrdersService', () => { const invalidOrderId = 99999; await expect( - service.confirmDelivery(invalidOrderId, { - dateReceived: new Date().toISOString(), - feedback: 'test', - photos: [], - }), + service.confirmDelivery( + invalidOrderId, + { dateReceived: new Date().toISOString(), feedback: 'test' }, + [], + ), ).rejects.toThrow(`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'), + ); + }); }); }); diff --git a/apps/backend/src/orders/order.service.ts b/apps/backend/src/orders/order.service.ts index 5f0ae5bde..8b62a51af 100644 --- a/apps/backend/src/orders/order.service.ts +++ b/apps/backend/src/orders/order.service.ts @@ -9,19 +9,18 @@ import { Order } from './order.entity'; import { Pantry } from '../pantries/pantries.entity'; import { FoodManufacturer } from '../foodManufacturers/manufacturers.entity'; import { FoodRequest } from '../foodRequests/request.entity'; -import { FoodRequestStatus } from '../foodRequests/types'; 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, - @InjectRepository(FoodRequest) - private requestRepo: Repository, + private requestsService: RequestsService, ) {} // TODO: when order is created, set FM @@ -157,6 +156,7 @@ export class OrdersService { async confirmDelivery( orderId: number, dto: ConfirmDeliveryDto, + photos: string[], ): Promise { validateId(orderId, 'Order'); @@ -173,46 +173,24 @@ export class OrdersService { 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 = dto.photos as unknown as string[]; + order.photos = photos; order.status = OrderStatus.DELIVERED; const updatedOrder = await this.repo.save(order); - await this.updateRequestStatus(order.requestId); + await this.requestsService.updateRequestStatus(order.requestId); return updatedOrder; } - private async updateRequestStatus(requestId: number): Promise { - validateId(requestId, 'Request'); - - const request = await this.requestRepo.findOne({ - where: { requestId }, - relations: ['orders'], - }); - - if (!request) { - throw new NotFoundException(`Request ${requestId} not found`); - } - - const orders = request.orders || []; - if (!orders.length) { - throw new NotFoundException(`No orders found for request ${requestId}`); - } - - const allDelivered = orders.every( - (order) => order.status === OrderStatus.DELIVERED, - ); - - request.status = allDelivered - ? FoodRequestStatus.CLOSED - : FoodRequestStatus.ACTIVE; - - await this.requestRepo.save(request); - } - async getOrdersByPantry(pantryId: number): Promise { validateId(pantryId, 'Pantry'); From b332d1018fde9eeef9a03a095b117f167204168b Mon Sep 17 00:00:00 2001 From: Max Norman <27299284+maxn990@users.noreply.github.com> Date: Tue, 24 Feb 2026 00:37:24 -0500 Subject: [PATCH 13/17] fix modules --- apps/backend/src/foodRequests/request.module.ts | 1 + apps/backend/src/orders/order.module.ts | 2 ++ 2 files changed, 3 insertions(+) diff --git a/apps/backend/src/foodRequests/request.module.ts b/apps/backend/src/foodRequests/request.module.ts index df1bf80c0..9fdf03554 100644 --- a/apps/backend/src/foodRequests/request.module.ts +++ b/apps/backend/src/foodRequests/request.module.ts @@ -11,5 +11,6 @@ import { Pantry } from '../pantries/pantries.entity'; imports: [TypeOrmModule.forFeature([FoodRequest, Order, Pantry]), AuthModule], controllers: [RequestsController], providers: [RequestsService], + exports: [RequestsService], }) export class RequestsModule {} diff --git a/apps/backend/src/orders/order.module.ts b/apps/backend/src/orders/order.module.ts index 12fb40f39..c312b0d18 100644 --- a/apps/backend/src/orders/order.module.ts +++ b/apps/backend/src/orders/order.module.ts @@ -9,6 +9,7 @@ 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: [ @@ -17,6 +18,7 @@ import { MulterModule } from '@nestjs/platform-express'; forwardRef(() => AuthModule), AWSS3Module, MulterModule.register({ dest: './uploads' }), + forwardRef(() => RequestsModule), ], controllers: [OrdersController], providers: [OrdersService], From 19af4cb35473ac0afcf7b3e3a96efa7d1db6305a Mon Sep 17 00:00:00 2001 From: Max Norman <27299284+maxn990@users.noreply.github.com> Date: Tue, 24 Feb 2026 00:48:31 -0500 Subject: [PATCH 14/17] fix order entity --- apps/backend/src/orders/order.entity.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/backend/src/orders/order.entity.ts b/apps/backend/src/orders/order.entity.ts index f77c31368..ca9fdccd9 100644 --- a/apps/backend/src/orders/order.entity.ts +++ b/apps/backend/src/orders/order.entity.ts @@ -68,10 +68,10 @@ export class Order { deliveredAt?: Date; @Column({ name: 'date_received', type: 'timestamp', nullable: true }) - dateReceived?: Date | null; + dateReceived?: Date; @Column({ name: 'feedback', type: 'text', nullable: true }) - feedback?: string | null; + feedback?: string; @Column({ name: 'photos', type: 'text', array: true, nullable: true }) photos?: string[]; From 0f58f883ed0185238b7795c5b4407a0da20a024f Mon Sep 17 00:00:00 2001 From: Max Norman <27299284+maxn990@users.noreply.github.com> Date: Tue, 24 Feb 2026 17:28:39 -0500 Subject: [PATCH 15/17] resolve comments --- apps/backend/src/aws/aws-s3.service.ts | 1 - .../src/orders/dtos/confirm-delivery.dto.ts | 8 ++- apps/backend/src/orders/order.controller.ts | 1 - apps/backend/src/orders/order.entity.ts | 14 +++--- apps/backend/src/orders/order.service.spec.ts | 49 +++---------------- 5 files changed, 21 insertions(+), 52 deletions(-) diff --git a/apps/backend/src/aws/aws-s3.service.ts b/apps/backend/src/aws/aws-s3.service.ts index 4121bc8f8..045038d52 100644 --- a/apps/backend/src/aws/aws-s3.service.ts +++ b/apps/backend/src/aws/aws-s3.service.ts @@ -1,6 +1,5 @@ import { Injectable } from '@nestjs/common'; import { S3Client, PutObjectCommand } from '@aws-sdk/client-s3'; -import 'multer'; @Injectable() export class AWSS3Service { diff --git a/apps/backend/src/orders/dtos/confirm-delivery.dto.ts b/apps/backend/src/orders/dtos/confirm-delivery.dto.ts index c34561eed..cb8bffe6d 100644 --- a/apps/backend/src/orders/dtos/confirm-delivery.dto.ts +++ b/apps/backend/src/orders/dtos/confirm-delivery.dto.ts @@ -1,4 +1,10 @@ +import { IsDateString, IsOptional, IsString } from 'class-validator'; + export class ConfirmDeliveryDto { - dateReceived: string; + @IsDateString() + dateReceived!: string; + + @IsOptional() + @IsString() feedback?: string; } diff --git a/apps/backend/src/orders/order.controller.ts b/apps/backend/src/orders/order.controller.ts index 6249b11da..6c1f0d30c 100644 --- a/apps/backend/src/orders/order.controller.ts +++ b/apps/backend/src/orders/order.controller.ts @@ -129,7 +129,6 @@ export class OrdersController { dateReceived: { type: 'string', format: 'date-time', - nullable: true, example: new Date().toISOString(), }, feedback: { diff --git a/apps/backend/src/orders/order.entity.ts b/apps/backend/src/orders/order.entity.ts index ca9fdccd9..6a7636bec 100644 --- a/apps/backend/src/orders/order.entity.ts +++ b/apps/backend/src/orders/order.entity.ts @@ -58,23 +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; + dateReceived!: Date | null; @Column({ name: 'feedback', type: 'text', nullable: true }) - feedback?: string; + feedback!: string | null; @Column({ name: 'photos', type: 'text', array: true, nullable: true }) - photos?: string[]; + photos!: string[] | null; @OneToMany(() => Allocation, (allocation) => allocation.order) allocations!: Allocation[]; @@ -85,7 +85,7 @@ export class Order { length: 255, nullable: true, }) - trackingLink?: string; + trackingLink!: string | null; @Column({ name: 'shipping_cost', @@ -94,5 +94,5 @@ export class Order { scale: 2, nullable: true, }) - shippingCost?: number; + shippingCost!: number | null; } diff --git a/apps/backend/src/orders/order.service.spec.ts b/apps/backend/src/orders/order.service.spec.ts index 5fc822805..e190e312d 100644 --- a/apps/backend/src/orders/order.service.spec.ts +++ b/apps/backend/src/orders/order.service.spec.ts @@ -412,7 +412,9 @@ describe('OrdersService', () => { { dateReceived: 'invalid-date', feedback: 'test feedback' }, [], ), - ).rejects.toThrow('Invalid date format for dateReceived'); + ).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 () => { @@ -420,7 +422,7 @@ describe('OrdersService', () => { const requestRepo = testDataSource.getRepository(FoodRequest); const shippedOrder = await orderRepo.findOne({ - where: { status: OrderStatus.SHIPPED }, + where: { status: OrderStatus.SHIPPED, orderId: 3 }, relations: ['request'], }); @@ -540,45 +542,6 @@ describe('OrdersService', () => { } }); - it('should set request status to ACTIVE when not all orders are delivered', async () => { - const orderRepo = testDataSource.getRepository(Order); - const requestRepo = testDataSource.getRepository(FoodRequest); - - const request = await requestRepo.findOne({ - where: { status: FoodRequestStatus.ACTIVE }, - relations: ['orders'], - }); - - if (request && request.orders.length > 1) { - const shippedOrder = request.orders.find( - (order) => order.status === OrderStatus.SHIPPED, - ); - - if (shippedOrder) { - await service.confirmDelivery( - shippedOrder.orderId, - { - dateReceived: new Date().toISOString(), - feedback: 'Partial delivery', - }, - [], - ); - - const updatedRequest = await requestRepo.findOne({ - where: { requestId: request.requestId }, - relations: ['orders'], - }); - - expect( - updatedRequest.orders.some( - (o) => o.status !== OrderStatus.DELIVERED, - ), - ).toBe(true); - expect(updatedRequest.status).toBe(FoodRequestStatus.ACTIVE); - } - } - }); - it('should throw NotFoundException for invalid order id', async () => { const invalidOrderId = 99999; @@ -588,7 +551,9 @@ describe('OrdersService', () => { { dateReceived: new Date().toISOString(), feedback: 'test' }, [], ), - ).rejects.toThrow(`Order ${invalidOrderId} not found`); + ).rejects.toThrow( + new NotFoundException(`Order ${invalidOrderId} not found`), + ); }); it('should throw BadRequestException when order is not shipped', async () => { From 48351e79fdcfd8dec60438b2468146f701c07f13 Mon Sep 17 00:00:00 2001 From: Max Norman <27299284+maxn990@users.noreply.github.com> Date: Tue, 24 Feb 2026 20:09:19 -0500 Subject: [PATCH 16/17] resolve comments --- .../src/orders/dtos/confirm-delivery.dto.ts | 8 ++- apps/backend/src/orders/order.service.spec.ts | 64 ++++++------------- 2 files changed, 27 insertions(+), 45 deletions(-) diff --git a/apps/backend/src/orders/dtos/confirm-delivery.dto.ts b/apps/backend/src/orders/dtos/confirm-delivery.dto.ts index cb8bffe6d..3d2c48fac 100644 --- a/apps/backend/src/orders/dtos/confirm-delivery.dto.ts +++ b/apps/backend/src/orders/dtos/confirm-delivery.dto.ts @@ -1,4 +1,9 @@ -import { IsDateString, IsOptional, IsString } from 'class-validator'; +import { + IsDateString, + IsNotEmpty, + IsOptional, + IsString, +} from 'class-validator'; export class ConfirmDeliveryDto { @IsDateString() @@ -6,5 +11,6 @@ export class ConfirmDeliveryDto { @IsOptional() @IsString() + @IsNotEmpty() feedback?: string; } diff --git a/apps/backend/src/orders/order.service.spec.ts b/apps/backend/src/orders/order.service.spec.ts index e190e312d..f0c441424 100644 --- a/apps/backend/src/orders/order.service.spec.ts +++ b/apps/backend/src/orders/order.service.spec.ts @@ -498,50 +498,6 @@ describe('OrdersService', () => { expect(updatedRequest.status).toBe(FoodRequestStatus.ACTIVE); }); - it('should set request status to CLOSED when all orders are delivered', async () => { - const orderRepo = testDataSource.getRepository(Order); - const requestRepo = testDataSource.getRepository(FoodRequest); - - const request = await requestRepo.findOne({ - where: { status: FoodRequestStatus.ACTIVE }, - relations: ['orders'], - }); - - const shippedOrder = request.orders.find( - (order) => order.status === OrderStatus.SHIPPED, - ); - - if (shippedOrder) { - for (const order of request.orders) { - if (order.orderId !== shippedOrder.orderId) { - order.status = OrderStatus.DELIVERED; - await orderRepo.save(order); - } - } - - await service.confirmDelivery( - shippedOrder.orderId, - { - dateReceived: new Date().toISOString(), - feedback: 'Final delivery', - }, - [], - ); - - const updatedRequest = await requestRepo.findOne({ - where: { requestId: request.requestId }, - relations: ['orders'], - }); - - expect( - updatedRequest.orders.every( - (o) => o.status === OrderStatus.DELIVERED, - ), - ).toBe(true); - expect(updatedRequest.status).toBe(FoodRequestStatus.CLOSED); - } - }); - it('should throw NotFoundException for invalid order id', async () => { const invalidOrderId = 99999; @@ -575,5 +531,25 @@ describe('OrdersService', () => { 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'), + ); + }); }); }); From ce69a9011e6637f0b225a4b9aed9268413ce9c81 Mon Sep 17 00:00:00 2001 From: Max Norman <27299284+maxn990@users.noreply.github.com> Date: Tue, 24 Feb 2026 20:51:22 -0500 Subject: [PATCH 17/17] resolve comments --- .../src/foodRequests/request.service.ts | 5 ++- apps/backend/src/orders/order.controller.ts | 7 ++-- apps/backend/src/orders/order.service.spec.ts | 32 +++++++++---------- 3 files changed, 25 insertions(+), 19 deletions(-) diff --git a/apps/backend/src/foodRequests/request.service.ts b/apps/backend/src/foodRequests/request.service.ts index 5a0a5c4c1..0deee0108 100644 --- a/apps/backend/src/foodRequests/request.service.ts +++ b/apps/backend/src/foodRequests/request.service.ts @@ -119,8 +119,11 @@ export class RequestsService { } const orders = request.orders || []; + if (!orders.length) { - throw new NotFoundException(`No orders found for request ${requestId}`); + request.status = FoodRequestStatus.ACTIVE; + await this.repo.save(request); + return; } const allDelivered = orders.every( diff --git a/apps/backend/src/orders/order.controller.ts b/apps/backend/src/orders/order.controller.ts index 6c1f0d30c..e075e9857 100644 --- a/apps/backend/src/orders/order.controller.ts +++ b/apps/backend/src/orders/order.controller.ts @@ -122,7 +122,7 @@ export class OrdersController { @Patch('/:orderId/confirm-delivery') @ApiBody({ - description: 'Details for a confirmation form', + description: 'Details for a confirmation of order delivery form', schema: { type: 'object', properties: { @@ -140,7 +140,10 @@ export class OrdersController { type: 'array', items: { type: 'string' }, nullable: true, - example: [], + example: [ + 'https://s3.amazonaws.com/bucket/photo1.jpg', + 'https://s3.amazonaws.com/bucket/photo2.jpg', + ], }, }, }, diff --git a/apps/backend/src/orders/order.service.spec.ts b/apps/backend/src/orders/order.service.spec.ts index f0c441424..a8fa1e9c7 100644 --- a/apps/backend/src/orders/order.service.spec.ts +++ b/apps/backend/src/orders/order.service.spec.ts @@ -457,41 +457,41 @@ describe('OrdersService', () => { const orderRepo = testDataSource.getRepository(Order); const requestRepo = testDataSource.getRepository(FoodRequest); - const shippedOrder = await orderRepo.findOne({ - where: { - status: OrderStatus.SHIPPED, - shippedAt: new Date('2024-02-06T08:00:00'), - }, - relations: ['request', 'request.orders'], + // Get an existing shipped order + const existingShippedOrder = await orderRepo.findOne({ + where: { status: OrderStatus.SHIPPED }, + relations: ['request'], }); - expect(shippedOrder).toBeDefined(); + expect(existingShippedOrder).toBeDefined(); - const hasOtherUndeliveredOrders = shippedOrder.request.orders.some( - (o) => - o.orderId !== shippedOrder.orderId && - o.status !== OrderStatus.DELIVERED, - ); - expect(hasOtherUndeliveredOrders).toBe(true); + // 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( - shippedOrder.orderId, + existingShippedOrder.orderId, { dateReceived, feedback }, photos, ); - expect(result.orderId).toBe(shippedOrder.orderId); + 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: shippedOrder.requestId }, + where: { requestId: existingShippedOrder.requestId }, relations: ['orders'], });