From 6a7e711fc4bceff6b4b4f5784fcb2d84f1451ae4 Mon Sep 17 00:00:00 2001 From: Justin Wang Date: Sun, 8 Mar 2026 20:07:38 -0400 Subject: [PATCH 01/15] logic for creating order itself --- .../src/donationItems/donationItems.module.ts | 8 ++- .../donationItems/donationItems.service.ts | 14 +++- .../foodManufacturers/manufacturers.module.ts | 1 + .../src/orders/dtos/create-order.dto.ts | 12 ++++ apps/backend/src/orders/order.controller.ts | 33 ++++++++++ apps/backend/src/orders/order.module.ts | 14 +++- apps/backend/src/orders/order.service.ts | 66 +++++++++++++++++++ 7 files changed, 144 insertions(+), 4 deletions(-) create mode 100644 apps/backend/src/orders/dtos/create-order.dto.ts diff --git a/apps/backend/src/donationItems/donationItems.module.ts b/apps/backend/src/donationItems/donationItems.module.ts index ef377d2ba..657cf6539 100644 --- a/apps/backend/src/donationItems/donationItems.module.ts +++ b/apps/backend/src/donationItems/donationItems.module.ts @@ -1,4 +1,4 @@ -import { Module } from '@nestjs/common'; +import { forwardRef, Module } from '@nestjs/common'; import { TypeOrmModule } from '@nestjs/typeorm'; import { DonationItemsService } from './donationItems.service'; import { DonationItem } from './donationItems.entity'; @@ -7,8 +7,12 @@ import { AuthModule } from '../auth/auth.module'; import { Donation } from '../donations/donations.entity'; @Module({ - imports: [TypeOrmModule.forFeature([DonationItem, Donation]), AuthModule], + imports: [ + TypeOrmModule.forFeature([DonationItem, Donation]), + forwardRef(() => AuthModule), + ], controllers: [DonationItemsController], providers: [DonationItemsService], + exports: [DonationItemsService], }) export class DonationItemsModule {} diff --git a/apps/backend/src/donationItems/donationItems.service.ts b/apps/backend/src/donationItems/donationItems.service.ts index f95e5fdb4..b9ffd0424 100644 --- a/apps/backend/src/donationItems/donationItems.service.ts +++ b/apps/backend/src/donationItems/donationItems.service.ts @@ -1,6 +1,6 @@ import { Injectable, NotFoundException } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; -import { Repository } from 'typeorm'; +import { In, Repository } from 'typeorm'; import { DonationItem } from './donationItems.entity'; import { validateId } from '../utils/validation.utils'; import { FoodType } from './types'; @@ -18,6 +18,18 @@ export class DonationItemsService { return this.repo.find({ where: { donation: { donationId } } }); } + async getDonationItemsByDonationIds( + donationIds: number[], + ): Promise { + return this.repo.find({ + where: { + donation: { + donationId: In(donationIds), + }, + }, + }); + } + async create( donationId: number, itemName: string, diff --git a/apps/backend/src/foodManufacturers/manufacturers.module.ts b/apps/backend/src/foodManufacturers/manufacturers.module.ts index e80b08f5e..83df4ce7d 100644 --- a/apps/backend/src/foodManufacturers/manufacturers.module.ts +++ b/apps/backend/src/foodManufacturers/manufacturers.module.ts @@ -13,5 +13,6 @@ import { Donation } from '../donations/donations.entity'; ], controllers: [FoodManufacturersController], providers: [FoodManufacturersService], + exports: [FoodManufacturersService], }) export class ManufacturerModule {} diff --git a/apps/backend/src/orders/dtos/create-order.dto.ts b/apps/backend/src/orders/dtos/create-order.dto.ts new file mode 100644 index 000000000..a38217cff --- /dev/null +++ b/apps/backend/src/orders/dtos/create-order.dto.ts @@ -0,0 +1,12 @@ +import { IsNumber, IsObject } from 'class-validator'; + +export class CreateOrderDto { + @IsNumber() + foodRequestId!: number; + + @IsNumber() + manufacturerId!: number; + + @IsObject() + donationItems!: Record; +} diff --git a/apps/backend/src/orders/order.controller.ts b/apps/backend/src/orders/order.controller.ts index 5103aa7cc..9b939c2e4 100644 --- a/apps/backend/src/orders/order.controller.ts +++ b/apps/backend/src/orders/order.controller.ts @@ -10,6 +10,7 @@ import { ValidationPipe, UploadedFiles, UseInterceptors, + Post, } from '@nestjs/common'; import { ApiBody } from '@nestjs/swagger'; import { OrdersService } from './order.service'; @@ -25,6 +26,7 @@ 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'; +import { CreateOrderDto } from './dtos/create-order.dto'; @Controller('orders') export class OrdersController { @@ -86,6 +88,37 @@ export class OrdersController { return this.ordersService.findOrderDetails(orderId); } + @Post('/create') + @ApiBody({ + description: 'Details for creating a order', + schema: { + type: 'object', + properties: { + requestId: { type: 'integer', example: 1 }, + manufacturerId: { type: 'integer', example: 1 }, + donationItems: { + type: 'object', + description: 'Map of donationItemId -> quantity', + additionalProperties: { + type: 'integer', + example: 10, + }, + example: { + '5': 10, + '8': 3, + '12': 7, + }, + }, + }, + }, + }) + async createOrder( + @Body(new ValidationPipe()) + orderData: CreateOrderDto, + ): Promise { + return this.ordersService.create(orderData); + } + @Get('/order/:requestId') async getOrderByRequestId( @Param('requestId', ParseIntPipe) requestId: number, diff --git a/apps/backend/src/orders/order.module.ts b/apps/backend/src/orders/order.module.ts index c312b0d18..9445b6dc2 100644 --- a/apps/backend/src/orders/order.module.ts +++ b/apps/backend/src/orders/order.module.ts @@ -10,15 +10,27 @@ 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'; +import { FoodManufacturer } from '../foodManufacturers/manufacturers.entity'; +import { DonationItem } from '../donationItems/donationItems.entity'; +import { ManufacturerModule } from '../foodManufacturers/manufacturers.module'; +import { DonationItemsModule } from '../donationItems/donationItems.module'; @Module({ imports: [ - TypeOrmModule.forFeature([Order, Pantry, FoodRequest]), + TypeOrmModule.forFeature([ + Order, + Pantry, + FoodRequest, + FoodManufacturer, + DonationItem, + ]), AllocationModule, forwardRef(() => AuthModule), AWSS3Module, MulterModule.register({ dest: './uploads' }), forwardRef(() => RequestsModule), + ManufacturerModule, + DonationItemsModule, ], controllers: [OrdersController], providers: [OrdersService], diff --git a/apps/backend/src/orders/order.service.ts b/apps/backend/src/orders/order.service.ts index 601b7ba42..2c46c81e0 100644 --- a/apps/backend/src/orders/order.service.ts +++ b/apps/backend/src/orders/order.service.ts @@ -15,6 +15,10 @@ import { OrderDetailsDto } from './dtos/order-details.dto'; import { FoodRequestSummaryDto } from '../foodRequests/dtos/food-request-summary.dto'; import { ConfirmDeliveryDto } from './dtos/confirm-delivery.dto'; import { RequestsService } from '../foodRequests/request.service'; +import { CreateOrderDto } from './dtos/create-order.dto'; +import { FoodRequestStatus } from '../foodRequests/types'; +import { FoodManufacturersService } from '../foodManufacturers/manufacturers.service'; +import { DonationItemsService } from '../donationItems/donationItems.service'; @Injectable() export class OrdersService { @@ -22,6 +26,8 @@ export class OrdersService { @InjectRepository(Order) private repo: Repository, @InjectRepository(Pantry) private pantryRepo: Repository, private requestsService: RequestsService, + private manufacturerService: FoodManufacturersService, + private donationItemsService: DonationItemsService, ) {} // TODO: when order is created, set FM @@ -70,6 +76,66 @@ export class OrdersService { }); } + async create(orderData: CreateOrderDto): Promise { + const requestId = orderData.foodRequestId; + const manufacturerId = orderData.manufacturerId; + + validateId(manufacturerId, 'Food Manufacturer'); + validateId(requestId, 'Food Request'); + + const request = await this.requestsService.findOne(requestId); + + if (!request) { + throw new NotFoundException(`Request ${requestId} not found`); + } + + if (request.status != FoodRequestStatus.ACTIVE) { + throw new BadRequestException(`Request ${requestId} is not active`); + } + + // Ensure all donation items belong to specified manufacturer + + const donations = this.manufacturerService.getFMDonations(manufacturerId); + const donationIds = (await donations).map((d) => d.donationId); + + const donationItems = + await this.donationItemsService.getDonationItemsByDonationIds( + donationIds, + ); + const validDonationItemIds = new Set(donationItems.map((d) => d.itemId)); + + for (const [itemId, quantity] of Object.entries(orderData.donationItems)) { + const id = Number(itemId); + const count = Number(quantity); + + if (!validDonationItemIds.has(id)) { + throw new BadRequestException( + `Donation item ${id} does not belong to this manufacturer`, + ); + } + + const donationItem = donationItems.find((d) => d.itemId === id); + + if (!donationItem) { + throw new NotFoundException(`Couldn't find donation item ${id} `); + } + + if (count > donationItem.quantity - donationItem.reservedQuantity) { + throw new BadRequestException( + `Donation item ${id} allocated quantity exceeds remaining quantity`, + ); + } + } + + const order = this.repo.create({ + requestId: requestId, + foodManufacturerId: manufacturerId, + status: OrderStatus.PENDING, + }); + + return this.repo.save(order); + } + async findOne(orderId: number): Promise { validateId(orderId, 'Order'); From 511b62fc3d74459b50b873a256bb79b2083b3571 Mon Sep 17 00:00:00 2001 From: Justin Wang Date: Tue, 10 Mar 2026 11:10:00 -0400 Subject: [PATCH 02/15] functionality for creating allocations and updating donation item reserved quantities --- .../src/allocations/allocations.controller.ts | 37 +++++++++++++++++- .../src/allocations/allocations.service.ts | 29 ++++++++++++++ .../dtos/create-allocations.dto.ts | 9 +++++ .../donationItems/donationItems.controller.ts | 7 ++++ .../donationItems/donationItems.service.ts | 12 ++++++ apps/backend/src/orders/order.module.ts | 3 ++ apps/backend/src/orders/order.service.spec.ts | 38 +++++++++++++++++++ apps/backend/src/orders/order.service.ts | 15 +++++++- 8 files changed, 148 insertions(+), 2 deletions(-) create mode 100644 apps/backend/src/allocations/dtos/create-allocations.dto.ts diff --git a/apps/backend/src/allocations/allocations.controller.ts b/apps/backend/src/allocations/allocations.controller.ts index d8d2324d0..0d8e6bafd 100644 --- a/apps/backend/src/allocations/allocations.controller.ts +++ b/apps/backend/src/allocations/allocations.controller.ts @@ -1,8 +1,43 @@ -import { Controller, Get, Param, ParseIntPipe } from '@nestjs/common'; +import { Body, Controller, Post } from '@nestjs/common'; import { AllocationsService } from './allocations.service'; import { Allocation } from './allocations.entity'; +import { ApiBody } from '@nestjs/swagger'; +import { CreateMultipleAllocationsDto } from './dtos/create-allocations.dto'; @Controller('allocations') export class AllocationsController { constructor(private allocationsService: AllocationsService) {} + + @Post('/create-multiple') + @ApiBody({ + description: + 'Bulk create allocations given multiple donation item ids and quantities and an order id', + schema: { + type: 'object', + properties: { + orderId: { + type: 'integer', + example: 1, + }, + donationItems: { + type: 'object', + description: 'Map of donationItemId -> quantity', + additionalProperties: { + type: 'integer', + example: 10, + }, + example: { + '5': 10, + '8': 3, + '12': 7, + }, + }, + }, + }, + }) + async createMultipleAllocations( + @Body() body: CreateMultipleAllocationsDto, + ): Promise { + return this.allocationsService.createMultiple(body); + } } diff --git a/apps/backend/src/allocations/allocations.service.ts b/apps/backend/src/allocations/allocations.service.ts index b68b50ae2..520e0d6a6 100644 --- a/apps/backend/src/allocations/allocations.service.ts +++ b/apps/backend/src/allocations/allocations.service.ts @@ -2,6 +2,8 @@ import { Injectable } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; import { Repository } from 'typeorm'; import { Allocation } from '../allocations/allocations.entity'; +import { CreateMultipleAllocationsDto } from './dtos/create-allocations.dto'; +import { validateId } from '../utils/validation.utils'; @Injectable() export class AllocationsService { @@ -23,4 +25,31 @@ export class AllocationsService { }, }); } + + async createMultiple( + body: CreateMultipleAllocationsDto, + ): Promise { + const orderId = body.orderId; + const donationItems = body.donationItems; + + validateId(orderId, 'Order'); + + const allocations: Allocation[] = []; + + for (const [itemIdStr, quantity] of Object.entries(donationItems)) { + const itemId = Number(itemIdStr); + + validateId(itemId, 'Item'); + + const allocation = this.repo.create({ + orderId, + itemId, + allocatedQuantity: quantity, + }); + + allocations.push(allocation); + } + + return await this.repo.save(allocations); + } } diff --git a/apps/backend/src/allocations/dtos/create-allocations.dto.ts b/apps/backend/src/allocations/dtos/create-allocations.dto.ts new file mode 100644 index 000000000..1b09d30ed --- /dev/null +++ b/apps/backend/src/allocations/dtos/create-allocations.dto.ts @@ -0,0 +1,9 @@ +import { IsNumber, IsObject } from 'class-validator'; + +export class CreateMultipleAllocationsDto { + @IsNumber() + orderId!: number; + + @IsObject() + donationItems!: Record; +} diff --git a/apps/backend/src/donationItems/donationItems.controller.ts b/apps/backend/src/donationItems/donationItems.controller.ts index 95c05b02a..6241d92a9 100644 --- a/apps/backend/src/donationItems/donationItems.controller.ts +++ b/apps/backend/src/donationItems/donationItems.controller.ts @@ -71,4 +71,11 @@ export class DonationItemsController { ): Promise { return this.donationItemsService.updateDonationItemQuantity(itemId); } + + @Patch('/set-quantities') + async setDonationItemQuantities( + @Body() body: Record, + ): Promise { + return this.donationItemsService.setDonationItemQuantities(body); + } } diff --git a/apps/backend/src/donationItems/donationItems.service.ts b/apps/backend/src/donationItems/donationItems.service.ts index b9ffd0424..66fb274be 100644 --- a/apps/backend/src/donationItems/donationItems.service.ts +++ b/apps/backend/src/donationItems/donationItems.service.ts @@ -97,4 +97,16 @@ export class DonationItemsService { donationItem.quantity -= 1; return this.repo.save(donationItem); } + + async setDonationItemQuantities( + donationItems: Record, + ): Promise { + for (const [itemId, quantity] of Object.entries(donationItems)) { + const id = Number(itemId); + + validateId(id, 'Item'); + + await this.repo.increment({ itemId: id }, 'reservedQuantity', quantity); + } + } } diff --git a/apps/backend/src/orders/order.module.ts b/apps/backend/src/orders/order.module.ts index 9445b6dc2..5010a307c 100644 --- a/apps/backend/src/orders/order.module.ts +++ b/apps/backend/src/orders/order.module.ts @@ -14,6 +14,7 @@ import { FoodManufacturer } from '../foodManufacturers/manufacturers.entity'; import { DonationItem } from '../donationItems/donationItems.entity'; import { ManufacturerModule } from '../foodManufacturers/manufacturers.module'; import { DonationItemsModule } from '../donationItems/donationItems.module'; +import { Allocation } from '../allocations/allocations.entity'; @Module({ imports: [ @@ -23,6 +24,7 @@ import { DonationItemsModule } from '../donationItems/donationItems.module'; FoodRequest, FoodManufacturer, DonationItem, + Allocation, ]), AllocationModule, forwardRef(() => AuthModule), @@ -31,6 +33,7 @@ import { DonationItemsModule } from '../donationItems/donationItems.module'; forwardRef(() => RequestsModule), ManufacturerModule, DonationItemsModule, + AllocationModule, ], controllers: [OrdersController], providers: [OrdersService], diff --git a/apps/backend/src/orders/order.service.spec.ts b/apps/backend/src/orders/order.service.spec.ts index 20510d45a..e2dd1de87 100644 --- a/apps/backend/src/orders/order.service.spec.ts +++ b/apps/backend/src/orders/order.service.spec.ts @@ -13,6 +13,16 @@ import { FoodRequest } from '../foodRequests/request.entity'; import 'multer'; import { FoodRequestStatus } from '../foodRequests/types'; import { RequestsService } from '../foodRequests/request.service'; +import { FoodManufacturersService } from '../foodManufacturers/manufacturers.service'; +import { DonationItemsService } from '../donationItems/donationItems.service'; +import { AllocationsService } from '../allocations/allocations.service'; +import { FoodManufacturer } from '../foodManufacturers/manufacturers.entity'; +import { UsersService } from '../users/users.service'; +import { DonationItem } from '../donationItems/donationItems.entity'; +import { Donation } from '../donations/donations.entity'; +import { Allocation } from '../allocations/allocations.entity'; +import { User } from '../users/user.entity'; +import { AuthService } from '../auth/auth.service'; // Set 1 minute timeout for async DB operations jest.setTimeout(60000); @@ -34,10 +44,18 @@ describe('OrdersService', () => { providers: [ OrdersService, RequestsService, + FoodManufacturersService, + DonationItemsService, + AllocationsService, + UsersService, { provide: getRepositoryToken(Order), useValue: testDataSource.getRepository(Order), }, + { + provide: getRepositoryToken(FoodManufacturer), + useValue: testDataSource.getRepository(FoodManufacturer), + }, { provide: getRepositoryToken(Pantry), useValue: testDataSource.getRepository(Pantry), @@ -46,6 +64,26 @@ describe('OrdersService', () => { provide: getRepositoryToken(FoodRequest), useValue: testDataSource.getRepository(FoodRequest), }, + { + provide: getRepositoryToken(DonationItem), + useValue: testDataSource.getRepository(DonationItem), + }, + { + provide: getRepositoryToken(Donation), + useValue: testDataSource.getRepository(Donation), + }, + { + provide: getRepositoryToken(Allocation), + useValue: testDataSource.getRepository(Allocation), + }, + { + provide: getRepositoryToken(User), + useValue: testDataSource.getRepository(User), + }, + { + provide: AuthService, + useValue: {}, + }, ], }).compile(); diff --git a/apps/backend/src/orders/order.service.ts b/apps/backend/src/orders/order.service.ts index 2c46c81e0..1ffecccfe 100644 --- a/apps/backend/src/orders/order.service.ts +++ b/apps/backend/src/orders/order.service.ts @@ -19,6 +19,7 @@ import { CreateOrderDto } from './dtos/create-order.dto'; import { FoodRequestStatus } from '../foodRequests/types'; import { FoodManufacturersService } from '../foodManufacturers/manufacturers.service'; import { DonationItemsService } from '../donationItems/donationItems.service'; +import { AllocationsService } from '../allocations/allocations.service'; @Injectable() export class OrdersService { @@ -28,6 +29,7 @@ export class OrdersService { private requestsService: RequestsService, private manufacturerService: FoodManufacturersService, private donationItemsService: DonationItemsService, + private allocationsService: AllocationsService, ) {} // TODO: when order is created, set FM @@ -133,7 +135,18 @@ export class OrdersService { status: OrderStatus.PENDING, }); - return this.repo.save(order); + const savedOrder = await this.repo.save(order); + + await this.allocationsService.createMultiple({ + orderId: savedOrder.orderId, + donationItems: orderData.donationItems, + }); + + await this.donationItemsService.setDonationItemQuantities( + orderData.donationItems, + ); + + return savedOrder; } async findOne(orderId: number): Promise { From efd05da9f7d0ccc5bf5294d4c7b2c861cefd84f4 Mon Sep 17 00:00:00 2001 From: Justin Wang Date: Tue, 10 Mar 2026 17:25:44 -0400 Subject: [PATCH 03/15] finish route functionality --- .../donationItems/donationItems.controller.ts | 15 ++++++ .../donationItems/donationItems.service.ts | 22 +++++++++ .../src/donations/donations.controller.ts | 5 ++ .../src/donations/donations.service.ts | 9 +++- apps/backend/src/orders/order.module.ts | 4 ++ apps/backend/src/orders/order.service.spec.ts | 2 + apps/backend/src/orders/order.service.ts | 48 +++++++++++++------ 7 files changed, 90 insertions(+), 15 deletions(-) diff --git a/apps/backend/src/donationItems/donationItems.controller.ts b/apps/backend/src/donationItems/donationItems.controller.ts index 6241d92a9..62eebb50b 100644 --- a/apps/backend/src/donationItems/donationItems.controller.ts +++ b/apps/backend/src/donationItems/donationItems.controller.ts @@ -12,6 +12,7 @@ import { DonationItemsService } from './donationItems.service'; import { DonationItem } from './donationItems.entity'; import { FoodType } from './types'; import { CreateMultipleDonationItemsDto } from './dtos/create-donation-items.dto'; +import { Donation } from '../donations/donations.entity'; @Controller('donation-items') //@UseInterceptors() @@ -25,6 +26,20 @@ export class DonationItemsController { return this.donationItemsService.getAllDonationItems(donationId); } + @Get('/get-associated-donations') + async getDonationsFromDonationItemIds( + @Body() donationItemIds: number[], + ): Promise { + return this.donationItemsService.getAssociatedDonations(donationItemIds); + } + + @Get('/all') + async getAllDonationItems( + @Body() donationItemIds: number[], + ): Promise { + return this.donationItemsService.getAll(donationItemIds); + } + @Post('/create-multiple') @ApiBody({ description: 'Bulk create donation items for a single donation', diff --git a/apps/backend/src/donationItems/donationItems.service.ts b/apps/backend/src/donationItems/donationItems.service.ts index 66fb274be..e93a3e3ec 100644 --- a/apps/backend/src/donationItems/donationItems.service.ts +++ b/apps/backend/src/donationItems/donationItems.service.ts @@ -18,6 +18,14 @@ export class DonationItemsService { return this.repo.find({ where: { donation: { donationId } } }); } + async getAll(donationItemIds: number[]): Promise { + return this.repo.find({ + where: { + itemId: In(donationItemIds), + }, + }); + } + async getDonationItemsByDonationIds( donationIds: number[], ): Promise { @@ -30,6 +38,20 @@ export class DonationItemsService { }); } + async getAssociatedDonations(donationItemIds: number[]): Promise { + const items = await this.repo.find({ + where: { itemId: In(donationItemIds) }, + relations: ['donation'], + }); + + const donations = items.map((i) => i.donation); + + // Assure no duplicates + return Array.from( + new Map(donations.map((d) => [d.donationId, d])).values(), + ); + } + async create( donationId: number, itemName: string, diff --git a/apps/backend/src/donations/donations.controller.ts b/apps/backend/src/donations/donations.controller.ts index dd662651e..8369addeb 100644 --- a/apps/backend/src/donations/donations.controller.ts +++ b/apps/backend/src/donations/donations.controller.ts @@ -85,4 +85,9 @@ export class DonationsController { } return updatedDonation; } + + @Patch('/match-all') + async matchAllDonations(@Body() donationIds: number[]): Promise { + await this.donationService.matchAll(donationIds); + } } diff --git a/apps/backend/src/donations/donations.service.ts b/apps/backend/src/donations/donations.service.ts index 85cf8f3c2..d71b1f4ab 100644 --- a/apps/backend/src/donations/donations.service.ts +++ b/apps/backend/src/donations/donations.service.ts @@ -5,7 +5,7 @@ import { NotFoundException, } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; -import { Repository } from 'typeorm'; +import { In, Repository } from 'typeorm'; import { Donation } from './donations.entity'; import { validateId } from '../utils/validation.utils'; import { DayOfWeek, DonationStatus, RecurrenceEnum } from './types'; @@ -101,6 +101,13 @@ export class DonationService { return this.repo.save(donation); } + async matchAll(donationIds: number[]): Promise { + await this.repo.update( + { donationId: In(donationIds) }, + { status: DonationStatus.MATCHED }, + ); + } + async handleRecurringDonations(): Promise { const donations = await this.getAll(); const today = new Date(); diff --git a/apps/backend/src/orders/order.module.ts b/apps/backend/src/orders/order.module.ts index 5010a307c..34f799666 100644 --- a/apps/backend/src/orders/order.module.ts +++ b/apps/backend/src/orders/order.module.ts @@ -15,6 +15,8 @@ import { DonationItem } from '../donationItems/donationItems.entity'; import { ManufacturerModule } from '../foodManufacturers/manufacturers.module'; import { DonationItemsModule } from '../donationItems/donationItems.module'; import { Allocation } from '../allocations/allocations.entity'; +import { DonationService } from '../donations/donations.service'; +import { DonationModule } from '../donations/donations.module'; @Module({ imports: [ @@ -25,6 +27,7 @@ import { Allocation } from '../allocations/allocations.entity'; FoodManufacturer, DonationItem, Allocation, + DonationService, ]), AllocationModule, forwardRef(() => AuthModule), @@ -34,6 +37,7 @@ import { Allocation } from '../allocations/allocations.entity'; ManufacturerModule, DonationItemsModule, AllocationModule, + DonationModule, ], controllers: [OrdersController], providers: [OrdersService], diff --git a/apps/backend/src/orders/order.service.spec.ts b/apps/backend/src/orders/order.service.spec.ts index e2dd1de87..f49ee9c2d 100644 --- a/apps/backend/src/orders/order.service.spec.ts +++ b/apps/backend/src/orders/order.service.spec.ts @@ -23,6 +23,7 @@ import { Donation } from '../donations/donations.entity'; import { Allocation } from '../allocations/allocations.entity'; import { User } from '../users/user.entity'; import { AuthService } from '../auth/auth.service'; +import { DonationService } from '../donations/donations.service'; // Set 1 minute timeout for async DB operations jest.setTimeout(60000); @@ -48,6 +49,7 @@ describe('OrdersService', () => { DonationItemsService, AllocationsService, UsersService, + DonationService, { provide: getRepositoryToken(Order), useValue: testDataSource.getRepository(Order), diff --git a/apps/backend/src/orders/order.service.ts b/apps/backend/src/orders/order.service.ts index 1ffecccfe..0dde026a6 100644 --- a/apps/backend/src/orders/order.service.ts +++ b/apps/backend/src/orders/order.service.ts @@ -20,6 +20,7 @@ import { FoodRequestStatus } from '../foodRequests/types'; import { FoodManufacturersService } from '../foodManufacturers/manufacturers.service'; import { DonationItemsService } from '../donationItems/donationItems.service'; import { AllocationsService } from '../allocations/allocations.service'; +import { DonationService } from '../donations/donations.service'; @Injectable() export class OrdersService { @@ -30,6 +31,7 @@ export class OrdersService { private manufacturerService: FoodManufacturersService, private donationItemsService: DonationItemsService, private allocationsService: AllocationsService, + private donationService: DonationService, ) {} // TODO: when order is created, set FM @@ -83,7 +85,7 @@ export class OrdersService { const manufacturerId = orderData.manufacturerId; validateId(manufacturerId, 'Food Manufacturer'); - validateId(requestId, 'Food Request'); + validateId(requestId, 'Request'); const request = await this.requestsService.findOne(requestId); @@ -95,26 +97,42 @@ export class OrdersService { throw new BadRequestException(`Request ${requestId} is not active`); } - // Ensure all donation items belong to specified manufacturer - - const donations = this.manufacturerService.getFMDonations(manufacturerId); - const donationIds = (await donations).map((d) => d.donationId); + const donationItemIds = Object.keys(orderData.donationItems).map(Number); + const donationItems = await this.donationItemsService.getAll( + donationItemIds, + ); - const donationItems = + // All donations associated with the given donation items + const associatedDonations = await this.donationItemsService.getDonationItemsByDonationIds( - donationIds, + donationItemIds, ); - const validDonationItemIds = new Set(donationItems.map((d) => d.itemId)); + const associatedDonationDonationIds = associatedDonations.map( + (d) => d.donationId, + ); + + const donations = await this.manufacturerService.getFMDonations( + manufacturerId, + ); + const FMDonationIds = donations.map((d) => d.donationId); + + const fmDonationSet = new Set(FMDonationIds); + + // True if there is an associated donation that does not belong to the current FM + const invalidDonation = associatedDonationDonationIds.find( + (id) => !fmDonationSet.has(id), + ); + + if (invalidDonation) { + throw new Error( + `Donation ${invalidDonation} is not associated with the current food manufacturer`, + ); + } for (const [itemId, quantity] of Object.entries(orderData.donationItems)) { const id = Number(itemId); const count = Number(quantity); - - if (!validDonationItemIds.has(id)) { - throw new BadRequestException( - `Donation item ${id} does not belong to this manufacturer`, - ); - } + validateId(id, 'Donation Item'); const donationItem = donationItems.find((d) => d.itemId === id); @@ -146,6 +164,8 @@ export class OrdersService { orderData.donationItems, ); + await this.donationService.matchAll(associatedDonationDonationIds); + return savedOrder; } From 51f80e3766636326bb23cd5b23af350436ab8090 Mon Sep 17 00:00:00 2001 From: Justin Wang Date: Tue, 10 Mar 2026 18:52:15 -0400 Subject: [PATCH 04/15] all tests except create order service test --- .../src/allocations/allocations.controller.ts | 2 +- .../donationItems.controller.spec.ts | 109 ++++++++++++---- .../donationItems/donationItems.controller.ts | 9 +- .../donations/donations.controller.spec.ts | 14 +++ .../backend/src/donations/donations.module.ts | 8 +- .../src/donations/donations.service.spec.ts | 23 +++- .../src/orders/order.controller.spec.ts | 117 ++++++++++++++++++ 7 files changed, 254 insertions(+), 28 deletions(-) diff --git a/apps/backend/src/allocations/allocations.controller.ts b/apps/backend/src/allocations/allocations.controller.ts index 0d8e6bafd..45fe6941c 100644 --- a/apps/backend/src/allocations/allocations.controller.ts +++ b/apps/backend/src/allocations/allocations.controller.ts @@ -11,7 +11,7 @@ export class AllocationsController { @Post('/create-multiple') @ApiBody({ description: - 'Bulk create allocations given multiple donation item ids and quantities and an order id', + 'Bulk create allocations given an order id, multiple donation item ids and quantities', schema: { type: 'object', properties: { diff --git a/apps/backend/src/donationItems/donationItems.controller.spec.ts b/apps/backend/src/donationItems/donationItems.controller.spec.ts index 1517c3912..99ea723b2 100644 --- a/apps/backend/src/donationItems/donationItems.controller.spec.ts +++ b/apps/backend/src/donationItems/donationItems.controller.spec.ts @@ -5,35 +5,14 @@ import { DonationItem } from './donationItems.entity'; import { mock } from 'jest-mock-extended'; import { FoodType } from './types'; import { CreateMultipleDonationItemsDto } from './dtos/create-donation-items.dto'; +import { Donation } from '../donations/donations.entity'; +import { DonationStatus } from '../donations/types'; const mockDonationItemsService = mock(); describe('DonationItemsController', () => { let controller: DonationItemsController; - const mockDonationItemsCreateData: Partial[] = [ - { - itemId: 1, - donationId: 1, - itemName: 'Canned Beans', - quantity: 100, - reservedQuantity: 0, - ozPerItem: 15, - estimatedValue: 200, - foodType: FoodType.DAIRY_FREE_ALTERNATIVES, - }, - { - itemId: 2, - donationId: 1, - itemName: 'Rice', - quantity: 50, - reservedQuantity: 0, - ozPerItem: 20, - estimatedValue: 150, - foodType: FoodType.GLUTEN_FREE_BAKING_PANCAKE_MIXES, - }, - ]; - beforeEach(async () => { const module: TestingModule = await Test.createTestingModule({ controllers: [DonationItemsController], @@ -90,4 +69,88 @@ describe('DonationItemsController', () => { expect(result).toEqual(mockCreatedItems); }); }); + + describe('getDonationsFromDonationItemIds', () => { + it('should call service.getAssociatedDonations with donationItemIds and return donations', async () => { + const donationItemIds = [1, 2, 3]; + const mockDonations = [ + { donationId: 1, status: DonationStatus.AVAILABLE }, + { donationId: 2, status: DonationStatus.FULFILLED }, + ] as Partial[]; + + mockDonationItemsService.getAssociatedDonations.mockResolvedValue( + mockDonations as Donation[], + ); + + const result = await controller.getDonationsFromDonationItemIds( + donationItemIds, + ); + + expect( + mockDonationItemsService.getAssociatedDonations, + ).toHaveBeenCalledWith(donationItemIds); + expect(result).toEqual(mockDonations); + }); + }); + + describe('getAllDonationItems', () => { + it('should call service.getAll with donationItemIds and return donation items', async () => { + const donationItemIds = [1, 2]; + const mockItems = [ + { itemId: 1, itemName: 'Rice' }, + { itemId: 2, itemName: 'Beans' }, + ] as Partial[]; + + mockDonationItemsService.getAll.mockResolvedValue( + mockItems as DonationItem[], + ); + + const result = await controller.getAllDonationItems(donationItemIds); + + expect(mockDonationItemsService.getAll).toHaveBeenCalledWith( + donationItemIds, + ); + expect(result).toEqual(mockItems); + }); + }); + + describe('setDonationItemQuantities', () => { + it('should call service.setDonationItemQuantities with the body', async () => { + const body: Record = { 1: 10, 2: 20 }; + + mockDonationItemsService.setDonationItemQuantities.mockResolvedValue( + undefined, + ); + + const result = await controller.setDonationItemQuantities(body); + + expect( + mockDonationItemsService.setDonationItemQuantities, + ).toHaveBeenCalledWith(body); + expect(result).toBeUndefined(); + }); + }); + + describe('getDonationItemsByDonationIds', () => { + it('should call service.getDonationItemsByDonationIds with donationIds and return items', async () => { + const donationIds = [1, 2]; + const mockItems = [ + { itemId: 1, donationId: 1, itemName: 'Rice' }, + { itemId: 2, donationId: 2, itemName: 'Beans' }, + ] as Partial[]; + + mockDonationItemsService.getDonationItemsByDonationIds.mockResolvedValue( + mockItems as DonationItem[], + ); + + const result = await controller.getDonationItemsByDonationIds( + donationIds, + ); + + expect( + mockDonationItemsService.getDonationItemsByDonationIds, + ).toHaveBeenCalledWith(donationIds); + expect(result).toEqual(mockItems); + }); + }); }); diff --git a/apps/backend/src/donationItems/donationItems.controller.ts b/apps/backend/src/donationItems/donationItems.controller.ts index 62eebb50b..c92efa8d8 100644 --- a/apps/backend/src/donationItems/donationItems.controller.ts +++ b/apps/backend/src/donationItems/donationItems.controller.ts @@ -26,13 +26,20 @@ export class DonationItemsController { return this.donationItemsService.getAllDonationItems(donationId); } - @Get('/get-associated-donations') + @Get('/associated-donations') async getDonationsFromDonationItemIds( @Body() donationItemIds: number[], ): Promise { return this.donationItemsService.getAssociatedDonations(donationItemIds); } + @Get('/donation-items-by-donation-ids') + async getDonationItemsByDonationIds( + @Body() donationIds: number[], + ): Promise { + return this.donationItemsService.getDonationItemsByDonationIds(donationIds); + } + @Get('/all') async getAllDonationItems( @Body() donationItemIds: number[], diff --git a/apps/backend/src/donations/donations.controller.spec.ts b/apps/backend/src/donations/donations.controller.spec.ts index 9f5b99095..9cc8d0553 100644 --- a/apps/backend/src/donations/donations.controller.spec.ts +++ b/apps/backend/src/donations/donations.controller.spec.ts @@ -52,4 +52,18 @@ describe('DonationsController', () => { expect(mockDonationService.findOne).toHaveBeenCalledWith(1); }); }); + + describe('PATCH /match-all', () => { + it('should call donationService.matchAll with donationIds', async () => { + const donationIds = [1, 2, 3]; + + mockDonationService.matchAll.mockResolvedValue(undefined); + + const result = await controller.matchAllDonations(donationIds); + + expect(mockDonationService.matchAll).toHaveBeenCalledWith(donationIds); + + expect(result).toBeUndefined(); + }); + }); }); diff --git a/apps/backend/src/donations/donations.module.ts b/apps/backend/src/donations/donations.module.ts index 0b813de8f..71ffbc7db 100644 --- a/apps/backend/src/donations/donations.module.ts +++ b/apps/backend/src/donations/donations.module.ts @@ -1,4 +1,4 @@ -import { Module } from '@nestjs/common'; +import { forwardRef, Module } from '@nestjs/common'; import { TypeOrmModule } from '@nestjs/typeorm'; import { Donation } from './donations.entity'; import { DonationService } from './donations.service'; @@ -8,8 +8,12 @@ import { FoodManufacturer } from '../foodManufacturers/manufacturers.entity'; import { DonationsSchedulerService } from './donations.scheduler'; @Module({ - imports: [TypeOrmModule.forFeature([Donation, FoodManufacturer]), AuthModule], + imports: [ + TypeOrmModule.forFeature([Donation, FoodManufacturer]), + forwardRef(() => AuthModule), + ], controllers: [DonationsController], providers: [DonationService, DonationsSchedulerService], + exports: [DonationService], }) export class DonationModule {} diff --git a/apps/backend/src/donations/donations.service.spec.ts b/apps/backend/src/donations/donations.service.spec.ts index 84da409cb..6b98e30cb 100644 --- a/apps/backend/src/donations/donations.service.spec.ts +++ b/apps/backend/src/donations/donations.service.spec.ts @@ -3,7 +3,7 @@ import { getRepositoryToken } from '@nestjs/typeorm'; import { Donation } from './donations.entity'; import { DonationService } from './donations.service'; import { FoodManufacturer } from '../foodManufacturers/manufacturers.entity'; -import { RecurrenceEnum, DayOfWeek } from './types'; +import { RecurrenceEnum, DayOfWeek, DonationStatus } from './types'; import { RepeatOnDaysDto } from './dtos/create-donation.dto'; import { testDataSource } from '../config/typeormTestDataSource'; @@ -136,6 +136,27 @@ describe('DonationService', () => { }); }); + describe('matchAll', () => { + it('should call repo.update with donationIds and set status MATCHED', async () => { + const donationId1 = 1; + const donationId2 = 2; + const donationIds = [donationId1, donationId2]; + + const donation1 = await service.findOne(donationId1); + const donation2 = await service.findOne(donationId2); + expect(donation1.status).toEqual(DonationStatus.AVAILABLE); + expect(donation2.status).toEqual(DonationStatus.MATCHED); + + await service.matchAll(donationIds); + + const updatedDonation1 = await service.findOne(donationId1); + const updatedDonation2 = await service.findOne(donationId2); + + expect(updatedDonation1.status).toEqual(DonationStatus.MATCHED); + expect(updatedDonation2.status).toEqual(DonationStatus.MATCHED); + }); + }); + describe('handleRecurringDonations', () => { describe('no-op cases', () => { it('skips donation with no nextDonationDates', async () => { diff --git a/apps/backend/src/orders/order.controller.spec.ts b/apps/backend/src/orders/order.controller.spec.ts index d1b8403fa..92200f790 100644 --- a/apps/backend/src/orders/order.controller.spec.ts +++ b/apps/backend/src/orders/order.controller.spec.ts @@ -16,6 +16,7 @@ import { BadRequestException, NotFoundException } from '@nestjs/common'; import { FoodManufacturer } from '../foodManufacturers/manufacturers.entity'; import { FoodRequestSummaryDto } from '../foodRequests/dtos/food-request-summary.dto'; import { ConfirmDeliveryDto } from './dtos/confirm-delivery.dto'; +import { CreateOrderDto } from './dtos/create-order.dto'; const mockOrdersService = mock(); const mockAllocationsService = mock(); @@ -531,4 +532,120 @@ describe('OrdersController', () => { ); }); }); + + describe('createOrder', () => { + it('should call ordersService.create and return the created order', async () => { + const createOrderDto = { + foodRequestId: 1, + manufacturerId: 1, + donationItems: { + 5: 10, + 8: 3, + 12: 7, + }, + }; + + const mockCreatedOrder: Partial = { + orderId: 42, + status: OrderStatus.PENDING, + request: { requestId: 1 } as FoodRequest, + foodManufacturer: { foodManufacturerId: 1 } as FoodManufacturer, + }; + + mockOrdersService.create.mockResolvedValueOnce(mockCreatedOrder as Order); + + const result = await controller.createOrder(createOrderDto); + + expect(mockOrdersService.create).toHaveBeenCalledWith(createOrderDto); + expect(result).toEqual(mockCreatedOrder); + }); + + it('should propagate NotFoundException when request not found', async () => { + const foodRequestId = 999; + + const createOrderDto: CreateOrderDto = { + foodRequestId: foodRequestId, + manufacturerId: 1, + donationItems: { 5: 10 }, + }; + + mockOrdersService.create.mockRejectedValueOnce( + new NotFoundException(`Request ${foodRequestId} not found`), + ); + + const promise = controller.createOrder(createOrderDto); + await expect(promise).rejects.toBeInstanceOf(NotFoundException); + await expect(promise).rejects.toThrow( + `Request ${foodRequestId} not found`, + ); + expect(mockOrdersService.create).toHaveBeenCalledWith(createOrderDto); + }); + + it('should propagate BadRequestException when request is not active', async () => { + const foodRequestId = 1; + + const createOrderDto: CreateOrderDto = { + foodRequestId: foodRequestId, + manufacturerId: 1, + donationItems: { 5: 10 }, + }; + + mockOrdersService.create.mockRejectedValueOnce( + new BadRequestException(`Request ${foodRequestId} is not active`), + ); + + const promise = controller.createOrder(createOrderDto); + await expect(promise).rejects.toBeInstanceOf(BadRequestException); + await expect(promise).rejects.toThrow( + `Request ${foodRequestId} is not active`, + ); + expect(mockOrdersService.create).toHaveBeenCalledWith(createOrderDto); + }); + + it('should propagate Error when donation item does not belong to FM', async () => { + const invalidDonationId = 1; + + const createOrderDto: CreateOrderDto = { + foodRequestId: 1, + manufacturerId: 1, + donationItems: { 5: 10 }, + }; + + mockOrdersService.create.mockRejectedValueOnce( + new Error( + `Donation ${invalidDonationId} is not associated with the current food manufacturer`, + ), + ); + + const promise = controller.createOrder(createOrderDto); + await expect(promise).rejects.toThrow(Error); + await expect(promise).rejects.toThrow( + `Donation ${invalidDonationId} is not associated with the current food manufacturer`, + ); + expect(mockOrdersService.create).toHaveBeenCalledWith(createOrderDto); + }); + + it('should propagate BadRequestException when allocated quantity exceeds remaining', async () => { + const donationItemId = 5; + + const createOrderDto: CreateOrderDto = { + foodRequestId: 1, + manufacturerId: 1, + donationItems: { [donationItemId]: 100 }, + }; + + mockOrdersService.create.mockRejectedValueOnce( + new BadRequestException( + `Donation item ${donationItemId} allocated quantity exceeds remaining quantity`, + ), + ); + + const promise = controller.createOrder(createOrderDto); + await expect(promise).rejects.toBeInstanceOf(BadRequestException); + await expect(promise).rejects.toThrow( + `Donation item ${donationItemId} allocated quantity exceeds remaining quantity`, + ); + expect(mockOrdersService.create).toHaveBeenCalledWith(createOrderDto); + }); + }); }); From 88dcbbd544248f1a0e5639eef59bd21cf03f45b4 Mon Sep 17 00:00:00 2001 From: Justin Wang Date: Tue, 10 Mar 2026 19:57:28 -0400 Subject: [PATCH 05/15] finish test --- .../donationItems.controller.spec.ts | 23 --- .../donationItems/donationItems.controller.ts | 7 - .../donationItems/donationItems.service.ts | 12 -- .../src/orders/order.controller.spec.ts | 31 ++++- apps/backend/src/orders/order.service.spec.ts | 131 ++++++++++++++++++ apps/backend/src/orders/order.service.ts | 31 +++-- 6 files changed, 175 insertions(+), 60 deletions(-) diff --git a/apps/backend/src/donationItems/donationItems.controller.spec.ts b/apps/backend/src/donationItems/donationItems.controller.spec.ts index 99ea723b2..e864783c9 100644 --- a/apps/backend/src/donationItems/donationItems.controller.spec.ts +++ b/apps/backend/src/donationItems/donationItems.controller.spec.ts @@ -130,27 +130,4 @@ describe('DonationItemsController', () => { expect(result).toBeUndefined(); }); }); - - describe('getDonationItemsByDonationIds', () => { - it('should call service.getDonationItemsByDonationIds with donationIds and return items', async () => { - const donationIds = [1, 2]; - const mockItems = [ - { itemId: 1, donationId: 1, itemName: 'Rice' }, - { itemId: 2, donationId: 2, itemName: 'Beans' }, - ] as Partial[]; - - mockDonationItemsService.getDonationItemsByDonationIds.mockResolvedValue( - mockItems as DonationItem[], - ); - - const result = await controller.getDonationItemsByDonationIds( - donationIds, - ); - - expect( - mockDonationItemsService.getDonationItemsByDonationIds, - ).toHaveBeenCalledWith(donationIds); - expect(result).toEqual(mockItems); - }); - }); }); diff --git a/apps/backend/src/donationItems/donationItems.controller.ts b/apps/backend/src/donationItems/donationItems.controller.ts index c92efa8d8..4344130e5 100644 --- a/apps/backend/src/donationItems/donationItems.controller.ts +++ b/apps/backend/src/donationItems/donationItems.controller.ts @@ -33,13 +33,6 @@ export class DonationItemsController { return this.donationItemsService.getAssociatedDonations(donationItemIds); } - @Get('/donation-items-by-donation-ids') - async getDonationItemsByDonationIds( - @Body() donationIds: number[], - ): Promise { - return this.donationItemsService.getDonationItemsByDonationIds(donationIds); - } - @Get('/all') async getAllDonationItems( @Body() donationItemIds: number[], diff --git a/apps/backend/src/donationItems/donationItems.service.ts b/apps/backend/src/donationItems/donationItems.service.ts index e93a3e3ec..d967491fc 100644 --- a/apps/backend/src/donationItems/donationItems.service.ts +++ b/apps/backend/src/donationItems/donationItems.service.ts @@ -26,18 +26,6 @@ export class DonationItemsService { }); } - async getDonationItemsByDonationIds( - donationIds: number[], - ): Promise { - return this.repo.find({ - where: { - donation: { - donationId: In(donationIds), - }, - }, - }); - } - async getAssociatedDonations(donationItemIds: number[]): Promise { const items = await this.repo.find({ where: { itemId: In(donationItemIds) }, diff --git a/apps/backend/src/orders/order.controller.spec.ts b/apps/backend/src/orders/order.controller.spec.ts index 92200f790..a18b159a6 100644 --- a/apps/backend/src/orders/order.controller.spec.ts +++ b/apps/backend/src/orders/order.controller.spec.ts @@ -581,6 +581,27 @@ describe('OrdersController', () => { expect(mockOrdersService.create).toHaveBeenCalledWith(createOrderDto); }); + it('should propagate NotFoundException when manufacturer not found', async () => { + const manufacturerId = 999; + + const createOrderDto: CreateOrderDto = { + foodRequestId: 1, + manufacturerId: manufacturerId, + donationItems: { 5: 10 }, + }; + + mockOrdersService.create.mockRejectedValueOnce( + new NotFoundException(`Food Manufacturer ${manufacturerId} not found`), + ); + + const promise = controller.createOrder(createOrderDto); + await expect(promise).rejects.toBeInstanceOf(NotFoundException); + await expect(promise).rejects.toThrow( + `Food Manufacturer ${manufacturerId} not found`, + ); + expect(mockOrdersService.create).toHaveBeenCalledWith(createOrderDto); + }); + it('should propagate BadRequestException when request is not active', async () => { const foodRequestId = 1; @@ -603,8 +624,6 @@ describe('OrdersController', () => { }); it('should propagate Error when donation item does not belong to FM', async () => { - const invalidDonationId = 1; - const createOrderDto: CreateOrderDto = { foodRequestId: 1, manufacturerId: 1, @@ -612,15 +631,15 @@ describe('OrdersController', () => { }; mockOrdersService.create.mockRejectedValueOnce( - new Error( - `Donation ${invalidDonationId} is not associated with the current food manufacturer`, + new BadRequestException( + `Donation is not associated with the current food manufacturer`, ), ); const promise = controller.createOrder(createOrderDto); - await expect(promise).rejects.toThrow(Error); + await expect(promise).rejects.toThrow(BadRequestException); await expect(promise).rejects.toThrow( - `Donation ${invalidDonationId} is not associated with the current food manufacturer`, + `Donation is not associated with the current food manufacturer`, ); expect(mockOrdersService.create).toHaveBeenCalledWith(createOrderDto); }); diff --git a/apps/backend/src/orders/order.service.spec.ts b/apps/backend/src/orders/order.service.spec.ts index f49ee9c2d..0b7cc1893 100644 --- a/apps/backend/src/orders/order.service.spec.ts +++ b/apps/backend/src/orders/order.service.spec.ts @@ -24,6 +24,8 @@ import { Allocation } from '../allocations/allocations.entity'; import { User } from '../users/user.entity'; import { AuthService } from '../auth/auth.service'; import { DonationService } from '../donations/donations.service'; +import { CreateOrderDto } from './dtos/create-order.dto'; +import { DonationStatus } from '../donations/types'; // Set 1 minute timeout for async DB operations jest.setTimeout(60000); @@ -667,4 +669,133 @@ describe('OrdersService', () => { ); }); }); + + describe('createOrder', () => { + let validCreateOrderDto: CreateOrderDto; + + beforeEach(() => { + validCreateOrderDto = { + foodRequestId: 1, + manufacturerId: 1, + donationItems: { + 1: 10, + 2: 3, + }, + }; + }); + + it('should create a new order successfully', async () => { + const allocationRepo = testDataSource.getRepository(Allocation); + const donationItemRepo = testDataSource.getRepository(DonationItem); + const donationRepo = testDataSource.getRepository(Donation); + + // Initial donation items + const donationItem1 = await donationItemRepo.findOne({ + where: { itemId: 1 }, + }); + const donationItem2 = await donationItemRepo.findOne({ + where: { itemId: 2 }, + }); + + if (!donationItem1 || !donationItem2) + throw new Error('Missing dummy donation items'); + + const createdOrder = await service.create(validCreateOrderDto); + + expect(createdOrder).toBeDefined(); + expect(createdOrder.orderId).toBeDefined(); + expect(createdOrder.status).toEqual(OrderStatus.PENDING); + expect(createdOrder.foodManufacturerId).toEqual( + validCreateOrderDto.manufacturerId, + ); + expect(createdOrder.requestId).toEqual(validCreateOrderDto.foodRequestId); + + const allocations = await allocationRepo.find({ + where: { orderId: createdOrder.orderId }, + }); + expect(allocations.length).toBe( + Object.keys(validCreateOrderDto.donationItems).length, + ); + expect(allocations.map((a) => a.itemId)).toEqual( + expect.arrayContaining([1, 2]), + ); + + const updatedDonation1 = await donationItemRepo.findOne({ + where: { itemId: 1 }, + }); + const updatedDonation2 = await donationItemRepo.findOne({ + where: { itemId: 2 }, + }); + + expect(updatedDonation1!.reservedQuantity).toBe( + donationItem1.reservedQuantity + 10, + ); + expect(updatedDonation2!.reservedQuantity).toBe( + donationItem2.reservedQuantity + 3, + ); + + const matchedDonation = await donationRepo.findOne({ + where: { donationId: 1 }, + }); + expect(matchedDonation?.status).toBe(DonationStatus.MATCHED); + }); + + it('should throw NotFoundException if request does not exist', async () => { + validCreateOrderDto.foodRequestId = 999; + await expect(service.create(validCreateOrderDto)).rejects.toThrow( + NotFoundException, + ); + await expect(service.create(validCreateOrderDto)).rejects.toThrow( + `Request ${validCreateOrderDto.foodRequestId} not found`, + ); + }); + + it('should throw NotFoundException if manufacturer does not exist', async () => { + validCreateOrderDto.manufacturerId = 999; + await expect(service.create(validCreateOrderDto)).rejects.toThrow( + NotFoundException, + ); + await expect(service.create(validCreateOrderDto)).rejects.toThrow( + `Food Manufacturer ${validCreateOrderDto.manufacturerId} not found`, + ); + }); + + it('should throw BadRequestException if request is not active', async () => { + const requestRepo = testDataSource.getRepository(FoodRequest); + + const request = await requestRepo.findOne({ where: { requestId: 2 } }); + + if (!request) throw new Error('Missing dummy request'); + + request.status = FoodRequestStatus.CLOSED; + await requestRepo.save(request); + + validCreateOrderDto.foodRequestId = 2; + await expect(service.create(validCreateOrderDto)).rejects.toThrow( + BadRequestException, + ); + await expect(service.create(validCreateOrderDto)).rejects.toThrow( + `Request ${validCreateOrderDto.foodRequestId} is not active`, + ); + }); + + it('should throw BadRequestException if allocated quantity exceeds remaining', async () => { + const donationItemId = 2; + + validCreateOrderDto.donationItems = { [donationItemId]: 500 }; + await expect(service.create(validCreateOrderDto)).rejects.toThrow( + BadRequestException, + ); + await expect(service.create(validCreateOrderDto)).rejects.toThrow( + `Donation item ${donationItemId} allocated quantity exceeds remaining quantity`, + ); + }); + + it('should throw Error if donation is not associated with manufacturer', async () => { + validCreateOrderDto.donationItems = { 7: 2 }; + await expect(service.create(validCreateOrderDto)).rejects.toThrow( + BadRequestException, + ); + }); + }); }); diff --git a/apps/backend/src/orders/order.service.ts b/apps/backend/src/orders/order.service.ts index 0dde026a6..fc61dd7f7 100644 --- a/apps/backend/src/orders/order.service.ts +++ b/apps/backend/src/orders/order.service.ts @@ -27,6 +27,8 @@ export class OrdersService { constructor( @InjectRepository(Order) private repo: Repository, @InjectRepository(Pantry) private pantryRepo: Repository, + @InjectRepository(FoodManufacturer) + private manufacturerRepo: Repository, private requestsService: RequestsService, private manufacturerService: FoodManufacturersService, private donationItemsService: DonationItemsService, @@ -97,6 +99,20 @@ export class OrdersService { throw new BadRequestException(`Request ${requestId} is not active`); } + const manufacturer = await this.manufacturerRepo.findOne({ + where: { foodManufacturerId: manufacturerId }, + relations: ['donations'], + }); + + if (!manufacturer) { + throw new NotFoundException( + `Food Manufacturer ${manufacturerId} not found`, + ); + } + + const fmDonationIds = manufacturer.donations.map((d) => d.donationId); + const fmDonationSet = new Set(fmDonationIds); + const donationItemIds = Object.keys(orderData.donationItems).map(Number); const donationItems = await this.donationItemsService.getAll( donationItemIds, @@ -104,28 +120,19 @@ export class OrdersService { // All donations associated with the given donation items const associatedDonations = - await this.donationItemsService.getDonationItemsByDonationIds( - donationItemIds, - ); + await this.donationItemsService.getAssociatedDonations(donationItemIds); const associatedDonationDonationIds = associatedDonations.map( (d) => d.donationId, ); - const donations = await this.manufacturerService.getFMDonations( - manufacturerId, - ); - const FMDonationIds = donations.map((d) => d.donationId); - - const fmDonationSet = new Set(FMDonationIds); - // True if there is an associated donation that does not belong to the current FM const invalidDonation = associatedDonationDonationIds.find( (id) => !fmDonationSet.has(id), ); if (invalidDonation) { - throw new Error( - `Donation ${invalidDonation} is not associated with the current food manufacturer`, + throw new BadRequestException( + `Donation is not associated with the current food manufacturer`, ); } From 64bc3c045c74f57d3f108b4761c069a50f66ab18 Mon Sep 17 00:00:00 2001 From: Justin Wang Date: Tue, 10 Mar 2026 20:11:41 -0400 Subject: [PATCH 06/15] touch ups --- apps/backend/src/donationItems/donationItems.service.ts | 2 +- apps/backend/src/orders/order.controller.spec.ts | 2 +- apps/backend/src/orders/order.service.spec.ts | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/apps/backend/src/donationItems/donationItems.service.ts b/apps/backend/src/donationItems/donationItems.service.ts index 131fd3637..2b086b46f 100644 --- a/apps/backend/src/donationItems/donationItems.service.ts +++ b/apps/backend/src/donationItems/donationItems.service.ts @@ -44,7 +44,7 @@ export class DonationItemsService { const donations = items.map((i) => i.donation); - // Assure no duplicates + // Ensure no duplicates return Array.from( new Map(donations.map((d) => [d.donationId, d])).values(), ); diff --git a/apps/backend/src/orders/order.controller.spec.ts b/apps/backend/src/orders/order.controller.spec.ts index 8de9a4799..a6ed73ed0 100644 --- a/apps/backend/src/orders/order.controller.spec.ts +++ b/apps/backend/src/orders/order.controller.spec.ts @@ -12,7 +12,7 @@ import { AWSS3Service } from '../aws/aws-s3.service'; import { TrackingCostDto } from './dtos/tracking-cost.dto'; import { OrderDetailsDto } from './dtos/order-details.dto'; import { FoodType } from '../donationItems/types'; -import { BadRequestException } from '@nestjs/common'; +import { BadRequestException, NotFoundException } from '@nestjs/common'; import { FoodManufacturer } from '../foodManufacturers/manufacturers.entity'; import { FoodRequestSummaryDto } from '../foodRequests/dtos/food-request-summary.dto'; import { ConfirmDeliveryDto } from './dtos/confirm-delivery.dto'; diff --git a/apps/backend/src/orders/order.service.spec.ts b/apps/backend/src/orders/order.service.spec.ts index 0b7cc1893..4d0ed9c33 100644 --- a/apps/backend/src/orders/order.service.spec.ts +++ b/apps/backend/src/orders/order.service.spec.ts @@ -21,7 +21,7 @@ import { UsersService } from '../users/users.service'; import { DonationItem } from '../donationItems/donationItems.entity'; import { Donation } from '../donations/donations.entity'; import { Allocation } from '../allocations/allocations.entity'; -import { User } from '../users/user.entity'; +import { User } from '../users/users.entity'; import { AuthService } from '../auth/auth.service'; import { DonationService } from '../donations/donations.service'; import { CreateOrderDto } from './dtos/create-order.dto'; From 3508e2a18592d99b93a57ff19a1d571f12e9b4bd Mon Sep 17 00:00:00 2001 From: Justin Wang Date: Wed, 11 Mar 2026 21:52:38 -0400 Subject: [PATCH 07/15] comments --- .../src/allocations/allocations.service.ts | 24 +++++------ .../donationItems.controller.spec.ts | 23 +++++----- .../donationItems/donationItems.controller.ts | 13 +++--- .../donationItems/donationItems.service.ts | 8 ++-- .../donations/donations.controller.spec.ts | 6 +-- .../src/donations/donations.controller.ts | 6 ++- .../src/donations/donations.service.spec.ts | 13 +++++- .../src/donations/donations.service.ts | 6 ++- .../src/orders/order.controller.spec.ts | 42 ------------------- apps/backend/src/orders/order.controller.ts | 2 +- apps/backend/src/orders/order.module.ts | 4 +- apps/backend/src/orders/order.service.spec.ts | 20 --------- apps/backend/src/orders/order.service.ts | 24 +++-------- 13 files changed, 63 insertions(+), 128 deletions(-) diff --git a/apps/backend/src/allocations/allocations.service.ts b/apps/backend/src/allocations/allocations.service.ts index 0ec386fac..36ae0d33e 100644 --- a/apps/backend/src/allocations/allocations.service.ts +++ b/apps/backend/src/allocations/allocations.service.ts @@ -32,21 +32,19 @@ export class AllocationsService { validateId(orderId, 'Order'); - const allocations: Allocation[] = []; + const allocations = Object.entries(donationItems).map( + ([itemIdStr, quantity]) => { + const itemId = Number(itemIdStr); - for (const [itemIdStr, quantity] of Object.entries(donationItems)) { - const itemId = Number(itemIdStr); + validateId(itemId, 'Item'); - validateId(itemId, 'Item'); - - const allocation = this.repo.create({ - orderId, - itemId, - allocatedQuantity: quantity, - }); - - allocations.push(allocation); - } + return this.repo.create({ + orderId, + itemId, + allocatedQuantity: quantity, + }); + }, + ); return await this.repo.save(allocations); } diff --git a/apps/backend/src/donationItems/donationItems.controller.spec.ts b/apps/backend/src/donationItems/donationItems.controller.spec.ts index e864783c9..1ba2a7c6f 100644 --- a/apps/backend/src/donationItems/donationItems.controller.spec.ts +++ b/apps/backend/src/donationItems/donationItems.controller.spec.ts @@ -93,41 +93,38 @@ describe('DonationItemsController', () => { }); }); - describe('getAllDonationItems', () => { - it('should call service.getAll with donationItemIds and return donation items', async () => { + describe('getByIds', () => { + it('should call service.getByIds with donationItemIds and return donation items', async () => { const donationItemIds = [1, 2]; const mockItems = [ { itemId: 1, itemName: 'Rice' }, { itemId: 2, itemName: 'Beans' }, ] as Partial[]; - mockDonationItemsService.getAll.mockResolvedValue( + mockDonationItemsService.getByIds.mockResolvedValue( mockItems as DonationItem[], ); - const result = await controller.getAllDonationItems(donationItemIds); + const result = await controller.getByIds(donationItemIds); - expect(mockDonationItemsService.getAll).toHaveBeenCalledWith( + expect(mockDonationItemsService.getByIds).toHaveBeenCalledWith( donationItemIds, ); expect(result).toEqual(mockItems); }); }); - describe('setDonationItemQuantities', () => { - it('should call service.setDonationItemQuantities with the body', async () => { + describe('setReservedQuantities', () => { + it('should call service.setReservedQuantities with the body', async () => { const body: Record = { 1: 10, 2: 20 }; - mockDonationItemsService.setDonationItemQuantities.mockResolvedValue( - undefined, - ); + mockDonationItemsService.setReservedQuantities.mockResolvedValue(); - const result = await controller.setDonationItemQuantities(body); + await controller.setReservedQuantities(body); expect( - mockDonationItemsService.setDonationItemQuantities, + mockDonationItemsService.setReservedQuantities, ).toHaveBeenCalledWith(body); - expect(result).toBeUndefined(); }); }); }); diff --git a/apps/backend/src/donationItems/donationItems.controller.ts b/apps/backend/src/donationItems/donationItems.controller.ts index e8058bbb7..d2b2d0b30 100644 --- a/apps/backend/src/donationItems/donationItems.controller.ts +++ b/apps/backend/src/donationItems/donationItems.controller.ts @@ -7,6 +7,7 @@ import { Patch, UseGuards, ParseIntPipe, + Query, } from '@nestjs/common'; import { ApiBody } from '@nestjs/swagger'; import { DonationItemsService } from './donationItems.service'; @@ -30,16 +31,16 @@ export class DonationItemsController { @Get('/associated-donations') async getDonationsFromDonationItemIds( - @Body() donationItemIds: number[], + @Query('donationItemIds') donationItemIds: number[], ): Promise { return this.donationItemsService.getAssociatedDonations(donationItemIds); } @Get('/all') - async getAllDonationItems( - @Body() donationItemIds: number[], + async getByIds( + @Query('donationItemIds') donationItemIds: number[], ): Promise { - return this.donationItemsService.getAll(donationItemIds); + return this.donationItemsService.getByIds(donationItemIds); } @Post('/create-multiple') @@ -90,9 +91,9 @@ export class DonationItemsController { } @Patch('/set-quantities') - async setDonationItemQuantities( + async setReservedQuantities( @Body() body: Record, ): Promise { - return this.donationItemsService.setDonationItemQuantities(body); + return this.donationItemsService.setReservedQuantities(body); } } diff --git a/apps/backend/src/donationItems/donationItems.service.ts b/apps/backend/src/donationItems/donationItems.service.ts index 2b086b46f..e8c44bbc3 100644 --- a/apps/backend/src/donationItems/donationItems.service.ts +++ b/apps/backend/src/donationItems/donationItems.service.ts @@ -28,7 +28,7 @@ export class DonationItemsService { return this.repo.find({ where: { donation: { donationId } } }); } - async getAll(donationItemIds: number[]): Promise { + async getByIds(donationItemIds: number[]): Promise { return this.repo.find({ where: { itemId: In(donationItemIds), @@ -118,10 +118,8 @@ export class DonationItemsService { return this.repo.save(donationItem); } - async setDonationItemQuantities( - donationItems: Record, - ): Promise { - for (const [itemId, quantity] of Object.entries(donationItems)) { + async setReservedQuantities(body: Record): Promise { + for (const [itemId, quantity] of Object.entries(body)) { const id = Number(itemId); validateId(id, 'Item'); diff --git a/apps/backend/src/donations/donations.controller.spec.ts b/apps/backend/src/donations/donations.controller.spec.ts index 9cc8d0553..bbbfa9a20 100644 --- a/apps/backend/src/donations/donations.controller.spec.ts +++ b/apps/backend/src/donations/donations.controller.spec.ts @@ -57,13 +57,11 @@ describe('DonationsController', () => { it('should call donationService.matchAll with donationIds', async () => { const donationIds = [1, 2, 3]; - mockDonationService.matchAll.mockResolvedValue(undefined); + mockDonationService.matchAll.mockResolvedValue(); - const result = await controller.matchAllDonations(donationIds); + await controller.matchAllDonations({ donationIds }); expect(mockDonationService.matchAll).toHaveBeenCalledWith(donationIds); - - expect(result).toBeUndefined(); }); }); }); diff --git a/apps/backend/src/donations/donations.controller.ts b/apps/backend/src/donations/donations.controller.ts index 8369addeb..a5e2e72b0 100644 --- a/apps/backend/src/donations/donations.controller.ts +++ b/apps/backend/src/donations/donations.controller.ts @@ -87,7 +87,9 @@ export class DonationsController { } @Patch('/match-all') - async matchAllDonations(@Body() donationIds: number[]): Promise { - await this.donationService.matchAll(donationIds); + async matchAllDonations( + @Body() body: { donationIds: number[] }, + ): Promise { + await this.donationService.matchAll(body.donationIds); } } diff --git a/apps/backend/src/donations/donations.service.spec.ts b/apps/backend/src/donations/donations.service.spec.ts index 6b98e30cb..d15796241 100644 --- a/apps/backend/src/donations/donations.service.spec.ts +++ b/apps/backend/src/donations/donations.service.spec.ts @@ -137,7 +137,7 @@ describe('DonationService', () => { }); describe('matchAll', () => { - it('should call repo.update with donationIds and set status MATCHED', async () => { + it('updates all given donations to have status MATCHED', async () => { const donationId1 = 1; const donationId2 = 2; const donationIds = [donationId1, donationId2]; @@ -155,6 +155,17 @@ describe('DonationService', () => { expect(updatedDonation1.status).toEqual(DonationStatus.MATCHED); expect(updatedDonation2.status).toEqual(DonationStatus.MATCHED); }); + + it('throws an error if one or more donationIds do not exist', async () => { + const existingDonationId = 1; + const nonExistingDonationId = 999; + + const donationIds = [existingDonationId, nonExistingDonationId]; + + await expect(service.matchAll(donationIds)).rejects.toThrow( + 'One or more donationIds do not exist', + ); + }); }); describe('handleRecurringDonations', () => { diff --git a/apps/backend/src/donations/donations.service.ts b/apps/backend/src/donations/donations.service.ts index d71b1f4ab..ae010e735 100644 --- a/apps/backend/src/donations/donations.service.ts +++ b/apps/backend/src/donations/donations.service.ts @@ -102,10 +102,14 @@ export class DonationService { } async matchAll(donationIds: number[]): Promise { - await this.repo.update( + const result = await this.repo.update( { donationId: In(donationIds) }, { status: DonationStatus.MATCHED }, ); + + if (result.affected !== donationIds.length) { + throw new NotFoundException('One or more donationIds do not exist'); + } } async handleRecurringDonations(): Promise { diff --git a/apps/backend/src/orders/order.controller.spec.ts b/apps/backend/src/orders/order.controller.spec.ts index a6ed73ed0..3e51dd3a3 100644 --- a/apps/backend/src/orders/order.controller.spec.ts +++ b/apps/backend/src/orders/order.controller.spec.ts @@ -416,48 +416,6 @@ describe('OrdersController', () => { expect(result).toEqual(mockCreatedOrder); }); - it('should propagate NotFoundException when request not found', async () => { - const foodRequestId = 999; - - const createOrderDto: CreateOrderDto = { - foodRequestId: foodRequestId, - manufacturerId: 1, - donationItems: { 5: 10 }, - }; - - mockOrdersService.create.mockRejectedValueOnce( - new NotFoundException(`Request ${foodRequestId} not found`), - ); - - const promise = controller.createOrder(createOrderDto); - await expect(promise).rejects.toBeInstanceOf(NotFoundException); - await expect(promise).rejects.toThrow( - `Request ${foodRequestId} not found`, - ); - expect(mockOrdersService.create).toHaveBeenCalledWith(createOrderDto); - }); - - it('should propagate NotFoundException when manufacturer not found', async () => { - const manufacturerId = 999; - - const createOrderDto: CreateOrderDto = { - foodRequestId: 1, - manufacturerId: manufacturerId, - donationItems: { 5: 10 }, - }; - - mockOrdersService.create.mockRejectedValueOnce( - new NotFoundException(`Food Manufacturer ${manufacturerId} not found`), - ); - - const promise = controller.createOrder(createOrderDto); - await expect(promise).rejects.toBeInstanceOf(NotFoundException); - await expect(promise).rejects.toThrow( - `Food Manufacturer ${manufacturerId} not found`, - ); - expect(mockOrdersService.create).toHaveBeenCalledWith(createOrderDto); - }); - it('should propagate BadRequestException when request is not active', async () => { const foodRequestId = 1; diff --git a/apps/backend/src/orders/order.controller.ts b/apps/backend/src/orders/order.controller.ts index 9d33f9c43..45b40a21e 100644 --- a/apps/backend/src/orders/order.controller.ts +++ b/apps/backend/src/orders/order.controller.ts @@ -109,7 +109,7 @@ export class OrdersController { schema: { type: 'object', properties: { - requestId: { type: 'integer', example: 1 }, + foodRequestId: { type: 'integer', example: 1 }, manufacturerId: { type: 'integer', example: 1 }, donationItems: { type: 'object', diff --git a/apps/backend/src/orders/order.module.ts b/apps/backend/src/orders/order.module.ts index 34f799666..611f91cfe 100644 --- a/apps/backend/src/orders/order.module.ts +++ b/apps/backend/src/orders/order.module.ts @@ -15,8 +15,8 @@ import { DonationItem } from '../donationItems/donationItems.entity'; import { ManufacturerModule } from '../foodManufacturers/manufacturers.module'; import { DonationItemsModule } from '../donationItems/donationItems.module'; import { Allocation } from '../allocations/allocations.entity'; -import { DonationService } from '../donations/donations.service'; import { DonationModule } from '../donations/donations.module'; +import { Donation } from '../donations/donations.entity'; @Module({ imports: [ @@ -27,7 +27,7 @@ import { DonationModule } from '../donations/donations.module'; FoodManufacturer, DonationItem, Allocation, - DonationService, + Donation, ]), AllocationModule, forwardRef(() => AuthModule), diff --git a/apps/backend/src/orders/order.service.spec.ts b/apps/backend/src/orders/order.service.spec.ts index 4d0ed9c33..b31d6b326 100644 --- a/apps/backend/src/orders/order.service.spec.ts +++ b/apps/backend/src/orders/order.service.spec.ts @@ -740,26 +740,6 @@ describe('OrdersService', () => { expect(matchedDonation?.status).toBe(DonationStatus.MATCHED); }); - it('should throw NotFoundException if request does not exist', async () => { - validCreateOrderDto.foodRequestId = 999; - await expect(service.create(validCreateOrderDto)).rejects.toThrow( - NotFoundException, - ); - await expect(service.create(validCreateOrderDto)).rejects.toThrow( - `Request ${validCreateOrderDto.foodRequestId} not found`, - ); - }); - - it('should throw NotFoundException if manufacturer does not exist', async () => { - validCreateOrderDto.manufacturerId = 999; - await expect(service.create(validCreateOrderDto)).rejects.toThrow( - NotFoundException, - ); - await expect(service.create(validCreateOrderDto)).rejects.toThrow( - `Food Manufacturer ${validCreateOrderDto.manufacturerId} not found`, - ); - }); - it('should throw BadRequestException if request is not active', async () => { const requestRepo = testDataSource.getRepository(FoodRequest); diff --git a/apps/backend/src/orders/order.service.ts b/apps/backend/src/orders/order.service.ts index 84838bc45..d59d33edd 100644 --- a/apps/backend/src/orders/order.service.ts +++ b/apps/backend/src/orders/order.service.ts @@ -91,30 +91,18 @@ export class OrdersService { const request = await this.requestsService.findOne(requestId); - if (!request) { - throw new NotFoundException(`Request ${requestId} not found`); - } - if (request.status != FoodRequestStatus.ACTIVE) { throw new BadRequestException(`Request ${requestId} is not active`); } - const manufacturer = await this.manufacturerRepo.findOne({ - where: { foodManufacturerId: manufacturerId }, - relations: ['donations'], - }); - - if (!manufacturer) { - throw new NotFoundException( - `Food Manufacturer ${manufacturerId} not found`, - ); - } - - const fmDonationIds = manufacturer.donations.map((d) => d.donationId); + const fmDonations = await this.manufacturerService.getFMDonations( + manufacturerId, + ); + const fmDonationIds = fmDonations.map((d) => d.donationId); const fmDonationSet = new Set(fmDonationIds); const donationItemIds = Object.keys(orderData.donationItems).map(Number); - const donationItems = await this.donationItemsService.getAll( + const donationItems = await this.donationItemsService.getByIds( donationItemIds, ); @@ -167,7 +155,7 @@ export class OrdersService { donationItems: orderData.donationItems, }); - await this.donationItemsService.setDonationItemQuantities( + await this.donationItemsService.setReservedQuantities( orderData.donationItems, ); From a9e177fb8824196d90c2506730260ae35d6d41b4 Mon Sep 17 00:00:00 2001 From: Justin Wang Date: Thu, 12 Mar 2026 10:40:06 -0400 Subject: [PATCH 08/15] comment --- apps/backend/src/allocations/allocations.service.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/backend/src/allocations/allocations.service.ts b/apps/backend/src/allocations/allocations.service.ts index 36ae0d33e..faecc2eab 100644 --- a/apps/backend/src/allocations/allocations.service.ts +++ b/apps/backend/src/allocations/allocations.service.ts @@ -36,7 +36,7 @@ export class AllocationsService { ([itemIdStr, quantity]) => { const itemId = Number(itemIdStr); - validateId(itemId, 'Item'); + validateId(itemId, 'Donation Item'); return this.repo.create({ orderId, From 10f7b385546bc043970e13d6d6b3c8685df38ea4 Mon Sep 17 00:00:00 2001 From: Justin Wang Date: Fri, 13 Mar 2026 10:57:46 -0400 Subject: [PATCH 09/15] comments --- .../src/allocations/allocations.service.ts | 4 +-- .../dtos/create-allocations.dto.ts | 2 +- .../donationItems/donationItems.controller.ts | 16 ++++++++-- .../src/donations/donations.service.spec.ts | 2 +- .../src/donations/donations.service.ts | 21 +++++++++---- .../src/orders/dtos/create-order.dto.ts | 2 +- .../src/orders/order.controller.spec.ts | 10 +++---- apps/backend/src/orders/order.module.ts | 1 - apps/backend/src/orders/order.service.spec.ts | 22 +++++++++++--- apps/backend/src/orders/order.service.ts | 30 +++++++++---------- 10 files changed, 72 insertions(+), 38 deletions(-) diff --git a/apps/backend/src/allocations/allocations.service.ts b/apps/backend/src/allocations/allocations.service.ts index faecc2eab..6c98f8d14 100644 --- a/apps/backend/src/allocations/allocations.service.ts +++ b/apps/backend/src/allocations/allocations.service.ts @@ -28,7 +28,7 @@ export class AllocationsService { body: CreateMultipleAllocationsDto, ): Promise { const orderId = body.orderId; - const donationItems = body.donationItems; + const donationItems = body.itemAllocations; validateId(orderId, 'Order'); @@ -46,6 +46,6 @@ export class AllocationsService { }, ); - return await this.repo.save(allocations); + return this.repo.save(allocations); } } diff --git a/apps/backend/src/allocations/dtos/create-allocations.dto.ts b/apps/backend/src/allocations/dtos/create-allocations.dto.ts index 1b09d30ed..562ac4454 100644 --- a/apps/backend/src/allocations/dtos/create-allocations.dto.ts +++ b/apps/backend/src/allocations/dtos/create-allocations.dto.ts @@ -5,5 +5,5 @@ export class CreateMultipleAllocationsDto { orderId!: number; @IsObject() - donationItems!: Record; + itemAllocations!: Record; } diff --git a/apps/backend/src/donationItems/donationItems.controller.ts b/apps/backend/src/donationItems/donationItems.controller.ts index d2b2d0b30..29d2722d4 100644 --- a/apps/backend/src/donationItems/donationItems.controller.ts +++ b/apps/backend/src/donationItems/donationItems.controller.ts @@ -8,6 +8,7 @@ import { UseGuards, ParseIntPipe, Query, + ParseArrayPipe, } from '@nestjs/common'; import { ApiBody } from '@nestjs/swagger'; import { DonationItemsService } from './donationItems.service'; @@ -29,16 +30,25 @@ export class DonationItemsController { return this.donationItemsService.getAllDonationItems(donationId); } + // Called like: /donation-items/associated-donations?donationItemIds=1,2,3 @Get('/associated-donations') async getDonationsFromDonationItemIds( - @Query('donationItemIds') donationItemIds: number[], + @Query( + 'donationItemIds', + new ParseArrayPipe({ items: Number, separator: ',' }), + ) + donationItemIds: number[], ): Promise { return this.donationItemsService.getAssociatedDonations(donationItemIds); } - @Get('/all') + @Get('/getByIds') async getByIds( - @Query('donationItemIds') donationItemIds: number[], + @Query( + 'donationItemIds', + new ParseArrayPipe({ items: Number, separator: ',' }), + ) + donationItemIds: number[], ): Promise { return this.donationItemsService.getByIds(donationItemIds); } diff --git a/apps/backend/src/donations/donations.service.spec.ts b/apps/backend/src/donations/donations.service.spec.ts index d15796241..446349fc8 100644 --- a/apps/backend/src/donations/donations.service.spec.ts +++ b/apps/backend/src/donations/donations.service.spec.ts @@ -163,7 +163,7 @@ describe('DonationService', () => { const donationIds = [existingDonationId, nonExistingDonationId]; await expect(service.matchAll(donationIds)).rejects.toThrow( - 'One or more donationIds do not exist', + `Donations not found for ids: ${nonExistingDonationId}`, ); }); }); diff --git a/apps/backend/src/donations/donations.service.ts b/apps/backend/src/donations/donations.service.ts index ae010e735..c4de821c7 100644 --- a/apps/backend/src/donations/donations.service.ts +++ b/apps/backend/src/donations/donations.service.ts @@ -102,14 +102,25 @@ export class DonationService { } async matchAll(donationIds: number[]): Promise { - const result = await this.repo.update( + const donations = await this.repo.find({ + where: { donationId: In(donationIds) }, + select: ['donationId'], + }); + + const foundIds = donations.map((d) => d.donationId); + + const missingIds = donationIds.filter((id) => !foundIds.includes(id)); + + if (missingIds.length > 0) { + throw new NotFoundException( + `Donations not found for ids: ${missingIds.join(', ')}`, + ); + } + + await this.repo.update( { donationId: In(donationIds) }, { status: DonationStatus.MATCHED }, ); - - if (result.affected !== donationIds.length) { - throw new NotFoundException('One or more donationIds do not exist'); - } } async handleRecurringDonations(): Promise { diff --git a/apps/backend/src/orders/dtos/create-order.dto.ts b/apps/backend/src/orders/dtos/create-order.dto.ts index a38217cff..b1dc32720 100644 --- a/apps/backend/src/orders/dtos/create-order.dto.ts +++ b/apps/backend/src/orders/dtos/create-order.dto.ts @@ -8,5 +8,5 @@ export class CreateOrderDto { manufacturerId!: number; @IsObject() - donationItems!: Record; + itemAllocations!: Record; } diff --git a/apps/backend/src/orders/order.controller.spec.ts b/apps/backend/src/orders/order.controller.spec.ts index 3e51dd3a3..a35eaa95a 100644 --- a/apps/backend/src/orders/order.controller.spec.ts +++ b/apps/backend/src/orders/order.controller.spec.ts @@ -12,7 +12,7 @@ import { AWSS3Service } from '../aws/aws-s3.service'; import { TrackingCostDto } from './dtos/tracking-cost.dto'; import { OrderDetailsDto } from './dtos/order-details.dto'; import { FoodType } from '../donationItems/types'; -import { BadRequestException, NotFoundException } from '@nestjs/common'; +import { BadRequestException } from '@nestjs/common'; import { FoodManufacturer } from '../foodManufacturers/manufacturers.entity'; import { FoodRequestSummaryDto } from '../foodRequests/dtos/food-request-summary.dto'; import { ConfirmDeliveryDto } from './dtos/confirm-delivery.dto'; @@ -394,7 +394,7 @@ describe('OrdersController', () => { const createOrderDto = { foodRequestId: 1, manufacturerId: 1, - donationItems: { + itemAllocations: { 5: 10, 8: 3, 12: 7, @@ -422,7 +422,7 @@ describe('OrdersController', () => { const createOrderDto: CreateOrderDto = { foodRequestId: foodRequestId, manufacturerId: 1, - donationItems: { 5: 10 }, + itemAllocations: { 5: 10 }, }; mockOrdersService.create.mockRejectedValueOnce( @@ -441,7 +441,7 @@ describe('OrdersController', () => { const createOrderDto: CreateOrderDto = { foodRequestId: 1, manufacturerId: 1, - donationItems: { 5: 10 }, + itemAllocations: { 5: 10 }, }; mockOrdersService.create.mockRejectedValueOnce( @@ -464,7 +464,7 @@ describe('OrdersController', () => { const createOrderDto: CreateOrderDto = { foodRequestId: 1, manufacturerId: 1, - donationItems: { [donationItemId]: 100 }, + itemAllocations: { [donationItemId]: 100 }, }; mockOrdersService.create.mockRejectedValueOnce( diff --git a/apps/backend/src/orders/order.module.ts b/apps/backend/src/orders/order.module.ts index 611f91cfe..71003cc7e 100644 --- a/apps/backend/src/orders/order.module.ts +++ b/apps/backend/src/orders/order.module.ts @@ -36,7 +36,6 @@ import { Donation } from '../donations/donations.entity'; forwardRef(() => RequestsModule), ManufacturerModule, DonationItemsModule, - AllocationModule, DonationModule, ], controllers: [OrdersController], diff --git a/apps/backend/src/orders/order.service.spec.ts b/apps/backend/src/orders/order.service.spec.ts index 77f340a46..1f0250094 100644 --- a/apps/backend/src/orders/order.service.spec.ts +++ b/apps/backend/src/orders/order.service.spec.ts @@ -692,7 +692,7 @@ describe('OrdersService', () => { validCreateOrderDto = { foodRequestId: 1, manufacturerId: 1, - donationItems: { + itemAllocations: { 1: 10, 2: 3, }, @@ -729,7 +729,7 @@ describe('OrdersService', () => { where: { orderId: createdOrder.orderId }, }); expect(allocations.length).toBe( - Object.keys(validCreateOrderDto.donationItems).length, + Object.keys(validCreateOrderDto.itemAllocations).length, ); expect(allocations.map((a) => a.itemId)).toEqual( expect.arrayContaining([1, 2]), @@ -777,7 +777,7 @@ describe('OrdersService', () => { it('should throw BadRequestException if allocated quantity exceeds remaining', async () => { const donationItemId = 2; - validCreateOrderDto.donationItems = { [donationItemId]: 500 }; + validCreateOrderDto.itemAllocations = { [donationItemId]: 500 }; await expect(service.create(validCreateOrderDto)).rejects.toThrow( BadRequestException, ); @@ -787,10 +787,24 @@ describe('OrdersService', () => { }); it('should throw Error if donation is not associated with manufacturer', async () => { - validCreateOrderDto.donationItems = { 7: 2 }; + validCreateOrderDto.itemAllocations = { 7: 2 }; await expect(service.create(validCreateOrderDto)).rejects.toThrow( BadRequestException, ); + await expect(service.create(validCreateOrderDto)).rejects.toThrow( + `Donation is not associated with the current food manufacturer`, + ); + }); + + it('should throw Error if donation item not found', async () => { + const invalidDonationItemId = 999; + validCreateOrderDto.itemAllocations = { [invalidDonationItemId]: 2 }; + await expect(service.create(validCreateOrderDto)).rejects.toThrow( + NotFoundException, + ); + await expect(service.create(validCreateOrderDto)).rejects.toThrow( + `Couldn't find donation item ${invalidDonationItemId}`, + ); }); }); }); diff --git a/apps/backend/src/orders/order.service.ts b/apps/backend/src/orders/order.service.ts index cadf7398d..c64a5ea66 100644 --- a/apps/backend/src/orders/order.service.ts +++ b/apps/backend/src/orders/order.service.ts @@ -27,8 +27,6 @@ export class OrdersService { constructor( @InjectRepository(Order) private repo: Repository, @InjectRepository(Pantry) private pantryRepo: Repository, - @InjectRepository(FoodManufacturer) - private manufacturerRepo: Repository, private requestsService: RequestsService, private manufacturerService: FoodManufacturersService, private donationItemsService: DonationItemsService, @@ -98,10 +96,9 @@ export class OrdersService { const fmDonations = await this.manufacturerService.getFMDonations( manufacturerId, ); - const fmDonationIds = fmDonations.map((d) => d.donationId); - const fmDonationSet = new Set(fmDonationIds); + const fmDonationSet = new Set(fmDonations.map((d) => d.donationId)); - const donationItemIds = Object.keys(orderData.donationItems).map(Number); + const donationItemIds = Object.keys(orderData.itemAllocations).map(Number); const donationItems = await this.donationItemsService.getByIds( donationItemIds, ); @@ -109,12 +106,10 @@ export class OrdersService { // All donations associated with the given donation items const associatedDonations = await this.donationItemsService.getAssociatedDonations(donationItemIds); - const associatedDonationDonationIds = associatedDonations.map( - (d) => d.donationId, - ); + const associatedDonationIds = associatedDonations.map((d) => d.donationId); // True if there is an associated donation that does not belong to the current FM - const invalidDonation = associatedDonationDonationIds.find( + const invalidDonation = associatedDonationIds.find( (id) => !fmDonationSet.has(id), ); @@ -124,9 +119,11 @@ export class OrdersService { ); } - for (const [itemId, quantity] of Object.entries(orderData.donationItems)) { + for (const [itemId, quantity] of Object.entries( + orderData.itemAllocations, + )) { const id = Number(itemId); - const count = Number(quantity); + const allocatedQuantity = Number(quantity); validateId(id, 'Donation Item'); const donationItem = donationItems.find((d) => d.itemId === id); @@ -135,7 +132,10 @@ export class OrdersService { throw new NotFoundException(`Couldn't find donation item ${id} `); } - if (count > donationItem.quantity - donationItem.reservedQuantity) { + if ( + allocatedQuantity > + donationItem.quantity - donationItem.reservedQuantity + ) { throw new BadRequestException( `Donation item ${id} allocated quantity exceeds remaining quantity`, ); @@ -152,14 +152,14 @@ export class OrdersService { await this.allocationsService.createMultiple({ orderId: savedOrder.orderId, - donationItems: orderData.donationItems, + itemAllocations: orderData.itemAllocations, }); await this.donationItemsService.setReservedQuantities( - orderData.donationItems, + orderData.itemAllocations, ); - await this.donationService.matchAll(associatedDonationDonationIds); + await this.donationService.matchAll(associatedDonationIds); return savedOrder; } From 5a14191aa2d9e73e578b36d641b4356a1e8cbf07 Mon Sep 17 00:00:00 2001 From: Justin Wang Date: Sun, 15 Mar 2026 12:15:40 -0400 Subject: [PATCH 10/15] comments --- .../src/allocations/allocations.controller.ts | 2 +- .../src/allocations/allocations.module.ts | 2 + .../src/allocations/allocations.service.ts | 34 ++-- .../donationItems.controller.spec.ts | 38 ++--- .../donationItems/donationItems.controller.ts | 17 +- .../donationItems/donationItems.service.ts | 55 ++++--- .../src/donations/donations.service.spec.ts | 2 +- .../src/donations/donations.service.ts | 17 +- .../src/orders/dtos/create-order.dto.ts | 5 +- apps/backend/src/orders/order.controller.ts | 17 +- apps/backend/src/orders/order.service.spec.ts | 23 ++- apps/backend/src/orders/order.service.ts | 146 ++++++++++-------- 12 files changed, 193 insertions(+), 165 deletions(-) diff --git a/apps/backend/src/allocations/allocations.controller.ts b/apps/backend/src/allocations/allocations.controller.ts index 45fe6941c..b04d8bfcb 100644 --- a/apps/backend/src/allocations/allocations.controller.ts +++ b/apps/backend/src/allocations/allocations.controller.ts @@ -19,7 +19,7 @@ export class AllocationsController { type: 'integer', example: 1, }, - donationItems: { + itemAllocations: { type: 'object', description: 'Map of donationItemId -> quantity', additionalProperties: { diff --git a/apps/backend/src/allocations/allocations.module.ts b/apps/backend/src/allocations/allocations.module.ts index 3284e1afd..2c9815f05 100644 --- a/apps/backend/src/allocations/allocations.module.ts +++ b/apps/backend/src/allocations/allocations.module.ts @@ -4,11 +4,13 @@ import { Allocation } from './allocations.entity'; import { AllocationsController } from './allocations.controller'; import { AllocationsService } from './allocations.service'; import { AuthModule } from '../auth/auth.module'; +import { DonationItemsModule } from '../donationItems/donationItems.module'; @Module({ imports: [ TypeOrmModule.forFeature([Allocation]), forwardRef(() => AuthModule), + DonationItemsModule, ], controllers: [AllocationsController], providers: [AllocationsService], diff --git a/apps/backend/src/allocations/allocations.service.ts b/apps/backend/src/allocations/allocations.service.ts index 6c98f8d14..b4235dcaa 100644 --- a/apps/backend/src/allocations/allocations.service.ts +++ b/apps/backend/src/allocations/allocations.service.ts @@ -1,14 +1,17 @@ import { Injectable } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; -import { Repository } from 'typeorm'; +import { EntityManager, Repository } from 'typeorm'; import { Allocation } from '../allocations/allocations.entity'; import { CreateMultipleAllocationsDto } from './dtos/create-allocations.dto'; import { validateId } from '../utils/validation.utils'; +import { DonationItem } from '../donationItems/donationItems.entity'; @Injectable() export class AllocationsService { constructor( @InjectRepository(Allocation) private repo: Repository, + @InjectRepository(DonationItem) + private donationItemRepo: Repository, ) {} async getAllAllocationsByOrder( @@ -26,26 +29,35 @@ export class AllocationsService { async createMultiple( body: CreateMultipleAllocationsDto, + manager?: EntityManager, ): Promise { + const repo = manager ? manager.getRepository(Allocation) : this.repo; + const itemRepo = manager + ? manager.getRepository(DonationItem) + : this.donationItemRepo; + const orderId = body.orderId; - const donationItems = body.itemAllocations; + const itemAllocations = body.itemAllocations; validateId(orderId, 'Order'); - const allocations = Object.entries(donationItems).map( - ([itemIdStr, quantity]) => { - const itemId = Number(itemIdStr); + const allocations: Allocation[] = []; - validateId(itemId, 'Donation Item'); + for (const [itemIdStr, quantity] of Object.entries(itemAllocations)) { + const itemId = Number(itemIdStr); + validateId(itemId, 'Donation Item'); - return this.repo.create({ + allocations.push( + repo.create({ orderId, itemId, allocatedQuantity: quantity, - }); - }, - ); + }), + ); + + await itemRepo.increment({ itemId }, 'reservedQuantity', quantity); + } - return this.repo.save(allocations); + return repo.save(allocations); } } diff --git a/apps/backend/src/donationItems/donationItems.controller.spec.ts b/apps/backend/src/donationItems/donationItems.controller.spec.ts index 1ba2a7c6f..d9df20127 100644 --- a/apps/backend/src/donationItems/donationItems.controller.spec.ts +++ b/apps/backend/src/donationItems/donationItems.controller.spec.ts @@ -5,8 +5,6 @@ import { DonationItem } from './donationItems.entity'; import { mock } from 'jest-mock-extended'; import { FoodType } from './types'; import { CreateMultipleDonationItemsDto } from './dtos/create-donation-items.dto'; -import { Donation } from '../donations/donations.entity'; -import { DonationStatus } from '../donations/types'; const mockDonationItemsService = mock(); @@ -70,26 +68,22 @@ describe('DonationItemsController', () => { }); }); - describe('getDonationsFromDonationItemIds', () => { - it('should call service.getAssociatedDonations with donationItemIds and return donations', async () => { + describe('getAssociatedDonationIds', () => { + it('should call service.getAssociatedDonationIds with donationItemIds and return donation id list', async () => { const donationItemIds = [1, 2, 3]; - const mockDonations = [ - { donationId: 1, status: DonationStatus.AVAILABLE }, - { donationId: 2, status: DonationStatus.FULFILLED }, - ] as Partial[]; + const mockDonationsids = [1, 2]; + const mockDonationsidsSet = new Set(mockDonationsids); - mockDonationItemsService.getAssociatedDonations.mockResolvedValue( - mockDonations as Donation[], + mockDonationItemsService.getAssociatedDonationIds.mockResolvedValue( + mockDonationsidsSet, ); - const result = await controller.getDonationsFromDonationItemIds( - donationItemIds, - ); + const result = await controller.getAssociatedDonationIds(donationItemIds); expect( - mockDonationItemsService.getAssociatedDonations, + mockDonationItemsService.getAssociatedDonationIds, ).toHaveBeenCalledWith(donationItemIds); - expect(result).toEqual(mockDonations); + expect(result).toEqual(mockDonationsidsSet); }); }); @@ -113,18 +107,4 @@ describe('DonationItemsController', () => { expect(result).toEqual(mockItems); }); }); - - describe('setReservedQuantities', () => { - it('should call service.setReservedQuantities with the body', async () => { - const body: Record = { 1: 10, 2: 20 }; - - mockDonationItemsService.setReservedQuantities.mockResolvedValue(); - - await controller.setReservedQuantities(body); - - expect( - mockDonationItemsService.setReservedQuantities, - ).toHaveBeenCalledWith(body); - }); - }); }); diff --git a/apps/backend/src/donationItems/donationItems.controller.ts b/apps/backend/src/donationItems/donationItems.controller.ts index 29d2722d4..a34f983ae 100644 --- a/apps/backend/src/donationItems/donationItems.controller.ts +++ b/apps/backend/src/donationItems/donationItems.controller.ts @@ -30,16 +30,16 @@ export class DonationItemsController { return this.donationItemsService.getAllDonationItems(donationId); } - // Called like: /donation-items/associated-donations?donationItemIds=1,2,3 - @Get('/associated-donations') - async getDonationsFromDonationItemIds( + // Called like: /donation-items/associated-donationIds?donationItemIds=1,2,3 + @Get('/associated-donationIds') + async getAssociatedDonationIds( @Query( 'donationItemIds', new ParseArrayPipe({ items: Number, separator: ',' }), ) donationItemIds: number[], - ): Promise { - return this.donationItemsService.getAssociatedDonations(donationItemIds); + ): Promise> { + return this.donationItemsService.getAssociatedDonationIds(donationItemIds); } @Get('/getByIds') @@ -99,11 +99,4 @@ export class DonationItemsController { ): Promise { return this.donationItemsService.updateDonationItemQuantity(itemId); } - - @Patch('/set-quantities') - async setReservedQuantities( - @Body() body: Record, - ): Promise { - return this.donationItemsService.setReservedQuantities(body); - } } diff --git a/apps/backend/src/donationItems/donationItems.service.ts b/apps/backend/src/donationItems/donationItems.service.ts index e8c44bbc3..4e36a8ff2 100644 --- a/apps/backend/src/donationItems/donationItems.service.ts +++ b/apps/backend/src/donationItems/donationItems.service.ts @@ -1,6 +1,6 @@ import { Injectable, NotFoundException } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; -import { In, Repository } from 'typeorm'; +import { EntityManager, In, Repository } from 'typeorm'; import { DonationItem } from './donationItems.entity'; import { validateId } from '../utils/validation.utils'; import { FoodType } from './types'; @@ -29,25 +29,46 @@ export class DonationItemsService { } async getByIds(donationItemIds: number[]): Promise { - return this.repo.find({ - where: { - itemId: In(donationItemIds), - }, + donationItemIds.forEach((id) => validateId(id, 'Donation Item')); + + const items = await this.repo.find({ + where: { itemId: In(donationItemIds) }, }); + + const foundIds = new Set(items.map((item) => item.itemId)); + + const missingIds = donationItemIds.filter((id) => !foundIds.has(id)); + + if (missingIds.length > 0) { + throw new NotFoundException( + `Donation items not found for ID(s): ${missingIds.join(', ')}`, + ); + } + + return items; } - async getAssociatedDonations(donationItemIds: number[]): Promise { + async getAssociatedDonationIds( + donationItemIds: number[], + ): Promise> { + donationItemIds.forEach((id) => validateId(id, 'Donation Item')); + const items = await this.repo.find({ where: { itemId: In(donationItemIds) }, - relations: ['donation'], + select: ['itemId', 'donationId'], }); - const donations = items.map((i) => i.donation); + const foundIds = new Set(items.map((i) => i.itemId)); - // Ensure no duplicates - return Array.from( - new Map(donations.map((d) => [d.donationId, d])).values(), - ); + const missingIds = donationItemIds.filter((id) => !foundIds.has(id)); + + if (missingIds.length > 0) { + throw new NotFoundException( + `Donation items not found for ID(s): ${missingIds.join(', ')}`, + ); + } + + return new Set(items.map((i) => i.donationId)); } async create( @@ -117,14 +138,4 @@ export class DonationItemsService { donationItem.quantity -= 1; return this.repo.save(donationItem); } - - async setReservedQuantities(body: Record): Promise { - for (const [itemId, quantity] of Object.entries(body)) { - const id = Number(itemId); - - validateId(id, 'Item'); - - await this.repo.increment({ itemId: id }, 'reservedQuantity', quantity); - } - } } diff --git a/apps/backend/src/donations/donations.service.spec.ts b/apps/backend/src/donations/donations.service.spec.ts index 446349fc8..a4909d71b 100644 --- a/apps/backend/src/donations/donations.service.spec.ts +++ b/apps/backend/src/donations/donations.service.spec.ts @@ -163,7 +163,7 @@ describe('DonationService', () => { const donationIds = [existingDonationId, nonExistingDonationId]; await expect(service.matchAll(donationIds)).rejects.toThrow( - `Donations not found for ids: ${nonExistingDonationId}`, + `Donations not found for ID(s): ${nonExistingDonationId}`, ); }); }); diff --git a/apps/backend/src/donations/donations.service.ts b/apps/backend/src/donations/donations.service.ts index c4de821c7..a3e00891b 100644 --- a/apps/backend/src/donations/donations.service.ts +++ b/apps/backend/src/donations/donations.service.ts @@ -5,7 +5,7 @@ import { NotFoundException, } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; -import { In, Repository } from 'typeorm'; +import { EntityManager, In, Repository } from 'typeorm'; import { Donation } from './donations.entity'; import { validateId } from '../utils/validation.utils'; import { DayOfWeek, DonationStatus, RecurrenceEnum } from './types'; @@ -101,8 +101,15 @@ export class DonationService { return this.repo.save(donation); } - async matchAll(donationIds: number[]): Promise { - const donations = await this.repo.find({ + async matchAll( + donationIds: number[], + manager?: EntityManager, + ): Promise { + donationIds.forEach((id) => validateId(id, 'Donation')); + + const repo = manager ? manager.getRepository(Donation) : this.repo; + + const donations = await repo.find({ where: { donationId: In(donationIds) }, select: ['donationId'], }); @@ -113,11 +120,11 @@ export class DonationService { if (missingIds.length > 0) { throw new NotFoundException( - `Donations not found for ids: ${missingIds.join(', ')}`, + `Donations not found for ID(s): ${missingIds.join(', ')}`, ); } - await this.repo.update( + await repo.update( { donationId: In(donationIds) }, { status: DonationStatus.MATCHED }, ); diff --git a/apps/backend/src/orders/dtos/create-order.dto.ts b/apps/backend/src/orders/dtos/create-order.dto.ts index b1dc32720..b822f463e 100644 --- a/apps/backend/src/orders/dtos/create-order.dto.ts +++ b/apps/backend/src/orders/dtos/create-order.dto.ts @@ -1,12 +1,15 @@ -import { IsNumber, IsObject } from 'class-validator'; +import { IsNotEmptyObject, IsNumber, IsObject, Min } from 'class-validator'; export class CreateOrderDto { @IsNumber() + @Min(1) foodRequestId!: number; + @Min(1) @IsNumber() manufacturerId!: number; @IsObject() + @IsNotEmptyObject() itemAllocations!: Record; } diff --git a/apps/backend/src/orders/order.controller.ts b/apps/backend/src/orders/order.controller.ts index 45b40a21e..bf4b018a1 100644 --- a/apps/backend/src/orders/order.controller.ts +++ b/apps/backend/src/orders/order.controller.ts @@ -109,11 +109,20 @@ export class OrdersController { schema: { type: 'object', properties: { - foodRequestId: { type: 'integer', example: 1 }, - manufacturerId: { type: 'integer', example: 1 }, - donationItems: { + foodRequestId: { + type: 'integer', + description: 'ID of the associated request this order is related to', + example: 1, + }, + manufacturerId: { + type: 'integer', + description: 'Food manufacturer ID of the FM fulfilling the order', + example: 1, + }, + itemAllocations: { type: 'object', - description: 'Map of donationItemId -> quantity', + description: + 'Map of donationItemId -> quantity to allocate, donation items and their quantity to allocate for this order', additionalProperties: { type: 'integer', example: 10, diff --git a/apps/backend/src/orders/order.service.spec.ts b/apps/backend/src/orders/order.service.spec.ts index 1f0250094..c98ea2bbb 100644 --- a/apps/backend/src/orders/order.service.spec.ts +++ b/apps/backend/src/orders/order.service.spec.ts @@ -26,6 +26,7 @@ import { AuthService } from '../auth/auth.service'; import { DonationService } from '../donations/donations.service'; import { CreateOrderDto } from './dtos/create-order.dto'; import { DonationStatus } from '../donations/types'; +import { DataSource } from 'typeorm'; // Set 1 minute timeout for async DB operations jest.setTimeout(60000); @@ -52,6 +53,10 @@ describe('OrdersService', () => { AllocationsService, UsersService, DonationService, + { + provide: DataSource, + useValue: testDataSource, + }, { provide: getRepositoryToken(Order), useValue: testDataSource.getRepository(Order), @@ -782,28 +787,18 @@ describe('OrdersService', () => { BadRequestException, ); await expect(service.create(validCreateOrderDto)).rejects.toThrow( - `Donation item ${donationItemId} allocated quantity exceeds remaining quantity`, + `Donation item ${donationItemId} quantity to allocate exceeds remaining quantity`, ); }); it('should throw Error if donation is not associated with manufacturer', async () => { - validCreateOrderDto.itemAllocations = { 7: 2 }; + const donationItemId = 7; + validCreateOrderDto.itemAllocations = { [donationItemId]: 2 }; await expect(service.create(validCreateOrderDto)).rejects.toThrow( BadRequestException, ); await expect(service.create(validCreateOrderDto)).rejects.toThrow( - `Donation is not associated with the current food manufacturer`, - ); - }); - - it('should throw Error if donation item not found', async () => { - const invalidDonationItemId = 999; - validCreateOrderDto.itemAllocations = { [invalidDonationItemId]: 2 }; - await expect(service.create(validCreateOrderDto)).rejects.toThrow( - NotFoundException, - ); - await expect(service.create(validCreateOrderDto)).rejects.toThrow( - `Couldn't find donation item ${invalidDonationItemId}`, + `The following donation items are not associated with the current food manufacturer: Donation item ID ${donationItemId} with Donation ID 3`, ); }); }); diff --git a/apps/backend/src/orders/order.service.ts b/apps/backend/src/orders/order.service.ts index c64a5ea66..ff6a6867d 100644 --- a/apps/backend/src/orders/order.service.ts +++ b/apps/backend/src/orders/order.service.ts @@ -3,8 +3,8 @@ import { Injectable, NotFoundException, } from '@nestjs/common'; -import { InjectRepository } from '@nestjs/typeorm'; -import { Repository, In } from 'typeorm'; +import { InjectDataSource, InjectRepository } from '@nestjs/typeorm'; +import { Repository, In, DataSource } from 'typeorm'; import { Order } from './order.entity'; import { Pantry } from '../pantries/pantries.entity'; import { FoodManufacturer } from '../foodManufacturers/manufacturers.entity'; @@ -21,6 +21,7 @@ import { FoodManufacturersService } from '../foodManufacturers/manufacturers.ser import { DonationItemsService } from '../donationItems/donationItems.service'; import { AllocationsService } from '../allocations/allocations.service'; import { DonationService } from '../donations/donations.service'; +import { DonationItem } from '../donationItems/donationItems.entity'; @Injectable() export class OrdersService { @@ -32,6 +33,7 @@ export class OrdersService { private donationItemsService: DonationItemsService, private allocationsService: AllocationsService, private donationService: DonationService, + @InjectDataSource() private dataSource: DataSource, ) {} // TODO: when order is created, set FM @@ -80,88 +82,102 @@ export class OrdersService { }); } + /* + This create method follows these high level steps: + 1. Validate the request status is active before allowing order creation. + 2. Ensure all donation items belong to the specified manufacturer. + 3. Validate allocated quantities do not exceed the remaining quantity (quantity - reserved_quantity). + 4. Create the order with status pending. + 5. Associate the order with the provided request and manufacturer. + 6. Create allocation records for each donation item included in the order. + 7. Update the reserved quantity for each allocated donation item. + 8. Identify all unique donations associated with the allocated donation items and set their status to matched. + */ async create(orderData: CreateOrderDto): Promise { - const requestId = orderData.foodRequestId; - const manufacturerId = orderData.manufacturerId; + return this.dataSource.transaction(async (manager) => { + const requestId = orderData.foodRequestId; + const manufacturerId = orderData.manufacturerId; + const itemAllocations = orderData.itemAllocations; - validateId(manufacturerId, 'Food Manufacturer'); - validateId(requestId, 'Request'); - - const request = await this.requestsService.findOne(requestId); - - if (request.status != FoodRequestStatus.ACTIVE) { - throw new BadRequestException(`Request ${requestId} is not active`); - } + validateId(manufacturerId, 'Food Manufacturer'); + validateId(requestId, 'Request'); - const fmDonations = await this.manufacturerService.getFMDonations( - manufacturerId, - ); - const fmDonationSet = new Set(fmDonations.map((d) => d.donationId)); + const request = await this.requestsService.findOne(requestId); - const donationItemIds = Object.keys(orderData.itemAllocations).map(Number); - const donationItems = await this.donationItemsService.getByIds( - donationItemIds, - ); + if (request.status !== FoodRequestStatus.ACTIVE) { + throw new BadRequestException(`Request ${requestId} is not active`); + } - // All donations associated with the given donation items - const associatedDonations = - await this.donationItemsService.getAssociatedDonations(donationItemIds); - const associatedDonationIds = associatedDonations.map((d) => d.donationId); + const fmDonations = await this.manufacturerService.getFMDonations( + manufacturerId, + ); + const fmDonationIdSet = new Set(fmDonations.map((d) => d.donationId)); - // True if there is an associated donation that does not belong to the current FM - const invalidDonation = associatedDonationIds.find( - (id) => !fmDonationSet.has(id), - ); + const donationItemIds = Object.keys(itemAllocations).map(Number); + const donationItems = await this.donationItemsService.getByIds( + donationItemIds, + ); - if (invalidDonation) { - throw new BadRequestException( - `Donation is not associated with the current food manufacturer`, + const invalidItems = donationItems.filter( + (item) => !fmDonationIdSet.has(item.donationId), ); - } - for (const [itemId, quantity] of Object.entries( - orderData.itemAllocations, - )) { - const id = Number(itemId); - const allocatedQuantity = Number(quantity); - validateId(id, 'Donation Item'); + if (invalidItems.length > 0) { + const messages = invalidItems.map( + (item) => + `Donation item ID ${item.itemId} with Donation ID ${item.donationId}`, + ); + throw new BadRequestException( + `The following donation items are not associated with the current food manufacturer: ${messages.join( + ', ', + )}`, + ); + } - const donationItem = donationItems.find((d) => d.itemId === id); + for (const donationItem of donationItems) { + const id = donationItem.itemId; + const quantityToAllocate = itemAllocations[id]; - if (!donationItem) { - throw new NotFoundException(`Couldn't find donation item ${id} `); - } + if (quantityToAllocate === undefined) continue; - if ( - allocatedQuantity > - donationItem.quantity - donationItem.reservedQuantity - ) { - throw new BadRequestException( - `Donation item ${id} allocated quantity exceeds remaining quantity`, - ); + if ( + quantityToAllocate > + donationItem.quantity - donationItem.reservedQuantity + ) { + throw new BadRequestException( + `Donation item ${id} quantity to allocate exceeds remaining quantity`, + ); + } } - } - const order = this.repo.create({ - requestId: requestId, - foodManufacturerId: manufacturerId, - status: OrderStatus.PENDING, - }); + const order = manager.create(Order, { + requestId: requestId, + foodManufacturerId: manufacturerId, + status: OrderStatus.PENDING, + }); - const savedOrder = await this.repo.save(order); + const savedOrder = await manager.save(order); - await this.allocationsService.createMultiple({ - orderId: savedOrder.orderId, - itemAllocations: orderData.itemAllocations, - }); + await this.allocationsService.createMultiple( + { + orderId: savedOrder.orderId, + itemAllocations: itemAllocations, + }, + manager, + ); - await this.donationItemsService.setReservedQuantities( - orderData.itemAllocations, - ); + const associatedDonationIdsSet = + await this.donationItemsService.getAssociatedDonationIds( + donationItemIds, + ); - await this.donationService.matchAll(associatedDonationIds); + await this.donationService.matchAll( + Array.from(associatedDonationIdsSet), + manager, + ); - return savedOrder; + return savedOrder; + }); } async findOne(orderId: number): Promise { From 4352d38e098a8d1c57ed9ac4bf9e335f580eb461 Mon Sep 17 00:00:00 2001 From: Justin Wang Date: Sun, 15 Mar 2026 12:26:52 -0400 Subject: [PATCH 11/15] fix import bug --- apps/backend/src/allocations/allocations.module.ts | 3 ++- apps/backend/src/orders/order.service.ts | 1 - 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/backend/src/allocations/allocations.module.ts b/apps/backend/src/allocations/allocations.module.ts index 2c9815f05..7d62e964d 100644 --- a/apps/backend/src/allocations/allocations.module.ts +++ b/apps/backend/src/allocations/allocations.module.ts @@ -5,10 +5,11 @@ import { AllocationsController } from './allocations.controller'; import { AllocationsService } from './allocations.service'; import { AuthModule } from '../auth/auth.module'; import { DonationItemsModule } from '../donationItems/donationItems.module'; +import { DonationItem } from '../donationItems/donationItems.entity'; @Module({ imports: [ - TypeOrmModule.forFeature([Allocation]), + TypeOrmModule.forFeature([Allocation, DonationItem]), forwardRef(() => AuthModule), DonationItemsModule, ], diff --git a/apps/backend/src/orders/order.service.ts b/apps/backend/src/orders/order.service.ts index ff6a6867d..560b814f3 100644 --- a/apps/backend/src/orders/order.service.ts +++ b/apps/backend/src/orders/order.service.ts @@ -21,7 +21,6 @@ import { FoodManufacturersService } from '../foodManufacturers/manufacturers.ser import { DonationItemsService } from '../donationItems/donationItems.service'; import { AllocationsService } from '../allocations/allocations.service'; import { DonationService } from '../donations/donations.service'; -import { DonationItem } from '../donationItems/donationItems.entity'; @Injectable() export class OrdersService { From e683f34158bfa097cf98ffa4f6224119d940604c Mon Sep 17 00:00:00 2001 From: Justin Wang Date: Sun, 15 Mar 2026 12:36:31 -0400 Subject: [PATCH 12/15] import --- apps/backend/src/orders/order.module.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/backend/src/orders/order.module.ts b/apps/backend/src/orders/order.module.ts index 71003cc7e..474d3f48c 100644 --- a/apps/backend/src/orders/order.module.ts +++ b/apps/backend/src/orders/order.module.ts @@ -29,7 +29,7 @@ import { Donation } from '../donations/donations.entity'; Allocation, Donation, ]), - AllocationModule, + forwardRef(() => AllocationModule), forwardRef(() => AuthModule), AWSS3Module, MulterModule.register({ dest: './uploads' }), From 9c93121e4e3718240d2f4e0ee9d18eb377fdeb5a Mon Sep 17 00:00:00 2001 From: Justin Wang Date: Sun, 15 Mar 2026 13:20:03 -0400 Subject: [PATCH 13/15] import fix --- apps/backend/src/orders/order.module.ts | 2 +- apps/backend/src/pantries/pantries.service.spec.ts | 12 ++++++++++++ .../src/volunteers/volunteers.service.spec.ts | 12 ++++++++++++ 3 files changed, 25 insertions(+), 1 deletion(-) diff --git a/apps/backend/src/orders/order.module.ts b/apps/backend/src/orders/order.module.ts index 474d3f48c..71003cc7e 100644 --- a/apps/backend/src/orders/order.module.ts +++ b/apps/backend/src/orders/order.module.ts @@ -29,7 +29,7 @@ import { Donation } from '../donations/donations.entity'; Allocation, Donation, ]), - forwardRef(() => AllocationModule), + AllocationModule, forwardRef(() => AuthModule), AWSS3Module, MulterModule.register({ dest: './uploads' }), diff --git a/apps/backend/src/pantries/pantries.service.spec.ts b/apps/backend/src/pantries/pantries.service.spec.ts index e6ed91f03..dc896aad8 100644 --- a/apps/backend/src/pantries/pantries.service.spec.ts +++ b/apps/backend/src/pantries/pantries.service.spec.ts @@ -27,6 +27,9 @@ import { Donation } from '../donations/donations.entity'; import { FoodManufacturersService } from '../foodManufacturers/manufacturers.service'; import { FoodManufacturer } from '../foodManufacturers/manufacturers.entity'; import { User } from '../users/users.entity'; +import { AllocationsService } from '../allocations/allocations.service'; +import { Allocation } from '../allocations/allocations.entity'; +import { DataSource } from 'typeorm'; jest.setTimeout(60000); @@ -77,6 +80,7 @@ describe('PantriesService', () => { DonationItemsService, DonationService, FoodManufacturersService, + AllocationsService, { provide: AuthService, useValue: { @@ -111,6 +115,14 @@ describe('PantriesService', () => { provide: getRepositoryToken(FoodManufacturer), useValue: testDataSource.getRepository(FoodManufacturer), }, + { + provide: getRepositoryToken(Allocation), + useValue: testDataSource.getRepository(Allocation), + }, + { + provide: DataSource, + useValue: testDataSource, + }, ], }).compile(); diff --git a/apps/backend/src/volunteers/volunteers.service.spec.ts b/apps/backend/src/volunteers/volunteers.service.spec.ts index 35e5a92c8..2df2802e8 100644 --- a/apps/backend/src/volunteers/volunteers.service.spec.ts +++ b/apps/backend/src/volunteers/volunteers.service.spec.ts @@ -18,6 +18,9 @@ import { DonationItem } from '../donationItems/donationItems.entity'; import { DonationItemsService } from '../donationItems/donationItems.service'; import { DonationService } from '../donations/donations.service'; import { Donation } from '../donations/donations.entity'; +import { Allocation } from '../allocations/allocations.entity'; +import { AllocationsService } from '../allocations/allocations.service'; +import { DataSource } from 'typeorm'; jest.setTimeout(60000); @@ -40,6 +43,7 @@ describe('VolunteersService', () => { FoodManufacturersService, DonationItemsService, DonationService, + AllocationsService, { provide: AuthService, useValue: { @@ -74,6 +78,14 @@ describe('VolunteersService', () => { provide: getRepositoryToken(Donation), useValue: testDataSource.getRepository(Donation), }, + { + provide: getRepositoryToken(Allocation), + useValue: testDataSource.getRepository(Allocation), + }, + { + provide: DataSource, + useValue: testDataSource, + }, ], }).compile(); From cdd1796c15c4e7fa54331f9afc8893a862d1c770 Mon Sep 17 00:00:00 2001 From: Justin Wang Date: Mon, 16 Mar 2026 10:46:41 -0400 Subject: [PATCH 14/15] comment --- apps/backend/src/allocations/dtos/create-allocations.dto.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/apps/backend/src/allocations/dtos/create-allocations.dto.ts b/apps/backend/src/allocations/dtos/create-allocations.dto.ts index 562ac4454..f11aed640 100644 --- a/apps/backend/src/allocations/dtos/create-allocations.dto.ts +++ b/apps/backend/src/allocations/dtos/create-allocations.dto.ts @@ -1,9 +1,11 @@ -import { IsNumber, IsObject } from 'class-validator'; +import { IsNotEmptyObject, IsNumber, IsObject, Min } from 'class-validator'; export class CreateMultipleAllocationsDto { @IsNumber() + @Min(1) orderId!: number; @IsObject() + @IsNotEmptyObject() itemAllocations!: Record; } From 86e5e1d72c53a00a18336719ba23ef0fef58609d Mon Sep 17 00:00:00 2001 From: Justin Wang Date: Mon, 16 Mar 2026 21:31:43 -0400 Subject: [PATCH 15/15] remove controller endpoints --- .../src/allocations/allocations.controller.ts | 38 +------- .../donationItems.controller.spec.ts | 40 -------- .../donationItems/donationItems.controller.ts | 26 ----- .../donations/donations.controller.spec.ts | 12 --- .../src/donations/donations.controller.ts | 7 -- .../src/orders/order.controller.spec.ts | 94 ------------------- apps/backend/src/orders/order.controller.ts | 42 --------- 7 files changed, 1 insertion(+), 258 deletions(-) diff --git a/apps/backend/src/allocations/allocations.controller.ts b/apps/backend/src/allocations/allocations.controller.ts index b04d8bfcb..06cdd4fc9 100644 --- a/apps/backend/src/allocations/allocations.controller.ts +++ b/apps/backend/src/allocations/allocations.controller.ts @@ -1,43 +1,7 @@ -import { Body, Controller, Post } from '@nestjs/common'; +import { Controller } from '@nestjs/common'; import { AllocationsService } from './allocations.service'; -import { Allocation } from './allocations.entity'; -import { ApiBody } from '@nestjs/swagger'; -import { CreateMultipleAllocationsDto } from './dtos/create-allocations.dto'; @Controller('allocations') export class AllocationsController { constructor(private allocationsService: AllocationsService) {} - - @Post('/create-multiple') - @ApiBody({ - description: - 'Bulk create allocations given an order id, multiple donation item ids and quantities', - schema: { - type: 'object', - properties: { - orderId: { - type: 'integer', - example: 1, - }, - itemAllocations: { - type: 'object', - description: 'Map of donationItemId -> quantity', - additionalProperties: { - type: 'integer', - example: 10, - }, - example: { - '5': 10, - '8': 3, - '12': 7, - }, - }, - }, - }, - }) - async createMultipleAllocations( - @Body() body: CreateMultipleAllocationsDto, - ): Promise { - return this.allocationsService.createMultiple(body); - } } diff --git a/apps/backend/src/donationItems/donationItems.controller.spec.ts b/apps/backend/src/donationItems/donationItems.controller.spec.ts index d9df20127..784a3dc5a 100644 --- a/apps/backend/src/donationItems/donationItems.controller.spec.ts +++ b/apps/backend/src/donationItems/donationItems.controller.spec.ts @@ -67,44 +67,4 @@ describe('DonationItemsController', () => { expect(result).toEqual(mockCreatedItems); }); }); - - describe('getAssociatedDonationIds', () => { - it('should call service.getAssociatedDonationIds with donationItemIds and return donation id list', async () => { - const donationItemIds = [1, 2, 3]; - const mockDonationsids = [1, 2]; - const mockDonationsidsSet = new Set(mockDonationsids); - - mockDonationItemsService.getAssociatedDonationIds.mockResolvedValue( - mockDonationsidsSet, - ); - - const result = await controller.getAssociatedDonationIds(donationItemIds); - - expect( - mockDonationItemsService.getAssociatedDonationIds, - ).toHaveBeenCalledWith(donationItemIds); - expect(result).toEqual(mockDonationsidsSet); - }); - }); - - describe('getByIds', () => { - it('should call service.getByIds with donationItemIds and return donation items', async () => { - const donationItemIds = [1, 2]; - const mockItems = [ - { itemId: 1, itemName: 'Rice' }, - { itemId: 2, itemName: 'Beans' }, - ] as Partial[]; - - mockDonationItemsService.getByIds.mockResolvedValue( - mockItems as DonationItem[], - ); - - const result = await controller.getByIds(donationItemIds); - - expect(mockDonationItemsService.getByIds).toHaveBeenCalledWith( - donationItemIds, - ); - expect(result).toEqual(mockItems); - }); - }); }); diff --git a/apps/backend/src/donationItems/donationItems.controller.ts b/apps/backend/src/donationItems/donationItems.controller.ts index a04927c86..13a47e6df 100644 --- a/apps/backend/src/donationItems/donationItems.controller.ts +++ b/apps/backend/src/donationItems/donationItems.controller.ts @@ -7,8 +7,6 @@ import { Patch, UseGuards, ParseIntPipe, - Query, - ParseArrayPipe, } from '@nestjs/common'; import { ApiBody } from '@nestjs/swagger'; import { DonationItemsService } from './donationItems.service'; @@ -16,7 +14,6 @@ import { DonationItem } from './donationItems.entity'; import { AuthGuard } from '@nestjs/passport'; import { FoodType } from './types'; import { CreateMultipleDonationItemsDto } from './dtos/create-donation-items.dto'; -import { Donation } from '../donations/donations.entity'; @Controller('donation-items') @UseGuards(AuthGuard('jwt')) @@ -30,29 +27,6 @@ export class DonationItemsController { return this.donationItemsService.getAllDonationItems(donationId); } - // Called like: /donation-items/associated-donationIds?donationItemIds=1,2,3 - @Get('/associated-donationIds') - async getAssociatedDonationIds( - @Query( - 'donationItemIds', - new ParseArrayPipe({ items: Number, separator: ',' }), - ) - donationItemIds: number[], - ): Promise> { - return this.donationItemsService.getAssociatedDonationIds(donationItemIds); - } - - @Get('/getByIds') - async getByIds( - @Query( - 'donationItemIds', - new ParseArrayPipe({ items: Number, separator: ',' }), - ) - donationItemIds: number[], - ): Promise { - return this.donationItemsService.getByIds(donationItemIds); - } - @Post('/create-multiple') @ApiBody({ description: 'Bulk create donation items for a single donation', diff --git a/apps/backend/src/donations/donations.controller.spec.ts b/apps/backend/src/donations/donations.controller.spec.ts index bbbfa9a20..9f5b99095 100644 --- a/apps/backend/src/donations/donations.controller.spec.ts +++ b/apps/backend/src/donations/donations.controller.spec.ts @@ -52,16 +52,4 @@ describe('DonationsController', () => { expect(mockDonationService.findOne).toHaveBeenCalledWith(1); }); }); - - describe('PATCH /match-all', () => { - it('should call donationService.matchAll with donationIds', async () => { - const donationIds = [1, 2, 3]; - - mockDonationService.matchAll.mockResolvedValue(); - - await controller.matchAllDonations({ donationIds }); - - expect(mockDonationService.matchAll).toHaveBeenCalledWith(donationIds); - }); - }); }); diff --git a/apps/backend/src/donations/donations.controller.ts b/apps/backend/src/donations/donations.controller.ts index 89d5e6073..95a7363b8 100644 --- a/apps/backend/src/donations/donations.controller.ts +++ b/apps/backend/src/donations/donations.controller.ts @@ -82,11 +82,4 @@ export class DonationsController { } return updatedDonation; } - - @Patch('/match-all') - async matchAllDonations( - @Body() body: { donationIds: number[] }, - ): Promise { - await this.donationService.matchAll(body.donationIds); - } } diff --git a/apps/backend/src/orders/order.controller.spec.ts b/apps/backend/src/orders/order.controller.spec.ts index a35eaa95a..1c989d88f 100644 --- a/apps/backend/src/orders/order.controller.spec.ts +++ b/apps/backend/src/orders/order.controller.spec.ts @@ -16,7 +16,6 @@ import { BadRequestException } from '@nestjs/common'; import { FoodManufacturer } from '../foodManufacturers/manufacturers.entity'; import { FoodRequestSummaryDto } from '../foodRequests/dtos/food-request-summary.dto'; import { ConfirmDeliveryDto } from './dtos/confirm-delivery.dto'; -import { CreateOrderDto } from './dtos/create-order.dto'; const mockOrdersService = mock(); const mockAllocationsService = mock(); @@ -388,97 +387,4 @@ describe('OrdersController', () => { ); }); }); - - describe('createOrder', () => { - it('should call ordersService.create and return the created order', async () => { - const createOrderDto = { - foodRequestId: 1, - manufacturerId: 1, - itemAllocations: { - 5: 10, - 8: 3, - 12: 7, - }, - }; - - const mockCreatedOrder: Partial = { - orderId: 42, - status: OrderStatus.PENDING, - request: { requestId: 1 } as FoodRequest, - foodManufacturer: { foodManufacturerId: 1 } as FoodManufacturer, - }; - - mockOrdersService.create.mockResolvedValueOnce(mockCreatedOrder as Order); - - const result = await controller.createOrder(createOrderDto); - - expect(mockOrdersService.create).toHaveBeenCalledWith(createOrderDto); - expect(result).toEqual(mockCreatedOrder); - }); - - it('should propagate BadRequestException when request is not active', async () => { - const foodRequestId = 1; - - const createOrderDto: CreateOrderDto = { - foodRequestId: foodRequestId, - manufacturerId: 1, - itemAllocations: { 5: 10 }, - }; - - mockOrdersService.create.mockRejectedValueOnce( - new BadRequestException(`Request ${foodRequestId} is not active`), - ); - - const promise = controller.createOrder(createOrderDto); - await expect(promise).rejects.toBeInstanceOf(BadRequestException); - await expect(promise).rejects.toThrow( - `Request ${foodRequestId} is not active`, - ); - expect(mockOrdersService.create).toHaveBeenCalledWith(createOrderDto); - }); - - it('should propagate Error when donation item does not belong to FM', async () => { - const createOrderDto: CreateOrderDto = { - foodRequestId: 1, - manufacturerId: 1, - itemAllocations: { 5: 10 }, - }; - - mockOrdersService.create.mockRejectedValueOnce( - new BadRequestException( - `Donation is not associated with the current food manufacturer`, - ), - ); - - const promise = controller.createOrder(createOrderDto); - await expect(promise).rejects.toThrow(BadRequestException); - await expect(promise).rejects.toThrow( - `Donation is not associated with the current food manufacturer`, - ); - expect(mockOrdersService.create).toHaveBeenCalledWith(createOrderDto); - }); - - it('should propagate BadRequestException when allocated quantity exceeds remaining', async () => { - const donationItemId = 5; - - const createOrderDto: CreateOrderDto = { - foodRequestId: 1, - manufacturerId: 1, - itemAllocations: { [donationItemId]: 100 }, - }; - - mockOrdersService.create.mockRejectedValueOnce( - new BadRequestException( - `Donation item ${donationItemId} allocated quantity exceeds remaining quantity`, - ), - ); - - const promise = controller.createOrder(createOrderDto); - await expect(promise).rejects.toBeInstanceOf(BadRequestException); - await expect(promise).rejects.toThrow( - `Donation item ${donationItemId} allocated quantity exceeds remaining quantity`, - ); - expect(mockOrdersService.create).toHaveBeenCalledWith(createOrderDto); - }); - }); }); diff --git a/apps/backend/src/orders/order.controller.ts b/apps/backend/src/orders/order.controller.ts index bf4b018a1..85c87ee7a 100644 --- a/apps/backend/src/orders/order.controller.ts +++ b/apps/backend/src/orders/order.controller.ts @@ -10,7 +10,6 @@ import { ValidationPipe, UploadedFiles, UseInterceptors, - Post, } from '@nestjs/common'; import { ApiBody } from '@nestjs/swagger'; import { OrdersService } from './order.service'; @@ -28,7 +27,6 @@ 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'; -import { CreateOrderDto } from './dtos/create-order.dto'; import { FoodRequest } from '../foodRequests/request.entity'; @Controller('orders') @@ -103,46 +101,6 @@ export class OrdersController { return this.ordersService.findOrderDetails(orderId); } - @Post('/create') - @ApiBody({ - description: 'Details for creating a order', - schema: { - type: 'object', - properties: { - foodRequestId: { - type: 'integer', - description: 'ID of the associated request this order is related to', - example: 1, - }, - manufacturerId: { - type: 'integer', - description: 'Food manufacturer ID of the FM fulfilling the order', - example: 1, - }, - itemAllocations: { - type: 'object', - description: - 'Map of donationItemId -> quantity to allocate, donation items and their quantity to allocate for this order', - additionalProperties: { - type: 'integer', - example: 10, - }, - example: { - '5': 10, - '8': 3, - '12': 7, - }, - }, - }, - }, - }) - async createOrder( - @Body(new ValidationPipe()) - orderData: CreateOrderDto, - ): Promise { - return this.ordersService.create(orderData); - } - @Get('/order/:requestId') async getOrderByRequestId( @Param('requestId', ParseIntPipe) requestId: number,