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
@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
@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
@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
@Injectable()
export class AccountsService {
constructor(
@InjectRepository(Account)
private readonly repo: Repository<Account>,
@InjectRepository(AccountType)
private readonly typeRepo: Repository<AccountType>,
) {}
async findByChart(chartId: string): Promise<Account[]> {
return this.repo.find({
where: { chartId },
relations: ['accountType', 'parent'],
order: { code: 'ASC' },
});
}
async findTreeByChart(chartId: string): Promise<Account[]> {
const accounts = await this.findByChart(chartId);
return this.buildTree(accounts);
}
async create(chartId: string, dto: CreateAccountDto): Promise<Account> {
// 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<Account> {
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<AccountBalance> {
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<TrialBalanceItem[]> {
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<string, Account>();
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<boolean> {
const count = await this.repo.manager.count(JournalLine, {
where: { accountId },
});
return count > 0;
}
}
JournalService
@Injectable()
export class JournalService {
constructor(
@InjectRepository(JournalEntry)
private readonly entryRepo: Repository<JournalEntry>,
@InjectRepository(JournalLine)
private readonly lineRepo: Repository<JournalLine>,
private readonly periodsService: FiscalPeriodsService,
private readonly currenciesService: CurrenciesService,
private readonly accountsService: AccountsService,
@InjectDataSource()
private readonly dataSource: DataSource,
) {}
async findAll(
tenantId: string,
query: QueryJournalDto
): Promise<PaginatedResult<JournalEntry>> {
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<JournalEntry> {
return this.entryRepo.findOne({
where: { id },
relations: ['lines', 'lines.account', 'lines.costCenter', 'fiscalPeriod'],
});
}
async create(
tenantId: string,
userId: string,
dto: CreateJournalEntryDto
): Promise<JournalEntry> {
// 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<JournalEntry> {
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<JournalEntry> {
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<LedgerEntry[]> {
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<string> {
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
@Injectable()
export class CurrenciesService {
constructor(
@InjectRepository(TenantCurrency)
private readonly currencyRepo: Repository<TenantCurrency>,
@InjectRepository(ExchangeRate)
private readonly rateRepo: Repository<ExchangeRate>,
) {}
async findByTenant(tenantId: string): Promise<TenantCurrency[]> {
return this.currencyRepo.find({
where: { tenantId, isActive: true },
relations: ['currency'],
});
}
async getBaseCurrency(tenantId: string): Promise<TenantCurrency> {
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<number> {
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<ConversionResult> {
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<ExchangeRate> {
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
@Injectable()
export class FiscalPeriodsService {
constructor(
@InjectRepository(FiscalYear)
private readonly yearRepo: Repository<FiscalYear>,
@InjectRepository(FiscalPeriod)
private readonly periodRepo: Repository<FiscalPeriod>,
) {}
async findOpenForDate(tenantId: string, date: Date): Promise<FiscalPeriod> {
return this.periodRepo.findOne({
where: {
tenantId,
startDate: LessThanOrEqual(date),
endDate: MoreThanOrEqual(date),
status: 'open',
},
});
}
async createYear(
tenantId: string,
dto: CreateFiscalYearDto
): Promise<FiscalYear> {
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<FiscalPeriod> {
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<FiscalPeriod> {
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<void> {
const periods: Partial<FiscalPeriod>[] = [];
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
@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
@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 |