From 98bafb673101a93d831ce2da27ab1897a9418daf Mon Sep 17 00:00:00 2001 From: Justin Wang Date: Fri, 5 Jun 2026 17:51:28 -0400 Subject: [PATCH 1/4] refactoring --- .../donationItems.service.spec.ts | 62 ++----------------- .../donationItems/donationItems.service.ts | 4 +- .../dtos/create-donation-items.dto.ts | 7 +-- .../src/donations/donations.controller.ts | 4 +- .../src/donations/donations.service.spec.ts | 2 + apps/backend/src/users/users.service.spec.ts | 2 + .../components/forms/newDonationFormModal.tsx | 34 +++++++--- .../forms/resubmitDonationModal.tsx | 8 +-- apps/frontend/src/types/types.ts | 4 +- 9 files changed, 45 insertions(+), 82 deletions(-) diff --git a/apps/backend/src/donationItems/donationItems.service.spec.ts b/apps/backend/src/donationItems/donationItems.service.spec.ts index 847041741..0011123ea 100644 --- a/apps/backend/src/donationItems/donationItems.service.spec.ts +++ b/apps/backend/src/donationItems/donationItems.service.spec.ts @@ -228,71 +228,17 @@ describe('DonationItemsService', () => { expect(rice.detailsConfirmed).toEqual(true); }); - it('creates items with optional fields omitted', async () => { + it('sets detailsConfirmed to true since ozPerItem and estimatedValue are required', 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(); - expect(result[0].detailsConfirmed).toEqual(false); - }); - - it('sets detailsConfirmed to true only when both ozPerItem and estimatedValue are provided', async () => { - const donation = await getSeedDonation(); - const transactionManager = testDataSource.createEntityManager(); - - const mixedItems: CreateDonationItemDto[] = [ - { - itemName: 'Both Fields', - quantity: 4, - ozPerItem: 12, - estimatedValue: 3.5, - foodType: FoodType.DRIED_BEANS, - foodRescue: false, - }, - { - itemName: 'Missing Estimated Value', - quantity: 2, - ozPerItem: 8, - foodType: FoodType.DRIED_BEANS, - foodRescue: false, - }, - { - itemName: 'Missing Oz Per Item', - quantity: 6, - estimatedValue: 1.99, - foodType: FoodType.DRIED_BEANS, - foodRescue: false, - }, - ]; - const result = await service.createMultiple( donation, - mixedItems, + validItems, transactionManager, ); - const byName = Object.fromEntries(result.map((i) => [i.itemName, i])); - expect(byName['Both Fields'].detailsConfirmed).toEqual(true); - expect(byName['Missing Estimated Value'].detailsConfirmed).toEqual(false); - expect(byName['Missing Oz Per Item'].detailsConfirmed).toEqual(false); + expect(result.every((item) => item.detailsConfirmed)).toBe(true); }); it('rolls back all items when one fails within a transaction', async () => { @@ -308,6 +254,8 @@ describe('DonationItemsService', () => { { itemName: 'a'.repeat(1000), quantity: 5, + ozPerItem: 10, + estimatedValue: 2.5, foodType: FoodType.DRIED_BEANS, foodRescue: false, }, diff --git a/apps/backend/src/donationItems/donationItems.service.ts b/apps/backend/src/donationItems/donationItems.service.ts index 916210174..38930cab5 100644 --- a/apps/backend/src/donationItems/donationItems.service.ts +++ b/apps/backend/src/donationItems/donationItems.service.ts @@ -110,7 +110,7 @@ export class DonationItemsService { updateData.estimatedValue = dto.estimatedValue; if (dto.foodRescue !== undefined) updateData.foodRescue = dto.foodRescue; - // If included in DTO, keep it, otherwise use whatever is in the DB (could be null) + // If included in DTO, keep it, otherwise use whatever is in the DB const resultingOzPerItem = updateData.ozPerItem !== undefined ? updateData.ozPerItem @@ -148,7 +148,7 @@ export class DonationItemsService { estimatedValue: item.estimatedValue, foodType: item.foodType, foodRescue: item.foodRescue, - detailsConfirmed: item.ozPerItem != null && item.estimatedValue != null, + detailsConfirmed: true, }), ); 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 3380ef7a6..6059732cd 100644 --- a/apps/backend/src/donationItems/dtos/create-donation-items.dto.ts +++ b/apps/backend/src/donationItems/dtos/create-donation-items.dto.ts @@ -5,7 +5,6 @@ import { IsEnum, IsNotEmpty, Length, - IsOptional, IsInt, IsBoolean, } from 'class-validator'; @@ -26,16 +25,14 @@ export class CreateDonationItemDto { { message: 'ozPerItem must have at most 2 decimal places' }, ) @Min(0.01) - @IsOptional() - ozPerItem?: number; + ozPerItem!: number; @IsNumber( { maxDecimalPlaces: 2 }, { message: 'estimatedValue must have at most 2 decimal places' }, ) @Min(0.01) - @IsOptional() - estimatedValue?: number; + estimatedValue!: number; @IsEnum(FoodType) foodType!: FoodType; diff --git a/apps/backend/src/donations/donations.controller.ts b/apps/backend/src/donations/donations.controller.ts index ab493edd5..b5315dbde 100644 --- a/apps/backend/src/donations/donations.controller.ts +++ b/apps/backend/src/donations/donations.controller.ts @@ -65,8 +65,8 @@ export class DonationsController { 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 }, + ozPerItem: { type: 'number', example: 0.01 }, + estimatedValue: { type: 'number', example: 0.01 }, foodType: { type: 'enum', enum: Object.values(FoodType), diff --git a/apps/backend/src/donations/donations.service.spec.ts b/apps/backend/src/donations/donations.service.spec.ts index 6fa0bcda3..0d82380ea 100644 --- a/apps/backend/src/donations/donations.service.spec.ts +++ b/apps/backend/src/donations/donations.service.spec.ts @@ -1229,6 +1229,8 @@ describe('DonationService', () => { quantity: 5, foodType: FoodType.DAIRY_FREE_ALTERNATIVES, foodRescue: false, + ozPerItem: 3.4, + estimatedValue: 3.4, }, ], }), diff --git a/apps/backend/src/users/users.service.spec.ts b/apps/backend/src/users/users.service.spec.ts index c81a6664b..c7f53ae6c 100644 --- a/apps/backend/src/users/users.service.spec.ts +++ b/apps/backend/src/users/users.service.spec.ts @@ -358,6 +358,8 @@ describe('UsersService', () => { itemName: 'Test Item', quantity: 10, foodType: FoodType.GRANOLA, + ozPerItem: 3.4, + estimatedValue: 3.4, foodRescue: false, }, ], diff --git a/apps/frontend/src/components/forms/newDonationFormModal.tsx b/apps/frontend/src/components/forms/newDonationFormModal.tsx index d9bf495a8..266719406 100644 --- a/apps/frontend/src/components/forms/newDonationFormModal.tsx +++ b/apps/frontend/src/components/forms/newDonationFormModal.tsx @@ -85,10 +85,16 @@ const getFirstValidationError = ( if (!isValidPositiveInt(row.numItems)) { return `Quantity${rowLabel} must be a positive whole number.`; } - if (row.ozPerItem !== '' && !isValidDecimal(row.ozPerItem)) { + if (row.ozPerItem === '') { + return `Oz. per item${rowLabel} is required.`; + } + if (!isValidDecimal(row.ozPerItem)) { return `Oz. per item${rowLabel} must be a positive number with at most 2 decimal places.`; } - if (row.valuePerItem !== '' && !isValidDecimal(row.valuePerItem)) { + if (row.valuePerItem === '') { + return `Donation value${rowLabel} is required.`; + } + if (!isValidDecimal(row.valuePerItem)) { return `Donation value${rowLabel} must be a positive number with at most 2 decimal places.`; } } @@ -220,10 +226,8 @@ const NewDonationFormModal: React.FC = ({ 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, + ozPerItem: parseFloat(row.ozPerItem), + estimatedValue: parseFloat(row.valuePerItem), foodType: row.foodType as FoodType, foodRescue: row.foodRescue, })), @@ -297,10 +301,14 @@ const NewDonationFormModal: React.FC = ({ - + Please fill out the following information to record donation details. + + Please do not include shipping/delivery costs in Food Donation + Value. + = ({ Oz. per item + + * + - Donation Value + Donation Value + (Fair Market Value of Food Only) + + * + = ({ lineHeight="tight" > Food Rescue + + * + diff --git a/apps/frontend/src/components/forms/resubmitDonationModal.tsx b/apps/frontend/src/components/forms/resubmitDonationModal.tsx index afa697297..a4fa437f7 100644 --- a/apps/frontend/src/components/forms/resubmitDonationModal.tsx +++ b/apps/frontend/src/components/forms/resubmitDonationModal.tsx @@ -116,12 +116,8 @@ const ResubmitDonationModal: React.FC = ({ items: items.map((item) => ({ itemName: item.itemName, quantity: item.quantity, - ozPerItem: - item.ozPerItem != null ? Number(item.ozPerItem) : undefined, - estimatedValue: - item.estimatedValue != null - ? Number(item.estimatedValue) - : undefined, + ozPerItem: Number(item.ozPerItem), + estimatedValue: Number(item.estimatedValue), foodType: item.foodType, foodRescue: item.foodRescue, })), diff --git a/apps/frontend/src/types/types.ts b/apps/frontend/src/types/types.ts index 8addbea64..d5a43a62f 100644 --- a/apps/frontend/src/types/types.ts +++ b/apps/frontend/src/types/types.ts @@ -481,8 +481,8 @@ export interface CreateDonationDto { export interface CreateDonationItemDto { itemName: string; quantity: number; - ozPerItem?: number; - estimatedValue?: number; + ozPerItem: number; + estimatedValue: number; foodType: FoodType; foodRescue: boolean; } From 69a70fbf7035737ae2b34278620a304098c9dd3e Mon Sep 17 00:00:00 2001 From: Justin Wang Date: Sun, 7 Jun 2026 11:20:02 -0400 Subject: [PATCH 2/4] update donation item dto changes --- .../donationItems.service.spec.ts | 57 ++++--------------- .../donationItems/donationItems.service.ts | 29 +++------- .../dtos/update-donation-item-details.dto.ts | 11 ++-- .../src/donations/donations.service.spec.ts | 15 ----- .../forms/fmCompleteRequiredActionsModal.tsx | 8 +-- apps/frontend/src/types/types.ts | 6 +- 6 files changed, 32 insertions(+), 94 deletions(-) diff --git a/apps/backend/src/donationItems/donationItems.service.spec.ts b/apps/backend/src/donationItems/donationItems.service.spec.ts index 0011123ea..ae58c23c0 100644 --- a/apps/backend/src/donationItems/donationItems.service.spec.ts +++ b/apps/backend/src/donationItems/donationItems.service.spec.ts @@ -444,50 +444,6 @@ describe('DonationItemsService', () => { expect(item?.ozPerItem).toBeNull(); }); - it('returns false and does not confirm when only some fields are provided', async () => { - const donationId = await insertMatchedDonation(); - const itemId = await insertDonationItem(donationId, 10, 5); - - const result = await testDataSource.transaction((tm) => - service.updateItemDetails(donationId, [{ itemId, ozPerItem: 8.5 }], tm), - ); - - expect(result).toBe(false); - const item = await testDataSource - .getRepository(DonationItem) - .findOneBy({ itemId }); - expect(Number(item?.ozPerItem)).toBe(8.5); - expect(item?.estimatedValue).toBeNull(); - expect(item?.detailsConfirmed).toBe(false); - }); - - it('confirms item on a second call that supplies the remaining fields', async () => { - const donationId = await insertMatchedDonation(); - const itemId = await insertDonationItem(donationId, 10, 5); - - const firstResult = await testDataSource.transaction((tm) => - service.updateItemDetails(donationId, [{ itemId, ozPerItem: 8.5 }], tm), - ); - expect(firstResult).toBe(false); - - const secondResult = await testDataSource.transaction((tm) => - service.updateItemDetails( - donationId, - [{ itemId, estimatedValue: 12.0, foodRescue: true }], - tm, - ), - ); - expect(secondResult).toBe(true); - - const item = await testDataSource - .getRepository(DonationItem) - .findOneBy({ itemId }); - expect(Number(item?.ozPerItem)).toBe(8.5); - expect(Number(item?.estimatedValue)).toBe(12.0); - expect(item?.foodRescue).toBe(true); - expect(item?.detailsConfirmed).toBe(true); - }); - it('allows updating an already-confirmed item without throwing', async () => { const donationId = await insertMatchedDonation(); const itemId = await insertDonationItem(donationId, 10, 5); @@ -499,7 +455,18 @@ describe('DonationItemsService', () => { ); const result = await testDataSource.transaction((tm) => - service.updateItemDetails(donationId, [{ itemId, ozPerItem: 9.0 }], tm), + service.updateItemDetails( + donationId, + [ + { + itemId, + ozPerItem: 9.0, + estimatedValue: 10.0, + foodRescue: true, + }, + ], + tm, + ), ); expect(result).toBe(true); diff --git a/apps/backend/src/donationItems/donationItems.service.ts b/apps/backend/src/donationItems/donationItems.service.ts index 38930cab5..350a33db5 100644 --- a/apps/backend/src/donationItems/donationItems.service.ts +++ b/apps/backend/src/donationItems/donationItems.service.ts @@ -104,26 +104,15 @@ export class DonationItemsService { ); } - const updateData: Partial = {}; - if (dto.ozPerItem !== undefined) updateData.ozPerItem = dto.ozPerItem; - if (dto.estimatedValue !== undefined) - updateData.estimatedValue = dto.estimatedValue; - if (dto.foodRescue !== undefined) updateData.foodRescue = dto.foodRescue; - - // If included in DTO, keep it, otherwise use whatever is in the DB - const resultingOzPerItem = - updateData.ozPerItem !== undefined - ? updateData.ozPerItem - : item.ozPerItem; - const resultingEstimatedValue = - updateData.estimatedValue !== undefined - ? updateData.estimatedValue - : item.estimatedValue; - - if (resultingOzPerItem != null && resultingEstimatedValue != null) { - updateData.detailsConfirmed = true; - confirmedDetailsForAnItem = true; - } + // ozPerItem, estimatedValue, and foodRescue are required on the DTO, so an + // update always supplies the full set of details and confirms the item. + const updateData: Partial = { + ozPerItem: dto.ozPerItem, + estimatedValue: dto.estimatedValue, + foodRescue: dto.foodRescue, + detailsConfirmed: true, + }; + confirmedDetailsForAnItem = true; await donationItemTransactionRepo.update(dto.itemId, updateData); } diff --git a/apps/backend/src/donationItems/dtos/update-donation-item-details.dto.ts b/apps/backend/src/donationItems/dtos/update-donation-item-details.dto.ts index 7c2366761..83812ea93 100644 --- a/apps/backend/src/donationItems/dtos/update-donation-item-details.dto.ts +++ b/apps/backend/src/donationItems/dtos/update-donation-item-details.dto.ts @@ -1,27 +1,24 @@ -import { IsNumber, Min, IsBoolean, IsInt, IsOptional } from 'class-validator'; +import { IsNumber, Min, IsBoolean, IsInt } from 'class-validator'; export class UpdateDonationItemDetailsDto { @IsInt() @Min(1) itemId!: number; - @IsOptional() @IsNumber( { maxDecimalPlaces: 2 }, { message: 'Oz per item must have at most 2 decimal places' }, ) @Min(0.01, { message: 'Oz per item must be at least 0.01' }) - ozPerItem?: number; + ozPerItem!: number; - @IsOptional() @IsNumber( { maxDecimalPlaces: 2 }, { message: 'Estimated value must have at most 2 decimal places' }, ) @Min(0.01, { message: 'Estimated value must be at least 0.01' }) - estimatedValue?: number; + estimatedValue!: number; - @IsOptional() @IsBoolean() - foodRescue?: boolean; + foodRescue!: boolean; } diff --git a/apps/backend/src/donations/donations.service.spec.ts b/apps/backend/src/donations/donations.service.spec.ts index 0d82380ea..5ce2843ca 100644 --- a/apps/backend/src/donations/donations.service.spec.ts +++ b/apps/backend/src/donations/donations.service.spec.ts @@ -1357,21 +1357,6 @@ describe('DonationService', () => { expect(spy).toHaveBeenCalled(); }); - - it('does not call checkAndFulfillDonation when no items are fully confirmed', async () => { - const donationId = await insertMatchedDonation(); - const itemId = await insertDonationItem(donationId, 10, 5); - - const spy = jest.spyOn(service, 'checkAndFulfillDonation'); - - await service.updateDonationItemDetails(donationId, [ - { itemId, ozPerItem: 5.0 }, - ]); - - const dbDonation = await service.findOne(donationId); - expect(dbDonation.status).toBe(DonationStatus.MATCHED); - expect(spy).not.toHaveBeenCalled(); - }); }); describe('checkAndFulfillDonation', () => { diff --git a/apps/frontend/src/components/forms/fmCompleteRequiredActionsModal.tsx b/apps/frontend/src/components/forms/fmCompleteRequiredActionsModal.tsx index 20e76d0ff..11ef3d573 100644 --- a/apps/frontend/src/components/forms/fmCompleteRequiredActionsModal.tsx +++ b/apps/frontend/src/components/forms/fmCompleteRequiredActionsModal.tsx @@ -233,14 +233,14 @@ const FmCompleteRequiredActionsModal: React.FC< }) .map((item) => { const formData = itemFormData[item.itemId]; + // Submit is gated on ozPerItem and estimatedValue being filled for every + // item, so all required fields are guaranteed present here. const dto: UpdateDonationItemDetailsDto = { itemId: item.itemId, + ozPerItem: parseFloat(formData.ozPerItem), + estimatedValue: parseFloat(formData.estimatedValue), foodRescue: formData.foodRescue, }; - if (formData.ozPerItem !== '') - dto.ozPerItem = parseFloat(formData.ozPerItem); - if (formData.estimatedValue !== '') - dto.estimatedValue = parseFloat(formData.estimatedValue); return dto; }); diff --git a/apps/frontend/src/types/types.ts b/apps/frontend/src/types/types.ts index d5a43a62f..48fb8f574 100644 --- a/apps/frontend/src/types/types.ts +++ b/apps/frontend/src/types/types.ts @@ -606,7 +606,7 @@ export interface BulkUpdateTrackingCostDto { export interface UpdateDonationItemDetailsDto { itemId: number; - ozPerItem?: number; - estimatedValue?: number; - foodRescue?: boolean; + ozPerItem: number; + estimatedValue: number; + foodRescue: boolean; } From 8c0cf3807b8890918b79ebecf1ee22294b6fc8fe Mon Sep 17 00:00:00 2001 From: Justin Wang Date: Sun, 21 Jun 2026 15:56:37 -0400 Subject: [PATCH 3/4] comments --- apps/backend/src/config/migrations.ts | 2 ++ .../src/donationItems/donationItems.entity.ts | 8 ++--- .../src/donations/donations.service.spec.ts | 2 ++ .../dtos/donation-details-dto.ts | 4 +-- .../manufacturers.controller.spec.ts | 2 ++ .../manufacturers.service.ts | 4 +-- ...0000000-MakeDonationItemDetailsRequired.ts | 31 +++++++++++++++++++ .../forms/fmCompleteRequiredActionsModal.tsx | 15 ++++----- apps/frontend/src/types/types.ts | 8 ++--- 9 files changed, 55 insertions(+), 21 deletions(-) create mode 100644 apps/backend/src/migrations/1782000000000-MakeDonationItemDetailsRequired.ts diff --git a/apps/backend/src/config/migrations.ts b/apps/backend/src/config/migrations.ts index 9e6387203..b49a45983 100644 --- a/apps/backend/src/config/migrations.ts +++ b/apps/backend/src/config/migrations.ts @@ -42,6 +42,7 @@ import { AddDonationItemConfirmation1774140453305 } from '../migrations/17741404 import { DonationItemsOnDeleteCascade1774214910101 } from '../migrations/1774214910101-DonationItemsOnDeleteCascade'; import { OrdersVolunteerActions1774883880543 } from '../migrations/1774883880543-OrdersVolunteerActions'; import { AddUserActiveField1780531200000 } from '../migrations/1780531200000-AddUserActiveField'; +import { MakeDonationItemDetailsRequired1782000000000 } from '../migrations/1782000000000-MakeDonationItemDetailsRequired'; const schemaMigrations = [ User1725726359198, @@ -88,6 +89,7 @@ const schemaMigrations = [ DonationItemsOnDeleteCascade1774214910101, OrdersVolunteerActions1774883880543, AddUserActiveField1780531200000, + MakeDonationItemDetailsRequired1782000000000, ]; export default schemaMigrations; diff --git a/apps/backend/src/donationItems/donationItems.entity.ts b/apps/backend/src/donationItems/donationItems.entity.ts index 9b8fd8061..d84a57a7d 100644 --- a/apps/backend/src/donationItems/donationItems.entity.ts +++ b/apps/backend/src/donationItems/donationItems.entity.ts @@ -31,11 +31,11 @@ export class DonationItem { @Column({ name: 'reserved_quantity', type: 'int', default: 0 }) reservedQuantity!: number; - @Column({ name: 'oz_per_item', type: 'numeric', nullable: true }) - ozPerItem!: number | null; + @Column({ name: 'oz_per_item', type: 'numeric' }) + ozPerItem!: number; - @Column({ name: 'estimated_value', type: 'numeric', nullable: true }) - estimatedValue!: number | null; + @Column({ name: 'estimated_value', type: 'numeric' }) + estimatedValue!: number; @Column({ name: 'food_type', diff --git a/apps/backend/src/donations/donations.service.spec.ts b/apps/backend/src/donations/donations.service.spec.ts index 5cdb06d69..4deae7d23 100644 --- a/apps/backend/src/donations/donations.service.spec.ts +++ b/apps/backend/src/donations/donations.service.spec.ts @@ -1256,6 +1256,8 @@ describe('DonationService', () => { quantity: 5, foodType: FoodType.DAIRY_FREE_ALTERNATIVES, foodRescue: false, + ozPerItem: 3.4, + estimatedValue: 3.4, }, ], }, diff --git a/apps/backend/src/foodManufacturers/dtos/donation-details-dto.ts b/apps/backend/src/foodManufacturers/dtos/donation-details-dto.ts index 5a8403bff..0b11f304e 100644 --- a/apps/backend/src/foodManufacturers/dtos/donation-details-dto.ts +++ b/apps/backend/src/foodManufacturers/dtos/donation-details-dto.ts @@ -6,8 +6,8 @@ export class DonationItemWithAllocatedQuantityDto { itemName!: string; foodType!: FoodType; allocatedQuantity!: number; - ozPerItem?: number; - estimatedValue?: number; + ozPerItem!: number; + estimatedValue!: number; foodRescue!: boolean; } diff --git a/apps/backend/src/foodManufacturers/manufacturers.controller.spec.ts b/apps/backend/src/foodManufacturers/manufacturers.controller.spec.ts index ac7d46fa5..0e3878c9c 100644 --- a/apps/backend/src/foodManufacturers/manufacturers.controller.spec.ts +++ b/apps/backend/src/foodManufacturers/manufacturers.controller.spec.ts @@ -123,6 +123,8 @@ describe('FoodManufacturersController', () => { foodType: FoodType.DAIRY_FREE_ALTERNATIVES, allocatedQuantity: 10, foodRescue: false, + estimatedValue: 3.4, + ozPerItem: 3.4, }, ], }, diff --git a/apps/backend/src/foodManufacturers/manufacturers.service.ts b/apps/backend/src/foodManufacturers/manufacturers.service.ts index 25e591cd0..09675016a 100644 --- a/apps/backend/src/foodManufacturers/manufacturers.service.ts +++ b/apps/backend/src/foodManufacturers/manufacturers.service.ts @@ -119,8 +119,8 @@ export class FoodManufacturersService { (sum, a) => sum + a.allocatedQuantity, 0, ), - ozPerItem: item.ozPerItem ?? undefined, - estimatedValue: item.estimatedValue ?? undefined, + ozPerItem: item.ozPerItem, + estimatedValue: item.estimatedValue, foodRescue: item.foodRescue, }); diff --git a/apps/backend/src/migrations/1782000000000-MakeDonationItemDetailsRequired.ts b/apps/backend/src/migrations/1782000000000-MakeDonationItemDetailsRequired.ts new file mode 100644 index 000000000..ab60bb698 --- /dev/null +++ b/apps/backend/src/migrations/1782000000000-MakeDonationItemDetailsRequired.ts @@ -0,0 +1,31 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class MakeDonationItemDetailsRequired1782000000000 + implements MigrationInterface +{ + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(` + UPDATE donation_items + SET oz_per_item = 0 + WHERE oz_per_item IS NULL + `); + await queryRunner.query(` + UPDATE donation_items + SET estimated_value = 0 + WHERE estimated_value IS NULL + `); + await queryRunner.query(` + ALTER TABLE donation_items + ALTER COLUMN oz_per_item SET NOT NULL, + ALTER COLUMN estimated_value SET NOT NULL + `); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(` + ALTER TABLE donation_items + ALTER COLUMN oz_per_item DROP NOT NULL, + ALTER COLUMN estimated_value DROP NOT NULL + `); + } +} diff --git a/apps/frontend/src/components/forms/fmCompleteRequiredActionsModal.tsx b/apps/frontend/src/components/forms/fmCompleteRequiredActionsModal.tsx index 20d6676aa..165dce6c2 100644 --- a/apps/frontend/src/components/forms/fmCompleteRequiredActionsModal.tsx +++ b/apps/frontend/src/components/forms/fmCompleteRequiredActionsModal.tsx @@ -150,8 +150,8 @@ const FmCompleteRequiredActionsModal: React.FC< donation.relevantDonationItems.map((item) => [ item.itemId, { - ozPerItem: item.ozPerItem?.toString() ?? '', - estimatedValue: item.estimatedValue?.toString() ?? '', + ozPerItem: item.ozPerItem.toString(), + estimatedValue: item.estimatedValue.toString(), foodRescue: item.foodRescue, }, ]), @@ -167,8 +167,8 @@ const FmCompleteRequiredActionsModal: React.FC< donation.relevantDonationItems.length > 0 && donation.relevantDonationItems.every( (item) => - (itemFormData[item.itemId]?.ozPerItem ?? '') !== '' && - (itemFormData[item.itemId]?.estimatedValue ?? '') !== '', + itemFormData[item.itemId].ozPerItem !== '' && + itemFormData[item.itemId].estimatedValue !== '', ), [itemFormData], ); @@ -228,16 +228,13 @@ const FmCompleteRequiredActionsModal: React.FC< .filter((item) => { const formData = itemFormData[item.itemId]; return ( - formData.ozPerItem !== (item.ozPerItem?.toString() ?? '') || - formData.estimatedValue !== - (item.estimatedValue?.toString() ?? '') || + formData.ozPerItem !== item.ozPerItem.toString() || + formData.estimatedValue !== item.estimatedValue.toString() || formData.foodRescue !== item.foodRescue ); }) .map((item) => { const formData = itemFormData[item.itemId]; - // Submit is gated on ozPerItem and estimatedValue being filled for every - // item, so all required fields are guaranteed present here. const dto: UpdateDonationItemDetailsDto = { itemId: item.itemId, ozPerItem: parseFloat(formData.ozPerItem), diff --git a/apps/frontend/src/types/types.ts b/apps/frontend/src/types/types.ts index d2169fb4f..4910e1521 100644 --- a/apps/frontend/src/types/types.ts +++ b/apps/frontend/src/types/types.ts @@ -211,8 +211,8 @@ export interface DonationItemWithAllocatedQuantity { foodType: FoodType; allocatedQuantity: number; detailsConfirmed: boolean; - ozPerItem?: number; - estimatedValue?: number; + ozPerItem: number; + estimatedValue: number; foodRescue: boolean; } @@ -236,8 +236,8 @@ export interface DonationItem { itemName: string; quantity: number; reservedQuantity: number; - ozPerItem?: number; - estimatedValue?: number; + ozPerItem: number; + estimatedValue: number; foodType: FoodType; foodRescue: boolean; } From 7df267793ca38ef562d723a7b44a387d6cec21f6 Mon Sep 17 00:00:00 2001 From: Justin Wang Date: Sun, 21 Jun 2026 16:24:54 -0400 Subject: [PATCH 4/4] comment --- .../donationItems.service.spec.ts | 5 ++-- .../src/donations/donations.service.spec.ts | 8 +++---- apps/backend/src/orders/order.service.spec.ts | 4 ++-- .../forms/fmCompleteRequiredActionsModal.tsx | 23 ++++++++++++++----- 4 files changed, 25 insertions(+), 15 deletions(-) diff --git a/apps/backend/src/donationItems/donationItems.service.spec.ts b/apps/backend/src/donationItems/donationItems.service.spec.ts index c86dd8553..37054660e 100644 --- a/apps/backend/src/donationItems/donationItems.service.spec.ts +++ b/apps/backend/src/donationItems/donationItems.service.spec.ts @@ -320,8 +320,8 @@ describe('DonationItemsService', () => { ): Promise { const result = await testDataSource.query( `INSERT INTO donation_items - (donation_id, item_name, quantity, reserved_quantity, food_type, details_confirmed) - VALUES ($1, 'Test Item', $2, $3, 'Granola', false) + (donation_id, item_name, quantity, reserved_quantity, oz_per_item, estimated_value, food_type, details_confirmed) + VALUES ($1, 'Test Item', $2, $3, 3.4, 3.4, 'Granola', false) RETURNING item_id`, [donationId, qty, reserved], ); @@ -442,7 +442,6 @@ describe('DonationItemsService', () => { .getRepository(DonationItem) .findOneBy({ itemId }); expect(item?.detailsConfirmed).toBe(false); - expect(item?.ozPerItem).toBeNull(); }); it('allows updating an already-confirmed item without throwing', async () => { diff --git a/apps/backend/src/donations/donations.service.spec.ts b/apps/backend/src/donations/donations.service.spec.ts index 4deae7d23..76b8e16fd 100644 --- a/apps/backend/src/donations/donations.service.spec.ts +++ b/apps/backend/src/donations/donations.service.spec.ts @@ -71,8 +71,8 @@ async function insertDonationItem( ): Promise { const result = await testDataSource.query( `INSERT INTO donation_items - (donation_id, item_name, quantity, reserved_quantity, food_type, details_confirmed) - VALUES ($1, 'Test Item', $2, $3, 'Granola', false) + (donation_id, item_name, quantity, reserved_quantity, oz_per_item, estimated_value, food_type, details_confirmed) + VALUES ($1, 'Test Item', $2, $3, 3.4, 3.4, 'Granola', false) RETURNING item_id`, [donationId, qty, reserved], ); @@ -1410,8 +1410,8 @@ describe('DonationService', () => { ): Promise { const result = await testDataSource.query( `INSERT INTO donation_items - (donation_id, item_name, quantity, reserved_quantity, food_type, details_confirmed) - VALUES ($1, 'Test Item', $2, $3, 'Granola', true) + (donation_id, item_name, quantity, reserved_quantity, oz_per_item, estimated_value, food_type, details_confirmed) + VALUES ($1, 'Test Item', $2, $3, 3.4, 3.4, 'Granola', true) RETURNING item_id`, [donationId, qty, reserved], ); diff --git a/apps/backend/src/orders/order.service.spec.ts b/apps/backend/src/orders/order.service.spec.ts index 3d32f52a5..0a8198e90 100644 --- a/apps/backend/src/orders/order.service.spec.ts +++ b/apps/backend/src/orders/order.service.spec.ts @@ -1331,8 +1331,8 @@ ${request.pantry.shipmentAddressCity}, ${request.pantry.shipmentAddressState} ${ async function insertDonationItem(donationId: number): Promise { const [{ item_id }] = await testDataSource.query( - `INSERT INTO donation_items (donation_id, item_name, quantity, reserved_quantity, food_type, food_rescue, details_confirmed) - VALUES ($1, 'Test Item', 10, 10, 'Granola', false, false) + `INSERT INTO donation_items (donation_id, item_name, quantity, reserved_quantity, oz_per_item, estimated_value, food_type, food_rescue, details_confirmed) + VALUES ($1, 'Test Item', 10, 10, 3.4, 3.4, 'Granola', false, false) RETURNING item_id`, [donationId], ); diff --git a/apps/frontend/src/components/forms/fmCompleteRequiredActionsModal.tsx b/apps/frontend/src/components/forms/fmCompleteRequiredActionsModal.tsx index 165dce6c2..092d4f7b4 100644 --- a/apps/frontend/src/components/forms/fmCompleteRequiredActionsModal.tsx +++ b/apps/frontend/src/components/forms/fmCompleteRequiredActionsModal.tsx @@ -499,8 +499,12 @@ const FmCompleteRequiredActionsModal: React.FC< {stage === 'itemDetails' && ( <> - Please fill out the missing fields information to record - donation details. + Please confirm the following information to record donation + details. + + + Please do not include shipping/delivery costs in Food Donation + Value. @@ -511,15 +515,22 @@ const FmCompleteRequiredActionsModal: React.FC< > - + Food Item - + Oz. per item + + * + - - Donation Value + + Donation Value (Fair Market Value of Food Only) + + * + +