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
2 changes: 2 additions & 0 deletions apps/backend/src/config/migrations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ import { MakeFoodRescueRequired1773889925002 } from '../migrations/1773889925002
import { AddDonationItemConfirmation1774140453305 } from '../migrations/1774140453305-AddDonationItemConfirmation';
import { DonationItemsOnDeleteCascade1774214910101 } from '../migrations/1774214910101-DonationItemsOnDeleteCascade';
import { OrdersVolunteerActions1774883880543 } from '../migrations/1774883880543-OrdersVolunteerActions';
import { UpdateFoodRequestTypesAndOrder1781476891610 } from '../migrations/1781476891610-UpdateFoodTypesAndOrder';

const schemaMigrations = [
User1725726359198,
Expand Down Expand Up @@ -86,6 +87,7 @@ const schemaMigrations = [
AddDonationItemConfirmation1774140453305,
DonationItemsOnDeleteCascade1774214910101,
OrdersVolunteerActions1774883880543,
UpdateFoodRequestTypesAndOrder1781476891610,
];

export default schemaMigrations;
17 changes: 10 additions & 7 deletions apps/backend/src/donationItems/types.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,19 @@
export enum FoodType {
DAIRY_FREE_ALTERNATIVES = 'Dairy-Free Alternatives',
DRIED_BEANS = 'Dried Beans (Gluten-Free, Nut-Free)',
DRIED_BEANS = 'Dried Beans',
FROZEN_MEALS = 'Frozen Meals',
GLUTEN_FREE_BAKING_PANCAKE_MIXES = 'Gluten-Free Baking/Pancake Mixes',
GLUTEN_FREE_BREAD = 'Gluten-Free Bread',
GLUTEN_FREE_TORTILLAS = 'Gluten-Free Tortillas',
GLUTEN_FREE_PASTA = 'Gluten-Free Pasta',
GLUTEN_FREE_TORTILLAS_FROZEN = 'Gluten-Free Tortillas (Frozen)',
GRANOLA = 'Granola',
GRANOLA_BARS = 'Granola Bars',
MASA_HARINA_FLOUR = 'Masa Harina Flour',
NUT_FREE_GRANOLA_BARS = 'Nut-Free Granola Bars',
NON_GMO_COOKIES = 'Non-GMO Cookies',
OLIVE_OIL = 'Olive Oil',
REFRIGERATED_MEALS = 'Refrigerated Meals',
RICE_NOODLES = 'Rice Noodles',
SEED_BUTTERS = 'Seed Butters (Peanut Butter Alternative)',
WHOLE_GRAIN_COOKIES = 'Whole-Grain Cookies',
QUINOA = 'Quinoa',
RICE_CERTIFIED_GLUTEN_FREE = 'Rice (Certified Gluten Free)',
SPREADS_SEED_BUTTERS = 'Spreads/Seed Butters (Peanut Butter Alternative)',
SNACKS = 'Snacks',
TEFF_FLOUR = 'Teff Flour',
}
9 changes: 9 additions & 0 deletions apps/backend/src/foodRequests/dtos/create-request.dto.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,17 @@ export class CreateRequestDto {
@IsEnum(FoodType, { each: true })
requestedFoodTypes!: FoodType[];

@IsString()
@IsNotEmpty()
location!: string;

@IsOptional()
@IsString()
@IsNotEmpty()
additionalInformation?: string;

@IsOptional()
@IsString()
@IsNotEmpty()
feedbackOnPriorDonation?: string;
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,9 @@ export class FoodRequestSummaryDto {
requestId!: number;
requestedSize!: RequestSize;
requestedFoodTypes!: FoodType[];
location!: string;
additionalInformation!: string | null;
feedbackOnPriorDonation!: string | null;
requestedAt!: Date;
status!: FoodRequestStatus;
pantry!: FoodRequestPantry;
Expand Down
10 changes: 10 additions & 0 deletions apps/backend/src/foodRequests/dtos/update-request.dto.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,18 @@ export class UpdateRequestDto {
@IsEnum(FoodType, { each: true })
requestedFoodTypes?: FoodType[];

@IsOptional()
@IsString()
@IsNotEmpty()
location?: string;

@IsOptional()
@IsString()
@IsNotEmpty()
additionalInformation?: string;

@IsOptional()
@IsString()
@IsNotEmpty()
feedbackOnPriorDonation?: string;
}
4 changes: 4 additions & 0 deletions apps/backend/src/foodRequests/request.controller.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -150,7 +150,9 @@ describe('RequestsController', () => {
FoodType.DAIRY_FREE_ALTERNATIVES,
FoodType.DRIED_BEANS,
],
location: 'Boston, MA',
additionalInformation: 'Test information.',
feedbackOnPriorDonation: 'Prior donation feedback.',
};

const createdRequest: Partial<FoodRequest> = {
Expand All @@ -173,7 +175,9 @@ describe('RequestsController', () => {
createBody.pantryId,
createBody.requestedSize,
createBody.requestedFoodTypes,
createBody.location,
createBody.additionalInformation,
createBody.feedbackOnPriorDonation,
);
});
});
Expand Down
11 changes: 11 additions & 0 deletions apps/backend/src/foodRequests/request.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -133,11 +133,20 @@ export class RequestsController {
items: { type: 'string', enum: Object.values(FoodType) },
example: [FoodType.DAIRY_FREE_ALTERNATIVES, FoodType.DRIED_BEANS],
},
location: {
type: 'string',
example: 'Boston, MA',
},
additionalInformation: {
type: 'string',
nullable: true,
example: 'Urgent request',
},
feedbackOnPriorDonation: {
type: 'string',
nullable: true,
example: 'The last donation was well received by our clients.',
},
},
},
})
Expand All @@ -149,7 +158,9 @@ export class RequestsController {
requestData.pantryId,
requestData.requestedSize,
requestData.requestedFoodTypes,
requestData.location,
requestData.additionalInformation,
requestData.feedbackOnPriorDonation,
);
}

Expand Down
6 changes: 6 additions & 0 deletions apps/backend/src/foodRequests/request.entity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,12 @@ export class FoodRequest {
@Column({ name: 'additional_information', type: 'text', nullable: true })
additionalInformation!: string | null;

@Column({ name: 'location', type: 'text' })
location!: string;

@Column({ name: 'feedback_on_prior_donation', type: 'text', nullable: true })
feedbackOnPriorDonation!: string | null;

@CreateDateColumn({
name: 'requested_at',
type: 'timestamp',
Expand Down
84 changes: 55 additions & 29 deletions apps/backend/src/foodRequests/request.service.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -137,7 +137,7 @@ describe('RequestsService', () => {
const firstRequest = sorted[0];
expect(firstRequest.requestedSize).toBe(RequestSize.LARGE);
expect(firstRequest.requestedFoodTypes).toEqual([
FoodType.SEED_BUTTERS,
FoodType.SPREADS_SEED_BUTTERS,
FoodType.GLUTEN_FREE_BREAD,
FoodType.DRIED_BEANS,
FoodType.DAIRY_FREE_ALTERNATIVES,
Expand Down Expand Up @@ -174,13 +174,13 @@ describe('RequestsService', () => {
id: 1,
name: 'Peanut Butter (16oz)',
quantity: 10,
foodType: 'Seed Butters (Peanut Butter Alternative)',
foodType: 'Spreads/Seed Butters (Peanut Butter Alternative)',
},
{
id: 3,
name: 'Canned Green Beans',
quantity: 5,
foodType: 'Refrigerated Meals',
foodType: 'Frozen Meals',
},
{
id: 2,
Expand Down Expand Up @@ -211,11 +211,12 @@ describe('RequestsService', () => {

it('should return empty list if no associated orders', async () => {
const result = await testDataSource.query(`
INSERT INTO food_requests (pantry_id, requested_size, requested_food_types, requested_at)
INSERT INTO food_requests (pantry_id, requested_size, requested_food_types, location, requested_at)
VALUES (
(SELECT pantry_id FROM pantries LIMIT 1),
'Small (2-5 boxes)',
ARRAY[]::food_type_enum[],
'Boston, MA',
NOW()
)
RETURNING request_id
Expand All @@ -232,33 +233,40 @@ describe('RequestsService', () => {
const result = await service.create(
pantryId,
RequestSize.MEDIUM,
[FoodType.DRIED_BEANS, FoodType.REFRIGERATED_MEALS],
[FoodType.DRIED_BEANS, FoodType.FROZEN_MEALS],
'Boston, MA',
'Additional info',
'Prior donation was great',
);
expect(result).toBeDefined();
expect(result.pantryId).toBe(pantryId);
expect(result.requestedSize).toBe(RequestSize.MEDIUM);
expect(result.requestedFoodTypes).toEqual([
FoodType.DRIED_BEANS,
FoodType.REFRIGERATED_MEALS,
FoodType.FROZEN_MEALS,
]);
expect(result.location).toBe('Boston, MA');
expect(result.additionalInformation).toBe('Additional info');
expect(result.feedbackOnPriorDonation).toBe('Prior donation was great');
});

it('should successfully create and return new food request w/o additional info', async () => {
const pantryId = 1;
const result = await service.create(pantryId, RequestSize.LARGE, [
FoodType.GRANOLA,
FoodType.NUT_FREE_GRANOLA_BARS,
]);
const result = await service.create(
pantryId,
RequestSize.LARGE,
[FoodType.GRANOLA, FoodType.GRANOLA_BARS],
'Boston, MA',
);
expect(result).toBeDefined();
expect(result.pantryId).toBe(pantryId);
expect(result.requestedSize).toBe(RequestSize.LARGE);
expect(result.requestedFoodTypes).toEqual([
FoodType.GRANOLA,
FoodType.NUT_FREE_GRANOLA_BARS,
FoodType.GRANOLA_BARS,
]);
expect(result.additionalInformation).toBeNull();
expect(result.feedbackOnPriorDonation).toBeNull();
});

it('should send food request email to pantry user with volunteers BCCed', async () => {
Expand All @@ -268,10 +276,12 @@ describe('RequestsService', () => {
relations: ['pantryUser', 'volunteers'],
});

await service.create(pantryId, RequestSize.MEDIUM, [
FoodType.DRIED_BEANS,
FoodType.REFRIGERATED_MEALS,
]);
await service.create(
pantryId,
RequestSize.MEDIUM,
[FoodType.DRIED_BEANS, FoodType.FROZEN_MEALS],
'Boston, MA',
);

if (!pantry) throw new Error('Missing pantry test object');
const message = emailTemplates.pantrySubmitsFoodRequest({
Expand Down Expand Up @@ -300,10 +310,12 @@ describe('RequestsService', () => {
relations: ['pantryUser', 'volunteers'],
});

await service.create(pantryId, RequestSize.MEDIUM, [
FoodType.DRIED_BEANS,
FoodType.REFRIGERATED_MEALS,
]);
await service.create(
pantryId,
RequestSize.MEDIUM,
[FoodType.DRIED_BEANS, FoodType.FROZEN_MEALS],
'Boston, MA',
);

if (!pantry) throw new Error('Missing pantry test object');
const volunteerEmails = (pantry.volunteers ?? []).map((v) => v.email);
Expand All @@ -319,7 +331,12 @@ describe('RequestsService', () => {

const pantryId = 1;
await expect(
service.create(pantryId, RequestSize.MEDIUM, [FoodType.DRIED_BEANS]),
service.create(
pantryId,
RequestSize.MEDIUM,
[FoodType.DRIED_BEANS],
'Boston, MA',
),
).rejects.toThrow(
new InternalServerErrorException(
'Failed to send new food request notification email to volunteers',
Expand All @@ -335,7 +352,8 @@ describe('RequestsService', () => {
service.create(
999,
RequestSize.MEDIUM,
[FoodType.DRIED_BEANS, FoodType.REFRIGERATED_MEALS],
[FoodType.DRIED_BEANS, FoodType.FROZEN_MEALS],
'Boston, MA',
'Additional info',
),
).rejects.toThrow(new NotFoundException('Pantry 999 not found'));
Expand All @@ -346,7 +364,8 @@ describe('RequestsService', () => {
service.create(
4,
RequestSize.MEDIUM,
[FoodType.DRIED_BEANS, FoodType.REFRIGERATED_MEALS],
[FoodType.DRIED_BEANS, FoodType.FROZEN_MEALS],
'Boston, MA',
'Additional info',
),
).rejects.toThrow(new ConflictException('Pantry 4 not approved'));
Expand All @@ -357,7 +376,8 @@ describe('RequestsService', () => {
service.create(
5,
RequestSize.MEDIUM,
[FoodType.DRIED_BEANS, FoodType.REFRIGERATED_MEALS],
[FoodType.DRIED_BEANS, FoodType.FROZEN_MEALS],
'Boston, MA',
'Additional info',
),
).rejects.toThrow(new ConflictException('Pantry 5 not approved'));
Expand Down Expand Up @@ -405,10 +425,12 @@ describe('RequestsService', () => {

it('should throw BadRequestException for request with no orders', async () => {
const pantryId = 1;
const result = await service.create(pantryId, RequestSize.MEDIUM, [
FoodType.DRIED_BEANS,
FoodType.REFRIGERATED_MEALS,
]);
const result = await service.create(
pantryId,
RequestSize.MEDIUM,
[FoodType.DRIED_BEANS, FoodType.FROZEN_MEALS],
'Boston, MA',
);
const requestId = result.requestId;

await expect(service.updateRequestStatus(requestId)).rejects.toThrow(
Expand Down Expand Up @@ -741,7 +763,7 @@ describe('RequestsService', () => {
FROM donations d
WHERE di.donation_id = d.donation_id
AND d.food_manufacturer_id = 2
AND di.food_type IN ('Whole-Grain Cookies', 'Dairy-Free Alternatives', 'Nut-Free Granola Bars')
AND di.food_type IN ('Non-GMO Cookies', 'Dairy-Free Alternatives', 'Granola Bars')
`);
const result = await service.getAvailableItems(4, 2);
expect(result.matchingItems).toHaveLength(0);
Expand Down Expand Up @@ -785,7 +807,7 @@ describe('RequestsService', () => {
const fromDb = await service.findOne(1);
expect(fromDb.requestedSize).toBe(RequestSize.MEDIUM);
expect(fromDb.requestedFoodTypes).toEqual([
FoodType.SEED_BUTTERS,
FoodType.SPREADS_SEED_BUTTERS,
FoodType.GLUTEN_FREE_BREAD,
FoodType.DRIED_BEANS,
FoodType.DAIRY_FREE_ALTERNATIVES,
Expand All @@ -807,13 +829,17 @@ describe('RequestsService', () => {
await service.update(1, {
requestedSize: RequestSize.SMALL,
requestedFoodTypes: [FoodType.GRANOLA],
location: 'Cambridge, MA',
additionalInformation: 'Updated information',
feedbackOnPriorDonation: 'Updated feedback',
});

const fromDb = await service.findOne(1);
expect(fromDb.requestedSize).toBe(RequestSize.SMALL);
expect(fromDb.requestedFoodTypes).toEqual([FoodType.GRANOLA]);
expect(fromDb.location).toBe('Cambridge, MA');
expect(fromDb.additionalInformation).toBe('Updated information');
expect(fromDb.feedbackOnPriorDonation).toBe('Updated feedback');
});

it('should throw BadRequestException when request is not active', async () => {
Expand Down
Loading
Loading