Skip to content

integrate description generation to donor offer creation + bug fixes - #190

Merged
Sectonic merged 3 commits into
mainfrom
alex/181-donor-process-change
Oct 21, 2025
Merged

Sectonic merged 3 commits into
mainfrom
alex/181-donor-process-change

Conversation

@alexdcdc

Copy link
Copy Markdown
Collaborator

Description

Resolves #181

Checklist

  • The ticket is mentioned above
  • The changes fulfill the success criteria of the ticket
  • The changes were self-reviewed
  • A review was requested

@alexdcdc
alexdcdc requested a review from Sectonic October 21, 2025 04:17
@alexdcdc alexdcdc self-assigned this Oct 21, 2025
@netlify

netlify Bot commented Oct 21, 2025 •

Copy link
Copy Markdown

❌ Deploy Preview for hope-for-haiti failed. Why did it fail? →

Name Link
🔨 Latest commit 6cec392
🔍 Latest deploy log https://app.netlify.com/projects/hope-for-haiti/deploys/68f7bc06f456d7000844e766

@greptile-apps greptile-apps Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Disclaimer: This comment was AI-generated and is not necessarily completely accurate. Please take code comments with a grain of salt.

Greptile Overview

Greptile Summary

This PR integrates AI-powered description generation directly into the donor offer creation workflow, eliminating the need for a separate /api/suggest/description endpoint. The changes add an optional description field to the GeneralItem Prisma model and modify the POST /api/donorOffers route to call DescriptionService.getOrGenerateDescriptions inline before creating the donor offer. File parsing logic was refactored to remap donor-provided column names (e.g., "Generic Description") to internal field names (e.g., "title") using a Map-based approach. The donor offer service was changed from createMany bulk inserts to individual upsert operations to handle duplicate items by incrementing initialQuantity. Frontend components (AdminDynamicDonorOfferScreen, PreviewTable) now display the generated descriptions. This consolidation reduces API roundtrips and aligns with the architecture described in issue #181.

PR Description Notes:

  • The PR title states "bug fixes" but the description doesn't specify which bugs were fixed beyond the process change integration.

Changed Files
Filename Score Overview
prisma/schema.prisma 5/5 Added optional description field to GeneralItem model for storing LLM-generated descriptions
prisma/migrations/20251021023559_add_general_item_description/migration.sql 5/5 Migration adds nullable description TEXT column to GeneralItem table
src/services/descriptionService.ts 5/5 Added deduplication logic using missingIds Set to prevent processing duplicate items
package.json 4/5 Updated openai dependency from ^4.63.0 to ^4.104.0 (41 minor versions) and reordered alphabetically
src/screens/DonorOffersScreens/AdminDynamicDonorOfferScreen.tsx 5/5 Added optional description field to type and new table column displaying descriptions
src/app/donorOffers/create/page.tsx 4/5 Changed endpoint from /api/donorOffers/create to /api/donorOffers and added loading modal; contains debug console.log
src/components/DonorOffers/PreviewTable.tsx 4/5 Added description field to type and table column; no fallback for undefined values
src/app/api/donorOffers/route.ts 2/5 Integrated description generation into POST endpoint; missing validation and error handling for critical operations
src/services/fileService.ts 2/5 Refactored to Map-based column remapping with hardcoded defaults; validation runs before empty row filtering
src/services/donorOfferService.ts 2/5 Changed from createMany to individual upsert operations; unsafe type casting and performance issues

Confidence score: 2/5

  • This PR contains critical issues in the backend that could cause runtime errors and data integrity problems in production.
  • Score reflects three major concerns: (1) unsafe type casting of potentially undefined expirationDate values in upsert operations will cause crashes, (2) missing validation and error handling when mapping descriptions to items could lead to index mismatches, (3) validation logic in file parsing runs before empty row filtering, allowing invalid files to fail prematurely. Additionally, the OpenAI SDK version jump spans 41 minor versions without documented compatibility testing.
  • Pay close attention to src/app/api/donorOffers/route.ts, src/services/donorOfferService.ts, and src/services/fileService.ts - all three contain logic errors that will cause failures.

Issues Requiring Attention

Critical Issues:

  1. src/services/donorOfferService.ts (Line 472): The expirationDate field is cast as Date when looking up the unique index, but the schema allows it to be undefined. This will cause runtime errors when attempting upsert operations on items without expiration dates.

  2. src/app/api/donorOffers/route.ts (Lines 38-49): No validation that descriptions array length matches parsedFileData.data length before the forEach loop. If description generation fails partially or returns fewer elements, descriptions[index] will be undefined, causing data corruption. Add length validation and error handling for description generation failures.

  3. src/services/fileService.ts (Lines 163, 177, 183): Validation error checking occurs at lines 163/177 (via containsRequiredKeys) before empty row filtering at line 183. This means files with valid columns but empty rows will fail validation before cleanup runs. The validation should occur after filtering.

  4. src/app/donorOffers/create/page.tsx (Line 52): Debug console.log(result) statement left in production code. Remove before merging.

  5. src/services/fileService.ts: The parseFinalizeFile method doesn't include the new Map-based remapping logic, creating inconsistency between create and finalize workflows. Both should use the same column mapping approach.

Code Quality Issues:

  1. src/services/donorOfferService.ts (Lines 491-494): Commented-out code should be removed: /* await tx.generalItem.createMany({ ... }) */

  2. src/services/donorOfferService.ts (Line 481): The spread operator ...item may include fields not in the Prisma schema. Explicitly map only valid schema fields in the create block.

  3. src/components/DonorOffers/PreviewTable.tsx (Line 73): item.description has no fallback for undefined values. Consider adding || "N/A" or || "Generating..." to handle missing descriptions gracefully.


PR Checklist Score: 62/100

Areas Needing Improvement:

General:

  • Console.logs or commented out code: Remove console.log(result) at line 52 in src/app/donorOffers/create/page.tsx and commented-out createMany code at lines 491-494 in src/services/donorOfferService.ts.

Backend:

  • Handles edge cases where necessary: Missing edge case handling for undefined expirationDate values in upsert operations (line 472 of donorOfferService.ts); missing validation that descriptions array matches items array length (lines38-49 of route.ts).
  • Uses try/catch blocks for error handling: No error handling around description generation in /api/donorOffers route; if DescriptionService.getOrGenerateDescriptions throws, the entire donor offer creation fails without cleanup.
  • Input is appropriately validated: File parsing validation runs before empty row filtering, causing premature failures; no validation of array lengths before mapping descriptions to items.
  • Operations are optimized to avoid unnecessary computations: Changed from createMany (single DB operation) to loop of individual upsert operations (N operations), significantly degrading performance for large uploads.
  • Schemas are strict: The spread operator at line 481 in donorOfferService.ts may inject fields not in the schema; use explicit field mapping instead.

Sequence Diagram

sequenceDiagram
    participant User
    participant CreateDonorOfferPage
    participant FileUpload
    participant API as /api/donorOffers
    participant FileService
    participant DescriptionService
    participant OpenAI as Azure OpenAI
    participant DB as Database
    participant DonorOfferService

    User->>CreateDonorOfferPage: "Fill form (offer name, dates, partners)"
    User->>CreateDonorOfferPage: "Upload CSV/XLSX file"
    CreateDonorOfferPage->>FileUpload: "handleFileChange(file)"
    FileUpload->>API: "POST /api/donorOffers?preview=true"
    API->>FileService: "parseDonorOfferFile(file)"
    FileService->>FileService: "Parse CSV/XLSX, extract item data"
    FileService-->>API: "parsedFileData (items without descriptions)"
    
    API->>API: "Build itemDescriptionInput from parsed data"
    API->>DescriptionService: "getOrGenerateDescriptions(itemDescriptionInput)"
    
    DescriptionService->>DB: "Query DescriptionLookup table for cached descriptions"
    DB-->>DescriptionService: "Return existing descriptions"
    
    alt "Some descriptions missing from cache"
        DescriptionService->>DescriptionService: "Identify missing items"
        DescriptionService->>OpenAI: "Generate bilingual descriptions (Haitian/English)"
        OpenAI-->>DescriptionService: "Return generated descriptions"
        DescriptionService->>DB: "Upsert new descriptions to cache"
    end
    
    DescriptionService-->>API: "Array of descriptions"
    API->>API: "Attach descriptions to parsedFileData items"
    API->>DonorOfferService: "createDonorOffer(formData, parsedFileData, preview=true)"
    DonorOfferService-->>API: "Preview result with first 8 items"
    API-->>CreateDonorOfferPage: "Success with donorOfferItems including descriptions"
    
    CreateDonorOfferPage->>PreviewTable: "Render preview with descriptions"
    PreviewTable-->>User: "Display table with Description column"
    
    User->>CreateDonorOfferPage: "Click Submit Offer"
    CreateDonorOfferPage->>API: "POST /api/donorOffers (final submission)"
    API->>FileService: "parseDonorOfferFile(file)"
    API->>DescriptionService: "getOrGenerateDescriptions(itemDescriptionInput)"
    DescriptionService-->>API: "Descriptions (from cache or newly generated)"
    API->>API: "Attach descriptions to items"
    API->>DonorOfferService: "createDonorOffer(formData, parsedFileData, preview=false)"
    
    DonorOfferService->>DB: "Create DonorOffer record"
    DonorOfferService->>DB: "Upsert GeneralItem records with descriptions"
    DB-->>DonorOfferService: "Success"
    DonorOfferService-->>API: "Success result"
    API-->>CreateDonorOfferPage: "Success response"
    CreateDonorOfferPage->>CreateDonorOfferPage: "Show success modal"
Loading

10 files reviewed, 10 comments

Edit Code Review Agent Settings | Greptile

Comment thread src/app/donorOffers/create/page.tsx Outdated
previewEndpoint: "/api/donorOffers/create?preview=true",
previewEndpoint: "/api/donorOffers?preview=true",
onSuccess: (result) => {
console.log(result);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

style: Remove debug console.log before merging

item.visible ? "Visible" : "Hidden",
item.notes,
]),
item.description,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

style: description can be undefined - should display fallback like 'N/A' or 'Generating...' instead of blank cell

Comment thread src/app/api/donorOffers/route.ts Outdated
Comment on lines +38 to +42
const itemDescriptionInput = parsedFileData.data.map((item) => ({
title: item["title"] as string,
type: item["type"] as string,
unitType: item["unitType"] as string,
}));

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

logic: Missing validation for required fields. If title, type, or unitType are undefined/null in parsedFileData.data, this will pass invalid data to DescriptionService

Comment thread src/app/api/donorOffers/route.ts Outdated
Comment on lines +44 to +45
const descriptions =
await DescriptionService.getOrGenerateDescriptions(itemDescriptionInput);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

logic: No error handling if getOrGenerateDescriptions fails or returns fewer descriptions than expected. This could lead to undefined values being assigned on line 48

Comment thread src/app/api/donorOffers/route.ts Outdated
Comment on lines +47 to +49
parsedFileData.data.forEach((item, index) => {
item.description = descriptions[index];
});

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

logic: If descriptions.length !== parsedFileData.data.length, array index mismatch will assign wrong descriptions or undefined to items

Comment thread src/services/fileService.ts Outdated
Comment on lines +183 to +189
jsonData = jsonData.filter((row) => {
return (
row["Generic Description"] !== undefined &&
row["Generic Description"] !== null &&
row["Generic Description"] !== ""
); //try to filter out invalid rows based on required field
});

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

logic: filtering happens after validation check, so invalid files with missing required keys will throw error before filtering empty rows. Move filter before containsRequiredKeys check on line 163

Comment thread src/services/fileService.ts Outdated

if (!meta.fields || !containsRequiredKeys(meta.fields)) {
throw new ArgumentError("CSV does not contain required keys");
throw new ArgumentError("XLSX does not contain required keys");

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

syntax: error message says 'XLSX' but this branch handles xlsx files correctly - probably copy-paste error from line177 CSV branch

Comment thread src/services/donorOfferService.ts Outdated
donorOfferId: item.donorOfferId,
title: item.title,
type: item.type,
expirationDate: item.expirationDate as Date,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

logic: expirationDate can be undefined according to schema (line 44-45), but cast as Date will cause runtime error when undefined. Check for undefined before using in unique index

Comment thread src/services/donorOfferService.ts Outdated
Comment on lines +465 to +490
for (const item of itemsWithDonorOfferId) {
await tx.generalItem.upsert({
where: {
donorOfferId_title_type_expirationDate_unitType_quantityPerUnit: {
donorOfferId: item.donorOfferId,
title: item.title,
type: item.type,
expirationDate: item.expirationDate as Date,
unitType: item.unitType,
quantityPerUnit: item.quantityPerUnit,
},
},
update: {
initialQuantity: { increment: item.initialQuantity },
},
create: {
...item,
},
select: {
id: true,
title: true,
type: true,
unitType: true,
},
});
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

style: looping with individual upsert operations inside transaction is significantly slower than createMany. For bulk operations with hundreds of items, this could cause transaction timeouts. What is the expected maximum number of items in a single donor offer upload?

Comment thread src/services/donorOfferService.ts Outdated
Comment on lines +491 to +494
/*
await tx.generalItem.createMany({
data: itemsWithDonorOfferId,
});
});*/

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

style: remove commented-out code before merging

Comment thread src/services/donorOfferService.ts Outdated
quantityPerUnit: item.quantityPerUnit,
},
},
update: {

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

update to aggregate quantities if row w/ data already exists

"unitType",
"quantityPerUnit",
];
const requiredKeys = new Map<string, string>([

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

mapping xlsx column names to schema fields

@Sectonic
Sectonic merged commit 69222d7 into main Oct 21, 2025
0 of 4 checks passed
@Sectonic
Sectonic deleted the alex/181-donor-process-change branch October 21, 2025 17:03
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Donor Form Creation Process Change

2 participants