611 lines
16 KiB
Markdown
611 lines
16 KiB
Markdown
# 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
|
|
|
|
```typescript
|
|
@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
|
|
|
|
```typescript
|
|
@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
|
|
|
|
```typescript
|
|
@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
|
|
|
|
```typescript
|
|
@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
|
|
|
|
```typescript
|
|
@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
|
|
|
|
```typescript
|
|
@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
|
|
|
|
```typescript
|
|
@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
|
|
|
|
```typescript
|
|
@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
|
|
|
|
```typescript
|
|
@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
|
|
|
|
```typescript
|
|
@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
|
|
|
|
```typescript
|
|
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
|
|
|
|
```typescript
|
|
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
|
|
|
|
```typescript
|
|
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
|
|
|
|
```typescript
|
|
@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
|
|
|
|
```typescript
|
|
@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
|
|
|
|
- [DDL-SPEC-finance.md](../../04-modelado/database-design/schemas/DDL-SPEC-finance.md)
|
|
- [FINANCE-CONTEXT.md](../../04-modelado/domain-models/FINANCE-CONTEXT.md)
|
|
- [EPIC-MAE-014](../../08-epicas/EPIC-MAE-014-finanzas.md)
|
|
|
|
---
|
|
|
|
*Ultima actualizacion: 2025-12-05*
|