# ET-FIN-BACKEND: Servicios y API REST ## Identificacion | Campo | Valor | |-------|-------| | **ID** | ET-FIN-BACKEND | | **Modulo** | MGN-010 Financial | | **Version** | 1.0 | | **Estado** | En Diseno | | **Framework** | NestJS | | **Autor** | Requirements-Analyst | | **Fecha** | 2025-12-05 | --- ## Estructura de Archivos ``` apps/backend/src/modules/financial/ ├── financial.module.ts ├── controllers/ │ ├── charts.controller.ts │ ├── accounts.controller.ts │ ├── currencies.controller.ts │ ├── fiscal-years.controller.ts │ ├── fiscal-periods.controller.ts │ ├── journal.controller.ts │ └── cost-centers.controller.ts ├── services/ │ ├── charts.service.ts │ ├── accounts.service.ts │ ├── currencies.service.ts │ ├── exchange-rates.service.ts │ ├── fiscal-years.service.ts │ ├── fiscal-periods.service.ts │ ├── journal.service.ts │ └── cost-centers.service.ts ├── entities/ │ ├── account-type.entity.ts │ ├── chart-of-accounts.entity.ts │ ├── account.entity.ts │ ├── tenant-currency.entity.ts │ ├── exchange-rate.entity.ts │ ├── fiscal-year.entity.ts │ ├── fiscal-period.entity.ts │ ├── journal-entry.entity.ts │ ├── journal-line.entity.ts │ └── cost-center.entity.ts ├── dto/ │ ├── create-account.dto.ts │ ├── create-journal-entry.dto.ts │ ├── convert-currency.dto.ts │ └── close-period.dto.ts └── interfaces/ ├── account-balance.interface.ts └── trial-balance.interface.ts ``` --- ## Entidades ### Account Entity ```typescript @Entity('accounts', { schema: 'core_financial' }) @Tree('materialized-path') export class Account { @PrimaryGeneratedColumn('uuid') id: string; @Column({ name: 'chart_id', type: 'uuid' }) chartId: string; @Column({ name: 'tenant_id', type: 'uuid' }) tenantId: string; @Column({ name: 'parent_id', type: 'uuid', nullable: true }) parentId: string; @Column({ name: 'account_type_id', type: 'uuid' }) accountTypeId: string; @Column({ length: 20 }) code: string; @Column({ length: 255 }) name: string; @Column({ type: 'text', nullable: true }) description: string; @Column({ default: 1 }) level: number; @Column({ name: 'is_detail', default: true }) isDetail: boolean; @Column({ name: 'is_bank', default: false }) isBank: boolean; @Column({ name: 'is_cash', default: false }) isCash: boolean; @Column({ name: 'currency_code', length: 3, nullable: true }) currencyCode: string; @Column({ name: 'opening_balance', type: 'decimal', precision: 18, scale: 4, default: 0 }) openingBalance: number; @Column({ name: 'current_balance', type: 'decimal', precision: 18, scale: 4, default: 0 }) currentBalance: number; @Column({ name: 'is_active', default: true }) isActive: boolean; @CreateDateColumn({ name: 'created_at' }) createdAt: Date; @UpdateDateColumn({ name: 'updated_at' }) updatedAt: Date; @ManyToOne(() => ChartOfAccounts) @JoinColumn({ name: 'chart_id' }) chart: ChartOfAccounts; @ManyToOne(() => Account) @JoinColumn({ name: 'parent_id' }) parent: Account; @OneToMany(() => Account, a => a.parent) children: Account[]; @ManyToOne(() => AccountType) @JoinColumn({ name: 'account_type_id' }) accountType: AccountType; } ``` ### JournalEntry Entity ```typescript @Entity('journal_entries', { schema: 'core_financial' }) export class JournalEntry { @PrimaryGeneratedColumn('uuid') id: string; @Column({ name: 'tenant_id', type: 'uuid' }) tenantId: string; @Column({ name: 'fiscal_period_id', type: 'uuid' }) fiscalPeriodId: string; @Column({ name: 'entry_number', length: 20 }) entryNumber: string; @Column({ name: 'entry_date', type: 'date' }) entryDate: Date; @Column({ name: 'currency_code', length: 3 }) currencyCode: string; @Column({ name: 'exchange_rate', type: 'decimal', precision: 18, scale: 8, default: 1 }) exchangeRate: number; @Column({ length: 100, nullable: true }) reference: string; @Column({ type: 'text' }) description: string; @Column({ name: 'source_module', length: 50, nullable: true }) sourceModule: string; @Column({ name: 'source_document_id', type: 'uuid', nullable: true }) sourceDocumentId: string; @Column({ length: 20, default: 'draft' }) status: JournalStatus; @Column({ name: 'total_debit', type: 'decimal', precision: 18, scale: 4, default: 0 }) totalDebit: number; @Column({ name: 'total_credit', type: 'decimal', precision: 18, scale: 4, default: 0 }) totalCredit: number; @Column({ name: 'posted_at', type: 'timestamptz', nullable: true }) postedAt: Date; @Column({ name: 'posted_by', type: 'uuid', nullable: true }) postedBy: string; @Column({ name: 'reversed_by', type: 'uuid', nullable: true }) reversedBy: string; @Column({ name: 'created_by', type: 'uuid' }) createdBy: string; @CreateDateColumn({ name: 'created_at' }) createdAt: Date; @UpdateDateColumn({ name: 'updated_at' }) updatedAt: Date; @ManyToOne(() => FiscalPeriod) @JoinColumn({ name: 'fiscal_period_id' }) fiscalPeriod: FiscalPeriod; @OneToMany(() => JournalLine, l => l.journalEntry, { cascade: true }) lines: JournalLine[]; } export type JournalStatus = 'draft' | 'posted' | 'reversed'; ``` ### JournalLine Entity ```typescript @Entity('journal_lines', { schema: 'core_financial' }) export class JournalLine { @PrimaryGeneratedColumn('uuid') id: string; @Column({ name: 'journal_entry_id', type: 'uuid' }) journalEntryId: string; @Column({ name: 'line_number' }) lineNumber: number; @Column({ name: 'account_id', type: 'uuid' }) accountId: string; @Column({ name: 'cost_center_id', type: 'uuid', nullable: true }) costCenterId: string; @Column({ type: 'decimal', precision: 18, scale: 4, default: 0 }) debit: number; @Column({ type: 'decimal', precision: 18, scale: 4, default: 0 }) credit: number; @Column({ name: 'debit_base', type: 'decimal', precision: 18, scale: 4, default: 0 }) debitBase: number; @Column({ name: 'credit_base', type: 'decimal', precision: 18, scale: 4, default: 0 }) creditBase: number; @Column({ type: 'text', nullable: true }) description: string; @Column({ length: 100, nullable: true }) reference: string; @ManyToOne(() => JournalEntry, e => e.lines) @JoinColumn({ name: 'journal_entry_id' }) journalEntry: JournalEntry; @ManyToOne(() => Account) @JoinColumn({ name: 'account_id' }) account: Account; @ManyToOne(() => CostCenter) @JoinColumn({ name: 'cost_center_id' }) costCenter: CostCenter; } ``` --- ## Servicios ### AccountsService ```typescript @Injectable() export class AccountsService { constructor( @InjectRepository(Account) private readonly repo: Repository, @InjectRepository(AccountType) private readonly typeRepo: Repository, ) {} async findByChart(chartId: string): Promise { return this.repo.find({ where: { chartId }, relations: ['accountType', 'parent'], order: { code: 'ASC' }, }); } async findTreeByChart(chartId: string): Promise { const accounts = await this.findByChart(chartId); return this.buildTree(accounts); } async create(chartId: string, dto: CreateAccountDto): Promise { // Validate parent if (dto.parentId) { const parent = await this.repo.findOne({ where: { id: dto.parentId } }); if (!parent) { throw new NotFoundException('Parent account not found'); } if (parent.isDetail) { throw new BadRequestException('Cannot add child to detail account'); } } // Validate code uniqueness const existing = await this.repo.findOne({ where: { chartId, code: dto.code }, }); if (existing) { throw new ConflictException('Account code already exists'); } // Calculate level const level = dto.parentId ? (await this.repo.findOne({ where: { id: dto.parentId } })).level + 1 : 1; const account = this.repo.create({ ...dto, chartId, tenantId: dto.tenantId, level, }); return this.repo.save(account); } async update(id: string, dto: UpdateAccountDto): Promise { const account = await this.repo.findOneOrFail({ where: { id } }); // Cannot change code if has movements if (dto.code && dto.code !== account.code) { const hasMovements = await this.hasMovements(id); if (hasMovements) { throw new BadRequestException('Cannot change code of account with movements'); } } Object.assign(account, dto); return this.repo.save(account); } async getBalance(accountId: string, asOfDate?: Date): Promise { const account = await this.repo.findOne({ where: { id: accountId }, relations: ['accountType'], }); const qb = this.repo.manager .createQueryBuilder(JournalLine, 'jl') .select('COALESCE(SUM(jl.debit_base), 0)', 'debitTotal') .addSelect('COALESCE(SUM(jl.credit_base), 0)', 'creditTotal') .innerJoin('jl.journalEntry', 'je') .where('jl.account_id = :accountId', { accountId }) .andWhere('je.status = :status', { status: 'posted' }); if (asOfDate) { qb.andWhere('je.entry_date <= :asOfDate', { asOfDate }); } const result = await qb.getRawOne(); const debitTotal = parseFloat(result.debitTotal); const creditTotal = parseFloat(result.creditTotal); const balance = account.accountType.normalBalance === 'debit' ? account.openingBalance + debitTotal - creditTotal : account.openingBalance + creditTotal - debitTotal; return { accountId, openingBalance: account.openingBalance, debitTotal, creditTotal, balance, }; } async getTrialBalance( chartId: string, periodId: string ): Promise { const accounts = await this.repo.find({ where: { chartId, isDetail: true }, relations: ['accountType'], order: { code: 'ASC' }, }); const period = await this.repo.manager.findOne(FiscalPeriod, { where: { id: periodId }, }); const result: TrialBalanceItem[] = []; for (const account of accounts) { const balance = await this.getBalance(account.id, period.endDate); if (balance.debitTotal > 0 || balance.creditTotal > 0 || balance.balance !== 0) { result.push({ accountCode: account.code, accountName: account.name, openingBalance: account.openingBalance, debit: balance.debitTotal, credit: balance.creditTotal, balance: balance.balance, }); } } return result; } private buildTree(accounts: Account[]): Account[] { const map = new Map(); const roots: Account[] = []; accounts.forEach(a => map.set(a.id, { ...a, children: [] })); map.forEach(account => { if (account.parentId) { const parent = map.get(account.parentId); if (parent) { parent.children.push(account); } } else { roots.push(account); } }); return roots; } private async hasMovements(accountId: string): Promise { const count = await this.repo.manager.count(JournalLine, { where: { accountId }, }); return count > 0; } } ``` ### JournalService ```typescript @Injectable() export class JournalService { constructor( @InjectRepository(JournalEntry) private readonly entryRepo: Repository, @InjectRepository(JournalLine) private readonly lineRepo: Repository, private readonly periodsService: FiscalPeriodsService, private readonly currenciesService: CurrenciesService, private readonly accountsService: AccountsService, @InjectDataSource() private readonly dataSource: DataSource, ) {} async findAll( tenantId: string, query: QueryJournalDto ): Promise> { const qb = this.entryRepo.createQueryBuilder('je') .where('je.tenant_id = :tenantId', { tenantId }) .leftJoinAndSelect('je.lines', 'lines') .leftJoinAndSelect('lines.account', 'account'); if (query.periodId) { qb.andWhere('je.fiscal_period_id = :periodId', { periodId: query.periodId }); } if (query.status) { qb.andWhere('je.status = :status', { status: query.status }); } if (query.dateFrom) { qb.andWhere('je.entry_date >= :dateFrom', { dateFrom: query.dateFrom }); } if (query.dateTo) { qb.andWhere('je.entry_date <= :dateTo', { dateTo: query.dateTo }); } qb.orderBy('je.entry_date', 'DESC') .addOrderBy('je.entry_number', 'DESC'); return paginate(qb, query); } async findById(id: string): Promise { return this.entryRepo.findOne({ where: { id }, relations: ['lines', 'lines.account', 'lines.costCenter', 'fiscalPeriod'], }); } async create( tenantId: string, userId: string, dto: CreateJournalEntryDto ): Promise { // Validate period const period = await this.periodsService.findOpenForDate(tenantId, dto.entryDate); if (!period) { throw new BadRequestException('No open period for the entry date'); } // Get exchange rate const baseCurrency = await this.currenciesService.getBaseCurrency(tenantId); const exchangeRate = dto.currencyCode === baseCurrency.code ? 1 : await this.currenciesService.getRate(tenantId, dto.currencyCode, baseCurrency.code, dto.entryDate); // Generate entry number const entryNumber = await this.generateEntryNumber(tenantId); // Calculate totals and base amounts let totalDebit = 0; let totalCredit = 0; const lines = dto.lines.map((line, index) => { totalDebit += line.debit || 0; totalCredit += line.credit || 0; return { ...line, lineNumber: index + 1, debitBase: (line.debit || 0) * exchangeRate, creditBase: (line.credit || 0) * exchangeRate, }; }); const entry = this.entryRepo.create({ tenantId, fiscalPeriodId: period.id, entryNumber, entryDate: dto.entryDate, currencyCode: dto.currencyCode, exchangeRate, reference: dto.reference, description: dto.description, sourceModule: dto.sourceModule, sourceDocumentId: dto.sourceDocumentId, totalDebit, totalCredit, createdBy: userId, lines, }); return this.entryRepo.save(entry); } async post(id: string, userId: string): Promise { return this.dataSource.transaction(async manager => { const entry = await manager.findOne(JournalEntry, { where: { id }, relations: ['lines', 'lines.account', 'lines.account.accountType'], lock: { mode: 'pessimistic_write' }, }); if (!entry) { throw new NotFoundException('Journal entry not found'); } if (entry.status !== 'draft') { throw new BadRequestException('Entry is not in draft status'); } // Verify period is open const period = await manager.findOne(FiscalPeriod, { where: { id: entry.fiscalPeriodId }, }); if (period.status !== 'open') { throw new BadRequestException('Fiscal period is not open'); } // Verify balance if (Math.abs(entry.totalDebit - entry.totalCredit) > 0.001) { throw new BadRequestException( `Entry is not balanced: debit=${entry.totalDebit} credit=${entry.totalCredit}` ); } // Update account balances for (const line of entry.lines) { const amount = line.account.accountType.normalBalance === 'debit' ? line.debitBase - line.creditBase : line.creditBase - line.debitBase; await manager.update(Account, line.accountId, { currentBalance: () => `current_balance + ${amount}`, updatedAt: new Date(), }); } // Mark as posted entry.status = 'posted'; entry.postedAt = new Date(); entry.postedBy = userId; return manager.save(entry); }); } async reverse(id: string, userId: string, description: string): Promise { const original = await this.findById(id); if (original.status !== 'posted') { throw new BadRequestException('Only posted entries can be reversed'); } // Create reversal entry const reversalDto: CreateJournalEntryDto = { entryDate: new Date(), currencyCode: original.currencyCode, description: description || `Reversal of ${original.entryNumber}`, reference: original.entryNumber, lines: original.lines.map(line => ({ accountId: line.accountId, costCenterId: line.costCenterId, debit: line.credit, // Swap debit/credit credit: line.debit, description: `Reversal: ${line.description || ''}`, })), }; const reversal = await this.create(original.tenantId, userId, reversalDto); // Post the reversal await this.post(reversal.id, userId); // Mark original as reversed await this.entryRepo.update(id, { status: 'reversed', reversedBy: reversal.id }); return this.findById(reversal.id); } async getLedger( accountId: string, query: QueryLedgerDto ): Promise { const qb = this.lineRepo.createQueryBuilder('jl') .innerJoinAndSelect('jl.journalEntry', 'je') .where('jl.account_id = :accountId', { accountId }) .andWhere('je.status = :status', { status: 'posted' }); if (query.dateFrom) { qb.andWhere('je.entry_date >= :dateFrom', { dateFrom: query.dateFrom }); } if (query.dateTo) { qb.andWhere('je.entry_date <= :dateTo', { dateTo: query.dateTo }); } qb.orderBy('je.entry_date', 'ASC') .addOrderBy('je.entry_number', 'ASC') .addOrderBy('jl.line_number', 'ASC'); const lines = await qb.getMany(); let runningBalance = 0; return lines.map(line => { runningBalance += line.debitBase - line.creditBase; return { date: line.journalEntry.entryDate, entryNumber: line.journalEntry.entryNumber, description: line.description || line.journalEntry.description, reference: line.reference, debit: line.debitBase, credit: line.creditBase, balance: runningBalance, }; }); } private async generateEntryNumber(tenantId: string): Promise { const year = new Date().getFullYear(); const prefix = `JE-${year}-`; const lastEntry = await this.entryRepo.findOne({ where: { tenantId, entryNumber: Like(`${prefix}%`) }, order: { entryNumber: 'DESC' }, }); let sequence = 1; if (lastEntry) { const lastNum = parseInt(lastEntry.entryNumber.replace(prefix, '')); sequence = lastNum + 1; } return `${prefix}${sequence.toString().padStart(6, '0')}`; } } ``` ### CurrenciesService ```typescript @Injectable() export class CurrenciesService { constructor( @InjectRepository(TenantCurrency) private readonly currencyRepo: Repository, @InjectRepository(ExchangeRate) private readonly rateRepo: Repository, ) {} async findByTenant(tenantId: string): Promise { return this.currencyRepo.find({ where: { tenantId, isActive: true }, relations: ['currency'], }); } async getBaseCurrency(tenantId: string): Promise { const base = await this.currencyRepo.findOne({ where: { tenantId, isBase: true }, }); if (!base) { throw new NotFoundException('Base currency not configured'); } return base; } async getRate( tenantId: string, fromCurrency: string, toCurrency: string, date: Date = new Date() ): Promise { if (fromCurrency === toCurrency) { return 1; } // Try direct rate const directRate = await this.rateRepo.findOne({ where: { tenantId, fromCurrency, toCurrency, effectiveDate: LessThanOrEqual(date), }, order: { effectiveDate: 'DESC' }, }); if (directRate) { return directRate.rate; } // Try inverse rate const inverseRate = await this.rateRepo.findOne({ where: { tenantId, fromCurrency: toCurrency, toCurrency: fromCurrency, effectiveDate: LessThanOrEqual(date), }, order: { effectiveDate: 'DESC' }, }); if (inverseRate) { return 1 / inverseRate.rate; } throw new NotFoundException( `Exchange rate not found for ${fromCurrency} to ${toCurrency}` ); } async convert( tenantId: string, amount: number, fromCurrency: string, toCurrency: string, date?: Date ): Promise { const rate = await this.getRate(tenantId, fromCurrency, toCurrency, date); const convertedAmount = amount * rate; return { originalAmount: amount, originalCurrency: fromCurrency, convertedAmount, targetCurrency: toCurrency, rate, date: date || new Date(), }; } async setRate( tenantId: string, userId: string, dto: CreateExchangeRateDto ): Promise { const rate = this.rateRepo.create({ tenantId, fromCurrency: dto.fromCurrency, toCurrency: dto.toCurrency, rate: dto.rate, effectiveDate: dto.effectiveDate, source: dto.source, createdBy: userId, }); return this.rateRepo.save(rate); } } ``` ### FiscalPeriodsService ```typescript @Injectable() export class FiscalPeriodsService { constructor( @InjectRepository(FiscalYear) private readonly yearRepo: Repository, @InjectRepository(FiscalPeriod) private readonly periodRepo: Repository, ) {} async findOpenForDate(tenantId: string, date: Date): Promise { return this.periodRepo.findOne({ where: { tenantId, startDate: LessThanOrEqual(date), endDate: MoreThanOrEqual(date), status: 'open', }, }); } async createYear( tenantId: string, dto: CreateFiscalYearDto ): Promise { const year = this.yearRepo.create({ tenantId, name: dto.name, startDate: dto.startDate, endDate: dto.endDate, }); const savedYear = await this.yearRepo.save(year); // Create monthly periods if (dto.createMonthlyPeriods) { await this.createMonthlyPeriods(savedYear); } return this.yearRepo.findOne({ where: { id: savedYear.id }, relations: ['periods'], }); } async closePeriod( periodId: string, userId: string ): Promise { const period = await this.periodRepo.findOneOrFail({ where: { id: periodId }, }); if (period.status !== 'open') { throw new BadRequestException('Period is not open'); } // Check for draft entries const draftCount = await this.periodRepo.manager.count(JournalEntry, { where: { fiscalPeriodId: periodId, status: 'draft' }, }); if (draftCount > 0) { throw new BadRequestException( `Cannot close period with ${draftCount} draft entries` ); } period.status = 'closed'; period.closedAt = new Date(); period.closedBy = userId; return this.periodRepo.save(period); } async reopenPeriod( periodId: string, userId: string ): Promise { const period = await this.periodRepo.findOneOrFail({ where: { id: periodId }, relations: ['fiscalYear'], }); if (period.status !== 'closed') { throw new BadRequestException('Period is not closed'); } if (period.fiscalYear.status === 'closed') { throw new BadRequestException('Cannot reopen period of closed fiscal year'); } period.status = 'open'; period.closedAt = null; period.closedBy = null; return this.periodRepo.save(period); } private async createMonthlyPeriods(year: FiscalYear): Promise { const periods: Partial[] = []; let currentDate = new Date(year.startDate); let periodNumber = 1; while (currentDate < year.endDate) { const startOfMonth = startOfMonth(currentDate); const endOfMonth = endOfMonth(currentDate); periods.push({ fiscalYearId: year.id, tenantId: year.tenantId, name: format(currentDate, 'MMMM yyyy'), periodNumber, startDate: startOfMonth > year.startDate ? startOfMonth : year.startDate, endDate: endOfMonth < year.endDate ? endOfMonth : year.endDate, }); currentDate = addMonths(currentDate, 1); periodNumber++; } await this.periodRepo.save(periods); } } ``` --- ## Controladores ### JournalController ```typescript @ApiTags('Journal Entries') @Controller('financial/journal') @UseGuards(JwtAuthGuard, RbacGuard) export class JournalController { constructor(private readonly service: JournalService) {} @Get() @Permissions('financial.journal.read') async findAll( @TenantId() tenantId: string, @Query() query: QueryJournalDto ) { return this.service.findAll(tenantId, query); } @Get(':id') @Permissions('financial.journal.read') async findById(@Param('id') id: string) { return this.service.findById(id); } @Post() @Permissions('financial.journal.create') async create( @TenantId() tenantId: string, @CurrentUser() user: User, @Body() dto: CreateJournalEntryDto ) { return this.service.create(tenantId, user.id, dto); } @Post(':id/post') @Permissions('financial.journal.post') async post( @Param('id') id: string, @CurrentUser() user: User ) { return this.service.post(id, user.id); } @Post(':id/reverse') @Permissions('financial.journal.reverse') async reverse( @Param('id') id: string, @CurrentUser() user: User, @Body('description') description?: string ) { return this.service.reverse(id, user.id, description); } } ``` ### AccountsController ```typescript @ApiTags('Chart of Accounts') @Controller('financial/charts/:chartId/accounts') @UseGuards(JwtAuthGuard, RbacGuard) export class AccountsController { constructor(private readonly service: AccountsService) {} @Get() @Permissions('financial.accounts.read') async findAll(@Param('chartId') chartId: string) { return this.service.findByChart(chartId); } @Get('tree') @Permissions('financial.accounts.read') async findTree(@Param('chartId') chartId: string) { return this.service.findTreeByChart(chartId); } @Post() @Permissions('financial.accounts.manage') async create( @Param('chartId') chartId: string, @TenantId() tenantId: string, @Body() dto: CreateAccountDto ) { return this.service.create(chartId, { ...dto, tenantId }); } @Get(':id/balance') @Permissions('financial.accounts.read') async getBalance( @Param('id') id: string, @Query('asOfDate') asOfDate?: string ) { return this.service.getBalance(id, asOfDate ? new Date(asOfDate) : undefined); } @Get(':id/ledger') @Permissions('financial.accounts.read') async getLedger( @Param('id') id: string, @Query() query: QueryLedgerDto ) { return this.service.getLedger(id, query); } } ``` --- ## API Endpoints Summary | Method | Path | Permission | Description | |--------|------|------------|-------------| | GET | /financial/charts | financial.accounts.read | List charts | | POST | /financial/charts | financial.accounts.manage | Create chart | | GET | /financial/charts/:id/accounts | financial.accounts.read | List accounts | | GET | /financial/charts/:id/accounts/tree | financial.accounts.read | Accounts tree | | POST | /financial/charts/:id/accounts | financial.accounts.manage | Create account | | GET | /financial/accounts/:id/balance | financial.accounts.read | Account balance | | GET | /financial/accounts/:id/ledger | financial.accounts.read | Account ledger | | GET | /financial/currencies | financial.currencies.read | List currencies | | POST | /financial/currencies/convert | financial.currencies.read | Convert amount | | POST | /financial/currencies/rates | financial.currencies.manage | Set rate | | GET | /financial/fiscal-years | financial.periods.read | List years | | POST | /financial/fiscal-years | financial.periods.manage | Create year | | GET | /financial/fiscal-periods | financial.periods.read | List periods | | POST | /financial/fiscal-periods/:id/close | financial.periods.manage | Close period | | POST | /financial/fiscal-periods/:id/reopen | financial.periods.manage | Reopen period | | GET | /financial/journal | financial.journal.read | List entries | | GET | /financial/journal/:id | financial.journal.read | Get entry | | POST | /financial/journal | financial.journal.create | Create entry | | POST | /financial/journal/:id/post | financial.journal.post | Post entry | | POST | /financial/journal/:id/reverse | financial.journal.reverse | Reverse entry | | GET | /financial/cost-centers | financial.costcenters.read | List cost centers | | POST | /financial/cost-centers | financial.costcenters.manage | Create cost center | | GET | /financial/reports/trial-balance | financial.reports.read | Trial balance | --- ## Historial | Version | Fecha | Autor | Cambios | |---------|-------|-------|---------| | 1.0 | 2025-12-05 | Requirements-Analyst | Creacion inicial |