From 3d25b555b6d765d1dd25f3c0646a982c8a4d6fb4 Mon Sep 17 00:00:00 2001 From: Robert Patch Date: Wed, 18 Mar 2026 01:07:07 -0700 Subject: [PATCH] feat(k1-import): Phase 2 foundational - modules, extractor interface, seed data, routes --- apps/api/src/app/app.module.ts | 4 + .../cell-mapping/cell-mapping.controller.ts | 27 +++ .../app/cell-mapping/cell-mapping.module.ts | 14 ++ .../app/cell-mapping/cell-mapping.service.ts | 181 ++++++++++++++++ .../extractors/k1-extractor.interface.ts | 22 ++ .../src/app/k1-import/k1-import.controller.ts | 31 +++ .../api/src/app/k1-import/k1-import.module.ts | 32 +++ .../src/app/k1-import/k1-import.service.ts | 30 +++ apps/client/src/app/app.routes.ts | 14 ++ .../cell-mapping/cell-mapping-page.routes.ts | 15 ++ .../pages/k1-import/k1-import-page.routes.ts | 15 ++ .../app/services/k1-import-data.service.ts | 201 ++++++++++++++++++ specs/004-k1-scan-import/tasks.md | 14 +- 13 files changed, 593 insertions(+), 7 deletions(-) create mode 100644 apps/api/src/app/cell-mapping/cell-mapping.controller.ts create mode 100644 apps/api/src/app/cell-mapping/cell-mapping.module.ts create mode 100644 apps/api/src/app/cell-mapping/cell-mapping.service.ts create mode 100644 apps/api/src/app/k1-import/extractors/k1-extractor.interface.ts create mode 100644 apps/api/src/app/k1-import/k1-import.controller.ts create mode 100644 apps/api/src/app/k1-import/k1-import.module.ts create mode 100644 apps/api/src/app/k1-import/k1-import.service.ts create mode 100644 apps/client/src/app/pages/cell-mapping/cell-mapping-page.routes.ts create mode 100644 apps/client/src/app/pages/k1-import/k1-import-page.routes.ts create mode 100644 apps/client/src/app/services/k1-import-data.service.ts diff --git a/apps/api/src/app/app.module.ts b/apps/api/src/app/app.module.ts index 7a2b1a948..57d7fb2f8 100644 --- a/apps/api/src/app/app.module.ts +++ b/apps/api/src/app/app.module.ts @@ -55,6 +55,8 @@ import { FamilyOfficeModule } from './family-office/family-office.module'; import { HealthModule } from './health/health.module'; import { ImportModule } from './import/import.module'; import { InfoModule } from './info/info.module'; +import { CellMappingModule } from './cell-mapping/cell-mapping.module'; +import { K1ImportModule } from './k1-import/k1-import.module'; import { KDocumentModule } from './k-document/k-document.module'; import { LogoModule } from './logo/logo.module'; import { PartnershipModule } from './partnership/partnership.module'; @@ -129,6 +131,8 @@ import { UserModule } from './user/user.module'; HealthModule, ImportModule, InfoModule, + CellMappingModule, + K1ImportModule, KDocumentModule, LogoModule, MarketDataModule, diff --git a/apps/api/src/app/cell-mapping/cell-mapping.controller.ts b/apps/api/src/app/cell-mapping/cell-mapping.controller.ts new file mode 100644 index 000000000..4c89640f9 --- /dev/null +++ b/apps/api/src/app/cell-mapping/cell-mapping.controller.ts @@ -0,0 +1,27 @@ +import { HasPermission } from '@ghostfolio/api/decorators/has-permission.decorator'; +import { HasPermissionGuard } from '@ghostfolio/api/guards/has-permission.guard'; +import { permissions } from '@ghostfolio/common/permissions'; +import type { RequestWithUser } from '@ghostfolio/common/types'; + +import { + Body, + Controller, + Delete, + Get, + Inject, + Put, + Query, + UseGuards +} from '@nestjs/common'; +import { REQUEST } from '@nestjs/core'; +import { AuthGuard } from '@nestjs/passport'; + +import { CellMappingService } from './cell-mapping.service'; + +@Controller('cell-mapping') +export class CellMappingController { + public constructor( + private readonly cellMappingService: CellMappingService, + @Inject(REQUEST) private readonly request: RequestWithUser + ) {} +} diff --git a/apps/api/src/app/cell-mapping/cell-mapping.module.ts b/apps/api/src/app/cell-mapping/cell-mapping.module.ts new file mode 100644 index 000000000..605eb5a37 --- /dev/null +++ b/apps/api/src/app/cell-mapping/cell-mapping.module.ts @@ -0,0 +1,14 @@ +import { PrismaModule } from '@ghostfolio/api/services/prisma/prisma.module'; + +import { Module } from '@nestjs/common'; + +import { CellMappingController } from './cell-mapping.controller'; +import { CellMappingService } from './cell-mapping.service'; + +@Module({ + controllers: [CellMappingController], + exports: [CellMappingService], + imports: [PrismaModule], + providers: [CellMappingService] +}) +export class CellMappingModule {} diff --git a/apps/api/src/app/cell-mapping/cell-mapping.service.ts b/apps/api/src/app/cell-mapping/cell-mapping.service.ts new file mode 100644 index 000000000..13dd20a4d --- /dev/null +++ b/apps/api/src/app/cell-mapping/cell-mapping.service.ts @@ -0,0 +1,181 @@ +import { PrismaService } from '@ghostfolio/api/services/prisma/prisma.service'; + +import { Injectable, OnModuleInit } from '@nestjs/common'; + +/** Default IRS K-1 (Form 1065) cell mappings */ +const IRS_DEFAULT_MAPPINGS: Array<{ + boxNumber: string; + label: string; + description: string; + sortOrder: number; +}> = [ + { boxNumber: '1', label: 'Ordinary business income (loss)', description: 'IRS Schedule K-1 Box 1', sortOrder: 1 }, + { boxNumber: '2', label: 'Net rental real estate income (loss)', description: 'IRS Schedule K-1 Box 2', sortOrder: 2 }, + { boxNumber: '3', label: 'Other net rental income (loss)', description: 'IRS Schedule K-1 Box 3', sortOrder: 3 }, + { boxNumber: '4', label: 'Guaranteed payments for services', description: 'IRS Schedule K-1 Box 4', sortOrder: 4 }, + { boxNumber: '4a', label: 'Guaranteed payments for capital', description: 'IRS Schedule K-1 Box 4a', sortOrder: 5 }, + { boxNumber: '4b', label: 'Total guaranteed payments', description: 'IRS Schedule K-1 Box 4b', sortOrder: 6 }, + { boxNumber: '5', label: 'Interest income', description: 'IRS Schedule K-1 Box 5', sortOrder: 7 }, + { boxNumber: '6a', label: 'Ordinary dividends', description: 'IRS Schedule K-1 Box 6a', sortOrder: 8 }, + { boxNumber: '6b', label: 'Qualified dividends', description: 'IRS Schedule K-1 Box 6b', sortOrder: 9 }, + { boxNumber: '6c', label: 'Dividend equivalents', description: 'IRS Schedule K-1 Box 6c', sortOrder: 10 }, + { boxNumber: '7', label: 'Royalties', description: 'IRS Schedule K-1 Box 7', sortOrder: 11 }, + { boxNumber: '8', label: 'Net short-term capital gain (loss)', description: 'IRS Schedule K-1 Box 8', sortOrder: 12 }, + { boxNumber: '9a', label: 'Net long-term capital gain (loss)', description: 'IRS Schedule K-1 Box 9a', sortOrder: 13 }, + { boxNumber: '9b', label: 'Collectibles (28%) gain (loss)', description: 'IRS Schedule K-1 Box 9b', sortOrder: 14 }, + { boxNumber: '9c', label: 'Unrecaptured section 1250 gain', description: 'IRS Schedule K-1 Box 9c', sortOrder: 15 }, + { boxNumber: '10', label: 'Net section 1231 gain (loss)', description: 'IRS Schedule K-1 Box 10', sortOrder: 16 }, + { boxNumber: '11', label: 'Other income (loss)', description: 'IRS Schedule K-1 Box 11', sortOrder: 17 }, + { boxNumber: '12', label: 'Section 179 deduction', description: 'IRS Schedule K-1 Box 12', sortOrder: 18 }, + { boxNumber: '13', label: 'Other deductions', description: 'IRS Schedule K-1 Box 13', sortOrder: 19 }, + { boxNumber: '14', label: 'Self-employment earnings (loss)', description: 'IRS Schedule K-1 Box 14', sortOrder: 20 }, + { boxNumber: '15', label: 'Credits', description: 'IRS Schedule K-1 Box 15', sortOrder: 21 }, + { boxNumber: '16', label: 'Foreign transactions', description: 'IRS Schedule K-1 Box 16', sortOrder: 22 }, + { boxNumber: '17', label: 'Alternative minimum tax (AMT) items', description: 'IRS Schedule K-1 Box 17', sortOrder: 23 }, + { boxNumber: '18', label: 'Tax-exempt income and nondeductible expenses', description: 'IRS Schedule K-1 Box 18', sortOrder: 24 }, + { boxNumber: '19a', label: 'Distributions — Cash and marketable securities', description: 'IRS Schedule K-1 Box 19a', sortOrder: 25 }, + { boxNumber: '19b', label: 'Distributions — Other property', description: 'IRS Schedule K-1 Box 19b', sortOrder: 26 }, + { boxNumber: '20', label: 'Other information', description: 'IRS Schedule K-1 Box 20', sortOrder: 27 }, + { boxNumber: '21', label: 'Foreign taxes paid or accrued', description: 'IRS Schedule K-1 Box 21', sortOrder: 28 } +]; + +/** Default aggregation rules */ +const DEFAULT_AGGREGATION_RULES: Array<{ + name: string; + operation: string; + sourceCells: string[]; + sortOrder: number; +}> = [ + { + name: 'Total Ordinary Income', + operation: 'SUM', + sourceCells: ['1'], + sortOrder: 1 + }, + { + name: 'Total Capital Gains', + operation: 'SUM', + sourceCells: ['8', '9a', '9b', '9c', '10'], + sortOrder: 2 + }, + { + name: 'Total Deductions', + operation: 'SUM', + sourceCells: ['12', '13'], + sortOrder: 3 + } +]; + +@Injectable() +export class CellMappingService implements OnModuleInit { + public constructor(private readonly prismaService: PrismaService) {} + + public async onModuleInit() { + await this.seedDefaultMappings(); + await this.seedDefaultAggregationRules(); + } + + /** + * Seed default IRS cell mappings (partnershipId = null) if they don't exist + */ + public async seedDefaultMappings() { + const existingCount = await this.prismaService.cellMapping.count({ + where: { partnershipId: null } + }); + + if (existingCount > 0) { + return; + } + + await this.prismaService.cellMapping.createMany({ + data: IRS_DEFAULT_MAPPINGS.map((mapping) => ({ + ...mapping, + partnershipId: null, + isCustom: false + })) + }); + } + + /** + * Seed default aggregation rules (partnershipId = null) if they don't exist + */ + public async seedDefaultAggregationRules() { + const existingCount = await this.prismaService.cellAggregationRule.count({ + where: { partnershipId: null } + }); + + if (existingCount > 0) { + return; + } + + await this.prismaService.cellAggregationRule.createMany({ + data: DEFAULT_AGGREGATION_RULES.map((rule) => ({ + ...rule, + partnershipId: null + })) + }); + } + + /** + * Get cell mappings for a partnership (with global defaults for unmapped boxes) + */ + public async getMappings(partnershipId?: string) { + if (!partnershipId) { + return this.prismaService.cellMapping.findMany({ + where: { partnershipId: null }, + orderBy: { sortOrder: 'asc' } + }); + } + + // Get partnership-specific mappings + const partnershipMappings = await this.prismaService.cellMapping.findMany({ + where: { partnershipId }, + orderBy: { sortOrder: 'asc' } + }); + + // Get global defaults for any boxes not overridden + const globalMappings = await this.prismaService.cellMapping.findMany({ + where: { partnershipId: null }, + orderBy: { sortOrder: 'asc' } + }); + + const partnershipBoxNumbers = new Set( + partnershipMappings.map((m) => m.boxNumber) + ); + + const mergedMappings = [ + ...partnershipMappings, + ...globalMappings.filter((g) => !partnershipBoxNumbers.has(g.boxNumber)) + ]; + + return mergedMappings.sort((a, b) => a.sortOrder - b.sortOrder); + } + + /** + * Get aggregation rules for a partnership (with global defaults) + */ + public async getAggregationRules(partnershipId?: string) { + if (!partnershipId) { + return this.prismaService.cellAggregationRule.findMany({ + where: { partnershipId: null }, + orderBy: { sortOrder: 'asc' } + }); + } + + const partnershipRules = + await this.prismaService.cellAggregationRule.findMany({ + where: { partnershipId }, + orderBy: { sortOrder: 'asc' } + }); + + if (partnershipRules.length > 0) { + return partnershipRules; + } + + // Fall back to global defaults + return this.prismaService.cellAggregationRule.findMany({ + where: { partnershipId: null }, + orderBy: { sortOrder: 'asc' } + }); + } +} diff --git a/apps/api/src/app/k1-import/extractors/k1-extractor.interface.ts b/apps/api/src/app/k1-import/extractors/k1-extractor.interface.ts new file mode 100644 index 000000000..6e6d7fe05 --- /dev/null +++ b/apps/api/src/app/k1-import/extractors/k1-extractor.interface.ts @@ -0,0 +1,22 @@ +import type { K1ExtractionResult } from '@ghostfolio/common/interfaces'; + +/** + * Interface for K-1 PDF data extractors. + * Each extractor implements a different extraction strategy + * (pdf-parse for digital PDFs, Azure DI for scanned, tesseract as fallback). + */ +export interface K1Extractor { + /** + * Extract structured K-1 data from a PDF buffer. + * @param buffer - The PDF file content as a Buffer + * @param fileName - Original filename of the uploaded PDF + * @returns Extracted K-1 fields with confidence scores + */ + extract(buffer: Buffer, fileName: string): Promise; + + /** + * Check if this extractor is available/configured. + * For example, Azure extractor requires API keys to be configured. + */ + isAvailable(): boolean; +} diff --git a/apps/api/src/app/k1-import/k1-import.controller.ts b/apps/api/src/app/k1-import/k1-import.controller.ts new file mode 100644 index 000000000..93094bedf --- /dev/null +++ b/apps/api/src/app/k1-import/k1-import.controller.ts @@ -0,0 +1,31 @@ +import { HasPermission } from '@ghostfolio/api/decorators/has-permission.decorator'; +import { HasPermissionGuard } from '@ghostfolio/api/guards/has-permission.guard'; +import { permissions } from '@ghostfolio/common/permissions'; +import type { RequestWithUser } from '@ghostfolio/common/types'; + +import { + Body, + Controller, + Get, + Inject, + Param, + Post, + Put, + Query, + UploadedFile, + UseGuards, + UseInterceptors +} from '@nestjs/common'; +import { REQUEST } from '@nestjs/core'; +import { AuthGuard } from '@nestjs/passport'; +import { FileInterceptor } from '@nestjs/platform-express'; + +import { K1ImportService } from './k1-import.service'; + +@Controller('k1-import') +export class K1ImportController { + public constructor( + private readonly k1ImportService: K1ImportService, + @Inject(REQUEST) private readonly request: RequestWithUser + ) {} +} diff --git a/apps/api/src/app/k1-import/k1-import.module.ts b/apps/api/src/app/k1-import/k1-import.module.ts new file mode 100644 index 000000000..2d64efd99 --- /dev/null +++ b/apps/api/src/app/k1-import/k1-import.module.ts @@ -0,0 +1,32 @@ +import { PrismaModule } from '@ghostfolio/api/services/prisma/prisma.module'; + +import { Module } from '@nestjs/common'; + +import { CellMappingModule } from '../cell-mapping/cell-mapping.module'; +import { UploadModule } from '../upload/upload.module'; +import { K1ImportController } from './k1-import.controller'; +import { K1ImportService } from './k1-import.service'; +import { K1AggregationService } from './k1-aggregation.service'; +import { K1AllocationService } from './k1-allocation.service'; +import { K1ConfidenceService } from './k1-confidence.service'; +import { K1FieldMapperService } from './k1-field-mapper.service'; +import { AzureExtractor } from './extractors/azure-extractor'; +import { PdfParseExtractor } from './extractors/pdf-parse-extractor'; +import { TesseractExtractor } from './extractors/tesseract-extractor'; + +@Module({ + controllers: [K1ImportController], + exports: [K1ImportService], + imports: [CellMappingModule, PrismaModule, UploadModule], + providers: [ + AzureExtractor, + K1AggregationService, + K1AllocationService, + K1ConfidenceService, + K1FieldMapperService, + K1ImportService, + PdfParseExtractor, + TesseractExtractor + ] +}) +export class K1ImportModule {} diff --git a/apps/api/src/app/k1-import/k1-import.service.ts b/apps/api/src/app/k1-import/k1-import.service.ts new file mode 100644 index 000000000..0f61bd90d --- /dev/null +++ b/apps/api/src/app/k1-import/k1-import.service.ts @@ -0,0 +1,30 @@ +import { PrismaService } from '@ghostfolio/api/services/prisma/prisma.service'; +import { UploadService } from '../upload/upload.service'; +import { CellMappingService } from '../cell-mapping/cell-mapping.service'; +import { K1FieldMapperService } from './k1-field-mapper.service'; +import { K1ConfidenceService } from './k1-confidence.service'; +import { K1AllocationService } from './k1-allocation.service'; +import { K1AggregationService } from './k1-aggregation.service'; +import { PdfParseExtractor } from './extractors/pdf-parse-extractor'; +import { AzureExtractor } from './extractors/azure-extractor'; +import { TesseractExtractor } from './extractors/tesseract-extractor'; + +import { HttpException, Injectable } from '@nestjs/common'; +import { K1ImportStatus } from '@prisma/client'; +import { StatusCodes, getReasonPhrase } from 'http-status-codes'; + +@Injectable() +export class K1ImportService { + public constructor( + private readonly prismaService: PrismaService, + private readonly uploadService: UploadService, + private readonly cellMappingService: CellMappingService, + private readonly fieldMapperService: K1FieldMapperService, + private readonly confidenceService: K1ConfidenceService, + private readonly allocationService: K1AllocationService, + private readonly aggregationService: K1AggregationService, + private readonly pdfParseExtractor: PdfParseExtractor, + private readonly azureExtractor: AzureExtractor, + private readonly tesseractExtractor: TesseractExtractor + ) {} +} diff --git a/apps/client/src/app/app.routes.ts b/apps/client/src/app/app.routes.ts index e70ff9477..07aa1b3f4 100644 --- a/apps/client/src/app/app.routes.ts +++ b/apps/client/src/app/app.routes.ts @@ -176,6 +176,13 @@ export const routes: Routes = [ (m) => m.routes ) }, + { + path: 'cell-mapping', + loadChildren: () => + import('./pages/cell-mapping/cell-mapping-page.routes').then( + (m) => m.routes + ) + }, { path: 'k-documents', loadChildren: () => @@ -183,6 +190,13 @@ export const routes: Routes = [ (m) => m.routes ) }, + { + path: 'k1-import', + loadChildren: () => + import('./pages/k1-import/k1-import-page.routes').then( + (m) => m.routes + ) + }, { path: 'reports', loadChildren: () => diff --git a/apps/client/src/app/pages/cell-mapping/cell-mapping-page.routes.ts b/apps/client/src/app/pages/cell-mapping/cell-mapping-page.routes.ts new file mode 100644 index 000000000..d4ed6151c --- /dev/null +++ b/apps/client/src/app/pages/cell-mapping/cell-mapping-page.routes.ts @@ -0,0 +1,15 @@ +import { AuthGuard } from '@ghostfolio/client/core/auth.guard'; + +import { Routes } from '@angular/router'; + +export const routes: Routes = [ + { + canActivate: [AuthGuard], + loadComponent: () => + import('./cell-mapping-page.component').then( + (c) => c.CellMappingPageComponent + ), + path: '', + title: 'Cell Mapping' + } +]; diff --git a/apps/client/src/app/pages/k1-import/k1-import-page.routes.ts b/apps/client/src/app/pages/k1-import/k1-import-page.routes.ts new file mode 100644 index 000000000..99189b03d --- /dev/null +++ b/apps/client/src/app/pages/k1-import/k1-import-page.routes.ts @@ -0,0 +1,15 @@ +import { AuthGuard } from '@ghostfolio/client/core/auth.guard'; + +import { Routes } from '@angular/router'; + +export const routes: Routes = [ + { + canActivate: [AuthGuard], + loadComponent: () => + import('./k1-import-page.component').then( + (c) => c.K1ImportPageComponent + ), + path: '', + title: 'K-1 Import' + } +]; diff --git a/apps/client/src/app/services/k1-import-data.service.ts b/apps/client/src/app/services/k1-import-data.service.ts new file mode 100644 index 000000000..4aa831f1e --- /dev/null +++ b/apps/client/src/app/services/k1-import-data.service.ts @@ -0,0 +1,201 @@ +import type { + K1ExtractionResult, + K1ImportSessionSummary, + K1AggregationResult +} from '@ghostfolio/common/interfaces'; +import type { + ConfirmK1ImportDto, + VerifyK1ImportDto +} from '@ghostfolio/common/dtos'; + +import { HttpClient, HttpParams } from '@angular/common/http'; +import { Injectable } from '@angular/core'; +import { Observable } from 'rxjs'; + +@Injectable({ + providedIn: 'root' +}) +export class K1ImportDataService { + public constructor(private http: HttpClient) {} + + // ── K1 Import Endpoints ────────────────────────────────────────── + + /** + * Upload a K-1 PDF and initiate extraction. + * POST /api/v1/k1-import/upload + */ + public uploadK1(formData: FormData): Observable { + return this.http.post('/api/v1/k1-import/upload', formData); + } + + /** + * Get the current state of an import session. + * GET /api/v1/k1-import/:id + */ + public fetchImportSession(sessionId: string): Observable { + return this.http.get(`/api/v1/k1-import/${sessionId}`); + } + + /** + * Submit user-verified extraction data. + * PUT /api/v1/k1-import/:id/verify + */ + public verifyImportSession( + sessionId: string, + data: VerifyK1ImportDto + ): Observable { + return this.http.put(`/api/v1/k1-import/${sessionId}/verify`, data); + } + + /** + * Confirm verified data and trigger auto-creation of model objects. + * POST /api/v1/k1-import/:id/confirm + */ + public confirmImportSession( + sessionId: string, + data: ConfirmK1ImportDto + ): Observable { + return this.http.post(`/api/v1/k1-import/${sessionId}/confirm`, data); + } + + /** + * Cancel an import session. + * POST /api/v1/k1-import/:id/cancel + */ + public cancelImportSession(sessionId: string): Observable { + return this.http.post(`/api/v1/k1-import/${sessionId}/cancel`, {}); + } + + /** + * List import sessions for a partnership. + * GET /api/v1/k1-import/history + */ + public fetchImportHistory(params: { + partnershipId: string; + taxYear?: number; + }): Observable { + let httpParams = new HttpParams().set( + 'partnershipId', + params.partnershipId + ); + + if (params.taxYear) { + httpParams = httpParams.set('taxYear', params.taxYear.toString()); + } + + return this.http.get( + '/api/v1/k1-import/history', + { params: httpParams } + ); + } + + /** + * Re-run extraction on a previously uploaded PDF. + * POST /api/v1/k1-import/:id/reprocess + */ + public reprocessImportSession(sessionId: string): Observable { + return this.http.post(`/api/v1/k1-import/${sessionId}/reprocess`, {}); + } + + // ── Cell Mapping Endpoints ─────────────────────────────────────── + + /** + * Get cell mappings for a partnership (with global defaults). + * GET /api/v1/cell-mapping + */ + public fetchCellMappings(partnershipId?: string): Observable { + let httpParams = new HttpParams(); + + if (partnershipId) { + httpParams = httpParams.set('partnershipId', partnershipId); + } + + return this.http.get('/api/v1/cell-mapping', { + params: httpParams + }); + } + + /** + * Update or create cell mappings for a partnership. + * PUT /api/v1/cell-mapping + */ + public updateCellMappings(data: { + partnershipId: string; + mappings: Array<{ + boxNumber: string; + label: string; + description?: string; + isCustom: boolean; + }>; + }): Observable { + return this.http.put('/api/v1/cell-mapping', data); + } + + /** + * Reset a partnership's cell mappings to IRS defaults. + * DELETE /api/v1/cell-mapping/reset + */ + public resetCellMappings(partnershipId: string): Observable { + const httpParams = new HttpParams().set('partnershipId', partnershipId); + + return this.http.delete('/api/v1/cell-mapping/reset', { + params: httpParams + }); + } + + // ── Aggregation Rule Endpoints ─────────────────────────────────── + + /** + * Get aggregation rules for a partnership. + * GET /api/v1/cell-mapping/aggregation-rules + */ + public fetchAggregationRules(partnershipId?: string): Observable { + let httpParams = new HttpParams(); + + if (partnershipId) { + httpParams = httpParams.set('partnershipId', partnershipId); + } + + return this.http.get('/api/v1/cell-mapping/aggregation-rules', { + params: httpParams + }); + } + + /** + * Create or update aggregation rules for a partnership. + * PUT /api/v1/cell-mapping/aggregation-rules + */ + public updateAggregationRules(data: { + partnershipId: string; + rules: Array<{ + name: string; + operation: string; + sourceCells: string[]; + }>; + }): Observable { + return this.http.put( + '/api/v1/cell-mapping/aggregation-rules', + data + ); + } + + /** + * Compute aggregation values for a specific KDocument. + * GET /api/v1/cell-mapping/aggregation-rules/compute + */ + public computeAggregations(params: { + kDocumentId: string; + partnershipId?: string; + }): Observable { + let httpParams = new HttpParams().set('kDocumentId', params.kDocumentId); + + if (params.partnershipId) { + httpParams = httpParams.set('partnershipId', params.partnershipId); + } + + return this.http.get( + '/api/v1/cell-mapping/aggregation-rules/compute', + { params: httpParams } + ); + } +} diff --git a/specs/004-k1-scan-import/tasks.md b/specs/004-k1-scan-import/tasks.md index e56c40824..446d295d6 100644 --- a/specs/004-k1-scan-import/tasks.md +++ b/specs/004-k1-scan-import/tasks.md @@ -42,13 +42,13 @@ **⚠️ CRITICAL**: No user story work can begin until this phase is complete -- [ ] T007 Create K1Import NestJS module skeleton (module, empty controller, empty service) in apps/api/src/app/k1-import/k1-import.module.ts -- [ ] T008 [P] Create CellMapping NestJS module skeleton (module, empty controller, empty service) in apps/api/src/app/cell-mapping/cell-mapping.module.ts -- [ ] T009 Register K1ImportModule and CellMappingModule in apps/api/src/app/app.module.ts -- [ ] T010 Create K1 extractor interface (K1Extractor with extract method returning K1ExtractionResult) in apps/api/src/app/k1-import/extractors/k1-extractor.interface.ts -- [ ] T011 Implement cell mapping seed logic (28 IRS default rows + 3 default aggregation rules) in apps/api/src/app/cell-mapping/cell-mapping.service.ts -- [ ] T012 [P] Create K1 import frontend data service (HTTP client for all k1-import and cell-mapping endpoints) in apps/client/src/app/services/k1-import-data.service.ts -- [ ] T013 [P] Create frontend route configurations for K1 import pages in apps/client/src/app/pages/k1-import/k1-import-page.routes.ts and cell mapping pages in apps/client/src/app/pages/cell-mapping/cell-mapping-page.routes.ts +- [X] T007 Create K1Import NestJS module skeleton (module, empty controller, empty service) in apps/api/src/app/k1-import/k1-import.module.ts +- [X] T008 [P] Create CellMapping NestJS module skeleton (module, empty controller, empty service) in apps/api/src/app/cell-mapping/cell-mapping.module.ts +- [X] T009 Register K1ImportModule and CellMappingModule in apps/api/src/app/app.module.ts +- [X] T010 Create K1 extractor interface (K1Extractor with extract method returning K1ExtractionResult) in apps/api/src/app/k1-import/extractors/k1-extractor.interface.ts +- [X] T011 Implement cell mapping seed logic (28 IRS default rows + 3 default aggregation rules) in apps/api/src/app/cell-mapping/cell-mapping.service.ts +- [X] T012 [P] Create K1 import frontend data service (HTTP client for all k1-import and cell-mapping endpoints) in apps/client/src/app/services/k1-import-data.service.ts +- [X] T013 [P] Create frontend route configurations for K1 import pages in apps/client/src/app/pages/k1-import/k1-import-page.routes.ts and cell mapping pages in apps/client/src/app/pages/cell-mapping/cell-mapping-page.routes.ts **Checkpoint**: Foundation ready — user story implementation can now begin in parallel