diff --git a/apps/backend/src/config/migrations.ts b/apps/backend/src/config/migrations.ts index 9573736c..3a3b1f32 100644 --- a/apps/backend/src/config/migrations.ts +++ b/apps/backend/src/config/migrations.ts @@ -25,6 +25,7 @@ import { RemoveUnusedStatuses1764816885341 } from '../migrations/1764816885341-R import { UpdatePantryFields1763762628431 } from '../migrations/1763762628431-UpdatePantryFields'; import { PopulateDummyData1768501812134 } from '../migrations/1768501812134-populateDummyData'; import { RemovePantryFromOrders1769316004958 } from '../migrations/1769316004958-RemovePantryFromOrders'; +import { UpdateOrderEntity1769990652833 } from '../migrations/1769990652833-UpdateOrderEntity'; const schemaMigrations = [ User1725726359198, @@ -54,6 +55,7 @@ const schemaMigrations = [ RemoveUnusedStatuses1764816885341, PopulateDummyData1768501812134, RemovePantryFromOrders1769316004958, + UpdateOrderEntity1769990652833, ]; export default schemaMigrations; diff --git a/apps/backend/src/migrations/1769990652833-UpdateOrderEntity.ts b/apps/backend/src/migrations/1769990652833-UpdateOrderEntity.ts new file mode 100644 index 00000000..780483df --- /dev/null +++ b/apps/backend/src/migrations/1769990652833-UpdateOrderEntity.ts @@ -0,0 +1,24 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class UpdateOrderEntity1769990652833 implements MigrationInterface { + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(` + ALTER TABLE orders + ADD COLUMN IF NOT EXISTS tracking_link VARCHAR(255), + ADD COLUMN IF NOT EXISTS shipping_cost NUMERIC(10,2); + + UPDATE orders + SET tracking_link = 'www.samplelink/samplelink', + shipping_cost = 20.00 + WHERE status = 'delivered' OR status = 'shipped' AND shipped_at IS NOT NULL; + `); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(` + ALTER TABLE orders + DROP COLUMN IF EXISTS tracking_link, + DROP COLUMN IF EXISTS shipping_cost; + `); + } +} diff --git a/apps/backend/src/orders/dtos/tracking-cost.dto.ts b/apps/backend/src/orders/dtos/tracking-cost.dto.ts new file mode 100644 index 00000000..1c29ce6e --- /dev/null +++ b/apps/backend/src/orders/dtos/tracking-cost.dto.ts @@ -0,0 +1,15 @@ +import { IsUrl, IsNumber, Min, IsOptional } from 'class-validator'; + +export class TrackingCostDto { + @IsUrl({}, { message: 'Tracking link must be a valid URL' }) + @IsOptional() + trackingLink?: string; + + @IsNumber( + { maxDecimalPlaces: 2 }, + { message: 'Shipping cost must have at most 2 decimal places' }, + ) + @Min(0, { message: 'Shipping cost cannot be negative' }) + @IsOptional() + shippingCost?: number; +} diff --git a/apps/backend/src/orders/order.controller.spec.ts b/apps/backend/src/orders/order.controller.spec.ts index e2e44818..3ecd7b1c 100644 --- a/apps/backend/src/orders/order.controller.spec.ts +++ b/apps/backend/src/orders/order.controller.spec.ts @@ -8,6 +8,9 @@ import { mock } from 'jest-mock-extended'; 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/manufacturer.entity'; const mockOrdersService = mock(); const mockAllocationsService = mock(); @@ -27,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, }, ]; @@ -85,6 +96,122 @@ 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); + }); + + it('should throw for non-numeric input', async () => { + const invalidInput = NaN; + mockOrdersService.findOrderPantry.mockRejectedValue( + new BadRequestException( + 'Validation failed (numeric string is expected)', + ), + ); + + await expect(controller.getPantryFromOrder(invalidInput)).rejects.toThrow( + new BadRequestException( + 'Validation failed (numeric string is expected)', + ), + ); + }); + }); + + 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; @@ -100,4 +227,43 @@ describe('OrdersController', () => { ).toHaveBeenCalledWith(orderId); }); }); + + 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; + const trackingLink = 'www.samplelink/samplelink'; + const shippingCost = 15.99; + const dto: TrackingCostDto = { trackingLink, shippingCost }; + + await controller.updateTrackingCostInfo(orderId, dto); + + expect(mockOrdersService.updateTrackingCostInfo).toHaveBeenCalledWith( + orderId, + dto, + ); + }); + }); }); diff --git a/apps/backend/src/orders/order.controller.ts b/apps/backend/src/orders/order.controller.ts index 870dc1ef..1093bc29 100644 --- a/apps/backend/src/orders/order.controller.ts +++ b/apps/backend/src/orders/order.controller.ts @@ -7,6 +7,7 @@ import { Body, Query, BadRequestException, + ValidationPipe, } from '@nestjs/common'; import { OrdersService } from './order.service'; import { Order } from './order.entity'; @@ -15,6 +16,7 @@ import { FoodManufacturer } from '../foodManufacturers/manufacturer.entity'; import { FoodRequest } from '../foodRequests/request.entity'; import { AllocationsService } from '../allocations/allocations.service'; import { OrderStatus } from './types'; +import { TrackingCostDto } from './dtos/tracking-cost.dto'; @Controller('orders') export class OrdersController { @@ -99,4 +101,13 @@ export class OrdersController { } return this.ordersService.updateStatus(orderId, newStatus as OrderStatus); } + + @Patch('/:orderId/update-tracking-cost-info') + async updateTrackingCostInfo( + @Param('orderId', ParseIntPipe) orderId: number, + @Body(new ValidationPipe()) + dto: TrackingCostDto, + ): Promise { + return this.ordersService.updateTrackingCostInfo(orderId, dto); + } } diff --git a/apps/backend/src/orders/order.entity.ts b/apps/backend/src/orders/order.entity.ts index 9a246d0c..a0053294 100644 --- a/apps/backend/src/orders/order.entity.ts +++ b/apps/backend/src/orders/order.entity.ts @@ -15,27 +15,27 @@ import { Allocation } from '../allocations/allocations.entity'; @Entity('orders') export class Order { @PrimaryGeneratedColumn({ name: 'order_id' }) - orderId: number; + orderId!: number; @ManyToOne(() => FoodRequest, { nullable: false }) @JoinColumn({ name: 'request_id', referencedColumnName: 'requestId', }) - request: FoodRequest; + request!: FoodRequest; @Column({ name: 'request_id' }) - requestId: number; + requestId!: number; - @ManyToOne(() => FoodManufacturer, { nullable: false }) + @ManyToOne(() => FoodManufacturer, { nullable: true }) @JoinColumn({ name: 'shipped_by', referencedColumnName: 'foodManufacturerId', }) - foodManufacturer: FoodManufacturer; + foodManufacturer?: FoodManufacturer; @Column({ name: 'shipped_by', nullable: true }) - shippedBy: number; + shippedBy?: number; @Column({ name: 'status', @@ -44,29 +44,46 @@ export class Order { enum: OrderStatus, default: OrderStatus.PENDING, }) - status: OrderStatus; + status!: OrderStatus; @CreateDateColumn({ name: 'created_at', type: 'timestamp', default: () => 'NOW()', }) - createdAt: Date; + createdAt!: Date; @Column({ name: 'shipped_at', type: 'timestamp', nullable: true, }) - shippedAt: Date | null; + shippedAt?: Date; @Column({ name: 'delivered_at', type: 'timestamp', nullable: true, }) - deliveredAt: Date | null; + deliveredAt?: Date; @OneToMany(() => Allocation, (allocation) => allocation.order) - allocations: Allocation[]; + allocations!: Allocation[]; + + @Column({ + name: 'tracking_link', + type: 'varchar', + length: 255, + nullable: true, + }) + trackingLink?: string; + + @Column({ + name: 'shipping_cost', + type: 'numeric', + precision: 10, + scale: 2, + nullable: true, + }) + shippingCost?: number; } diff --git a/apps/backend/src/orders/order.service.spec.ts b/apps/backend/src/orders/order.service.spec.ts index e8e41949..e554f11b 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,290 @@ 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 BadRequestException for non positive ID', async () => { + const orderId = 0; + await expect(service.findOne(orderId)).rejects.toThrow( + new BadRequestException('Invalid Order ID'), + ); + }); + + it('throws BadRequestException when not given ID', async () => { + await expect(service.findOne(null)).rejects.toThrow( + new BadRequestException('Invalid Order ID'), + ); + }); + + 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.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); + + 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.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([]); + }); + }); + + 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); + console.log('BEFORE UPDATE:', { + orderId: order.orderId, + status: order.status, + trackingLink: order.trackingLink, + shippingCost: order.shippingCost, + shippedAt: order.shippedAt, + }); + + expect(order.status).toEqual(OrderStatus.PENDING); + expect(order.shippedAt).toBeNull(); + + await service.updateTrackingCostInfo(orderId, trackingCostDto); + + const updatedOrder = await service.findOne(orderId); + console.log('AFTER UPDATE:', { + orderId: updatedOrder.orderId, + status: updatedOrder.status, + trackingLink: updatedOrder.trackingLink, + shippingCost: updatedOrder.shippingCost, + shippedAt: updatedOrder.shippedAt, + }); + 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 fdce0ab3..adf327b7 100644 --- a/apps/backend/src/orders/order.service.ts +++ b/apps/backend/src/orders/order.service.ts @@ -1,4 +1,8 @@ -import { Injectable, NotFoundException } from '@nestjs/common'; +import { + BadRequestException, + Injectable, + NotFoundException, +} from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; import { Repository, In } from 'typeorm'; import { Order } from './order.entity'; @@ -7,6 +11,7 @@ import { FoodManufacturer } from '../foodManufacturers/manufacturer.entity'; import { FoodRequest } from '../foodRequests/request.entity'; import { validateId } from '../utils/validation.utils'; import { OrderStatus } from './types'; +import { TrackingCostDto } from './dtos/tracking-cost.dto'; @Injectable() export class OrdersService { @@ -92,10 +97,6 @@ export class OrdersService { pantryId: request.pantryId, }); - if (!pantry) { - throw new NotFoundException(`Pantry ${request.pantryId} not found`); - } - return pantry; } @@ -124,6 +125,11 @@ 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; } @@ -137,8 +143,9 @@ export class OrdersService { .set({ status: newStatus as OrderStatus, shippedBy: 1, - shippedAt: newStatus === OrderStatus.SHIPPED ? new Date() : null, - deliveredAt: newStatus === OrderStatus.DELIVERED ? new Date() : null, + shippedAt: newStatus === OrderStatus.SHIPPED ? new Date() : undefined, + deliveredAt: + newStatus === OrderStatus.DELIVERED ? new Date() : undefined, }) .where('order_id = :orderId', { orderId }) .execute(); @@ -159,4 +166,49 @@ export class OrdersService { return orders; } + + async updateTrackingCostInfo(orderId: number, dto: TrackingCostDto) { + validateId(orderId, 'Order'); + if (!dto.trackingLink && !dto.shippingCost) { + throw new BadRequestException( + 'At least one of tracking link or shipping cost must be provided', + ); + } + + const order = await this.repo.findOneBy({ orderId }); + if (!order) { + throw new NotFoundException(`Order ${orderId} not found`); + } + + const isFirstTimeSetting = !order.trackingLink && !order.shippingCost; + + if (isFirstTimeSetting && (!dto.trackingLink || !dto.shippingCost)) { + throw new BadRequestException( + 'Must provide both tracking link and shipping cost on initial assignment', + ); + } + + if ( + order.status !== OrderStatus.SHIPPED && + order.status !== OrderStatus.PENDING + ) { + throw new BadRequestException( + 'Can only update tracking info for pending or shipped orders', + ); + } + + if (dto.trackingLink) order.trackingLink = dto.trackingLink; + if (dto.shippingCost) order.shippingCost = dto.shippingCost; + + if ( + order.status === OrderStatus.PENDING && + order.trackingLink && + order.shippingCost + ) { + order.status = OrderStatus.SHIPPED; + order.shippedAt = new Date(); + } + + await this.repo.save(order); + } } diff --git a/apps/frontend/src/types/types.ts b/apps/frontend/src/types/types.ts index 12fd0798..8129c76c 100644 --- a/apps/frontend/src/types/types.ts +++ b/apps/frontend/src/types/types.ts @@ -194,12 +194,15 @@ export interface Order { orderId: number; request: FoodRequest; requestId: number; - foodManufacturer: FoodManufacturer | null; - shippedBy: number | null; + foodManufacturer?: FoodManufacturer; + shippedBy?: number; status: OrderStatus; createdAt: string; - shippedAt: string | null; - deliveredAt: string | null; + shippedAt?: Date; + deliveredAt?: Date; + allocations: Allocation[]; + trackingLink?: string; + shippingCost?: number; } export interface OrderItemDetails {