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..11e034ed --- /dev/null +++ b/apps/backend/src/migrations/1769316004958-RemovePantryFromOrders.ts @@ -0,0 +1,25 @@ +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; + 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); + `); + } +} 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..3fd0b1f2 100644 --- a/apps/backend/src/orders/order.service.spec.ts +++ b/apps/backend/src/orders/order.service.spec.ts @@ -14,8 +14,10 @@ import { ServeAllergicChildren, } from '../pantries/types'; import { OrderStatus } from './types'; +import { FoodRequest } from '../foodRequests/request.entity'; const mockOrdersRepository = mock>(); +const mockPantryRepository = mock>(); const mockPantry: Partial = { pantryId: 1, @@ -54,6 +56,10 @@ describe('OrdersService', () => { provide: getRepositoryToken(Order), useValue: mockOrdersRepository, }, + { + provide: getRepositoryToken(Pantry), + useValue: mockPantryRepository, + }, ], }).compile(); @@ -63,6 +69,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 +115,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 +160,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 +190,105 @@ describe('OrdersService', () => { ); }); }); + + describe('findOrderPantry', () => { + it('should return pantry for given order', async () => { + const mockFoodRequest: Partial = { + requestId: 1, + pantryId: 1, + }; + + const mockOrder: Partial = { + orderId: 1, + requestId: 1, + request: mockFoodRequest as FoodRequest, + }; + + (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: Partial = { + requestId: 1, + pantryId: 999, + }; + + const mockOrder: Partial = { + orderId: 1, + requestId: 1, + request: mockFoodRequest as FoodRequest, + }; + + (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'], });