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.entity.ts b/apps/backend/src/foodRequests/request.entity.ts index 9864b4cc..25ba8e66 100644 --- a/apps/backend/src/foodRequests/request.entity.ts +++ b/apps/backend/src/foodRequests/request.entity.ts @@ -4,9 +4,12 @@ import { PrimaryGeneratedColumn, CreateDateColumn, OneToMany, + ManyToOne, + JoinColumn, } from 'typeorm'; import { Order } from '../orders/order.entity'; import { RequestSize } from './types'; +import { Pantry } from '../pantries/pantries.entity'; @Entity('food_requests') export class FoodRequest { @@ -16,6 +19,10 @@ export class FoodRequest { @Column({ name: 'pantry_id', type: 'int' }) pantryId: number; + @ManyToOne(() => Pantry, { nullable: false }) + @JoinColumn({ name: 'pantry_id', referencedColumnName: 'pantryId' }) + pantry: Pantry; + @Column({ name: 'requested_size', type: 'enum', diff --git a/apps/backend/src/foodRequests/request.service.spec.ts b/apps/backend/src/foodRequests/request.service.spec.ts index 89d43414..23e07c87 100644 --- a/apps/backend/src/foodRequests/request.service.spec.ts +++ b/apps/backend/src/foodRequests/request.service.spec.ts @@ -336,7 +336,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, @@ -451,7 +450,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.controller.spec.ts b/apps/backend/src/orders/order.controller.spec.ts index 4c06f83a..938366ff 100644 --- a/apps/backend/src/orders/order.controller.spec.ts +++ b/apps/backend/src/orders/order.controller.spec.ts @@ -6,6 +6,8 @@ import { Order } from './order.entity'; import { Allocation } from '../allocations/allocations.entity'; import { mock } from 'jest-mock-extended'; import { OrderStatus } from './types'; +import { FoodRequest } from '../foodRequests/request.entity'; +import { Pantry } from '../pantries/pantries.entity'; const mockOrdersService = mock(); const mockAllocationsService = mock(); @@ -13,9 +15,27 @@ const mockAllocationsService = mock(); describe('OrdersController', () => { let controller: OrdersController; + const mockPantries: Partial[] = [ + { pantryId: 1, pantryName: 'Test Pantry' }, + { pantryId: 2, pantryName: 'Test Pantry 2' }, + ]; + + const mockRequests: Partial[] = [ + { requestId: 1, pantry: mockPantries[0] as Pantry }, + { requestId: 2, pantry: mockPantries[1] as Pantry }, + ]; + const mockOrders: Partial[] = [ - { orderId: 1, status: OrderStatus.PENDING }, - { orderId: 2, status: OrderStatus.DELIVERED }, + { + orderId: 1, + status: OrderStatus.PENDING, + request: mockRequests[0] as FoodRequest, + }, + { + orderId: 2, + status: OrderStatus.DELIVERED, + request: mockRequests[1] as FoodRequest, + }, ]; const mockAllocations: Partial[] = [ diff --git a/apps/backend/src/orders/order.entity.ts b/apps/backend/src/orders/order.entity.ts index 7c40fdb4..9a246d0c 100644 --- a/apps/backend/src/orders/order.entity.ts +++ b/apps/backend/src/orders/order.entity.ts @@ -8,7 +8,6 @@ import { OneToMany, } from 'typeorm'; import { FoodRequest } from '../foodRequests/request.entity'; -import { Pantry } from '../pantries/pantries.entity'; import { FoodManufacturer } from '../foodManufacturers/manufacturer.entity'; import { OrderStatus } from './types'; import { Allocation } from '../allocations/allocations.entity'; @@ -18,13 +17,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..ac4c2a4d 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') + .leftJoinAndSelect('request.pantry', 'pantry') .leftJoinAndSelect('pantry.volunteers', 'volunteers') .select([ 'order.orderId', @@ -23,6 +27,7 @@ export class OrdersService { 'order.createdAt', 'order.shippedAt', 'order.deliveredAt', + 'request.pantryId', 'pantry.pantryName', 'volunteers.id', 'volunteers.firstName', @@ -82,17 +87,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 +144,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'], }); diff --git a/apps/frontend/src/types/types.ts b/apps/frontend/src/types/types.ts index eb98e3a2..12fd0798 100644 --- a/apps/frontend/src/types/types.ts +++ b/apps/frontend/src/types/types.ts @@ -178,18 +178,20 @@ export interface UserDto { export interface FoodRequest { requestId: number; - requestedAt: string; - dateReceived: string | null; + pantryId: number; + pantry: Pantry; requestedSize: string; requestedItems: string[]; - additionalInformation: string; - orderId: number; + additionalInformation: string | null; + requestedAt: Date; + dateReceived: Date | null; + feedback: string | null; + photos: string[] | null; orders?: Order[]; } export interface Order { orderId: number; - pantry?: Pantry; request: FoodRequest; requestId: number; foodManufacturer: FoodManufacturer | null;