Browse Source

fix: address PR review comments from copilot review

- 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
pull/6701/head
copilot-swe-agent[bot] 2 months ago
parent
commit
77dfde34b9
  1. 2
      apps/api/src/app/k1-import/k1-field-mapper.service.ts
  2. 29
      apps/api/src/app/k1-import/k1-import.service.ts
  3. 6
      apps/api/src/app/k1-import/k1-materialized-view.service.ts
  4. 7
      apps/client/src/app/pages/cell-mapping/cell-mapping-page.component.ts
  5. 10
      apps/client/src/app/pages/k-documents/k-document-detail/k-document-detail.html

2
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 {

29
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,10 +753,12 @@ 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;
await this.prismaService.$transaction(async (tx) => {
if (existingKDocument && data.existingKDocumentAction === 'UPDATE') {
kDocument = await this.prismaService.kDocument.update({
kDocument = await tx.kDocument.update({
where: { id: existingKDocument.id },
data: {
filingStatus: data.filingStatus,
@ -766,7 +768,7 @@ export class K1ImportService {
});
// FR-016: Mark existing active K1LineItems as superseded (ESTIMATED→FINAL)
await this.prismaService.k1LineItem.updateMany({
await tx.k1LineItem.updateMany({
where: {
kDocumentId: existingKDocument.id,
isSuperseded: false
@ -777,12 +779,12 @@ export class K1ImportService {
// 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({
await tx.kDocument.delete({
where: { id: existingKDocument.id }
});
}
kDocument = await this.prismaService.kDocument.create({
kDocument = await tx.kDocument.create({
data: {
partnershipId: session.partnershipId,
type: 'K1',
@ -796,7 +798,7 @@ export class K1ImportService {
// Create K1LineItem rows (the authoritative normalized data)
if (lineItemsToCreate.length > 0) {
await this.prismaService.k1LineItem.createMany({
await tx.k1LineItem.createMany({
data: lineItemsToCreate.map((item) => ({
kDocumentId: kDocument.id,
boxKey: item.boxKey,
@ -814,8 +816,11 @@ export class K1ImportService {
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
};

6
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;
}>
> {

7
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;
}

10
apps/client/src/app/pages/k-documents/k-document-detail/k-document-detail.html

@ -54,16 +54,6 @@
<div class="aggregation-value">
{{ agg.value | currency:'USD':'symbol':'1.2-6' }}
</div>
@if (agg.breakdown && agg.breakdown.length > 0) {
<div class="breakdown">
@for (item of agg.breakdown; track item.boxNumber) {
<div class="breakdown-row">
<span class="box-label">Box {{ item.boxNumber }}:</span>
<span class="box-value">{{ item.value | currency:'USD':'symbol':'1.2-6' }}</span>
</div>
}
</div>
}
</mat-card-content>
</mat-card>
}

Loading…
Cancel
Save