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
24 changes: 0 additions & 24 deletions apps/backend/src/allocations/allocations.service.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,30 +55,6 @@ describe('AllocationsService', () => {
expect(service).toBeDefined();
});

describe('getAllAllocationsByOrder', () => {
it('should return empty array for order with no allocations', async () => {
await testDataSource.query(`DELETE FROM allocations WHERE order_id = 1`);

const result = await service.getAllAllocationsByOrder(1);

expect(result).toEqual([]);
});

it('should return all allocations for a given order', async () => {
const result = await service.getAllAllocationsByOrder(2);

expect(result).toHaveLength(3);
const quantities = result
.map((a) => a.allocatedQuantity)
.sort((a, b) => a! - b!);
expect(quantities).toEqual([15, 20, 30]);
result.forEach((a) => {
expect(a.allocationId).toBeDefined();
expect(a.item).toBeDefined();
});
});
});

describe('createMultiple', () => {
it('should create a single allocation and increment reservedQuantity', async () => {
const orderId = 1;
Expand Down
13 changes: 0 additions & 13 deletions apps/backend/src/allocations/allocations.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,19 +13,6 @@ export class AllocationsService {
private donationItemRepo: Repository<DonationItem>,
) {}

async getAllAllocationsByOrder(
orderId: number,
): Promise<Partial<Allocation>[]> {
return this.repo.find({
where: { orderId },
relations: ['item'],
select: {
allocationId: true,
allocatedQuantity: true,
},
});
}

// This function assumes that orderId and itemAllocations were already correctly validated (see call in create method of OrdersService)
async createMultiple(
orderId: number,
Expand Down
2 changes: 1 addition & 1 deletion apps/backend/src/auth/ownership.guard.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ export class OwnershipGuard implements CanActivate {
return true;
}

// Specified bypass ownership checks for other roles
// Specified roles bypass ownership checks for other roles
if (config.bypassRoles?.includes(user.role as Role)) {
return true;
}
Expand Down
26 changes: 26 additions & 0 deletions apps/backend/src/donationItems/donationItems.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,38 @@ import {
import { DonationItemsService } from './donationItems.service';
import { DonationItem } from './donationItems.entity';
import { AuthGuard } from '@nestjs/passport';
import { Roles } from '../auth/roles.decorator';
import { Role } from '../users/types';
import {
CheckOwnership,
OwnerIdResolver,
pipeNullable,
} from '../auth/ownership.decorator';
import { DonationService } from '../donations/donations.service';
import { Donation } from '../donations/donations.entity';

const resolveDonationAuthorizedUserIds: OwnerIdResolver = ({
entityId,
services,
}) =>
pipeNullable(
() => services.get(DonationService).findOne(entityId),
(donation: Donation) => [
donation.foodManufacturer.foodManufacturerRepresentative.id,
],
);

@Controller('donation-items')
@UseGuards(AuthGuard('jwt'))
export class DonationItemsController {
constructor(private donationItemsService: DonationItemsService) {}

@CheckOwnership({
idParam: 'donationId',
resolver: resolveDonationAuthorizedUserIds,
bypassRoles: [Role.ADMIN],
})
@Roles(Role.ADMIN, Role.FOODMANUFACTURER)
@Get('/:donationId/all')
async getAllDonationItemsForDonation(
@Param('donationId', ParseIntPipe) donationId: number,
Expand Down
12 changes: 0 additions & 12 deletions apps/backend/src/donations/donations.controller.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -92,18 +92,6 @@ describe('DonationsController', () => {
});
});

describe('PATCH /:donationId/fulfill', () => {
it('should call donationService.fulfill', async () => {
const donationId = 1;

mockDonationService.fulfill.mockResolvedValueOnce(undefined);

await controller.fulfillDonation(donationId);

expect(mockDonationService.fulfill).toHaveBeenCalledWith(donationId);
});
});

describe('PATCH /:donationId/item-details', () => {
it('calls updateDonationItemDetails with the correct donationId and body, returns result', async () => {
const donationId = 1;
Expand Down
65 changes: 44 additions & 21 deletions apps/backend/src/donations/donations.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,19 +18,54 @@ import { UpdateDonationItemDetailsDto } from '../donationItems/dtos/update-donat
import { FoodType } from '../donationItems/types';
import { Roles } from '../auth/roles.decorator';
import { Role } from '../users/types';
import { CheckOwnership, pipeNullable } from '../auth/ownership.decorator';
import {
CheckOwnership,
OwnerIdResolver,
pipeNullable,
} from '../auth/ownership.decorator';
import { FoodManufacturersService } from '../foodManufacturers/manufacturers.service';
import { FoodManufacturer } from '../foodManufacturers/manufacturers.entity';

const resolveDonationAuthorizedUserIds: OwnerIdResolver = ({
entityId,
services,
}) =>
pipeNullable(
() => services.get(DonationService).findOne(entityId),
(donation: Donation) => [
donation.foodManufacturer.foodManufacturerRepresentative.id,
],
);

// For creating a donation, the foodManufacturerId comes from the request body
// and the only authorized non-admin caller is the manufacturer representative.
const resolveCreateDonationAuthorizedUserIds: OwnerIdResolver = ({
entityId,
services,
}) =>
pipeNullable(
() => services.get(FoodManufacturersService).findOne(entityId),
(manufacturer: FoodManufacturer) => [
manufacturer.foodManufacturerRepresentative.id,
],
);

@Controller('donations')
export class DonationsController {
constructor(private donationService: DonationService) {}

@Roles(Role.ADMIN)
@Get()
async getAllDonations(): Promise<Donation[]> {
return this.donationService.getAll();
}

@Roles(Role.FOODMANUFACTURER)
@CheckOwnership({
idParam: 'foodManufacturerId',
idSource: 'body',
resolver: resolveCreateDonationAuthorizedUserIds,
})
@Post()
@ApiBody({
description: 'Details for creating a donation',
Expand Down Expand Up @@ -86,28 +121,10 @@ export class DonationsController {
return this.donationService.create(body);
}

@Patch('/:donationId/fulfill')
async fulfillDonation(
@Param('donationId', ParseIntPipe) donationId: number,
): Promise<void> {
await this.donationService.fulfill(donationId);
}

@Roles(Role.ADMIN, Role.FOODMANUFACTURER)
@Roles(Role.FOODMANUFACTURER)
@CheckOwnership({
idParam: 'donationId',
resolver: async ({ entityId, services }) => {
return pipeNullable(
() => services.get(DonationService).findOne(entityId),
(donation: Donation) =>
services
.get(FoodManufacturersService)
.findOne(donation.foodManufacturer.foodManufacturerId),
(manufacturer: FoodManufacturer) => [
manufacturer.foodManufacturerRepresentative.id,
],
);
},
resolver: resolveDonationAuthorizedUserIds,
})
@Patch('/:donationId/item-details')
async updateDonationItemDetails(
Expand All @@ -118,6 +135,12 @@ export class DonationsController {
await this.donationService.updateDonationItemDetails(donationId, body);
}

@Roles(Role.FOODMANUFACTURER)
@CheckOwnership({
idParam: 'donationId',
resolver: resolveDonationAuthorizedUserIds,
})
@Roles(Role.FOODMANUFACTURER)
@Delete('/:donationId')
async deleteDonation(
@Param('donationId', ParseIntPipe) donationId: number,
Expand Down
5 changes: 4 additions & 1 deletion apps/backend/src/donations/donations.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,10 @@ export class DonationService {

const donation = await this.repo.findOne({
where: { donationId },
relations: ['foodManufacturer'],
relations: [
'foodManufacturer',
'foodManufacturer.foodManufacturerRepresentative',
],
});
if (!donation) {
throw new NotFoundException(`Donation ${donationId} not found`);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@ describe('FoodManufacturersController', () => {
});
});

describe('GET /:foodManufacturerId/donations', () => {
describe('GET /me/donations', () => {
it('should return donation details for a given food manufacturer', async () => {
const mockDonations: Partial<Donation>[] = [
{
Expand Down Expand Up @@ -135,25 +135,29 @@ describe('FoodManufacturersController', () => {

const req = { user: { id: 1 } };

mockManufacturersService.findByUserId.mockResolvedValueOnce({
foodManufacturerId: 1,
} as FoodManufacturer);
mockManufacturersService.getFMDonations.mockResolvedValue(
mockDonationDetails,
);

const result = await controller.getFoodManufacturerDonations(
req as AuthenticatedRequest,
1,
);

expect(result).toBe(mockDonationDetails);
expect(mockManufacturersService.findByUserId).toHaveBeenCalledWith(1);
expect(mockManufacturersService.getFMDonations).toHaveBeenCalledWith(
1,
1,
);
});
});

describe('GET /:foodManufacturerId/next-two-reminders', () => {
it('should return the next two upcoming donation reminders for a given food manufacturer', async () => {
describe('GET /me/next-two-reminders', () => {
it('should return the next two upcoming donation reminders for the authenticated manufacturer', async () => {
const req = { user: { id: 3 } };
const mockDonationReminders: DonationReminderDto[] = [
{
donation: {
Expand All @@ -171,13 +175,19 @@ describe('FoodManufacturersController', () => {
},
];

mockManufacturersService.findByUserId.mockResolvedValueOnce({
foodManufacturerId: 1,
} as FoodManufacturer);
mockManufacturersService.getUpcomingDonationReminders.mockResolvedValue(
mockDonationReminders,
);

const result = await controller.getNextTwoDonationReminders(1);
const result = await controller.getNextTwoDonationReminders(
req as AuthenticatedRequest,
);

expect(result).toEqual(mockDonationReminders);
expect(mockManufacturersService.findByUserId).toHaveBeenCalledWith(3);
expect(
mockManufacturersService.getUpcomingDonationReminders,
).toHaveBeenCalledWith(1);
Expand Down
Loading
Loading