erp-construccion/docs/05-backend-specs/modules/SPEC-finance.md

16 KiB

Backend Specification: Finance Module

Version: 1.0.0 Fecha: 2025-12-05 Modulos: MAE-014 (Finanzas y Controlling)


Resumen

Metrica Valor
Controllers 6
Services 8
Entities 15
Endpoints 50+
Tests Requeridos 70+

Estructura del Modulo

modules/finance/
+-- finance.module.ts
+-- controllers/
|   +-- accounting.controller.ts
|   +-- accounts-payable.controller.ts
|   +-- accounts-receivable.controller.ts
|   +-- bank.controller.ts
|   +-- cash-flow.controller.ts
|   +-- reports.controller.ts
+-- services/
|   +-- accounting.service.ts
|   +-- chart-of-accounts.service.ts
|   +-- accounts-payable.service.ts
|   +-- accounts-receivable.service.ts
|   +-- bank.service.ts
|   +-- reconciliation.service.ts
|   +-- cash-flow.service.ts
|   +-- financial-reports.service.ts
+-- entities/
|   +-- chart-of-accounts.entity.ts
|   +-- cost-center.entity.ts
|   +-- accounting-entry.entity.ts
|   +-- accounting-entry-line.entity.ts
|   +-- accounts-payable.entity.ts
|   +-- ap-payment.entity.ts
|   +-- accounts-receivable.entity.ts
|   +-- ar-collection.entity.ts
|   +-- bank-account.entity.ts
|   +-- bank-movement.entity.ts
|   +-- bank-reconciliation.entity.ts
|   +-- cash-flow-projection.entity.ts
|   +-- cash-flow-item.entity.ts
+-- dto/
+-- integrations/
|   +-- sap.integration.ts
|   +-- contpaqi.integration.ts
+-- events/

Controllers

1. AccountingController

@Controller('api/v1/finance/accounting')
@ApiTags('finance-accounting')
export class AccountingController {

  // Chart of Accounts
  @Get('accounts')
  async getAccounts(@Query() query: AccountQueryDto): Promise<AccountDto[]>;

  @Get('accounts/:id')
  async getAccount(@Param('id') id: UUID): Promise<AccountDto>;

  @Post('accounts')
  async createAccount(@Body() dto: CreateAccountDto): Promise<AccountDto>;

  @Put('accounts/:id')
  async updateAccount(@Param('id') id: UUID, @Body() dto: UpdateAccountDto): Promise<AccountDto>;

  @Get('accounts/tree')
  async getAccountTree(): Promise<AccountTreeDto[]>;

  // Cost Centers
  @Get('cost-centers')
  async getCostCenters(): Promise<CostCenterDto[]>;

  @Post('cost-centers')
  async createCostCenter(@Body() dto: CreateCostCenterDto): Promise<CostCenterDto>;

  // Accounting Entries
  @Get('entries')
  async getEntries(@Query() query: EntryQueryDto): Promise<PaginatedResponse<EntryDto>>;

  @Get('entries/:id')
  async getEntry(@Param('id') id: UUID): Promise<EntryDetailDto>;

  @Post('entries')
  async createEntry(@Body() dto: CreateEntryDto): Promise<EntryDto>;

  @Put('entries/:id')
  async updateEntry(@Param('id') id: UUID, @Body() dto: UpdateEntryDto): Promise<EntryDto>;

  @Post('entries/:id/post')
  async postEntry(@Param('id') id: UUID): Promise<EntryDto>;

  @Post('entries/:id/reverse')
  async reverseEntry(@Param('id') id: UUID): Promise<EntryDto>;

  // Period Management
  @Post('periods/close')
  async closePeriod(@Body() dto: ClosePeriodDto): Promise<PeriodDto>;
}

2. AccountsPayableController

@Controller('api/v1/finance/ap')
@ApiTags('finance-ap')
export class AccountsPayableController {

  @Get()
  async findAll(@Query() query: APQueryDto): Promise<PaginatedResponse<APDto>>;

  @Get(':id')
  async findOne(@Param('id') id: UUID): Promise<APDetailDto>;

  @Post()
  async create(@Body() dto: CreateAPDto): Promise<APDto>;

  @Put(':id')
  async update(@Param('id') id: UUID, @Body() dto: UpdateAPDto): Promise<APDto>;

  @Get('aging')
  async getAging(@Query() query: AgingQueryDto): Promise<AgingReportDto>;

  @Get('by-supplier/:supplierId')
  async getBySupplier(@Param('supplierId') supplierId: UUID): Promise<APDto[]>;

  @Get('by-project/:projectId')
  async getByProject(@Param('projectId') projectId: UUID): Promise<APDto[]>;

  // Payments
  @Post(':id/payments')
  async addPayment(@Param('id') id: UUID, @Body() dto: CreatePaymentDto): Promise<PaymentDto>;

  @Get(':id/payments')
  async getPayments(@Param('id') id: UUID): Promise<PaymentDto[]>;

  @Delete('payments/:paymentId')
  async cancelPayment(@Param('paymentId') paymentId: UUID): Promise<void>;

  // Dashboard
  @Get('dashboard')
  async getDashboard(): Promise<APDashboardDto>;
}

3. AccountsReceivableController

@Controller('api/v1/finance/ar')
@ApiTags('finance-ar')
export class AccountsReceivableController {

  @Get()
  async findAll(@Query() query: ARQueryDto): Promise<PaginatedResponse<ARDto>>;

  @Get(':id')
  async findOne(@Param('id') id: UUID): Promise<ARDetailDto>;

  @Post()
  async create(@Body() dto: CreateARDto): Promise<ARDto>;

  @Get('aging')
  async getAging(@Query() query: AgingQueryDto): Promise<AgingReportDto>;

  @Get('by-customer/:customerId')
  async getByCustomer(@Param('customerId') customerId: UUID): Promise<ARDto[]>;

  @Get('by-project/:projectId')
  async getByProject(@Param('projectId') projectId: UUID): Promise<ARDto[]>;

  // Collections
  @Post(':id/collections')
  async addCollection(@Param('id') id: UUID, @Body() dto: CreateCollectionDto): Promise<CollectionDto>;

  @Get(':id/collections')
  async getCollections(@Param('id') id: UUID): Promise<CollectionDto[]>;

  // Dashboard
  @Get('dashboard')
  async getDashboard(): Promise<ARDashboardDto>;
}

4. BankController

@Controller('api/v1/finance/bank')
@ApiTags('finance-bank')
export class BankController {

  // Bank Accounts
  @Get('accounts')
  async getBankAccounts(): Promise<BankAccountDto[]>;

  @Get('accounts/:id')
  async getBankAccount(@Param('id') id: UUID): Promise<BankAccountDto>;

  @Post('accounts')
  async createBankAccount(@Body() dto: CreateBankAccountDto): Promise<BankAccountDto>;

  // Movements
  @Get('accounts/:id/movements')
  async getMovements(
    @Param('id') id: UUID,
    @Query() query: MovementQueryDto
  ): Promise<PaginatedResponse<MovementDto>>;

  @Post('accounts/:id/import')
  @UseInterceptors(FileInterceptor('file'))
  async importMovements(
    @Param('id') id: UUID,
    @UploadedFile() file: Express.Multer.File
  ): Promise<ImportResultDto>;

  // Reconciliation
  @Get('accounts/:id/reconciliation')
  async getReconciliation(@Param('id') id: UUID, @Query() query: ReconciliationQueryDto): Promise<ReconciliationDto>;

  @Post('accounts/:id/reconcile')
  async startReconciliation(@Param('id') id: UUID, @Body() dto: StartReconciliationDto): Promise<ReconciliationDto>;

  @Post('reconciliation/:id/match')
  async matchMovement(@Param('id') id: UUID, @Body() dto: MatchMovementDto): Promise<MovementDto>;

  @Post('reconciliation/:id/complete')
  async completeReconciliation(@Param('id') id: UUID): Promise<ReconciliationDto>;
}

5. CashFlowController

@Controller('api/v1/finance/cash-flow')
@ApiTags('finance-cash-flow')
export class CashFlowController {

  @Get('projection/:projectId')
  async getProjection(
    @Param('projectId') projectId: UUID,
    @Query() query: CashFlowQueryDto
  ): Promise<CashFlowProjectionDto>;

  @Get('comparison/:projectId')
  async getComparison(
    @Param('projectId') projectId: UUID,
    @Query() query: CashFlowQueryDto
  ): Promise<CashFlowComparisonDto>;

  @Post('projection')
  async createProjection(@Body() dto: CreateProjectionDto): Promise<CashFlowProjectionDto>;

  @Put('projection/:id')
  async updateProjection(@Param('id') id: UUID, @Body() dto: UpdateProjectionDto): Promise<CashFlowProjectionDto>;

  @Post('generate/:projectId')
  async generateProjection(@Param('projectId') projectId: UUID): Promise<CashFlowProjectionDto>;
}

6. ReportsController

@Controller('api/v1/finance/reports')
@ApiTags('finance-reports')
export class ReportsController {

  @Get('balance/:projectId')
  async getBalance(@Param('projectId') projectId: UUID, @Query() query: ReportQueryDto): Promise<BalanceSheetDto>;

  @Get('income-statement/:projectId')
  async getIncomeStatement(@Param('projectId') projectId: UUID, @Query() query: ReportQueryDto): Promise<IncomeStatementDto>;

  @Get('trial-balance')
  async getTrialBalance(@Query() query: ReportQueryDto): Promise<TrialBalanceDto>;

  @Get('ledger/:accountId')
  async getLedger(@Param('accountId') accountId: UUID, @Query() query: ReportQueryDto): Promise<LedgerDto>;

  @Get('dashboard')
  async getDashboard(@Query() query: DashboardQueryDto): Promise<FinanceDashboardDto>;

  // Export
  @Get('export/entries')
  async exportEntries(@Query() query: ExportQueryDto): Promise<StreamableFile>;

  @Post('export/contpaqi')
  async exportToContpaqi(@Body() dto: ExportContpaqiDto): Promise<StreamableFile>;

  @Post('export/sap')
  async exportToSap(@Body() dto: ExportSapDto): Promise<{ status: string; batchId: string }>;
}

Services

AccountingService

@Injectable()
export class AccountingService {
  async createEntry(tenantId: UUID, userId: UUID, dto: CreateEntryDto): Promise<EntryDto>;
  async postEntry(tenantId: UUID, entryId: UUID, userId: UUID): Promise<EntryDto>;
  async reverseEntry(tenantId: UUID, entryId: UUID, userId: UUID): Promise<EntryDto>;
  async validateEntry(entry: AccountingEntry): Promise<boolean>;
  async generateEntryFromPurchase(purchaseId: UUID): Promise<EntryDto>;
  async generateEntryFromEstimation(estimationId: UUID): Promise<EntryDto>;
}

AccountsPayableService

@Injectable()
export class AccountsPayableService {
  async create(tenantId: UUID, userId: UUID, dto: CreateAPDto): Promise<APDto>;
  async addPayment(apId: UUID, userId: UUID, dto: CreatePaymentDto): Promise<PaymentDto>;
  async getAging(tenantId: UUID, query: AgingQueryDto): Promise<AgingReportDto>;
  async getOverdue(tenantId: UUID): Promise<APDto[]>;
  async calculateBalance(apId: UUID): Promise<number>;
}

CashFlowService

@Injectable()
export class CashFlowService {
  async generateProjection(tenantId: UUID, projectId: UUID, periodType: PeriodType): Promise<CashFlowProjectionDto>;
  async getComparison(tenantId: UUID, projectId: UUID, query: CashFlowQueryDto): Promise<CashFlowComparisonDto>;
  async calculateVariance(projectionId: UUID): Promise<VarianceDto>;
}

FinancialReportsService

@Injectable()
export class FinancialReportsService {
  async generateBalanceSheet(tenantId: UUID, projectId: UUID, asOfDate: Date): Promise<BalanceSheetDto>;
  async generateIncomeStatement(tenantId: UUID, projectId: UUID, period: Period): Promise<IncomeStatementDto>;
  async generateTrialBalance(tenantId: UUID, period: Period): Promise<TrialBalanceDto>;
  async generateLedger(tenantId: UUID, accountId: UUID, period: Period): Promise<LedgerDto>;
}

DTOs

Accounting DTOs

export class CreateAccountDto {
  @IsString() @MaxLength(30)
  code: string;

  @IsString() @MaxLength(200)
  name: string;

  @IsEnum(AccountType)
  accountType: AccountType;

  @IsEnum(AccountNature)
  nature: AccountNature;

  @IsOptional() @IsUUID()
  parentId?: string;

  @IsOptional() @IsBoolean()
  costCenterRequired?: boolean;

  @IsOptional() @IsBoolean()
  projectRequired?: boolean;
}

export class CreateEntryDto {
  @IsEnum(EntryType)
  entryType: EntryType;

  @IsDateString()
  entryDate: string;

  @IsString()
  description: string;

  @IsOptional() @IsString()
  reference?: string;

  @IsOptional() @IsUUID()
  projectId?: string;

  @IsOptional() @IsUUID()
  costCenterId?: string;

  @IsArray()
  @ValidateNested({ each: true })
  @Type(() => CreateEntryLineDto)
  lines: CreateEntryLineDto[];
}

export class CreateEntryLineDto {
  @IsUUID()
  accountId: string;

  @IsOptional() @IsString()
  description?: string;

  @IsNumber()
  debitAmount: number;

  @IsNumber()
  creditAmount: number;

  @IsOptional() @IsUUID()
  costCenterId?: string;

  @IsOptional() @IsUUID()
  projectId?: string;
}

AP/AR DTOs

export class CreateAPDto {
  @IsString() @MaxLength(50)
  documentNumber: string;

  @IsUUID()
  supplierId: string;

  @IsNumber()
  subtotal: number;

  @IsNumber()
  taxAmount: number;

  @IsNumber()
  totalAmount: number;

  @IsDateString()
  documentDate: string;

  @IsDateString()
  dueDate: string;

  @IsOptional() @IsUUID()
  projectId?: string;

  @IsOptional() @IsUUID()
  purchaseOrderId?: string;
}

export class CreatePaymentDto {
  @IsDateString()
  paymentDate: string;

  @IsNumber()
  amount: number;

  @IsEnum(PaymentMethod)
  paymentMethod: PaymentMethod;

  @IsOptional() @IsUUID()
  bankAccountId?: string;

  @IsOptional() @IsString()
  reference?: string;
}

export class AgingReportDto {
  suppliers: {
    supplierId: string;
    supplierName: string;
    current: number;
    days1_30: number;
    days31_60: number;
    days61_90: number;
    daysOver90: number;
    total: number;
  }[];
  summary: {
    totalCurrent: number;
    total1_30: number;
    total31_60: number;
    total61_90: number;
    totalOver90: number;
    grandTotal: number;
  };
}

Cash Flow DTOs

export class CashFlowProjectionDto {
  projectId: string;
  periodType: PeriodType;
  periods: {
    periodDate: string;
    projectedIncome: number;
    projectedExpenses: number;
    actualIncome: number;
    actualExpenses: number;
    openingBalance: number;
    projectedClosing: number;
    actualClosing?: number;
  }[];
  summary: {
    totalProjectedIncome: number;
    totalProjectedExpenses: number;
    netProjectedFlow: number;
    totalActualIncome: number;
    totalActualExpenses: number;
    netActualFlow: number;
    variance: number;
    variancePercentage: number;
  };
}

export class CashFlowComparisonDto {
  projectId: string;
  period: { start: string; end: string };
  projectedVsActual: {
    category: string;
    projected: number;
    actual: number;
    variance: number;
    variancePercentage: number;
  }[];
  insights: string[];
}

Integrations

CONTPAQi Integration

@Injectable()
export class ContpaqiIntegrationService {
  async exportEntries(tenantId: UUID, entries: AccountingEntry[]): Promise<Buffer> {
    const xml = this.generateXml(entries);
    return Buffer.from(xml);
  }

  private generateXml(entries: AccountingEntry[]): string {
    // Generate CONTPAQi compatible XML format
    return `<?xml version="1.0" encoding="UTF-8"?>
      <Polizas>
        ${entries.map(e => this.entryToXml(e)).join('\n')}
      </Polizas>`;
  }
}

SAP Integration

@Injectable()
export class SapIntegrationService {
  constructor(private readonly httpService: HttpService) {}

  async exportEntries(tenantId: UUID, entries: AccountingEntry[]): Promise<{ status: string; batchId: string }> {
    // Call SAP RFC/API
    const response = await this.httpService.post(
      `${this.sapEndpoint}/finance/entries`,
      this.transformToSapFormat(entries)
    ).toPromise();

    return { status: 'sent', batchId: response.data.batchId };
  }
}

Referencias


Ultima actualizacion: 2025-12-05