Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions apps/backend/src/config/migrations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -54,6 +55,7 @@ const schemaMigrations = [
RemoveUnusedStatuses1764816885341,
PopulateDummyData1768501812134,
RemovePantryFromOrders1769316004958,
UpdateOrderEntity1769990652833,
];

export default schemaMigrations;
24 changes: 24 additions & 0 deletions apps/backend/src/migrations/1769990652833-UpdateOrderEntity.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { MigrationInterface, QueryRunner } from 'typeorm';

export class UpdateOrderEntity1769990652833 implements MigrationInterface {
public async up(queryRunner: QueryRunner): Promise<void> {
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<void> {
await queryRunner.query(`
ALTER TABLE orders
DROP COLUMN IF EXISTS tracking_link,
DROP COLUMN IF EXISTS shipping_cost;
`);
}
}
15 changes: 15 additions & 0 deletions apps/backend/src/orders/dtos/tracking-cost.dto.ts
Original file line number Diff line number Diff line change
@@ -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;
}
17 changes: 17 additions & 0 deletions apps/backend/src/orders/order.controller.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ 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';

const mockOrdersService = mock<OrdersService>();
const mockAllocationsService = mock<AllocationsService>();
Expand Down Expand Up @@ -100,4 +101,20 @@ describe('OrdersController', () => {
).toHaveBeenCalledWith(orderId);
});
});

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,
);
});
});
});
11 changes: 11 additions & 0 deletions apps/backend/src/orders/order.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
Body,
Query,
BadRequestException,
ValidationPipe,
} from '@nestjs/common';
import { OrdersService } from './order.service';
import { Order } from './order.entity';
Expand All @@ -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 {
Expand Down Expand Up @@ -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<void> {
return this.ordersService.updateTrackingCostInfo(orderId, dto);
}
}
39 changes: 28 additions & 11 deletions apps/backend/src/orders/order.entity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand All @@ -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;
}
41 changes: 38 additions & 3 deletions apps/backend/src/orders/order.service.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -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 {
Expand Down Expand Up @@ -124,6 +129,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;
}

Expand All @@ -137,8 +147,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();
Expand All @@ -159,4 +170,28 @@ export class OrdersService {

return orders;
}

async updateTrackingCostInfo(orderId: number, dto: TrackingCostDto) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

update with product clarification - fms will initially have to enter both tracking link & cost but can edit either fields later on - could you reflect that in service logic here so that it requires both fields if order does not yet have tracking info

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`);
}
if (order.status !== OrderStatus.SHIPPED) {
throw new BadRequestException(
'Can only update tracking info for shipped orders',
);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

and can you add logic to support this: if order is pending and the tracking list & cost are provided then set the order status to shipped.

}

if (dto.trackingLink) order.trackingLink = dto.trackingLink;
if (dto.shippingCost) order.shippingCost = dto.shippingCost;

await this.repo.save(order);
}
}
11 changes: 7 additions & 4 deletions apps/frontend/src/types/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down