@hed-hog/finance 0.0.237 → 0.0.239
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-finance-tag.dto.d.ts +5 -0
- package/dist/dto/create-finance-tag.dto.d.ts.map +1 -0
- package/dist/dto/create-finance-tag.dto.js +29 -0
- package/dist/dto/create-finance-tag.dto.js.map +1 -0
- package/dist/dto/reject-title.dto.d.ts +4 -0
- package/dist/dto/reject-title.dto.d.ts.map +1 -0
- package/dist/dto/reject-title.dto.js +22 -0
- package/dist/dto/reject-title.dto.js.map +1 -0
- package/dist/dto/reverse-settlement.dto.d.ts +4 -0
- package/dist/dto/reverse-settlement.dto.d.ts.map +1 -0
- package/dist/dto/reverse-settlement.dto.js +22 -0
- package/dist/dto/reverse-settlement.dto.js.map +1 -0
- package/dist/dto/settle-installment.dto.d.ts +12 -0
- package/dist/dto/settle-installment.dto.d.ts.map +1 -0
- package/dist/dto/settle-installment.dto.js +71 -0
- package/dist/dto/settle-installment.dto.js.map +1 -0
- package/dist/dto/update-installment-tags.dto.d.ts +4 -0
- package/dist/dto/update-installment-tags.dto.d.ts.map +1 -0
- package/dist/dto/update-installment-tags.dto.js +27 -0
- package/dist/dto/update-installment-tags.dto.js.map +1 -0
- package/dist/finance-data.controller.d.ts +17 -5
- package/dist/finance-data.controller.d.ts.map +1 -1
- package/dist/finance-installments.controller.d.ts +325 -8
- package/dist/finance-installments.controller.d.ts.map +1 -1
- package/dist/finance-installments.controller.js +128 -0
- package/dist/finance-installments.controller.js.map +1 -1
- package/dist/finance.service.d.ts +357 -13
- package/dist/finance.service.d.ts.map +1 -1
- package/dist/finance.service.js +835 -64
- package/dist/finance.service.js.map +1 -1
- package/hedhog/data/route.yaml +90 -0
- package/hedhog/frontend/app/_lib/use-finance-data.ts.ejs +2 -0
- package/hedhog/frontend/app/accounts-payable/approvals/page.tsx.ejs +92 -12
- package/hedhog/frontend/app/accounts-payable/installments/[id]/page.tsx.ejs +601 -79
- package/hedhog/frontend/app/accounts-payable/installments/page.tsx.ejs +481 -19
- package/hedhog/frontend/app/accounts-receivable/installments/[id]/page.tsx.ejs +598 -69
- package/hedhog/frontend/app/accounts-receivable/installments/page.tsx.ejs +472 -15
- package/hedhog/frontend/messages/en.json +38 -0
- package/hedhog/frontend/messages/pt.json +38 -0
- package/package.json +5 -5
- package/src/dto/create-finance-tag.dto.ts +15 -0
- package/src/dto/reject-title.dto.ts +7 -0
- package/src/dto/reverse-settlement.dto.ts +7 -0
- package/src/dto/settle-installment.dto.ts +55 -0
- package/src/dto/update-installment-tags.dto.ts +12 -0
- package/src/finance-installments.controller.ts +145 -9
- package/src/finance.service.ts +1333 -165
package/src/finance.service.ts
CHANGED
|
@@ -6,19 +6,37 @@ import {
|
|
|
6
6
|
BadRequestException,
|
|
7
7
|
Injectable,
|
|
8
8
|
Logger,
|
|
9
|
-
NotFoundException
|
|
9
|
+
NotFoundException,
|
|
10
10
|
} from '@nestjs/common';
|
|
11
11
|
import { CreateBankAccountDto } from './dto/create-bank-account.dto';
|
|
12
12
|
import { CreateCostCenterDto } from './dto/create-cost-center.dto';
|
|
13
13
|
import { CreateFinanceCategoryDto } from './dto/create-finance-category.dto';
|
|
14
|
+
import { CreateFinanceTagDto } from './dto/create-finance-tag.dto';
|
|
14
15
|
import { CreateFinancialTitleDto } from './dto/create-financial-title.dto';
|
|
15
16
|
import { CreatePeriodCloseDto } from './dto/create-period-close.dto';
|
|
16
17
|
import { MoveFinanceCategoryDto } from './dto/move-finance-category.dto';
|
|
18
|
+
import { RejectTitleDto } from './dto/reject-title.dto';
|
|
19
|
+
import { ReverseSettlementDto } from './dto/reverse-settlement.dto';
|
|
20
|
+
import { SettleInstallmentDto } from './dto/settle-installment.dto';
|
|
17
21
|
import { UpdateBankAccountDto } from './dto/update-bank-account.dto';
|
|
18
22
|
import { UpdateCostCenterDto } from './dto/update-cost-center.dto';
|
|
19
23
|
import { UpdateFinanceCategoryDto } from './dto/update-finance-category.dto';
|
|
20
24
|
|
|
21
25
|
type TitleType = 'payable' | 'receivable';
|
|
26
|
+
type InstallmentStatus =
|
|
27
|
+
| 'open'
|
|
28
|
+
| 'partial'
|
|
29
|
+
| 'settled'
|
|
30
|
+
| 'canceled'
|
|
31
|
+
| 'overdue';
|
|
32
|
+
type TitleStatus =
|
|
33
|
+
| 'draft'
|
|
34
|
+
| 'approved'
|
|
35
|
+
| 'open'
|
|
36
|
+
| 'partial'
|
|
37
|
+
| 'settled'
|
|
38
|
+
| 'canceled'
|
|
39
|
+
| 'overdue';
|
|
22
40
|
|
|
23
41
|
@Injectable()
|
|
24
42
|
export class FinanceService {
|
|
@@ -697,15 +715,15 @@ export class FinanceService {
|
|
|
697
715
|
|
|
698
716
|
async getData() {
|
|
699
717
|
const [
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
] = await Promise.
|
|
718
|
+
payablesResult,
|
|
719
|
+
receivablesResult,
|
|
720
|
+
peopleResult,
|
|
721
|
+
categoriesResult,
|
|
722
|
+
costCentersResult,
|
|
723
|
+
bankAccountsResult,
|
|
724
|
+
tagsResult,
|
|
725
|
+
auditLogsResult,
|
|
726
|
+
] = await Promise.allSettled([
|
|
709
727
|
this.loadTitles('payable'),
|
|
710
728
|
this.loadTitles('receivable'),
|
|
711
729
|
this.loadPeople(),
|
|
@@ -716,6 +734,57 @@ export class FinanceService {
|
|
|
716
734
|
this.loadAuditLogs(),
|
|
717
735
|
]);
|
|
718
736
|
|
|
737
|
+
const payables = payablesResult.status === 'fulfilled' ? payablesResult.value : [];
|
|
738
|
+
const receivables =
|
|
739
|
+
receivablesResult.status === 'fulfilled' ? receivablesResult.value : [];
|
|
740
|
+
const people = peopleResult.status === 'fulfilled' ? peopleResult.value : [];
|
|
741
|
+
const categories =
|
|
742
|
+
categoriesResult.status === 'fulfilled' ? categoriesResult.value : [];
|
|
743
|
+
const costCenters =
|
|
744
|
+
costCentersResult.status === 'fulfilled' ? costCentersResult.value : [];
|
|
745
|
+
const bankAccounts =
|
|
746
|
+
bankAccountsResult.status === 'fulfilled' ? bankAccountsResult.value : [];
|
|
747
|
+
const tags = tagsResult.status === 'fulfilled' ? tagsResult.value : [];
|
|
748
|
+
const auditLogs =
|
|
749
|
+
auditLogsResult.status === 'fulfilled' ? auditLogsResult.value : [];
|
|
750
|
+
|
|
751
|
+
if (payablesResult.status === 'rejected') {
|
|
752
|
+
this.logger.error('Failed to load finance payables', payablesResult.reason);
|
|
753
|
+
}
|
|
754
|
+
if (receivablesResult.status === 'rejected') {
|
|
755
|
+
this.logger.error('Failed to load finance receivables', receivablesResult.reason);
|
|
756
|
+
}
|
|
757
|
+
if (peopleResult.status === 'rejected') {
|
|
758
|
+
this.logger.error('Failed to load finance people', peopleResult.reason);
|
|
759
|
+
}
|
|
760
|
+
if (categoriesResult.status === 'rejected') {
|
|
761
|
+
this.logger.error('Failed to load finance categories', categoriesResult.reason);
|
|
762
|
+
}
|
|
763
|
+
if (costCentersResult.status === 'rejected') {
|
|
764
|
+
this.logger.error('Failed to load finance cost centers', costCentersResult.reason);
|
|
765
|
+
}
|
|
766
|
+
if (bankAccountsResult.status === 'rejected') {
|
|
767
|
+
this.logger.error('Failed to load finance bank accounts', bankAccountsResult.reason);
|
|
768
|
+
}
|
|
769
|
+
if (tagsResult.status === 'rejected') {
|
|
770
|
+
this.logger.error('Failed to load finance tags', tagsResult.reason);
|
|
771
|
+
}
|
|
772
|
+
if (auditLogsResult.status === 'rejected') {
|
|
773
|
+
this.logger.error('Failed to load finance audit logs', auditLogsResult.reason);
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
const aprovacoesPendentes = payables
|
|
777
|
+
.filter((title: any) => title.status === 'rascunho')
|
|
778
|
+
.map((title: any) => ({
|
|
779
|
+
id: String(title.id),
|
|
780
|
+
tituloId: String(title.id),
|
|
781
|
+
solicitante: '-',
|
|
782
|
+
valor: Number(title.valorTotal || 0),
|
|
783
|
+
politica: 'Aprovação financeira',
|
|
784
|
+
urgencia: 'media',
|
|
785
|
+
dataSolicitacao: title.criadoEm,
|
|
786
|
+
}));
|
|
787
|
+
|
|
719
788
|
return {
|
|
720
789
|
kpis: {
|
|
721
790
|
saldoCaixa: 0,
|
|
@@ -733,7 +802,7 @@ export class FinanceService {
|
|
|
733
802
|
pessoas: people,
|
|
734
803
|
categorias: categories,
|
|
735
804
|
centrosCusto: costCenters,
|
|
736
|
-
aprovacoesPendentes
|
|
805
|
+
aprovacoesPendentes,
|
|
737
806
|
agingInadimplencia: [],
|
|
738
807
|
cenarios: [],
|
|
739
808
|
transferencias: [],
|
|
@@ -779,6 +848,45 @@ export class FinanceService {
|
|
|
779
848
|
return this.createTitle(data, 'payable', locale, userId);
|
|
780
849
|
}
|
|
781
850
|
|
|
851
|
+
async approveAccountsPayableTitle(id: number, locale: string, userId?: number) {
|
|
852
|
+
return this.approveTitle(id, 'payable', locale, userId);
|
|
853
|
+
}
|
|
854
|
+
|
|
855
|
+
async rejectAccountsPayableTitle(
|
|
856
|
+
id: number,
|
|
857
|
+
data: RejectTitleDto,
|
|
858
|
+
locale: string,
|
|
859
|
+
userId?: number,
|
|
860
|
+
) {
|
|
861
|
+
return this.rejectTitle(id, data, 'payable', locale, userId);
|
|
862
|
+
}
|
|
863
|
+
|
|
864
|
+
async settleAccountsPayableInstallment(
|
|
865
|
+
id: number,
|
|
866
|
+
data: SettleInstallmentDto,
|
|
867
|
+
locale: string,
|
|
868
|
+
userId?: number,
|
|
869
|
+
) {
|
|
870
|
+
return this.settleTitleInstallment(id, data, 'payable', locale, userId);
|
|
871
|
+
}
|
|
872
|
+
|
|
873
|
+
async reverseAccountsPayableSettlement(
|
|
874
|
+
id: number,
|
|
875
|
+
settlementId: number,
|
|
876
|
+
data: ReverseSettlementDto,
|
|
877
|
+
locale: string,
|
|
878
|
+
userId?: number,
|
|
879
|
+
) {
|
|
880
|
+
return this.reverseTitleSettlement(
|
|
881
|
+
id,
|
|
882
|
+
settlementId,
|
|
883
|
+
data,
|
|
884
|
+
'payable',
|
|
885
|
+
locale,
|
|
886
|
+
userId,
|
|
887
|
+
);
|
|
888
|
+
}
|
|
889
|
+
|
|
782
890
|
async createAccountsReceivableTitle(
|
|
783
891
|
data: CreateFinancialTitleDto,
|
|
784
892
|
locale: string,
|
|
@@ -787,6 +895,102 @@ export class FinanceService {
|
|
|
787
895
|
return this.createTitle(data, 'receivable', locale, userId);
|
|
788
896
|
}
|
|
789
897
|
|
|
898
|
+
async approveAccountsReceivableTitle(
|
|
899
|
+
id: number,
|
|
900
|
+
locale: string,
|
|
901
|
+
userId?: number,
|
|
902
|
+
) {
|
|
903
|
+
return this.approveTitle(id, 'receivable', locale, userId);
|
|
904
|
+
}
|
|
905
|
+
|
|
906
|
+
async settleAccountsReceivableInstallment(
|
|
907
|
+
id: number,
|
|
908
|
+
data: SettleInstallmentDto,
|
|
909
|
+
locale: string,
|
|
910
|
+
userId?: number,
|
|
911
|
+
) {
|
|
912
|
+
return this.settleTitleInstallment(id, data, 'receivable', locale, userId);
|
|
913
|
+
}
|
|
914
|
+
|
|
915
|
+
async reverseAccountsReceivableSettlement(
|
|
916
|
+
id: number,
|
|
917
|
+
settlementId: number,
|
|
918
|
+
data: ReverseSettlementDto,
|
|
919
|
+
locale: string,
|
|
920
|
+
userId?: number,
|
|
921
|
+
) {
|
|
922
|
+
return this.reverseTitleSettlement(
|
|
923
|
+
id,
|
|
924
|
+
settlementId,
|
|
925
|
+
data,
|
|
926
|
+
'receivable',
|
|
927
|
+
locale,
|
|
928
|
+
userId,
|
|
929
|
+
);
|
|
930
|
+
}
|
|
931
|
+
|
|
932
|
+
async createTag(data: CreateFinanceTagDto) {
|
|
933
|
+
const slug = this.normalizeTagSlug(data.name);
|
|
934
|
+
|
|
935
|
+
if (!slug) {
|
|
936
|
+
throw new BadRequestException('Tag name is required');
|
|
937
|
+
}
|
|
938
|
+
|
|
939
|
+
const existingTag = await this.prisma.tag.findFirst({
|
|
940
|
+
where: {
|
|
941
|
+
slug,
|
|
942
|
+
},
|
|
943
|
+
select: {
|
|
944
|
+
id: true,
|
|
945
|
+
slug: true,
|
|
946
|
+
color: true,
|
|
947
|
+
},
|
|
948
|
+
});
|
|
949
|
+
|
|
950
|
+
if (existingTag) {
|
|
951
|
+
return {
|
|
952
|
+
id: String(existingTag.id),
|
|
953
|
+
nome: existingTag.slug,
|
|
954
|
+
cor: existingTag.color,
|
|
955
|
+
};
|
|
956
|
+
}
|
|
957
|
+
|
|
958
|
+
const createdTag = await this.prisma.tag.create({
|
|
959
|
+
data: {
|
|
960
|
+
slug,
|
|
961
|
+
color: data.color || '#000000',
|
|
962
|
+
status: 'active',
|
|
963
|
+
},
|
|
964
|
+
select: {
|
|
965
|
+
id: true,
|
|
966
|
+
slug: true,
|
|
967
|
+
color: true,
|
|
968
|
+
},
|
|
969
|
+
});
|
|
970
|
+
|
|
971
|
+
return {
|
|
972
|
+
id: String(createdTag.id),
|
|
973
|
+
nome: createdTag.slug,
|
|
974
|
+
cor: createdTag.color,
|
|
975
|
+
};
|
|
976
|
+
}
|
|
977
|
+
|
|
978
|
+
async updateAccountsPayableInstallmentTags(
|
|
979
|
+
id: number,
|
|
980
|
+
tagIds: number[],
|
|
981
|
+
locale: string,
|
|
982
|
+
) {
|
|
983
|
+
return this.updateTitleTags(id, 'payable', tagIds, locale);
|
|
984
|
+
}
|
|
985
|
+
|
|
986
|
+
async updateAccountsReceivableInstallmentTags(
|
|
987
|
+
id: number,
|
|
988
|
+
tagIds: number[],
|
|
989
|
+
locale: string,
|
|
990
|
+
) {
|
|
991
|
+
return this.updateTitleTags(id, 'receivable', tagIds, locale);
|
|
992
|
+
}
|
|
993
|
+
|
|
790
994
|
async listBankAccounts() {
|
|
791
995
|
const bankAccounts = await this.prisma.bank_account.findMany({
|
|
792
996
|
include: {
|
|
@@ -1427,197 +1631,924 @@ export class FinanceService {
|
|
|
1427
1631
|
locale: string,
|
|
1428
1632
|
userId?: number,
|
|
1429
1633
|
) {
|
|
1430
|
-
const
|
|
1431
|
-
where: { id: data.person_id },
|
|
1432
|
-
select: { id: true },
|
|
1433
|
-
});
|
|
1434
|
-
|
|
1435
|
-
if (!person) {
|
|
1436
|
-
throw new BadRequestException(
|
|
1437
|
-
getLocaleText('personNotFound', locale, 'Person not found'),
|
|
1438
|
-
);
|
|
1439
|
-
}
|
|
1634
|
+
const installments = this.normalizeAndValidateInstallments(data, locale);
|
|
1440
1635
|
|
|
1441
|
-
|
|
1442
|
-
const
|
|
1443
|
-
where: { id: data.
|
|
1636
|
+
const createdTitleId = await this.prisma.$transaction(async (tx) => {
|
|
1637
|
+
const person = await tx.person.findUnique({
|
|
1638
|
+
where: { id: data.person_id },
|
|
1444
1639
|
select: { id: true },
|
|
1445
1640
|
});
|
|
1446
1641
|
|
|
1447
|
-
if (!
|
|
1642
|
+
if (!person) {
|
|
1448
1643
|
throw new BadRequestException(
|
|
1449
|
-
getLocaleText('
|
|
1644
|
+
getLocaleText('personNotFound', locale, 'Person not found'),
|
|
1450
1645
|
);
|
|
1451
1646
|
}
|
|
1452
|
-
}
|
|
1453
1647
|
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
1648
|
+
if (data.finance_category_id) {
|
|
1649
|
+
const category = await tx.finance_category.findUnique({
|
|
1650
|
+
where: { id: data.finance_category_id },
|
|
1651
|
+
select: { id: true },
|
|
1652
|
+
});
|
|
1459
1653
|
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1654
|
+
if (!category) {
|
|
1655
|
+
throw new BadRequestException(
|
|
1656
|
+
getLocaleText('categoryNotFound', locale, 'Category not found'),
|
|
1657
|
+
);
|
|
1658
|
+
}
|
|
1464
1659
|
}
|
|
1465
|
-
}
|
|
1466
1660
|
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
installment_number: 1,
|
|
1473
|
-
due_date: data.due_date,
|
|
1474
|
-
amount: data.total_amount,
|
|
1475
|
-
},
|
|
1476
|
-
];
|
|
1661
|
+
if (data.cost_center_id) {
|
|
1662
|
+
const costCenter = await tx.cost_center.findUnique({
|
|
1663
|
+
where: { id: data.cost_center_id },
|
|
1664
|
+
select: { id: true },
|
|
1665
|
+
});
|
|
1477
1666
|
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1667
|
+
if (!costCenter) {
|
|
1668
|
+
throw new BadRequestException(
|
|
1669
|
+
getLocaleText('costCenterNotFound', locale, 'Cost center not found'),
|
|
1670
|
+
);
|
|
1671
|
+
}
|
|
1672
|
+
}
|
|
1673
|
+
|
|
1674
|
+
const attachmentFileIds = [
|
|
1675
|
+
...new Set((data.attachment_file_ids || []).filter((fileId) => fileId > 0)),
|
|
1676
|
+
];
|
|
1677
|
+
|
|
1678
|
+
if (attachmentFileIds.length > 0) {
|
|
1679
|
+
const existingFiles = await tx.file.findMany({
|
|
1680
|
+
where: {
|
|
1681
|
+
id: { in: attachmentFileIds },
|
|
1682
|
+
},
|
|
1683
|
+
select: {
|
|
1684
|
+
id: true,
|
|
1685
|
+
},
|
|
1686
|
+
});
|
|
1687
|
+
|
|
1688
|
+
const existingFileIds = new Set(existingFiles.map((file) => file.id));
|
|
1689
|
+
const invalidFileIds = attachmentFileIds.filter(
|
|
1690
|
+
(fileId) => !existingFileIds.has(fileId),
|
|
1691
|
+
);
|
|
1692
|
+
|
|
1693
|
+
if (invalidFileIds.length > 0) {
|
|
1694
|
+
throw new BadRequestException(
|
|
1695
|
+
`Invalid attachment file IDs: ${invalidFileIds.join(', ')}`,
|
|
1696
|
+
);
|
|
1697
|
+
}
|
|
1698
|
+
}
|
|
1494
1699
|
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
1700
|
+
await this.assertDateNotInClosedPeriod(
|
|
1701
|
+
tx,
|
|
1702
|
+
data.competence_date
|
|
1703
|
+
? new Date(data.competence_date)
|
|
1704
|
+
: new Date(installments[0].due_date),
|
|
1705
|
+
'create title',
|
|
1706
|
+
);
|
|
1498
1707
|
|
|
1499
|
-
const
|
|
1708
|
+
const title = await tx.financial_title.create({
|
|
1500
1709
|
data: {
|
|
1501
|
-
|
|
1502
|
-
|
|
1710
|
+
person_id: data.person_id,
|
|
1711
|
+
title_type: titleType,
|
|
1712
|
+
status: 'draft',
|
|
1713
|
+
document_number: data.document_number,
|
|
1714
|
+
description: data.description,
|
|
1503
1715
|
competence_date: data.competence_date
|
|
1504
1716
|
? new Date(data.competence_date)
|
|
1505
|
-
:
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
notes: data.description,
|
|
1717
|
+
: null,
|
|
1718
|
+
issue_date: data.issue_date ? new Date(data.issue_date) : null,
|
|
1719
|
+
total_amount_cents: this.toCents(data.total_amount),
|
|
1720
|
+
finance_category_id: data.finance_category_id,
|
|
1721
|
+
created_by_user_id: userId,
|
|
1511
1722
|
},
|
|
1512
1723
|
});
|
|
1513
1724
|
|
|
1514
|
-
if (
|
|
1515
|
-
await
|
|
1725
|
+
if (attachmentFileIds.length > 0) {
|
|
1726
|
+
await tx.financial_title_attachment.createMany({
|
|
1727
|
+
data: attachmentFileIds.map((fileId) => ({
|
|
1728
|
+
title_id: title.id,
|
|
1729
|
+
file_id: fileId,
|
|
1730
|
+
uploaded_by_user_id: userId,
|
|
1731
|
+
})),
|
|
1732
|
+
});
|
|
1733
|
+
}
|
|
1734
|
+
|
|
1735
|
+
for (let index = 0; index < installments.length; index++) {
|
|
1736
|
+
const installment = installments[index];
|
|
1737
|
+
const amountCents = installment.amount_cents;
|
|
1738
|
+
|
|
1739
|
+
const createdInstallment = await tx.financial_installment.create({
|
|
1516
1740
|
data: {
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
|
|
1741
|
+
title_id: title.id,
|
|
1742
|
+
installment_number: installment.installment_number,
|
|
1743
|
+
competence_date: data.competence_date
|
|
1744
|
+
? new Date(data.competence_date)
|
|
1745
|
+
: new Date(installment.due_date),
|
|
1746
|
+
due_date: new Date(installment.due_date),
|
|
1747
|
+
amount_cents: amountCents,
|
|
1748
|
+
open_amount_cents: amountCents,
|
|
1749
|
+
status: this.resolveInstallmentStatus(
|
|
1750
|
+
amountCents,
|
|
1751
|
+
amountCents,
|
|
1752
|
+
new Date(installment.due_date),
|
|
1753
|
+
),
|
|
1754
|
+
notes: data.description,
|
|
1520
1755
|
},
|
|
1521
1756
|
});
|
|
1757
|
+
|
|
1758
|
+
if (data.cost_center_id) {
|
|
1759
|
+
await tx.installment_allocation.create({
|
|
1760
|
+
data: {
|
|
1761
|
+
installment_id: createdInstallment.id,
|
|
1762
|
+
cost_center_id: data.cost_center_id,
|
|
1763
|
+
allocated_amount_cents: amountCents,
|
|
1764
|
+
},
|
|
1765
|
+
});
|
|
1766
|
+
}
|
|
1522
1767
|
}
|
|
1523
|
-
}
|
|
1524
1768
|
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
|
|
1769
|
+
await this.createAuditLog(tx, {
|
|
1770
|
+
action: 'CREATE_TITLE',
|
|
1771
|
+
entityTable: 'financial_title',
|
|
1772
|
+
entityId: String(title.id),
|
|
1773
|
+
actorUserId: userId,
|
|
1774
|
+
summary: `Created ${titleType} title ${title.id} in draft`,
|
|
1775
|
+
afterData: JSON.stringify({
|
|
1776
|
+
status: 'draft',
|
|
1777
|
+
total_amount_cents: this.toCents(data.total_amount),
|
|
1778
|
+
}),
|
|
1779
|
+
});
|
|
1528
1780
|
|
|
1529
|
-
|
|
1530
|
-
const titles = await this.prisma.financial_title.findMany({
|
|
1531
|
-
where: { title_type: type },
|
|
1532
|
-
include: this.defaultTitleInclude(),
|
|
1533
|
-
orderBy: { created_at: 'desc' },
|
|
1781
|
+
return title.id;
|
|
1534
1782
|
});
|
|
1535
1783
|
|
|
1536
|
-
|
|
1784
|
+
const createdTitle = await this.getTitleById(createdTitleId, titleType, locale);
|
|
1785
|
+
return this.mapTitleToFront(createdTitle, data.payment_channel);
|
|
1537
1786
|
}
|
|
1538
1787
|
|
|
1539
|
-
private
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
1788
|
+
private normalizeAndValidateInstallments(
|
|
1789
|
+
data: CreateFinancialTitleDto,
|
|
1790
|
+
locale: string,
|
|
1791
|
+
) {
|
|
1792
|
+
const fallbackDueDate = data.due_date;
|
|
1793
|
+
const totalAmountCents = this.toCents(data.total_amount);
|
|
1794
|
+
const sourceInstallments =
|
|
1795
|
+
data.installments && data.installments.length > 0
|
|
1796
|
+
? data.installments
|
|
1797
|
+
: [
|
|
1798
|
+
{
|
|
1799
|
+
installment_number: 1,
|
|
1800
|
+
due_date: fallbackDueDate,
|
|
1801
|
+
amount: data.total_amount,
|
|
1802
|
+
},
|
|
1803
|
+
];
|
|
1543
1804
|
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
|
|
1805
|
+
const normalizedInstallments = sourceInstallments.map(
|
|
1806
|
+
(installment, index) => {
|
|
1807
|
+
const installmentDueDate = installment.due_date || fallbackDueDate;
|
|
1808
|
+
|
|
1809
|
+
if (!installmentDueDate) {
|
|
1810
|
+
throw new BadRequestException(
|
|
1811
|
+
getLocaleText(
|
|
1812
|
+
'installmentDueDateRequired',
|
|
1813
|
+
locale,
|
|
1814
|
+
'Installment due date is required',
|
|
1815
|
+
),
|
|
1816
|
+
);
|
|
1817
|
+
}
|
|
1551
1818
|
|
|
1552
|
-
|
|
1553
|
-
const categories = await this.prisma.finance_category.findMany({
|
|
1554
|
-
orderBy: [{ code: 'asc' }, { name: 'asc' }],
|
|
1555
|
-
});
|
|
1819
|
+
const amountCents = this.toCents(installment.amount);
|
|
1556
1820
|
|
|
1557
|
-
|
|
1558
|
-
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
|
|
1566
|
-
: category.kind,
|
|
1567
|
-
status: category.status,
|
|
1568
|
-
}));
|
|
1569
|
-
}
|
|
1821
|
+
if (amountCents <= 0) {
|
|
1822
|
+
throw new BadRequestException(
|
|
1823
|
+
getLocaleText(
|
|
1824
|
+
'installmentAmountInvalid',
|
|
1825
|
+
locale,
|
|
1826
|
+
'Installment amount must be greater than zero',
|
|
1827
|
+
),
|
|
1828
|
+
);
|
|
1829
|
+
}
|
|
1570
1830
|
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
|
|
1574
|
-
|
|
1831
|
+
return {
|
|
1832
|
+
installment_number: installment.installment_number || index + 1,
|
|
1833
|
+
due_date: installmentDueDate,
|
|
1834
|
+
amount_cents: amountCents,
|
|
1835
|
+
};
|
|
1836
|
+
},
|
|
1837
|
+
);
|
|
1575
1838
|
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
status: costCenter.status,
|
|
1581
|
-
}));
|
|
1582
|
-
}
|
|
1839
|
+
const installmentsTotalCents = normalizedInstallments.reduce(
|
|
1840
|
+
(acc, installment) => acc + installment.amount_cents,
|
|
1841
|
+
0,
|
|
1842
|
+
);
|
|
1583
1843
|
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
|
|
1588
|
-
|
|
1589
|
-
|
|
1590
|
-
|
|
1591
|
-
|
|
1592
|
-
|
|
1593
|
-
},
|
|
1594
|
-
orderBy: [{ code: 'asc' }, { name: 'asc' }],
|
|
1595
|
-
});
|
|
1844
|
+
if (installmentsTotalCents !== totalAmountCents) {
|
|
1845
|
+
throw new BadRequestException(
|
|
1846
|
+
getLocaleText(
|
|
1847
|
+
'installmentsTotalMismatch',
|
|
1848
|
+
locale,
|
|
1849
|
+
'Installments total must be equal to title total amount',
|
|
1850
|
+
),
|
|
1851
|
+
);
|
|
1852
|
+
}
|
|
1596
1853
|
|
|
1597
|
-
return
|
|
1854
|
+
return normalizedInstallments;
|
|
1598
1855
|
}
|
|
1599
1856
|
|
|
1600
|
-
private async
|
|
1601
|
-
|
|
1602
|
-
|
|
1603
|
-
|
|
1857
|
+
private async approveTitle(
|
|
1858
|
+
titleId: number,
|
|
1859
|
+
titleType: TitleType,
|
|
1860
|
+
locale: string,
|
|
1861
|
+
userId?: number,
|
|
1862
|
+
) {
|
|
1863
|
+
const updatedTitle = await this.prisma.$transaction(async (tx) => {
|
|
1864
|
+
const title = await tx.financial_title.findFirst({
|
|
1865
|
+
where: {
|
|
1866
|
+
id: titleId,
|
|
1867
|
+
title_type: titleType,
|
|
1868
|
+
},
|
|
1869
|
+
select: {
|
|
1870
|
+
id: true,
|
|
1871
|
+
status: true,
|
|
1872
|
+
competence_date: true,
|
|
1873
|
+
},
|
|
1874
|
+
});
|
|
1604
1875
|
|
|
1605
|
-
|
|
1606
|
-
|
|
1607
|
-
|
|
1608
|
-
|
|
1609
|
-
|
|
1610
|
-
|
|
1876
|
+
if (!title) {
|
|
1877
|
+
throw new NotFoundException(
|
|
1878
|
+
getLocaleText(
|
|
1879
|
+
'itemNotFound',
|
|
1880
|
+
locale,
|
|
1881
|
+
`Financial title with ID ${titleId} not found`,
|
|
1882
|
+
).replace('{{item}}', 'Financial title'),
|
|
1883
|
+
);
|
|
1884
|
+
}
|
|
1611
1885
|
|
|
1612
|
-
|
|
1613
|
-
|
|
1614
|
-
|
|
1615
|
-
|
|
1616
|
-
|
|
1886
|
+
await this.assertDateNotInClosedPeriod(
|
|
1887
|
+
tx,
|
|
1888
|
+
title.competence_date,
|
|
1889
|
+
'approve title',
|
|
1890
|
+
);
|
|
1617
1891
|
|
|
1618
|
-
|
|
1619
|
-
|
|
1620
|
-
|
|
1892
|
+
if (title.status !== 'draft') {
|
|
1893
|
+
throw new BadRequestException('Only draft titles can be approved');
|
|
1894
|
+
}
|
|
1895
|
+
|
|
1896
|
+
await tx.financial_title.update({
|
|
1897
|
+
where: { id: title.id },
|
|
1898
|
+
data: {
|
|
1899
|
+
status: 'approved',
|
|
1900
|
+
},
|
|
1901
|
+
});
|
|
1902
|
+
|
|
1903
|
+
await this.createAuditLog(tx, {
|
|
1904
|
+
action: 'APPROVE_TITLE',
|
|
1905
|
+
entityTable: 'financial_title',
|
|
1906
|
+
entityId: String(title.id),
|
|
1907
|
+
actorUserId: userId,
|
|
1908
|
+
summary: `Approved ${titleType} title ${title.id}`,
|
|
1909
|
+
beforeData: JSON.stringify({ status: title.status }),
|
|
1910
|
+
afterData: JSON.stringify({ status: 'approved' }),
|
|
1911
|
+
});
|
|
1912
|
+
|
|
1913
|
+
return tx.financial_title.findFirst({
|
|
1914
|
+
where: {
|
|
1915
|
+
id: title.id,
|
|
1916
|
+
title_type: titleType,
|
|
1917
|
+
},
|
|
1918
|
+
include: this.defaultTitleInclude(),
|
|
1919
|
+
});
|
|
1920
|
+
});
|
|
1921
|
+
|
|
1922
|
+
if (!updatedTitle) {
|
|
1923
|
+
throw new NotFoundException('Financial title not found');
|
|
1924
|
+
}
|
|
1925
|
+
|
|
1926
|
+
return this.mapTitleToFront(updatedTitle);
|
|
1927
|
+
}
|
|
1928
|
+
|
|
1929
|
+
private async rejectTitle(
|
|
1930
|
+
titleId: number,
|
|
1931
|
+
data: RejectTitleDto,
|
|
1932
|
+
titleType: TitleType,
|
|
1933
|
+
locale: string,
|
|
1934
|
+
userId?: number,
|
|
1935
|
+
) {
|
|
1936
|
+
const updatedTitle = await this.prisma.$transaction(async (tx) => {
|
|
1937
|
+
const title = await tx.financial_title.findFirst({
|
|
1938
|
+
where: {
|
|
1939
|
+
id: titleId,
|
|
1940
|
+
title_type: titleType,
|
|
1941
|
+
},
|
|
1942
|
+
select: {
|
|
1943
|
+
id: true,
|
|
1944
|
+
status: true,
|
|
1945
|
+
competence_date: true,
|
|
1946
|
+
},
|
|
1947
|
+
});
|
|
1948
|
+
|
|
1949
|
+
if (!title) {
|
|
1950
|
+
throw new NotFoundException(
|
|
1951
|
+
getLocaleText(
|
|
1952
|
+
'itemNotFound',
|
|
1953
|
+
locale,
|
|
1954
|
+
`Financial title with ID ${titleId} not found`,
|
|
1955
|
+
).replace('{{item}}', 'Financial title'),
|
|
1956
|
+
);
|
|
1957
|
+
}
|
|
1958
|
+
|
|
1959
|
+
await this.assertDateNotInClosedPeriod(
|
|
1960
|
+
tx,
|
|
1961
|
+
title.competence_date,
|
|
1962
|
+
'reject title',
|
|
1963
|
+
);
|
|
1964
|
+
|
|
1965
|
+
if (title.status !== 'draft') {
|
|
1966
|
+
throw new BadRequestException('Only draft titles can be rejected');
|
|
1967
|
+
}
|
|
1968
|
+
|
|
1969
|
+
await tx.financial_title.update({
|
|
1970
|
+
where: { id: title.id },
|
|
1971
|
+
data: {
|
|
1972
|
+
status: 'canceled',
|
|
1973
|
+
},
|
|
1974
|
+
});
|
|
1975
|
+
|
|
1976
|
+
await this.createAuditLog(tx, {
|
|
1977
|
+
action: 'REJECT_TITLE',
|
|
1978
|
+
entityTable: 'financial_title',
|
|
1979
|
+
entityId: String(title.id),
|
|
1980
|
+
actorUserId: userId,
|
|
1981
|
+
summary: `Rejected ${titleType} title ${title.id}`,
|
|
1982
|
+
beforeData: JSON.stringify({ status: title.status }),
|
|
1983
|
+
afterData: JSON.stringify({
|
|
1984
|
+
status: 'canceled',
|
|
1985
|
+
reason: data?.reason || null,
|
|
1986
|
+
}),
|
|
1987
|
+
});
|
|
1988
|
+
|
|
1989
|
+
return tx.financial_title.findFirst({
|
|
1990
|
+
where: {
|
|
1991
|
+
id: title.id,
|
|
1992
|
+
title_type: titleType,
|
|
1993
|
+
},
|
|
1994
|
+
include: this.defaultTitleInclude(),
|
|
1995
|
+
});
|
|
1996
|
+
});
|
|
1997
|
+
|
|
1998
|
+
if (!updatedTitle) {
|
|
1999
|
+
throw new NotFoundException('Financial title not found');
|
|
2000
|
+
}
|
|
2001
|
+
|
|
2002
|
+
return this.mapTitleToFront(updatedTitle);
|
|
2003
|
+
}
|
|
2004
|
+
|
|
2005
|
+
private async settleTitleInstallment(
|
|
2006
|
+
titleId: number,
|
|
2007
|
+
data: SettleInstallmentDto,
|
|
2008
|
+
titleType: TitleType,
|
|
2009
|
+
locale: string,
|
|
2010
|
+
userId?: number,
|
|
2011
|
+
) {
|
|
2012
|
+
const amountCents = this.toCents(data.amount);
|
|
2013
|
+
|
|
2014
|
+
if (amountCents <= 0) {
|
|
2015
|
+
throw new BadRequestException('Settlement amount must be greater than zero');
|
|
2016
|
+
}
|
|
2017
|
+
|
|
2018
|
+
const settledAt = data.settled_at ? new Date(data.settled_at) : new Date();
|
|
2019
|
+
if (Number.isNaN(settledAt.getTime())) {
|
|
2020
|
+
throw new BadRequestException('Invalid settlement date');
|
|
2021
|
+
}
|
|
2022
|
+
|
|
2023
|
+
const result = await this.prisma.$transaction(async (tx) => {
|
|
2024
|
+
const title = await tx.financial_title.findFirst({
|
|
2025
|
+
where: {
|
|
2026
|
+
id: titleId,
|
|
2027
|
+
title_type: titleType,
|
|
2028
|
+
},
|
|
2029
|
+
select: {
|
|
2030
|
+
id: true,
|
|
2031
|
+
person_id: true,
|
|
2032
|
+
status: true,
|
|
2033
|
+
competence_date: true,
|
|
2034
|
+
},
|
|
2035
|
+
});
|
|
2036
|
+
|
|
2037
|
+
if (!title) {
|
|
2038
|
+
throw new NotFoundException(
|
|
2039
|
+
getLocaleText(
|
|
2040
|
+
'itemNotFound',
|
|
2041
|
+
locale,
|
|
2042
|
+
`Financial title with ID ${titleId} not found`,
|
|
2043
|
+
).replace('{{item}}', 'Financial title'),
|
|
2044
|
+
);
|
|
2045
|
+
}
|
|
2046
|
+
|
|
2047
|
+
if (!['approved', 'open', 'partial'].includes(title.status)) {
|
|
2048
|
+
throw new BadRequestException(
|
|
2049
|
+
'Only approved/open/partial titles can be settled',
|
|
2050
|
+
);
|
|
2051
|
+
}
|
|
2052
|
+
|
|
2053
|
+
await this.assertDateNotInClosedPeriod(
|
|
2054
|
+
tx,
|
|
2055
|
+
title.competence_date,
|
|
2056
|
+
'settle installment',
|
|
2057
|
+
);
|
|
2058
|
+
|
|
2059
|
+
const installment = await tx.financial_installment.findFirst({
|
|
2060
|
+
where: {
|
|
2061
|
+
id: data.installment_id,
|
|
2062
|
+
title_id: title.id,
|
|
2063
|
+
},
|
|
2064
|
+
select: {
|
|
2065
|
+
id: true,
|
|
2066
|
+
title_id: true,
|
|
2067
|
+
amount_cents: true,
|
|
2068
|
+
open_amount_cents: true,
|
|
2069
|
+
due_date: true,
|
|
2070
|
+
status: true,
|
|
2071
|
+
},
|
|
2072
|
+
});
|
|
2073
|
+
|
|
2074
|
+
if (!installment) {
|
|
2075
|
+
throw new BadRequestException('Installment not found for this title');
|
|
2076
|
+
}
|
|
2077
|
+
|
|
2078
|
+
if (installment.status === 'settled' || installment.status === 'canceled') {
|
|
2079
|
+
throw new BadRequestException('This installment cannot be settled');
|
|
2080
|
+
}
|
|
2081
|
+
|
|
2082
|
+
if (amountCents > installment.open_amount_cents) {
|
|
2083
|
+
throw new BadRequestException('Settlement amount exceeds open amount');
|
|
2084
|
+
}
|
|
2085
|
+
|
|
2086
|
+
const paymentMethodId = await this.resolvePaymentMethodId(
|
|
2087
|
+
tx,
|
|
2088
|
+
data.payment_channel,
|
|
2089
|
+
);
|
|
2090
|
+
|
|
2091
|
+
const settlement = await tx.settlement.create({
|
|
2092
|
+
data: {
|
|
2093
|
+
person_id: title.person_id,
|
|
2094
|
+
bank_account_id: data.bank_account_id || null,
|
|
2095
|
+
payment_method_id: paymentMethodId,
|
|
2096
|
+
settlement_type: titleType,
|
|
2097
|
+
status: 'confirmed',
|
|
2098
|
+
settled_at: settledAt,
|
|
2099
|
+
amount_cents: amountCents,
|
|
2100
|
+
description: data.description?.trim() || null,
|
|
2101
|
+
created_by_user_id: userId,
|
|
2102
|
+
},
|
|
2103
|
+
});
|
|
2104
|
+
|
|
2105
|
+
await tx.settlement_allocation.create({
|
|
2106
|
+
data: {
|
|
2107
|
+
settlement_id: settlement.id,
|
|
2108
|
+
installment_id: installment.id,
|
|
2109
|
+
allocated_amount_cents: amountCents,
|
|
2110
|
+
discount_cents: this.toCents(data.discount || 0),
|
|
2111
|
+
interest_cents: this.toCents(data.interest || 0),
|
|
2112
|
+
penalty_cents: this.toCents(data.penalty || 0),
|
|
2113
|
+
},
|
|
2114
|
+
});
|
|
2115
|
+
|
|
2116
|
+
const decrementResult = await tx.financial_installment.updateMany({
|
|
2117
|
+
where: {
|
|
2118
|
+
id: installment.id,
|
|
2119
|
+
open_amount_cents: {
|
|
2120
|
+
gte: amountCents,
|
|
2121
|
+
},
|
|
2122
|
+
},
|
|
2123
|
+
data: {
|
|
2124
|
+
open_amount_cents: {
|
|
2125
|
+
decrement: amountCents,
|
|
2126
|
+
},
|
|
2127
|
+
},
|
|
2128
|
+
});
|
|
2129
|
+
|
|
2130
|
+
if (decrementResult.count !== 1) {
|
|
2131
|
+
throw new BadRequestException(
|
|
2132
|
+
'Installment was updated concurrently, please try again',
|
|
2133
|
+
);
|
|
2134
|
+
}
|
|
2135
|
+
|
|
2136
|
+
const updatedInstallment = await tx.financial_installment.findUnique({
|
|
2137
|
+
where: {
|
|
2138
|
+
id: installment.id,
|
|
2139
|
+
},
|
|
2140
|
+
select: {
|
|
2141
|
+
id: true,
|
|
2142
|
+
amount_cents: true,
|
|
2143
|
+
open_amount_cents: true,
|
|
2144
|
+
due_date: true,
|
|
2145
|
+
status: true,
|
|
2146
|
+
},
|
|
2147
|
+
});
|
|
2148
|
+
|
|
2149
|
+
if (!updatedInstallment) {
|
|
2150
|
+
throw new NotFoundException('Installment not found');
|
|
2151
|
+
}
|
|
2152
|
+
|
|
2153
|
+
const nextInstallmentStatus = this.resolveInstallmentStatus(
|
|
2154
|
+
updatedInstallment.amount_cents,
|
|
2155
|
+
updatedInstallment.open_amount_cents,
|
|
2156
|
+
updatedInstallment.due_date,
|
|
2157
|
+
);
|
|
2158
|
+
|
|
2159
|
+
if (updatedInstallment.status !== nextInstallmentStatus) {
|
|
2160
|
+
await tx.financial_installment.update({
|
|
2161
|
+
where: {
|
|
2162
|
+
id: updatedInstallment.id,
|
|
2163
|
+
},
|
|
2164
|
+
data: {
|
|
2165
|
+
status: nextInstallmentStatus,
|
|
2166
|
+
},
|
|
2167
|
+
});
|
|
2168
|
+
}
|
|
2169
|
+
|
|
2170
|
+
const previousTitleStatus = title.status;
|
|
2171
|
+
const nextTitleStatus = await this.recalculateTitleStatus(tx, title.id);
|
|
2172
|
+
|
|
2173
|
+
await this.createAuditLog(tx, {
|
|
2174
|
+
action: 'SETTLE_INSTALLMENT',
|
|
2175
|
+
entityTable: 'financial_title',
|
|
2176
|
+
entityId: String(title.id),
|
|
2177
|
+
actorUserId: userId,
|
|
2178
|
+
summary: `Settled installment ${installment.id} of title ${title.id}`,
|
|
2179
|
+
beforeData: JSON.stringify({
|
|
2180
|
+
title_status: previousTitleStatus,
|
|
2181
|
+
installment_open_amount_cents: installment.open_amount_cents,
|
|
2182
|
+
}),
|
|
2183
|
+
afterData: JSON.stringify({
|
|
2184
|
+
title_status: nextTitleStatus,
|
|
2185
|
+
installment_open_amount_cents: updatedInstallment.open_amount_cents,
|
|
2186
|
+
settlement_id: settlement.id,
|
|
2187
|
+
}),
|
|
2188
|
+
});
|
|
2189
|
+
|
|
2190
|
+
const updatedTitle = await tx.financial_title.findFirst({
|
|
2191
|
+
where: {
|
|
2192
|
+
id: title.id,
|
|
2193
|
+
title_type: titleType,
|
|
2194
|
+
},
|
|
2195
|
+
include: this.defaultTitleInclude(),
|
|
2196
|
+
});
|
|
2197
|
+
|
|
2198
|
+
if (!updatedTitle) {
|
|
2199
|
+
throw new NotFoundException('Financial title not found');
|
|
2200
|
+
}
|
|
2201
|
+
|
|
2202
|
+
return {
|
|
2203
|
+
title: updatedTitle,
|
|
2204
|
+
settlementId: settlement.id,
|
|
2205
|
+
};
|
|
2206
|
+
});
|
|
2207
|
+
|
|
2208
|
+
return {
|
|
2209
|
+
...this.mapTitleToFront(result.title),
|
|
2210
|
+
settlementId: String(result.settlementId),
|
|
2211
|
+
};
|
|
2212
|
+
}
|
|
2213
|
+
|
|
2214
|
+
private async reverseTitleSettlement(
|
|
2215
|
+
titleId: number,
|
|
2216
|
+
settlementId: number,
|
|
2217
|
+
data: ReverseSettlementDto,
|
|
2218
|
+
titleType: TitleType,
|
|
2219
|
+
locale: string,
|
|
2220
|
+
userId?: number,
|
|
2221
|
+
) {
|
|
2222
|
+
const updatedTitle = await this.prisma.$transaction(async (tx) => {
|
|
2223
|
+
const title = await tx.financial_title.findFirst({
|
|
2224
|
+
where: {
|
|
2225
|
+
id: titleId,
|
|
2226
|
+
title_type: titleType,
|
|
2227
|
+
},
|
|
2228
|
+
select: {
|
|
2229
|
+
id: true,
|
|
2230
|
+
status: true,
|
|
2231
|
+
competence_date: true,
|
|
2232
|
+
},
|
|
2233
|
+
});
|
|
2234
|
+
|
|
2235
|
+
if (!title) {
|
|
2236
|
+
throw new NotFoundException(
|
|
2237
|
+
getLocaleText(
|
|
2238
|
+
'itemNotFound',
|
|
2239
|
+
locale,
|
|
2240
|
+
`Financial title with ID ${titleId} not found`,
|
|
2241
|
+
).replace('{{item}}', 'Financial title'),
|
|
2242
|
+
);
|
|
2243
|
+
}
|
|
2244
|
+
|
|
2245
|
+
await this.assertDateNotInClosedPeriod(
|
|
2246
|
+
tx,
|
|
2247
|
+
title.competence_date,
|
|
2248
|
+
'reverse settlement',
|
|
2249
|
+
);
|
|
2250
|
+
|
|
2251
|
+
const settlement = await tx.settlement.findFirst({
|
|
2252
|
+
where: {
|
|
2253
|
+
id: settlementId,
|
|
2254
|
+
settlement_type: titleType,
|
|
2255
|
+
settlement_allocation: {
|
|
2256
|
+
some: {
|
|
2257
|
+
financial_installment: {
|
|
2258
|
+
title_id: title.id,
|
|
2259
|
+
},
|
|
2260
|
+
},
|
|
2261
|
+
},
|
|
2262
|
+
},
|
|
2263
|
+
include: {
|
|
2264
|
+
settlement_allocation: {
|
|
2265
|
+
include: {
|
|
2266
|
+
financial_installment: {
|
|
2267
|
+
select: {
|
|
2268
|
+
id: true,
|
|
2269
|
+
amount_cents: true,
|
|
2270
|
+
open_amount_cents: true,
|
|
2271
|
+
due_date: true,
|
|
2272
|
+
status: true,
|
|
2273
|
+
},
|
|
2274
|
+
},
|
|
2275
|
+
},
|
|
2276
|
+
},
|
|
2277
|
+
},
|
|
2278
|
+
});
|
|
2279
|
+
|
|
2280
|
+
if (!settlement) {
|
|
2281
|
+
throw new NotFoundException('Settlement not found for this title');
|
|
2282
|
+
}
|
|
2283
|
+
|
|
2284
|
+
if (settlement.status === 'reversed') {
|
|
2285
|
+
throw new BadRequestException('This settlement is already reversed');
|
|
2286
|
+
}
|
|
2287
|
+
|
|
2288
|
+
for (const allocation of settlement.settlement_allocation) {
|
|
2289
|
+
const installment = allocation.financial_installment;
|
|
2290
|
+
|
|
2291
|
+
if (!installment) {
|
|
2292
|
+
continue;
|
|
2293
|
+
}
|
|
2294
|
+
|
|
2295
|
+
const nextOpenAmountCents =
|
|
2296
|
+
installment.open_amount_cents + allocation.allocated_amount_cents;
|
|
2297
|
+
|
|
2298
|
+
if (nextOpenAmountCents > installment.amount_cents) {
|
|
2299
|
+
throw new BadRequestException(
|
|
2300
|
+
`Reverse would exceed installment amount for installment ${installment.id}`,
|
|
2301
|
+
);
|
|
2302
|
+
}
|
|
2303
|
+
|
|
2304
|
+
const nextInstallmentStatus = this.resolveInstallmentStatus(
|
|
2305
|
+
installment.amount_cents,
|
|
2306
|
+
nextOpenAmountCents,
|
|
2307
|
+
installment.due_date,
|
|
2308
|
+
);
|
|
2309
|
+
|
|
2310
|
+
await tx.financial_installment.update({
|
|
2311
|
+
where: {
|
|
2312
|
+
id: installment.id,
|
|
2313
|
+
},
|
|
2314
|
+
data: {
|
|
2315
|
+
open_amount_cents: nextOpenAmountCents,
|
|
2316
|
+
status: nextInstallmentStatus,
|
|
2317
|
+
},
|
|
2318
|
+
});
|
|
2319
|
+
}
|
|
2320
|
+
|
|
2321
|
+
await tx.settlement.update({
|
|
2322
|
+
where: {
|
|
2323
|
+
id: settlement.id,
|
|
2324
|
+
},
|
|
2325
|
+
data: {
|
|
2326
|
+
status: 'reversed',
|
|
2327
|
+
description: [
|
|
2328
|
+
settlement.description,
|
|
2329
|
+
data.reason ? `Reversed: ${data.reason.trim()}` : 'Reversed',
|
|
2330
|
+
]
|
|
2331
|
+
.filter(Boolean)
|
|
2332
|
+
.join(' | '),
|
|
2333
|
+
},
|
|
2334
|
+
});
|
|
2335
|
+
|
|
2336
|
+
const previousTitleStatus = title.status;
|
|
2337
|
+
const nextTitleStatus = await this.recalculateTitleStatus(tx, title.id);
|
|
2338
|
+
|
|
2339
|
+
await this.createAuditLog(tx, {
|
|
2340
|
+
action: 'REVERSE_SETTLEMENT',
|
|
2341
|
+
entityTable: 'financial_title',
|
|
2342
|
+
entityId: String(title.id),
|
|
2343
|
+
actorUserId: userId,
|
|
2344
|
+
summary: `Reversed settlement ${settlement.id} from title ${title.id}`,
|
|
2345
|
+
beforeData: JSON.stringify({
|
|
2346
|
+
title_status: previousTitleStatus,
|
|
2347
|
+
settlement_status: settlement.status,
|
|
2348
|
+
}),
|
|
2349
|
+
afterData: JSON.stringify({
|
|
2350
|
+
title_status: nextTitleStatus,
|
|
2351
|
+
settlement_status: 'reversed',
|
|
2352
|
+
}),
|
|
2353
|
+
});
|
|
2354
|
+
|
|
2355
|
+
return tx.financial_title.findFirst({
|
|
2356
|
+
where: {
|
|
2357
|
+
id: title.id,
|
|
2358
|
+
title_type: titleType,
|
|
2359
|
+
},
|
|
2360
|
+
include: this.defaultTitleInclude(),
|
|
2361
|
+
});
|
|
2362
|
+
});
|
|
2363
|
+
|
|
2364
|
+
if (!updatedTitle) {
|
|
2365
|
+
throw new NotFoundException('Financial title not found');
|
|
2366
|
+
}
|
|
2367
|
+
|
|
2368
|
+
return this.mapTitleToFront(updatedTitle);
|
|
2369
|
+
}
|
|
2370
|
+
|
|
2371
|
+
private async updateTitleTags(
|
|
2372
|
+
titleId: number,
|
|
2373
|
+
titleType: TitleType,
|
|
2374
|
+
tagIds: number[],
|
|
2375
|
+
locale: string,
|
|
2376
|
+
) {
|
|
2377
|
+
const title = await this.getTitleById(titleId, titleType, locale);
|
|
2378
|
+
|
|
2379
|
+
const installmentIds = (title.financial_installment || []).map(
|
|
2380
|
+
(installment) => installment.id,
|
|
2381
|
+
);
|
|
2382
|
+
|
|
2383
|
+
if (installmentIds.length === 0) {
|
|
2384
|
+
throw new BadRequestException('Financial title has no installments');
|
|
2385
|
+
}
|
|
2386
|
+
|
|
2387
|
+
const normalizedTagIds = [
|
|
2388
|
+
...new Set(
|
|
2389
|
+
(tagIds || [])
|
|
2390
|
+
.map((tagId) => Number(tagId))
|
|
2391
|
+
.filter((tagId) => Number.isInteger(tagId) && tagId > 0),
|
|
2392
|
+
),
|
|
2393
|
+
];
|
|
2394
|
+
|
|
2395
|
+
if (normalizedTagIds.length > 0) {
|
|
2396
|
+
const existingTags = await this.prisma.tag.findMany({
|
|
2397
|
+
where: {
|
|
2398
|
+
id: {
|
|
2399
|
+
in: normalizedTagIds,
|
|
2400
|
+
},
|
|
2401
|
+
},
|
|
2402
|
+
select: {
|
|
2403
|
+
id: true,
|
|
2404
|
+
},
|
|
2405
|
+
});
|
|
2406
|
+
|
|
2407
|
+
const existingTagIds = new Set(existingTags.map((tag) => tag.id));
|
|
2408
|
+
const invalidTagIds = normalizedTagIds.filter(
|
|
2409
|
+
(tagId) => !existingTagIds.has(tagId),
|
|
2410
|
+
);
|
|
2411
|
+
|
|
2412
|
+
if (invalidTagIds.length > 0) {
|
|
2413
|
+
throw new BadRequestException(
|
|
2414
|
+
`Invalid tag IDs: ${invalidTagIds.join(', ')}`,
|
|
2415
|
+
);
|
|
2416
|
+
}
|
|
2417
|
+
}
|
|
2418
|
+
|
|
2419
|
+
await this.prisma.$transaction(async (tx) => {
|
|
2420
|
+
if (normalizedTagIds.length === 0) {
|
|
2421
|
+
await tx.financial_installment_tag.deleteMany({
|
|
2422
|
+
where: {
|
|
2423
|
+
installment_id: {
|
|
2424
|
+
in: installmentIds,
|
|
2425
|
+
},
|
|
2426
|
+
},
|
|
2427
|
+
});
|
|
2428
|
+
|
|
2429
|
+
return;
|
|
2430
|
+
}
|
|
2431
|
+
|
|
2432
|
+
await tx.financial_installment_tag.deleteMany({
|
|
2433
|
+
where: {
|
|
2434
|
+
installment_id: {
|
|
2435
|
+
in: installmentIds,
|
|
2436
|
+
},
|
|
2437
|
+
tag_id: {
|
|
2438
|
+
notIn: normalizedTagIds,
|
|
2439
|
+
},
|
|
2440
|
+
},
|
|
2441
|
+
});
|
|
2442
|
+
|
|
2443
|
+
const newRelations = installmentIds.flatMap((installmentId) =>
|
|
2444
|
+
normalizedTagIds.map((tagId) => ({
|
|
2445
|
+
installment_id: installmentId,
|
|
2446
|
+
tag_id: tagId,
|
|
2447
|
+
})),
|
|
2448
|
+
);
|
|
2449
|
+
|
|
2450
|
+
await tx.financial_installment_tag.createMany({
|
|
2451
|
+
data: newRelations,
|
|
2452
|
+
skipDuplicates: true,
|
|
2453
|
+
});
|
|
2454
|
+
});
|
|
2455
|
+
|
|
2456
|
+
const updatedTitle = await this.getTitleById(titleId, titleType, locale);
|
|
2457
|
+
return this.mapTitleToFront(updatedTitle);
|
|
2458
|
+
}
|
|
2459
|
+
|
|
2460
|
+
private async loadTitles(type: TitleType) {
|
|
2461
|
+
const titles = await this.prisma.financial_title.findMany({
|
|
2462
|
+
where: { title_type: type },
|
|
2463
|
+
include: this.defaultTitleInclude(),
|
|
2464
|
+
orderBy: { created_at: 'desc' },
|
|
2465
|
+
});
|
|
2466
|
+
|
|
2467
|
+
return titles.map((title) => this.mapTitleToFront(title));
|
|
2468
|
+
}
|
|
2469
|
+
|
|
2470
|
+
private async loadPeople() {
|
|
2471
|
+
const people = await this.prisma.person.findMany({
|
|
2472
|
+
orderBy: { name: 'asc' },
|
|
2473
|
+
});
|
|
2474
|
+
|
|
2475
|
+
return people.map((person) => ({
|
|
2476
|
+
id: String(person.id),
|
|
2477
|
+
nome: person.name,
|
|
2478
|
+
tipo: 'ambos',
|
|
2479
|
+
documento: '',
|
|
2480
|
+
}));
|
|
2481
|
+
}
|
|
2482
|
+
|
|
2483
|
+
private async loadCategories() {
|
|
2484
|
+
const categories = await this.prisma.finance_category.findMany({
|
|
2485
|
+
orderBy: [{ code: 'asc' }, { name: 'asc' }],
|
|
2486
|
+
});
|
|
2487
|
+
|
|
2488
|
+
return categories.map((category) => ({
|
|
2489
|
+
id: String(category.id),
|
|
2490
|
+
codigo: category.code,
|
|
2491
|
+
nome: category.name,
|
|
2492
|
+
natureza:
|
|
2493
|
+
category.kind === 'revenue'
|
|
2494
|
+
? 'receita'
|
|
2495
|
+
: category.kind === 'expense'
|
|
2496
|
+
? 'despesa'
|
|
2497
|
+
: category.kind,
|
|
2498
|
+
status: category.status,
|
|
2499
|
+
}));
|
|
2500
|
+
}
|
|
2501
|
+
|
|
2502
|
+
private async loadCostCenters() {
|
|
2503
|
+
const costCenters = await this.prisma.cost_center.findMany({
|
|
2504
|
+
orderBy: [{ code: 'asc' }, { name: 'asc' }],
|
|
2505
|
+
});
|
|
2506
|
+
|
|
2507
|
+
return costCenters.map((costCenter) => ({
|
|
2508
|
+
id: String(costCenter.id),
|
|
2509
|
+
codigo: costCenter.code,
|
|
2510
|
+
nome: costCenter.name,
|
|
2511
|
+
status: costCenter.status,
|
|
2512
|
+
}));
|
|
2513
|
+
}
|
|
2514
|
+
|
|
2515
|
+
private async loadBankAccounts() {
|
|
2516
|
+
const bankAccounts = await this.prisma.bank_account.findMany({
|
|
2517
|
+
include: {
|
|
2518
|
+
bank_statement_line: {
|
|
2519
|
+
select: {
|
|
2520
|
+
amount_cents: true,
|
|
2521
|
+
status: true,
|
|
2522
|
+
},
|
|
2523
|
+
},
|
|
2524
|
+
},
|
|
2525
|
+
orderBy: [{ code: 'asc' }, { name: 'asc' }],
|
|
2526
|
+
});
|
|
2527
|
+
|
|
2528
|
+
return bankAccounts.map((bankAccount) => this.mapBankAccountToFront(bankAccount));
|
|
2529
|
+
}
|
|
2530
|
+
|
|
2531
|
+
private async loadTags() {
|
|
2532
|
+
const tags = await this.prisma.tag.findMany({
|
|
2533
|
+
orderBy: { slug: 'asc' },
|
|
2534
|
+
});
|
|
2535
|
+
|
|
2536
|
+
return tags.map((tag) => ({
|
|
2537
|
+
id: String(tag.id),
|
|
2538
|
+
nome: tag.slug,
|
|
2539
|
+
cor: tag.color,
|
|
2540
|
+
}));
|
|
2541
|
+
}
|
|
2542
|
+
|
|
2543
|
+
private async loadAuditLogs() {
|
|
2544
|
+
const logs = await this.prisma.audit_log.findMany({
|
|
2545
|
+
orderBy: { created_at: 'desc' },
|
|
2546
|
+
take: 500,
|
|
2547
|
+
});
|
|
2548
|
+
|
|
2549
|
+
return logs.map((log) => ({
|
|
2550
|
+
id: String(log.id),
|
|
2551
|
+
entidade: log.entity_table === 'financial_title' ? 'TituloPagar' : log.entity_table,
|
|
1621
2552
|
entidadeId: log.entity_id,
|
|
1622
2553
|
usuarioId: log.actor_user_id ? String(log.actor_user_id) : null,
|
|
1623
2554
|
acao: log.action,
|
|
@@ -1628,6 +2559,189 @@ export class FinanceService {
|
|
|
1628
2559
|
}));
|
|
1629
2560
|
}
|
|
1630
2561
|
|
|
2562
|
+
private async resolvePaymentMethodId(tx: any, paymentChannel?: string) {
|
|
2563
|
+
const paymentType = this.mapPaymentMethodFromPt(paymentChannel);
|
|
2564
|
+
|
|
2565
|
+
if (!paymentType) {
|
|
2566
|
+
return null;
|
|
2567
|
+
}
|
|
2568
|
+
|
|
2569
|
+
const paymentMethod = await tx.payment_method.findFirst({
|
|
2570
|
+
where: {
|
|
2571
|
+
type: paymentType,
|
|
2572
|
+
status: 'active',
|
|
2573
|
+
},
|
|
2574
|
+
select: {
|
|
2575
|
+
id: true,
|
|
2576
|
+
},
|
|
2577
|
+
});
|
|
2578
|
+
|
|
2579
|
+
return paymentMethod?.id || null;
|
|
2580
|
+
}
|
|
2581
|
+
|
|
2582
|
+
private async assertDateNotInClosedPeriod(
|
|
2583
|
+
tx: any,
|
|
2584
|
+
competenceDate: Date | null | undefined,
|
|
2585
|
+
operation: string,
|
|
2586
|
+
) {
|
|
2587
|
+
if (!competenceDate) {
|
|
2588
|
+
return;
|
|
2589
|
+
}
|
|
2590
|
+
|
|
2591
|
+
const closedPeriod = await tx.period_close.findFirst({
|
|
2592
|
+
where: {
|
|
2593
|
+
status: 'closed',
|
|
2594
|
+
period_start: {
|
|
2595
|
+
lte: competenceDate,
|
|
2596
|
+
},
|
|
2597
|
+
period_end: {
|
|
2598
|
+
gte: competenceDate,
|
|
2599
|
+
},
|
|
2600
|
+
},
|
|
2601
|
+
select: {
|
|
2602
|
+
id: true,
|
|
2603
|
+
},
|
|
2604
|
+
});
|
|
2605
|
+
|
|
2606
|
+
if (closedPeriod) {
|
|
2607
|
+
throw new BadRequestException(
|
|
2608
|
+
`Cannot ${operation}: competence is in a closed period`,
|
|
2609
|
+
);
|
|
2610
|
+
}
|
|
2611
|
+
}
|
|
2612
|
+
|
|
2613
|
+
private resolveInstallmentStatus(
|
|
2614
|
+
amountCents: number,
|
|
2615
|
+
openAmountCents: number,
|
|
2616
|
+
dueDate: Date,
|
|
2617
|
+
currentStatus?: string,
|
|
2618
|
+
): InstallmentStatus {
|
|
2619
|
+
if (currentStatus === 'canceled') {
|
|
2620
|
+
return 'canceled';
|
|
2621
|
+
}
|
|
2622
|
+
|
|
2623
|
+
if (openAmountCents <= 0) {
|
|
2624
|
+
return 'settled';
|
|
2625
|
+
}
|
|
2626
|
+
|
|
2627
|
+
if (openAmountCents < amountCents) {
|
|
2628
|
+
return 'partial';
|
|
2629
|
+
}
|
|
2630
|
+
|
|
2631
|
+
const today = new Date();
|
|
2632
|
+
today.setHours(0, 0, 0, 0);
|
|
2633
|
+
const dueDateOnly = new Date(dueDate);
|
|
2634
|
+
dueDateOnly.setHours(0, 0, 0, 0);
|
|
2635
|
+
|
|
2636
|
+
if (dueDateOnly < today) {
|
|
2637
|
+
return 'overdue';
|
|
2638
|
+
}
|
|
2639
|
+
|
|
2640
|
+
return 'open';
|
|
2641
|
+
}
|
|
2642
|
+
|
|
2643
|
+
private deriveTitleStatusFromInstallments(
|
|
2644
|
+
installments: Array<{
|
|
2645
|
+
amount_cents: number;
|
|
2646
|
+
open_amount_cents: number;
|
|
2647
|
+
due_date: Date;
|
|
2648
|
+
status?: string;
|
|
2649
|
+
}>,
|
|
2650
|
+
): TitleStatus {
|
|
2651
|
+
if (installments.length === 0) {
|
|
2652
|
+
return 'open';
|
|
2653
|
+
}
|
|
2654
|
+
|
|
2655
|
+
const effectiveStatuses = installments.map((installment) =>
|
|
2656
|
+
this.resolveInstallmentStatus(
|
|
2657
|
+
installment.amount_cents,
|
|
2658
|
+
installment.open_amount_cents,
|
|
2659
|
+
installment.due_date,
|
|
2660
|
+
installment.status,
|
|
2661
|
+
),
|
|
2662
|
+
);
|
|
2663
|
+
|
|
2664
|
+
if (effectiveStatuses.every((status) => status === 'settled')) {
|
|
2665
|
+
return 'settled';
|
|
2666
|
+
}
|
|
2667
|
+
|
|
2668
|
+
const hasPayment = installments.some(
|
|
2669
|
+
(installment) => installment.open_amount_cents < installment.amount_cents,
|
|
2670
|
+
);
|
|
2671
|
+
|
|
2672
|
+
if (hasPayment) {
|
|
2673
|
+
return 'partial';
|
|
2674
|
+
}
|
|
2675
|
+
|
|
2676
|
+
return 'open';
|
|
2677
|
+
}
|
|
2678
|
+
|
|
2679
|
+
private async recalculateTitleStatus(tx: any, titleId: number): Promise<TitleStatus> {
|
|
2680
|
+
const title = await tx.financial_title.findUnique({
|
|
2681
|
+
where: {
|
|
2682
|
+
id: titleId,
|
|
2683
|
+
},
|
|
2684
|
+
select: {
|
|
2685
|
+
id: true,
|
|
2686
|
+
status: true,
|
|
2687
|
+
financial_installment: {
|
|
2688
|
+
select: {
|
|
2689
|
+
amount_cents: true,
|
|
2690
|
+
open_amount_cents: true,
|
|
2691
|
+
due_date: true,
|
|
2692
|
+
status: true,
|
|
2693
|
+
},
|
|
2694
|
+
},
|
|
2695
|
+
},
|
|
2696
|
+
});
|
|
2697
|
+
|
|
2698
|
+
if (!title) {
|
|
2699
|
+
throw new NotFoundException('Financial title not found');
|
|
2700
|
+
}
|
|
2701
|
+
|
|
2702
|
+
const nextStatus = this.deriveTitleStatusFromInstallments(
|
|
2703
|
+
title.financial_installment,
|
|
2704
|
+
);
|
|
2705
|
+
|
|
2706
|
+
if (title.status !== nextStatus) {
|
|
2707
|
+
await tx.financial_title.update({
|
|
2708
|
+
where: {
|
|
2709
|
+
id: title.id,
|
|
2710
|
+
},
|
|
2711
|
+
data: {
|
|
2712
|
+
status: nextStatus,
|
|
2713
|
+
},
|
|
2714
|
+
});
|
|
2715
|
+
}
|
|
2716
|
+
|
|
2717
|
+
return nextStatus;
|
|
2718
|
+
}
|
|
2719
|
+
|
|
2720
|
+
private async createAuditLog(
|
|
2721
|
+
tx: any,
|
|
2722
|
+
data: {
|
|
2723
|
+
actorUserId?: number;
|
|
2724
|
+
action: string;
|
|
2725
|
+
entityTable: string;
|
|
2726
|
+
entityId: string;
|
|
2727
|
+
summary?: string;
|
|
2728
|
+
beforeData?: string;
|
|
2729
|
+
afterData?: string;
|
|
2730
|
+
},
|
|
2731
|
+
) {
|
|
2732
|
+
await tx.audit_log.create({
|
|
2733
|
+
data: {
|
|
2734
|
+
actor_user_id: data.actorUserId || null,
|
|
2735
|
+
action: data.action,
|
|
2736
|
+
entity_table: data.entityTable,
|
|
2737
|
+
entity_id: data.entityId,
|
|
2738
|
+
summary: data.summary || null,
|
|
2739
|
+
before_data: data.beforeData || null,
|
|
2740
|
+
after_data: data.afterData || null,
|
|
2741
|
+
},
|
|
2742
|
+
});
|
|
2743
|
+
}
|
|
2744
|
+
|
|
1631
2745
|
private mapTitleToFront(title: any, paymentChannelOverride?: string) {
|
|
1632
2746
|
const allocations = title.financial_installment.flatMap(
|
|
1633
2747
|
(installment) => installment.installment_allocation,
|
|
@@ -1652,13 +2766,24 @@ export class FinanceService {
|
|
|
1652
2766
|
numero: installment.installment_number,
|
|
1653
2767
|
vencimento: installment.due_date.toISOString(),
|
|
1654
2768
|
valor: this.fromCents(installment.amount_cents),
|
|
1655
|
-
|
|
2769
|
+
valorAberto: this.fromCents(installment.open_amount_cents),
|
|
2770
|
+
status: this.mapStatusToPt(
|
|
2771
|
+
this.resolveInstallmentStatus(
|
|
2772
|
+
installment.amount_cents,
|
|
2773
|
+
installment.open_amount_cents,
|
|
2774
|
+
installment.due_date,
|
|
2775
|
+
installment.status,
|
|
2776
|
+
),
|
|
2777
|
+
),
|
|
1656
2778
|
metodoPagamento:
|
|
1657
2779
|
this.mapPaymentMethodToPt(
|
|
1658
2780
|
installment.settlement_allocation[0]?.settlement?.payment_method?.type,
|
|
1659
2781
|
) || paymentChannelOverride || 'transferencia',
|
|
1660
2782
|
liquidacoes: installment.settlement_allocation.map((allocation) => ({
|
|
1661
2783
|
id: String(allocation.id),
|
|
2784
|
+
settlementId: allocation.settlement?.id
|
|
2785
|
+
? String(allocation.settlement.id)
|
|
2786
|
+
: null,
|
|
1662
2787
|
data: allocation.settlement?.settled_at?.toISOString(),
|
|
1663
2788
|
valor: this.fromCents(allocation.allocated_amount_cents),
|
|
1664
2789
|
juros: this.fromCents(allocation.interest_cents || 0),
|
|
@@ -1667,6 +2792,7 @@ export class FinanceService {
|
|
|
1667
2792
|
contaBancariaId: allocation.settlement?.bank_account_id
|
|
1668
2793
|
? String(allocation.settlement.bank_account_id)
|
|
1669
2794
|
: null,
|
|
2795
|
+
status: allocation.settlement?.status || null,
|
|
1670
2796
|
metodo:
|
|
1671
2797
|
this.mapPaymentMethodToPt(
|
|
1672
2798
|
allocation.settlement?.payment_method?.type,
|
|
@@ -1674,6 +2800,11 @@ export class FinanceService {
|
|
|
1674
2800
|
})),
|
|
1675
2801
|
}));
|
|
1676
2802
|
|
|
2803
|
+
const attachmentDetails = title.financial_title_attachment.map((attachment) => ({
|
|
2804
|
+
id: String(attachment.file_id),
|
|
2805
|
+
nome: attachment.file?.filename || attachment.file?.path,
|
|
2806
|
+
}));
|
|
2807
|
+
|
|
1677
2808
|
return {
|
|
1678
2809
|
id: String(title.id),
|
|
1679
2810
|
documento: title.document_number || `TIT-${title.id}`,
|
|
@@ -1686,9 +2817,8 @@ export class FinanceService {
|
|
|
1686
2817
|
criadoEm: title.created_at.toISOString(),
|
|
1687
2818
|
categoriaId: title.finance_category_id ? String(title.finance_category_id) : null,
|
|
1688
2819
|
centroCustoId: firstCostCenter ? String(firstCostCenter) : null,
|
|
1689
|
-
anexos:
|
|
1690
|
-
|
|
1691
|
-
),
|
|
2820
|
+
anexos: attachmentDetails.map((attachment) => attachment.nome),
|
|
2821
|
+
anexosDetalhes: attachmentDetails,
|
|
1692
2822
|
tags,
|
|
1693
2823
|
parcelas: mappedInstallments,
|
|
1694
2824
|
canal:
|
|
@@ -1744,6 +2874,20 @@ export class FinanceService {
|
|
|
1744
2874
|
return statusMap[status] || 'aberto';
|
|
1745
2875
|
}
|
|
1746
2876
|
|
|
2877
|
+
private normalizeTagSlug(value?: string | null) {
|
|
2878
|
+
if (!value) {
|
|
2879
|
+
return '';
|
|
2880
|
+
}
|
|
2881
|
+
|
|
2882
|
+
return value
|
|
2883
|
+
.normalize('NFD')
|
|
2884
|
+
.replace(/[\u0300-\u036f]/g, '')
|
|
2885
|
+
.toLowerCase()
|
|
2886
|
+
.trim()
|
|
2887
|
+
.replace(/[^a-z0-9]+/g, '-')
|
|
2888
|
+
.replace(/(^-|-$)+/g, '');
|
|
2889
|
+
}
|
|
2890
|
+
|
|
1747
2891
|
private mapStatusFromPt(status?: string) {
|
|
1748
2892
|
if (!status || status === 'all') {
|
|
1749
2893
|
return undefined;
|
|
@@ -1763,7 +2907,7 @@ export class FinanceService {
|
|
|
1763
2907
|
}
|
|
1764
2908
|
|
|
1765
2909
|
private mapPaymentMethodToPt(paymentMethodType?: string | null) {
|
|
1766
|
-
const paymentMethodMap = {
|
|
2910
|
+
const paymentMethodMap: Record<string, string> = {
|
|
1767
2911
|
boleto: 'boleto',
|
|
1768
2912
|
pix: 'pix',
|
|
1769
2913
|
ted: 'transferencia',
|
|
@@ -1776,6 +2920,30 @@ export class FinanceService {
|
|
|
1776
2920
|
return paymentMethodMap[paymentMethodType] || undefined;
|
|
1777
2921
|
}
|
|
1778
2922
|
|
|
2923
|
+
private mapPaymentMethodFromPt(paymentMethodType?: string | null) {
|
|
2924
|
+
if (!paymentMethodType) {
|
|
2925
|
+
return undefined;
|
|
2926
|
+
}
|
|
2927
|
+
|
|
2928
|
+
const paymentMethodMap = {
|
|
2929
|
+
boleto: 'boleto',
|
|
2930
|
+
pix: 'pix',
|
|
2931
|
+
transferencia: 'ted',
|
|
2932
|
+
transferência: 'ted',
|
|
2933
|
+
ted: 'ted',
|
|
2934
|
+
doc: 'doc',
|
|
2935
|
+
cartao: 'card',
|
|
2936
|
+
cartão: 'card',
|
|
2937
|
+
dinheiro: 'cash',
|
|
2938
|
+
cheque: 'other',
|
|
2939
|
+
cash: 'cash',
|
|
2940
|
+
card: 'card',
|
|
2941
|
+
other: 'other',
|
|
2942
|
+
};
|
|
2943
|
+
|
|
2944
|
+
return paymentMethodMap[(paymentMethodType || '').toLowerCase()];
|
|
2945
|
+
}
|
|
2946
|
+
|
|
1779
2947
|
private mapAccountTypeToPt(accountType?: string | null) {
|
|
1780
2948
|
const accountTypeMap = {
|
|
1781
2949
|
checking: 'corrente',
|