@hed-hog/finance 0.0.233 → 0.0.236

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 (76) hide show
  1. package/dist/dto/create-cost-center.dto.d.ts +4 -0
  2. package/dist/dto/create-cost-center.dto.d.ts.map +1 -0
  3. package/dist/dto/create-cost-center.dto.js +24 -0
  4. package/dist/dto/create-cost-center.dto.js.map +1 -0
  5. package/dist/dto/create-finance-category.dto.d.ts +6 -0
  6. package/dist/dto/create-finance-category.dto.d.ts.map +1 -0
  7. package/dist/dto/create-finance-category.dto.js +37 -0
  8. package/dist/dto/create-finance-category.dto.js.map +1 -0
  9. package/dist/dto/create-period-close.dto.d.ts +7 -0
  10. package/dist/dto/create-period-close.dto.d.ts.map +1 -0
  11. package/dist/dto/create-period-close.dto.js +44 -0
  12. package/dist/dto/create-period-close.dto.js.map +1 -0
  13. package/dist/dto/move-finance-category.dto.d.ts +5 -0
  14. package/dist/dto/move-finance-category.dto.d.ts.map +1 -0
  15. package/dist/dto/move-finance-category.dto.js +32 -0
  16. package/dist/dto/move-finance-category.dto.js.map +1 -0
  17. package/dist/dto/update-cost-center.dto.d.ts +5 -0
  18. package/dist/dto/update-cost-center.dto.d.ts.map +1 -0
  19. package/dist/dto/update-cost-center.dto.js +32 -0
  20. package/dist/dto/update-cost-center.dto.js.map +1 -0
  21. package/dist/dto/update-finance-category.dto.d.ts +7 -0
  22. package/dist/dto/update-finance-category.dto.d.ts.map +1 -0
  23. package/dist/dto/update-finance-category.dto.js +46 -0
  24. package/dist/dto/update-finance-category.dto.js.map +1 -0
  25. package/dist/finance-audit-logs.controller.d.ts +13 -0
  26. package/dist/finance-audit-logs.controller.d.ts.map +1 -0
  27. package/dist/finance-audit-logs.controller.js +54 -0
  28. package/dist/finance-audit-logs.controller.js.map +1 -0
  29. package/dist/finance-categories.controller.d.ts +42 -0
  30. package/dist/finance-categories.controller.d.ts.map +1 -0
  31. package/dist/finance-categories.controller.js +84 -0
  32. package/dist/finance-categories.controller.js.map +1 -0
  33. package/dist/finance-cost-centers.controller.d.ts +32 -0
  34. package/dist/finance-cost-centers.controller.d.ts.map +1 -0
  35. package/dist/finance-cost-centers.controller.js +72 -0
  36. package/dist/finance-cost-centers.controller.js.map +1 -0
  37. package/dist/finance-installments.controller.d.ts +4 -0
  38. package/dist/finance-installments.controller.d.ts.map +1 -1
  39. package/dist/finance-period-close.controller.d.ts +27 -0
  40. package/dist/finance-period-close.controller.d.ts.map +1 -0
  41. package/dist/finance-period-close.controller.js +64 -0
  42. package/dist/finance-period-close.controller.js.map +1 -0
  43. package/dist/finance.module.d.ts.map +1 -1
  44. package/dist/finance.module.js +8 -0
  45. package/dist/finance.module.js.map +1 -1
  46. package/dist/finance.service.d.ts +119 -0
  47. package/dist/finance.service.d.ts.map +1 -1
  48. package/dist/finance.service.js +733 -36
  49. package/dist/finance.service.js.map +1 -1
  50. package/dist/index.d.ts +4 -0
  51. package/dist/index.d.ts.map +1 -1
  52. package/dist/index.js +4 -0
  53. package/dist/index.js.map +1 -1
  54. package/hedhog/data/route.yaml +108 -0
  55. package/hedhog/frontend/app/accounts-payable/installments/page.tsx.ejs +76 -6
  56. package/hedhog/frontend/app/accounts-receivable/installments/page.tsx.ejs +76 -6
  57. package/hedhog/frontend/app/administration/audit-logs/page.tsx.ejs +309 -0
  58. package/hedhog/frontend/app/administration/categories/page.tsx.ejs +642 -0
  59. package/hedhog/frontend/app/administration/cost-centers/page.tsx.ejs +371 -0
  60. package/hedhog/frontend/app/administration/period-close/page.tsx.ejs +502 -0
  61. package/hedhog/frontend/messages/en.json +225 -0
  62. package/hedhog/frontend/messages/pt.json +225 -0
  63. package/package.json +5 -5
  64. package/src/dto/create-cost-center.dto.ts +9 -0
  65. package/src/dto/create-finance-category.dto.ts +21 -0
  66. package/src/dto/create-period-close.dto.ts +34 -0
  67. package/src/dto/move-finance-category.dto.ts +18 -0
  68. package/src/dto/update-cost-center.dto.ts +17 -0
  69. package/src/dto/update-finance-category.dto.ts +30 -0
  70. package/src/finance-audit-logs.controller.ts +30 -0
  71. package/src/finance-categories.controller.ts +52 -0
  72. package/src/finance-cost-centers.controller.ts +43 -0
  73. package/src/finance-period-close.controller.ts +34 -0
  74. package/src/finance.module.ts +8 -0
  75. package/src/finance.service.ts +1020 -29
  76. package/src/index.ts +4 -0
@@ -8,6 +8,7 @@ var __decorate = (this && this.__decorate) || function (decorators, target, key,
8
8
  var __metadata = (this && this.__metadata) || function (k, v) {
9
9
  if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
10
10
  };
11
+ var FinanceService_1;
11
12
  Object.defineProperty(exports, "__esModule", { value: true });
12
13
  exports.FinanceService = void 0;
13
14
  const api_locale_1 = require("@hed-hog/api-locale");
@@ -15,11 +16,12 @@ const api_pagination_1 = require("@hed-hog/api-pagination");
15
16
  const api_prisma_1 = require("@hed-hog/api-prisma");
16
17
  const core_1 = require("@hed-hog/core");
17
18
  const common_1 = require("@nestjs/common");
18
- let FinanceService = class FinanceService {
19
+ let FinanceService = FinanceService_1 = class FinanceService {
19
20
  constructor(prisma, paginationService, ai) {
20
21
  this.prisma = prisma;
21
22
  this.paginationService = paginationService;
22
23
  this.ai = ai;
24
+ this.logger = new common_1.Logger(FinanceService_1.name);
23
25
  }
24
26
  async getAgentExtractInfoFromFile(file, fileId, titleType = 'payable') {
25
27
  if (!file && !fileId) {
@@ -29,47 +31,282 @@ let FinanceService = class FinanceService {
29
31
  const slug = isPayable
30
32
  ? 'finance-payable-extractor'
31
33
  : 'finance-receivable-extractor';
34
+ const llmAttachmentDebug = await this.ai.debugAttachmentForLlm(file, fileId);
35
+ this.logger.warn(`[FINANCE-EXTRACT] start type=${titleType} fileId=${fileId || 'upload'} mode=${(llmAttachmentDebug === null || llmAttachmentDebug === void 0 ? void 0 : llmAttachmentDebug.mode) || 'none'} mime=${(llmAttachmentDebug === null || llmAttachmentDebug === void 0 ? void 0 : llmAttachmentDebug.mimeType) || 'n/a'} textLength=${(llmAttachmentDebug === null || llmAttachmentDebug === void 0 ? void 0 : llmAttachmentDebug.textLength) || 0}`);
32
36
  const schema = isPayable
33
37
  ? '{"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}'
34
38
  : '{"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}';
35
39
  const extractionMessage = isPayable
36
- ? 'Extract the payable form fields from the attached file. If uncertain, return null for that field. Response must be JSON only.'
37
- : 'Extract the receivable form fields from the attached file. If uncertain, return null for that field. Response must be JSON only.';
40
+ ? 'Extract the payable form fields from the attached file. IMPORTANT: prioritize Brazilian invoice formats (pt-BR). Monetary values may come as 1.900,00 or 1900.00. Always return numeric value in JSON (example: 1900). Prefer the field explicitly labeled as valor total / total da nota / total a pagar. Never guess; if uncertain, return null. Response must be JSON only.'
41
+ : 'Extract the receivable form fields from the attached file. IMPORTANT: prioritize Brazilian invoice formats (pt-BR). Monetary values may come as 1.900,00 or 1900.00. Always return numeric value in JSON (example: 1900). Prefer the field explicitly labeled as valor total / total da nota / total a receber. Never guess; if uncertain, return null. Response must be JSON only.';
38
42
  await this.ai.createAgent({
39
43
  slug,
40
44
  provider: 'openai',
41
- instructions: `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}.`,
45
+ model: 'gpt-4o',
46
+ instructions: `You are a financial OCR extraction assistant. Extract only ${titleType} title fields from invoice/bill files. Be strict with monetary values in pt-BR format: 1.900,00 means 1900.00. Never confuse punctuation separators. Prefer explicit labels in this order: "valor dos serviços" > "valor total da nota" > "valor líquido". For vencimento, use fields like "vencimento" or "data de vencimento" only; do not use data de emissão as vencimento. If confidence is low, return null. Always return valid JSON only, no markdown. Use this schema: ${schema}.`,
42
47
  });
43
48
  const result = await this.ai.chatWithAgent(slug, {
44
49
  message: extractionMessage,
45
50
  file_id: fileId,
46
51
  }, file);
47
52
  const parsed = this.parseAiJson((result === null || result === void 0 ? void 0 : result.content) || '{}');
48
- const personName = isPayable ? parsed.fornecedor_nome : parsed.cliente_nome;
53
+ this.logger.warn(`[FINANCE-EXTRACT] pass1 type=${titleType} hasDocumento=${!!this.toNullableString(parsed === null || parsed === void 0 ? void 0 : parsed.documento)} hasValor=${this.toNullableNumber(parsed === null || parsed === void 0 ? void 0 : parsed.valor) !== null} hasVencimento=${!!this.normalizeDate(parsed === null || parsed === void 0 ? void 0 : parsed.vencimento)}`);
54
+ const mergedParsed = await this.enhanceParsedExtraction(parsed, slug, isPayable, llmAttachmentDebug, fileId, file);
55
+ const personName = isPayable
56
+ ? mergedParsed.fornecedor_nome
57
+ : mergedParsed.cliente_nome;
49
58
  const personId = await this.matchPersonId(personName);
50
- const categoriaId = await this.matchCategoryId(parsed.categoria_nome);
51
- const centroCustoId = await this.matchCostCenterId(parsed.centro_custo_nome);
52
- const metodo = this.normalizePaymentMethod(parsed.metodo || parsed.canal);
59
+ const categoriaId = await this.matchCategoryId(mergedParsed.categoria_nome);
60
+ const centroCustoId = await this.matchCostCenterId(mergedParsed.centro_custo_nome);
61
+ const metodo = this.normalizePaymentMethod(mergedParsed.metodo || mergedParsed.canal);
62
+ const documento = this.toNullableString(mergedParsed.documento);
63
+ const categoriaNome = this.toNullableString(mergedParsed.categoria_nome);
64
+ const centroCustoNome = this.toNullableString(mergedParsed.centro_custo_nome);
65
+ const competencia = this.normalizeMonth(mergedParsed.competencia);
66
+ const vencimento = this.normalizeDate(mergedParsed.vencimento);
67
+ const valor = this.toNullableNumber(mergedParsed.valor);
68
+ const descricao = this.toNullableString(mergedParsed.descricao);
69
+ const confidence = this.resolveExtractionConfidence(this.toNullableNumber(mergedParsed.confidence), {
70
+ documento,
71
+ personId,
72
+ categoriaId,
73
+ centroCustoId,
74
+ vencimento,
75
+ valor,
76
+ });
77
+ const warnings = this.buildExtractionWarnings(confidence, {
78
+ valor,
79
+ vencimento,
80
+ });
81
+ this.logger.warn(`[FINANCE-EXTRACT] done type=${titleType} confidence=${confidence !== null && confidence !== void 0 ? confidence : 'null'} valor=${valor !== null && valor !== void 0 ? valor : 'null'} vencimento=${vencimento || 'null'} fallback=${(mergedParsed === null || mergedParsed === void 0 ? void 0 : mergedParsed.__fallbackSource) || 'none'}`);
53
82
  return {
54
- documento: this.toNullableString(parsed.documento),
83
+ documento,
55
84
  fornecedorId: isPayable ? personId : '',
56
85
  fornecedorNome: isPayable ? this.toNullableString(personName) : null,
57
86
  clienteId: !isPayable ? personId : '',
58
87
  clienteNome: !isPayable ? this.toNullableString(personName) : null,
59
88
  categoriaId,
60
- categoriaNome: this.toNullableString(parsed.categoria_nome),
89
+ categoriaNome,
61
90
  centroCustoId,
62
- centroCustoNome: this.toNullableString(parsed.centro_custo_nome),
63
- competencia: this.normalizeMonth(parsed.competencia),
64
- vencimento: this.normalizeDate(parsed.vencimento),
65
- valor: this.toNullableNumber(parsed.valor),
91
+ centroCustoNome,
92
+ competencia,
93
+ vencimento,
94
+ valor,
66
95
  metodo,
67
96
  canal: metodo,
68
- descricao: this.toNullableString(parsed.descricao),
69
- confidence: this.toNullableNumber(parsed.confidence),
70
- raw: parsed,
97
+ descricao,
98
+ confidence,
99
+ confidenceLevel: confidence === null ? null : confidence < 70 ? 'low' : 'high',
100
+ warnings,
101
+ raw: Object.assign(Object.assign({}, mergedParsed), { __llmAttachmentDebug: llmAttachmentDebug }),
71
102
  };
72
103
  }
104
+ async enhanceParsedExtraction(parsed, slug, isPayable, llmAttachmentDebug, fileId, file) {
105
+ var _a, _b, _c, _d, _e, _f;
106
+ const base = parsed || {};
107
+ const hasDocumento = !!this.toNullableString(base.documento);
108
+ const hasValor = this.toNullableNumber(base.valor) !== null;
109
+ const hasVencimento = !!this.normalizeDate(base.vencimento);
110
+ const hasPessoa = !!this.toNullableString(isPayable ? base.fornecedor_nome : base.cliente_nome);
111
+ if (hasDocumento || hasValor || hasVencimento || hasPessoa) {
112
+ this.logger.warn('[FINANCE-EXTRACT] pass2 skipped: pass1 already has key fields');
113
+ return base;
114
+ }
115
+ const fallbackSchema = isPayable
116
+ ? '{"documento":"string|null","fornecedor_nome":"string|null","vencimento":"YYYY-MM-DD|null","valor":number|null,"confidence":number|null}'
117
+ : '{"documento":"string|null","cliente_nome":"string|null","vencimento":"YYYY-MM-DD|null","valor":number|null,"confidence":number|null}';
118
+ const fallbackMessage = isPayable
119
+ ? `Second pass. Extract ONLY documento, fornecedor_nome, vencimento, valor. Prefer "valor dos serviços" then "valor total" then "valor líquido". Return JSON only with this schema: ${fallbackSchema}`
120
+ : `Second pass. Extract ONLY documento, cliente_nome, vencimento, valor. Prefer "valor dos serviços" then "valor total" then "valor líquido". Return JSON only with this schema: ${fallbackSchema}`;
121
+ const retry = await this.ai.chatWithAgent(slug, {
122
+ message: fallbackMessage,
123
+ file_id: fileId,
124
+ }, file);
125
+ const retryParsed = this.parseAiJson((retry === null || retry === void 0 ? void 0 : retry.content) || '{}');
126
+ this.logger.warn(`[FINANCE-EXTRACT] pass2 hasDocumento=${!!this.toNullableString(retryParsed === null || retryParsed === void 0 ? void 0 : retryParsed.documento)} hasValor=${this.toNullableNumber(retryParsed === null || retryParsed === void 0 ? void 0 : retryParsed.valor) !== null} hasVencimento=${!!this.normalizeDate(retryParsed === null || retryParsed === void 0 ? void 0 : retryParsed.vencimento)}`);
127
+ const mergedAfterRetry = Object.assign(Object.assign(Object.assign({}, retryParsed), base), { documento: this.toNullableString(base.documento)
128
+ ? base.documento
129
+ : retryParsed.documento, valor: this.toNullableNumber(base.valor) !== null ? base.valor : retryParsed.valor, vencimento: this.normalizeDate(base.vencimento)
130
+ ? base.vencimento
131
+ : retryParsed.vencimento, fornecedor_nome: isPayable
132
+ ? this.toNullableString(base.fornecedor_nome)
133
+ ? base.fornecedor_nome
134
+ : retryParsed.fornecedor_nome
135
+ : base.fornecedor_nome, cliente_nome: !isPayable
136
+ ? this.toNullableString(base.cliente_nome)
137
+ ? base.cliente_nome
138
+ : retryParsed.cliente_nome
139
+ : base.cliente_nome, confidence: this.toNullableNumber(base.confidence) !== null
140
+ ? base.confidence
141
+ : retryParsed.confidence });
142
+ const stillEmpty = !this.toNullableString(mergedAfterRetry.documento) &&
143
+ this.toNullableNumber(mergedAfterRetry.valor) === null &&
144
+ !this.normalizeDate(mergedAfterRetry.vencimento) &&
145
+ !this.toNullableString(isPayable
146
+ ? mergedAfterRetry.fornecedor_nome
147
+ : mergedAfterRetry.cliente_nome);
148
+ if (!stillEmpty) {
149
+ this.logger.warn('[FINANCE-EXTRACT] regex fallback skipped: pass2 produced data');
150
+ return mergedAfterRetry;
151
+ }
152
+ const rawText = await this.ai.extractAttachmentText(file, fileId);
153
+ const regexFallback = this.extractFinancialFieldsFromRawText(rawText, isPayable);
154
+ this.logger.warn(`[FINANCE-EXTRACT] regex fallback activated hasText=${!!rawText} hasDocumento=${!!this.toNullableString(regexFallback === null || regexFallback === void 0 ? void 0 : regexFallback.documento)} hasValor=${this.toNullableNumber(regexFallback === null || regexFallback === void 0 ? void 0 : regexFallback.valor) !== null} hasVencimento=${!!this.normalizeDate(regexFallback === null || regexFallback === void 0 ? void 0 : regexFallback.vencimento)}`);
155
+ const stillEmptyAfterRegex = !this.toNullableString(regexFallback.documento) &&
156
+ this.toNullableNumber(regexFallback.valor) === null &&
157
+ !this.normalizeDate(regexFallback.vencimento);
158
+ if (stillEmptyAfterRegex &&
159
+ (llmAttachmentDebug === null || llmAttachmentDebug === void 0 ? void 0 : llmAttachmentDebug.mode) === 'pdf-text' &&
160
+ Number((llmAttachmentDebug === null || llmAttachmentDebug === void 0 ? void 0 : llmAttachmentDebug.textLength) || 0) === 0) {
161
+ this.logger.warn('[FINANCE-EXTRACT] gemini fallback activated for scanned PDF (no text layer)');
162
+ const geminiSchema = isPayable
163
+ ? '{"documento":"string|null","fornecedor_nome":"string|null","vencimento":"YYYY-MM-DD|null","valor":number|null,"confidence":number|null}'
164
+ : '{"documento":"string|null","cliente_nome":"string|null","vencimento":"YYYY-MM-DD|null","valor":number|null,"confidence":number|null}';
165
+ const geminiPrompt = isPayable
166
+ ? `Extract documento, fornecedor_nome, vencimento and valor from this invoice PDF. For valor, prefer 'valor dos serviços', then 'valor total', then 'valor líquido'. Return JSON only using schema: ${geminiSchema}`
167
+ : `Extract documento, cliente_nome, vencimento and valor from this invoice PDF. For valor, prefer 'valor dos serviços', then 'valor total', then 'valor líquido'. Return JSON only using schema: ${geminiSchema}`;
168
+ const geminiModels = [
169
+ 'gemini-2.5-flash',
170
+ 'gemini-2.5-pro',
171
+ 'gemini-2.0-flash',
172
+ 'gemini-2.0-flash-lite',
173
+ 'gemini-1.5-flash',
174
+ 'gemini-1.5-pro',
175
+ 'gemini-1.5-flash-latest',
176
+ 'gemini-1.5-pro-latest',
177
+ ];
178
+ for (const model of geminiModels) {
179
+ try {
180
+ this.logger.warn(`[FINANCE-EXTRACT] gemini fallback trying model=${model}`);
181
+ const geminiResult = await this.ai.chat({
182
+ provider: 'gemini',
183
+ model,
184
+ message: geminiPrompt,
185
+ file_id: fileId,
186
+ }, file);
187
+ const geminiParsed = this.parseAiJson((geminiResult === null || geminiResult === void 0 ? void 0 : geminiResult.content) || '{}');
188
+ this.logger.warn(`[FINANCE-EXTRACT] gemini fallback result model=${model} hasDocumento=${!!this.toNullableString(geminiParsed === null || geminiParsed === void 0 ? void 0 : geminiParsed.documento)} hasValor=${this.toNullableNumber(geminiParsed === null || geminiParsed === void 0 ? void 0 : geminiParsed.valor) !== null} hasVencimento=${!!this.normalizeDate(geminiParsed === null || geminiParsed === void 0 ? void 0 : geminiParsed.vencimento)}`);
189
+ return Object.assign(Object.assign(Object.assign({}, geminiParsed), regexFallback), { documento: this.toNullableString(geminiParsed === null || geminiParsed === void 0 ? void 0 : geminiParsed.documento)
190
+ ? geminiParsed.documento
191
+ : regexFallback.documento, valor: this.toNullableNumber(geminiParsed === null || geminiParsed === void 0 ? void 0 : geminiParsed.valor) !== null
192
+ ? geminiParsed.valor
193
+ : regexFallback.valor, vencimento: this.normalizeDate(geminiParsed === null || geminiParsed === void 0 ? void 0 : geminiParsed.vencimento)
194
+ ? geminiParsed.vencimento
195
+ : regexFallback.vencimento, competencia: this.normalizeMonth(geminiParsed === null || geminiParsed === void 0 ? void 0 : geminiParsed.competencia)
196
+ ? geminiParsed.competencia
197
+ : regexFallback.competencia, fornecedor_nome: isPayable
198
+ ? this.toNullableString(geminiParsed === null || geminiParsed === void 0 ? void 0 : geminiParsed.fornecedor_nome)
199
+ ? geminiParsed.fornecedor_nome
200
+ : regexFallback.fornecedor_nome
201
+ : null, cliente_nome: !isPayable
202
+ ? this.toNullableString(geminiParsed === null || geminiParsed === void 0 ? void 0 : geminiParsed.cliente_nome)
203
+ ? geminiParsed.cliente_nome
204
+ : regexFallback.cliente_nome
205
+ : null, confidence: (_a = this.toNullableNumber(geminiParsed === null || geminiParsed === void 0 ? void 0 : geminiParsed.confidence)) !== null && _a !== void 0 ? _a : regexFallback.confidence, __fallbackSource: 'gemini-pdf-binary', __fallbackModel: model, __fallbackHasText: !!rawText });
206
+ }
207
+ catch (error) {
208
+ const statusCode = (_b = error === null || error === void 0 ? void 0 : error.response) === null || _b === void 0 ? void 0 : _b.status;
209
+ const message = (error === null || error === void 0 ? void 0 : error.message) || 'unknown error';
210
+ const apiError = ((_e = (_d = (_c = error === null || error === void 0 ? void 0 : error.response) === null || _c === void 0 ? void 0 : _c.data) === null || _d === void 0 ? void 0 : _d.error) === null || _e === void 0 ? void 0 : _e.message) ||
211
+ JSON.stringify(((_f = error === null || error === void 0 ? void 0 : error.response) === null || _f === void 0 ? void 0 : _f.data) || {});
212
+ this.logger.warn(`[FINANCE-EXTRACT] gemini fallback failed model=${model} status=${statusCode !== null && statusCode !== void 0 ? statusCode : 'n/a'} message=${message} apiError=${apiError}`);
213
+ }
214
+ }
215
+ }
216
+ return Object.assign(Object.assign(Object.assign({}, regexFallback), mergedAfterRetry), { documento: this.toNullableString(mergedAfterRetry.documento)
217
+ ? mergedAfterRetry.documento
218
+ : regexFallback.documento, valor: this.toNullableNumber(mergedAfterRetry.valor) !== null
219
+ ? mergedAfterRetry.valor
220
+ : regexFallback.valor, vencimento: this.normalizeDate(mergedAfterRetry.vencimento)
221
+ ? mergedAfterRetry.vencimento
222
+ : regexFallback.vencimento, competencia: this.normalizeMonth(mergedAfterRetry.competencia)
223
+ ? mergedAfterRetry.competencia
224
+ : regexFallback.competencia, fornecedor_nome: isPayable
225
+ ? this.toNullableString(mergedAfterRetry.fornecedor_nome)
226
+ ? mergedAfterRetry.fornecedor_nome
227
+ : regexFallback.fornecedor_nome
228
+ : mergedAfterRetry.fornecedor_nome, cliente_nome: !isPayable
229
+ ? this.toNullableString(mergedAfterRetry.cliente_nome)
230
+ ? mergedAfterRetry.cliente_nome
231
+ : regexFallback.cliente_nome
232
+ : mergedAfterRetry.cliente_nome, __fallbackSource: 'regex-pdf-text', __fallbackHasText: !!rawText });
233
+ }
234
+ extractFinancialFieldsFromRawText(text, isPayable) {
235
+ const source = text || '';
236
+ const documento = this.firstMatch(source, [
237
+ /\b(?:n[úu]mero\s+da\s+nota|nota\s+fiscal|nfs-?e|nf-?e|nf)\s*[:#-]?\s*([a-z0-9._\/-]{4,})/i,
238
+ ]) || null;
239
+ const emissao = this.firstMatch(source, [
240
+ /\b(?:data\s+de\s+emiss[aã]o|emiss[aã]o)\s*[:#-]?\s*(\d{2}\/\d{2}\/\d{4})/i,
241
+ ]);
242
+ const vencimento = this.firstMatch(source, [
243
+ /\b(?:data\s+de\s+vencimento|vencimento)\s*[:#-]?\s*(\d{2}\/\d{2}\/\d{4})/i,
244
+ ]);
245
+ const valorLabeled = this.firstMatch(source, [
246
+ /\bvalor\s+dos\s+servi[cç]os\b[^\d]{0,20}(?:r\$\s*)?([\d.,]+)/i,
247
+ /\bvalor\s+total\b[^\d]{0,20}(?:r\$\s*)?([\d.,]+)/i,
248
+ /\btotal\s+da\s+nota\b[^\d]{0,20}(?:r\$\s*)?([\d.,]+)/i,
249
+ /\bvalor\s+l[ií]quido\b[^\d]{0,20}(?:r\$\s*)?([\d.,]+)/i,
250
+ ]);
251
+ const fallbackAnyMoney = this.firstMatch(source, [
252
+ /(?:r\$\s*)([\d]{1,3}(?:\.[\d]{3})*,\d{2})/i,
253
+ ]);
254
+ const valor = this.toNullableNumber(valorLabeled || fallbackAnyMoney);
255
+ const personName = this.firstMatch(source, [
256
+ /\b(?:tomador|cliente|fornecedor|prestador)\b\s*[:#-]?\s*([^\n\r]{3,80})/i,
257
+ ]);
258
+ return {
259
+ documento,
260
+ fornecedor_nome: isPayable ? this.toNullableString(personName) : null,
261
+ cliente_nome: !isPayable ? this.toNullableString(personName) : null,
262
+ competencia: emissao ? this.normalizeMonth(emissao) : null,
263
+ vencimento: vencimento ? this.normalizeDate(vencimento) : null,
264
+ valor,
265
+ confidence: valor !== null ? 68 : 52,
266
+ };
267
+ }
268
+ firstMatch(text, patterns) {
269
+ for (const pattern of patterns) {
270
+ const match = text.match(pattern);
271
+ if (match === null || match === void 0 ? void 0 : match[1]) {
272
+ return match[1].trim();
273
+ }
274
+ }
275
+ return '';
276
+ }
277
+ resolveExtractionConfidence(aiConfidence, data) {
278
+ if (aiConfidence !== null) {
279
+ const normalized = aiConfidence <= 1 ? aiConfidence * 100 : aiConfidence;
280
+ return Math.max(0, Math.min(100, Math.round(normalized)));
281
+ }
282
+ let score = 35;
283
+ if (data.documento)
284
+ score += 15;
285
+ if (data.vencimento)
286
+ score += 10;
287
+ if (data.valor !== null && data.valor > 0)
288
+ score += 20;
289
+ if (data.personId)
290
+ score += 10;
291
+ if (data.categoriaId)
292
+ score += 5;
293
+ if (data.centroCustoId)
294
+ score += 5;
295
+ return Math.max(0, Math.min(100, score));
296
+ }
297
+ buildExtractionWarnings(confidence, data) {
298
+ const warnings = [];
299
+ if (confidence !== null && confidence < 70) {
300
+ warnings.push('Baixa confiança na extração. Revise os campos antes de salvar.');
301
+ }
302
+ if (data.valor === null || data.valor <= 0) {
303
+ warnings.push('Valor não identificado com segurança no documento.');
304
+ }
305
+ if (!data.vencimento) {
306
+ warnings.push('Data de vencimento não identificada com segurança.');
307
+ }
308
+ return warnings;
309
+ }
73
310
  parseAiJson(content) {
74
311
  const trimmed = (content || '').trim();
75
312
  if (!trimmed) {
@@ -226,7 +463,38 @@ let FinanceService = class FinanceService {
226
463
  toNullableNumber(value) {
227
464
  if (value === null || value === undefined || value === '')
228
465
  return null;
229
- const num = Number(value);
466
+ if (typeof value === 'number') {
467
+ return Number.isFinite(value) ? value : null;
468
+ }
469
+ const raw = String(value).trim();
470
+ if (!raw)
471
+ return null;
472
+ const cleaned = raw.replace(/[^\d,.-]/g, '');
473
+ if (!cleaned)
474
+ return null;
475
+ const lastComma = cleaned.lastIndexOf(',');
476
+ const lastDot = cleaned.lastIndexOf('.');
477
+ let normalized = cleaned;
478
+ if (lastComma >= 0 && lastDot >= 0) {
479
+ if (lastComma > lastDot) {
480
+ normalized = cleaned.replace(/\./g, '').replace(',', '.');
481
+ }
482
+ else {
483
+ normalized = cleaned.replace(/,/g, '');
484
+ }
485
+ }
486
+ else if (lastComma >= 0) {
487
+ normalized = cleaned.replace(/\./g, '').replace(',', '.');
488
+ }
489
+ else {
490
+ const dotParts = cleaned.split('.');
491
+ if (dotParts.length > 1 &&
492
+ dotParts[dotParts.length - 1].length === 3 &&
493
+ dotParts.length <= 4) {
494
+ normalized = cleaned.replace(/\./g, '');
495
+ }
496
+ }
497
+ const num = Number(normalized);
230
498
  return Number.isFinite(num) ? num : null;
231
499
  }
232
500
  async getData() {
@@ -304,6 +572,159 @@ let FinanceService = class FinanceService {
304
572
  });
305
573
  return bankAccounts.map((bankAccount) => this.mapBankAccountToFront(bankAccount));
306
574
  }
575
+ async listCostCenters() {
576
+ const costCenters = await this.prisma.cost_center.findMany({
577
+ orderBy: [{ code: 'asc' }, { name: 'asc' }],
578
+ });
579
+ return costCenters.map((costCenter) => this.mapCostCenterToFront(costCenter));
580
+ }
581
+ async listAuditLogs(paginationParams, filters) {
582
+ var _a;
583
+ const actorUserId = (filters === null || filters === void 0 ? void 0 : filters.actor_user_id)
584
+ ? Number.parseInt(filters.actor_user_id, 10)
585
+ : undefined;
586
+ const fromDate = (filters === null || filters === void 0 ? void 0 : filters.from) ? new Date(filters.from) : undefined;
587
+ const toDate = (filters === null || filters === void 0 ? void 0 : filters.to) ? new Date(filters.to) : undefined;
588
+ const where = Object.assign(Object.assign(Object.assign(Object.assign({}, ((filters === null || filters === void 0 ? void 0 : filters.action) ? { action: filters.action } : {})), ((filters === null || filters === void 0 ? void 0 : filters.entity_table) ? { entity_table: filters.entity_table } : {})), (Number.isNaN(actorUserId) || !actorUserId
589
+ ? {}
590
+ : { actor_user_id: actorUserId })), ((fromDate || toDate) &&
591
+ !(fromDate && Number.isNaN(fromDate.getTime())) &&
592
+ !(toDate && Number.isNaN(toDate.getTime()))
593
+ ? {
594
+ created_at: Object.assign(Object.assign({}, (fromDate ? { gte: fromDate } : {})), (toDate ? { lte: toDate } : {})),
595
+ }
596
+ : {}));
597
+ const search = (_a = filters === null || filters === void 0 ? void 0 : filters.search) === null || _a === void 0 ? void 0 : _a.trim();
598
+ if (search) {
599
+ where.OR = [
600
+ { action: { contains: search, mode: 'insensitive' } },
601
+ { entity_table: { contains: search, mode: 'insensitive' } },
602
+ { entity_id: { contains: search, mode: 'insensitive' } },
603
+ { summary: { contains: search, mode: 'insensitive' } },
604
+ { ip_address: { contains: search, mode: 'insensitive' } },
605
+ ];
606
+ }
607
+ const paginated = await this.paginationService.paginatePrismaModel(this.prisma.audit_log, {
608
+ page: paginationParams === null || paginationParams === void 0 ? void 0 : paginationParams.page,
609
+ pageSize: paginationParams === null || paginationParams === void 0 ? void 0 : paginationParams.pageSize,
610
+ sortField: (paginationParams === null || paginationParams === void 0 ? void 0 : paginationParams.sortField) || 'created_at',
611
+ sortOrder: (paginationParams === null || paginationParams === void 0 ? void 0 : paginationParams.sortOrder) || 'desc',
612
+ validSortFields: [
613
+ 'id',
614
+ 'created_at',
615
+ 'action',
616
+ 'entity_table',
617
+ 'entity_id',
618
+ ],
619
+ where,
620
+ include: {
621
+ user: {
622
+ select: {
623
+ id: true,
624
+ name: true,
625
+ },
626
+ },
627
+ },
628
+ });
629
+ return Object.assign(Object.assign({}, paginated), { data: (paginated.data || []).map((log) => {
630
+ var _a, _b, _c;
631
+ return ({
632
+ id: String(log.id),
633
+ actorUserId: log.actor_user_id ? String(log.actor_user_id) : null,
634
+ actorName: ((_a = log.user) === null || _a === void 0 ? void 0 : _a.name) || '-',
635
+ actorEmail: '',
636
+ action: log.action,
637
+ entityTable: log.entity_table,
638
+ entityId: log.entity_id,
639
+ summary: log.summary || '',
640
+ ipAddress: log.ip_address || '',
641
+ beforeData: log.before_data || '',
642
+ afterData: log.after_data || '',
643
+ createdAt: ((_c = (_b = log.created_at) === null || _b === void 0 ? void 0 : _b.toISOString) === null || _c === void 0 ? void 0 : _c.call(_b)) || null,
644
+ });
645
+ }) });
646
+ }
647
+ async listFinanceCategories() {
648
+ const categories = await this.prisma.finance_category.findMany({
649
+ orderBy: [{ parent_id: 'asc' }, { updated_at: 'asc' }, { name: 'asc' }],
650
+ });
651
+ return categories.map((category) => this.mapFinanceCategoryToFront(category));
652
+ }
653
+ async listPeriodClose(paginationParams, filters) {
654
+ var _a, _b;
655
+ const fromDate = (filters === null || filters === void 0 ? void 0 : filters.from) ? new Date(filters.from) : undefined;
656
+ const toDate = (filters === null || filters === void 0 ? void 0 : filters.to) ? new Date(filters.to) : undefined;
657
+ const userNumericId = (filters === null || filters === void 0 ? void 0 : filters.user)
658
+ ? Number.parseInt(filters.user, 10)
659
+ : undefined;
660
+ const andConditions = [];
661
+ if ((filters === null || filters === void 0 ? void 0 : filters.status) && filters.status !== 'all') {
662
+ andConditions.push({ status: filters.status });
663
+ }
664
+ if ((fromDate && !Number.isNaN(fromDate.getTime())) ||
665
+ (toDate && !Number.isNaN(toDate.getTime()))) {
666
+ andConditions.push({
667
+ period_start: Object.assign({}, (fromDate && !Number.isNaN(fromDate.getTime())
668
+ ? { gte: fromDate }
669
+ : {})),
670
+ });
671
+ andConditions.push({
672
+ period_end: Object.assign({}, (toDate && !Number.isNaN(toDate.getTime()) ? { lte: toDate } : {})),
673
+ });
674
+ }
675
+ const search = (_a = filters === null || filters === void 0 ? void 0 : filters.search) === null || _a === void 0 ? void 0 : _a.trim();
676
+ if (search) {
677
+ andConditions.push({
678
+ OR: [
679
+ { notes: { contains: search, mode: 'insensitive' } },
680
+ { user: { is: { name: { contains: search, mode: 'insensitive' } } } },
681
+ {
682
+ user: { is: { email: { contains: search, mode: 'insensitive' } } },
683
+ },
684
+ ],
685
+ });
686
+ }
687
+ if ((_b = filters === null || filters === void 0 ? void 0 : filters.user) === null || _b === void 0 ? void 0 : _b.trim()) {
688
+ andConditions.push({
689
+ OR: [
690
+ ...(Number.isNaN(userNumericId) || !userNumericId
691
+ ? []
692
+ : [{ closed_by_user_id: userNumericId }]),
693
+ { user: { is: { name: { contains: filters.user, mode: 'insensitive' } } } },
694
+ {
695
+ user: {
696
+ is: { email: { contains: filters.user, mode: 'insensitive' } },
697
+ },
698
+ },
699
+ ],
700
+ });
701
+ }
702
+ const where = andConditions.length > 0 ? { AND: andConditions } : {};
703
+ const paginated = await this.paginationService.paginatePrismaModel(this.prisma.period_close, {
704
+ page: paginationParams === null || paginationParams === void 0 ? void 0 : paginationParams.page,
705
+ pageSize: paginationParams === null || paginationParams === void 0 ? void 0 : paginationParams.pageSize,
706
+ sortField: (paginationParams === null || paginationParams === void 0 ? void 0 : paginationParams.sortField) || 'period_start',
707
+ sortOrder: (paginationParams === null || paginationParams === void 0 ? void 0 : paginationParams.sortOrder) || 'desc',
708
+ validSortFields: [
709
+ 'id',
710
+ 'period_start',
711
+ 'period_end',
712
+ 'status',
713
+ 'closed_at',
714
+ 'created_at',
715
+ ],
716
+ where,
717
+ include: {
718
+ user: {
719
+ select: {
720
+ id: true,
721
+ name: true,
722
+ },
723
+ },
724
+ },
725
+ });
726
+ return Object.assign(Object.assign({}, paginated), { data: (paginated.data || []).map((period) => this.mapPeriodCloseToFront(period)) });
727
+ }
307
728
  async listBankStatements(bankAccountId) {
308
729
  const statements = await this.prisma.bank_statement_line.findMany({
309
730
  where: Object.assign({}, (bankAccountId ? { bank_account_id: bankAccountId } : {})),
@@ -376,6 +797,74 @@ let FinanceService = class FinanceService {
376
797
  });
377
798
  return this.mapBankAccountToFront(account);
378
799
  }
800
+ async createCostCenter(data) {
801
+ const code = await this.generateCostCenterCode(data.name);
802
+ const created = await this.prisma.cost_center.create({
803
+ data: {
804
+ code,
805
+ name: data.name.trim(),
806
+ status: 'active',
807
+ },
808
+ });
809
+ return this.mapCostCenterToFront(created);
810
+ }
811
+ async createFinanceCategory(data) {
812
+ if (data.parent_id) {
813
+ await this.ensureFinanceCategoryExists(data.parent_id);
814
+ }
815
+ const created = await this.prisma.finance_category.create({
816
+ data: {
817
+ code: await this.generateFinanceCategoryCode(),
818
+ name: data.name.trim(),
819
+ kind: this.mapCategoryKindFromPt(data.kind),
820
+ status: 'active',
821
+ parent_id: data.parent_id || null,
822
+ },
823
+ });
824
+ return this.mapFinanceCategoryToFront(created);
825
+ }
826
+ async createPeriodClose(data, userId) {
827
+ var _a;
828
+ const periodStart = new Date(data.period_start);
829
+ const periodEnd = new Date(data.period_end);
830
+ if (Number.isNaN(periodStart.getTime()) ||
831
+ Number.isNaN(periodEnd.getTime())) {
832
+ throw new common_1.BadRequestException('Invalid period dates');
833
+ }
834
+ if (periodStart > periodEnd) {
835
+ throw new common_1.BadRequestException('period_start must be before period_end');
836
+ }
837
+ const overlapped = await this.prisma.period_close.findFirst({
838
+ where: {
839
+ period_start: { lte: periodEnd },
840
+ period_end: { gte: periodStart },
841
+ },
842
+ select: { id: true },
843
+ });
844
+ if (overlapped) {
845
+ throw new common_1.BadRequestException('There is already a period in this range');
846
+ }
847
+ const status = data.status || 'closed';
848
+ const created = await this.prisma.period_close.create({
849
+ data: {
850
+ period_start: periodStart,
851
+ period_end: periodEnd,
852
+ status,
853
+ closed_at: status === 'closed' ? new Date() : null,
854
+ closed_by_user_id: status === 'closed' ? userId || null : null,
855
+ notes: ((_a = data.notes) === null || _a === void 0 ? void 0 : _a.trim()) || null,
856
+ },
857
+ include: {
858
+ user: {
859
+ select: {
860
+ id: true,
861
+ name: true,
862
+ },
863
+ },
864
+ },
865
+ });
866
+ return this.mapPeriodCloseToFront(created);
867
+ }
379
868
  async updateBankAccount(id, data) {
380
869
  const current = await this.prisma.bank_account.findUnique({
381
870
  where: { id },
@@ -405,6 +894,82 @@ let FinanceService = class FinanceService {
405
894
  });
406
895
  return this.mapBankAccountToFront(updated);
407
896
  }
897
+ async updateCostCenter(id, data) {
898
+ var _a;
899
+ const current = await this.prisma.cost_center.findUnique({
900
+ where: { id },
901
+ select: { id: true },
902
+ });
903
+ if (!current) {
904
+ throw new common_1.NotFoundException('Cost center not found');
905
+ }
906
+ const updated = await this.prisma.cost_center.update({
907
+ where: { id },
908
+ data: {
909
+ name: (_a = data.name) === null || _a === void 0 ? void 0 : _a.trim(),
910
+ status: data.status,
911
+ },
912
+ });
913
+ return this.mapCostCenterToFront(updated);
914
+ }
915
+ async updateFinanceCategory(id, data) {
916
+ var _a;
917
+ const current = await this.prisma.finance_category.findUnique({
918
+ where: { id },
919
+ select: { id: true },
920
+ });
921
+ if (!current) {
922
+ throw new common_1.NotFoundException('Finance category not found');
923
+ }
924
+ if (data.parent_id) {
925
+ await this.ensureFinanceCategoryExists(data.parent_id);
926
+ await this.ensureNoFinanceCategoryCycle(id, data.parent_id);
927
+ }
928
+ const updated = await this.prisma.finance_category.update({
929
+ where: { id },
930
+ data: {
931
+ name: (_a = data.name) === null || _a === void 0 ? void 0 : _a.trim(),
932
+ kind: data.kind ? this.mapCategoryKindFromPt(data.kind) : undefined,
933
+ parent_id: data.parent_id,
934
+ status: data.status,
935
+ },
936
+ });
937
+ return this.mapFinanceCategoryToFront(updated);
938
+ }
939
+ async moveFinanceCategory(id, data) {
940
+ var _a;
941
+ const current = await this.prisma.finance_category.findUnique({
942
+ where: { id },
943
+ select: { id: true, parent_id: true },
944
+ });
945
+ if (!current) {
946
+ throw new common_1.NotFoundException('Finance category not found');
947
+ }
948
+ const targetParentId = data.parent_id || null;
949
+ if (targetParentId) {
950
+ await this.ensureFinanceCategoryExists(targetParentId);
951
+ await this.ensureNoFinanceCategoryCycle(id, targetParentId);
952
+ }
953
+ await this.prisma.finance_category.update({
954
+ where: { id },
955
+ data: { parent_id: targetParentId },
956
+ });
957
+ const siblings = await this.prisma.finance_category.findMany({
958
+ where: { parent_id: targetParentId },
959
+ select: { id: true },
960
+ orderBy: [{ updated_at: 'asc' }, { name: 'asc' }],
961
+ });
962
+ const siblingIds = siblings.map((item) => item.id).filter((item) => item !== id);
963
+ const targetPosition = Math.max(0, Math.min((_a = data.position) !== null && _a !== void 0 ? _a : siblingIds.length, siblingIds.length));
964
+ siblingIds.splice(targetPosition, 0, id);
965
+ await this.prisma.$transaction(siblingIds.map((categoryId) => this.prisma.finance_category.update({
966
+ where: { id: categoryId },
967
+ data: {
968
+ parent_id: targetParentId,
969
+ },
970
+ })));
971
+ return { success: true };
972
+ }
408
973
  async deleteBankAccount(id) {
409
974
  const current = await this.prisma.bank_account.findUnique({
410
975
  where: { id },
@@ -421,6 +986,38 @@ let FinanceService = class FinanceService {
421
986
  });
422
987
  return { success: true };
423
988
  }
989
+ async deleteCostCenter(id) {
990
+ const current = await this.prisma.cost_center.findUnique({
991
+ where: { id },
992
+ select: { id: true },
993
+ });
994
+ if (!current) {
995
+ throw new common_1.NotFoundException('Cost center not found');
996
+ }
997
+ await this.prisma.cost_center.update({
998
+ where: { id },
999
+ data: {
1000
+ status: 'inactive',
1001
+ },
1002
+ });
1003
+ return { success: true };
1004
+ }
1005
+ async deleteFinanceCategory(id) {
1006
+ const current = await this.prisma.finance_category.findUnique({
1007
+ where: { id },
1008
+ select: { id: true },
1009
+ });
1010
+ if (!current) {
1011
+ throw new common_1.NotFoundException('Finance category not found');
1012
+ }
1013
+ await this.prisma.finance_category.update({
1014
+ where: { id },
1015
+ data: {
1016
+ status: 'inactive',
1017
+ },
1018
+ });
1019
+ return { success: true };
1020
+ }
424
1021
  async listTitles(titleType, paginationParams, status) {
425
1022
  const prismaStatus = this.mapStatusFromPt(status);
426
1023
  const where = {
@@ -539,25 +1136,14 @@ let FinanceService = class FinanceService {
539
1136
  }
540
1137
  async loadPeople() {
541
1138
  const people = await this.prisma.person.findMany({
542
- include: {
543
- document: {
544
- select: {
545
- value: true,
546
- },
547
- take: 1,
548
- },
549
- },
550
1139
  orderBy: { name: 'asc' },
551
1140
  });
552
- return people.map((person) => {
553
- var _a;
554
- return ({
555
- id: String(person.id),
556
- nome: person.name,
557
- tipo: 'ambos',
558
- documento: ((_a = person.document[0]) === null || _a === void 0 ? void 0 : _a.value) || '',
559
- });
560
- });
1141
+ return people.map((person) => ({
1142
+ id: String(person.id),
1143
+ nome: person.name,
1144
+ tipo: 'ambos',
1145
+ documento: '',
1146
+ }));
561
1147
  }
562
1148
  async loadCategories() {
563
1149
  const categories = await this.prisma.finance_category.findMany({
@@ -779,6 +1365,70 @@ let FinanceService = class FinanceService {
779
1365
  ativo: bankAccount.status === 'active',
780
1366
  };
781
1367
  }
1368
+ mapCostCenterToFront(costCenter) {
1369
+ return {
1370
+ id: String(costCenter.id),
1371
+ codigo: costCenter.code,
1372
+ nome: costCenter.name,
1373
+ status: costCenter.status,
1374
+ ativo: costCenter.status === 'active',
1375
+ };
1376
+ }
1377
+ mapCategoryKindToPt(kind) {
1378
+ const kindMap = {
1379
+ revenue: 'receita',
1380
+ expense: 'despesa',
1381
+ transfer: 'transferencia',
1382
+ adjustment: 'ajuste',
1383
+ other: 'outro',
1384
+ };
1385
+ return kindMap[kind] || 'outro';
1386
+ }
1387
+ mapCategoryKindFromPt(kind) {
1388
+ const normalized = (kind || '').toLowerCase();
1389
+ const kindMap = {
1390
+ receita: 'revenue',
1391
+ revenue: 'revenue',
1392
+ despesa: 'expense',
1393
+ expense: 'expense',
1394
+ transferencia: 'transfer',
1395
+ transferência: 'transfer',
1396
+ transfer: 'transfer',
1397
+ ajuste: 'adjustment',
1398
+ adjustment: 'adjustment',
1399
+ outro: 'other',
1400
+ other: 'other',
1401
+ };
1402
+ return kindMap[normalized] || 'other';
1403
+ }
1404
+ mapFinanceCategoryToFront(category) {
1405
+ return {
1406
+ id: String(category.id),
1407
+ codigo: category.code,
1408
+ nome: category.name,
1409
+ parentId: category.parent_id ? String(category.parent_id) : null,
1410
+ natureza: this.mapCategoryKindToPt(category.kind),
1411
+ status: category.status,
1412
+ ativo: category.status === 'active',
1413
+ };
1414
+ }
1415
+ mapPeriodCloseToFront(period) {
1416
+ var _a, _b, _c, _d, _e, _f, _g, _h, _j;
1417
+ return {
1418
+ id: String(period.id),
1419
+ periodStart: ((_b = (_a = period.period_start) === null || _a === void 0 ? void 0 : _a.toISOString) === null || _b === void 0 ? void 0 : _b.call(_a)) || null,
1420
+ periodEnd: ((_d = (_c = period.period_end) === null || _c === void 0 ? void 0 : _c.toISOString) === null || _d === void 0 ? void 0 : _d.call(_c)) || null,
1421
+ status: period.status,
1422
+ closedAt: ((_f = (_e = period.closed_at) === null || _e === void 0 ? void 0 : _e.toISOString) === null || _f === void 0 ? void 0 : _f.call(_e)) || null,
1423
+ closedByUserId: period.closed_by_user_id
1424
+ ? String(period.closed_by_user_id)
1425
+ : null,
1426
+ closedByName: ((_g = period.user) === null || _g === void 0 ? void 0 : _g.name) || '-',
1427
+ closedByEmail: '',
1428
+ notes: period.notes || '',
1429
+ createdAt: ((_j = (_h = period.created_at) === null || _h === void 0 ? void 0 : _h.toISOString) === null || _j === void 0 ? void 0 : _j.call(_h)) || null,
1430
+ };
1431
+ }
782
1432
  mapStatementStatusToPt(status) {
783
1433
  const statusMap = {
784
1434
  pending: 'pendente',
@@ -799,6 +1449,53 @@ let FinanceService = class FinanceService {
799
1449
  .padStart(4, '0');
800
1450
  return `${bankPrefix || 'ACC'}-${accountSuffix}`;
801
1451
  }
1452
+ async generateCostCenterCode(name) {
1453
+ const sanitizedName = (name || 'CENTRO')
1454
+ .trim()
1455
+ .replace(/\s+/g, '-')
1456
+ .replace(/[^A-Za-z0-9-]/g, '')
1457
+ .toUpperCase();
1458
+ const basePrefix = sanitizedName.slice(0, 8) || 'CENTRO';
1459
+ const lastCostCenter = await this.prisma.cost_center.findFirst({
1460
+ orderBy: { id: 'desc' },
1461
+ select: { id: true },
1462
+ });
1463
+ const suffix = String(((lastCostCenter === null || lastCostCenter === void 0 ? void 0 : lastCostCenter.id) || 0) + 1).padStart(4, '0');
1464
+ return `${basePrefix}-${suffix}`;
1465
+ }
1466
+ async generateFinanceCategoryCode() {
1467
+ const lastCategory = await this.prisma.finance_category.findFirst({
1468
+ orderBy: { id: 'desc' },
1469
+ select: { id: true },
1470
+ });
1471
+ const suffix = String(((lastCategory === null || lastCategory === void 0 ? void 0 : lastCategory.id) || 0) + 1).padStart(4, '0');
1472
+ return `CAT-${suffix}`;
1473
+ }
1474
+ async ensureFinanceCategoryExists(id) {
1475
+ const category = await this.prisma.finance_category.findUnique({
1476
+ where: { id },
1477
+ select: { id: true },
1478
+ });
1479
+ if (!category) {
1480
+ throw new common_1.NotFoundException('Finance category not found');
1481
+ }
1482
+ }
1483
+ async ensureNoFinanceCategoryCycle(categoryId, targetParentId) {
1484
+ if (categoryId === targetParentId) {
1485
+ throw new common_1.BadRequestException('Category cannot be parent of itself');
1486
+ }
1487
+ let currentParentId = targetParentId;
1488
+ while (currentParentId) {
1489
+ if (currentParentId === categoryId) {
1490
+ throw new common_1.BadRequestException('Invalid parent category hierarchy');
1491
+ }
1492
+ const parent = await this.prisma.finance_category.findUnique({
1493
+ where: { id: currentParentId },
1494
+ select: { parent_id: true },
1495
+ });
1496
+ currentParentId = (parent === null || parent === void 0 ? void 0 : parent.parent_id) || null;
1497
+ }
1498
+ }
802
1499
  toCents(value) {
803
1500
  return Math.round(value * 100);
804
1501
  }
@@ -807,7 +1504,7 @@ let FinanceService = class FinanceService {
807
1504
  }
808
1505
  };
809
1506
  exports.FinanceService = FinanceService;
810
- exports.FinanceService = FinanceService = __decorate([
1507
+ exports.FinanceService = FinanceService = FinanceService_1 = __decorate([
811
1508
  (0, common_1.Injectable)(),
812
1509
  __metadata("design:paramtypes", [api_prisma_1.PrismaService,
813
1510
  api_pagination_1.PaginationService,