diff --git a/apps/backend/src/config/migrations.ts b/apps/backend/src/config/migrations.ts index 9ebf53b0d..65a88e352 100644 --- a/apps/backend/src/config/migrations.ts +++ b/apps/backend/src/config/migrations.ts @@ -37,6 +37,7 @@ import { AddAssigneeToOrders1773009000618 } from '../migrations/1773009000618-Ad import { DropDonationTotalColumns1772241115031 } from '../migrations/1772241115031-DropDonationTotalColumns'; import { FixTrackingLinks1773041840374 } from '../migrations/1773041840374-FixTrackingLinks'; import { CleanupRequestsAndAllocations1771821377918 } from '../migrations/1771821377918-CleanupRequestsAndAllocations'; +import { MakeFoodRescueRequired1773889925002 } from '../migrations/1773889925002-MakeFoodRescueRequired.ts'; import { AddDonationItemConfirmation1774140453305 } from '../migrations/1774140453305-AddDonationItemConfirmation'; import { DonationItemsOnDeleteCascade1774214910101 } from '../migrations/1774214910101-DonationItemsOnDeleteCascade'; import { OrdersVolunteerActions1774883880543 } from '../migrations/1774883880543-OrdersVolunteerActions'; @@ -81,6 +82,7 @@ const schemaMigrations = [ DropDonationTotalColumns1772241115031, FixTrackingLinks1773041840374, CleanupRequestsAndAllocations1771821377918, + MakeFoodRescueRequired1773889925002, AddDonationItemConfirmation1774140453305, DonationItemsOnDeleteCascade1774214910101, OrdersVolunteerActions1774883880543, diff --git a/apps/backend/src/donationItems/donationItems.controller.spec.ts b/apps/backend/src/donationItems/donationItems.controller.spec.ts index 784a3dc5a..c5b20c18e 100644 --- a/apps/backend/src/donationItems/donationItems.controller.spec.ts +++ b/apps/backend/src/donationItems/donationItems.controller.spec.ts @@ -1,10 +1,7 @@ import { Test, TestingModule } from '@nestjs/testing'; import { DonationItemsController } from './donationItems.controller'; import { DonationItemsService } from './donationItems.service'; -import { DonationItem } from './donationItems.entity'; import { mock } from 'jest-mock-extended'; -import { FoodType } from './types'; -import { CreateMultipleDonationItemsDto } from './dtos/create-donation-items.dto'; const mockDonationItemsService = mock(); @@ -25,46 +22,4 @@ describe('DonationItemsController', () => { it('should be defined', () => { expect(controller).toBeDefined(); }); - - describe('createMultipleDonationItems', () => { - it('should call donationItemsService.createMultipleDonationItems with donationId and items, and return the created donation items', async () => { - const mockBody: CreateMultipleDonationItemsDto = { - donationId: 1, - items: [ - { - itemName: 'Rice Noodles', - quantity: 100, - reservedQuantity: 0, - ozPerItem: 5, - estimatedValue: 100, - foodType: FoodType.DAIRY_FREE_ALTERNATIVES, - }, - { - itemName: 'Beans', - quantity: 50, - reservedQuantity: 0, - ozPerItem: 10, - estimatedValue: 80, - foodType: FoodType.GLUTEN_FREE_BAKING_PANCAKE_MIXES, - }, - ], - }; - - const mockCreatedItems: Partial[] = [ - { itemId: 1, donationId: 1, ...mockBody.items[0] }, - { itemId: 2, donationId: 1, ...mockBody.items[1] }, - ]; - - mockDonationItemsService.createMultipleDonationItems.mockResolvedValue( - mockCreatedItems as DonationItem[], - ); - - const result = await controller.createMultipleDonationItems(mockBody); - - expect( - mockDonationItemsService.createMultipleDonationItems, - ).toHaveBeenCalledWith(mockBody.donationId, mockBody.items); - expect(result).toEqual(mockCreatedItems); - }); - }); }); diff --git a/apps/backend/src/donationItems/donationItems.controller.ts b/apps/backend/src/donationItems/donationItems.controller.ts index 13a47e6df..b4fd4f531 100644 --- a/apps/backend/src/donationItems/donationItems.controller.ts +++ b/apps/backend/src/donationItems/donationItems.controller.ts @@ -1,19 +1,13 @@ import { Controller, - Post, - Body, Param, Get, - Patch, UseGuards, ParseIntPipe, } from '@nestjs/common'; -import { ApiBody } from '@nestjs/swagger'; import { DonationItemsService } from './donationItems.service'; import { DonationItem } from './donationItems.entity'; import { AuthGuard } from '@nestjs/passport'; -import { FoodType } from './types'; -import { CreateMultipleDonationItemsDto } from './dtos/create-donation-items.dto'; @Controller('donation-items') @UseGuards(AuthGuard('jwt')) @@ -26,51 +20,4 @@ export class DonationItemsController { ): Promise { return this.donationItemsService.getAllDonationItems(donationId); } - - @Post('/create-multiple') - @ApiBody({ - description: 'Bulk create donation items for a single donation', - schema: { - type: 'object', - properties: { - donationId: { - type: 'integer', - example: 1, - }, - items: { - type: 'array', - items: { - type: 'object', - properties: { - itemName: { type: 'string', example: 'Rice Noodles' }, - quantity: { type: 'integer', example: 100 }, - reservedQuantity: { type: 'integer', example: 0 }, - ozPerItem: { type: 'integer', example: 5 }, - estimatedValue: { type: 'integer', example: 100 }, - foodType: { - type: 'string', - enum: Object.values(FoodType), - example: FoodType.DAIRY_FREE_ALTERNATIVES, - }, - }, - }, - }, - }, - }, - }) - async createMultipleDonationItems( - @Body() body: CreateMultipleDonationItemsDto, - ): Promise { - return this.donationItemsService.createMultipleDonationItems( - body.donationId, - body.items, - ); - } - - @Patch('/update-quantity/:itemId') - async updateDonationItemQuantity( - @Param('itemId', ParseIntPipe) itemId: number, - ): Promise { - return this.donationItemsService.updateDonationItemQuantity(itemId); - } } diff --git a/apps/backend/src/donationItems/donationItems.entity.ts b/apps/backend/src/donationItems/donationItems.entity.ts index 862ea18b5..9b8fd8061 100644 --- a/apps/backend/src/donationItems/donationItems.entity.ts +++ b/apps/backend/src/donationItems/donationItems.entity.ts @@ -48,8 +48,8 @@ export class DonationItem { @OneToMany(() => Allocation, (allocation) => allocation.item) allocations!: Allocation[]; - @Column({ name: 'food_rescue', type: 'boolean', nullable: true }) - foodRescue!: boolean | null; + @Column({ name: 'food_rescue', type: 'boolean' }) + foodRescue!: boolean; @Column({ name: 'details_confirmed', type: 'boolean' }) detailsConfirmed!: boolean; diff --git a/apps/backend/src/donationItems/donationItems.service.spec.ts b/apps/backend/src/donationItems/donationItems.service.spec.ts new file mode 100644 index 000000000..e487e9aa4 --- /dev/null +++ b/apps/backend/src/donationItems/donationItems.service.spec.ts @@ -0,0 +1,298 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { getRepositoryToken } from '@nestjs/typeorm'; +import { In } from 'typeorm'; +import { DonationItem } from './donationItems.entity'; +import { DonationItemsService } from './donationItems.service'; +import { Donation } from '../donations/donations.entity'; +import { FoodType } from './types'; +import { NotFoundException } from '@nestjs/common'; +import { testDataSource } from '../config/typeormTestDataSource'; +import { CreateDonationItemDto } from './dtos/create-donation-items.dto'; + +jest.setTimeout(60000); + +// Get seeded data for tests +async function getSeedDonationId(): Promise { + const result = await testDataSource.query( + `SELECT donation_id FROM donations + WHERE food_manufacturer_id = ( + SELECT food_manufacturer_id FROM food_manufacturers + WHERE food_manufacturer_name = 'FoodCorp Industries' LIMIT 1 + ) + AND status = 'available' + LIMIT 1`, + ); + return result[0].donation_id; +} + +describe('DonationItemsService', () => { + let service: DonationItemsService; + + beforeAll(async () => { + if (!testDataSource.isInitialized) { + await testDataSource.initialize(); + } + + await testDataSource.query(`DROP SCHEMA IF EXISTS public CASCADE`); + await testDataSource.query(`CREATE SCHEMA public`); + + const module: TestingModule = await Test.createTestingModule({ + providers: [ + DonationItemsService, + { + provide: getRepositoryToken(DonationItem), + useValue: testDataSource.getRepository(DonationItem), + }, + { + provide: getRepositoryToken(Donation), + useValue: testDataSource.getRepository(Donation), + }, + ], + }).compile(); + + service = module.get(DonationItemsService); + }); + + beforeEach(async () => { + await testDataSource.query(`DROP SCHEMA IF EXISTS public CASCADE`); + await testDataSource.query(`CREATE SCHEMA public`); + await testDataSource.runMigrations(); + }); + + afterEach(async () => { + await testDataSource.query(`DROP SCHEMA public CASCADE`); + await testDataSource.query(`CREATE SCHEMA public`); + }); + + afterAll(async () => { + if (testDataSource.isInitialized) { + await testDataSource.destroy(); + } + }); + + it('should be defined', () => { + expect(service).toBeDefined(); + }); + + describe('findOne', () => { + it('returns a donation item by id', async () => { + const result = await testDataSource.query( + `SELECT item_id FROM donation_items WHERE item_name = 'Peanut Butter (16oz)' LIMIT 1`, + ); + const itemId = result[0].item_id; + + const item = await service.findOne(itemId); + expect(item).toBeDefined(); + expect(item.itemId).toEqual(itemId); + expect(item.itemName).toEqual('Peanut Butter (16oz)'); + expect(Number(item.ozPerItem)).toEqual(16.0); + }); + + it('throws NotFoundException when item does not exist', async () => { + await expect(service.findOne(99999)).rejects.toThrow(NotFoundException); + }); + }); + + describe('getAllDonationItems', () => { + it('returns all items for a donation', async () => { + const donationId = await getSeedDonationId(); + + const items = await service.getAllDonationItems(donationId); + + // seed data inserts 3 items for the FoodCorp 150-item donation + expect(items).toHaveLength(3); + }); + + it('returns empty array when donation has no items', async () => { + const result = await testDataSource.query( + `INSERT INTO donations (food_manufacturer_id, status, recurrence) + VALUES ( + (SELECT food_manufacturer_id FROM food_manufacturers + WHERE food_manufacturer_name = 'FoodCorp Industries' LIMIT 1), + 'available', + 'none' + ) RETURNING donation_id`, + ); + const emptyDonationId = result[0].donation_id; + + const items = await service.getAllDonationItems(emptyDonationId); + expect(items).toHaveLength(0); + }); + }); + + describe('create', () => { + it('successfully creates a donation item on an existing donation', async () => { + const donationId = await getSeedDonationId(); + + const item = await service.create( + donationId, + 'Canned Beans', + 10, + 15.5, + 2.99, + FoodType.DRIED_BEANS, + ); + + const itemDb = await service.findOne(item.itemId); + expect(itemDb).toBeDefined(); + expect(itemDb.itemId).toBeDefined(); + expect(itemDb.donationId).toEqual(donationId); + expect(itemDb.itemName).toEqual('Canned Beans'); + expect(itemDb.quantity).toEqual(10); + expect(itemDb.reservedQuantity).toEqual(0); + expect(Number(itemDb.ozPerItem)).toEqual(15.5); + expect(Number(itemDb.estimatedValue)).toEqual(2.99); + expect(itemDb.foodType).toEqual(FoodType.DRIED_BEANS); + expect(itemDb.foodRescue).toEqual(false); + }); + + it('throws NotFoundException when donation does not exist', async () => { + await expect( + service.create( + 99999, + 'Canned Beans', + 10, + 15.5, + 2.99, + FoodType.DRIED_BEANS, + ), + ).rejects.toThrow(new NotFoundException('Donation not found')); + }); + }); + + describe('createMultiple', () => { + const validItems: CreateDonationItemDto[] = [ + { + itemName: 'Canned Beans', + quantity: 10, + ozPerItem: 15.5, + estimatedValue: 2.99, + foodType: FoodType.DRIED_BEANS, + foodRescue: false, + }, + { + itemName: 'Rice Bag', + quantity: 5, + ozPerItem: 32, + estimatedValue: 4.99, + foodType: FoodType.GRANOLA, + foodRescue: true, + }, + ]; + + async function getSeedDonation(): Promise { + const donationId = await getSeedDonationId(); + return testDataSource + .getRepository(Donation) + .findOneByOrFail({ donationId }); + } + + it('creates all items with correct fields persisted to the database', async () => { + const donation = await getSeedDonation(); + const transactionManager = testDataSource.createEntityManager(); + + const result = await service.createMultiple( + donation, + validItems, + transactionManager, + ); + + expect(result).toHaveLength(2); + + const itemRepo = testDataSource.getRepository(DonationItem); + const [beans, rice] = await itemRepo.findBy({ + itemId: In(result.map((i) => i.itemId)), + }); + + expect(beans.itemId).toBeDefined(); + expect(beans.donationId).toEqual(donation.donationId); + expect(beans.itemName).toEqual('Canned Beans'); + expect(beans.quantity).toEqual(10); + expect(beans.reservedQuantity).toEqual(0); + expect(Number(beans.ozPerItem)).toEqual(15.5); + expect(Number(beans.estimatedValue)).toEqual(2.99); + expect(beans.foodType).toEqual(FoodType.DRIED_BEANS); + expect(beans.foodRescue).toEqual(false); + + expect(rice.itemId).toBeDefined(); + expect(rice.donationId).toEqual(donation.donationId); + expect(rice.itemName).toEqual('Rice Bag'); + expect(rice.quantity).toEqual(5); + expect(rice.reservedQuantity).toEqual(0); + expect(Number(rice.ozPerItem)).toEqual(32); + expect(Number(rice.estimatedValue)).toEqual(4.99); + expect(rice.foodType).toEqual(FoodType.GRANOLA); + expect(rice.foodRescue).toEqual(true); + }); + + it('creates items with optional fields omitted', async () => { + const donation = await getSeedDonation(); + const transactionManager = testDataSource.createEntityManager(); + + const minimalItems: CreateDonationItemDto[] = [ + { + itemName: 'Plain Item', + quantity: 3, + foodType: FoodType.DRIED_BEANS, + foodRescue: true, + }, + ]; + + const result = await service.createMultiple( + donation, + minimalItems, + transactionManager, + ); + + expect(result).toHaveLength(1); + expect(result[0].itemId).toBeDefined(); + expect(result[0].ozPerItem).toBeNull(); + expect(result[0].estimatedValue).toBeNull(); + }); + + it('rolls back all items when one fails within a transaction', async () => { + const donation = await getSeedDonation(); + + const itemsBefore = await testDataSource.query( + `SELECT * FROM donation_items WHERE donation_id = $1`, + [donation.donationId], + ); + + const badItems: CreateDonationItemDto[] = [ + ...validItems, + { + itemName: 'a'.repeat(1000), + quantity: 5, + foodType: FoodType.DRIED_BEANS, + foodRescue: false, + }, + ]; + + await expect( + testDataSource.transaction(async (transactionManager) => { + await service.createMultiple(donation, badItems, transactionManager); + }), + ).rejects.toThrow(); + + const itemsAfter = await testDataSource.query( + `SELECT * FROM donation_items WHERE donation_id = $1`, + [donation.donationId], + ); + + expect(itemsAfter).toHaveLength(itemsBefore.length); + }); + + it('returns empty array when given empty items list', async () => { + const donation = await getSeedDonation(); + const transactionManager = testDataSource.createEntityManager(); + + const result = await service.createMultiple( + donation, + [], + transactionManager, + ); + + expect(result).toHaveLength(0); + }); + }); +}); diff --git a/apps/backend/src/donationItems/donationItems.service.ts b/apps/backend/src/donationItems/donationItems.service.ts index 76b9651c6..3bf2041f1 100644 --- a/apps/backend/src/donationItems/donationItems.service.ts +++ b/apps/backend/src/donationItems/donationItems.service.ts @@ -1,10 +1,11 @@ import { Injectable, NotFoundException } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; -import { In, Repository } from 'typeorm'; +import { EntityManager, Repository, In } from 'typeorm'; import { DonationItem } from './donationItems.entity'; import { validateId } from '../utils/validation.utils'; import { FoodType } from './types'; import { Donation } from '../donations/donations.entity'; +import { CreateDonationItemDto } from './dtos/create-donation-items.dto'; @Injectable() export class DonationItemsService { @@ -75,7 +76,6 @@ export class DonationItemsService { donationId: number, itemName: string, quantity: number, - reservedQuantity: number, ozPerItem: number, estimatedValue: number, foodType: FoodType, @@ -88,7 +88,7 @@ export class DonationItemsService { donation, itemName, quantity, - reservedQuantity, + reservedQuantity: 0, ozPerItem, estimatedValue, foodType, @@ -97,45 +97,25 @@ export class DonationItemsService { return this.repo.save(donationItem); } - async createMultipleDonationItems( - donationId: number, - items: { - itemName: string; - quantity: number; - reservedQuantity: number; - ozPerItem?: number; - estimatedValue?: number; - foodType: FoodType; - }[], + async createMultiple( + savedDonation: Donation, + items: CreateDonationItemDto[], + transactionManager: EntityManager, ): Promise { - validateId(donationId, 'Donation'); - - const donation = await this.donationRepo.findOneBy({ donationId }); - if (!donation) throw new NotFoundException('Donation not found'); + const transactionRepo = transactionManager.getRepository(DonationItem); const donationItems = items.map((item) => - this.repo.create({ - donation, + transactionRepo.create({ + donation: savedDonation, itemName: item.itemName, quantity: item.quantity, - reservedQuantity: item.reservedQuantity, + reservedQuantity: 0, ozPerItem: item.ozPerItem, estimatedValue: item.estimatedValue, foodType: item.foodType, + foodRescue: item.foodRescue, }), ); - - return this.repo.save(donationItems); - } - - async updateDonationItemQuantity(itemId: number): Promise { - validateId(itemId, 'Donation Item'); - - const donationItem = await this.repo.findOneBy({ itemId }); - if (!donationItem) { - throw new NotFoundException(`Donation item ${itemId} not found`); - } - donationItem.quantity -= 1; - return this.repo.save(donationItem); + return transactionRepo.save(donationItems); } } diff --git a/apps/backend/src/donationItems/dtos/create-donation-items.dto.ts b/apps/backend/src/donationItems/dtos/create-donation-items.dto.ts index d4734d097..84c662cc7 100644 --- a/apps/backend/src/donationItems/dtos/create-donation-items.dto.ts +++ b/apps/backend/src/donationItems/dtos/create-donation-items.dto.ts @@ -9,8 +9,9 @@ import { Length, IsOptional, IsInt, + IsBoolean, } from 'class-validator'; -import { Transform, Type } from 'class-transformer'; +import { Type } from 'class-transformer'; import { FoodType } from '../types'; export class CreateDonationItemDto { @@ -19,45 +20,31 @@ export class CreateDonationItemDto { @Length(1, 255) itemName!: string; - @Transform(({ value }) => parseInt(value, 10)) - @IsInt({ message: 'Quantity must be an integer value' }) - @Min(1, { message: 'Quantity must be at least 1' }) - quantity!: number; - @IsInt() - @Min(0) - reservedQuantity!: number; + @Min(1) + quantity!: number; - @Transform(({ value }) => parseFloat(value)) @IsNumber( { maxDecimalPlaces: 2 }, - { message: 'Oz per item must have at most 2 decimal places' }, + { message: 'ozPerItem must have at most 2 decimal places' }, ) - @Min(0.01, { message: 'Oz per item must be at least 0.01' }) + @Min(0.01) @IsOptional() ozPerItem?: number; - @Transform(({ value }) => parseFloat(value)) @IsNumber( { maxDecimalPlaces: 2 }, - { message: 'Estimated value must have at most 2 decimal places' }, + { message: 'estimatedValue must have at most 2 decimal places' }, ) - @Min(0.01, { message: 'Estimated value must be at least 0.01' }) + @Min(0.01) @IsOptional() estimatedValue?: number; @IsEnum(FoodType) foodType!: FoodType; -} -export class CreateMultipleDonationItemsDto { - @IsNumber() - donationId!: number; - - @IsArray() - @ValidateNested({ each: true }) - @Type(() => CreateDonationItemDto) - items!: CreateDonationItemDto[]; + @IsBoolean() + foodRescue!: boolean; } export class ReplaceDonationItemDto extends CreateDonationItemDto { diff --git a/apps/backend/src/donations/donations.controller.spec.ts b/apps/backend/src/donations/donations.controller.spec.ts index 4ad76efdf..20174adc8 100644 --- a/apps/backend/src/donations/donations.controller.spec.ts +++ b/apps/backend/src/donations/donations.controller.spec.ts @@ -4,6 +4,7 @@ import { Test, TestingModule } from '@nestjs/testing'; import { mock } from 'jest-mock-extended'; import { Donation } from './donations.entity'; import { CreateDonationDto } from './dtos/create-donation.dto'; +import { CreateDonationItemDto } from '../donationItems/dtos/create-donation-items.dto'; import { DonationStatus, RecurrenceEnum } from './types'; import { DonationItem } from '../donationItems/donationItems.entity'; import { ReplaceDonationItemsDto } from '../donationItems/dtos/create-donation-items.dto'; @@ -82,13 +83,21 @@ describe('DonationsController', () => { }); }); - describe('POST /create', () => { + describe('POST /', () => { it('should call donationService.create and return the created donation', async () => { const createBody: Partial = { foodManufacturerId: 1, recurrence: RecurrenceEnum.MONTHLY, recurrenceFreq: 3, occurrencesRemaining: 2, + items: [ + { + itemName: 'Item 1', + } as CreateDonationItemDto, + { + itemName: 'Item 2', + } as CreateDonationItemDto, + ] as CreateDonationItemDto[], }; const createdDonation: Partial = { diff --git a/apps/backend/src/donations/donations.controller.ts b/apps/backend/src/donations/donations.controller.ts index 9f930bc35..7085d97fb 100644 --- a/apps/backend/src/donations/donations.controller.ts +++ b/apps/backend/src/donations/donations.controller.ts @@ -14,6 +14,7 @@ import { Donation } from './donations.entity'; import { DonationService } from './donations.service'; import { RecurrenceEnum } from './types'; import { CreateDonationDto } from './dtos/create-donation.dto'; +import { FoodType } from '../donationItems/types'; import { ReplaceDonationItemsDto } from '../donationItems/dtos/create-donation-items.dto'; @Controller('donations') @@ -37,7 +38,7 @@ export class DonationsController { return this.donationService.findOne(donationId); } - @Post('/create') + @Post() @ApiBody({ description: 'Details for creating a donation', schema: { @@ -64,6 +65,24 @@ export class DonationsController { }, }, occurrencesRemaining: { type: 'integer', example: 2, nullable: true }, + items: { + type: 'array', + items: { + type: 'object', + properties: { + itemName: { type: 'string', example: 'Canned Beans' }, + quantity: { type: 'integer', example: 1 }, + ozPerItem: { type: 'number', example: 0.01, nullable: true }, + estimatedValue: { type: 'number', example: 0.01, nullable: true }, + foodType: { + type: 'enum', + enum: Object.values(FoodType), + example: FoodType.QUINOA, + }, + foodRescue: { type: 'boolean', example: false }, + }, + }, + }, }, }, }) diff --git a/apps/backend/src/donations/donations.service.spec.ts b/apps/backend/src/donations/donations.service.spec.ts index 8694fac2f..b37427025 100644 --- a/apps/backend/src/donations/donations.service.spec.ts +++ b/apps/backend/src/donations/donations.service.spec.ts @@ -6,9 +6,7 @@ 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 { NotFoundException } from '@nestjs/common'; -import { DonationItemsService } from '../donationItems/donationItems.service'; -import { DonationItem } from '../donationItems/donationItems.entity'; +import { BadRequestException, NotFoundException } from '@nestjs/common'; import { Allocation } from '../allocations/allocations.entity'; import { DataSource, In } from 'typeorm'; import { @@ -16,6 +14,8 @@ import { ReplaceDonationItemsDto, } from '../donationItems/dtos/create-donation-items.dto'; import { FoodType } from '../donationItems/types'; +import { DonationItemsService } from '../donationItems/donationItems.service'; +import { DonationItem } from '../donationItems/donationItems.entity'; jest.setTimeout(60000); @@ -860,6 +860,163 @@ describe('DonationService', () => { }); }); + describe('create', () => { + const validItems = [ + { + itemName: 'Canned Beans', + quantity: 10, + ozPerItem: 15.5, + estimatedValue: 2.99, + foodType: FoodType.DAIRY_FREE_ALTERNATIVES, + foodRescue: false, + }, + { + itemName: 'Canned Corn', + quantity: 5, + ozPerItem: 12, + estimatedValue: 1.99, + foodType: FoodType.GRANOLA, + foodRescue: true, + }, + ]; + + it('successfully creates a donation with items', async () => { + const donation = await service.create({ + foodManufacturerId: 1, + recurrence: RecurrenceEnum.NONE, + items: validItems, + }); + + expect(donation).toBeDefined(); + expect(donation.donationId).toBeDefined(); + + const donationDb = await testDataSource.query( + `SELECT * FROM donations WHERE donation_id = $1`, + [donation.donationId], + ); + + expect(donationDb).toHaveLength(1); + const donationRow = donationDb[0]; + expect(donationRow.donation_id).toEqual(donation.donationId); + expect(donationRow.food_manufacturer_id).toEqual(1); + expect(donationRow.status).toEqual(DonationStatus.AVAILABLE); + expect(donationRow.recurrence).toEqual(RecurrenceEnum.NONE); + expect(donationRow.recurrence_freq).toBeNull(); + expect(donationRow.next_donation_dates).toBeNull(); + expect(donationRow.occurrences_remaining).toBeNull(); + + const items = await testDataSource.query( + `SELECT * FROM donation_items WHERE donation_id = $1`, + [donation.donationId], + ); + + expect(items).toHaveLength(2); + }); + + it('populates nextDonationDates in the database for a recurring donation', async () => { + 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 rows = await testDataSource.query( + `SELECT next_donation_dates, occurrences_remaining, recurrence, recurrence_freq + FROM donations WHERE donation_id = $1`, + [donation.donationId], + ); + + expect(rows).toHaveLength(1); + const row = rows[0]; + + expect(row.recurrence).toEqual(RecurrenceEnum.MONTHLY); + expect(row.recurrence_freq).toEqual(1); + expect(row.occurrences_remaining).toEqual(3); + + const dates: Date[] = row.next_donation_dates; + expect(dates).toHaveLength(1); + + // Clip the before date if necessary + const expectedDate = new Date(before); + if (expectedDate.getDate() > 28) expectedDate.setDate(28); + expectedDate.setMonth(expectedDate.getMonth() + 1); + + const actualDate = new Date(dates[0]); + expect(actualDate.getFullYear()).toEqual(expectedDate.getFullYear()); + expect(actualDate.getMonth()).toEqual(expectedDate.getMonth()); + expect(actualDate.getDate()).toEqual(expectedDate.getDate()); + }); + + it('throws when foodManufacturerId does not exist', async () => { + expect( + service.create({ + foodManufacturerId: 99999, + recurrence: RecurrenceEnum.NONE, + items: validItems, + }), + ).rejects.toThrow( + new NotFoundException('Food Manufacturer 99999 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, + }, + items: validItems, + }), + ).rejects.toThrow( + new BadRequestException( + 'recurrenceFreq is required for recurring donations', + ), + ); + + donations = await testDataSource.query(`SELECT * FROM donations`); + expect(donations).toHaveLength(4); + }); + + it('rolls back donation when a donation item fails to save', async () => { + let donations = await testDataSource.query(`SELECT * FROM donations`); + 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, + }, + ], + }), + ).rejects.toThrow(); + + donations = await testDataSource.query(`SELECT * FROM donations`); + expect(donations).toHaveLength(4); + }); + }); + describe('replaceDonationItems', () => { it('should replace donation items for an available donation', async () => { const donationId = 1; diff --git a/apps/backend/src/donations/donations.service.ts b/apps/backend/src/donations/donations.service.ts index 6fefadb03..ef638e884 100644 --- a/apps/backend/src/donations/donations.service.ts +++ b/apps/backend/src/donations/donations.service.ts @@ -11,6 +11,7 @@ import { validateId } from '../utils/validation.utils'; import { DayOfWeek, DonationStatus, RecurrenceEnum } from './types'; import { CreateDonationDto, RepeatOnDaysDto } from './dtos/create-donation.dto'; import { FoodManufacturer } from '../foodManufacturers/manufacturers.entity'; +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'; @@ -27,6 +28,7 @@ export class DonationService { private donationItemsRepo: Repository, @InjectRepository(FoodManufacturer) private manufacturerRepo: Repository, + private donationItemsService: DonationItemsService, @InjectDataSource() private dataSource: DataSource, ) {} @@ -82,17 +84,29 @@ export class DonationService { ); } - const donation = this.repo.create({ - foodManufacturer: manufacturer, - dateDonated: new Date(), - status: DonationStatus.AVAILABLE, - recurrence: donationData.recurrence, - recurrenceFreq: donationData.recurrenceFreq, - nextDonationDates: nextDonationDates, - occurrencesRemaining: donationData.occurrencesRemaining, - }); + return this.dataSource.transaction(async (transactionManager) => { + const transactionRepo = transactionManager.getRepository(Donation); + + const donation = transactionRepo.create({ + foodManufacturer: manufacturer, + dateDonated: new Date(), + status: DonationStatus.AVAILABLE, + recurrence: donationData.recurrence, + recurrenceFreq: donationData.recurrenceFreq, + nextDonationDates, + occurrencesRemaining: donationData.occurrencesRemaining, + }); + + const savedDonation = await transactionRepo.save(donation); + + await this.donationItemsService.createMultiple( + savedDonation, + donationData.items, + transactionManager, + ); - return this.repo.save(donation); + return savedDonation; + }); } async fulfill(donationId: number): Promise { diff --git a/apps/backend/src/donations/dtos/create-donation.dto.ts b/apps/backend/src/donations/dtos/create-donation.dto.ts index fca118c16..523e6c085 100644 --- a/apps/backend/src/donations/dtos/create-donation.dto.ts +++ b/apps/backend/src/donations/dtos/create-donation.dto.ts @@ -1,8 +1,10 @@ import { + ArrayMinSize, + IsArray, IsBoolean, IsEnum, + IsInt, IsNotEmpty, - IsNumber, IsObject, IsOptional, Min, @@ -12,6 +14,7 @@ import { } from 'class-validator'; import { RecurrenceEnum } from '../types'; import { Type } from 'class-transformer'; +import { CreateDonationItemDto } from '../../donationItems/dtos/create-donation-items.dto'; function AtLeastOneDaySelected() { return function (object: object, propertyName: string) { @@ -23,6 +26,9 @@ function AtLeastOneDaySelected() { validate(value: Record) { return !!value && Object.values(value).some((v) => v === true); }, + defaultMessage() { + return 'At least one day must be selected for weekly recurrence'; + }, }, }); }; @@ -59,7 +65,7 @@ export class RepeatOnDaysDto { } export class CreateDonationDto { - @IsNumber() + @IsInt() @Min(1) foodManufacturerId!: number; @@ -67,7 +73,7 @@ export class CreateDonationDto { @IsEnum(RecurrenceEnum) recurrence!: RecurrenceEnum; - @IsNumber() + @IsInt() @ValidateIf((o) => o.recurrence !== RecurrenceEnum.NONE) @Min(1) recurrenceFreq?: number; @@ -79,8 +85,14 @@ export class CreateDonationDto { @ValidateIf((o) => o.recurrence === RecurrenceEnum.WEEKLY) repeatOnDays?: RepeatOnDaysDto; - @IsNumber() + @IsInt() @ValidateIf((o) => o.recurrence !== RecurrenceEnum.NONE) @Min(1) occurrencesRemaining?: number; + + @IsArray() + @ArrayMinSize(1) + @ValidateNested({ each: true }) + @Type(() => CreateDonationItemDto) + items!: CreateDonationItemDto[]; } diff --git a/apps/backend/src/foodManufacturers/manufacturers.module.ts b/apps/backend/src/foodManufacturers/manufacturers.module.ts index 07b734a7c..d44c97155 100644 --- a/apps/backend/src/foodManufacturers/manufacturers.module.ts +++ b/apps/backend/src/foodManufacturers/manufacturers.module.ts @@ -6,12 +6,14 @@ import { FoodManufacturersService } from './manufacturers.service'; import { UsersModule } from '../users/users.module'; import { Donation } from '../donations/donations.entity'; import { EmailsModule } from '../emails/email.module'; +import { DonationItemsModule } from '../donationItems/donationItems.module'; @Module({ imports: [ TypeOrmModule.forFeature([FoodManufacturer, Donation]), forwardRef(() => UsersModule), EmailsModule, + DonationItemsModule, ], controllers: [FoodManufacturersController], providers: [FoodManufacturersService], diff --git a/apps/backend/src/foodManufacturers/manufacturers.service.spec.ts b/apps/backend/src/foodManufacturers/manufacturers.service.spec.ts index b972887ea..71503bc8d 100644 --- a/apps/backend/src/foodManufacturers/manufacturers.service.spec.ts +++ b/apps/backend/src/foodManufacturers/manufacturers.service.spec.ts @@ -13,15 +13,11 @@ import { ApplicationStatus } from '../shared/types'; import { testDataSource } from '../config/typeormTestDataSource'; import { Donation } from '../donations/donations.entity'; import { User } from '../users/users.entity'; +import { Order } from '../orders/order.entity'; +import { FoodRequest } from '../foodRequests/request.entity'; import { UsersService } from '../users/users.service'; import { AuthService } from '../auth/auth.service'; import { EmailsService } from '../emails/email.service'; -import { Pantry } from '../pantries/pantries.entity'; -import { Order } from '../orders/order.entity'; -import { FoodRequest } from '../foodRequests/request.entity'; -import { DonationItem } from '../donationItems/donationItems.entity'; -import { DonationService } from '../donations/donations.service'; -import { PantriesService } from '../pantries/pantries.service'; import { mock } from 'jest-mock-extended'; import { emailTemplates, SSF_PARTNER_EMAIL } from '../emails/emailTemplates'; import { Allergen, DonateWastedFood, ManufacturerAttribute } from './types'; @@ -66,8 +62,6 @@ describe('FoodManufacturersService', () => { providers: [ FoodManufacturersService, UsersService, - DonationService, - PantriesService, { provide: AuthService, useValue: { @@ -90,10 +84,6 @@ describe('FoodManufacturersService', () => { provide: getRepositoryToken(Donation), useValue: testDataSource.getRepository(Donation), }, - { - provide: getRepositoryToken(Pantry), - useValue: testDataSource.getRepository(Pantry), - }, { provide: getRepositoryToken(Order), useValue: testDataSource.getRepository(Order), @@ -102,18 +92,6 @@ describe('FoodManufacturersService', () => { provide: getRepositoryToken(FoodRequest), useValue: testDataSource.getRepository(FoodRequest), }, - { - provide: getRepositoryToken(DonationItem), - useValue: testDataSource.getRepository(DonationItem), - }, - { - provide: getRepositoryToken(Allocation), - useValue: testDataSource.getRepository(Allocation), - }, - { - provide: DataSource, - useValue: testDataSource, - }, ], }).compile(); diff --git a/apps/backend/src/foodRequests/request.controller.spec.ts b/apps/backend/src/foodRequests/request.controller.spec.ts index bf37a6db4..0c177293e 100644 --- a/apps/backend/src/foodRequests/request.controller.spec.ts +++ b/apps/backend/src/foodRequests/request.controller.spec.ts @@ -168,7 +168,7 @@ describe('RequestsController', () => { }); }); - describe('POST /create', () => { + describe('POST /', () => { it('should call requestsService.create and return the created food request', async () => { const createBody: Partial = { pantryId: 1, diff --git a/apps/backend/src/foodRequests/request.controller.ts b/apps/backend/src/foodRequests/request.controller.ts index e94a5ac0a..93a1db234 100644 --- a/apps/backend/src/foodRequests/request.controller.ts +++ b/apps/backend/src/foodRequests/request.controller.ts @@ -73,7 +73,7 @@ export class RequestsController { return this.requestsService.getAvailableItems(requestId, manufacturerId); } - @Post('/create') + @Post() @ApiBody({ description: 'Details for creating a food request', schema: { diff --git a/apps/backend/src/migrations/1773889925002-MakeFoodRescueRequired.ts.ts b/apps/backend/src/migrations/1773889925002-MakeFoodRescueRequired.ts.ts new file mode 100644 index 000000000..feca02102 --- /dev/null +++ b/apps/backend/src/migrations/1773889925002-MakeFoodRescueRequired.ts.ts @@ -0,0 +1,24 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class MakeFoodRescueRequired1773889925002 implements MigrationInterface { + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(` + UPDATE donation_items + SET food_rescue = false + WHERE food_rescue IS NULL + `); + await queryRunner.query(` + ALTER TABLE donation_items + ALTER COLUMN food_rescue SET NOT NULL, + ALTER COLUMN food_rescue SET DEFAULT false + `); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(` + ALTER TABLE donation_items + ALTER COLUMN food_rescue DROP NOT NULL, + ALTER COLUMN food_rescue DROP DEFAULT + `); + } +} diff --git a/apps/backend/src/pantries/pantries.service.spec.ts b/apps/backend/src/pantries/pantries.service.spec.ts index 270643f42..bbdf83127 100644 --- a/apps/backend/src/pantries/pantries.service.spec.ts +++ b/apps/backend/src/pantries/pantries.service.spec.ts @@ -21,16 +21,9 @@ import { ApplicationStatus } from '../shared/types'; import { testDataSource } from '../config/typeormTestDataSource'; import { Order } from '../orders/order.entity'; import { FoodRequest } from '../foodRequests/request.entity'; -import { RequestsService } from '../foodRequests/request.service'; -import { OrdersService } from '../orders/order.service'; +import { Donation } from '../donations/donations.entity'; import { UsersService } from '../users/users.service'; import { AuthService } from '../auth/auth.service'; -import { DonationItem } from '../donationItems/donationItems.entity'; -import { DonationItemsService } from '../donationItems/donationItems.service'; -import { DonationService } from '../donations/donations.service'; -import { Donation } from '../donations/donations.entity'; -import { FoodManufacturersService } from '../foodManufacturers/manufacturers.service'; -import { FoodManufacturer } from '../foodManufacturers/manufacturers.entity'; import { User } from '../users/users.entity'; import { AllocationsService } from '../allocations/allocations.service'; import { UpdatePantryApplicationDto } from './dtos/update-pantry-application.dto'; @@ -114,13 +107,7 @@ describe('PantriesService', () => { testModule = await Test.createTestingModule({ providers: [ PantriesService, - OrdersService, - RequestsService, UsersService, - DonationItemsService, - DonationService, - FoodManufacturersService, - AllocationsService, { provide: AuthService, useValue: { @@ -147,26 +134,10 @@ describe('PantriesService', () => { provide: getRepositoryToken(FoodRequest), useValue: testDataSource.getRepository(FoodRequest), }, - { - provide: getRepositoryToken(DonationItem), - useValue: testDataSource.getRepository(DonationItem), - }, { provide: getRepositoryToken(Donation), useValue: testDataSource.getRepository(Donation), }, - { - provide: getRepositoryToken(FoodManufacturer), - useValue: testDataSource.getRepository(FoodManufacturer), - }, - { - provide: getRepositoryToken(Allocation), - useValue: testDataSource.getRepository(Allocation), - }, - { - provide: DataSource, - useValue: testDataSource, - }, ], }).compile(); diff --git a/apps/backend/src/users/users.service.spec.ts b/apps/backend/src/users/users.service.spec.ts index c852266a8..569e0b184 100644 --- a/apps/backend/src/users/users.service.spec.ts +++ b/apps/backend/src/users/users.service.spec.ts @@ -21,6 +21,7 @@ import { OrdersService } from '../orders/order.service'; import { DonationService } from '../donations/donations.service'; import { RecurrenceEnum } from '../donations/types'; import { CreateDonationDto } from '../donations/dtos/create-donation.dto'; +import { FoodType } from '../donationItems/types'; import { Pantry } from '../pantries/pantries.entity'; import { FoodManufacturer } from '../foodManufacturers/manufacturers.entity'; import { DonationItem } from '../donationItems/donationItems.entity'; @@ -348,6 +349,14 @@ describe('UsersService', () => { recurrence: RecurrenceEnum.MONTHLY, recurrenceFreq: 3, occurrencesRemaining: 2, + items: [ + { + itemName: 'Test Item', + quantity: 10, + foodType: FoodType.GRANOLA, + foodRescue: false, + }, + ], }; await donationService.create(createDonationBody as CreateDonationDto); diff --git a/apps/backend/src/volunteers/volunteers.service.spec.ts b/apps/backend/src/volunteers/volunteers.service.spec.ts index ae323d9de..a871d5b30 100644 --- a/apps/backend/src/volunteers/volunteers.service.spec.ts +++ b/apps/backend/src/volunteers/volunteers.service.spec.ts @@ -7,21 +7,14 @@ import { Pantry } from '../pantries/pantries.entity'; import { testDataSource } from '../config/typeormTestDataSource'; import { UsersService } from '../users/users.service'; import { PantriesService } from '../pantries/pantries.service'; -import { OrdersService } from '../orders/order.service'; import { Order } from '../orders/order.entity'; import { RequestsService } from '../foodRequests/request.service'; import { FoodRequest } from '../foodRequests/request.entity'; import { AuthService } from '../auth/auth.service'; import { EmailsService } from '../emails/email.service'; import { FoodManufacturer } from '../foodManufacturers/manufacturers.entity'; -import { FoodManufacturersService } from '../foodManufacturers/manufacturers.service'; import { DonationItem } from '../donationItems/donationItems.entity'; -import { DonationItemsService } from '../donationItems/donationItems.service'; -import { DonationService } from '../donations/donations.service'; import { Donation } from '../donations/donations.entity'; -import { Allocation } from '../allocations/allocations.entity'; -import { AllocationsService } from '../allocations/allocations.service'; -import { DataSource } from 'typeorm'; jest.setTimeout(60000); @@ -39,19 +32,19 @@ describe('VolunteersService', () => { VolunteersService, UsersService, PantriesService, - EmailsService, - OrdersService, RequestsService, - FoodManufacturersService, - DonationItemsService, - DonationService, - AllocationsService, { provide: AuthService, useValue: { adminCreateUser: jest.fn().mockResolvedValue('test-sub'), }, }, + { + provide: EmailsService, + useValue: { + sendEmails: jest.fn().mockResolvedValue(undefined), + }, + }, { provide: getRepositoryToken(User), useValue: testDataSource.getRepository(User), @@ -80,20 +73,6 @@ describe('VolunteersService', () => { provide: getRepositoryToken(Donation), useValue: testDataSource.getRepository(Donation), }, - { - provide: getRepositoryToken(Allocation), - useValue: testDataSource.getRepository(Allocation), - }, - { - provide: EmailsService, - useValue: { - sendEmails: jest.fn().mockResolvedValue(undefined), - }, - }, - { - provide: DataSource, - useValue: testDataSource, - }, ], }).compile(); diff --git a/apps/frontend/src/api/apiClient.ts b/apps/frontend/src/api/apiClient.ts index 127d77c1c..297f66b8e 100644 --- a/apps/frontend/src/api/apiClient.ts +++ b/apps/frontend/src/api/apiClient.ts @@ -17,7 +17,6 @@ import { CreateFoodRequestBody, Pantry, PantryApplicationDto, - CreateMultipleDonationItemsBody, ManufacturerApplicationDto, OrderSummary, UserDto, @@ -28,6 +27,7 @@ import { OrderWithoutFoodManufacturer, PantryWithUser, Assignments, + CreateDonationDto, UpdateProfileFields, } from 'types/types'; @@ -85,22 +85,14 @@ export class ApiClient { .then((response) => response.data); } - public async postDonation(body: unknown): Promise { - return this.post('/api/donations/create', body) as Promise; + public async postDonation(body: CreateDonationDto): Promise { + return this.post('/api/donations/', body) as Promise; } public async createFoodRequest( body: CreateFoodRequestBody, ): Promise { - return this.post('/api/requests/create', body) as Promise; - } - - public async postMultipleDonationItems( - body: CreateMultipleDonationItemsBody, - ): Promise { - return this.post('/api/donation-items/create-multiple', body) as Promise< - DonationItem[] - >; + return this.post('/api/requests/', body) as Promise; } private async patch(path: string, body: unknown): Promise { @@ -133,16 +125,6 @@ export class ApiClient { ) as Promise; } - public async updateDonationItemQuantity( - itemId: number, - body?: unknown, - ): Promise { - return this.patch( - `/api/donation-items/update-quantity/${itemId}`, - body, - ) as Promise; - } - private async delete(path: string): Promise { return this.axiosInstance.delete(path).then((response) => response.data); } diff --git a/apps/frontend/src/components/forms/newDonationFormModal.tsx b/apps/frontend/src/components/forms/newDonationFormModal.tsx index f85f76f36..e702b74c7 100644 --- a/apps/frontend/src/components/forms/newDonationFormModal.tsx +++ b/apps/frontend/src/components/forms/newDonationFormModal.tsx @@ -15,11 +15,11 @@ import { Menu, NumberInput, Tooltip, - InputGroup, } from '@chakra-ui/react'; import { useState } from 'react'; import ApiClient from '@api/apiClient'; import { + CreateDonationDto, DayOfWeek, FoodType, RecurrenceEnum, @@ -133,7 +133,6 @@ const NewDonationFormModal: React.FC = ({ Sunday: false, }); const [endsAfter, setEndsAfter] = useState('1'); - const [alertState, setAlertMessage] = useAlert(); const handleChange = (id: number, field: string, value: string | boolean) => { @@ -205,54 +204,45 @@ const NewDonationFormModal: React.FC = ({ return; } - const donation_body = { + const donationBody: CreateDonationDto = { foodManufacturerId: 1, - recurrenceFreq: isRecurring ? parseInt(repeatEvery) : null, + recurrenceFreq: isRecurring ? parseInt(repeatEvery) : undefined, recurrence: isRecurring ? repeatInterval : RecurrenceEnum.NONE, repeatOnDays: isRecurring && repeatInterval === RecurrenceEnum.WEEKLY ? repeatOn - : null, - occurrencesRemaining: isRecurring ? parseInt(endsAfter) : null, + : undefined, + occurrencesRemaining: isRecurring ? parseInt(endsAfter) : undefined, + items: rows.map((row) => ({ + itemName: row.foodItem, + quantity: parseInt(row.numItems), + ozPerItem: row.ozPerItem ? parseFloat(row.ozPerItem) : undefined, + estimatedValue: row.valuePerItem + ? parseFloat(row.valuePerItem) + : undefined, + foodType: row.foodType as FoodType, + foodRescue: row.foodRescue, + })), }; try { - const donationResponse = await ApiClient.postDonation(donation_body); - const donationId = donationResponse?.donationId; - - if (donationId) { - const items = rows.map((row) => ({ - itemName: row.foodItem, - quantity: parseInt(row.numItems), - reservedQuantity: 0, - ozPerItem: - row.ozPerItem !== '' ? parseFloat(row.ozPerItem) : undefined, - estimatedValue: - row.valuePerItem !== '' ? parseFloat(row.valuePerItem) : undefined, - foodType: row.foodType as FoodType, - foodRescue: row.foodRescue, - })); - - await ApiClient.postMultipleDonationItems({ donationId, items }); - onDonationSuccess(); - - setRows([ - { - id: 1, - foodItem: '', - foodType: '', - numItems: '', - ozPerItem: '', - valuePerItem: '', - foodRescue: false, - }, - ]); - setIsRecurring(false); - setRepeatInterval(RecurrenceEnum.NONE); - onClose(); - } else { - setAlertMessage('Failed to submit donation'); - } + await ApiClient.postDonation(donationBody); + onDonationSuccess(); + + setRows([ + { + id: 1, + foodItem: '', + foodType: '', + numItems: '', + ozPerItem: '', + valuePerItem: '', + foodRescue: false, + }, + ]); + setIsRecurring(false); + setRepeatInterval(RecurrenceEnum.NONE); + onClose(); } catch { setAlertMessage('Error submitting new donation'); } diff --git a/apps/frontend/src/types/types.ts b/apps/frontend/src/types/types.ts index 578f4de2b..298e4c650 100644 --- a/apps/frontend/src/types/types.ts +++ b/apps/frontend/src/types/types.ts @@ -145,7 +145,7 @@ export interface DonationItem { ozPerItem?: number; estimatedValue?: number; foodType: FoodType; - foodRescue?: boolean; + foodRescue: boolean; } export enum FoodType { @@ -303,17 +303,22 @@ export interface CreateFoodRequestBody { additionalInformation?: string; } -export interface CreateMultipleDonationItemsBody { - donationId: number; - items: { - itemName: string; - quantity: number; - reservedQuantity: number; - ozPerItem?: number; - estimatedValue?: number; - foodType: FoodType; - foodRescue?: boolean; - }[]; +export interface CreateDonationDto { + foodManufacturerId: number; + recurrenceFreq?: number; + recurrence: RecurrenceEnum; + repeatOnDays?: RepeatOnState; + occurrencesRemaining?: number; + items: CreateDonationItemDto[]; +} + +export interface CreateDonationItemDto { + itemName: string; + quantity: number; + ozPerItem?: number; + estimatedValue?: number; + foodType: FoodType; + foodRescue: boolean; } export interface Allocation {