diff --git a/apps/backend/src/foodRequests/request.service.spec.ts b/apps/backend/src/foodRequests/request.service.spec.ts index 6e2f3a76..1eb21b69 100644 --- a/apps/backend/src/foodRequests/request.service.spec.ts +++ b/apps/backend/src/foodRequests/request.service.spec.ts @@ -337,9 +337,6 @@ describe('RequestsService', () => { const mockOrder: Partial = { orderId: 1, request: null, - requestId: 1, - foodManufacturer: null, - shippedBy: 1, status: OrderStatus.SHIPPED, createdAt: new Date(), shippedAt: new Date(), @@ -446,48 +443,5 @@ describe('RequestsService', () => { relations: ['orders'], }); }); - - it('should throw an error if the order does not have a food manufacturer', async () => { - const mockOrder: Partial = { - orderId: 1, - request: null, - requestId: 1, - foodManufacturer: null, - shippedBy: null, - status: OrderStatus.SHIPPED, - createdAt: new Date(), - shippedAt: new Date(), - deliveredAt: null, - }; - const mockRequest2: Partial = { - ...mockRequest, - orders: [mockOrder] as Order[], - }; - - const requestId = 1; - const deliveryDate = new Date(); - const feedback = 'Good delivery!'; - const photos = ['photo1.jpg', 'photo2.jpg']; - - mockRequestsRepository.findOne.mockResolvedValueOnce( - mockRequest2 as FoodRequest, - ); - - await expect( - service.updateDeliveryDetails( - requestId, - deliveryDate, - feedback, - photos, - ), - ).rejects.toThrow( - 'No associated food manufacturer found for an associated order', - ); - - expect(mockRequestsRepository.findOne).toHaveBeenCalledWith({ - where: { requestId }, - relations: ['orders'], - }); - }); }); }); diff --git a/apps/backend/src/foodRequests/request.service.ts b/apps/backend/src/foodRequests/request.service.ts index 80093c58..c74b2f87 100644 --- a/apps/backend/src/foodRequests/request.service.ts +++ b/apps/backend/src/foodRequests/request.service.ts @@ -134,16 +134,6 @@ export class RequestsService { ); } - const orders = request.orders; - - for (const order of orders) { - if (!order.shippedBy) { - throw new NotFoundException( - 'No associated food manufacturer found for an associated order', - ); - } - } - request.feedback = feedback; request.dateReceived = deliveryDate; request.photos = photos; diff --git a/apps/backend/src/migrations/1769990652833-UpdateOrderEntity.ts b/apps/backend/src/migrations/1769990652833-UpdateOrderEntity.ts index 780483df..cad6fa61 100644 --- a/apps/backend/src/migrations/1769990652833-UpdateOrderEntity.ts +++ b/apps/backend/src/migrations/1769990652833-UpdateOrderEntity.ts @@ -11,6 +11,9 @@ export class UpdateOrderEntity1769990652833 implements MigrationInterface { SET tracking_link = 'www.samplelink/samplelink', shipping_cost = 20.00 WHERE status = 'delivered' OR status = 'shipped' AND shipped_at IS NOT NULL; + + ALTER TABLE orders + RENAME COLUMN shipped_by TO food_manufacturer_id; `); } @@ -19,6 +22,9 @@ export class UpdateOrderEntity1769990652833 implements MigrationInterface { ALTER TABLE orders DROP COLUMN IF EXISTS tracking_link, DROP COLUMN IF EXISTS shipping_cost; + + ALTER TABLE orders + RENAME COLUMN food_manufacturer_id TO shipped_by; `); } } diff --git a/apps/backend/src/orders/order.controller.spec.ts b/apps/backend/src/orders/order.controller.spec.ts index 980fc6ba..9f84f195 100644 --- a/apps/backend/src/orders/order.controller.spec.ts +++ b/apps/backend/src/orders/order.controller.spec.ts @@ -9,6 +9,8 @@ import { OrderStatus } from './types'; import { FoodRequest } from '../foodRequests/request.entity'; import { Pantry } from '../pantries/pantries.entity'; import { TrackingCostDto } from './dtos/tracking-cost.dto'; +import { BadRequestException } from '@nestjs/common'; +import { FoodManufacturer } from '../foodManufacturers/manufacturers.entity'; const mockOrdersService = mock(); const mockAllocationsService = mock(); @@ -28,21 +30,29 @@ describe('OrdersController', () => { { requestId: 3, pantry: mockPantries[2] as Pantry }, ]; + const mockFoodManufacturer: Partial = { + foodManufacturerId: 1, + foodManufacturerName: 'Test FM', + }; + const mockOrders: Partial[] = [ { orderId: 1, status: OrderStatus.PENDING, request: mockRequests[0] as FoodRequest, + foodManufacturer: mockFoodManufacturer as FoodManufacturer, }, { orderId: 2, status: OrderStatus.DELIVERED, request: mockRequests[1] as FoodRequest, + foodManufacturer: mockFoodManufacturer as FoodManufacturer, }, { orderId: 3, status: OrderStatus.SHIPPED, request: mockRequests[2] as FoodRequest, + foodManufacturer: mockFoodManufacturer as FoodManufacturer, }, ]; @@ -86,6 +96,107 @@ describe('OrdersController', () => { }); }); + describe('getCurrentOrders', () => { + it('should call ordersService.getCurrentOrders and return orders', async () => { + mockOrdersService.getCurrentOrders.mockResolvedValueOnce([ + mockOrders[0], + mockOrders[2], + ] as Order[]); + + const result = await controller.getCurrentOrders(); + + expect(result).toEqual([mockOrders[0], mockOrders[2]] as Order[]); + expect(mockOrdersService.getCurrentOrders).toHaveBeenCalled(); + }); + }); + + describe('getPastOrders', () => { + it('should call ordersService.getPastOrders and return orders', async () => { + mockOrdersService.getPastOrders.mockResolvedValueOnce([ + mockOrders[1], + ] as Order[]); + + const result = await controller.getPastOrders(); + + expect(result).toEqual([mockOrders[1]] as Order[]); + expect(mockOrdersService.getPastOrders).toHaveBeenCalled(); + }); + }); + + describe('getPantryFromOrder', () => { + it('should call ordersService.findOrderPantry and return pantry', async () => { + const orderId = 1; + mockOrdersService.findOrderPantry.mockResolvedValueOnce( + mockPantries[0] as Pantry, + ); + + const result = await controller.getPantryFromOrder(orderId); + + expect(result).toEqual(mockPantries[0] as Pantry); + expect(mockOrdersService.findOrderPantry).toHaveBeenCalledWith(orderId); + }); + }); + + describe('getRequestFromOrder', () => { + it('should call ordersService.findOrderFoodRequest and return food request', async () => { + const orderId = 1; + mockOrdersService.findOrderFoodRequest.mockResolvedValueOnce( + mockRequests[0] as FoodRequest, + ); + + const result = await controller.getRequestFromOrder(orderId); + + expect(result).toEqual(mockRequests[0] as Pantry); + expect(mockOrdersService.findOrderFoodRequest).toHaveBeenCalledWith( + orderId, + ); + }); + }); + + describe('getManufacturerFromOrder', () => { + it('should call ordersService.findOrderFoodManufacturer and return FM', async () => { + const orderId = 1; + mockOrdersService.findOrderFoodManufacturer.mockResolvedValueOnce( + mockFoodManufacturer as FoodManufacturer, + ); + + const result = await controller.getManufacturerFromOrder(orderId); + + expect(result).toEqual(mockFoodManufacturer as FoodManufacturer); + expect(mockOrdersService.findOrderFoodManufacturer).toHaveBeenCalledWith( + orderId, + ); + }); + }); + + describe('getOrder', () => { + it('should call ordersService.findOne and return order', async () => { + const orderId = 1; + mockOrdersService.findOne.mockResolvedValueOnce(mockOrders[0] as Order); + + const result = await controller.getOrder(orderId); + + expect(result).toEqual(mockOrders[0] as Order); + expect(mockOrdersService.findOne).toHaveBeenCalledWith(orderId); + }); + }); + + describe('getOrderByRequestId', () => { + it('should call ordersService.findOrderByRequest and return order', async () => { + const requestId = 1; + mockOrdersService.findOrderByRequest.mockResolvedValueOnce( + mockOrders[0] as Order, + ); + + const result = await controller.getOrderByRequestId(requestId); + + expect(result).toEqual(mockOrders[0] as Order); + expect(mockOrdersService.findOrderByRequest).toHaveBeenCalledWith( + requestId, + ); + }); + }); + describe('getAllAllocationsByOrder', () => { it('should call allocationsService.getAllAllocationsByOrder and return allocations', async () => { const orderId = 1; @@ -102,6 +213,29 @@ describe('OrdersController', () => { }); }); + describe('updateStatus', () => { + it('should call ordersService.updateStatus', async () => { + const status = OrderStatus.DELIVERED; + const orderId = 1; + + await controller.updateStatus(orderId, status); + + expect(mockOrdersService.updateStatus).toHaveBeenCalledWith( + orderId, + status, + ); + }); + + it('should throw with invalid status', async () => { + const invalidStatus = 'invalid status'; + const orderId = 1; + + await expect( + controller.updateStatus(orderId, invalidStatus), + ).rejects.toThrow(new BadRequestException('Invalid status')); + }); + }); + describe('updateTrackingCostInfo', () => { it('should call ordersService.updateTrackingCostInfo with correct parameters', async () => { const orderId = 1; diff --git a/apps/backend/src/orders/order.entity.ts b/apps/backend/src/orders/order.entity.ts index 32145287..2913ff15 100644 --- a/apps/backend/src/orders/order.entity.ts +++ b/apps/backend/src/orders/order.entity.ts @@ -27,15 +27,15 @@ export class Order { @Column({ name: 'request_id' }) requestId!: number; - @ManyToOne(() => FoodManufacturer, { nullable: true }) + @ManyToOne(() => FoodManufacturer, { nullable: false }) @JoinColumn({ - name: 'shipped_by', + name: 'food_manufacturer_id', referencedColumnName: 'foodManufacturerId', }) - foodManufacturer?: FoodManufacturer; + foodManufacturer!: FoodManufacturer; - @Column({ name: 'shipped_by', nullable: true }) - shippedBy?: number; + @Column({ name: 'food_manufacturer_id' }) + foodManufacturerId!: number; @Column({ name: 'status', diff --git a/apps/backend/src/orders/order.service.spec.ts b/apps/backend/src/orders/order.service.spec.ts index e8e41949..808ef510 100644 --- a/apps/backend/src/orders/order.service.spec.ts +++ b/apps/backend/src/orders/order.service.spec.ts @@ -5,6 +5,8 @@ import { Order } from './order.entity'; import { testDataSource } from '../config/typeormTestDataSource'; import { OrderStatus } from './types'; import { Pantry } from '../pantries/pantries.entity'; +import { BadRequestException, NotFoundException } from '@nestjs/common'; +import { TrackingCostDto } from './dtos/tracking-cost.dto'; // Set 1 minute timeout for async DB operations jest.setTimeout(60000); @@ -121,4 +123,275 @@ describe('OrdersService', () => { expect(orders[0].status).toBe(OrderStatus.DELIVERED); }); }); + + describe('getCurrentOrders', () => { + it(`returns only orders with status 'pending' or 'shipped'`, async () => { + const orders = await service.getCurrentOrders(); + expect(orders).toHaveLength(2); + expect( + orders.every( + (order) => + order.status === OrderStatus.PENDING || + order.status === OrderStatus.SHIPPED, + ), + ).toBe(true); + }); + }); + + describe('getPastOrders', () => { + it(`returns only orders with status 'delivered'`, async () => { + const orders = await service.getPastOrders(); + expect(orders).toHaveLength(2); + expect( + orders.every((order) => order.status === OrderStatus.DELIVERED), + ).toBe(true); + }); + }); + + describe('findOne', () => { + it('returns order by ID', async () => { + const orderId = 1; + const result = await service.findOne(orderId); + + expect(result).toBeDefined(); + expect(result.orderId).toBe(1); + }); + + it('throws NotFoundException for non-existent order', async () => { + await expect(service.findOne(9999)).rejects.toThrow( + new NotFoundException('Order 9999 not found'), + ); + }); + }); + + describe('findOrderByRequest', () => { + it('returns order by request ID', async () => { + const order = await service.findOrderByRequest(1); + + expect(order).toBeDefined(); + expect(order.request).toBeDefined(); + expect(order.requestId).toBe(1); + }); + + it('throws NotFoundException for non-existent order', async () => { + await expect(service.findOrderByRequest(9999)).rejects.toThrow( + new NotFoundException('Order with request ID 9999 not found'), + ); + }); + }); + + describe('findOrderFoodRequest', () => { + it('returns food request of order', async () => { + const foodRequest = await service.findOrderFoodRequest(1); + + expect(foodRequest).toBeDefined(); + expect(foodRequest.requestId).toBe(1); + }); + + it('throws NotFoundException for non-existent order', async () => { + await expect(service.findOrderFoodRequest(9999)).rejects.toThrow( + new NotFoundException('Order 9999 not found'), + ); + }); + }); + + describe('findOrderPantry', () => { + it('returns pantry of order', async () => { + const pantry = await service.findOrderPantry(1); + + expect(pantry).toBeDefined(); + expect(pantry.pantryName).toEqual('Community Food Pantry Downtown'); + expect(pantry.pantryId).toEqual(1); + }); + }); + + describe('findOrderFoodManufacturer', () => { + it('returns FM of order', async () => { + const foodManufacturer = await service.findOrderFoodManufacturer(2); + + expect(foodManufacturer).toBeDefined(); + expect(foodManufacturer.foodManufacturerName).toEqual('Healthy Foods Co'); + expect(foodManufacturer.foodManufacturerId).toEqual(2); + }); + + it('throws NotFoundException for non-existent order', async () => { + await expect(service.findOrderFoodManufacturer(9999)).rejects.toThrow( + new NotFoundException('Order 9999 not found'), + ); + }); + }); + + describe('updateStatus', () => { + it('updates order status to delivered', async () => { + const orderId = 3; + const order = await service.findOne(orderId); + + expect(order.status).toEqual(OrderStatus.SHIPPED); + expect(order.shippedAt).toBeDefined(); + + await service.updateStatus(orderId, OrderStatus.DELIVERED); + const updatedOrder = await service.findOne(orderId); + + expect(updatedOrder.status).toEqual(OrderStatus.DELIVERED); + expect(updatedOrder.deliveredAt).toBeDefined(); + }); + + it('updates order status to shipped', async () => { + const orderId = 4; + const order = await service.findOne(orderId); + + expect(order.status).toEqual(OrderStatus.PENDING); + + await service.updateStatus(orderId, OrderStatus.SHIPPED); + const updatedOrder = await service.findOne(orderId); + + expect(updatedOrder.status).toEqual(OrderStatus.SHIPPED); + expect(updatedOrder.shippedAt).toBeDefined(); + expect(updatedOrder.deliveredAt).toBeNull(); + }); + }); + + describe('getOrdersByPantry', () => { + it('returns order from pantry ID', async () => { + const pantryId = 1; + const orders = await service.getOrdersByPantry(pantryId); + + expect(orders.length).toBe(2); + expect(orders.every((order) => order.request)).toBeDefined(); + expect(orders.every((order) => order.request.pantryId === 1)).toBe(true); + }); + + it('returns empty list for pantry with no orderes', async () => { + const pantryId = 5; + const orders = await service.getOrdersByPantry(pantryId); + + expect(orders).toEqual([]); + }); + + it('throws NotFoundException for non-existent pantry', async () => { + const pantryId = 9999; + + await expect(service.getOrdersByPantry(pantryId)).rejects.toThrow( + new NotFoundException(`Pantry ${pantryId} not found`), + ); + }); + }); + + describe('updateTrackingCostInfo', () => { + it('throws when order is non-existent', async () => { + const trackingCostDto: TrackingCostDto = { + trackingLink: 'test', + shippingCost: 5.99, + }; + + await expect( + service.updateTrackingCostInfo(9999, trackingCostDto), + ).rejects.toThrow(new NotFoundException('Order 9999 not found')); + }); + + it('throws when tracking link and shipping cost not given', async () => { + await expect(service.updateTrackingCostInfo(3, {})).rejects.toThrow( + new BadRequestException( + 'At least one of tracking link or shipping cost must be provided', + ), + ); + }); + + it('updates tracking link for shipped order', async () => { + const trackingCostDto: TrackingCostDto = { + trackingLink: 'samplelink.com', + }; + + await service.updateTrackingCostInfo(3, trackingCostDto); + + const order = await service.findOne(3); + expect(order.trackingLink).toBeDefined(); + expect(order.trackingLink).toEqual('samplelink.com'); + }); + + it('updates shipping cost for shipped order', async () => { + const trackingCostDto: TrackingCostDto = { + shippingCost: 12.99, + }; + + await service.updateTrackingCostInfo(3, trackingCostDto); + + const order = await service.findOne(3); + expect(order.shippingCost).toBeDefined(); + expect(order.shippingCost).toEqual('12.99'); + }); + + it('updates both shipping cost and tracking link', async () => { + const trackingCostDto: TrackingCostDto = { + trackingLink: 'testtracking.com', + shippingCost: 7.5, + }; + + await service.updateTrackingCostInfo(3, trackingCostDto); + + const order = await service.findOne(3); + expect(order.trackingLink).toEqual('testtracking.com'); + expect(order.shippingCost).toEqual('7.50'); + }); + + it('throws BadRequestException for delivered order', async () => { + const trackingCostDto: TrackingCostDto = { + trackingLink: 'testtracking.com', + shippingCost: 7.5, + }; + const orderId = 2; + + const order = await service.findOne(orderId); + + expect(order.status).toEqual(OrderStatus.DELIVERED); + + await expect( + service.updateTrackingCostInfo(orderId, trackingCostDto), + ).rejects.toThrow( + new BadRequestException( + 'Can only update tracking info for pending or shipped orders', + ), + ); + }); + + it('throws when both fields are not provided for first time setting', async () => { + const trackingCostDto: TrackingCostDto = { + trackingLink: 'testtracking.com', + }; + const orderId = 4; + + const order = await service.findOne(orderId); + + expect(order.shippedAt).toBeNull(); + expect(order.trackingLink).toBeNull(); + + await expect( + service.updateTrackingCostInfo(4, trackingCostDto), + ).rejects.toThrow( + new BadRequestException( + 'Must provide both tracking link and shipping cost on initial assignment', + ), + ); + }); + + it('sets status to shipped when both fields provided and previous status pending', async () => { + const trackingCostDto: TrackingCostDto = { + trackingLink: 'testtracking.com', + shippingCost: 5.75, + }; + const orderId = 4; + + const order = await service.findOne(orderId); + + expect(order.status).toEqual(OrderStatus.PENDING); + expect(order.shippedAt).toBeNull(); + + await service.updateTrackingCostInfo(orderId, trackingCostDto); + + const updatedOrder = await service.findOne(orderId); + + expect(updatedOrder.status).toEqual(OrderStatus.SHIPPED); + expect(updatedOrder.shippedAt).toBeDefined(); + }); + }); }); diff --git a/apps/backend/src/orders/order.service.ts b/apps/backend/src/orders/order.service.ts index 4948f2fd..b7af3beb 100644 --- a/apps/backend/src/orders/order.service.ts +++ b/apps/backend/src/orders/order.service.ts @@ -20,6 +20,8 @@ export class OrdersService { @InjectRepository(Pantry) private pantryRepo: Repository, ) {} + // TODO: when order is created, set FM + async getAll(filters?: { status?: string; pantryNames?: string[] }) { const qb = this.repo .createQueryBuilder('order') @@ -93,14 +95,14 @@ export class OrdersService { async findOrderPantry(orderId: number): Promise { const request = await this.findOrderFoodRequest(orderId); + if (!request) { + throw new NotFoundException(`Request for order ${orderId} not found`); + } + const pantry = await this.pantryRepo.findOneBy({ pantryId: request.pantryId, }); - if (!pantry) { - throw new NotFoundException(`Pantry ${request.pantryId} not found`); - } - return pantry; } @@ -129,24 +131,17 @@ export class OrdersService { if (!order) { throw new NotFoundException(`Order ${orderId} not found`); } - if (!order.foodManufacturer) { - throw new NotFoundException( - `Order ${orderId} does not have a food manufacturer assigned`, - ); - } return order.foodManufacturer; } async updateStatus(orderId: number, newStatus: OrderStatus) { validateId(orderId, 'Order'); - // TODO: Once we start navigating to proper food manufacturer page, change the 1 to be the proper food manufacturer id await this.repo .createQueryBuilder() .update(Order) .set({ status: newStatus as OrderStatus, - shippedBy: 1, shippedAt: newStatus === OrderStatus.SHIPPED ? new Date() : undefined, deliveredAt: newStatus === OrderStatus.DELIVERED ? new Date() : undefined, diff --git a/apps/backend/src/pantries/pantries.controller.spec.ts b/apps/backend/src/pantries/pantries.controller.spec.ts index 41cac9cb..c0d94332 100644 --- a/apps/backend/src/pantries/pantries.controller.spec.ts +++ b/apps/backend/src/pantries/pantries.controller.spec.ts @@ -210,12 +210,12 @@ describe('PantriesController', () => { { orderId: 26, requestId: 26, - shippedBy: 32, + foodManufacturerId: 32, }, { orderId: 27, requestId: 27, - shippedBy: 33, + foodManufacturerId: 33, }, ]; diff --git a/apps/frontend/src/types/types.ts b/apps/frontend/src/types/types.ts index 9156ecf5..7fc911e6 100644 --- a/apps/frontend/src/types/types.ts +++ b/apps/frontend/src/types/types.ts @@ -193,8 +193,8 @@ export interface Order { orderId: number; request: FoodRequest; requestId: number; - foodManufacturer?: FoodManufacturer; - shippedBy?: number; + foodManufacturer: FoodManufacturer; + foodManufacturerId: number; status: OrderStatus; createdAt: string; shippedAt?: Date;