From ae1bbf9041e79b90e8d68da7699ccbc061654693 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 22 Mar 2026 10:38:03 +0000 Subject: [PATCH 1/2] Initial plan From 77dfde34b992a3a698959efed924e0c682a6f06a Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 22 Mar 2026 10:44:26 +0000 Subject: [PATCH 2/2] fix: address PR review comments from copilot review MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Wrap KDocument write ops (update/supersede/createMany) in $transaction - Fix BOX_KEY_TO_K1DATA_FIELD: 4c → 4, L_CAPITAL_CONTRIBUTED → L_CONTRIBUTED - Fix customLabel logic: use ?? instead of ternary to preserve override value - Fix getSectionLabel() to match actual K1BoxSection enum values from backend - Replace $executeRawUnsafe with $executeRaw; fix total_amount type to string - Remove agg.breakdown template refs to match simplified {name,value} type Co-authored-by: RobertgPatch <5817970+RobertgPatch@users.noreply.github.com> Agent-Logs-Url: https://github.com/RobertgPatch/portfolio-management/sessions/784d8100-7260-4f87-ac51-662285407c97 --- .../app/k1-import/k1-field-mapper.service.ts | 2 +- .../src/app/k1-import/k1-import.service.ts | 125 +++++++++--------- .../k1-import/k1-materialized-view.service.ts | 6 +- .../cell-mapping-page.component.ts | 7 +- .../k-document-detail/k-document-detail.html | 10 -- 5 files changed, 71 insertions(+), 79 deletions(-) diff --git a/apps/api/src/app/k1-import/k1-field-mapper.service.ts b/apps/api/src/app/k1-import/k1-field-mapper.service.ts index 2bf9e5035..5ef3960d4 100644 --- a/apps/api/src/app/k1-import/k1-field-mapper.service.ts +++ b/apps/api/src/app/k1-import/k1-field-mapper.service.ts @@ -42,7 +42,7 @@ export class K1FieldMapperService { mappedFields.push({ ...field, label: def.label, - customLabel: def.customLabel ? def.label : field.customLabel, + customLabel: def.customLabel ?? field.customLabel, cellType: def.dataType } as any); } else { diff --git a/apps/api/src/app/k1-import/k1-import.service.ts b/apps/api/src/app/k1-import/k1-import.service.ts index 60fb5fff1..c38dda2ec 100644 --- a/apps/api/src/app/k1-import/k1-import.service.ts +++ b/apps/api/src/app/k1-import/k1-import.service.ts @@ -712,9 +712,9 @@ export class K1ImportService { '1': 'ordinaryIncome', '2': 'netRentalIncome', '3': 'otherRentalIncome', - '4a': 'guaranteedPayments', // 4a guaranteed payments (services) - '4b': 'guaranteedPayments', // 4b guaranteed payments (capital) — merged - '4c': 'guaranteedPayments', // 4c total guaranteed payments + '4': 'guaranteedPayments', // 4 guaranteed payments (services) + '4a': 'guaranteedPayments', // 4a guaranteed payments (capital) — merged + '4b': 'guaranteedPayments', // 4b total guaranteed payments — merged '5': 'interestIncome', '6a': 'dividends', '6b': 'qualifiedDividends', @@ -734,7 +734,7 @@ export class K1ImportService { '21': 'foreignTaxesPaid', // Section L: Tax basis / capital account fields 'L_BEG_CAPITAL': 'beginningTaxBasis', - 'L_CAPITAL_CONTRIBUTED': 'k1CapitalAccount', + 'L_CONTRIBUTED': 'k1CapitalAccount', 'L_END_CAPITAL': 'endingTaxBasis', }; @@ -753,69 +753,74 @@ export class K1ImportService { // Merge named fields into the document data (named fields take precedence for views) const finalDocumentData = { ...kDocumentData, ...k1DataSnapshot }; - // FR-012: Create or update KDocument + // FR-012: Create or update KDocument with K1LineItems in a single transaction + // to ensure consistent state (no superseded-but-unreplaced line items). let kDocument; - if (existingKDocument && data.existingKDocumentAction === 'UPDATE') { - kDocument = await this.prismaService.kDocument.update({ - where: { id: existingKDocument.id }, - data: { - filingStatus: data.filingStatus, - data: finalDocumentData as any, - documentFileId: session.documentId + await this.prismaService.$transaction(async (tx) => { + if (existingKDocument && data.existingKDocumentAction === 'UPDATE') { + kDocument = await tx.kDocument.update({ + where: { id: existingKDocument.id }, + data: { + filingStatus: data.filingStatus, + data: finalDocumentData as any, + documentFileId: session.documentId + } + }); + + // FR-016: Mark existing active K1LineItems as superseded (ESTIMATED→FINAL) + await tx.k1LineItem.updateMany({ + where: { + kDocumentId: existingKDocument.id, + isSuperseded: false + }, + data: { isSuperseded: true } + }); + } else { + // CREATE_NEW or no existing document + if (existingKDocument && data.existingKDocumentAction === 'CREATE_NEW') { + // Delete existing unique constraint holder to create new + await tx.kDocument.delete({ + where: { id: existingKDocument.id } + }); } - }); - // FR-016: Mark existing active K1LineItems as superseded (ESTIMATED→FINAL) - await this.prismaService.k1LineItem.updateMany({ - where: { - kDocumentId: existingKDocument.id, - isSuperseded: false - }, - data: { isSuperseded: true } - }); - } else { - // CREATE_NEW or no existing document - if (existingKDocument && data.existingKDocumentAction === 'CREATE_NEW') { - // Delete existing unique constraint holder to create new - await this.prismaService.kDocument.delete({ - where: { id: existingKDocument.id } + kDocument = await tx.kDocument.create({ + data: { + partnershipId: session.partnershipId, + type: 'K1', + taxYear: session.taxYear, + filingStatus: data.filingStatus, + data: finalDocumentData as any, + documentFileId: session.documentId + } }); } - kDocument = await this.prismaService.kDocument.create({ - data: { - partnershipId: session.partnershipId, - type: 'K1', - taxYear: session.taxYear, - filingStatus: data.filingStatus, - data: finalDocumentData as any, - documentFileId: session.documentId - } - }); - } - - // Create K1LineItem rows (the authoritative normalized data) - if (lineItemsToCreate.length > 0) { - await this.prismaService.k1LineItem.createMany({ - data: lineItemsToCreate.map((item) => ({ - kDocumentId: kDocument.id, - boxKey: item.boxKey, - amount: item.amount, - textValue: item.textValue, - rawText: item.rawText, - confidence: item.confidence, - sourcePage: item.sourcePage, - sourceCoords: item.sourceCoords, - isUserEdited: item.isUserEdited, - isSuperseded: false - })) - }); + // Create K1LineItem rows (the authoritative normalized data) + if (lineItemsToCreate.length > 0) { + await tx.k1LineItem.createMany({ + data: lineItemsToCreate.map((item) => ({ + kDocumentId: kDocument.id, + boxKey: item.boxKey, + amount: item.amount, + textValue: item.textValue, + rawText: item.rawText, + confidence: item.confidence, + sourcePage: item.sourcePage, + sourceCoords: item.sourceCoords, + isUserEdited: item.isUserEdited, + isSuperseded: false + })) + }); - this.logger.log( - `Session ${sessionId}: Created ${lineItemsToCreate.length} K1LineItem rows for KDocument ${kDocument.id}` - ); + this.logger.log( + `Session ${sessionId}: Created ${lineItemsToCreate.length} K1LineItem rows for KDocument ${kDocument.id}` + ); + } + }); - // Emit event to refresh materialized views (US5) + // Emit event after the transaction commits to refresh materialized views (US5) + if (kDocument && lineItemsToCreate.length > 0) { this.eventEmitter.emit('k-document.changed', { kDocumentId: kDocument.id, partnershipId: kDocument.partnershipId @@ -886,7 +891,7 @@ export class K1ImportService { // This drives Portfolio Summary (DPI/RVPI/TVPI) computations const sectionLData = { beginningCapital: boxValues['L_BEG_CAPITAL'] ?? null, - capitalContributed: boxValues['L_CAPITAL_CONTRIBUTED'] ?? null, + capitalContributed: boxValues['L_CONTRIBUTED'] ?? null, endingCapital: boxValues['L_END_CAPITAL'] ?? null, withdrawals: boxValues['L_WITHDRAWALS'] ?? null }; diff --git a/apps/api/src/app/k1-import/k1-materialized-view.service.ts b/apps/api/src/app/k1-import/k1-materialized-view.service.ts index 8897f083c..549753cbf 100644 --- a/apps/api/src/app/k1-import/k1-materialized-view.service.ts +++ b/apps/api/src/app/k1-import/k1-materialized-view.service.ts @@ -22,9 +22,7 @@ export class K1MaterializedViewService { async refreshAll() { try { - await this.prismaService.$executeRawUnsafe( - `REFRESH MATERIALIZED VIEW CONCURRENTLY mv_k1_partnership_year_summary` - ); + await this.prismaService.$executeRaw`REFRESH MATERIALIZED VIEW CONCURRENTLY mv_k1_partnership_year_summary`; this.logger.log('Materialized view mv_k1_partnership_year_summary refreshed.'); } catch (error) { this.logger.error( @@ -45,7 +43,7 @@ export class K1MaterializedViewService { box_key: string; label: string; section: string | null; - total_amount: number | null; + total_amount: string | null; line_count: bigint; }> > { diff --git a/apps/client/src/app/pages/cell-mapping/cell-mapping-page.component.ts b/apps/client/src/app/pages/cell-mapping/cell-mapping-page.component.ts index d99356423..ae546f8d0 100644 --- a/apps/client/src/app/pages/cell-mapping/cell-mapping-page.component.ts +++ b/apps/client/src/app/pages/cell-mapping/cell-mapping-page.component.ts @@ -105,10 +105,9 @@ export class CellMappingPageComponent implements OnInit { SECTION_J: 'Section J', SECTION_K: 'Section K', SECTION_L: 'Section L', - SECTION_M_N: 'Sections M & N', - PART_III_A: 'Part III (Income)', - PART_III_B: 'Part III (Deductions)', - PART_III_C: 'Part III (Other)' + SECTION_M: 'Section M', + SECTION_N: 'Section N', + PART_III: 'Part III' }; return map[section] ?? section; } diff --git a/apps/client/src/app/pages/k-documents/k-document-detail/k-document-detail.html b/apps/client/src/app/pages/k-documents/k-document-detail/k-document-detail.html index 627db29ba..4396cef11 100644 --- a/apps/client/src/app/pages/k-documents/k-document-detail/k-document-detail.html +++ b/apps/client/src/app/pages/k-documents/k-document-detail/k-document-detail.html @@ -54,16 +54,6 @@
{{ agg.value | currency:'USD':'symbol':'1.2-6' }}
- @if (agg.breakdown && agg.breakdown.length > 0) { -
- @for (item of agg.breakdown; track item.boxNumber) { -
- Box {{ item.boxNumber }}: - {{ item.value | currency:'USD':'symbol':'1.2-6' }} -
- } -
- } }