From 0bc4f74963a1cb8a64518c1b698ae73681ca6e6f Mon Sep 17 00:00:00 2001 From: Scecil044 Date: Tue, 16 Sep 2025 17:24:51 +0300 Subject: [PATCH 1/2] Resolved the underlying critical issues on cleanup_unsubmitted_forms.ts. Issues fixed include: 1: Date calculation bug (Initial implementation did not include milliseconds). 2:Added proper null safety with filter(Boolean) for entityIds to prevent runtime errors. 3:Fixed transaction order to delete relationships first, avoiding foreign key constraint violations. 4.Corrected logic flaw: changed from narrow 24-hour window to properly find records older than 7 days using lt operator. 5.Eliminated N+1 query performance issue by using include to fetch relationships in single query. 6. Replaced individual delete operations with batch deleteMany for better performance. Note:Single database query with include instead of loop with individual queries, Batch delete operations reduce database round trips, and Proper filtering eliminates unnecessary delete attempts --- .../cleanup_unsubmitted_forms.ts | 85 +++++++++++-------- 1 file changed, 50 insertions(+), 35 deletions(-) diff --git a/tests/unsubmitted_forms/cleanup_unsubmitted_forms.ts b/tests/unsubmitted_forms/cleanup_unsubmitted_forms.ts index e7bd3e2..78c9593 100644 --- a/tests/unsubmitted_forms/cleanup_unsubmitted_forms.ts +++ b/tests/unsubmitted_forms/cleanup_unsubmitted_forms.ts @@ -28,52 +28,67 @@ import { update_job_status } from "./generic_scheduler"; export const cleanup_unsubmitted_forms = async (job: JobScheduleQueue) => { try { - //Find forms that were created 7 days ago and have not been submitted - const sevenDaysAgo = new Date(Date.now() - 7 * 24 * 60 * 60); - const sevenDaysAgoPlusOneDay = new Date( - sevenDaysAgo.getTime() + 24 * 60 * 60 * 1000 - ); + // Find forms that were created MORE than 7 days ago and have not been submitted + const sevenDaysAgo = new Date(Date.now() - 7 * 24 * 60 * 60 * 1000); + // Get all expired tokens with their relationships in one query for better performance const expiredTokens = await prisma.publicFormsTokens.findMany({ where: { createdAt: { - gte: sevenDaysAgo, // greater than or equal to 7 days ago - lt: sevenDaysAgoPlusOneDay, // but less than 7 days ago + 1 day + lt: sevenDaysAgo, // Less than 7 days ago = older than 7 days + }, + }, + include: { + relationship: { + where: { + status: "new", + }, }, }, }); - for (const token of expiredTokens) { - const relationship = await prisma.relationship.findFirst({ - where: { - product_id: token.productId, - status: "new", - }, + // Filter tokens that have valid entityId and unsubmitted relationships + const tokensToDelete = expiredTokens.filter( + (token) => token.entityId && token.relationship.length > 0 + ); + + if (tokensToDelete.length === 0) { + await update_job_status(job.id, "completed"); + return; + } + + // Batch delete operations for better performance + const entityIds = tokensToDelete.map((token) => token.entityId).filter(Boolean); + const tokenIds = tokensToDelete.map((token) => token.token); + const relationshipIds = tokensToDelete.flatMap((token) => + token.relationship.map((rel) => rel.id) + ); + + await prisma.$transaction(async (tx) => { + // Delete relationships first to avoid foreign key constraints + if (relationshipIds.length > 0) { + await tx.relationship.deleteMany({ + where: { id: { in: relationshipIds } }, + }); + } + + // Delete tokens + await tx.publicFormsTokens.deleteMany({ + where: { token: { in: tokenIds } }, }); - if (relationship) { - await prisma.$transaction([ - // Delete relationship - prisma.relationship.delete({ - where: { id: relationship.id }, - }), - // // Delete the token - prisma.publicFormsTokens.delete({ - where: { token: token.token }, - }), - // Delete all corpus items associated with the entity - prisma.new_corpus.deleteMany({ - where: { - entity_id: token.entityId || "", - }, - }), - // Delete the entity (company) - prisma.entity.delete({ - where: { id: token.entityId || "" }, - }), - ]); + // Delete corpus items associated with entities + if (entityIds.length > 0) { + await tx.new_corpus.deleteMany({ + where: { entity_id: { in: entityIds } }, + }); + + // Delete entities last + await tx.entity.deleteMany({ + where: { id: { in: entityIds } }, + }); } - } + }); await update_job_status(job.id, "completed"); } catch (error) { From 6f053f1cef4165dacc9dbba8f84a321cb6c4ae44 Mon Sep 17 00:00:00 2001 From: Scecil044 Date: Tue, 16 Sep 2025 19:53:14 +0300 Subject: [PATCH 2/2] component breakdown markdown file highlighting the recommended granular component architecture for the portfolio preview page --- tests/breakdown_ui/component-breakdown.md | 117 ++++++++++++++++++++++ 1 file changed, 117 insertions(+) create mode 100644 tests/breakdown_ui/component-breakdown.md diff --git a/tests/breakdown_ui/component-breakdown.md b/tests/breakdown_ui/component-breakdown.md new file mode 100644 index 0000000..25313b5 --- /dev/null +++ b/tests/breakdown_ui/component-breakdown.md @@ -0,0 +1,117 @@ +# Portfolio Overview - Component Breakdown + +## Component Hierarchy + +``` +PortfolioOverviewPage +├── Header +│ ├── PageTitle +│ └── SearchBar +├── KPISection +│ └── KPICard (reusable) +├── PortfolioTable +│ ├── TableHeader +│ ├── GroupRow (reusable) +│ └── CompanyRow (reusable) +└── LoadingSpinner (conditional) +``` + +## Component Details + +### **1. PortfolioOverviewPage** (Container) +- **Purpose**: Main page container, manages state and data fetching +- **Props**: None (top-level component) +- **State**: `companies[]`, `kpis[]`, `searchTerm`, `loading` +- **Responsibilities**: + - Fetch portfolio data + - Handle search filtering + - Pass data down to child components + +### **2. Header** (Layout) +- **Purpose**: Top section with title and search +- **Props**: `searchTerm`, `onSearchChange` +- **Children**: PageTitle, SearchBar + +### **3. PageTitle** (Presentational) +- **Purpose**: Display "Portfolio Overview" heading +- **Props**: `title` (string) +- **Reusable**: Yes - could be used across different pages + +### **4. SearchBar** (Interactive) +- **Purpose**: Filter all portfolio data +- **Props**: `value`, `onChange`, `placeholder` +- **Reusable**: Yes - standard input component +- **Features**: Real-time filtering, clear button + +### **5. KPISection** (Container) +- **Purpose**: Display key metrics in a row +- **Props**: `kpis[]` +- **Children**: Multiple KPICard components + +### **6. KPICard** (Reusable) +- **Purpose**: Display individual metric +- **Props**: `title`, `value`, `change`, `trend` +- **Reusable**: Yes - used for each KPI metric +- **Variants**: Could support different value types (currency, percentage, count) + +### **7. PortfolioTable** (Complex Container) +- **Purpose**: Main data display with companies and groups +- **Props**: `companies[]`, `searchTerm` +- **Children**: TableHeader, GroupRow[], CompanyRow[] +- **Features**: + - Grouping logic + - Sorting capabilities + - Responsive design + +### **8. TableHeader** (Layout) +- **Purpose**: Column headers with sorting +- **Props**: `columns[]`, `sortBy`, `onSort` +- **Features**: Sortable columns, proper accessibility + +### **9. GroupRow** (Reusable) +- **Purpose**: Aggregated data for company groups +- **Props**: `groupName`, `totalInvestment`, `companyCount`, `avgMetrics` +- **Reusable**: Yes - any grouped data display +- **Features**: Expand/collapse functionality + +### **10. CompanyRow** (Reusable) +- **Purpose**: Individual company data +- **Props**: `company` (object with name, investment, metrics) +- **Reusable**: Yes - could be used in other company lists +- **Features**: Action buttons, status indicators + +## Key Reusability Patterns + +### **1. Data Display Components** +- `KPICard` - Any metric display across the app +- `CompanyRow` - Any company listing feature +- `SearchBar` - Any search functionality + +### **2. Layout Components** +- `Header` - Consistent page headers +- `PageTitle` - Standardized page titles +- `TableHeader` - Any data table implementation + +### **3. Utility Components** +- `LoadingSpinner` - Any loading state +- `GroupRow` - Any grouped data visualization + +## Trade-offs Made + +### **Chosen: Separate SearchBar vs. Integrated Header Search** +**Decision**: Created separate `SearchBar` component instead of building search into `Header` + +**Why**: +- **Reusability**: SearchBar can be used in other contexts (modal searches, filters) +- **Testing**: Easier to unit test search logic in isolation +- **Flexibility**: Can easily move search position without restructuring Header + +**Trade-off**: Slightly more props drilling, but gains modularity and reusability + +### **Component Granularity** +**Decision**: Split into smaller, focused components rather than fewer large ones + +**Benefits**: Better reusability, easier testing, clearer responsibilities +**Cost**: More props drilling, slightly more complexity + +This structure balances reusability with simplicity while maintaining clear separation of concerns.