From a3ec50ae8e2cb582d47c2887cbb2276d6b9ab1e8 Mon Sep 17 00:00:00 2001 From: Tarun-Nagesh Date: Sun, 8 Feb 2026 12:43:51 -0500 Subject: [PATCH 1/3] 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 2/3] 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 3/3] 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) {