Skip to content
Merged
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 @@ -7,6 +7,7 @@ import { CreateDonationDto } from './dtos/create-donation.dto';
import { CreateDonationItemDto } from '../donationItems/dtos/create-donation-items.dto';
import { DonationStatus, RecurrenceEnum } from './types';
import { UpdateDonationItemDetailsDto } from '../donationItems/dtos/update-donation-item-details.dto';
import { AuthenticatedRequest } from '../auth/authenticated-request';

const mockDonationService = mock<DonationService>();

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

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

const createdDonation: Partial<Donation> = {
donationId: 1,
...createBody,
Expand All @@ -84,11 +86,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 @@ -8,6 +8,7 @@ import {
ParseArrayPipe,
Get,
Delete,
Req,
} from '@nestjs/common';
import { ApiBody } from '@nestjs/swagger';
import { Donation } from './donations.entity';
Expand All @@ -21,6 +22,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 All @@ -37,7 +39,6 @@ export class DonationsController {
schema: {
type: 'object',
properties: {
foodManufacturerId: { type: 'integer', example: 1 },
recurrence: {
type: 'string',
enum: Object.values(RecurrenceEnum),
Expand Down Expand Up @@ -79,11 +80,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
120 changes: 72 additions & 48 deletions apps/backend/src/donations/donations.service.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,19 @@ import { DonationItemsService } from '../donationItems/donationItems.service';
import { Allocation } from '../allocations/allocations.entity';
import { DataSource, In } from 'typeorm';
import { FoodType } from '../donationItems/types';
import { mock } from 'jest-mock-extended';
import { FoodManufacturersService } from '../foodManufacturers/manufacturers.service';
import { UsersService } from '../users/users.service';
import { EmailsService } from '../emails/email.service';
import { mock } from 'jest-mock-extended';
import { emailTemplates } from '../emails/emailTemplates';

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 @@ -131,8 +138,6 @@ const TODAYOfWeek = (iso: string): DayOfWeek => {
return days[new Date(iso).getDay()];
};

const mockEmailsService = mock<EmailsService>();

describe('DonationService', () => {
let service: DonationService;
let donationItemService: DonationItemsService;
Expand All @@ -151,6 +156,7 @@ describe('DonationService', () => {
providers: [
DonationService,
DonationItemsService,
FoodManufacturersService,
{
provide: getRepositoryToken(Allocation),
useValue: testDataSource.getRepository(Allocation),
Expand All @@ -163,6 +169,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 @@ -1103,11 +1117,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 @@ -1139,13 +1155,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 @@ -1174,36 +1192,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 () => {
await 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 @@ -1219,19 +1241,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
24 changes: 9 additions & 15 deletions apps/backend/src/donations/donations.service.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import {
BadRequestException,
Injectable,
InternalServerErrorException,
Logger,
NotFoundException,
} from '@nestjs/common';
Expand All @@ -13,11 +12,11 @@ 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 { DonationItem } from '../donationItems/donationItems.entity';
import { Allocation } from '../allocations/allocations.entity';
import { FoodManufacturersService } from '../foodManufacturers/manufacturers.service';
import { EmailsService } from '../emails/email.service';
import { emailTemplates } from '../emails/emailTemplates';

Expand All @@ -30,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,
private emailsService: EmailsService,
) {}
Expand All @@ -59,17 +57,13 @@ export class DonationService {
});
}

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 @@ -616,6 +616,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
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