Skip to content
Open
7 changes: 5 additions & 2 deletions apps/backend/src/donations/donations.controller.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ 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';

const mockDonationService = mock<DonationService>();

Expand Down Expand Up @@ -86,7 +87,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 +100,8 @@ describe('DonationsController', () => {
] as CreateDonationItemDto[],
};

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

const createdDonation: Partial<Donation> = {
donationId: 1,
...createBody,
Expand All @@ -112,11 +114,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
10 changes: 6 additions & 4 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,6 +24,7 @@ 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 {
Expand Down Expand Up @@ -51,7 +53,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 +94,12 @@ export class DonationsController {
},
},
})
@Roles(Role.FOODMANUFACTURER)
async createDonation(
@Body()
body: CreateDonationDto,
@Req() req: AuthenticatedRequest,
@Body() body: CreateDonationDto,
): Promise<Donation> {
return this.donationService.create(body);
return this.donationService.create(body, req.user.id);
}

@Patch('/:donationId/fulfill')
Expand Down
118 changes: 73 additions & 45 deletions apps/backend/src/donations/donations.service.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,18 @@ import {
ReplaceDonationItemsDto,
} from '../donationItems/dtos/create-donation-items.dto';
import { FoodType } from '../donationItems/types';
import { FoodManufacturersService } from '../foodManufacturers/manufacturers.service';
import { UsersService } from '../users/users.service';
import { EmailsService } from '../emails/email.service';
import { mock } from 'jest-mock-extended';

jest.setTimeout(60000);

// findByUserId only touches the FoodManufacturer repo, so UsersService and
// EmailsService are mocked to satisfy FoodManufacturersService's DI.
const mockUsersService = mock<UsersService>();
const mockEmailsService = mock<EmailsService>();

const TODAY = new Date();
TODAY.setHours(0, 0, 0, 0);
const MOCK_MONDAY = new Date(2025, 0, 6);
Expand Down Expand Up @@ -148,6 +157,7 @@ describe('DonationService', () => {
providers: [
DonationService,
DonationItemsService,
FoodManufacturersService,
{
provide: getRepositoryToken(Allocation),
useValue: testDataSource.getRepository(Allocation),
Expand All @@ -160,6 +170,14 @@ describe('DonationService', () => {
provide: getRepositoryToken(FoodManufacturer),
useValue: testDataSource.getRepository(FoodManufacturer),
},
{
provide: UsersService,
useValue: mockUsersService,
},
{
provide: EmailsService,
useValue: mockEmailsService,
},
{
provide: getRepositoryToken(DonationItem),
useValue: testDataSource.getRepository(DonationItem),
Expand Down Expand Up @@ -924,11 +942,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,
},
3,
);

expect(donation).toBeDefined();
expect(donation.donationId).toBeDefined();
Expand Down Expand Up @@ -960,13 +980,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,
},
3,
);

const rows = await testDataSource.query(
`SELECT next_donation_dates, occurrences_remaining, recurrence, recurrence_freq
Expand Down Expand Up @@ -995,36 +1017,40 @@ describe('DonationService', () => {
expect(actualDate.getDate()).toEqual(expectedDate.getDate());
});

it('throws when foodManufacturerId does not exist', async () => {
it('throws when user ID is not a food manufacturer', async () => {
expect(
service.create({
foodManufacturerId: 99999,
recurrence: RecurrenceEnum.NONE,
items: validItems,
}),
service.create(
{
recurrence: RecurrenceEnum.NONE,
items: validItems,
},
1,
),
).rejects.toThrow(
new NotFoundException('Food Manufacturer 99999 not found'),
new NotFoundException('Food Manufacturer for User 1 not found'),
);
});

it('throws when recurrence is not NONE but recurrenceFreq is missing', async () => {
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,
}),
3,
),
).rejects.toThrow(
new BadRequestException(
'recurrenceFreq is required for recurring donations',
Expand All @@ -1040,19 +1066,21 @@ 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,
},
],
},
3,
),
).rejects.toThrow();

donations = await testDataSource.query(`SELECT * FROM donations`);
Expand Down
23 changes: 9 additions & 14 deletions apps/backend/src/donations/donations.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,12 @@ import { DayOfWeek, DonationStatus, RecurrenceEnum } from './types';
import { OrderStatus } from '../orders/types';
import { calculateNextDonationDate } from './recurrence.utils';
import { CreateDonationDto, RepeatOnDaysDto } from './dtos/create-donation.dto';
import { FoodManufacturer } from '../foodManufacturers/manufacturers.entity';
import { UpdateDonationItemDetailsDto } from '../donationItems/dtos/update-donation-item-details.dto';
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 { FoodManufacturersService } from '../foodManufacturers/manufacturers.service';

@Injectable()
export class DonationService {
Expand All @@ -29,9 +29,8 @@ export class DonationService {
private allocationRepo: Repository<Allocation>,
@InjectRepository(DonationItem)
private donationItemsRepo: Repository<DonationItem>,
@InjectRepository(FoodManufacturer)
private manufacturerRepo: Repository<FoodManufacturer>,
private donationItemsService: DonationItemsService,
private foodManufacturersService: FoodManufacturersService,
@InjectDataSource() private dataSource: DataSource,
) {}

Expand All @@ -58,17 +57,13 @@ export class DonationService {
return this.repo.count();
}

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

if (!manufacturer) {
throw new NotFoundException(
`Food Manufacturer ${donationData.foodManufacturerId} not found`,
);
}
async create(
donationData: CreateDonationDto,
userId: number,
): Promise<Donation> {
const manufacturer = await this.foodManufacturersService.findByUserId(
userId,
);

let nextDonationDates = null;

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
Original file line number Diff line number Diff line change
Expand Up @@ -611,9 +611,12 @@ describe('FoodManufacturersService', () => {

it('returns next two upcoming donation reminders from same donation', async () => {
const futureDate1 = new Date();
futureDate1.setMilliseconds(0);
futureDate1.setDate(futureDate1.getDate() + 30);
clampDay(futureDate1);

const futureDate2 = new Date();
futureDate2.setMilliseconds(0);
futureDate2.setDate(futureDate2.getDate() + 60);
clampDay(futureDate2);

Expand Down
3 changes: 1 addition & 2 deletions apps/backend/src/users/users.service.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -349,7 +349,6 @@ describe('UsersService', () => {
const now = new Date();

const createDonationBody: Partial<CreateDonationDto> = {
foodManufacturerId: 1,
recurrence: RecurrenceEnum.MONTHLY,
recurrenceFreq: 3,
occurrencesRemaining: 2,
Expand All @@ -363,7 +362,7 @@ describe('UsersService', () => {
],
};

await donationService.create(createDonationBody as CreateDonationDto);
await donationService.create(createDonationBody as CreateDonationDto, 3);

// updating existing request to have a current month requested at date
const existingRequest = await foodRequestService.findOne(1);
Expand Down
5 changes: 3 additions & 2 deletions apps/frontend/src/components/floatingAlert.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import { Alert } from '@chakra-ui/react';
import { useEffect, useState } from 'react';
import { AlertStatus } from '../types/types';

type FloatingAlertProps = {
message?: string | null;
status?: 'info' | 'error';
status?: AlertStatus;
timeout?: number;
};

Expand Down Expand Up @@ -35,7 +36,7 @@ export function FloatingAlert({

return (
<Alert.Root
color={status === 'info' ? 'neutral.800' : 'red'}
color={status === AlertStatus.INFO ? 'neutral.800' : 'red'}
status="info"
bg="white"
alignItems="center"
Expand Down
Loading
Loading