@hed-hog/finance 0.0.231 → 0.0.233

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 (29) hide show
  1. package/dist/dto/create-financial-title.dto.d.ts +1 -0
  2. package/dist/dto/create-financial-title.dto.d.ts.map +1 -1
  3. package/dist/dto/create-financial-title.dto.js +11 -0
  4. package/dist/dto/create-financial-title.dto.js.map +1 -1
  5. package/dist/dto/extract-financial-title-from-file.dto.d.ts +4 -0
  6. package/dist/dto/extract-financial-title-from-file.dto.d.ts.map +1 -0
  7. package/dist/dto/extract-financial-title-from-file.dto.js +24 -0
  8. package/dist/dto/extract-financial-title-from-file.dto.js.map +1 -0
  9. package/dist/finance-installments.controller.d.ts +39 -0
  10. package/dist/finance-installments.controller.d.ts.map +1 -1
  11. package/dist/finance-installments.controller.js +26 -0
  12. package/dist/finance-installments.controller.js.map +1 -1
  13. package/dist/finance.module.d.ts.map +1 -1
  14. package/dist/finance.module.js +3 -1
  15. package/dist/finance.module.js.map +1 -1
  16. package/dist/finance.service.d.ts +33 -1
  17. package/dist/finance.service.d.ts.map +1 -1
  18. package/dist/finance.service.js +213 -2
  19. package/dist/finance.service.js.map +1 -1
  20. package/hedhog/data/route.yaml +18 -0
  21. package/hedhog/frontend/app/accounts-payable/installments/page.tsx.ejs +234 -4
  22. package/hedhog/frontend/app/accounts-receivable/installments/page.tsx.ejs +205 -0
  23. package/hedhog/frontend/app/cash-and-banks/bank-accounts/page.tsx.ejs +78 -61
  24. package/package.json +6 -5
  25. package/src/dto/create-financial-title.dto.ts +12 -0
  26. package/src/dto/extract-financial-title-from-file.dto.ts +9 -0
  27. package/src/finance-installments.controller.ts +33 -7
  28. package/src/finance.module.ts +3 -1
  29. package/src/finance.service.ts +251 -3
@@ -1,10 +1,11 @@
1
1
  import { getLocaleText } from '@hed-hog/api-locale';
2
2
  import { PaginationDTO, PaginationService } from '@hed-hog/api-pagination';
3
3
  import { PrismaService } from '@hed-hog/api-prisma';
4
+ import { AiService } from '@hed-hog/core';
4
5
  import {
5
- BadRequestException,
6
- Injectable,
7
- NotFoundException,
6
+ BadRequestException,
7
+ Injectable,
8
+ NotFoundException
8
9
  } from '@nestjs/common';
9
10
  import { CreateBankAccountDto } from './dto/create-bank-account.dto';
10
11
  import { CreateFinancialTitleDto } from './dto/create-financial-title.dto';
@@ -17,8 +18,255 @@ export class FinanceService {
17
18
  constructor(
18
19
  private readonly prisma: PrismaService,
19
20
  private readonly paginationService: PaginationService,
21
+ private readonly ai: AiService,
20
22
  ) {}
21
23
 
24
+ async getAgentExtractInfoFromFile(
25
+ file?: MulterFile,
26
+ fileId?: number,
27
+ titleType: TitleType = 'payable',
28
+ ) {
29
+ if (!file && !fileId) {
30
+ throw new BadRequestException('File upload or file_id is required');
31
+ }
32
+
33
+ const isPayable = titleType === 'payable';
34
+ const slug = isPayable
35
+ ? 'finance-payable-extractor'
36
+ : 'finance-receivable-extractor';
37
+
38
+ const schema = isPayable
39
+ ? '{"documento":"string|null","fornecedor_nome":"string|null","categoria_nome":"string|null","centro_custo_nome":"string|null","competencia":"YYYY-MM|null","vencimento":"YYYY-MM-DD|null","valor":number|null,"metodo":"boleto|pix|transferencia|cartao|dinheiro|cheque|null","descricao":"string|null","confidence":number|null}'
40
+ : '{"documento":"string|null","cliente_nome":"string|null","categoria_nome":"string|null","centro_custo_nome":"string|null","competencia":"YYYY-MM|null","vencimento":"YYYY-MM-DD|null","valor":number|null,"canal":"boleto|pix|transferencia|cartao|null","descricao":"string|null","confidence":number|null}';
41
+
42
+ const extractionMessage = isPayable
43
+ ? 'Extract the payable form fields from the attached file. If uncertain, return null for that field. Response must be JSON only.'
44
+ : 'Extract the receivable form fields from the attached file. If uncertain, return null for that field. Response must be JSON only.';
45
+
46
+ await this.ai.createAgent({
47
+ slug,
48
+ provider: 'openai',
49
+ instructions:
50
+ `You are a financial OCR extraction assistant. Extract only ${titleType} title fields from invoice/bill files. Always return valid JSON only, no markdown. Use this schema: ${schema}.`,
51
+ });
52
+
53
+ const result = await this.ai.chatWithAgent(
54
+ slug,
55
+ {
56
+ message: extractionMessage,
57
+ file_id: fileId,
58
+ },
59
+ file,
60
+ );
61
+
62
+ const parsed = this.parseAiJson(result?.content || '{}');
63
+
64
+ const personName = isPayable ? parsed.fornecedor_nome : parsed.cliente_nome;
65
+ const personId = await this.matchPersonId(personName);
66
+ const categoriaId = await this.matchCategoryId(parsed.categoria_nome);
67
+ const centroCustoId = await this.matchCostCenterId(parsed.centro_custo_nome);
68
+
69
+ const metodo = this.normalizePaymentMethod(parsed.metodo || parsed.canal);
70
+
71
+ return {
72
+ documento: this.toNullableString(parsed.documento),
73
+ fornecedorId: isPayable ? personId : '',
74
+ fornecedorNome: isPayable ? this.toNullableString(personName) : null,
75
+ clienteId: !isPayable ? personId : '',
76
+ clienteNome: !isPayable ? this.toNullableString(personName) : null,
77
+ categoriaId,
78
+ categoriaNome: this.toNullableString(parsed.categoria_nome),
79
+ centroCustoId,
80
+ centroCustoNome: this.toNullableString(parsed.centro_custo_nome),
81
+ competencia: this.normalizeMonth(parsed.competencia),
82
+ vencimento: this.normalizeDate(parsed.vencimento),
83
+ valor: this.toNullableNumber(parsed.valor),
84
+ metodo,
85
+ canal: metodo,
86
+ descricao: this.toNullableString(parsed.descricao),
87
+ confidence: this.toNullableNumber(parsed.confidence),
88
+ raw: parsed,
89
+ };
90
+ }
91
+
92
+ private parseAiJson(content: string): any {
93
+ const trimmed = (content || '').trim();
94
+ if (!trimmed) {
95
+ return {};
96
+ }
97
+
98
+ try {
99
+ return JSON.parse(trimmed);
100
+ } catch {
101
+ const fenced = trimmed.match(/```(?:json)?\s*([\s\S]*?)```/i);
102
+ if (fenced?.[1]) {
103
+ try {
104
+ return JSON.parse(fenced[1].trim());
105
+ } catch {
106
+ return {};
107
+ }
108
+ }
109
+
110
+ const firstBrace = trimmed.indexOf('{');
111
+ const lastBrace = trimmed.lastIndexOf('}');
112
+ if (firstBrace >= 0 && lastBrace > firstBrace) {
113
+ try {
114
+ return JSON.parse(trimmed.slice(firstBrace, lastBrace + 1));
115
+ } catch {
116
+ return {};
117
+ }
118
+ }
119
+
120
+ return {};
121
+ }
122
+ }
123
+
124
+ private async matchPersonId(name?: string | null): Promise<string> {
125
+ if (!name?.trim()) return '';
126
+
127
+ const person = await this.prisma.person.findFirst({
128
+ where: {
129
+ name: {
130
+ contains: name.trim(),
131
+ mode: 'insensitive',
132
+ },
133
+ },
134
+ select: {
135
+ id: true,
136
+ },
137
+ orderBy: {
138
+ id: 'asc',
139
+ },
140
+ });
141
+
142
+ return person?.id ? String(person.id) : '';
143
+ }
144
+
145
+ private async matchCategoryId(name?: string | null): Promise<string> {
146
+ if (!name?.trim()) return '';
147
+
148
+ const category = await this.prisma.finance_category.findFirst({
149
+ where: {
150
+ name: {
151
+ contains: name.trim(),
152
+ mode: 'insensitive',
153
+ },
154
+ },
155
+ select: {
156
+ id: true,
157
+ },
158
+ orderBy: {
159
+ id: 'asc',
160
+ },
161
+ });
162
+
163
+ return category?.id ? String(category.id) : '';
164
+ }
165
+
166
+ private async matchCostCenterId(name?: string | null): Promise<string> {
167
+ if (!name?.trim()) return '';
168
+
169
+ const costCenter = await this.prisma.cost_center.findFirst({
170
+ where: {
171
+ name: {
172
+ contains: name.trim(),
173
+ mode: 'insensitive',
174
+ },
175
+ },
176
+ select: {
177
+ id: true,
178
+ },
179
+ orderBy: {
180
+ id: 'asc',
181
+ },
182
+ });
183
+
184
+ return costCenter?.id ? String(costCenter.id) : '';
185
+ }
186
+
187
+ private normalizeDate(value: any): string {
188
+ if (!value) return '';
189
+ const raw = String(value).trim();
190
+
191
+ if (/^\d{4}-\d{2}-\d{2}$/.test(raw)) {
192
+ return raw;
193
+ }
194
+
195
+ const br = raw.match(/^(\d{2})\/(\d{2})\/(\d{4})$/);
196
+ if (br) {
197
+ return `${br[3]}-${br[2]}-${br[1]}`;
198
+ }
199
+
200
+ const dt = new Date(raw);
201
+ if (Number.isNaN(dt.getTime())) {
202
+ return '';
203
+ }
204
+
205
+ return dt.toISOString().slice(0, 10);
206
+ }
207
+
208
+ private normalizeMonth(value: any): string {
209
+ if (!value) return '';
210
+ const raw = String(value).trim();
211
+
212
+ if (/^\d{4}-\d{2}$/.test(raw)) {
213
+ return raw;
214
+ }
215
+
216
+ if (/^\d{4}-\d{2}-\d{2}$/.test(raw)) {
217
+ return raw.slice(0, 7);
218
+ }
219
+
220
+ const br = raw.match(/^(\d{2})\/(\d{4})$/);
221
+ if (br) {
222
+ return `${br[2]}-${br[1]}`;
223
+ }
224
+
225
+ const dt = new Date(raw);
226
+ if (Number.isNaN(dt.getTime())) {
227
+ return '';
228
+ }
229
+
230
+ return dt.toISOString().slice(0, 7);
231
+ }
232
+
233
+ private normalizePaymentMethod(value: any): string {
234
+ if (!value) return '';
235
+
236
+ const normalized = String(value).trim().toLowerCase();
237
+
238
+ const map: Record<string, string> = {
239
+ boleto: 'boleto',
240
+ pix: 'pix',
241
+ transferencia: 'transferencia',
242
+ transfer: 'transferencia',
243
+ ted: 'transferencia',
244
+ doc: 'transferencia',
245
+ cartao: 'cartao',
246
+ card: 'cartao',
247
+ credito: 'cartao',
248
+ debito: 'cartao',
249
+ dinheiro: 'dinheiro',
250
+ cash: 'dinheiro',
251
+ cheque: 'cheque',
252
+ check: 'cheque',
253
+ };
254
+
255
+ return map[normalized] || '';
256
+ }
257
+
258
+ private toNullableString(value: any): string | null {
259
+ if (value === null || value === undefined) return null;
260
+ const text = String(value).trim();
261
+ return text ? text : null;
262
+ }
263
+
264
+ private toNullableNumber(value: any): number | null {
265
+ if (value === null || value === undefined || value === '') return null;
266
+ const num = Number(value);
267
+ return Number.isFinite(num) ? num : null;
268
+ }
269
+
22
270
  async getData() {
23
271
  const [
24
272
  payables,