@hed-hog/finance 0.0.232 → 0.0.233
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/dto/extract-financial-title-from-file.dto.d.ts +4 -0
- package/dist/dto/extract-financial-title-from-file.dto.d.ts.map +1 -0
- package/dist/dto/extract-financial-title-from-file.dto.js +24 -0
- package/dist/dto/extract-financial-title-from-file.dto.js.map +1 -0
- package/dist/finance-installments.controller.d.ts +39 -0
- package/dist/finance-installments.controller.d.ts.map +1 -1
- package/dist/finance-installments.controller.js +26 -0
- package/dist/finance-installments.controller.js.map +1 -1
- package/dist/finance.module.d.ts.map +1 -1
- package/dist/finance.module.js +3 -1
- package/dist/finance.module.js.map +1 -1
- package/dist/finance.service.d.ts +33 -1
- package/dist/finance.service.d.ts.map +1 -1
- package/dist/finance.service.js +213 -2
- package/dist/finance.service.js.map +1 -1
- package/hedhog/data/route.yaml +18 -0
- package/hedhog/frontend/app/accounts-payable/installments/page.tsx.ejs +95 -0
- package/hedhog/frontend/app/accounts-receivable/installments/page.tsx.ejs +205 -0
- package/package.json +5 -4
- package/src/dto/extract-financial-title-from-file.dto.ts +9 -0
- package/src/finance-installments.controller.ts +33 -7
- package/src/finance.module.ts +3 -1
- package/src/finance.service.ts +251 -3
|
@@ -100,6 +100,7 @@ function NovoTituloSheet({
|
|
|
100
100
|
const [uploadedFileId, setUploadedFileId] = useState<number | null>(null);
|
|
101
101
|
const [uploadedFileName, setUploadedFileName] = useState('');
|
|
102
102
|
const [isUploadingFile, setIsUploadingFile] = useState(false);
|
|
103
|
+
const [isExtractingFileData, setIsExtractingFileData] = useState(false);
|
|
103
104
|
const [uploadProgress, setUploadProgress] = useState(0);
|
|
104
105
|
|
|
105
106
|
const normalizeFilenameForDisplay = (filename: string) => {
|
|
@@ -215,6 +216,95 @@ function NovoTituloSheet({
|
|
|
215
216
|
);
|
|
216
217
|
setUploadProgress(100);
|
|
217
218
|
showToastHandler?.('success', 'Arquivo relacionado com sucesso');
|
|
219
|
+
|
|
220
|
+
setIsExtractingFileData(true);
|
|
221
|
+
try {
|
|
222
|
+
const extraction = await request<{
|
|
223
|
+
documento?: string | null;
|
|
224
|
+
fornecedorId?: string;
|
|
225
|
+
competencia?: string;
|
|
226
|
+
vencimento?: string;
|
|
227
|
+
valor?: number | null;
|
|
228
|
+
categoriaId?: string;
|
|
229
|
+
centroCustoId?: string;
|
|
230
|
+
metodo?: string;
|
|
231
|
+
descricao?: string | null;
|
|
232
|
+
}>({
|
|
233
|
+
url: '/finance/accounts-payable/installments/extract-from-file',
|
|
234
|
+
method: 'POST',
|
|
235
|
+
data: {
|
|
236
|
+
file_id: data.id,
|
|
237
|
+
},
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
const extracted = extraction.data || {};
|
|
241
|
+
|
|
242
|
+
if (extracted.documento) {
|
|
243
|
+
form.setValue('documento', extracted.documento, {
|
|
244
|
+
shouldValidate: true,
|
|
245
|
+
});
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
if (extracted.fornecedorId) {
|
|
249
|
+
form.setValue('fornecedorId', extracted.fornecedorId, {
|
|
250
|
+
shouldValidate: true,
|
|
251
|
+
});
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
if (extracted.competencia) {
|
|
255
|
+
form.setValue('competencia', extracted.competencia, {
|
|
256
|
+
shouldValidate: true,
|
|
257
|
+
});
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
if (extracted.vencimento) {
|
|
261
|
+
form.setValue('vencimento', extracted.vencimento, {
|
|
262
|
+
shouldValidate: true,
|
|
263
|
+
});
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
if (typeof extracted.valor === 'number' && extracted.valor > 0) {
|
|
267
|
+
form.setValue('valor', extracted.valor, {
|
|
268
|
+
shouldValidate: true,
|
|
269
|
+
});
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
if (extracted.categoriaId) {
|
|
273
|
+
form.setValue('categoriaId', extracted.categoriaId, {
|
|
274
|
+
shouldValidate: true,
|
|
275
|
+
});
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
if (extracted.centroCustoId) {
|
|
279
|
+
form.setValue('centroCustoId', extracted.centroCustoId, {
|
|
280
|
+
shouldValidate: true,
|
|
281
|
+
});
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
if (extracted.metodo) {
|
|
285
|
+
form.setValue('metodo', extracted.metodo, {
|
|
286
|
+
shouldValidate: true,
|
|
287
|
+
});
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
if (extracted.descricao) {
|
|
291
|
+
form.setValue('descricao', extracted.descricao, {
|
|
292
|
+
shouldValidate: true,
|
|
293
|
+
});
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
showToastHandler?.(
|
|
297
|
+
'success',
|
|
298
|
+
'Dados da fatura extraídos e preenchidos automaticamente'
|
|
299
|
+
);
|
|
300
|
+
} catch {
|
|
301
|
+
showToastHandler?.(
|
|
302
|
+
'error',
|
|
303
|
+
'Não foi possível extrair os dados automaticamente'
|
|
304
|
+
);
|
|
305
|
+
} finally {
|
|
306
|
+
setIsExtractingFileData(false);
|
|
307
|
+
}
|
|
218
308
|
} catch {
|
|
219
309
|
setUploadedFileId(null);
|
|
220
310
|
setUploadedFileName('');
|
|
@@ -299,6 +389,11 @@ function NovoTituloSheet({
|
|
|
299
389
|
Arquivo relacionado: {uploadedFileName}
|
|
300
390
|
</p>
|
|
301
391
|
)}
|
|
392
|
+
{isExtractingFileData && (
|
|
393
|
+
<p className="text-xs text-muted-foreground">
|
|
394
|
+
Extraindo dados da fatura com IA...
|
|
395
|
+
</p>
|
|
396
|
+
)}
|
|
302
397
|
</div>
|
|
303
398
|
|
|
304
399
|
<FormField
|
|
@@ -22,6 +22,7 @@ import {
|
|
|
22
22
|
import { Input } from '@/components/ui/input';
|
|
23
23
|
import { InputMoney } from '@/components/ui/input-money';
|
|
24
24
|
import { Money } from '@/components/ui/money';
|
|
25
|
+
import { Progress } from '@/components/ui/progress';
|
|
25
26
|
import {
|
|
26
27
|
Select,
|
|
27
28
|
SelectContent,
|
|
@@ -95,6 +96,29 @@ function NovoTituloSheet({
|
|
|
95
96
|
}) {
|
|
96
97
|
const { request, showToastHandler } = useApp();
|
|
97
98
|
const [open, setOpen] = useState(false);
|
|
99
|
+
const [uploadedFileId, setUploadedFileId] = useState<number | null>(null);
|
|
100
|
+
const [uploadedFileName, setUploadedFileName] = useState('');
|
|
101
|
+
const [isUploadingFile, setIsUploadingFile] = useState(false);
|
|
102
|
+
const [isExtractingFileData, setIsExtractingFileData] = useState(false);
|
|
103
|
+
const [uploadProgress, setUploadProgress] = useState(0);
|
|
104
|
+
|
|
105
|
+
const normalizeFilenameForDisplay = (filename: string) => {
|
|
106
|
+
if (!filename) {
|
|
107
|
+
return filename;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
if (!/Ã.|Â.|â[\u0080-\u00BF]/.test(filename)) {
|
|
111
|
+
return filename;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
try {
|
|
115
|
+
const bytes = Uint8Array.from(filename, (char) => char.charCodeAt(0));
|
|
116
|
+
const decoded = new TextDecoder('utf-8').decode(bytes);
|
|
117
|
+
return /Ã.|Â.|â[\u0080-\u00BF]/.test(decoded) ? filename : decoded;
|
|
118
|
+
} catch {
|
|
119
|
+
return filename;
|
|
120
|
+
}
|
|
121
|
+
};
|
|
98
122
|
|
|
99
123
|
const form = useForm<NewTitleFormValues>({
|
|
100
124
|
resolver: zodResolver(newTitleFormSchema),
|
|
@@ -132,11 +156,14 @@ function NovoTituloSheet({
|
|
|
132
156
|
: undefined,
|
|
133
157
|
payment_channel: values.canal || undefined,
|
|
134
158
|
description: values.descricao?.trim() || undefined,
|
|
159
|
+
attachment_file_ids: uploadedFileId ? [uploadedFileId] : undefined,
|
|
135
160
|
},
|
|
136
161
|
});
|
|
137
162
|
|
|
138
163
|
await onCreated();
|
|
139
164
|
form.reset();
|
|
165
|
+
setUploadedFileId(null);
|
|
166
|
+
setUploadedFileName('');
|
|
140
167
|
setOpen(false);
|
|
141
168
|
showToastHandler?.('success', 'Título criado com sucesso');
|
|
142
169
|
} catch {
|
|
@@ -146,9 +173,147 @@ function NovoTituloSheet({
|
|
|
146
173
|
|
|
147
174
|
const handleCancel = () => {
|
|
148
175
|
form.reset();
|
|
176
|
+
setUploadedFileId(null);
|
|
177
|
+
setUploadedFileName('');
|
|
178
|
+
setUploadProgress(0);
|
|
149
179
|
setOpen(false);
|
|
150
180
|
};
|
|
151
181
|
|
|
182
|
+
const uploadRelatedFile = async (file: File) => {
|
|
183
|
+
setIsUploadingFile(true);
|
|
184
|
+
setUploadProgress(0);
|
|
185
|
+
|
|
186
|
+
try {
|
|
187
|
+
const formData = new FormData();
|
|
188
|
+
formData.append('file', file);
|
|
189
|
+
formData.append('destination', 'finance/titles');
|
|
190
|
+
|
|
191
|
+
const { data } = await request<{ id: number; filename: string }>({
|
|
192
|
+
url: '/file',
|
|
193
|
+
method: 'POST',
|
|
194
|
+
data: formData,
|
|
195
|
+
headers: {
|
|
196
|
+
'Content-Type': 'multipart/form-data',
|
|
197
|
+
},
|
|
198
|
+
onUploadProgress: (event) => {
|
|
199
|
+
if (!event.total) {
|
|
200
|
+
return;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
const progress = Math.round((event.loaded * 100) / event.total);
|
|
204
|
+
setUploadProgress(progress);
|
|
205
|
+
},
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
if (!data?.id) {
|
|
209
|
+
throw new Error('Arquivo inválido');
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
setUploadedFileId(data.id);
|
|
213
|
+
setUploadedFileName(
|
|
214
|
+
normalizeFilenameForDisplay(data.filename || file.name)
|
|
215
|
+
);
|
|
216
|
+
setUploadProgress(100);
|
|
217
|
+
showToastHandler?.('success', 'Arquivo relacionado com sucesso');
|
|
218
|
+
|
|
219
|
+
setIsExtractingFileData(true);
|
|
220
|
+
try {
|
|
221
|
+
const extraction = await request<{
|
|
222
|
+
documento?: string | null;
|
|
223
|
+
clienteId?: string;
|
|
224
|
+
competencia?: string;
|
|
225
|
+
vencimento?: string;
|
|
226
|
+
valor?: number | null;
|
|
227
|
+
categoriaId?: string;
|
|
228
|
+
centroCustoId?: string;
|
|
229
|
+
canal?: string;
|
|
230
|
+
descricao?: string | null;
|
|
231
|
+
}>({
|
|
232
|
+
url: '/finance/accounts-receivable/installments/extract-from-file',
|
|
233
|
+
method: 'POST',
|
|
234
|
+
data: {
|
|
235
|
+
file_id: data.id,
|
|
236
|
+
},
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
const extracted = extraction.data || {};
|
|
240
|
+
|
|
241
|
+
if (extracted.documento) {
|
|
242
|
+
form.setValue('documento', extracted.documento, {
|
|
243
|
+
shouldValidate: true,
|
|
244
|
+
});
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
if (extracted.clienteId) {
|
|
248
|
+
form.setValue('clienteId', extracted.clienteId, {
|
|
249
|
+
shouldValidate: true,
|
|
250
|
+
});
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
if (extracted.competencia) {
|
|
254
|
+
form.setValue('competencia', extracted.competencia, {
|
|
255
|
+
shouldValidate: true,
|
|
256
|
+
});
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
if (extracted.vencimento) {
|
|
260
|
+
form.setValue('vencimento', extracted.vencimento, {
|
|
261
|
+
shouldValidate: true,
|
|
262
|
+
});
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
if (typeof extracted.valor === 'number' && extracted.valor > 0) {
|
|
266
|
+
form.setValue('valor', extracted.valor, {
|
|
267
|
+
shouldValidate: true,
|
|
268
|
+
});
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
if (extracted.categoriaId) {
|
|
272
|
+
form.setValue('categoriaId', extracted.categoriaId, {
|
|
273
|
+
shouldValidate: true,
|
|
274
|
+
});
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
if (extracted.centroCustoId) {
|
|
278
|
+
form.setValue('centroCustoId', extracted.centroCustoId, {
|
|
279
|
+
shouldValidate: true,
|
|
280
|
+
});
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
if (extracted.canal) {
|
|
284
|
+
form.setValue('canal', extracted.canal, {
|
|
285
|
+
shouldValidate: true,
|
|
286
|
+
});
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
if (extracted.descricao) {
|
|
290
|
+
form.setValue('descricao', extracted.descricao, {
|
|
291
|
+
shouldValidate: true,
|
|
292
|
+
});
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
showToastHandler?.(
|
|
296
|
+
'success',
|
|
297
|
+
'Dados da fatura extraídos e preenchidos automaticamente'
|
|
298
|
+
);
|
|
299
|
+
} catch {
|
|
300
|
+
showToastHandler?.(
|
|
301
|
+
'error',
|
|
302
|
+
'Não foi possível extrair os dados automaticamente'
|
|
303
|
+
);
|
|
304
|
+
} finally {
|
|
305
|
+
setIsExtractingFileData(false);
|
|
306
|
+
}
|
|
307
|
+
} catch {
|
|
308
|
+
setUploadedFileId(null);
|
|
309
|
+
setUploadedFileName('');
|
|
310
|
+
setUploadProgress(0);
|
|
311
|
+
showToastHandler?.('error', 'Não foi possível enviar o arquivo');
|
|
312
|
+
} finally {
|
|
313
|
+
setIsUploadingFile(false);
|
|
314
|
+
}
|
|
315
|
+
};
|
|
316
|
+
|
|
152
317
|
return (
|
|
153
318
|
<Sheet open={open} onOpenChange={setOpen}>
|
|
154
319
|
<SheetTrigger asChild>
|
|
@@ -165,6 +330,46 @@ function NovoTituloSheet({
|
|
|
165
330
|
<Form {...form}>
|
|
166
331
|
<form className="px-4" onSubmit={form.handleSubmit(handleSubmit)}>
|
|
167
332
|
<div className="grid gap-4">
|
|
333
|
+
<div className="grid gap-2">
|
|
334
|
+
<FormLabel>Arquivo da fatura (opcional)</FormLabel>
|
|
335
|
+
<div className="flex flex-col gap-2 sm:flex-row sm:items-center">
|
|
336
|
+
<Input
|
|
337
|
+
type="file"
|
|
338
|
+
accept=".pdf,.png,.jpg,.jpeg,.xml,.txt"
|
|
339
|
+
onChange={(event) => {
|
|
340
|
+
const file = event.target.files?.[0];
|
|
341
|
+
if (!file) {
|
|
342
|
+
return;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
setUploadedFileId(null);
|
|
346
|
+
setUploadedFileName('');
|
|
347
|
+
setUploadProgress(0);
|
|
348
|
+
void uploadRelatedFile(file);
|
|
349
|
+
}}
|
|
350
|
+
disabled={isUploadingFile || form.formState.isSubmitting}
|
|
351
|
+
/>
|
|
352
|
+
</div>
|
|
353
|
+
{isUploadingFile && (
|
|
354
|
+
<div className="space-y-1">
|
|
355
|
+
<Progress value={uploadProgress} className="h-2" />
|
|
356
|
+
<p className="text-xs text-muted-foreground">
|
|
357
|
+
Upload em andamento: {uploadProgress}%
|
|
358
|
+
</p>
|
|
359
|
+
</div>
|
|
360
|
+
)}
|
|
361
|
+
{uploadedFileId && (
|
|
362
|
+
<p className="text-xs text-muted-foreground">
|
|
363
|
+
Arquivo relacionado: {uploadedFileName}
|
|
364
|
+
</p>
|
|
365
|
+
)}
|
|
366
|
+
{isExtractingFileData && (
|
|
367
|
+
<p className="text-xs text-muted-foreground">
|
|
368
|
+
Extraindo dados da fatura com IA...
|
|
369
|
+
</p>
|
|
370
|
+
)}
|
|
371
|
+
</div>
|
|
372
|
+
|
|
168
373
|
<FormField
|
|
169
374
|
control={form.control}
|
|
170
375
|
name="documento"
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hed-hog/finance",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.233",
|
|
4
4
|
"main": "dist/index.js",
|
|
5
5
|
"types": "dist/index.d.ts",
|
|
6
6
|
"dependencies": {
|
|
@@ -10,12 +10,13 @@
|
|
|
10
10
|
"@nestjs/jwt": "^11",
|
|
11
11
|
"@nestjs/mapped-types": "*",
|
|
12
12
|
"@hed-hog/api-locale": "0.0.11",
|
|
13
|
-
"@hed-hog/api-prisma": "0.0.4",
|
|
14
13
|
"@hed-hog/api-pagination": "0.0.5",
|
|
14
|
+
"@hed-hog/api-prisma": "0.0.4",
|
|
15
15
|
"@hed-hog/api-types": "0.0.1",
|
|
16
|
+
"@hed-hog/tag": "0.0.233",
|
|
16
17
|
"@hed-hog/api": "0.0.3",
|
|
17
|
-
"@hed-hog/
|
|
18
|
-
"@hed-hog/
|
|
18
|
+
"@hed-hog/contact": "0.0.233",
|
|
19
|
+
"@hed-hog/core": "0.0.233"
|
|
19
20
|
},
|
|
20
21
|
"exports": {
|
|
21
22
|
".": {
|
|
@@ -2,15 +2,19 @@ import { Role, User } from '@hed-hog/api';
|
|
|
2
2
|
import { Locale } from '@hed-hog/api-locale';
|
|
3
3
|
import { Pagination } from '@hed-hog/api-pagination';
|
|
4
4
|
import {
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
5
|
+
Body,
|
|
6
|
+
Controller,
|
|
7
|
+
Get,
|
|
8
|
+
Param,
|
|
9
|
+
ParseIntPipe,
|
|
10
|
+
Post,
|
|
11
|
+
Query,
|
|
12
|
+
UploadedFile,
|
|
13
|
+
UseInterceptors,
|
|
12
14
|
} from '@nestjs/common';
|
|
15
|
+
import { FileInterceptor } from '@nestjs/platform-express';
|
|
13
16
|
import { CreateFinancialTitleDto } from './dto/create-financial-title.dto';
|
|
17
|
+
import { ExtractFinancialTitleFromFileDto } from './dto/extract-financial-title-from-file.dto';
|
|
14
18
|
import { FinanceService } from './finance.service';
|
|
15
19
|
|
|
16
20
|
@Role()
|
|
@@ -50,6 +54,15 @@ export class FinanceInstallmentsController {
|
|
|
50
54
|
);
|
|
51
55
|
}
|
|
52
56
|
|
|
57
|
+
@Post('accounts-payable/installments/extract-from-file')
|
|
58
|
+
@UseInterceptors(FileInterceptor('file'))
|
|
59
|
+
async extractAccountsPayableInfoFromFile(
|
|
60
|
+
@UploadedFile() file: MulterFile,
|
|
61
|
+
@Body() data: ExtractFinancialTitleFromFileDto,
|
|
62
|
+
) {
|
|
63
|
+
return this.financeService.getAgentExtractInfoFromFile(file, data.file_id);
|
|
64
|
+
}
|
|
65
|
+
|
|
53
66
|
@Get('accounts-receivable/installments')
|
|
54
67
|
async listAccountsReceivableInstallments(
|
|
55
68
|
@Pagination() paginationParams,
|
|
@@ -81,4 +94,17 @@ export class FinanceInstallmentsController {
|
|
|
81
94
|
user?.id,
|
|
82
95
|
);
|
|
83
96
|
}
|
|
97
|
+
|
|
98
|
+
@Post('accounts-receivable/installments/extract-from-file')
|
|
99
|
+
@UseInterceptors(FileInterceptor('file'))
|
|
100
|
+
async extractAccountsReceivableInfoFromFile(
|
|
101
|
+
@UploadedFile() file: MulterFile,
|
|
102
|
+
@Body() data: ExtractFinancialTitleFromFileDto,
|
|
103
|
+
) {
|
|
104
|
+
return this.financeService.getAgentExtractInfoFromFile(
|
|
105
|
+
file,
|
|
106
|
+
data.file_id,
|
|
107
|
+
'receivable',
|
|
108
|
+
);
|
|
109
|
+
}
|
|
84
110
|
}
|
package/src/finance.module.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { LocaleModule } from '@hed-hog/api-locale';
|
|
2
2
|
import { PaginationModule } from '@hed-hog/api-pagination';
|
|
3
3
|
import { PrismaModule } from '@hed-hog/api-prisma';
|
|
4
|
+
import { AiModule } from '@hed-hog/core';
|
|
4
5
|
import { forwardRef, Module } from '@nestjs/common';
|
|
5
6
|
import { ConfigModule } from '@nestjs/config';
|
|
6
7
|
import { FinanceBankAccountsController } from './finance-bank-accounts.controller';
|
|
@@ -14,7 +15,8 @@ import { FinanceService } from './finance.service';
|
|
|
14
15
|
ConfigModule.forRoot(),
|
|
15
16
|
forwardRef(() => PaginationModule),
|
|
16
17
|
forwardRef(() => PrismaModule),
|
|
17
|
-
forwardRef(() => LocaleModule)
|
|
18
|
+
forwardRef(() => LocaleModule),
|
|
19
|
+
forwardRef(() => AiModule),
|
|
18
20
|
],
|
|
19
21
|
controllers: [
|
|
20
22
|
FinanceDataController,
|