import { Body, Controller, Delete, Get, Param, Patch, Post, Query, StreamableFile, UploadedFile, UseGuards, UseInterceptors } from '@nestjs/common'; import { FileInterceptor } from '@nestjs/platform-express'; import { memoryStorage } from 'multer'; import { JwtAuthGuard } from '../auth/jwt-auth.guard'; import { CurrentUser } from '../auth/current-user.decorator'; import { OrgModuleGuard } from '../billing/org-module.guard'; import { RequireOrgModule } from '../billing/org-module.decorator'; import { TreasuryAccessGuard } from './treasury-access.guard'; import type { Actor } from '../common/record-scope'; import { TreasuryOpsService } from './treasury-ops.service'; import { TreasuryReceivablesService } from './treasury-receivables.service'; import { TreasuryService } from './treasury.service'; @Controller('treasury') @UseGuards(JwtAuthGuard, OrgModuleGuard, TreasuryAccessGuard) @RequireOrgModule('treasury') export class TreasuryController { constructor( private readonly treasury: TreasuryService, private readonly receivables: TreasuryReceivablesService, private readonly ops: TreasuryOpsService, ) {} @Get('dashboard') dashboard( @CurrentUser() user: Actor, @Query('from') from?: string, @Query('to') to?: string, ) { return this.treasury.dashboard(user, { from, to }); } @Get('categories') categories(@CurrentUser() user: Actor, @Query('kind') kind?: string) { return this.treasury.listCategories(user, kind); } @Post('categories') createCategory( @CurrentUser() user: Actor, @Body() body: { name: string; kind: 'income' | 'expense' }, ) { return this.treasury.createCategory(user, body); } @Patch('categories/:id') updateCategory( @CurrentUser() user: Actor, @Param('id') id: string, @Body() body: { name?: string; isActive?: boolean }, ) { return this.treasury.updateCategory(user, id, body); } @Delete('categories/:id') deleteCategory(@CurrentUser() user: Actor, @Param('id') id: string) { return this.treasury.deleteCategory(user, id); } @Get('accounts') accounts( @CurrentUser() user: Actor, @Query('from') from?: string, @Query('to') to?: string, ) { return this.treasury.listAccounts(user, { from, to }); } @Post('accounts') createAccount(@CurrentUser() user: Actor, @Body() body: Parameters[1]) { return this.treasury.createAccount(user, body); } @Patch('accounts/:id') updateAccount( @CurrentUser() user: Actor, @Param('id') id: string, @Body() body: { name?: string; type?: 'bank' | 'cash' | 'petty'; bankName?: string | null; accountRef?: string | null; notes?: string | null; isActive?: boolean; responsibleName?: string | null; openingBalance?: string | number; openingDate?: string; }, ) { return this.treasury.updateAccount(user, id, body); } @Get('accounts/:id/ledger') ledger( @CurrentUser() user: Actor, @Param('id') id: string, @Query('from') from?: string, @Query('to') to?: string, ) { return this.treasury.ledger(user, id, { from, to }); } @Post('transfers') transfer(@CurrentUser() user: Actor, @Body() body: Parameters[1]) { return this.treasury.transfer(user, body); } @Get('parties') parties(@CurrentUser() user: Actor, @Query('q') q?: string) { return this.treasury.searchParties(user, q); } @Post('parties/person') createPartyPerson( @CurrentUser() user: Actor, @Body() body: { firstName?: string; lastName?: string }, ) { return this.treasury.createPartyPerson(user, body); } @Post('parties/company') createPartyCompany(@CurrentUser() user: Actor, @Body() body: { name?: string }) { return this.treasury.createPartyCompany(user, body); } @Get('party-balances') partyBalances(@CurrentUser() user: Actor, @Query('kind') kind?: string) { return this.treasury.partyBalances(user, kind === 'expense' ? 'expense' : 'income'); } @Post('party-balances/collect') collectPartyBalance( @CurrentUser() user: Actor, @Body() body: { kind?: string; partyKind?: 'person' | 'company' | 'user'; partyId?: string; amount?: string | number; accountId?: string; occurredAt?: string; }, ) { return this.treasury.collectPartyBalance(user, { kind: body.kind === 'expense' ? 'expense' : 'income', partyKind: body.partyKind ?? 'person', partyId: body.partyId ?? '', amount: body.amount ?? 0, accountId: body.accountId ?? '', occurredAt: body.occurredAt, }); } @Get('documents') documents( @CurrentUser() user: Actor, @Query('kind') kind: 'income' | 'expense', @Query('q') q?: string, ) { return this.treasury.listDocuments(user, kind === 'expense' ? 'expense' : 'income', q); } @Post('documents') createDocument(@CurrentUser() user: Actor, @Body() body: Parameters[1]) { return this.treasury.createDocument(user, body); } @Get('documents/export') async exportDocuments( @CurrentUser() user: Actor, @Query('kind') kind?: string, ) { const buffer = await this.treasury.documentsExcel( user, kind === 'expense' ? 'expense' : 'income', ); return new StreamableFile(buffer, { type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', disposition: 'attachment; filename="treasury-documents.xlsx"', }); } @Get('attachments/:id') async downloadAttachment(@CurrentUser() user: Actor, @Param('id') id: string) { const file = await this.treasury.openAttachment(user, id); return new StreamableFile(file.stream, { type: file.mimeType, disposition: `attachment; filename="${encodeURIComponent(file.fileName)}"`, }); } @Get('audit') audit(@CurrentUser() user: Actor) { return this.treasury.listAudit(user); } @Post('documents/:id/attachments') @UseInterceptors(FileInterceptor('file', { storage: memoryStorage(), limits: { fileSize: 8_000_000 } })) addAttachment( @CurrentUser() user: Actor, @Param('id') id: string, @UploadedFile() file: { originalname: string; mimetype: string; buffer: Buffer }, ) { return this.treasury.addAttachment(user, id, file); } @Post('documents/:id/settle') settle( @CurrentUser() user: Actor, @Param('id') id: string, @Body() body: { amount: string | number; accountId: string; occurredAt?: string; reference?: string | null }, ) { return this.treasury.settleDocument(user, id, body); } @Get('receivables/export') async exportReceivables(@CurrentUser() user: Actor) { const buffer = await this.receivables.excel(user); return new StreamableFile(buffer, { type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', disposition: 'attachment; filename="treasury-installments.xlsx"', }); } @Get('receivables') receivablesList(@CurrentUser() user: Actor) { return this.receivables.list(user); } @Post('receivables') createReceivable( @CurrentUser() user: Actor, @Body() body: Parameters[1], ) { return this.receivables.create(user, body); } @Post('installments/:id/collect') collectInstallment( @CurrentUser() user: Actor, @Param('id') id: string, @Body() body: Parameters[2], ) { return this.receivables.collect(user, id, body); } @Get('payroll') payroll(@CurrentUser() user: Actor) { return this.ops.listPayroll(user); } @Post('payroll') createPayroll(@CurrentUser() user: Actor, @Body() body: Parameters[1]) { return this.ops.createPayroll(user, body); } @Patch('payroll/:id') updatePayroll( @CurrentUser() user: Actor, @Param('id') id: string, @Body() body: Parameters[2], ) { return this.ops.updatePayroll(user, id, body); } @Patch('payroll/:id/payments/:movementId') updatePayrollPayment( @CurrentUser() user: Actor, @Param('id') id: string, @Param('movementId') movementId: string, @Body() body: Parameters[3], ) { return this.ops.updatePayrollPayment(user, id, movementId, body); } @Post('payroll/:id/pay') payPayroll( @CurrentUser() user: Actor, @Param('id') id: string, @Body() body: Parameters[2], ) { return this.ops.payPayroll(user, id, body); } @Get('recurring') recurring(@CurrentUser() user: Actor) { return this.ops.listRecurring(user); } @Post('recurring') createRecurring( @CurrentUser() user: Actor, @Body() body: Parameters[1], ) { return this.ops.createRecurring(user, body); } @Get('reports') report( @CurrentUser() user: Actor, @Query('from') from?: string, @Query('to') to?: string, ) { return this.ops.report(user, from, to); } @Get('reports/export') async exportReport( @CurrentUser() user: Actor, @Query('from') from?: string, @Query('to') to?: string, ) { const [debtors, creditors] = await Promise.all([ this.treasury.partyBalances(user, 'income'), this.treasury.partyBalances(user, 'expense'), ]); const buffer = await this.ops.reportExcel(user, from, to, { debtors, creditors }); return new StreamableFile(buffer, { type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', disposition: 'attachment; filename="treasury-report.xlsx"', }); } @Post('reconciliations') reconcile(@CurrentUser() user: Actor, @Body() body: Parameters[1]) { return this.ops.reconcile(user, body); } @Post('adjustments') adjust(@CurrentUser() user: Actor, @Body() body: Parameters[1]) { return this.ops.adjust(user, body); } @Post('documents/:id/void') voidDoc( @CurrentUser() user: Actor, @Param('id') id: string, @Body() body: { reason?: string }, ) { return this.treasury.voidDocument(user, id, body.reason); } }