Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
c3b3d8d
added auth gating and modified donations controller/service for backe…
jiang-h-y May 20, 2026
da98675
updated tests
jiang-h-y May 20, 2026
a95b36a
removed fm id
jiang-h-y May 22, 2026
b226841
refactored alert hook + implemented success/error toast
jiang-h-y May 23, 2026
d976889
removed remaining references to FM ID
jiang-h-y May 23, 2026
6c0684b
fixed flaky test
jiang-h-y May 23, 2026
e537533
only allow pending pantries to update pantry applications
jiang-h-y May 24, 2026
35a76f2
only allow updating pantry volunteers for approved pantries
jiang-h-y May 24, 2026
c5d20ac
only allow approved FMs to get upcoming donation reminders
jiang-h-y May 24, 2026
5fc5e31
only allow pending FMs to update applications
jiang-h-y May 24, 2026
e0d0d11
only allow getting orders from pantries that have been approved
jiang-h-y May 24, 2026
74b719f
only allow getting approved manufacturers from orders
jiang-h-y May 24, 2026
c9baa1d
only allow getting requests from orders that have an associated pantr…
jiang-h-y May 24, 2026
8f98c86
only allow creating orders with approved pantries
jiang-h-y May 24, 2026
a6cd1d9
only allow approved FMs to create donations
jiang-h-y May 24, 2026
983c84b
only allow getting available items from approved FMs
jiang-h-y May 24, 2026
887c8e6
only allow approved pantries to create requests
jiang-h-y May 24, 2026
595c29e
only allow approved (not pending) pantries to update pantry applicati…
jiang-h-y May 24, 2026
349cfb0
only allow approved (not pending) FMs to update applications, fixes c…
jiang-h-y May 24, 2026
66bdb96
added tests for findOrderPantry (commit e0d0d11)
jiang-h-y May 24, 2026
cca20f3
added tests for create() in orders (commit 8f98c86)
jiang-h-y May 24, 2026
5d6622b
add tests for findOrderFoodRequest (commit c9baa1d)
jiang-h-y May 24, 2026
e7f3434
Merge branch 'main' into hj/SSF-212-filter-approved-pantry-FM
jiang-h-y May 24, 2026
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
17 changes: 15 additions & 2 deletions apps/backend/src/donations/donations.controller.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,10 @@ import { DonationStatus, RecurrenceEnum } from './types';
import { UpdateDonationItemDetailsDto } from '../donationItems/dtos/update-donation-item-details.dto';
import { ReplaceDonationItemsDto } from '../donationItems/dtos/create-donation-items.dto';
import { FoodType } from '../donationItems/types';
import { AuthenticatedRequest } from '../auth/authenticated-request';
import { FoodManufacturersService } from '../foodManufacturers/manufacturers.service';

const mockFoodManufacturersService = mock<FoodManufacturersService>();
const mockDonationService = mock<DonationService>();

const donation1: Partial<Donation> = {
Expand All @@ -35,6 +38,10 @@ describe('DonationsController', () => {
provide: DonationService,
useValue: mockDonationService,
},
{
provide: FoodManufacturersService,
useValue: mockFoodManufacturersService,
},
],
}).compile();

Expand Down Expand Up @@ -86,7 +93,6 @@ describe('DonationsController', () => {
describe('POST /', () => {
it('should call donationService.create and return the created donation', async () => {
const createBody: Partial<CreateDonationDto> = {
foodManufacturerId: 1,
recurrence: RecurrenceEnum.MONTHLY,
recurrenceFreq: 3,
occurrencesRemaining: 2,
Expand All @@ -100,6 +106,12 @@ describe('DonationsController', () => {
] as CreateDonationItemDto[],
};

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

mockFoodManufacturersService.findByUserId.mockResolvedValueOnce({
foodManufacturerId: 1,
} as any);

const createdDonation: Partial<Donation> = {
donationId: 1,
...createBody,
Expand All @@ -112,11 +124,12 @@ describe('DonationsController', () => {
);

const result = await controller.createDonation(
mockReq as AuthenticatedRequest,
createBody as CreateDonationDto,
);

expect(result).toEqual(createdDonation);
expect(mockDonationService.create).toHaveBeenCalledWith(createBody);
expect(mockDonationService.create).toHaveBeenCalledWith(createBody, 1);
});
});

Expand Down
19 changes: 14 additions & 5 deletions apps/backend/src/donations/donations.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
ParseArrayPipe,
Put,
Delete,
Req,
} from '@nestjs/common';
import { ApiBody } from '@nestjs/swagger';
import { Donation } from './donations.entity';
Expand All @@ -23,10 +24,14 @@ import { Role } from '../users/types';
import { CheckOwnership, pipeNullable } from '../auth/ownership.decorator';
import { FoodManufacturersService } from '../foodManufacturers/manufacturers.service';
import { FoodManufacturer } from '../foodManufacturers/manufacturers.entity';
import { AuthenticatedRequest } from '../auth/authenticated-request';

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

@Get()
async getAllDonations(): Promise<Donation[]> {
Expand All @@ -51,7 +56,6 @@ export class DonationsController {
schema: {
type: 'object',
properties: {
foodManufacturerId: { type: 'integer', example: 1 },
recurrence: {
type: 'string',
enum: Object.values(RecurrenceEnum),
Expand Down Expand Up @@ -93,11 +97,16 @@ export class DonationsController {
},
},
})
@Roles(Role.FOODMANUFACTURER)
async createDonation(
@Body()
body: CreateDonationDto,
@Req() req: AuthenticatedRequest,
@Body() body: CreateDonationDto,
): Promise<Donation> {
return this.donationService.create(body);
const manufacturer = await this.foodManufacturersService.findByUserId(
req.user.id,
);
const foodManufacturerId = manufacturer.foodManufacturerId;
return this.donationService.create(body, foodManufacturerId);
}

@Patch('/:donationId/fulfill')
Expand Down
116 changes: 72 additions & 44 deletions apps/backend/src/donations/donations.service.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,11 @@ import { FoodManufacturer } from '../foodManufacturers/manufacturers.entity';
import { RecurrenceEnum, DayOfWeek, DonationStatus } from './types';
import { RepeatOnDaysDto } from './dtos/create-donation.dto';
import { testDataSource } from '../config/typeormTestDataSource';
import { BadRequestException, NotFoundException } from '@nestjs/common';
import {
BadRequestException,
ConflictException,
NotFoundException,
} from '@nestjs/common';
import { DonationItem } from '../donationItems/donationItems.entity';
import { UpdateDonationItemDetailsDto } from '../donationItems/dtos/update-donation-item-details.dto';
import { DonationItemsService } from '../donationItems/donationItems.service';
Expand Down Expand Up @@ -924,11 +928,13 @@ describe('DonationService', () => {
];

it('successfully creates a donation with items', async () => {
const donation = await service.create({
foodManufacturerId: 1,
recurrence: RecurrenceEnum.NONE,
items: validItems,
});
const donation = await service.create(
{
recurrence: RecurrenceEnum.NONE,
items: validItems,
},
1,
);

expect(donation).toBeDefined();
expect(donation.donationId).toBeDefined();
Expand Down Expand Up @@ -960,13 +966,15 @@ describe('DonationService', () => {
const before = new Date();
before.setHours(0, 0, 0, 0);

const donation = await service.create({
foodManufacturerId: 1,
recurrence: RecurrenceEnum.MONTHLY,
recurrenceFreq: 1,
occurrencesRemaining: 3,
items: validItems,
});
const donation = await service.create(
{
recurrence: RecurrenceEnum.MONTHLY,
recurrenceFreq: 1,
occurrencesRemaining: 3,
items: validItems,
},
1,
);

const rows = await testDataSource.query(
`SELECT next_donation_dates, occurrences_remaining, recurrence, recurrence_freq
Expand Down Expand Up @@ -997,11 +1005,13 @@ describe('DonationService', () => {

it('throws when foodManufacturerId does not exist', async () => {
expect(
service.create({
foodManufacturerId: 99999,
recurrence: RecurrenceEnum.NONE,
items: validItems,
}),
service.create(
{
recurrence: RecurrenceEnum.NONE,
items: validItems,
},
99999,
),
).rejects.toThrow(
new NotFoundException('Food Manufacturer 99999 not found'),
);
Expand All @@ -1011,20 +1021,22 @@ describe('DonationService', () => {
let donations = await testDataSource.query(`SELECT * FROM donations`);
expect(donations).toHaveLength(4);
await expect(
service.create({
foodManufacturerId: 1,
recurrence: RecurrenceEnum.WEEKLY,
repeatOnDays: {
Sunday: false,
Monday: true,
Tuesday: false,
Wednesday: false,
Thursday: false,
Friday: false,
Saturday: false,
service.create(
{
recurrence: RecurrenceEnum.WEEKLY,
repeatOnDays: {
Sunday: false,
Monday: true,
Tuesday: false,
Wednesday: false,
Thursday: false,
Friday: false,
Saturday: false,
},
items: validItems,
},
items: validItems,
}),
1,
),
).rejects.toThrow(
new BadRequestException(
'recurrenceFreq is required for recurring donations',
Expand All @@ -1040,24 +1052,40 @@ describe('DonationService', () => {
expect(donations).toHaveLength(4);

await expect(
service.create({
foodManufacturerId: 1,
recurrence: RecurrenceEnum.NONE,
items: [
...validItems,
{
itemName: 'a'.repeat(1000),
quantity: 5,
foodType: FoodType.DAIRY_FREE_ALTERNATIVES,
foodRescue: false,
},
],
}),
service.create(
{
recurrence: RecurrenceEnum.NONE,
items: [
...validItems,
{
itemName: 'a'.repeat(1000),
quantity: 5,
foodType: FoodType.DAIRY_FREE_ALTERNATIVES,
foodRescue: false,
},
],
},
1,
),
).rejects.toThrow();

donations = await testDataSource.query(`SELECT * FROM donations`);
expect(donations).toHaveLength(4);
});

it('throws ConflictException when foodManufacturerId not approved', async () => {
await expect(
service.create(
{
recurrence: RecurrenceEnum.NONE,
items: validItems,
},
3,
),
).rejects.toThrow(
new ConflictException('Food Manufacturer 3 not approved'),
);
});
});

describe('replaceDonationItems', () => {
Expand Down
19 changes: 15 additions & 4 deletions apps/backend/src/donations/donations.service.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import {
BadRequestException,
ConflictException,
Injectable,
Logger,
NotFoundException,
Expand All @@ -18,6 +19,7 @@ import { DonationItemsService } from '../donationItems/donationItems.service';
import { ReplaceDonationItemsDto } from '../donationItems/dtos/create-donation-items.dto';
import { DonationItem } from '../donationItems/donationItems.entity';
import { Allocation } from '../allocations/allocations.entity';
import { ApplicationStatus } from '../shared/types';

@Injectable()
export class DonationService {
Expand Down Expand Up @@ -58,15 +60,24 @@ export class DonationService {
return this.repo.count();
}

async create(donationData: CreateDonationDto): Promise<Donation> {
validateId(donationData.foodManufacturerId, 'Food Manufacturer');
async create(
donationData: CreateDonationDto,
foodManufacturerId: number,
): Promise<Donation> {
validateId(foodManufacturerId, 'Food Manufacturer');
const manufacturer = await this.manufacturerRepo.findOne({
where: { foodManufacturerId: donationData.foodManufacturerId },
where: { foodManufacturerId },
});

if (!manufacturer) {
throw new NotFoundException(
`Food Manufacturer ${donationData.foodManufacturerId} not found`,
`Food Manufacturer ${foodManufacturerId} not found`,
);
}

if (manufacturer.status !== ApplicationStatus.APPROVED) {
throw new ConflictException(
`Food Manufacturer ${foodManufacturerId} not approved`,
);
}

Expand Down
4 changes: 0 additions & 4 deletions apps/backend/src/donations/dtos/create-donation.dto.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,10 +65,6 @@ export class RepeatOnDaysDto {
}

export class CreateDonationDto {
@IsInt()
@Min(1)
foodManufacturerId!: number;

@IsNotEmpty()
@IsEnum(RecurrenceEnum)
recurrence!: RecurrenceEnum;
Expand Down
25 changes: 25 additions & 0 deletions apps/backend/src/foodManufacturers/manufacturers.service.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ import { PantriesService } from '../pantries/pantries.service';
import { Pantry } from '../pantries/pantries.entity';
import { Allocation } from '../allocations/allocations.entity';
import { RecurrenceEnum } from '../donations/types';
import { UpdateFoodManufacturerApplicationDto } from './dtos/update-manufacturer-application.dto';

jest.setTimeout(60000);

Expand Down Expand Up @@ -616,6 +617,7 @@ describe('FoodManufacturersService', () => {
futureDate1.setMilliseconds(0);
futureDate1.setDate(futureDate1.getDate() + 30);
clampDay(futureDate1);

const futureDate2 = new Date();
futureDate2.setMilliseconds(0);
futureDate2.setDate(futureDate2.getDate() + 60);
Expand Down Expand Up @@ -876,5 +878,28 @@ describe('FoodManufacturersService', () => {
new NotFoundException('Food Manufacturer 9999 not found'),
);
});

it('throws ConflictException for pending manufacturer', async () => {
await expect(service.getUpcomingDonationReminders(3)).rejects.toThrow(
new ConflictException(
'Cannot get donation reminders for a pending food manufacturer',
),
);
});
});

describe(`updateFoodManufacturerApplication`, () => {
it('throws ConflictException for a pending manufacturer', async () => {
const dto: UpdateFoodManufacturerApplicationDto = {
secondaryContactFirstName: 'Jane',
};
await expect(
service.updateFoodManufacturerApplication(3, dto, 5),
).rejects.toThrow(
new ConflictException(
'Cannot update application for a pending manufacturer',
),
);
});
});
});
12 changes: 12 additions & 0 deletions apps/backend/src/foodManufacturers/manufacturers.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,12 @@ export class FoodManufacturersService {
);
}

if (manufacturer.status != ApplicationStatus.APPROVED) {
throw new ConflictException(
`Cannot get donation reminders for a ${manufacturer.status} food manufacturer`,
);
}

const donations = await this.donationsRepo.find({
where: { foodManufacturer: { foodManufacturerId } },
});
Expand Down Expand Up @@ -341,6 +347,12 @@ export class FoodManufacturersService {
);
}

if (manufacturer.status !== ApplicationStatus.APPROVED) {
throw new ConflictException(
`Cannot update application for a ${manufacturer.status} manufacturer`,
);
}

Object.assign(manufacturer, foodManufacturerData);

return this.repo.save(manufacturer);
Expand Down
Loading
Loading