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..994f3bdc9 100644 --- a/apps/backend/src/donationItems/donationItems.controller.ts +++ b/apps/backend/src/donationItems/donationItems.controller.ts @@ -1,19 +1,14 @@ 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')) @@ -27,46 +22,6 @@ export class DonationItemsController { 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, 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..1a6b9b23c --- /dev/null +++ b/apps/backend/src/donationItems/donationItems.service.spec.ts @@ -0,0 +1,168 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { getRepositoryToken } from '@nestjs/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'; + +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); + }); + + 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, + ); + + expect(item).toBeDefined(); + expect(item.itemId).toBeDefined(); + expect(item.quantity).toEqual(10); + }); + + 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('updateDonationItemQuantity', () => { + it('decrements quantity by 1', async () => { + const result = await testDataSource.query( + `SELECT item_id, quantity FROM donation_items WHERE item_name = 'Peanut Butter (16oz)' LIMIT 1`, + ); + const { item_id: itemId, quantity } = result[0]; + + const updated = await service.updateDonationItemQuantity(itemId); + expect(updated.quantity).toEqual(quantity - 1); + }); + + it('throws NotFoundException when item does not exist', async () => { + await expect(service.updateDonationItemQuantity(99999)).rejects.toThrow( + new NotFoundException(`Donation item 99999 not found`), + ); + }); + }); +}); diff --git a/apps/backend/src/donationItems/donationItems.service.ts b/apps/backend/src/donationItems/donationItems.service.ts index 2397f545d..16175d108 100644 --- a/apps/backend/src/donationItems/donationItems.service.ts +++ b/apps/backend/src/donationItems/donationItems.service.ts @@ -32,7 +32,6 @@ export class DonationItemsService { donationId: number, itemName: string, quantity: number, - reservedQuantity: number, ozPerItem: number, estimatedValue: number, foodType: FoodType, @@ -45,7 +44,7 @@ export class DonationItemsService { donation, itemName, quantity, - reservedQuantity, + reservedQuantity: 0, ozPerItem, estimatedValue, foodType, @@ -54,37 +53,6 @@ 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; - }[], - ): Promise { - validateId(donationId, 'Donation'); - - const donation = await this.donationRepo.findOneBy({ donationId }); - if (!donation) throw new NotFoundException('Donation not found'); - - const donationItems = items.map((item) => - this.repo.create({ - donation, - itemName: item.itemName, - quantity: item.quantity, - reservedQuantity: item.reservedQuantity, - ozPerItem: item.ozPerItem, - estimatedValue: item.estimatedValue, - foodType: item.foodType, - }), - ); - - return this.repo.save(donationItems); - } - async updateDonationItemQuantity(itemId: number): Promise { validateId(itemId, 'Donation Item'); 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 0e4b04b39..f71f9445a 100644 --- a/apps/backend/src/donationItems/dtos/create-donation-items.dto.ts +++ b/apps/backend/src/donationItems/dtos/create-donation-items.dto.ts @@ -1,15 +1,13 @@ import { IsNumber, IsString, - IsArray, - ValidateNested, Min, IsEnum, IsNotEmpty, Length, IsOptional, + IsBoolean, } from 'class-validator'; -import { Type } from 'class-transformer'; import { FoodType } from '../types'; export class CreateDonationItemDto { @@ -22,10 +20,6 @@ export class CreateDonationItemDto { @Min(1) quantity!: number; - @IsNumber() - @Min(0) - reservedQuantity!: number; - @IsNumber() @Min(0.01) @IsOptional() @@ -38,14 +32,8 @@ export class CreateDonationItemDto { @IsEnum(FoodType) foodType!: FoodType; -} -export class CreateMultipleDonationItemsDto { - @IsNumber() - donationId!: number; - - @IsArray() - @ValidateNested({ each: true }) - @Type(() => CreateDonationItemDto) - items!: CreateDonationItemDto[]; + @IsBoolean() + @IsOptional() + foodRescue?: boolean; } diff --git a/apps/backend/src/donations/donations.module.ts b/apps/backend/src/donations/donations.module.ts index 0b813de8f..daf9ac67a 100644 --- a/apps/backend/src/donations/donations.module.ts +++ b/apps/backend/src/donations/donations.module.ts @@ -6,9 +6,13 @@ import { DonationsController } from './donations.controller'; import { AuthModule } from '../auth/auth.module'; import { FoodManufacturer } from '../foodManufacturers/manufacturers.entity'; import { DonationsSchedulerService } from './donations.scheduler'; +import { DonationItem } from '../donationItems/donationItems.entity'; @Module({ - imports: [TypeOrmModule.forFeature([Donation, FoodManufacturer]), AuthModule], + imports: [ + TypeOrmModule.forFeature([Donation, FoodManufacturer, DonationItem]), + AuthModule, + ], controllers: [DonationsController], providers: [DonationService, DonationsSchedulerService], }) diff --git a/apps/backend/src/donations/donations.service.spec.ts b/apps/backend/src/donations/donations.service.spec.ts index 84da409cb..f93547486 100644 --- a/apps/backend/src/donations/donations.service.spec.ts +++ b/apps/backend/src/donations/donations.service.spec.ts @@ -6,6 +6,15 @@ import { FoodManufacturer } from '../foodManufacturers/manufacturers.entity'; import { RecurrenceEnum, DayOfWeek } from './types'; import { RepeatOnDaysDto } from './dtos/create-donation.dto'; import { testDataSource } from '../config/typeormTestDataSource'; +import { FoodType } from '../donationItems/types'; +import { + BadRequestException, + InternalServerErrorException, + NotFoundException, +} from '@nestjs/common'; +import { DonationItem } from '../donationItems/donationItems.entity'; +import { DataSource } from 'typeorm'; +import { DonationItemsService } from '../donationItems/donationItems.service'; jest.setTimeout(60000); @@ -94,6 +103,7 @@ describe('DonationService', () => { const module: TestingModule = await Test.createTestingModule({ providers: [ DonationService, + DonationItemsService, { provide: getRepositoryToken(Donation), useValue: testDataSource.getRepository(Donation), @@ -102,6 +112,14 @@ describe('DonationService', () => { provide: getRepositoryToken(FoodManufacturer), useValue: testDataSource.getRepository(FoodManufacturer), }, + { + provide: getRepositoryToken(DonationItem), + useValue: testDataSource.getRepository(DonationItem), + }, + { + provide: DataSource, + useValue: testDataSource, + }, ], }).compile(); @@ -765,4 +783,109 @@ describe('DonationService', () => { expect(result).toHaveLength(0); }); }); + + 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 items = await testDataSource.query( + `SELECT * FROM donation_items WHERE donation_id = $1`, + [donation.donationId], + ); + + expect(items).toHaveLength(2); + expect( + items.every((item: any) => item.donation_id === donation.donationId), + ).toBe(true); + }); + + 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 () => { + 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', + ), + ); + + const donations = await testDataSource.query(`SELECT * FROM donations`); + expect(donations).toHaveLength(4); + }); + + it('rolls back donation when a donation item fails to save', async () => { + 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( + new InternalServerErrorException( + 'Failed to create donation, no changes were saved', + ), + ); + + const donations = await testDataSource.query(`SELECT * FROM donations`); + expect(donations).toHaveLength(4); + }); + }); }); diff --git a/apps/backend/src/donations/donations.service.ts b/apps/backend/src/donations/donations.service.ts index 3e58fcc98..2ef24184e 100644 --- a/apps/backend/src/donations/donations.service.ts +++ b/apps/backend/src/donations/donations.service.ts @@ -1,16 +1,18 @@ import { BadRequestException, Injectable, + InternalServerErrorException, Logger, NotFoundException, } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; -import { Repository } from 'typeorm'; +import { DataSource, Repository } from 'typeorm'; import { Donation } from './donations.entity'; 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 { DonationItem } from '../donationItems/donationItems.entity'; @Injectable() export class DonationService { @@ -18,8 +20,11 @@ export class DonationService { constructor( @InjectRepository(Donation) private repo: Repository, + @InjectRepository(DonationItem) + private donationItemRepo: Repository, @InjectRepository(FoodManufacturer) private manufacturerRepo: Repository, + private dataSource: DataSource, ) {} async findOne(donationId: number): Promise { @@ -84,7 +89,31 @@ export class DonationService { occurrencesRemaining: donationData.occurrencesRemaining, }); - return this.repo.save(donation); + try { + await this.dataSource.transaction(async (manager) => { + const savedDonation = await manager.save(Donation, donation); + + const donationItems = donationData.items.map((item) => + manager.create(DonationItem, { + donation: savedDonation, + itemName: item.itemName, + quantity: item.quantity, + ozPerItem: item.ozPerItem, + estimatedValue: item.estimatedValue, + foodType: item.foodType, + foodRescue: item.foodRescue, + }), + ); + + await manager.save(DonationItem, donationItems); + }); + } catch { + throw new InternalServerErrorException( + 'Failed to create donation, no changes were saved', + ); + } + + return donation; } 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..7ee82a14b 100644 --- a/apps/backend/src/donations/dtos/create-donation.dto.ts +++ b/apps/backend/src/donations/dtos/create-donation.dto.ts @@ -1,4 +1,6 @@ import { + ArrayMinSize, + IsArray, IsBoolean, IsEnum, IsNotEmpty, @@ -11,6 +13,7 @@ import { registerDecorator, } from 'class-validator'; import { RecurrenceEnum } from '../types'; +import { CreateDonationItemDto } from '../../donationItems/dtos/create-donation-items.dto'; import { Type } from 'class-transformer'; function AtLeastOneDaySelected() { @@ -83,4 +86,10 @@ export class CreateDonationDto { @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/pantries/pantries.service.spec.ts b/apps/backend/src/pantries/pantries.service.spec.ts index e6ed91f03..358d13129 100644 --- a/apps/backend/src/pantries/pantries.service.spec.ts +++ b/apps/backend/src/pantries/pantries.service.spec.ts @@ -27,6 +27,7 @@ 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 { DataSource } from 'typeorm'; jest.setTimeout(60000); @@ -111,6 +112,10 @@ describe('PantriesService', () => { provide: getRepositoryToken(FoodManufacturer), useValue: testDataSource.getRepository(FoodManufacturer), }, + { + provide: DataSource, + useValue: testDataSource, + }, ], }).compile(); diff --git a/apps/backend/src/volunteers/volunteers.service.spec.ts b/apps/backend/src/volunteers/volunteers.service.spec.ts index 35e5a92c8..7f7727640 100644 --- a/apps/backend/src/volunteers/volunteers.service.spec.ts +++ b/apps/backend/src/volunteers/volunteers.service.spec.ts @@ -18,6 +18,7 @@ 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 { DataSource } from 'typeorm'; jest.setTimeout(60000); @@ -74,6 +75,10 @@ describe('VolunteersService', () => { provide: getRepositoryToken(Donation), useValue: testDataSource.getRepository(Donation), }, + { + provide: DataSource, + useValue: testDataSource, + }, ], }).compile(); diff --git a/apps/frontend/src/api/apiClient.ts b/apps/frontend/src/api/apiClient.ts index 37e4ed11f..a77f3cc33 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, @@ -25,6 +24,7 @@ import { Assignments, FoodRequestSummaryDto, PantryWithUser, + CreateDonationBody, } from 'types/types'; const defaultBaseUrl = @@ -91,14 +91,6 @@ export class ApiClient { 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[] - >; - } - private async patch(path: string, body: unknown): Promise { return this.axiosInstance .patch(path, body) diff --git a/apps/frontend/src/components/forms/newDonationFormModal.tsx b/apps/frontend/src/components/forms/newDonationFormModal.tsx index f4cf7df7b..00e5cb9b8 100644 --- a/apps/frontend/src/components/forms/newDonationFormModal.tsx +++ b/apps/frontend/src/components/forms/newDonationFormModal.tsx @@ -18,6 +18,8 @@ import { import { useState } from 'react'; import ApiClient from '@api/apiClient'; import { + CreateDonationBody, + CreateDonationDto, DayOfWeek, FoodType, RecurrenceEnum, @@ -166,52 +168,45 @@ const NewDonationFormModal: React.FC = ({ return; } - const donation_body = { + const donationBody: CreateDonationBody = { 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 || undefined, + })), }; 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: parseFloat(row.ozPerItem), - estimatedValue: parseFloat(row.valuePerItem), - 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 40a9ff216..29b814b20 100644 --- a/apps/frontend/src/types/types.ts +++ b/apps/frontend/src/types/types.ts @@ -267,12 +267,32 @@ export interface CreateFoodRequestBody { additionalInformation?: string; } -export interface CreateMultipleDonationItemsBody { - donationId: number; +export interface CreateDonationDto { + foodManufacturerId: number; + recurrenceFreq?: number; + recurrence: RecurrenceEnum; + repeatOnDays?: RepeatOnState; + occurrencesRemaining?: number; +} + +export interface CreateDonationItemDto { + itemName: string; + quantity: number; + ozPerItem?: number; + estimatedValue?: number; + foodType: FoodType; + foodRescue?: boolean; +} + +export interface CreateDonationBody { + foodManufacturerId: number; + recurrenceFreq?: number; + recurrence: RecurrenceEnum; + repeatOnDays?: RepeatOnState; + occurrencesRemaining?: number; items: { itemName: string; quantity: number; - reservedQuantity: number; ozPerItem?: number; estimatedValue?: number; foodType: FoodType;