@hed-hog/finance 0.0.239 → 0.0.244
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/README.md +1 -22
- package/dist/finance-installments.controller.d.ts +132 -0
- package/dist/finance-installments.controller.d.ts.map +1 -1
- package/dist/finance-installments.controller.js +52 -0
- package/dist/finance-installments.controller.js.map +1 -1
- package/dist/finance-statements.controller.d.ts +8 -0
- package/dist/finance-statements.controller.d.ts.map +1 -1
- package/dist/finance-statements.controller.js +40 -0
- package/dist/finance-statements.controller.js.map +1 -1
- package/dist/finance.module.d.ts.map +1 -1
- package/dist/finance.module.js +1 -0
- package/dist/finance.module.js.map +1 -1
- package/dist/finance.service.d.ts +160 -2
- package/dist/finance.service.d.ts.map +1 -1
- package/dist/finance.service.js +626 -8
- package/dist/finance.service.js.map +1 -1
- package/hedhog/data/route.yaml +54 -0
- package/hedhog/frontend/app/accounts-payable/installments/[id]/page.tsx.ejs +80 -4
- package/hedhog/frontend/app/accounts-payable/installments/page.tsx.ejs +736 -13
- package/hedhog/frontend/app/accounts-receivable/installments/[id]/page.tsx.ejs +1 -1
- package/hedhog/frontend/app/accounts-receivable/installments/page.tsx.ejs +1 -3
- package/hedhog/frontend/app/cash-and-banks/statements/page.tsx.ejs +212 -60
- package/hedhog/frontend/messages/en.json +1 -0
- package/hedhog/frontend/messages/pt.json +1 -0
- package/hedhog/query/constraints.sql +86 -0
- package/hedhog/table/bank_account.yaml +0 -8
- package/hedhog/table/financial_title.yaml +1 -9
- package/hedhog/table/settlement.yaml +0 -8
- package/package.json +6 -6
- package/src/finance-installments.controller.ts +70 -10
- package/src/finance-statements.controller.ts +61 -2
- package/src/finance.module.ts +2 -1
- package/src/finance.service.ts +868 -12
- package/hedhog/table/branch.yaml +0 -18
package/src/finance.service.ts
CHANGED
|
@@ -1,13 +1,17 @@
|
|
|
1
1
|
import { getLocaleText } from '@hed-hog/api-locale';
|
|
2
2
|
import { PaginationDTO, PaginationService } from '@hed-hog/api-pagination';
|
|
3
3
|
import { PrismaService } from '@hed-hog/api-prisma';
|
|
4
|
-
import { AiService } from '@hed-hog/core';
|
|
4
|
+
import { AiService, FileService } from '@hed-hog/core';
|
|
5
5
|
import {
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
6
|
+
BadRequestException,
|
|
7
|
+
forwardRef,
|
|
8
|
+
Inject,
|
|
9
|
+
Injectable,
|
|
10
|
+
Logger,
|
|
11
|
+
NotFoundException,
|
|
10
12
|
} from '@nestjs/common';
|
|
13
|
+
import { createHash } from 'node:crypto';
|
|
14
|
+
import { readFile } from 'node:fs/promises';
|
|
11
15
|
import { CreateBankAccountDto } from './dto/create-bank-account.dto';
|
|
12
16
|
import { CreateCostCenterDto } from './dto/create-cost-center.dto';
|
|
13
17
|
import { CreateFinanceCategoryDto } from './dto/create-finance-category.dto';
|
|
@@ -31,7 +35,6 @@ type InstallmentStatus =
|
|
|
31
35
|
| 'overdue';
|
|
32
36
|
type TitleStatus =
|
|
33
37
|
| 'draft'
|
|
34
|
-
| 'approved'
|
|
35
38
|
| 'open'
|
|
36
39
|
| 'partial'
|
|
37
40
|
| 'settled'
|
|
@@ -46,6 +49,8 @@ export class FinanceService {
|
|
|
46
49
|
private readonly prisma: PrismaService,
|
|
47
50
|
private readonly paginationService: PaginationService,
|
|
48
51
|
private readonly ai: AiService,
|
|
52
|
+
@Inject(forwardRef(() => FileService))
|
|
53
|
+
private readonly fileService: FileService,
|
|
49
54
|
) {}
|
|
50
55
|
|
|
51
56
|
async getAgentExtractInfoFromFile(
|
|
@@ -848,6 +853,15 @@ export class FinanceService {
|
|
|
848
853
|
return this.createTitle(data, 'payable', locale, userId);
|
|
849
854
|
}
|
|
850
855
|
|
|
856
|
+
async updateAccountsPayableTitle(
|
|
857
|
+
id: number,
|
|
858
|
+
data: CreateFinancialTitleDto,
|
|
859
|
+
locale: string,
|
|
860
|
+
userId?: number,
|
|
861
|
+
) {
|
|
862
|
+
return this.updateDraftTitle(id, data, 'payable', locale, userId);
|
|
863
|
+
}
|
|
864
|
+
|
|
851
865
|
async approveAccountsPayableTitle(id: number, locale: string, userId?: number) {
|
|
852
866
|
return this.approveTitle(id, 'payable', locale, userId);
|
|
853
867
|
}
|
|
@@ -861,6 +875,15 @@ export class FinanceService {
|
|
|
861
875
|
return this.rejectTitle(id, data, 'payable', locale, userId);
|
|
862
876
|
}
|
|
863
877
|
|
|
878
|
+
async cancelAccountsPayableTitle(
|
|
879
|
+
id: number,
|
|
880
|
+
data: RejectTitleDto,
|
|
881
|
+
locale: string,
|
|
882
|
+
userId?: number,
|
|
883
|
+
) {
|
|
884
|
+
return this.cancelTitle(id, data, 'payable', locale, userId);
|
|
885
|
+
}
|
|
886
|
+
|
|
864
887
|
async settleAccountsPayableInstallment(
|
|
865
888
|
id: number,
|
|
866
889
|
data: SettleInstallmentDto,
|
|
@@ -895,6 +918,15 @@ export class FinanceService {
|
|
|
895
918
|
return this.createTitle(data, 'receivable', locale, userId);
|
|
896
919
|
}
|
|
897
920
|
|
|
921
|
+
async updateAccountsReceivableTitle(
|
|
922
|
+
id: number,
|
|
923
|
+
data: CreateFinancialTitleDto,
|
|
924
|
+
locale: string,
|
|
925
|
+
userId?: number,
|
|
926
|
+
) {
|
|
927
|
+
return this.updateDraftTitle(id, data, 'receivable', locale, userId);
|
|
928
|
+
}
|
|
929
|
+
|
|
898
930
|
async approveAccountsReceivableTitle(
|
|
899
931
|
id: number,
|
|
900
932
|
locale: string,
|
|
@@ -903,6 +935,15 @@ export class FinanceService {
|
|
|
903
935
|
return this.approveTitle(id, 'receivable', locale, userId);
|
|
904
936
|
}
|
|
905
937
|
|
|
938
|
+
async cancelAccountsReceivableTitle(
|
|
939
|
+
id: number,
|
|
940
|
+
data: RejectTitleDto,
|
|
941
|
+
locale: string,
|
|
942
|
+
userId?: number,
|
|
943
|
+
) {
|
|
944
|
+
return this.cancelTitle(id, data, 'receivable', locale, userId);
|
|
945
|
+
}
|
|
946
|
+
|
|
906
947
|
async settleAccountsReceivableInstallment(
|
|
907
948
|
id: number,
|
|
908
949
|
data: SettleInstallmentDto,
|
|
@@ -1248,6 +1289,497 @@ export class FinanceService {
|
|
|
1248
1289
|
}));
|
|
1249
1290
|
}
|
|
1250
1291
|
|
|
1292
|
+
async exportBankStatementsCsv(bankAccountId: number) {
|
|
1293
|
+
const statements = await this.listBankStatements(bankAccountId);
|
|
1294
|
+
|
|
1295
|
+
const headers = [
|
|
1296
|
+
'id',
|
|
1297
|
+
'data',
|
|
1298
|
+
'descricao',
|
|
1299
|
+
'valor',
|
|
1300
|
+
'tipo',
|
|
1301
|
+
'status_conciliacao',
|
|
1302
|
+
'conta_bancaria_id',
|
|
1303
|
+
];
|
|
1304
|
+
|
|
1305
|
+
const lines = statements.map((statement) => {
|
|
1306
|
+
const columns = [
|
|
1307
|
+
statement.id,
|
|
1308
|
+
statement.data,
|
|
1309
|
+
statement.descricao,
|
|
1310
|
+
String(statement.valor),
|
|
1311
|
+
statement.tipo,
|
|
1312
|
+
statement.statusConciliacao,
|
|
1313
|
+
statement.contaBancariaId,
|
|
1314
|
+
];
|
|
1315
|
+
|
|
1316
|
+
return columns.map((column) => this.escapeCsvCell(column)).join(';');
|
|
1317
|
+
});
|
|
1318
|
+
|
|
1319
|
+
const csv = ['\uFEFF' + headers.join(';'), ...lines].join('\n');
|
|
1320
|
+
const fileName = `extrato-bancario-${bankAccountId}-${new Date()
|
|
1321
|
+
.toISOString()
|
|
1322
|
+
.slice(0, 10)}.csv`;
|
|
1323
|
+
|
|
1324
|
+
return {
|
|
1325
|
+
csv,
|
|
1326
|
+
fileName,
|
|
1327
|
+
};
|
|
1328
|
+
}
|
|
1329
|
+
|
|
1330
|
+
async importBankStatements(
|
|
1331
|
+
bankAccountId: number,
|
|
1332
|
+
file: MulterFile,
|
|
1333
|
+
locale: string,
|
|
1334
|
+
userId?: number,
|
|
1335
|
+
) {
|
|
1336
|
+
if (!file) {
|
|
1337
|
+
throw new BadRequestException('File is required');
|
|
1338
|
+
}
|
|
1339
|
+
|
|
1340
|
+
const bankAccount = await this.prisma.bank_account.findUnique({
|
|
1341
|
+
where: { id: bankAccountId },
|
|
1342
|
+
select: { id: true },
|
|
1343
|
+
});
|
|
1344
|
+
|
|
1345
|
+
if (!bankAccount) {
|
|
1346
|
+
throw new NotFoundException(
|
|
1347
|
+
getLocaleText('itemNotFound', locale, 'Bank account not found').replace(
|
|
1348
|
+
'{{item}}',
|
|
1349
|
+
'Bank account',
|
|
1350
|
+
),
|
|
1351
|
+
);
|
|
1352
|
+
}
|
|
1353
|
+
|
|
1354
|
+
const sourceType = this.detectStatementSourceType(file);
|
|
1355
|
+
const uploadedFile = await this.fileService.upload('finance/statements', file);
|
|
1356
|
+
const rawContent = await this.getUploadedFileText(file);
|
|
1357
|
+
const parsedEntries =
|
|
1358
|
+
sourceType === 'ofx'
|
|
1359
|
+
? this.parseOfxStatements(rawContent)
|
|
1360
|
+
: this.parseCsvStatements(rawContent);
|
|
1361
|
+
|
|
1362
|
+
if (parsedEntries.length === 0) {
|
|
1363
|
+
throw new BadRequestException(
|
|
1364
|
+
'No valid statement rows were found in the uploaded file',
|
|
1365
|
+
);
|
|
1366
|
+
}
|
|
1367
|
+
|
|
1368
|
+
const normalizedEntries = parsedEntries
|
|
1369
|
+
.filter((entry) => entry.postedDate && Number.isFinite(entry.amount))
|
|
1370
|
+
.map((entry, index) => {
|
|
1371
|
+
const postedDate = new Date(entry.postedDate);
|
|
1372
|
+
|
|
1373
|
+
if (Number.isNaN(postedDate.getTime())) {
|
|
1374
|
+
return null;
|
|
1375
|
+
}
|
|
1376
|
+
|
|
1377
|
+
const amountCents = this.toCents(entry.amount);
|
|
1378
|
+
|
|
1379
|
+
if (!Number.isFinite(amountCents) || amountCents === 0) {
|
|
1380
|
+
return null;
|
|
1381
|
+
}
|
|
1382
|
+
|
|
1383
|
+
const dedupeBase = [
|
|
1384
|
+
bankAccountId,
|
|
1385
|
+
postedDate.toISOString().slice(0, 10),
|
|
1386
|
+
amountCents,
|
|
1387
|
+
entry.externalId || '',
|
|
1388
|
+
entry.description || '',
|
|
1389
|
+
index,
|
|
1390
|
+
].join('|');
|
|
1391
|
+
|
|
1392
|
+
return {
|
|
1393
|
+
postedDate,
|
|
1394
|
+
amountCents,
|
|
1395
|
+
externalId: entry.externalId || null,
|
|
1396
|
+
description: (entry.description || 'Lançamento importado').slice(
|
|
1397
|
+
0,
|
|
1398
|
+
1000,
|
|
1399
|
+
),
|
|
1400
|
+
dedupeKey: this.createShortHash(`statement-line:${dedupeBase}`, 64),
|
|
1401
|
+
};
|
|
1402
|
+
})
|
|
1403
|
+
.filter(Boolean) as Array<{
|
|
1404
|
+
postedDate: Date;
|
|
1405
|
+
amountCents: number;
|
|
1406
|
+
externalId: string | null;
|
|
1407
|
+
description: string;
|
|
1408
|
+
dedupeKey: string;
|
|
1409
|
+
}>;
|
|
1410
|
+
|
|
1411
|
+
if (normalizedEntries.length === 0) {
|
|
1412
|
+
throw new BadRequestException('No valid financial entries were parsed');
|
|
1413
|
+
}
|
|
1414
|
+
|
|
1415
|
+
const statementFingerprint = this.createShortHash(
|
|
1416
|
+
[
|
|
1417
|
+
bankAccountId,
|
|
1418
|
+
sourceType,
|
|
1419
|
+
uploadedFile.id,
|
|
1420
|
+
file.originalname,
|
|
1421
|
+
file.size,
|
|
1422
|
+
normalizedEntries.length,
|
|
1423
|
+
].join('|'),
|
|
1424
|
+
24,
|
|
1425
|
+
);
|
|
1426
|
+
|
|
1427
|
+
const statement = await this.prisma.bank_statement.create({
|
|
1428
|
+
data: {
|
|
1429
|
+
bank_account_id: bankAccountId,
|
|
1430
|
+
source_type: sourceType,
|
|
1431
|
+
idempotency_key: `import-${uploadedFile.id}-${statementFingerprint}`,
|
|
1432
|
+
period_start: normalizedEntries
|
|
1433
|
+
.map((entry) => entry.postedDate)
|
|
1434
|
+
.sort((a, b) => a.getTime() - b.getTime())[0],
|
|
1435
|
+
period_end: normalizedEntries
|
|
1436
|
+
.map((entry) => entry.postedDate)
|
|
1437
|
+
.sort((a, b) => b.getTime() - a.getTime())[0],
|
|
1438
|
+
imported_at: new Date(),
|
|
1439
|
+
imported_by_user_id: userId || null,
|
|
1440
|
+
},
|
|
1441
|
+
select: { id: true },
|
|
1442
|
+
});
|
|
1443
|
+
|
|
1444
|
+
await this.prisma.bank_statement_line.createMany({
|
|
1445
|
+
data: normalizedEntries.map((entry) => ({
|
|
1446
|
+
bank_statement_id: statement.id,
|
|
1447
|
+
bank_account_id: bankAccountId,
|
|
1448
|
+
external_id: entry.externalId,
|
|
1449
|
+
posted_date: entry.postedDate,
|
|
1450
|
+
amount_cents: entry.amountCents,
|
|
1451
|
+
description: entry.description,
|
|
1452
|
+
status: 'imported',
|
|
1453
|
+
dedupe_key: entry.dedupeKey,
|
|
1454
|
+
})),
|
|
1455
|
+
skipDuplicates: true,
|
|
1456
|
+
});
|
|
1457
|
+
|
|
1458
|
+
return {
|
|
1459
|
+
statementId: String(statement.id),
|
|
1460
|
+
fileId: String(uploadedFile.id),
|
|
1461
|
+
importedRows: normalizedEntries.length,
|
|
1462
|
+
sourceType,
|
|
1463
|
+
};
|
|
1464
|
+
}
|
|
1465
|
+
|
|
1466
|
+
private detectStatementSourceType(file: MulterFile): 'csv' | 'ofx' {
|
|
1467
|
+
const filename = (file.originalname || '').toLowerCase();
|
|
1468
|
+
const mimetype = (file.mimetype || '').toLowerCase();
|
|
1469
|
+
|
|
1470
|
+
if (
|
|
1471
|
+
filename.endsWith('.ofx') ||
|
|
1472
|
+
mimetype.includes('ofx') ||
|
|
1473
|
+
mimetype.includes('x-ofx')
|
|
1474
|
+
) {
|
|
1475
|
+
return 'ofx';
|
|
1476
|
+
}
|
|
1477
|
+
|
|
1478
|
+
if (
|
|
1479
|
+
filename.endsWith('.csv') ||
|
|
1480
|
+
mimetype.includes('csv') ||
|
|
1481
|
+
mimetype.includes('comma-separated-values')
|
|
1482
|
+
) {
|
|
1483
|
+
return 'csv';
|
|
1484
|
+
}
|
|
1485
|
+
|
|
1486
|
+
throw new BadRequestException('Only CSV and OFX files are supported');
|
|
1487
|
+
}
|
|
1488
|
+
|
|
1489
|
+
private async getUploadedFileText(file: MulterFile): Promise<string> {
|
|
1490
|
+
let buffer = file.buffer;
|
|
1491
|
+
|
|
1492
|
+
if (!buffer && file.path) {
|
|
1493
|
+
buffer = await readFile(file.path);
|
|
1494
|
+
}
|
|
1495
|
+
|
|
1496
|
+
if (!buffer || buffer.length === 0) {
|
|
1497
|
+
throw new BadRequestException('Uploaded file is empty');
|
|
1498
|
+
}
|
|
1499
|
+
|
|
1500
|
+
const utf8 = buffer.toString('utf8');
|
|
1501
|
+
if (utf8.includes('�')) {
|
|
1502
|
+
return buffer.toString('latin1');
|
|
1503
|
+
}
|
|
1504
|
+
|
|
1505
|
+
return utf8;
|
|
1506
|
+
}
|
|
1507
|
+
|
|
1508
|
+
private parseCsvStatements(content: string) {
|
|
1509
|
+
const normalizedContent = content.replace(/^\uFEFF/, '').trim();
|
|
1510
|
+
|
|
1511
|
+
if (!normalizedContent) {
|
|
1512
|
+
return [];
|
|
1513
|
+
}
|
|
1514
|
+
|
|
1515
|
+
const rows = normalizedContent
|
|
1516
|
+
.split(/\r?\n/)
|
|
1517
|
+
.map((line) => line.trim())
|
|
1518
|
+
.filter((line) => !!line);
|
|
1519
|
+
|
|
1520
|
+
if (rows.length === 0) {
|
|
1521
|
+
return [];
|
|
1522
|
+
}
|
|
1523
|
+
|
|
1524
|
+
const delimiter = this.detectCsvDelimiter(rows[0]);
|
|
1525
|
+
const headerCells = this.parseCsvLine(rows[0], delimiter);
|
|
1526
|
+
const normalizedHeaders = headerCells.map((cell) =>
|
|
1527
|
+
this.normalizeCsvHeader(cell),
|
|
1528
|
+
);
|
|
1529
|
+
|
|
1530
|
+
const headerIndexes = {
|
|
1531
|
+
date: this.findFirstHeaderIndex(normalizedHeaders, [
|
|
1532
|
+
'data',
|
|
1533
|
+
'dt',
|
|
1534
|
+
'posteddate',
|
|
1535
|
+
'lancamento',
|
|
1536
|
+
'movimento',
|
|
1537
|
+
]),
|
|
1538
|
+
description: this.findFirstHeaderIndex(normalizedHeaders, [
|
|
1539
|
+
'descricao',
|
|
1540
|
+
'historico',
|
|
1541
|
+
'memo',
|
|
1542
|
+
'complemento',
|
|
1543
|
+
]),
|
|
1544
|
+
amount: this.findFirstHeaderIndex(normalizedHeaders, [
|
|
1545
|
+
'valor',
|
|
1546
|
+
'amount',
|
|
1547
|
+
'montante',
|
|
1548
|
+
]),
|
|
1549
|
+
debit: this.findFirstHeaderIndex(normalizedHeaders, ['debito', 'debit']),
|
|
1550
|
+
credit: this.findFirstHeaderIndex(normalizedHeaders, [
|
|
1551
|
+
'credito',
|
|
1552
|
+
'credit',
|
|
1553
|
+
]),
|
|
1554
|
+
externalId: this.findFirstHeaderIndex(normalizedHeaders, [
|
|
1555
|
+
'id',
|
|
1556
|
+
'documento',
|
|
1557
|
+
'numero',
|
|
1558
|
+
'fitid',
|
|
1559
|
+
]),
|
|
1560
|
+
};
|
|
1561
|
+
|
|
1562
|
+
const hasHeader =
|
|
1563
|
+
headerIndexes.date >= 0 ||
|
|
1564
|
+
headerIndexes.description >= 0 ||
|
|
1565
|
+
headerIndexes.amount >= 0 ||
|
|
1566
|
+
headerIndexes.debit >= 0 ||
|
|
1567
|
+
headerIndexes.credit >= 0;
|
|
1568
|
+
|
|
1569
|
+
const dataRows = hasHeader ? rows.slice(1) : rows;
|
|
1570
|
+
|
|
1571
|
+
return dataRows
|
|
1572
|
+
.map((line) => this.parseCsvLine(line, delimiter))
|
|
1573
|
+
.map((columns) => {
|
|
1574
|
+
const dateRaw =
|
|
1575
|
+
(headerIndexes.date >= 0
|
|
1576
|
+
? columns[headerIndexes.date]
|
|
1577
|
+
: columns[0]) || '';
|
|
1578
|
+
const descriptionRaw =
|
|
1579
|
+
(headerIndexes.description >= 0
|
|
1580
|
+
? columns[headerIndexes.description]
|
|
1581
|
+
: columns[1]) || '';
|
|
1582
|
+
const amountRaw =
|
|
1583
|
+
headerIndexes.amount >= 0 ? columns[headerIndexes.amount] : '';
|
|
1584
|
+
const debitRaw =
|
|
1585
|
+
headerIndexes.debit >= 0 ? columns[headerIndexes.debit] : '';
|
|
1586
|
+
const creditRaw =
|
|
1587
|
+
headerIndexes.credit >= 0 ? columns[headerIndexes.credit] : '';
|
|
1588
|
+
const fallbackAmountRaw = columns[2] || '';
|
|
1589
|
+
const externalIdRaw =
|
|
1590
|
+
headerIndexes.externalId >= 0 ? columns[headerIndexes.externalId] : '';
|
|
1591
|
+
|
|
1592
|
+
const postedDate = this.parseStatementDate(dateRaw);
|
|
1593
|
+
if (!postedDate) {
|
|
1594
|
+
return null;
|
|
1595
|
+
}
|
|
1596
|
+
|
|
1597
|
+
const amount = this.resolveCsvAmount(
|
|
1598
|
+
amountRaw || fallbackAmountRaw,
|
|
1599
|
+
debitRaw,
|
|
1600
|
+
creditRaw,
|
|
1601
|
+
);
|
|
1602
|
+
|
|
1603
|
+
if (amount === null) {
|
|
1604
|
+
return null;
|
|
1605
|
+
}
|
|
1606
|
+
|
|
1607
|
+
return {
|
|
1608
|
+
postedDate,
|
|
1609
|
+
amount,
|
|
1610
|
+
description: descriptionRaw?.trim() || 'Movimentação importada',
|
|
1611
|
+
externalId: externalIdRaw?.trim() || null,
|
|
1612
|
+
};
|
|
1613
|
+
})
|
|
1614
|
+
.filter(Boolean) as Array<{
|
|
1615
|
+
postedDate: string;
|
|
1616
|
+
amount: number;
|
|
1617
|
+
description: string;
|
|
1618
|
+
externalId: string | null;
|
|
1619
|
+
}>;
|
|
1620
|
+
}
|
|
1621
|
+
|
|
1622
|
+
private parseOfxStatements(content: string) {
|
|
1623
|
+
const normalized = content.replace(/\r/g, '');
|
|
1624
|
+
const segments = normalized
|
|
1625
|
+
.split(/<STMTTRN>/i)
|
|
1626
|
+
.slice(1)
|
|
1627
|
+
.map((segment) => segment.split(/<\/STMTTRN>/i)[0]);
|
|
1628
|
+
|
|
1629
|
+
return segments
|
|
1630
|
+
.map((segment) => {
|
|
1631
|
+
const dateRaw = this.extractOfxTag(segment, 'DTPOSTED');
|
|
1632
|
+
const amountRaw = this.extractOfxTag(segment, 'TRNAMT');
|
|
1633
|
+
const fitId = this.extractOfxTag(segment, 'FITID');
|
|
1634
|
+
const memo = this.extractOfxTag(segment, 'MEMO');
|
|
1635
|
+
const name = this.extractOfxTag(segment, 'NAME');
|
|
1636
|
+
const trnType = this.extractOfxTag(segment, 'TRNTYPE')?.toLowerCase();
|
|
1637
|
+
|
|
1638
|
+
const postedDate = this.parseStatementDate(dateRaw);
|
|
1639
|
+
const amount = this.toNullableNumber(amountRaw);
|
|
1640
|
+
|
|
1641
|
+
if (!postedDate || amount === null) {
|
|
1642
|
+
return null;
|
|
1643
|
+
}
|
|
1644
|
+
|
|
1645
|
+
let normalizedAmount = amount;
|
|
1646
|
+
if (
|
|
1647
|
+
['debit', 'payment', 'withdrawal'].includes(trnType || '') &&
|
|
1648
|
+
normalizedAmount > 0
|
|
1649
|
+
) {
|
|
1650
|
+
normalizedAmount *= -1;
|
|
1651
|
+
}
|
|
1652
|
+
|
|
1653
|
+
const description = [memo, name].filter(Boolean).join(' - ').trim();
|
|
1654
|
+
|
|
1655
|
+
return {
|
|
1656
|
+
postedDate,
|
|
1657
|
+
amount: normalizedAmount,
|
|
1658
|
+
description: description || 'Movimentação OFX',
|
|
1659
|
+
externalId: fitId || null,
|
|
1660
|
+
};
|
|
1661
|
+
})
|
|
1662
|
+
.filter(Boolean) as Array<{
|
|
1663
|
+
postedDate: string;
|
|
1664
|
+
amount: number;
|
|
1665
|
+
description: string;
|
|
1666
|
+
externalId: string | null;
|
|
1667
|
+
}>;
|
|
1668
|
+
}
|
|
1669
|
+
|
|
1670
|
+
private detectCsvDelimiter(line: string) {
|
|
1671
|
+
const delimiters = [';', ',', '\t'];
|
|
1672
|
+
const counts = delimiters.map((delimiter) => ({
|
|
1673
|
+
delimiter,
|
|
1674
|
+
count: (line.match(new RegExp(`\\${delimiter}`, 'g')) || []).length,
|
|
1675
|
+
}));
|
|
1676
|
+
|
|
1677
|
+
return counts.sort((a, b) => b.count - a.count)[0]?.delimiter || ';';
|
|
1678
|
+
}
|
|
1679
|
+
|
|
1680
|
+
private parseCsvLine(line: string, delimiter: string) {
|
|
1681
|
+
const result: string[] = [];
|
|
1682
|
+
let current = '';
|
|
1683
|
+
let inQuotes = false;
|
|
1684
|
+
|
|
1685
|
+
for (let i = 0; i < line.length; i++) {
|
|
1686
|
+
const char = line[i];
|
|
1687
|
+
const next = line[i + 1];
|
|
1688
|
+
|
|
1689
|
+
if (char === '"') {
|
|
1690
|
+
if (inQuotes && next === '"') {
|
|
1691
|
+
current += '"';
|
|
1692
|
+
i++;
|
|
1693
|
+
} else {
|
|
1694
|
+
inQuotes = !inQuotes;
|
|
1695
|
+
}
|
|
1696
|
+
continue;
|
|
1697
|
+
}
|
|
1698
|
+
|
|
1699
|
+
if (char === delimiter && !inQuotes) {
|
|
1700
|
+
result.push(current.trim());
|
|
1701
|
+
current = '';
|
|
1702
|
+
continue;
|
|
1703
|
+
}
|
|
1704
|
+
|
|
1705
|
+
current += char;
|
|
1706
|
+
}
|
|
1707
|
+
|
|
1708
|
+
result.push(current.trim());
|
|
1709
|
+
return result;
|
|
1710
|
+
}
|
|
1711
|
+
|
|
1712
|
+
private normalizeCsvHeader(value: string) {
|
|
1713
|
+
return String(value || '')
|
|
1714
|
+
.normalize('NFD')
|
|
1715
|
+
.replace(/[\u0300-\u036f]/g, '')
|
|
1716
|
+
.toLowerCase()
|
|
1717
|
+
.replace(/[^a-z0-9]/g, '');
|
|
1718
|
+
}
|
|
1719
|
+
|
|
1720
|
+
private findFirstHeaderIndex(headers: string[], candidates: string[]) {
|
|
1721
|
+
return headers.findIndex((header) =>
|
|
1722
|
+
candidates.some((candidate) => header.includes(candidate)),
|
|
1723
|
+
);
|
|
1724
|
+
}
|
|
1725
|
+
|
|
1726
|
+
private parseStatementDate(value?: string) {
|
|
1727
|
+
const raw = String(value || '').trim();
|
|
1728
|
+
if (!raw) {
|
|
1729
|
+
return '';
|
|
1730
|
+
}
|
|
1731
|
+
|
|
1732
|
+
const onlyDateDigits = raw.match(/^(\d{8})/);
|
|
1733
|
+
if (onlyDateDigits?.[1]) {
|
|
1734
|
+
const v = onlyDateDigits[1];
|
|
1735
|
+
return `${v.slice(0, 4)}-${v.slice(4, 6)}-${v.slice(6, 8)}`;
|
|
1736
|
+
}
|
|
1737
|
+
|
|
1738
|
+
const normalized = this.normalizeDate(raw);
|
|
1739
|
+
if (normalized) {
|
|
1740
|
+
return normalized;
|
|
1741
|
+
}
|
|
1742
|
+
|
|
1743
|
+
return '';
|
|
1744
|
+
}
|
|
1745
|
+
|
|
1746
|
+
private resolveCsvAmount(
|
|
1747
|
+
amountRaw?: string,
|
|
1748
|
+
debitRaw?: string,
|
|
1749
|
+
creditRaw?: string,
|
|
1750
|
+
): number | null {
|
|
1751
|
+
const amount = this.toNullableNumber(amountRaw);
|
|
1752
|
+
const debit = this.toNullableNumber(debitRaw);
|
|
1753
|
+
const credit = this.toNullableNumber(creditRaw);
|
|
1754
|
+
|
|
1755
|
+
if (credit !== null || debit !== null) {
|
|
1756
|
+
return (credit || 0) - (debit || 0);
|
|
1757
|
+
}
|
|
1758
|
+
|
|
1759
|
+
if (amount === null) {
|
|
1760
|
+
return null;
|
|
1761
|
+
}
|
|
1762
|
+
|
|
1763
|
+
return amount;
|
|
1764
|
+
}
|
|
1765
|
+
|
|
1766
|
+
private extractOfxTag(segment: string, tag: string) {
|
|
1767
|
+
const regex = new RegExp(`<${tag}>([^\n\r<]*)`, 'i');
|
|
1768
|
+
const match = segment.match(regex);
|
|
1769
|
+
return match?.[1]?.trim() || '';
|
|
1770
|
+
}
|
|
1771
|
+
|
|
1772
|
+
private createShortHash(input: string, length = 40) {
|
|
1773
|
+
return createHash('sha256').update(input).digest('hex').slice(0, length);
|
|
1774
|
+
}
|
|
1775
|
+
|
|
1776
|
+
private escapeCsvCell(value: string | number | null | undefined) {
|
|
1777
|
+
const normalizedValue = String(value ?? '');
|
|
1778
|
+
const escapedValue = normalizedValue.replace(/"/g, '""');
|
|
1779
|
+
|
|
1780
|
+
return `"${escapedValue}"`;
|
|
1781
|
+
}
|
|
1782
|
+
|
|
1251
1783
|
async createBankAccount(data: CreateBankAccountDto, userId?: number) {
|
|
1252
1784
|
const accountType = this.mapAccountTypeFromPt(data.type);
|
|
1253
1785
|
|
|
@@ -1785,6 +2317,255 @@ export class FinanceService {
|
|
|
1785
2317
|
return this.mapTitleToFront(createdTitle, data.payment_channel);
|
|
1786
2318
|
}
|
|
1787
2319
|
|
|
2320
|
+
private async updateDraftTitle(
|
|
2321
|
+
titleId: number,
|
|
2322
|
+
data: CreateFinancialTitleDto,
|
|
2323
|
+
titleType: TitleType,
|
|
2324
|
+
locale: string,
|
|
2325
|
+
userId?: number,
|
|
2326
|
+
) {
|
|
2327
|
+
const installments = this.normalizeAndValidateInstallments(data, locale);
|
|
2328
|
+
|
|
2329
|
+
const updatedTitle = await this.prisma.$transaction(async (tx) => {
|
|
2330
|
+
const title = await tx.financial_title.findFirst({
|
|
2331
|
+
where: {
|
|
2332
|
+
id: titleId,
|
|
2333
|
+
title_type: titleType,
|
|
2334
|
+
},
|
|
2335
|
+
include: {
|
|
2336
|
+
financial_installment: {
|
|
2337
|
+
select: {
|
|
2338
|
+
id: true,
|
|
2339
|
+
},
|
|
2340
|
+
},
|
|
2341
|
+
},
|
|
2342
|
+
});
|
|
2343
|
+
|
|
2344
|
+
if (!title) {
|
|
2345
|
+
throw new NotFoundException(
|
|
2346
|
+
getLocaleText(
|
|
2347
|
+
'itemNotFound',
|
|
2348
|
+
locale,
|
|
2349
|
+
`Financial title with ID ${titleId} not found`,
|
|
2350
|
+
).replace('{{item}}', 'Financial title'),
|
|
2351
|
+
);
|
|
2352
|
+
}
|
|
2353
|
+
|
|
2354
|
+
if (title.status !== 'draft') {
|
|
2355
|
+
throw new BadRequestException('Only draft titles can be edited');
|
|
2356
|
+
}
|
|
2357
|
+
|
|
2358
|
+
const person = await tx.person.findUnique({
|
|
2359
|
+
where: { id: data.person_id },
|
|
2360
|
+
select: { id: true },
|
|
2361
|
+
});
|
|
2362
|
+
|
|
2363
|
+
if (!person) {
|
|
2364
|
+
throw new BadRequestException(
|
|
2365
|
+
getLocaleText('personNotFound', locale, 'Person not found'),
|
|
2366
|
+
);
|
|
2367
|
+
}
|
|
2368
|
+
|
|
2369
|
+
if (data.finance_category_id) {
|
|
2370
|
+
const category = await tx.finance_category.findUnique({
|
|
2371
|
+
where: { id: data.finance_category_id },
|
|
2372
|
+
select: { id: true },
|
|
2373
|
+
});
|
|
2374
|
+
|
|
2375
|
+
if (!category) {
|
|
2376
|
+
throw new BadRequestException(
|
|
2377
|
+
getLocaleText('categoryNotFound', locale, 'Category not found'),
|
|
2378
|
+
);
|
|
2379
|
+
}
|
|
2380
|
+
}
|
|
2381
|
+
|
|
2382
|
+
if (data.cost_center_id) {
|
|
2383
|
+
const costCenter = await tx.cost_center.findUnique({
|
|
2384
|
+
where: { id: data.cost_center_id },
|
|
2385
|
+
select: { id: true },
|
|
2386
|
+
});
|
|
2387
|
+
|
|
2388
|
+
if (!costCenter) {
|
|
2389
|
+
throw new BadRequestException(
|
|
2390
|
+
getLocaleText('costCenterNotFound', locale, 'Cost center not found'),
|
|
2391
|
+
);
|
|
2392
|
+
}
|
|
2393
|
+
}
|
|
2394
|
+
|
|
2395
|
+
const attachmentFileIds = [
|
|
2396
|
+
...new Set((data.attachment_file_ids || []).filter((fileId) => fileId > 0)),
|
|
2397
|
+
];
|
|
2398
|
+
|
|
2399
|
+
if (attachmentFileIds.length > 0) {
|
|
2400
|
+
const existingFiles = await tx.file.findMany({
|
|
2401
|
+
where: {
|
|
2402
|
+
id: { in: attachmentFileIds },
|
|
2403
|
+
},
|
|
2404
|
+
select: {
|
|
2405
|
+
id: true,
|
|
2406
|
+
},
|
|
2407
|
+
});
|
|
2408
|
+
|
|
2409
|
+
const existingFileIds = new Set(existingFiles.map((file) => file.id));
|
|
2410
|
+
const invalidFileIds = attachmentFileIds.filter(
|
|
2411
|
+
(fileId) => !existingFileIds.has(fileId),
|
|
2412
|
+
);
|
|
2413
|
+
|
|
2414
|
+
if (invalidFileIds.length > 0) {
|
|
2415
|
+
throw new BadRequestException(
|
|
2416
|
+
`Invalid attachment file IDs: ${invalidFileIds.join(', ')}`,
|
|
2417
|
+
);
|
|
2418
|
+
}
|
|
2419
|
+
}
|
|
2420
|
+
|
|
2421
|
+
await this.assertDateNotInClosedPeriod(
|
|
2422
|
+
tx,
|
|
2423
|
+
data.competence_date
|
|
2424
|
+
? new Date(data.competence_date)
|
|
2425
|
+
: new Date(installments[0].due_date),
|
|
2426
|
+
'update title',
|
|
2427
|
+
);
|
|
2428
|
+
|
|
2429
|
+
const installmentIds = title.financial_installment.map((item) => item.id);
|
|
2430
|
+
|
|
2431
|
+
if (installmentIds.length > 0) {
|
|
2432
|
+
const hasSettlements = await tx.settlement_allocation.findFirst({
|
|
2433
|
+
where: {
|
|
2434
|
+
installment_id: {
|
|
2435
|
+
in: installmentIds,
|
|
2436
|
+
},
|
|
2437
|
+
},
|
|
2438
|
+
select: {
|
|
2439
|
+
id: true,
|
|
2440
|
+
},
|
|
2441
|
+
});
|
|
2442
|
+
|
|
2443
|
+
if (hasSettlements) {
|
|
2444
|
+
throw new BadRequestException(
|
|
2445
|
+
'Cannot edit title with settled installments',
|
|
2446
|
+
);
|
|
2447
|
+
}
|
|
2448
|
+
}
|
|
2449
|
+
|
|
2450
|
+
await tx.financial_title.update({
|
|
2451
|
+
where: { id: title.id },
|
|
2452
|
+
data: {
|
|
2453
|
+
person_id: data.person_id,
|
|
2454
|
+
document_number: data.document_number,
|
|
2455
|
+
description: data.description,
|
|
2456
|
+
competence_date: data.competence_date
|
|
2457
|
+
? new Date(data.competence_date)
|
|
2458
|
+
: null,
|
|
2459
|
+
issue_date: data.issue_date ? new Date(data.issue_date) : null,
|
|
2460
|
+
total_amount_cents: this.toCents(data.total_amount),
|
|
2461
|
+
finance_category_id: data.finance_category_id,
|
|
2462
|
+
},
|
|
2463
|
+
});
|
|
2464
|
+
|
|
2465
|
+
if (installmentIds.length > 0) {
|
|
2466
|
+
await tx.installment_allocation.deleteMany({
|
|
2467
|
+
where: {
|
|
2468
|
+
installment_id: {
|
|
2469
|
+
in: installmentIds,
|
|
2470
|
+
},
|
|
2471
|
+
},
|
|
2472
|
+
});
|
|
2473
|
+
|
|
2474
|
+
await tx.financial_installment_tag.deleteMany({
|
|
2475
|
+
where: {
|
|
2476
|
+
installment_id: {
|
|
2477
|
+
in: installmentIds,
|
|
2478
|
+
},
|
|
2479
|
+
},
|
|
2480
|
+
});
|
|
2481
|
+
|
|
2482
|
+
await tx.financial_installment.deleteMany({
|
|
2483
|
+
where: {
|
|
2484
|
+
title_id: title.id,
|
|
2485
|
+
},
|
|
2486
|
+
});
|
|
2487
|
+
}
|
|
2488
|
+
|
|
2489
|
+
for (const installment of installments) {
|
|
2490
|
+
const amountCents = installment.amount_cents;
|
|
2491
|
+
|
|
2492
|
+
const createdInstallment = await tx.financial_installment.create({
|
|
2493
|
+
data: {
|
|
2494
|
+
title_id: title.id,
|
|
2495
|
+
installment_number: installment.installment_number,
|
|
2496
|
+
competence_date: data.competence_date
|
|
2497
|
+
? new Date(data.competence_date)
|
|
2498
|
+
: new Date(installment.due_date),
|
|
2499
|
+
due_date: new Date(installment.due_date),
|
|
2500
|
+
amount_cents: amountCents,
|
|
2501
|
+
open_amount_cents: amountCents,
|
|
2502
|
+
status: this.resolveInstallmentStatus(
|
|
2503
|
+
amountCents,
|
|
2504
|
+
amountCents,
|
|
2505
|
+
new Date(installment.due_date),
|
|
2506
|
+
),
|
|
2507
|
+
notes: data.description,
|
|
2508
|
+
},
|
|
2509
|
+
});
|
|
2510
|
+
|
|
2511
|
+
if (data.cost_center_id) {
|
|
2512
|
+
await tx.installment_allocation.create({
|
|
2513
|
+
data: {
|
|
2514
|
+
installment_id: createdInstallment.id,
|
|
2515
|
+
cost_center_id: data.cost_center_id,
|
|
2516
|
+
allocated_amount_cents: amountCents,
|
|
2517
|
+
},
|
|
2518
|
+
});
|
|
2519
|
+
}
|
|
2520
|
+
}
|
|
2521
|
+
|
|
2522
|
+
if (data.attachment_file_ids) {
|
|
2523
|
+
await tx.financial_title_attachment.deleteMany({
|
|
2524
|
+
where: {
|
|
2525
|
+
title_id: title.id,
|
|
2526
|
+
},
|
|
2527
|
+
});
|
|
2528
|
+
|
|
2529
|
+
if (attachmentFileIds.length > 0) {
|
|
2530
|
+
await tx.financial_title_attachment.createMany({
|
|
2531
|
+
data: attachmentFileIds.map((fileId) => ({
|
|
2532
|
+
title_id: title.id,
|
|
2533
|
+
file_id: fileId,
|
|
2534
|
+
uploaded_by_user_id: userId,
|
|
2535
|
+
})),
|
|
2536
|
+
});
|
|
2537
|
+
}
|
|
2538
|
+
}
|
|
2539
|
+
|
|
2540
|
+
await this.createAuditLog(tx, {
|
|
2541
|
+
action: 'UPDATE_TITLE',
|
|
2542
|
+
entityTable: 'financial_title',
|
|
2543
|
+
entityId: String(title.id),
|
|
2544
|
+
actorUserId: userId,
|
|
2545
|
+
summary: `Updated draft ${titleType} title ${title.id}`,
|
|
2546
|
+
beforeData: JSON.stringify({ status: title.status }),
|
|
2547
|
+
afterData: JSON.stringify({
|
|
2548
|
+
status: title.status,
|
|
2549
|
+
total_amount_cents: this.toCents(data.total_amount),
|
|
2550
|
+
}),
|
|
2551
|
+
});
|
|
2552
|
+
|
|
2553
|
+
return tx.financial_title.findFirst({
|
|
2554
|
+
where: {
|
|
2555
|
+
id: title.id,
|
|
2556
|
+
title_type: titleType,
|
|
2557
|
+
},
|
|
2558
|
+
include: this.defaultTitleInclude(),
|
|
2559
|
+
});
|
|
2560
|
+
});
|
|
2561
|
+
|
|
2562
|
+
if (!updatedTitle) {
|
|
2563
|
+
throw new NotFoundException('Financial title not found');
|
|
2564
|
+
}
|
|
2565
|
+
|
|
2566
|
+
return this.mapTitleToFront(updatedTitle, data.payment_channel);
|
|
2567
|
+
}
|
|
2568
|
+
|
|
1788
2569
|
private normalizeAndValidateInstallments(
|
|
1789
2570
|
data: CreateFinancialTitleDto,
|
|
1790
2571
|
locale: string,
|
|
@@ -1896,7 +2677,7 @@ export class FinanceService {
|
|
|
1896
2677
|
await tx.financial_title.update({
|
|
1897
2678
|
where: { id: title.id },
|
|
1898
2679
|
data: {
|
|
1899
|
-
status: '
|
|
2680
|
+
status: 'open',
|
|
1900
2681
|
},
|
|
1901
2682
|
});
|
|
1902
2683
|
|
|
@@ -1907,7 +2688,7 @@ export class FinanceService {
|
|
|
1907
2688
|
actorUserId: userId,
|
|
1908
2689
|
summary: `Approved ${titleType} title ${title.id}`,
|
|
1909
2690
|
beforeData: JSON.stringify({ status: title.status }),
|
|
1910
|
-
afterData: JSON.stringify({ status: '
|
|
2691
|
+
afterData: JSON.stringify({ status: 'open' }),
|
|
1911
2692
|
});
|
|
1912
2693
|
|
|
1913
2694
|
return tx.financial_title.findFirst({
|
|
@@ -2002,6 +2783,82 @@ export class FinanceService {
|
|
|
2002
2783
|
return this.mapTitleToFront(updatedTitle);
|
|
2003
2784
|
}
|
|
2004
2785
|
|
|
2786
|
+
private async cancelTitle(
|
|
2787
|
+
titleId: number,
|
|
2788
|
+
data: RejectTitleDto,
|
|
2789
|
+
titleType: TitleType,
|
|
2790
|
+
locale: string,
|
|
2791
|
+
userId?: number,
|
|
2792
|
+
) {
|
|
2793
|
+
const updatedTitle = await this.prisma.$transaction(async (tx) => {
|
|
2794
|
+
const title = await tx.financial_title.findFirst({
|
|
2795
|
+
where: {
|
|
2796
|
+
id: titleId,
|
|
2797
|
+
title_type: titleType,
|
|
2798
|
+
},
|
|
2799
|
+
select: {
|
|
2800
|
+
id: true,
|
|
2801
|
+
status: true,
|
|
2802
|
+
competence_date: true,
|
|
2803
|
+
},
|
|
2804
|
+
});
|
|
2805
|
+
|
|
2806
|
+
if (!title) {
|
|
2807
|
+
throw new NotFoundException(
|
|
2808
|
+
getLocaleText(
|
|
2809
|
+
'itemNotFound',
|
|
2810
|
+
locale,
|
|
2811
|
+
`Financial title with ID ${titleId} not found`,
|
|
2812
|
+
).replace('{{item}}', 'Financial title'),
|
|
2813
|
+
);
|
|
2814
|
+
}
|
|
2815
|
+
|
|
2816
|
+
if (title.status === 'settled' || title.status === 'canceled') {
|
|
2817
|
+
throw new BadRequestException('Title cannot be canceled in current status');
|
|
2818
|
+
}
|
|
2819
|
+
|
|
2820
|
+
await this.assertDateNotInClosedPeriod(
|
|
2821
|
+
tx,
|
|
2822
|
+
title.competence_date,
|
|
2823
|
+
'cancel title',
|
|
2824
|
+
);
|
|
2825
|
+
|
|
2826
|
+
await tx.financial_title.update({
|
|
2827
|
+
where: { id: title.id },
|
|
2828
|
+
data: {
|
|
2829
|
+
status: 'canceled',
|
|
2830
|
+
},
|
|
2831
|
+
});
|
|
2832
|
+
|
|
2833
|
+
await this.createAuditLog(tx, {
|
|
2834
|
+
action: 'CANCEL_TITLE',
|
|
2835
|
+
entityTable: 'financial_title',
|
|
2836
|
+
entityId: String(title.id),
|
|
2837
|
+
actorUserId: userId,
|
|
2838
|
+
summary: `Canceled ${titleType} title ${title.id}`,
|
|
2839
|
+
beforeData: JSON.stringify({ status: title.status }),
|
|
2840
|
+
afterData: JSON.stringify({
|
|
2841
|
+
status: 'canceled',
|
|
2842
|
+
reason: data?.reason || null,
|
|
2843
|
+
}),
|
|
2844
|
+
});
|
|
2845
|
+
|
|
2846
|
+
return tx.financial_title.findFirst({
|
|
2847
|
+
where: {
|
|
2848
|
+
id: title.id,
|
|
2849
|
+
title_type: titleType,
|
|
2850
|
+
},
|
|
2851
|
+
include: this.defaultTitleInclude(),
|
|
2852
|
+
});
|
|
2853
|
+
});
|
|
2854
|
+
|
|
2855
|
+
if (!updatedTitle) {
|
|
2856
|
+
throw new NotFoundException('Financial title not found');
|
|
2857
|
+
}
|
|
2858
|
+
|
|
2859
|
+
return this.mapTitleToFront(updatedTitle);
|
|
2860
|
+
}
|
|
2861
|
+
|
|
2005
2862
|
private async settleTitleInstallment(
|
|
2006
2863
|
titleId: number,
|
|
2007
2864
|
data: SettleInstallmentDto,
|
|
@@ -2044,9 +2901,9 @@ export class FinanceService {
|
|
|
2044
2901
|
);
|
|
2045
2902
|
}
|
|
2046
2903
|
|
|
2047
|
-
if (!['
|
|
2904
|
+
if (!['open', 'partial'].includes(title.status)) {
|
|
2048
2905
|
throw new BadRequestException(
|
|
2049
|
-
'Only
|
|
2906
|
+
'Only open/partial titles can be settled',
|
|
2050
2907
|
);
|
|
2051
2908
|
}
|
|
2052
2909
|
|
|
@@ -2863,7 +3720,6 @@ export class FinanceService {
|
|
|
2863
3720
|
private mapStatusToPt(status?: string | null) {
|
|
2864
3721
|
const statusMap = {
|
|
2865
3722
|
draft: 'rascunho',
|
|
2866
|
-
approved: 'aprovado',
|
|
2867
3723
|
open: 'aberto',
|
|
2868
3724
|
partial: 'parcial',
|
|
2869
3725
|
settled: 'liquidado',
|
|
@@ -2895,7 +3751,7 @@ export class FinanceService {
|
|
|
2895
3751
|
|
|
2896
3752
|
const statusMap = {
|
|
2897
3753
|
rascunho: 'draft',
|
|
2898
|
-
aprovado: '
|
|
3754
|
+
aprovado: 'open',
|
|
2899
3755
|
aberto: 'open',
|
|
2900
3756
|
parcial: 'partial',
|
|
2901
3757
|
liquidado: 'settled',
|