@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.
- package/dist/dto/create-cost-center.dto.d.ts +4 -0
- package/dist/dto/create-cost-center.dto.d.ts.map +1 -0
- package/dist/dto/create-cost-center.dto.js +24 -0
- package/dist/dto/create-cost-center.dto.js.map +1 -0
- package/dist/dto/create-finance-category.dto.d.ts +6 -0
- package/dist/dto/create-finance-category.dto.d.ts.map +1 -0
- package/dist/dto/create-finance-category.dto.js +37 -0
- package/dist/dto/create-finance-category.dto.js.map +1 -0
- package/dist/dto/create-period-close.dto.d.ts +7 -0
- package/dist/dto/create-period-close.dto.d.ts.map +1 -0
- package/dist/dto/create-period-close.dto.js +44 -0
- package/dist/dto/create-period-close.dto.js.map +1 -0
- package/dist/dto/move-finance-category.dto.d.ts +5 -0
- package/dist/dto/move-finance-category.dto.d.ts.map +1 -0
- package/dist/dto/move-finance-category.dto.js +32 -0
- package/dist/dto/move-finance-category.dto.js.map +1 -0
- package/dist/dto/update-cost-center.dto.d.ts +5 -0
- package/dist/dto/update-cost-center.dto.d.ts.map +1 -0
- package/dist/dto/update-cost-center.dto.js +32 -0
- package/dist/dto/update-cost-center.dto.js.map +1 -0
- package/dist/dto/update-finance-category.dto.d.ts +7 -0
- package/dist/dto/update-finance-category.dto.d.ts.map +1 -0
- package/dist/dto/update-finance-category.dto.js +46 -0
- package/dist/dto/update-finance-category.dto.js.map +1 -0
- package/dist/finance-audit-logs.controller.d.ts +13 -0
- package/dist/finance-audit-logs.controller.d.ts.map +1 -0
- package/dist/finance-audit-logs.controller.js +54 -0
- package/dist/finance-audit-logs.controller.js.map +1 -0
- package/dist/finance-categories.controller.d.ts +42 -0
- package/dist/finance-categories.controller.d.ts.map +1 -0
- package/dist/finance-categories.controller.js +84 -0
- package/dist/finance-categories.controller.js.map +1 -0
- package/dist/finance-cost-centers.controller.d.ts +32 -0
- package/dist/finance-cost-centers.controller.d.ts.map +1 -0
- package/dist/finance-cost-centers.controller.js +72 -0
- package/dist/finance-cost-centers.controller.js.map +1 -0
- package/dist/finance-installments.controller.d.ts +4 -0
- package/dist/finance-installments.controller.d.ts.map +1 -1
- package/dist/finance-period-close.controller.d.ts +27 -0
- package/dist/finance-period-close.controller.d.ts.map +1 -0
- package/dist/finance-period-close.controller.js +64 -0
- package/dist/finance-period-close.controller.js.map +1 -0
- package/dist/finance.module.d.ts.map +1 -1
- package/dist/finance.module.js +8 -0
- package/dist/finance.module.js.map +1 -1
- package/dist/finance.service.d.ts +119 -0
- package/dist/finance.service.d.ts.map +1 -1
- package/dist/finance.service.js +733 -36
- package/dist/finance.service.js.map +1 -1
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -0
- package/dist/index.js.map +1 -1
- package/hedhog/data/route.yaml +108 -0
- package/hedhog/frontend/app/accounts-payable/installments/page.tsx.ejs +76 -6
- package/hedhog/frontend/app/accounts-receivable/installments/page.tsx.ejs +76 -6
- package/hedhog/frontend/app/administration/audit-logs/page.tsx.ejs +309 -0
- package/hedhog/frontend/app/administration/categories/page.tsx.ejs +642 -0
- package/hedhog/frontend/app/administration/cost-centers/page.tsx.ejs +371 -0
- package/hedhog/frontend/app/administration/period-close/page.tsx.ejs +502 -0
- package/hedhog/frontend/messages/en.json +225 -0
- package/hedhog/frontend/messages/pt.json +225 -0
- package/package.json +5 -5
- package/src/dto/create-cost-center.dto.ts +9 -0
- package/src/dto/create-finance-category.dto.ts +21 -0
- package/src/dto/create-period-close.dto.ts +34 -0
- package/src/dto/move-finance-category.dto.ts +18 -0
- package/src/dto/update-cost-center.dto.ts +17 -0
- package/src/dto/update-finance-category.dto.ts +30 -0
- package/src/finance-audit-logs.controller.ts +30 -0
- package/src/finance-categories.controller.ts +52 -0
- package/src/finance-cost-centers.controller.ts +43 -0
- package/src/finance-period-close.controller.ts +34 -0
- package/src/finance.module.ts +8 -0
- package/src/finance.service.ts +1020 -29
- package/src/index.ts +4 -0
package/dist/finance.service.js
CHANGED
|
@@ -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.
|
|
37
|
-
: 'Extract the receivable form fields from the attached file.
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
51
|
-
const centroCustoId = await this.matchCostCenterId(
|
|
52
|
-
const metodo = this.normalizePaymentMethod(
|
|
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
|
|
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
|
|
89
|
+
categoriaNome,
|
|
61
90
|
centroCustoId,
|
|
62
|
-
centroCustoNome
|
|
63
|
-
competencia
|
|
64
|
-
vencimento
|
|
65
|
-
valor
|
|
91
|
+
centroCustoNome,
|
|
92
|
+
competencia,
|
|
93
|
+
vencimento,
|
|
94
|
+
valor,
|
|
66
95
|
metodo,
|
|
67
96
|
canal: metodo,
|
|
68
|
-
descricao
|
|
69
|
-
confidence
|
|
70
|
-
|
|
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
|
-
|
|
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
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
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,
|