A settled document of the same amount no longer leaves an existing remaining unchanged. Co-authored-by: Cursor <cursoragent@cursor.com>
344 lines
10 KiB
TypeScript
344 lines
10 KiB
TypeScript
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<TreasuryService['createAccount']>[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<TreasuryService['transfer']>[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<TreasuryService['createDocument']>[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<TreasuryReceivablesService['create']>[1],
|
|
) {
|
|
return this.receivables.create(user, body);
|
|
}
|
|
|
|
@Post('installments/:id/collect')
|
|
collectInstallment(
|
|
@CurrentUser() user: Actor,
|
|
@Param('id') id: string,
|
|
@Body() body: Parameters<TreasuryReceivablesService['collect']>[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<TreasuryOpsService['createPayroll']>[1]) {
|
|
return this.ops.createPayroll(user, body);
|
|
}
|
|
|
|
@Patch('payroll/:id')
|
|
updatePayroll(
|
|
@CurrentUser() user: Actor,
|
|
@Param('id') id: string,
|
|
@Body() body: Parameters<TreasuryOpsService['updatePayroll']>[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<TreasuryOpsService['updatePayrollPayment']>[3],
|
|
) {
|
|
return this.ops.updatePayrollPayment(user, id, movementId, body);
|
|
}
|
|
|
|
@Post('payroll/:id/pay')
|
|
payPayroll(
|
|
@CurrentUser() user: Actor,
|
|
@Param('id') id: string,
|
|
@Body() body: Parameters<TreasuryOpsService['payPayroll']>[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<TreasuryOpsService['createRecurring']>[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<TreasuryOpsService['reconcile']>[1]) {
|
|
return this.ops.reconcile(user, body);
|
|
}
|
|
|
|
@Post('adjustments')
|
|
adjust(@CurrentUser() user: Actor, @Body() body: Parameters<TreasuryOpsService['adjust']>[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);
|
|
}
|
|
}
|