From 2ef718a9dca8a3ab762f321e35c30fc1f0ace7e5 Mon Sep 17 00:00:00 2001 From: amywng <147568742+amywng@users.noreply.github.com> Date: Sat, 24 Jan 2026 23:56:00 -0500 Subject: [PATCH 1/3] remove pantry from order entity --- apps/backend/src/config/typeorm.ts | 2 + .../src/foodRequests/request.controller.ts | 1 - .../src/foodRequests/request.service.spec.ts | 2 - .../1769316004958-RemovePantryFromOrders.ts | 19 +++ apps/backend/src/orders/order.entity.ts | 8 -- apps/backend/src/orders/order.module.ts | 3 +- apps/backend/src/orders/order.service.spec.ts | 113 +++++++++++++++++- apps/backend/src/orders/order.service.ts | 30 +++-- 8 files changed, 149 insertions(+), 29 deletions(-) create mode 100644 apps/backend/src/migrations/1769316004958-RemovePantryFromOrders.ts diff --git a/apps/backend/src/config/typeorm.ts b/apps/backend/src/config/typeorm.ts index 82384673..10c8a5af 100644 --- a/apps/backend/src/config/typeorm.ts +++ b/apps/backend/src/config/typeorm.ts @@ -27,6 +27,7 @@ import { RemoveMultipleVolunteerTypes1764811878152 } from '../migrations/1764811 import { RemoveUnusedStatuses1764816885341 } from '../migrations/1764816885341-RemoveUnusedStatuses'; import { UpdatePantryFields1763762628431 } from '../migrations/1763762628431-UpdatePantryFields'; import { PopulateDummyData1768501812134 } from '../migrations/1768501812134-populateDummyData'; +import { RemovePantryFromOrders1769316004958 } from '../migrations/1769316004958-RemovePantryFromOrders'; const config = { type: 'postgres', @@ -67,6 +68,7 @@ const config = { RemoveMultipleVolunteerTypes1764811878152, RemoveUnusedStatuses1764816885341, PopulateDummyData1768501812134, + RemovePantryFromOrders1769316004958, ], }; diff --git a/apps/backend/src/foodRequests/request.controller.ts b/apps/backend/src/foodRequests/request.controller.ts index 1f449491..2f44c84e 100644 --- a/apps/backend/src/foodRequests/request.controller.ts +++ b/apps/backend/src/foodRequests/request.controller.ts @@ -16,7 +16,6 @@ import { AWSS3Service } from '../aws/aws-s3.service'; import { FilesInterceptor } from '@nestjs/platform-express'; import * as multer from 'multer'; import { OrdersService } from '../orders/order.service'; -import { Order } from '../orders/order.entity'; import { RequestSize } from './types'; import { OrderStatus } from '../orders/types'; diff --git a/apps/backend/src/foodRequests/request.service.spec.ts b/apps/backend/src/foodRequests/request.service.spec.ts index cc69a5a3..369faf2d 100644 --- a/apps/backend/src/foodRequests/request.service.spec.ts +++ b/apps/backend/src/foodRequests/request.service.spec.ts @@ -200,7 +200,6 @@ describe('RequestsService', () => { it('should update and return the food request with new delivery details', async () => { const mockOrder: Partial = { orderId: 1, - pantry: null, request: null, requestId: 1, foodManufacturer: null, @@ -315,7 +314,6 @@ describe('RequestsService', () => { it('should throw an error if the order does not have a food manufacturer', async () => { const mockOrder: Partial = { orderId: 1, - pantry: null, request: null, requestId: 1, foodManufacturer: null, diff --git a/apps/backend/src/migrations/1769316004958-RemovePantryFromOrders.ts b/apps/backend/src/migrations/1769316004958-RemovePantryFromOrders.ts new file mode 100644 index 00000000..18ee67a5 --- /dev/null +++ b/apps/backend/src/migrations/1769316004958-RemovePantryFromOrders.ts @@ -0,0 +1,19 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class RemovePantryFromOrders1769316004958 implements MigrationInterface { + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(` + ALTER TABLE orders + DROP CONSTRAINT IF EXISTS fk_pantry, + DROP COLUMN IF EXISTS pantry_id; + `); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(` + ALTER TABLE orders + ADD COLUMN pantry_id INT NOT NULL, + ADD CONSTRAINT fk_pantry FOREIGN KEY(pantry_id) REFERENCES pantries(pantry_id); + `); + } +} diff --git a/apps/backend/src/orders/order.entity.ts b/apps/backend/src/orders/order.entity.ts index 4c38457b..0f552f12 100644 --- a/apps/backend/src/orders/order.entity.ts +++ b/apps/backend/src/orders/order.entity.ts @@ -7,7 +7,6 @@ import { JoinColumn, } from 'typeorm'; import { FoodRequest } from '../foodRequests/request.entity'; -import { Pantry } from '../pantries/pantries.entity'; import { FoodManufacturer } from '../foodManufacturers/manufacturer.entity'; import { OrderStatus } from './types'; @@ -16,13 +15,6 @@ export class Order { @PrimaryGeneratedColumn({ name: 'order_id' }) orderId: number; - @ManyToOne(() => Pantry, { nullable: false }) - @JoinColumn({ - name: 'pantry_id', - referencedColumnName: 'pantryId', - }) - pantry: Pantry; - @ManyToOne(() => FoodRequest, { nullable: false }) @JoinColumn({ name: 'request_id', diff --git a/apps/backend/src/orders/order.module.ts b/apps/backend/src/orders/order.module.ts index c6b307a0..4937eced 100644 --- a/apps/backend/src/orders/order.module.ts +++ b/apps/backend/src/orders/order.module.ts @@ -5,10 +5,11 @@ import { Order } from './order.entity'; import { OrdersService } from './order.service'; import { JwtStrategy } from '../auth/jwt.strategy'; import { AuthService } from '../auth/auth.service'; +import { Pantry } from '../pantries/pantries.entity'; import { AllocationModule } from '../allocations/allocations.module'; @Module({ - imports: [TypeOrmModule.forFeature([Order]), AllocationModule], + imports: [TypeOrmModule.forFeature([Order, Pantry]), AllocationModule], controllers: [OrdersController], providers: [OrdersService, AuthService, JwtStrategy], exports: [OrdersService], diff --git a/apps/backend/src/orders/order.service.spec.ts b/apps/backend/src/orders/order.service.spec.ts index ce653481..ceb0705a 100644 --- a/apps/backend/src/orders/order.service.spec.ts +++ b/apps/backend/src/orders/order.service.spec.ts @@ -16,6 +16,7 @@ import { import { OrderStatus } from './types'; const mockOrdersRepository = mock>(); +const mockPantryRepository = mock>(); const mockPantry: Partial = { pantryId: 1, @@ -54,6 +55,10 @@ describe('OrdersService', () => { provide: getRepositoryToken(Order), useValue: mockOrdersRepository, }, + { + provide: getRepositoryToken(Pantry), + useValue: mockPantryRepository, + }, ], }).compile(); @@ -63,6 +68,7 @@ describe('OrdersService', () => { beforeEach(() => { qb = { leftJoinAndSelect: jest.fn().mockReturnThis(), + leftJoin: jest.fn().mockReturnThis(), select: jest.fn().mockReturnThis(), andWhere: jest.fn().mockReturnThis(), getMany: jest.fn().mockResolvedValue([]), @@ -108,17 +114,14 @@ describe('OrdersService', () => { { orderId: 3, status: OrderStatus.DELIVERED, - pantry: { ...(mockPantry as Pantry), pantryName: 'Test Pantry' }, }, { orderId: 4, status: OrderStatus.DELIVERED, - pantry: { ...(mockPantry as Pantry), pantryName: 'Test Pantry 2' }, }, { orderId: 5, status: OrderStatus.DELIVERED, - pantry: { ...(mockPantry as Pantry), pantryName: 'Test Pantry 3' }, }, ]; @@ -156,17 +159,14 @@ describe('OrdersService', () => { { orderId: 3, status: OrderStatus.DELIVERED, - pantry: { ...(mockPantry as Pantry), pantryName: 'Test Pantry 1' }, }, { orderId: 4, status: OrderStatus.DELIVERED, - pantry: { ...(mockPantry as Pantry), pantryName: 'Test Pantry 2' }, }, { orderId: 5, status: OrderStatus.DELIVERED, - pantry: { ...(mockPantry as Pantry), pantryName: 'Test Pantry 2' }, }, ]; @@ -189,4 +189,105 @@ describe('OrdersService', () => { ); }); }); + + describe('findOrderPantry', () => { + it('should return pantry for given order', async () => { + const mockFoodRequest = { + requestId: 1, + pantryId: 1, + }; + + const mockOrder = { + orderId: 1, + requestId: 1, + request: mockFoodRequest, + }; + + (mockOrdersRepository.findOne as jest.Mock).mockResolvedValue(mockOrder); + (mockPantryRepository.findOneBy as jest.Mock).mockResolvedValue( + mockPantry as Pantry, + ); + + const result = await service.findOrderPantry(1); + + expect(result).toEqual(mockPantry); + expect(mockPantryRepository.findOneBy).toHaveBeenCalledWith({ + pantryId: 1, + }); + }); + + it('should throw NotFoundException if order not found', async () => { + (mockOrdersRepository.findOne as jest.Mock).mockResolvedValue(null); + + await expect(service.findOrderPantry(999)).rejects.toThrow( + 'Order 999 not found', + ); + }); + + it('should throw NotFoundException if pantry not found', async () => { + const mockFoodRequest = { + requestId: 1, + pantryId: 999, + }; + + const mockOrder = { + orderId: 1, + requestId: 1, + request: mockFoodRequest, + }; + + (mockOrdersRepository.findOne as jest.Mock).mockResolvedValue(mockOrder); + (mockPantryRepository.findOneBy as jest.Mock).mockResolvedValue(null); + + await expect(service.findOrderPantry(1)).rejects.toThrow( + 'Pantry 999 not found', + ); + }); + }); + + describe('getOrdersByPantry', () => { + it('should return orders for given pantry', async () => { + const mockOrders: Partial[] = [ + { orderId: 1, requestId: 1 }, + { orderId: 2, requestId: 2 }, + ]; + + (mockPantryRepository.findOneBy as jest.Mock).mockResolvedValue( + mockPantry as Pantry, + ); + (mockOrdersRepository.find as jest.Mock).mockResolvedValue( + mockOrders as Order[], + ); + + const result = await service.getOrdersByPantry(1); + + expect(result).toEqual(mockOrders); + expect(mockPantryRepository.findOneBy).toHaveBeenCalledWith({ + pantryId: 1, + }); + expect(mockOrdersRepository.find).toHaveBeenCalledWith({ + where: { request: { pantryId: 1 } }, + relations: ['request'], + }); + }); + + it('should throw NotFoundException if pantry does not exist', async () => { + (mockPantryRepository.findOneBy as jest.Mock).mockResolvedValue(null); + + await expect(service.getOrdersByPantry(999)).rejects.toThrow( + 'Pantry 999 not found', + ); + }); + + it('should return empty array if pantry has no orders', async () => { + (mockPantryRepository.findOneBy as jest.Mock).mockResolvedValue( + mockPantry as Pantry, + ); + (mockOrdersRepository.find as jest.Mock).mockResolvedValue([]); + + const result = await service.getOrdersByPantry(1); + + expect(result).toEqual([]); + }); + }); }); diff --git a/apps/backend/src/orders/order.service.ts b/apps/backend/src/orders/order.service.ts index 05f37bca..ff175420 100644 --- a/apps/backend/src/orders/order.service.ts +++ b/apps/backend/src/orders/order.service.ts @@ -10,12 +10,16 @@ import { OrderStatus } from './types'; @Injectable() export class OrdersService { - constructor(@InjectRepository(Order) private repo: Repository) {} + constructor( + @InjectRepository(Order) private repo: Repository, + @InjectRepository(Pantry) private pantryRepo: Repository, + ) {} async getAll(filters?: { status?: string; pantryNames?: string[] }) { const qb = this.repo .createQueryBuilder('order') - .leftJoinAndSelect('order.pantry', 'pantry') + .leftJoinAndSelect('order.request', 'request') + .leftJoin('pantries', 'pantry', 'pantry.pantryId = request.pantryId') .leftJoinAndSelect('pantry.volunteers', 'volunteers') .select([ 'order.orderId', @@ -82,17 +86,16 @@ export class OrdersService { } async findOrderPantry(orderId: number): Promise { - validateId(orderId, 'Order'); - - const order = await this.repo.findOne({ - where: { orderId }, - relations: ['pantry'], + const request = await this.findOrderFoodRequest(orderId); + const pantry = await this.pantryRepo.findOneBy({ + pantryId: request.pantryId, }); - if (!order) { - throw new NotFoundException(`Order ${orderId} not found`); + if (!pantry) { + throw new NotFoundException(`Pantry ${request.pantryId} not found`); } - return order.pantry; + + return pantry; } async findOrderFoodRequest(orderId: number): Promise { @@ -140,8 +143,13 @@ export class OrdersService { async getOrdersByPantry(pantryId: number): Promise { validateId(pantryId, 'Pantry'); + const pantry = await this.pantryRepo.findOneBy({ pantryId }); + if (!pantry) { + throw new NotFoundException(`Pantry ${pantryId} not found`); + } + const orders = await this.repo.find({ - where: { pantry: { pantryId } }, + where: { request: { pantryId } }, relations: ['request'], }); From e5e577ded91dc68bf31c158f130773547105f294 Mon Sep 17 00:00:00 2001 From: amywng <147568742+amywng@users.noreply.github.com> Date: Sun, 25 Jan 2026 00:00:55 -0500 Subject: [PATCH 2/3] fix migration down --- .../migrations/1769316004958-RemovePantryFromOrders.ts | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/apps/backend/src/migrations/1769316004958-RemovePantryFromOrders.ts b/apps/backend/src/migrations/1769316004958-RemovePantryFromOrders.ts index 18ee67a5..11e034ed 100644 --- a/apps/backend/src/migrations/1769316004958-RemovePantryFromOrders.ts +++ b/apps/backend/src/migrations/1769316004958-RemovePantryFromOrders.ts @@ -12,7 +12,13 @@ export class RemovePantryFromOrders1769316004958 implements MigrationInterface { public async down(queryRunner: QueryRunner): Promise { await queryRunner.query(` ALTER TABLE orders - ADD COLUMN pantry_id INT NOT NULL, + ADD COLUMN pantry_id INT; + UPDATE orders o + SET pantry_id = fr.pantry_id + FROM food_requests fr + WHERE o.request_id = fr.request_id; + ALTER TABLE orders + ALTER COLUMN pantry_id SET NOT NULL, ADD CONSTRAINT fk_pantry FOREIGN KEY(pantry_id) REFERENCES pantries(pantry_id); `); } From 7c9016ec1bc2af95a472ace7d250dd37b1166217 Mon Sep 17 00:00:00 2001 From: amywng <147568742+amywng@users.noreply.github.com> Date: Mon, 26 Jan 2026 10:46:22 -0500 Subject: [PATCH 3/3] add typing --- apps/backend/src/orders/order.service.spec.ts | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/apps/backend/src/orders/order.service.spec.ts b/apps/backend/src/orders/order.service.spec.ts index ceb0705a..3fd0b1f2 100644 --- a/apps/backend/src/orders/order.service.spec.ts +++ b/apps/backend/src/orders/order.service.spec.ts @@ -14,6 +14,7 @@ import { ServeAllergicChildren, } from '../pantries/types'; import { OrderStatus } from './types'; +import { FoodRequest } from '../foodRequests/request.entity'; const mockOrdersRepository = mock>(); const mockPantryRepository = mock>(); @@ -192,15 +193,15 @@ describe('OrdersService', () => { describe('findOrderPantry', () => { it('should return pantry for given order', async () => { - const mockFoodRequest = { + const mockFoodRequest: Partial = { requestId: 1, pantryId: 1, }; - const mockOrder = { + const mockOrder: Partial = { orderId: 1, requestId: 1, - request: mockFoodRequest, + request: mockFoodRequest as FoodRequest, }; (mockOrdersRepository.findOne as jest.Mock).mockResolvedValue(mockOrder); @@ -225,15 +226,15 @@ describe('OrdersService', () => { }); it('should throw NotFoundException if pantry not found', async () => { - const mockFoodRequest = { + const mockFoodRequest: Partial = { requestId: 1, pantryId: 999, }; - const mockOrder = { + const mockOrder: Partial = { orderId: 1, requestId: 1, - request: mockFoodRequest, + request: mockFoodRequest as FoodRequest, }; (mockOrdersRepository.findOne as jest.Mock).mockResolvedValue(mockOrder);