@hed-hog/finance 0.0.223 → 0.0.225

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (46) hide show
  1. package/dist/dto/create-financial-title.dto.d.ts +19 -0
  2. package/dist/dto/create-financial-title.dto.d.ts.map +1 -0
  3. package/dist/dto/create-financial-title.dto.js +128 -0
  4. package/dist/dto/create-financial-title.dto.js.map +1 -0
  5. package/dist/finance.controller.d.ts +276 -0
  6. package/dist/finance.controller.d.ts.map +1 -0
  7. package/dist/finance.controller.js +110 -0
  8. package/dist/finance.controller.js.map +1 -0
  9. package/dist/finance.module.d.ts.map +1 -1
  10. package/dist/finance.module.js +8 -3
  11. package/dist/finance.module.js.map +1 -1
  12. package/dist/finance.service.d.ts +295 -0
  13. package/dist/finance.service.d.ts.map +1 -0
  14. package/dist/finance.service.js +416 -0
  15. package/dist/finance.service.js.map +1 -0
  16. package/dist/index.d.ts +3 -0
  17. package/dist/index.d.ts.map +1 -1
  18. package/dist/index.js +3 -0
  19. package/dist/index.js.map +1 -1
  20. package/hedhog/data/menu.yaml +72 -25
  21. package/hedhog/data/route.yaml +55 -1
  22. package/hedhog/frontend/app/_lib/formatters.ts.ejs +20 -0
  23. package/hedhog/frontend/app/_lib/use-finance-data.ts.ejs +87 -0
  24. package/hedhog/frontend/app/accounts-payable/approvals/page.tsx.ejs +290 -0
  25. package/hedhog/frontend/app/accounts-payable/installments/[id]/page.tsx.ejs +410 -0
  26. package/hedhog/frontend/app/accounts-payable/installments/page.tsx.ejs +388 -0
  27. package/hedhog/frontend/app/accounts-receivable/collections-default/page.tsx.ejs +423 -0
  28. package/hedhog/frontend/app/accounts-receivable/installments/[id]/page.tsx.ejs +411 -0
  29. package/hedhog/frontend/app/accounts-receivable/installments/page.tsx.ejs +385 -0
  30. package/hedhog/frontend/app/cash-and-banks/bank-accounts/page.tsx.ejs +296 -0
  31. package/hedhog/frontend/app/cash-and-banks/bank-reconciliation/page.tsx.ejs +427 -0
  32. package/hedhog/frontend/app/cash-and-banks/statements/page.tsx.ejs +273 -0
  33. package/hedhog/frontend/app/cash-and-banks/transfers/page.tsx.ejs +253 -0
  34. package/hedhog/frontend/app/page.tsx.ejs +338 -17
  35. package/hedhog/frontend/app/planning/cash-flow-forecast/page.tsx.ejs +298 -0
  36. package/hedhog/frontend/app/planning/receivables-calendar/page.tsx.ejs +225 -0
  37. package/hedhog/frontend/app/planning/scenarios/page.tsx.ejs +338 -0
  38. package/hedhog/frontend/messages/en.json +776 -0
  39. package/hedhog/frontend/messages/pt.json +776 -0
  40. package/hedhog/query/constraints.sql +169 -0
  41. package/package.json +3 -3
  42. package/src/dto/create-financial-title.dto.ts +142 -0
  43. package/src/finance.controller.ts +89 -0
  44. package/src/finance.module.ts +8 -3
  45. package/src/finance.service.ts +529 -0
  46. package/src/index.ts +4 -0
@@ -0,0 +1,529 @@
1
+ import { getLocaleText } from '@hed-hog/api-locale';
2
+ import { PaginationDTO, PaginationService } from '@hed-hog/api-pagination';
3
+ import { PrismaService } from '@hed-hog/api-prisma';
4
+ import {
5
+ BadRequestException,
6
+ Injectable,
7
+ NotFoundException,
8
+ } from '@nestjs/common';
9
+ import { CreateFinancialTitleDto } from './dto/create-financial-title.dto';
10
+
11
+ type TitleType = 'payable' | 'receivable';
12
+
13
+ @Injectable()
14
+ export class FinanceService {
15
+ constructor(
16
+ private readonly prisma: PrismaService,
17
+ private readonly paginationService: PaginationService,
18
+ ) {}
19
+
20
+ async getData() {
21
+ const [
22
+ payables,
23
+ receivables,
24
+ people,
25
+ categories,
26
+ costCenters,
27
+ bankAccounts,
28
+ tags,
29
+ auditLogs,
30
+ ] = await Promise.all([
31
+ this.loadTitles('payable'),
32
+ this.loadTitles('receivable'),
33
+ this.loadPeople(),
34
+ this.loadCategories(),
35
+ this.loadCostCenters(),
36
+ this.loadBankAccounts(),
37
+ this.loadTags(),
38
+ this.loadAuditLogs(),
39
+ ]);
40
+
41
+ return {
42
+ kpis: {
43
+ saldoCaixa: 0,
44
+ aPagar30dias: 0,
45
+ aPagar7dias: 0,
46
+ aReceber30dias: 0,
47
+ aReceber7dias: 0,
48
+ inadimplencia: 0,
49
+ },
50
+ fluxoCaixaPrevisto: [],
51
+ titulosPagar: payables,
52
+ titulosReceber: receivables,
53
+ extratos: [],
54
+ contasBancarias: bankAccounts,
55
+ pessoas: people,
56
+ categorias: categories,
57
+ centrosCusto: costCenters,
58
+ aprovacoesPendentes: [],
59
+ agingInadimplencia: [],
60
+ cenarios: [],
61
+ transferencias: [],
62
+ tags,
63
+ logsAuditoria: auditLogs,
64
+ recebiveis: [],
65
+ adquirentes: [],
66
+ historicoContatos: [],
67
+ entradasPrevistas: [],
68
+ saidasPrevistas: [],
69
+ };
70
+ }
71
+
72
+ async listAccountsPayableInstallments(
73
+ paginationParams: PaginationDTO,
74
+ status?: string,
75
+ ) {
76
+ return this.listTitles('payable', paginationParams, status);
77
+ }
78
+
79
+ async listAccountsReceivableInstallments(
80
+ paginationParams: PaginationDTO,
81
+ status?: string,
82
+ ) {
83
+ return this.listTitles('receivable', paginationParams, status);
84
+ }
85
+
86
+ async getAccountsPayableInstallment(id: number, locale: string) {
87
+ const title = await this.getTitleById(id, 'payable', locale);
88
+ return this.mapTitleToFront(title);
89
+ }
90
+
91
+ async getAccountsReceivableInstallment(id: number, locale: string) {
92
+ const title = await this.getTitleById(id, 'receivable', locale);
93
+ return this.mapTitleToFront(title);
94
+ }
95
+
96
+ async createAccountsPayableTitle(
97
+ data: CreateFinancialTitleDto,
98
+ locale: string,
99
+ userId?: number,
100
+ ) {
101
+ return this.createTitle(data, 'payable', locale, userId);
102
+ }
103
+
104
+ async createAccountsReceivableTitle(
105
+ data: CreateFinancialTitleDto,
106
+ locale: string,
107
+ userId?: number,
108
+ ) {
109
+ return this.createTitle(data, 'receivable', locale, userId);
110
+ }
111
+
112
+ private async listTitles(
113
+ titleType: TitleType,
114
+ paginationParams: PaginationDTO,
115
+ status?: string,
116
+ ) {
117
+ const prismaStatus = this.mapStatusFromPt(status);
118
+ const where: any = {
119
+ title_type: titleType,
120
+ };
121
+
122
+ if (prismaStatus) {
123
+ where.status = prismaStatus;
124
+ }
125
+
126
+ return this.paginationService.paginate(
127
+ this.prisma.financial_title,
128
+ paginationParams,
129
+ {
130
+ where,
131
+ include: this.defaultTitleInclude(),
132
+ orderBy: { created_at: 'desc' },
133
+ },
134
+ );
135
+ }
136
+
137
+ private async getTitleById(id: number, titleType: TitleType, locale: string) {
138
+ const title = await this.prisma.financial_title.findFirst({
139
+ where: {
140
+ id,
141
+ title_type: titleType,
142
+ },
143
+ include: this.defaultTitleInclude(),
144
+ });
145
+
146
+ if (!title) {
147
+ throw new NotFoundException(
148
+ getLocaleText(
149
+ 'itemNotFound',
150
+ locale,
151
+ `Financial title with ID ${id} not found`,
152
+ ).replace('{{item}}', 'Financial title'),
153
+ );
154
+ }
155
+
156
+ return title;
157
+ }
158
+
159
+ private async createTitle(
160
+ data: CreateFinancialTitleDto,
161
+ titleType: TitleType,
162
+ locale: string,
163
+ userId?: number,
164
+ ) {
165
+ const person = await this.prisma.person.findUnique({
166
+ where: { id: data.person_id },
167
+ select: { id: true },
168
+ });
169
+
170
+ if (!person) {
171
+ throw new BadRequestException(
172
+ getLocaleText('personNotFound', locale, 'Person not found'),
173
+ );
174
+ }
175
+
176
+ if (data.finance_category_id) {
177
+ const category = await this.prisma.finance_category.findUnique({
178
+ where: { id: data.finance_category_id },
179
+ select: { id: true },
180
+ });
181
+
182
+ if (!category) {
183
+ throw new BadRequestException(
184
+ getLocaleText('categoryNotFound', locale, 'Category not found'),
185
+ );
186
+ }
187
+ }
188
+
189
+ if (data.cost_center_id) {
190
+ const costCenter = await this.prisma.cost_center.findUnique({
191
+ where: { id: data.cost_center_id },
192
+ select: { id: true },
193
+ });
194
+
195
+ if (!costCenter) {
196
+ throw new BadRequestException(
197
+ getLocaleText('costCenterNotFound', locale, 'Cost center not found'),
198
+ );
199
+ }
200
+ }
201
+
202
+ const installments =
203
+ data.installments && data.installments.length > 0
204
+ ? data.installments
205
+ : [
206
+ {
207
+ installment_number: 1,
208
+ due_date: data.due_date,
209
+ amount: data.total_amount,
210
+ },
211
+ ];
212
+
213
+ const title = await this.prisma.financial_title.create({
214
+ data: {
215
+ person_id: data.person_id,
216
+ title_type: titleType,
217
+ status: 'open',
218
+ document_number: data.document_number,
219
+ description: data.description,
220
+ competence_date: data.competence_date
221
+ ? new Date(data.competence_date)
222
+ : null,
223
+ issue_date: data.issue_date ? new Date(data.issue_date) : null,
224
+ total_amount_cents: this.toCents(data.total_amount),
225
+ finance_category_id: data.finance_category_id,
226
+ created_by_user_id: userId,
227
+ },
228
+ });
229
+
230
+ for (let index = 0; index < installments.length; index++) {
231
+ const installment = installments[index];
232
+ const amountCents = this.toCents(installment.amount);
233
+
234
+ const createdInstallment = await this.prisma.financial_installment.create({
235
+ data: {
236
+ title_id: title.id,
237
+ installment_number: installment.installment_number || index + 1,
238
+ competence_date: data.competence_date
239
+ ? new Date(data.competence_date)
240
+ : new Date(installment.due_date),
241
+ due_date: new Date(installment.due_date),
242
+ amount_cents: amountCents,
243
+ open_amount_cents: amountCents,
244
+ status: 'open',
245
+ notes: data.description,
246
+ },
247
+ });
248
+
249
+ if (data.cost_center_id) {
250
+ await this.prisma.installment_allocation.create({
251
+ data: {
252
+ installment_id: createdInstallment.id,
253
+ cost_center_id: data.cost_center_id,
254
+ allocated_amount_cents: amountCents,
255
+ },
256
+ });
257
+ }
258
+ }
259
+
260
+ const createdTitle = await this.getTitleById(title.id, titleType, locale);
261
+ return this.mapTitleToFront(createdTitle, data.payment_channel);
262
+ }
263
+
264
+ private async loadTitles(type: TitleType) {
265
+ const titles = await this.prisma.financial_title.findMany({
266
+ where: { title_type: type },
267
+ include: this.defaultTitleInclude(),
268
+ orderBy: { created_at: 'desc' },
269
+ });
270
+
271
+ return titles.map((title) => this.mapTitleToFront(title));
272
+ }
273
+
274
+ private async loadPeople() {
275
+ const people = await this.prisma.person.findMany({
276
+ include: {
277
+ document: {
278
+ select: {
279
+ value: true,
280
+ },
281
+ take: 1,
282
+ },
283
+ },
284
+ orderBy: { name: 'asc' },
285
+ });
286
+
287
+ return people.map((person) => ({
288
+ id: String(person.id),
289
+ nome: person.name,
290
+ tipo: 'ambos',
291
+ documento: person.document[0]?.value || '',
292
+ }));
293
+ }
294
+
295
+ private async loadCategories() {
296
+ const categories = await this.prisma.finance_category.findMany({
297
+ orderBy: [{ code: 'asc' }, { name: 'asc' }],
298
+ });
299
+
300
+ return categories.map((category) => ({
301
+ id: String(category.id),
302
+ codigo: category.code,
303
+ nome: category.name,
304
+ natureza:
305
+ category.kind === 'revenue'
306
+ ? 'receita'
307
+ : category.kind === 'expense'
308
+ ? 'despesa'
309
+ : category.kind,
310
+ status: category.status,
311
+ }));
312
+ }
313
+
314
+ private async loadCostCenters() {
315
+ const costCenters = await this.prisma.cost_center.findMany({
316
+ orderBy: [{ code: 'asc' }, { name: 'asc' }],
317
+ });
318
+
319
+ return costCenters.map((costCenter) => ({
320
+ id: String(costCenter.id),
321
+ codigo: costCenter.code,
322
+ nome: costCenter.name,
323
+ status: costCenter.status,
324
+ }));
325
+ }
326
+
327
+ private async loadBankAccounts() {
328
+ const bankAccounts = await this.prisma.bank_account.findMany({
329
+ orderBy: [{ code: 'asc' }, { name: 'asc' }],
330
+ });
331
+
332
+ return bankAccounts.map((bankAccount) => ({
333
+ id: String(bankAccount.id),
334
+ codigo: bankAccount.code,
335
+ descricao: bankAccount.name,
336
+ banco: bankAccount.bank_name,
337
+ agencia: bankAccount.agency,
338
+ conta: bankAccount.account_number,
339
+ ativo: bankAccount.status === 'active',
340
+ }));
341
+ }
342
+
343
+ private async loadTags() {
344
+ const tags = await this.prisma.tag.findMany({
345
+ orderBy: { slug: 'asc' },
346
+ });
347
+
348
+ return tags.map((tag) => ({
349
+ id: String(tag.id),
350
+ nome: tag.slug,
351
+ cor: tag.color,
352
+ }));
353
+ }
354
+
355
+ private async loadAuditLogs() {
356
+ const logs = await this.prisma.audit_log.findMany({
357
+ orderBy: { created_at: 'desc' },
358
+ take: 500,
359
+ });
360
+
361
+ return logs.map((log) => ({
362
+ id: String(log.id),
363
+ entidade: log.entity_table === 'financial_title' ? 'TituloPagar' : log.entity_table,
364
+ entidadeId: log.entity_id,
365
+ usuarioId: log.actor_user_id ? String(log.actor_user_id) : null,
366
+ acao: log.action,
367
+ detalhes: log.summary,
368
+ antes: log.before_data,
369
+ depois: log.after_data,
370
+ data: log.created_at.toISOString(),
371
+ }));
372
+ }
373
+
374
+ private mapTitleToFront(title: any, paymentChannelOverride?: string) {
375
+ const allocations = title.financial_installment.flatMap(
376
+ (installment) => installment.installment_allocation,
377
+ );
378
+ const firstCostCenter = allocations[0]?.cost_center_id;
379
+
380
+ const tags = [
381
+ ...new Set(
382
+ title.financial_installment
383
+ .flatMap((installment) => installment.financial_installment_tag)
384
+ .map((tagRelation) => String(tagRelation.tag_id)),
385
+ ),
386
+ ];
387
+
388
+ const channelFromSettlement = title.financial_installment
389
+ .flatMap((installment) => installment.settlement_allocation)
390
+ .map((allocation) => allocation.settlement?.payment_method?.type)
391
+ .find(Boolean);
392
+
393
+ const mappedInstallments = title.financial_installment.map((installment) => ({
394
+ id: String(installment.id),
395
+ numero: installment.installment_number,
396
+ vencimento: installment.due_date.toISOString(),
397
+ valor: this.fromCents(installment.amount_cents),
398
+ status: this.mapStatusToPt(installment.status),
399
+ metodoPagamento:
400
+ this.mapPaymentMethodToPt(
401
+ installment.settlement_allocation[0]?.settlement?.payment_method?.type,
402
+ ) || paymentChannelOverride || 'transferencia',
403
+ liquidacoes: installment.settlement_allocation.map((allocation) => ({
404
+ id: String(allocation.id),
405
+ data: allocation.settlement?.settled_at?.toISOString(),
406
+ valor: this.fromCents(allocation.allocated_amount_cents),
407
+ juros: this.fromCents(allocation.interest_cents || 0),
408
+ desconto: this.fromCents(allocation.discount_cents || 0),
409
+ multa: this.fromCents(allocation.penalty_cents || 0),
410
+ contaBancariaId: allocation.settlement?.bank_account_id
411
+ ? String(allocation.settlement.bank_account_id)
412
+ : null,
413
+ metodo:
414
+ this.mapPaymentMethodToPt(
415
+ allocation.settlement?.payment_method?.type,
416
+ ) || 'transferencia',
417
+ })),
418
+ }));
419
+
420
+ return {
421
+ id: String(title.id),
422
+ documento: title.document_number || `TIT-${title.id}`,
423
+ descricao: title.description || '',
424
+ competencia: title.competence_date
425
+ ? title.competence_date.toISOString().slice(0, 7)
426
+ : '',
427
+ valorTotal: this.fromCents(title.total_amount_cents),
428
+ status: this.mapStatusToPt(title.status),
429
+ criadoEm: title.created_at.toISOString(),
430
+ categoriaId: title.finance_category_id ? String(title.finance_category_id) : null,
431
+ centroCustoId: firstCostCenter ? String(firstCostCenter) : null,
432
+ anexos: title.financial_title_attachment.map(
433
+ (attachment) => attachment.file?.filename || attachment.file?.path,
434
+ ),
435
+ tags,
436
+ parcelas: mappedInstallments,
437
+ canal:
438
+ this.mapPaymentMethodToPt(channelFromSettlement) ||
439
+ paymentChannelOverride ||
440
+ 'transferencia',
441
+ ...(title.title_type === 'payable'
442
+ ? { fornecedorId: String(title.person_id) }
443
+ : { clienteId: String(title.person_id) }),
444
+ };
445
+ }
446
+
447
+ private defaultTitleInclude() {
448
+ return {
449
+ person: true,
450
+ financial_title_attachment: {
451
+ include: {
452
+ file: true,
453
+ },
454
+ },
455
+ financial_installment: {
456
+ include: {
457
+ installment_allocation: true,
458
+ financial_installment_tag: true,
459
+ settlement_allocation: {
460
+ include: {
461
+ settlement: {
462
+ include: {
463
+ payment_method: true,
464
+ },
465
+ },
466
+ },
467
+ },
468
+ },
469
+ orderBy: {
470
+ installment_number: 'asc' as const,
471
+ },
472
+ },
473
+ };
474
+ }
475
+
476
+ private mapStatusToPt(status?: string | null) {
477
+ const statusMap = {
478
+ draft: 'rascunho',
479
+ approved: 'aprovado',
480
+ open: 'aberto',
481
+ partial: 'parcial',
482
+ settled: 'liquidado',
483
+ canceled: 'cancelado',
484
+ overdue: 'vencido',
485
+ };
486
+
487
+ return statusMap[status] || 'aberto';
488
+ }
489
+
490
+ private mapStatusFromPt(status?: string) {
491
+ if (!status || status === 'all') {
492
+ return undefined;
493
+ }
494
+
495
+ const statusMap = {
496
+ rascunho: 'draft',
497
+ aprovado: 'approved',
498
+ aberto: 'open',
499
+ parcial: 'partial',
500
+ liquidado: 'settled',
501
+ cancelado: 'canceled',
502
+ vencido: 'overdue',
503
+ };
504
+
505
+ return statusMap[status];
506
+ }
507
+
508
+ private mapPaymentMethodToPt(paymentMethodType?: string | null) {
509
+ const paymentMethodMap = {
510
+ boleto: 'boleto',
511
+ pix: 'pix',
512
+ ted: 'transferencia',
513
+ doc: 'transferencia',
514
+ card: 'cartao',
515
+ cash: 'dinheiro',
516
+ other: 'transferencia',
517
+ };
518
+
519
+ return paymentMethodMap[paymentMethodType] || undefined;
520
+ }
521
+
522
+ private toCents(value: number) {
523
+ return Math.round(value * 100);
524
+ }
525
+
526
+ private fromCents(value: number) {
527
+ return Number((value / 100).toFixed(2));
528
+ }
529
+ }
package/src/index.ts CHANGED
@@ -1 +1,5 @@
1
+ export * from './dto/create-financial-title.dto';
2
+ export * from './finance.controller';
1
3
  export * from './finance.module';
4
+ export * from './finance.service';
5
+