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
45 changes: 0 additions & 45 deletions apps/backend/src/donationItems/donationItems.controller.spec.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,7 @@
import { Test, TestingModule } from '@nestjs/testing';
import { DonationItemsController } from './donationItems.controller';
import { DonationItemsService } from './donationItems.service';
import { DonationItem } from './donationItems.entity';
import { mock } from 'jest-mock-extended';
import { FoodType } from './types';
import { CreateMultipleDonationItemsDto } from './dtos/create-donation-items.dto';

const mockDonationItemsService = mock<DonationItemsService>();

Expand All @@ -25,46 +22,4 @@ describe('DonationItemsController', () => {
it('should be defined', () => {
expect(controller).toBeDefined();
});

describe('createMultipleDonationItems', () => {
it('should call donationItemsService.createMultipleDonationItems with donationId and items, and return the created donation items', async () => {
const mockBody: CreateMultipleDonationItemsDto = {
donationId: 1,
items: [
{
itemName: 'Rice Noodles',
quantity: 100,
reservedQuantity: 0,
ozPerItem: 5,
estimatedValue: 100,
foodType: FoodType.DAIRY_FREE_ALTERNATIVES,
},
{
itemName: 'Beans',
quantity: 50,
reservedQuantity: 0,
ozPerItem: 10,
estimatedValue: 80,
foodType: FoodType.GLUTEN_FREE_BAKING_PANCAKE_MIXES,
},
],
};

const mockCreatedItems: Partial<DonationItem>[] = [
{ itemId: 1, donationId: 1, ...mockBody.items[0] },
{ itemId: 2, donationId: 1, ...mockBody.items[1] },
];

mockDonationItemsService.createMultipleDonationItems.mockResolvedValue(
mockCreatedItems as DonationItem[],
);

const result = await controller.createMultipleDonationItems(mockBody);

expect(
mockDonationItemsService.createMultipleDonationItems,
).toHaveBeenCalledWith(mockBody.donationId, mockBody.items);
expect(result).toEqual(mockCreatedItems);
});
});
});
45 changes: 0 additions & 45 deletions apps/backend/src/donationItems/donationItems.controller.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,14 @@
import {
Controller,
Post,
Body,
Param,
Get,
Patch,
UseGuards,
ParseIntPipe,
} from '@nestjs/common';
import { ApiBody } from '@nestjs/swagger';
import { DonationItemsService } from './donationItems.service';
import { DonationItem } from './donationItems.entity';
import { AuthGuard } from '@nestjs/passport';
import { FoodType } from './types';
import { CreateMultipleDonationItemsDto } from './dtos/create-donation-items.dto';

@Controller('donation-items')
@UseGuards(AuthGuard('jwt'))
Expand All @@ -27,46 +22,6 @@ export class DonationItemsController {
return this.donationItemsService.getAllDonationItems(donationId);
}

@Post('/create-multiple')
@ApiBody({
description: 'Bulk create donation items for a single donation',
schema: {
type: 'object',
properties: {
donationId: {
type: 'integer',
example: 1,
},
items: {
type: 'array',
items: {
type: 'object',
properties: {
itemName: { type: 'string', example: 'Rice Noodles' },
quantity: { type: 'integer', example: 100 },
reservedQuantity: { type: 'integer', example: 0 },
ozPerItem: { type: 'integer', example: 5 },
estimatedValue: { type: 'integer', example: 100 },
foodType: {
type: 'string',
enum: Object.values(FoodType),
example: FoodType.DAIRY_FREE_ALTERNATIVES,
},
},
},
},
},
},
})
async createMultipleDonationItems(
@Body() body: CreateMultipleDonationItemsDto,
): Promise<DonationItem[]> {
return this.donationItemsService.createMultipleDonationItems(
body.donationId,
body.items,
);
}

@Patch('/update-quantity/:itemId')
async updateDonationItemQuantity(
@Param('itemId', ParseIntPipe) itemId: number,
Expand Down
168 changes: 168 additions & 0 deletions apps/backend/src/donationItems/donationItems.service.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,168 @@
import { Test, TestingModule } from '@nestjs/testing';
import { getRepositoryToken } from '@nestjs/typeorm';
import { DonationItem } from './donationItems.entity';
import { DonationItemsService } from './donationItems.service';
import { Donation } from '../donations/donations.entity';
import { FoodType } from './types';
import { NotFoundException } from '@nestjs/common';
import { testDataSource } from '../config/typeormTestDataSource';

jest.setTimeout(60000);

// Get seeded data for tests
async function getSeedDonationId(): Promise<number> {
const result = await testDataSource.query(
`SELECT donation_id FROM donations
WHERE food_manufacturer_id = (
SELECT food_manufacturer_id FROM food_manufacturers
WHERE food_manufacturer_name = 'FoodCorp Industries' LIMIT 1
)
AND status = 'available'
LIMIT 1`,
);
return result[0].donation_id;
}

describe('DonationItemsService', () => {
let service: DonationItemsService;

beforeAll(async () => {
if (!testDataSource.isInitialized) {
await testDataSource.initialize();
}

await testDataSource.query(`DROP SCHEMA IF EXISTS public CASCADE`);
await testDataSource.query(`CREATE SCHEMA public`);

const module: TestingModule = await Test.createTestingModule({
providers: [
DonationItemsService,
{
provide: getRepositoryToken(DonationItem),
useValue: testDataSource.getRepository(DonationItem),
},
{
provide: getRepositoryToken(Donation),
useValue: testDataSource.getRepository(Donation),
},
],
}).compile();

service = module.get<DonationItemsService>(DonationItemsService);
});

beforeEach(async () => {
await testDataSource.query(`DROP SCHEMA IF EXISTS public CASCADE`);
await testDataSource.query(`CREATE SCHEMA public`);
await testDataSource.runMigrations();
});

afterEach(async () => {
await testDataSource.query(`DROP SCHEMA public CASCADE`);
await testDataSource.query(`CREATE SCHEMA public`);
});

afterAll(async () => {
if (testDataSource.isInitialized) {
await testDataSource.destroy();
}
});

it('should be defined', () => {
expect(service).toBeDefined();
});

describe('findOne', () => {
it('returns a donation item by id', async () => {
const result = await testDataSource.query(
`SELECT item_id FROM donation_items WHERE item_name = 'Peanut Butter (16oz)' LIMIT 1`,
);
const itemId = result[0].item_id;

const item = await service.findOne(itemId);
expect(item).toBeDefined();
expect(item.itemId).toEqual(itemId);
});

it('throws NotFoundException when item does not exist', async () => {
await expect(service.findOne(99999)).rejects.toThrow(NotFoundException);
});
});

describe('getAllDonationItems', () => {
it('returns all items for a donation', async () => {
const donationId = await getSeedDonationId();

const items = await service.getAllDonationItems(donationId);

// seed data inserts 3 items for the FoodCorp 150-item donation
expect(items).toHaveLength(3);
});

it('returns empty array when donation has no items', async () => {
const result = await testDataSource.query(
`INSERT INTO donations (food_manufacturer_id, status, recurrence)
VALUES (
(SELECT food_manufacturer_id FROM food_manufacturers
WHERE food_manufacturer_name = 'FoodCorp Industries' LIMIT 1),
'available',
'none'
) RETURNING donation_id`,
);
const emptyDonationId = result[0].donation_id;

const items = await service.getAllDonationItems(emptyDonationId);
expect(items).toHaveLength(0);
});
});

describe('create', () => {
it('successfully creates a donation item on an existing donation', async () => {
const donationId = await getSeedDonationId();

const item = await service.create(
donationId,
'Canned Beans',
10,
15.5,
2.99,
FoodType.DRIED_BEANS,
);

expect(item).toBeDefined();
expect(item.itemId).toBeDefined();
expect(item.quantity).toEqual(10);
});

it('throws NotFoundException when donation does not exist', async () => {
await expect(
service.create(
99999,
'Canned Beans',
10,
15.5,
2.99,
FoodType.DRIED_BEANS,
),
).rejects.toThrow(new NotFoundException('Donation not found'));
});
});

describe('updateDonationItemQuantity', () => {
it('decrements quantity by 1', async () => {
const result = await testDataSource.query(
`SELECT item_id, quantity FROM donation_items WHERE item_name = 'Peanut Butter (16oz)' LIMIT 1`,
);
const { item_id: itemId, quantity } = result[0];

const updated = await service.updateDonationItemQuantity(itemId);
expect(updated.quantity).toEqual(quantity - 1);
});

it('throws NotFoundException when item does not exist', async () => {
await expect(service.updateDonationItemQuantity(99999)).rejects.toThrow(
new NotFoundException(`Donation item 99999 not found`),
);
});
});
});
34 changes: 1 addition & 33 deletions apps/backend/src/donationItems/donationItems.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,6 @@ export class DonationItemsService {
donationId: number,
itemName: string,
quantity: number,
reservedQuantity: number,
ozPerItem: number,
estimatedValue: number,
foodType: FoodType,
Expand All @@ -45,7 +44,7 @@ export class DonationItemsService {
donation,
itemName,
quantity,
reservedQuantity,
reservedQuantity: 0,
ozPerItem,
estimatedValue,
foodType,
Expand All @@ -54,37 +53,6 @@ export class DonationItemsService {
return this.repo.save(donationItem);
}

async createMultipleDonationItems(
donationId: number,
items: {
itemName: string;
quantity: number;
reservedQuantity: number;
ozPerItem?: number;
estimatedValue?: number;
foodType: FoodType;
}[],
): Promise<DonationItem[]> {
validateId(donationId, 'Donation');

const donation = await this.donationRepo.findOneBy({ donationId });
if (!donation) throw new NotFoundException('Donation not found');

const donationItems = items.map((item) =>
this.repo.create({
donation,
itemName: item.itemName,
quantity: item.quantity,
reservedQuantity: item.reservedQuantity,
ozPerItem: item.ozPerItem,
estimatedValue: item.estimatedValue,
foodType: item.foodType,
}),
);

return this.repo.save(donationItems);
}

async updateDonationItemQuantity(itemId: number): Promise<DonationItem> {
validateId(itemId, 'Donation Item');

Expand Down
20 changes: 4 additions & 16 deletions apps/backend/src/donationItems/dtos/create-donation-items.dto.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,13 @@
import {
IsNumber,
IsString,
IsArray,
ValidateNested,
Min,
IsEnum,
IsNotEmpty,
Length,
IsOptional,
IsBoolean,
} from 'class-validator';
import { Type } from 'class-transformer';
import { FoodType } from '../types';

export class CreateDonationItemDto {
Expand All @@ -22,10 +20,6 @@ export class CreateDonationItemDto {
@Min(1)
quantity!: number;

@IsNumber()
@Min(0)
reservedQuantity!: number;

@IsNumber()
@Min(0.01)
@IsOptional()
Expand All @@ -38,14 +32,8 @@ export class CreateDonationItemDto {

@IsEnum(FoodType)
foodType!: FoodType;
}

export class CreateMultipleDonationItemsDto {
@IsNumber()
donationId!: number;

@IsArray()
@ValidateNested({ each: true })
@Type(() => CreateDonationItemDto)
items!: CreateDonationItemDto[];
@IsBoolean()
@IsOptional()
foodRescue?: boolean;
}
Loading
Loading