@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/dist/finance.service.js
CHANGED
|
@@ -498,7 +498,7 @@ let FinanceService = FinanceService_1 = class FinanceService {
|
|
|
498
498
|
return Number.isFinite(num) ? num : null;
|
|
499
499
|
}
|
|
500
500
|
async getData() {
|
|
501
|
-
const [
|
|
501
|
+
const [payablesResult, receivablesResult, peopleResult, categoriesResult, costCentersResult, bankAccountsResult, tagsResult, auditLogsResult,] = await Promise.allSettled([
|
|
502
502
|
this.loadTitles('payable'),
|
|
503
503
|
this.loadTitles('receivable'),
|
|
504
504
|
this.loadPeople(),
|
|
@@ -508,6 +508,49 @@ let FinanceService = FinanceService_1 = class FinanceService {
|
|
|
508
508
|
this.loadTags(),
|
|
509
509
|
this.loadAuditLogs(),
|
|
510
510
|
]);
|
|
511
|
+
const payables = payablesResult.status === 'fulfilled' ? payablesResult.value : [];
|
|
512
|
+
const receivables = receivablesResult.status === 'fulfilled' ? receivablesResult.value : [];
|
|
513
|
+
const people = peopleResult.status === 'fulfilled' ? peopleResult.value : [];
|
|
514
|
+
const categories = categoriesResult.status === 'fulfilled' ? categoriesResult.value : [];
|
|
515
|
+
const costCenters = costCentersResult.status === 'fulfilled' ? costCentersResult.value : [];
|
|
516
|
+
const bankAccounts = bankAccountsResult.status === 'fulfilled' ? bankAccountsResult.value : [];
|
|
517
|
+
const tags = tagsResult.status === 'fulfilled' ? tagsResult.value : [];
|
|
518
|
+
const auditLogs = auditLogsResult.status === 'fulfilled' ? auditLogsResult.value : [];
|
|
519
|
+
if (payablesResult.status === 'rejected') {
|
|
520
|
+
this.logger.error('Failed to load finance payables', payablesResult.reason);
|
|
521
|
+
}
|
|
522
|
+
if (receivablesResult.status === 'rejected') {
|
|
523
|
+
this.logger.error('Failed to load finance receivables', receivablesResult.reason);
|
|
524
|
+
}
|
|
525
|
+
if (peopleResult.status === 'rejected') {
|
|
526
|
+
this.logger.error('Failed to load finance people', peopleResult.reason);
|
|
527
|
+
}
|
|
528
|
+
if (categoriesResult.status === 'rejected') {
|
|
529
|
+
this.logger.error('Failed to load finance categories', categoriesResult.reason);
|
|
530
|
+
}
|
|
531
|
+
if (costCentersResult.status === 'rejected') {
|
|
532
|
+
this.logger.error('Failed to load finance cost centers', costCentersResult.reason);
|
|
533
|
+
}
|
|
534
|
+
if (bankAccountsResult.status === 'rejected') {
|
|
535
|
+
this.logger.error('Failed to load finance bank accounts', bankAccountsResult.reason);
|
|
536
|
+
}
|
|
537
|
+
if (tagsResult.status === 'rejected') {
|
|
538
|
+
this.logger.error('Failed to load finance tags', tagsResult.reason);
|
|
539
|
+
}
|
|
540
|
+
if (auditLogsResult.status === 'rejected') {
|
|
541
|
+
this.logger.error('Failed to load finance audit logs', auditLogsResult.reason);
|
|
542
|
+
}
|
|
543
|
+
const aprovacoesPendentes = payables
|
|
544
|
+
.filter((title) => title.status === 'rascunho')
|
|
545
|
+
.map((title) => ({
|
|
546
|
+
id: String(title.id),
|
|
547
|
+
tituloId: String(title.id),
|
|
548
|
+
solicitante: '-',
|
|
549
|
+
valor: Number(title.valorTotal || 0),
|
|
550
|
+
politica: 'Aprovação financeira',
|
|
551
|
+
urgencia: 'media',
|
|
552
|
+
dataSolicitacao: title.criadoEm,
|
|
553
|
+
}));
|
|
511
554
|
return {
|
|
512
555
|
kpis: {
|
|
513
556
|
saldoCaixa: 0,
|
|
@@ -525,7 +568,7 @@ let FinanceService = FinanceService_1 = class FinanceService {
|
|
|
525
568
|
pessoas: people,
|
|
526
569
|
categorias: categories,
|
|
527
570
|
centrosCusto: costCenters,
|
|
528
|
-
aprovacoesPendentes
|
|
571
|
+
aprovacoesPendentes,
|
|
529
572
|
agingInadimplencia: [],
|
|
530
573
|
cenarios: [],
|
|
531
574
|
transferencias: [],
|
|
@@ -555,9 +598,76 @@ let FinanceService = FinanceService_1 = class FinanceService {
|
|
|
555
598
|
async createAccountsPayableTitle(data, locale, userId) {
|
|
556
599
|
return this.createTitle(data, 'payable', locale, userId);
|
|
557
600
|
}
|
|
601
|
+
async approveAccountsPayableTitle(id, locale, userId) {
|
|
602
|
+
return this.approveTitle(id, 'payable', locale, userId);
|
|
603
|
+
}
|
|
604
|
+
async rejectAccountsPayableTitle(id, data, locale, userId) {
|
|
605
|
+
return this.rejectTitle(id, data, 'payable', locale, userId);
|
|
606
|
+
}
|
|
607
|
+
async settleAccountsPayableInstallment(id, data, locale, userId) {
|
|
608
|
+
return this.settleTitleInstallment(id, data, 'payable', locale, userId);
|
|
609
|
+
}
|
|
610
|
+
async reverseAccountsPayableSettlement(id, settlementId, data, locale, userId) {
|
|
611
|
+
return this.reverseTitleSettlement(id, settlementId, data, 'payable', locale, userId);
|
|
612
|
+
}
|
|
558
613
|
async createAccountsReceivableTitle(data, locale, userId) {
|
|
559
614
|
return this.createTitle(data, 'receivable', locale, userId);
|
|
560
615
|
}
|
|
616
|
+
async approveAccountsReceivableTitle(id, locale, userId) {
|
|
617
|
+
return this.approveTitle(id, 'receivable', locale, userId);
|
|
618
|
+
}
|
|
619
|
+
async settleAccountsReceivableInstallment(id, data, locale, userId) {
|
|
620
|
+
return this.settleTitleInstallment(id, data, 'receivable', locale, userId);
|
|
621
|
+
}
|
|
622
|
+
async reverseAccountsReceivableSettlement(id, settlementId, data, locale, userId) {
|
|
623
|
+
return this.reverseTitleSettlement(id, settlementId, data, 'receivable', locale, userId);
|
|
624
|
+
}
|
|
625
|
+
async createTag(data) {
|
|
626
|
+
const slug = this.normalizeTagSlug(data.name);
|
|
627
|
+
if (!slug) {
|
|
628
|
+
throw new common_1.BadRequestException('Tag name is required');
|
|
629
|
+
}
|
|
630
|
+
const existingTag = await this.prisma.tag.findFirst({
|
|
631
|
+
where: {
|
|
632
|
+
slug,
|
|
633
|
+
},
|
|
634
|
+
select: {
|
|
635
|
+
id: true,
|
|
636
|
+
slug: true,
|
|
637
|
+
color: true,
|
|
638
|
+
},
|
|
639
|
+
});
|
|
640
|
+
if (existingTag) {
|
|
641
|
+
return {
|
|
642
|
+
id: String(existingTag.id),
|
|
643
|
+
nome: existingTag.slug,
|
|
644
|
+
cor: existingTag.color,
|
|
645
|
+
};
|
|
646
|
+
}
|
|
647
|
+
const createdTag = await this.prisma.tag.create({
|
|
648
|
+
data: {
|
|
649
|
+
slug,
|
|
650
|
+
color: data.color || '#000000',
|
|
651
|
+
status: 'active',
|
|
652
|
+
},
|
|
653
|
+
select: {
|
|
654
|
+
id: true,
|
|
655
|
+
slug: true,
|
|
656
|
+
color: true,
|
|
657
|
+
},
|
|
658
|
+
});
|
|
659
|
+
return {
|
|
660
|
+
id: String(createdTag.id),
|
|
661
|
+
nome: createdTag.slug,
|
|
662
|
+
cor: createdTag.color,
|
|
663
|
+
};
|
|
664
|
+
}
|
|
665
|
+
async updateAccountsPayableInstallmentTags(id, tagIds, locale) {
|
|
666
|
+
return this.updateTitleTags(id, 'payable', tagIds, locale);
|
|
667
|
+
}
|
|
668
|
+
async updateAccountsReceivableInstallmentTags(id, tagIds, locale) {
|
|
669
|
+
return this.updateTitleTags(id, 'receivable', tagIds, locale);
|
|
670
|
+
}
|
|
561
671
|
async listBankAccounts() {
|
|
562
672
|
const bankAccounts = await this.prisma.bank_account.findMany({
|
|
563
673
|
include: {
|
|
@@ -1046,85 +1156,583 @@ let FinanceService = FinanceService_1 = class FinanceService {
|
|
|
1046
1156
|
return title;
|
|
1047
1157
|
}
|
|
1048
1158
|
async createTitle(data, titleType, locale, userId) {
|
|
1049
|
-
const
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
if (!person) {
|
|
1054
|
-
throw new common_1.BadRequestException((0, api_locale_1.getLocaleText)('personNotFound', locale, 'Person not found'));
|
|
1055
|
-
}
|
|
1056
|
-
if (data.finance_category_id) {
|
|
1057
|
-
const category = await this.prisma.finance_category.findUnique({
|
|
1058
|
-
where: { id: data.finance_category_id },
|
|
1159
|
+
const installments = this.normalizeAndValidateInstallments(data, locale);
|
|
1160
|
+
const createdTitleId = await this.prisma.$transaction(async (tx) => {
|
|
1161
|
+
const person = await tx.person.findUnique({
|
|
1162
|
+
where: { id: data.person_id },
|
|
1059
1163
|
select: { id: true },
|
|
1060
1164
|
});
|
|
1061
|
-
if (!
|
|
1062
|
-
throw new common_1.BadRequestException((0, api_locale_1.getLocaleText)('
|
|
1165
|
+
if (!person) {
|
|
1166
|
+
throw new common_1.BadRequestException((0, api_locale_1.getLocaleText)('personNotFound', locale, 'Person not found'));
|
|
1063
1167
|
}
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1168
|
+
if (data.finance_category_id) {
|
|
1169
|
+
const category = await tx.finance_category.findUnique({
|
|
1170
|
+
where: { id: data.finance_category_id },
|
|
1171
|
+
select: { id: true },
|
|
1172
|
+
});
|
|
1173
|
+
if (!category) {
|
|
1174
|
+
throw new common_1.BadRequestException((0, api_locale_1.getLocaleText)('categoryNotFound', locale, 'Category not found'));
|
|
1175
|
+
}
|
|
1176
|
+
}
|
|
1177
|
+
if (data.cost_center_id) {
|
|
1178
|
+
const costCenter = await tx.cost_center.findUnique({
|
|
1179
|
+
where: { id: data.cost_center_id },
|
|
1180
|
+
select: { id: true },
|
|
1181
|
+
});
|
|
1182
|
+
if (!costCenter) {
|
|
1183
|
+
throw new common_1.BadRequestException((0, api_locale_1.getLocaleText)('costCenterNotFound', locale, 'Cost center not found'));
|
|
1184
|
+
}
|
|
1185
|
+
}
|
|
1186
|
+
const attachmentFileIds = [
|
|
1187
|
+
...new Set((data.attachment_file_ids || []).filter((fileId) => fileId > 0)),
|
|
1188
|
+
];
|
|
1189
|
+
if (attachmentFileIds.length > 0) {
|
|
1190
|
+
const existingFiles = await tx.file.findMany({
|
|
1191
|
+
where: {
|
|
1192
|
+
id: { in: attachmentFileIds },
|
|
1193
|
+
},
|
|
1194
|
+
select: {
|
|
1195
|
+
id: true,
|
|
1196
|
+
},
|
|
1197
|
+
});
|
|
1198
|
+
const existingFileIds = new Set(existingFiles.map((file) => file.id));
|
|
1199
|
+
const invalidFileIds = attachmentFileIds.filter((fileId) => !existingFileIds.has(fileId));
|
|
1200
|
+
if (invalidFileIds.length > 0) {
|
|
1201
|
+
throw new common_1.BadRequestException(`Invalid attachment file IDs: ${invalidFileIds.join(', ')}`);
|
|
1202
|
+
}
|
|
1203
|
+
}
|
|
1204
|
+
await this.assertDateNotInClosedPeriod(tx, data.competence_date
|
|
1205
|
+
? new Date(data.competence_date)
|
|
1206
|
+
: new Date(installments[0].due_date), 'create title');
|
|
1207
|
+
const title = await tx.financial_title.create({
|
|
1208
|
+
data: {
|
|
1209
|
+
person_id: data.person_id,
|
|
1210
|
+
title_type: titleType,
|
|
1211
|
+
status: 'draft',
|
|
1212
|
+
document_number: data.document_number,
|
|
1213
|
+
description: data.description,
|
|
1214
|
+
competence_date: data.competence_date
|
|
1215
|
+
? new Date(data.competence_date)
|
|
1216
|
+
: null,
|
|
1217
|
+
issue_date: data.issue_date ? new Date(data.issue_date) : null,
|
|
1218
|
+
total_amount_cents: this.toCents(data.total_amount),
|
|
1219
|
+
finance_category_id: data.finance_category_id,
|
|
1220
|
+
created_by_user_id: userId,
|
|
1221
|
+
},
|
|
1069
1222
|
});
|
|
1070
|
-
if (
|
|
1071
|
-
|
|
1223
|
+
if (attachmentFileIds.length > 0) {
|
|
1224
|
+
await tx.financial_title_attachment.createMany({
|
|
1225
|
+
data: attachmentFileIds.map((fileId) => ({
|
|
1226
|
+
title_id: title.id,
|
|
1227
|
+
file_id: fileId,
|
|
1228
|
+
uploaded_by_user_id: userId,
|
|
1229
|
+
})),
|
|
1230
|
+
});
|
|
1072
1231
|
}
|
|
1073
|
-
|
|
1074
|
-
|
|
1232
|
+
for (let index = 0; index < installments.length; index++) {
|
|
1233
|
+
const installment = installments[index];
|
|
1234
|
+
const amountCents = installment.amount_cents;
|
|
1235
|
+
const createdInstallment = await tx.financial_installment.create({
|
|
1236
|
+
data: {
|
|
1237
|
+
title_id: title.id,
|
|
1238
|
+
installment_number: installment.installment_number,
|
|
1239
|
+
competence_date: data.competence_date
|
|
1240
|
+
? new Date(data.competence_date)
|
|
1241
|
+
: new Date(installment.due_date),
|
|
1242
|
+
due_date: new Date(installment.due_date),
|
|
1243
|
+
amount_cents: amountCents,
|
|
1244
|
+
open_amount_cents: amountCents,
|
|
1245
|
+
status: this.resolveInstallmentStatus(amountCents, amountCents, new Date(installment.due_date)),
|
|
1246
|
+
notes: data.description,
|
|
1247
|
+
},
|
|
1248
|
+
});
|
|
1249
|
+
if (data.cost_center_id) {
|
|
1250
|
+
await tx.installment_allocation.create({
|
|
1251
|
+
data: {
|
|
1252
|
+
installment_id: createdInstallment.id,
|
|
1253
|
+
cost_center_id: data.cost_center_id,
|
|
1254
|
+
allocated_amount_cents: amountCents,
|
|
1255
|
+
},
|
|
1256
|
+
});
|
|
1257
|
+
}
|
|
1258
|
+
}
|
|
1259
|
+
await this.createAuditLog(tx, {
|
|
1260
|
+
action: 'CREATE_TITLE',
|
|
1261
|
+
entityTable: 'financial_title',
|
|
1262
|
+
entityId: String(title.id),
|
|
1263
|
+
actorUserId: userId,
|
|
1264
|
+
summary: `Created ${titleType} title ${title.id} in draft`,
|
|
1265
|
+
afterData: JSON.stringify({
|
|
1266
|
+
status: 'draft',
|
|
1267
|
+
total_amount_cents: this.toCents(data.total_amount),
|
|
1268
|
+
}),
|
|
1269
|
+
});
|
|
1270
|
+
return title.id;
|
|
1271
|
+
});
|
|
1272
|
+
const createdTitle = await this.getTitleById(createdTitleId, titleType, locale);
|
|
1273
|
+
return this.mapTitleToFront(createdTitle, data.payment_channel);
|
|
1274
|
+
}
|
|
1275
|
+
normalizeAndValidateInstallments(data, locale) {
|
|
1276
|
+
const fallbackDueDate = data.due_date;
|
|
1277
|
+
const totalAmountCents = this.toCents(data.total_amount);
|
|
1278
|
+
const sourceInstallments = data.installments && data.installments.length > 0
|
|
1075
1279
|
? data.installments
|
|
1076
1280
|
: [
|
|
1077
1281
|
{
|
|
1078
1282
|
installment_number: 1,
|
|
1079
|
-
due_date:
|
|
1283
|
+
due_date: fallbackDueDate,
|
|
1080
1284
|
amount: data.total_amount,
|
|
1081
1285
|
},
|
|
1082
1286
|
];
|
|
1083
|
-
const
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
document_number: data.document_number,
|
|
1089
|
-
description: data.description,
|
|
1090
|
-
competence_date: data.competence_date
|
|
1091
|
-
? new Date(data.competence_date)
|
|
1092
|
-
: null,
|
|
1093
|
-
issue_date: data.issue_date ? new Date(data.issue_date) : null,
|
|
1094
|
-
total_amount_cents: this.toCents(data.total_amount),
|
|
1095
|
-
finance_category_id: data.finance_category_id,
|
|
1096
|
-
created_by_user_id: userId,
|
|
1097
|
-
},
|
|
1098
|
-
});
|
|
1099
|
-
for (let index = 0; index < installments.length; index++) {
|
|
1100
|
-
const installment = installments[index];
|
|
1287
|
+
const normalizedInstallments = sourceInstallments.map((installment, index) => {
|
|
1288
|
+
const installmentDueDate = installment.due_date || fallbackDueDate;
|
|
1289
|
+
if (!installmentDueDate) {
|
|
1290
|
+
throw new common_1.BadRequestException((0, api_locale_1.getLocaleText)('installmentDueDateRequired', locale, 'Installment due date is required'));
|
|
1291
|
+
}
|
|
1101
1292
|
const amountCents = this.toCents(installment.amount);
|
|
1102
|
-
|
|
1293
|
+
if (amountCents <= 0) {
|
|
1294
|
+
throw new common_1.BadRequestException((0, api_locale_1.getLocaleText)('installmentAmountInvalid', locale, 'Installment amount must be greater than zero'));
|
|
1295
|
+
}
|
|
1296
|
+
return {
|
|
1297
|
+
installment_number: installment.installment_number || index + 1,
|
|
1298
|
+
due_date: installmentDueDate,
|
|
1299
|
+
amount_cents: amountCents,
|
|
1300
|
+
};
|
|
1301
|
+
});
|
|
1302
|
+
const installmentsTotalCents = normalizedInstallments.reduce((acc, installment) => acc + installment.amount_cents, 0);
|
|
1303
|
+
if (installmentsTotalCents !== totalAmountCents) {
|
|
1304
|
+
throw new common_1.BadRequestException((0, api_locale_1.getLocaleText)('installmentsTotalMismatch', locale, 'Installments total must be equal to title total amount'));
|
|
1305
|
+
}
|
|
1306
|
+
return normalizedInstallments;
|
|
1307
|
+
}
|
|
1308
|
+
async approveTitle(titleId, titleType, locale, userId) {
|
|
1309
|
+
const updatedTitle = await this.prisma.$transaction(async (tx) => {
|
|
1310
|
+
const title = await tx.financial_title.findFirst({
|
|
1311
|
+
where: {
|
|
1312
|
+
id: titleId,
|
|
1313
|
+
title_type: titleType,
|
|
1314
|
+
},
|
|
1315
|
+
select: {
|
|
1316
|
+
id: true,
|
|
1317
|
+
status: true,
|
|
1318
|
+
competence_date: true,
|
|
1319
|
+
},
|
|
1320
|
+
});
|
|
1321
|
+
if (!title) {
|
|
1322
|
+
throw new common_1.NotFoundException((0, api_locale_1.getLocaleText)('itemNotFound', locale, `Financial title with ID ${titleId} not found`).replace('{{item}}', 'Financial title'));
|
|
1323
|
+
}
|
|
1324
|
+
await this.assertDateNotInClosedPeriod(tx, title.competence_date, 'approve title');
|
|
1325
|
+
if (title.status !== 'draft') {
|
|
1326
|
+
throw new common_1.BadRequestException('Only draft titles can be approved');
|
|
1327
|
+
}
|
|
1328
|
+
await tx.financial_title.update({
|
|
1329
|
+
where: { id: title.id },
|
|
1103
1330
|
data: {
|
|
1331
|
+
status: 'approved',
|
|
1332
|
+
},
|
|
1333
|
+
});
|
|
1334
|
+
await this.createAuditLog(tx, {
|
|
1335
|
+
action: 'APPROVE_TITLE',
|
|
1336
|
+
entityTable: 'financial_title',
|
|
1337
|
+
entityId: String(title.id),
|
|
1338
|
+
actorUserId: userId,
|
|
1339
|
+
summary: `Approved ${titleType} title ${title.id}`,
|
|
1340
|
+
beforeData: JSON.stringify({ status: title.status }),
|
|
1341
|
+
afterData: JSON.stringify({ status: 'approved' }),
|
|
1342
|
+
});
|
|
1343
|
+
return tx.financial_title.findFirst({
|
|
1344
|
+
where: {
|
|
1345
|
+
id: title.id,
|
|
1346
|
+
title_type: titleType,
|
|
1347
|
+
},
|
|
1348
|
+
include: this.defaultTitleInclude(),
|
|
1349
|
+
});
|
|
1350
|
+
});
|
|
1351
|
+
if (!updatedTitle) {
|
|
1352
|
+
throw new common_1.NotFoundException('Financial title not found');
|
|
1353
|
+
}
|
|
1354
|
+
return this.mapTitleToFront(updatedTitle);
|
|
1355
|
+
}
|
|
1356
|
+
async rejectTitle(titleId, data, titleType, locale, userId) {
|
|
1357
|
+
const updatedTitle = await this.prisma.$transaction(async (tx) => {
|
|
1358
|
+
const title = await tx.financial_title.findFirst({
|
|
1359
|
+
where: {
|
|
1360
|
+
id: titleId,
|
|
1361
|
+
title_type: titleType,
|
|
1362
|
+
},
|
|
1363
|
+
select: {
|
|
1364
|
+
id: true,
|
|
1365
|
+
status: true,
|
|
1366
|
+
competence_date: true,
|
|
1367
|
+
},
|
|
1368
|
+
});
|
|
1369
|
+
if (!title) {
|
|
1370
|
+
throw new common_1.NotFoundException((0, api_locale_1.getLocaleText)('itemNotFound', locale, `Financial title with ID ${titleId} not found`).replace('{{item}}', 'Financial title'));
|
|
1371
|
+
}
|
|
1372
|
+
await this.assertDateNotInClosedPeriod(tx, title.competence_date, 'reject title');
|
|
1373
|
+
if (title.status !== 'draft') {
|
|
1374
|
+
throw new common_1.BadRequestException('Only draft titles can be rejected');
|
|
1375
|
+
}
|
|
1376
|
+
await tx.financial_title.update({
|
|
1377
|
+
where: { id: title.id },
|
|
1378
|
+
data: {
|
|
1379
|
+
status: 'canceled',
|
|
1380
|
+
},
|
|
1381
|
+
});
|
|
1382
|
+
await this.createAuditLog(tx, {
|
|
1383
|
+
action: 'REJECT_TITLE',
|
|
1384
|
+
entityTable: 'financial_title',
|
|
1385
|
+
entityId: String(title.id),
|
|
1386
|
+
actorUserId: userId,
|
|
1387
|
+
summary: `Rejected ${titleType} title ${title.id}`,
|
|
1388
|
+
beforeData: JSON.stringify({ status: title.status }),
|
|
1389
|
+
afterData: JSON.stringify({
|
|
1390
|
+
status: 'canceled',
|
|
1391
|
+
reason: (data === null || data === void 0 ? void 0 : data.reason) || null,
|
|
1392
|
+
}),
|
|
1393
|
+
});
|
|
1394
|
+
return tx.financial_title.findFirst({
|
|
1395
|
+
where: {
|
|
1396
|
+
id: title.id,
|
|
1397
|
+
title_type: titleType,
|
|
1398
|
+
},
|
|
1399
|
+
include: this.defaultTitleInclude(),
|
|
1400
|
+
});
|
|
1401
|
+
});
|
|
1402
|
+
if (!updatedTitle) {
|
|
1403
|
+
throw new common_1.NotFoundException('Financial title not found');
|
|
1404
|
+
}
|
|
1405
|
+
return this.mapTitleToFront(updatedTitle);
|
|
1406
|
+
}
|
|
1407
|
+
async settleTitleInstallment(titleId, data, titleType, locale, userId) {
|
|
1408
|
+
const amountCents = this.toCents(data.amount);
|
|
1409
|
+
if (amountCents <= 0) {
|
|
1410
|
+
throw new common_1.BadRequestException('Settlement amount must be greater than zero');
|
|
1411
|
+
}
|
|
1412
|
+
const settledAt = data.settled_at ? new Date(data.settled_at) : new Date();
|
|
1413
|
+
if (Number.isNaN(settledAt.getTime())) {
|
|
1414
|
+
throw new common_1.BadRequestException('Invalid settlement date');
|
|
1415
|
+
}
|
|
1416
|
+
const result = await this.prisma.$transaction(async (tx) => {
|
|
1417
|
+
var _a;
|
|
1418
|
+
const title = await tx.financial_title.findFirst({
|
|
1419
|
+
where: {
|
|
1420
|
+
id: titleId,
|
|
1421
|
+
title_type: titleType,
|
|
1422
|
+
},
|
|
1423
|
+
select: {
|
|
1424
|
+
id: true,
|
|
1425
|
+
person_id: true,
|
|
1426
|
+
status: true,
|
|
1427
|
+
competence_date: true,
|
|
1428
|
+
},
|
|
1429
|
+
});
|
|
1430
|
+
if (!title) {
|
|
1431
|
+
throw new common_1.NotFoundException((0, api_locale_1.getLocaleText)('itemNotFound', locale, `Financial title with ID ${titleId} not found`).replace('{{item}}', 'Financial title'));
|
|
1432
|
+
}
|
|
1433
|
+
if (!['approved', 'open', 'partial'].includes(title.status)) {
|
|
1434
|
+
throw new common_1.BadRequestException('Only approved/open/partial titles can be settled');
|
|
1435
|
+
}
|
|
1436
|
+
await this.assertDateNotInClosedPeriod(tx, title.competence_date, 'settle installment');
|
|
1437
|
+
const installment = await tx.financial_installment.findFirst({
|
|
1438
|
+
where: {
|
|
1439
|
+
id: data.installment_id,
|
|
1104
1440
|
title_id: title.id,
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1441
|
+
},
|
|
1442
|
+
select: {
|
|
1443
|
+
id: true,
|
|
1444
|
+
title_id: true,
|
|
1445
|
+
amount_cents: true,
|
|
1446
|
+
open_amount_cents: true,
|
|
1447
|
+
due_date: true,
|
|
1448
|
+
status: true,
|
|
1449
|
+
},
|
|
1450
|
+
});
|
|
1451
|
+
if (!installment) {
|
|
1452
|
+
throw new common_1.BadRequestException('Installment not found for this title');
|
|
1453
|
+
}
|
|
1454
|
+
if (installment.status === 'settled' || installment.status === 'canceled') {
|
|
1455
|
+
throw new common_1.BadRequestException('This installment cannot be settled');
|
|
1456
|
+
}
|
|
1457
|
+
if (amountCents > installment.open_amount_cents) {
|
|
1458
|
+
throw new common_1.BadRequestException('Settlement amount exceeds open amount');
|
|
1459
|
+
}
|
|
1460
|
+
const paymentMethodId = await this.resolvePaymentMethodId(tx, data.payment_channel);
|
|
1461
|
+
const settlement = await tx.settlement.create({
|
|
1462
|
+
data: {
|
|
1463
|
+
person_id: title.person_id,
|
|
1464
|
+
bank_account_id: data.bank_account_id || null,
|
|
1465
|
+
payment_method_id: paymentMethodId,
|
|
1466
|
+
settlement_type: titleType,
|
|
1467
|
+
status: 'confirmed',
|
|
1468
|
+
settled_at: settledAt,
|
|
1110
1469
|
amount_cents: amountCents,
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
notes: data.description,
|
|
1470
|
+
description: ((_a = data.description) === null || _a === void 0 ? void 0 : _a.trim()) || null,
|
|
1471
|
+
created_by_user_id: userId,
|
|
1114
1472
|
},
|
|
1115
1473
|
});
|
|
1116
|
-
|
|
1117
|
-
|
|
1474
|
+
await tx.settlement_allocation.create({
|
|
1475
|
+
data: {
|
|
1476
|
+
settlement_id: settlement.id,
|
|
1477
|
+
installment_id: installment.id,
|
|
1478
|
+
allocated_amount_cents: amountCents,
|
|
1479
|
+
discount_cents: this.toCents(data.discount || 0),
|
|
1480
|
+
interest_cents: this.toCents(data.interest || 0),
|
|
1481
|
+
penalty_cents: this.toCents(data.penalty || 0),
|
|
1482
|
+
},
|
|
1483
|
+
});
|
|
1484
|
+
const decrementResult = await tx.financial_installment.updateMany({
|
|
1485
|
+
where: {
|
|
1486
|
+
id: installment.id,
|
|
1487
|
+
open_amount_cents: {
|
|
1488
|
+
gte: amountCents,
|
|
1489
|
+
},
|
|
1490
|
+
},
|
|
1491
|
+
data: {
|
|
1492
|
+
open_amount_cents: {
|
|
1493
|
+
decrement: amountCents,
|
|
1494
|
+
},
|
|
1495
|
+
},
|
|
1496
|
+
});
|
|
1497
|
+
if (decrementResult.count !== 1) {
|
|
1498
|
+
throw new common_1.BadRequestException('Installment was updated concurrently, please try again');
|
|
1499
|
+
}
|
|
1500
|
+
const updatedInstallment = await tx.financial_installment.findUnique({
|
|
1501
|
+
where: {
|
|
1502
|
+
id: installment.id,
|
|
1503
|
+
},
|
|
1504
|
+
select: {
|
|
1505
|
+
id: true,
|
|
1506
|
+
amount_cents: true,
|
|
1507
|
+
open_amount_cents: true,
|
|
1508
|
+
due_date: true,
|
|
1509
|
+
status: true,
|
|
1510
|
+
},
|
|
1511
|
+
});
|
|
1512
|
+
if (!updatedInstallment) {
|
|
1513
|
+
throw new common_1.NotFoundException('Installment not found');
|
|
1514
|
+
}
|
|
1515
|
+
const nextInstallmentStatus = this.resolveInstallmentStatus(updatedInstallment.amount_cents, updatedInstallment.open_amount_cents, updatedInstallment.due_date);
|
|
1516
|
+
if (updatedInstallment.status !== nextInstallmentStatus) {
|
|
1517
|
+
await tx.financial_installment.update({
|
|
1518
|
+
where: {
|
|
1519
|
+
id: updatedInstallment.id,
|
|
1520
|
+
},
|
|
1118
1521
|
data: {
|
|
1119
|
-
|
|
1120
|
-
cost_center_id: data.cost_center_id,
|
|
1121
|
-
allocated_amount_cents: amountCents,
|
|
1522
|
+
status: nextInstallmentStatus,
|
|
1122
1523
|
},
|
|
1123
1524
|
});
|
|
1124
1525
|
}
|
|
1526
|
+
const previousTitleStatus = title.status;
|
|
1527
|
+
const nextTitleStatus = await this.recalculateTitleStatus(tx, title.id);
|
|
1528
|
+
await this.createAuditLog(tx, {
|
|
1529
|
+
action: 'SETTLE_INSTALLMENT',
|
|
1530
|
+
entityTable: 'financial_title',
|
|
1531
|
+
entityId: String(title.id),
|
|
1532
|
+
actorUserId: userId,
|
|
1533
|
+
summary: `Settled installment ${installment.id} of title ${title.id}`,
|
|
1534
|
+
beforeData: JSON.stringify({
|
|
1535
|
+
title_status: previousTitleStatus,
|
|
1536
|
+
installment_open_amount_cents: installment.open_amount_cents,
|
|
1537
|
+
}),
|
|
1538
|
+
afterData: JSON.stringify({
|
|
1539
|
+
title_status: nextTitleStatus,
|
|
1540
|
+
installment_open_amount_cents: updatedInstallment.open_amount_cents,
|
|
1541
|
+
settlement_id: settlement.id,
|
|
1542
|
+
}),
|
|
1543
|
+
});
|
|
1544
|
+
const updatedTitle = await tx.financial_title.findFirst({
|
|
1545
|
+
where: {
|
|
1546
|
+
id: title.id,
|
|
1547
|
+
title_type: titleType,
|
|
1548
|
+
},
|
|
1549
|
+
include: this.defaultTitleInclude(),
|
|
1550
|
+
});
|
|
1551
|
+
if (!updatedTitle) {
|
|
1552
|
+
throw new common_1.NotFoundException('Financial title not found');
|
|
1553
|
+
}
|
|
1554
|
+
return {
|
|
1555
|
+
title: updatedTitle,
|
|
1556
|
+
settlementId: settlement.id,
|
|
1557
|
+
};
|
|
1558
|
+
});
|
|
1559
|
+
return Object.assign(Object.assign({}, this.mapTitleToFront(result.title)), { settlementId: String(result.settlementId) });
|
|
1560
|
+
}
|
|
1561
|
+
async reverseTitleSettlement(titleId, settlementId, data, titleType, locale, userId) {
|
|
1562
|
+
const updatedTitle = await this.prisma.$transaction(async (tx) => {
|
|
1563
|
+
const title = await tx.financial_title.findFirst({
|
|
1564
|
+
where: {
|
|
1565
|
+
id: titleId,
|
|
1566
|
+
title_type: titleType,
|
|
1567
|
+
},
|
|
1568
|
+
select: {
|
|
1569
|
+
id: true,
|
|
1570
|
+
status: true,
|
|
1571
|
+
competence_date: true,
|
|
1572
|
+
},
|
|
1573
|
+
});
|
|
1574
|
+
if (!title) {
|
|
1575
|
+
throw new common_1.NotFoundException((0, api_locale_1.getLocaleText)('itemNotFound', locale, `Financial title with ID ${titleId} not found`).replace('{{item}}', 'Financial title'));
|
|
1576
|
+
}
|
|
1577
|
+
await this.assertDateNotInClosedPeriod(tx, title.competence_date, 'reverse settlement');
|
|
1578
|
+
const settlement = await tx.settlement.findFirst({
|
|
1579
|
+
where: {
|
|
1580
|
+
id: settlementId,
|
|
1581
|
+
settlement_type: titleType,
|
|
1582
|
+
settlement_allocation: {
|
|
1583
|
+
some: {
|
|
1584
|
+
financial_installment: {
|
|
1585
|
+
title_id: title.id,
|
|
1586
|
+
},
|
|
1587
|
+
},
|
|
1588
|
+
},
|
|
1589
|
+
},
|
|
1590
|
+
include: {
|
|
1591
|
+
settlement_allocation: {
|
|
1592
|
+
include: {
|
|
1593
|
+
financial_installment: {
|
|
1594
|
+
select: {
|
|
1595
|
+
id: true,
|
|
1596
|
+
amount_cents: true,
|
|
1597
|
+
open_amount_cents: true,
|
|
1598
|
+
due_date: true,
|
|
1599
|
+
status: true,
|
|
1600
|
+
},
|
|
1601
|
+
},
|
|
1602
|
+
},
|
|
1603
|
+
},
|
|
1604
|
+
},
|
|
1605
|
+
});
|
|
1606
|
+
if (!settlement) {
|
|
1607
|
+
throw new common_1.NotFoundException('Settlement not found for this title');
|
|
1608
|
+
}
|
|
1609
|
+
if (settlement.status === 'reversed') {
|
|
1610
|
+
throw new common_1.BadRequestException('This settlement is already reversed');
|
|
1611
|
+
}
|
|
1612
|
+
for (const allocation of settlement.settlement_allocation) {
|
|
1613
|
+
const installment = allocation.financial_installment;
|
|
1614
|
+
if (!installment) {
|
|
1615
|
+
continue;
|
|
1616
|
+
}
|
|
1617
|
+
const nextOpenAmountCents = installment.open_amount_cents + allocation.allocated_amount_cents;
|
|
1618
|
+
if (nextOpenAmountCents > installment.amount_cents) {
|
|
1619
|
+
throw new common_1.BadRequestException(`Reverse would exceed installment amount for installment ${installment.id}`);
|
|
1620
|
+
}
|
|
1621
|
+
const nextInstallmentStatus = this.resolveInstallmentStatus(installment.amount_cents, nextOpenAmountCents, installment.due_date);
|
|
1622
|
+
await tx.financial_installment.update({
|
|
1623
|
+
where: {
|
|
1624
|
+
id: installment.id,
|
|
1625
|
+
},
|
|
1626
|
+
data: {
|
|
1627
|
+
open_amount_cents: nextOpenAmountCents,
|
|
1628
|
+
status: nextInstallmentStatus,
|
|
1629
|
+
},
|
|
1630
|
+
});
|
|
1631
|
+
}
|
|
1632
|
+
await tx.settlement.update({
|
|
1633
|
+
where: {
|
|
1634
|
+
id: settlement.id,
|
|
1635
|
+
},
|
|
1636
|
+
data: {
|
|
1637
|
+
status: 'reversed',
|
|
1638
|
+
description: [
|
|
1639
|
+
settlement.description,
|
|
1640
|
+
data.reason ? `Reversed: ${data.reason.trim()}` : 'Reversed',
|
|
1641
|
+
]
|
|
1642
|
+
.filter(Boolean)
|
|
1643
|
+
.join(' | '),
|
|
1644
|
+
},
|
|
1645
|
+
});
|
|
1646
|
+
const previousTitleStatus = title.status;
|
|
1647
|
+
const nextTitleStatus = await this.recalculateTitleStatus(tx, title.id);
|
|
1648
|
+
await this.createAuditLog(tx, {
|
|
1649
|
+
action: 'REVERSE_SETTLEMENT',
|
|
1650
|
+
entityTable: 'financial_title',
|
|
1651
|
+
entityId: String(title.id),
|
|
1652
|
+
actorUserId: userId,
|
|
1653
|
+
summary: `Reversed settlement ${settlement.id} from title ${title.id}`,
|
|
1654
|
+
beforeData: JSON.stringify({
|
|
1655
|
+
title_status: previousTitleStatus,
|
|
1656
|
+
settlement_status: settlement.status,
|
|
1657
|
+
}),
|
|
1658
|
+
afterData: JSON.stringify({
|
|
1659
|
+
title_status: nextTitleStatus,
|
|
1660
|
+
settlement_status: 'reversed',
|
|
1661
|
+
}),
|
|
1662
|
+
});
|
|
1663
|
+
return tx.financial_title.findFirst({
|
|
1664
|
+
where: {
|
|
1665
|
+
id: title.id,
|
|
1666
|
+
title_type: titleType,
|
|
1667
|
+
},
|
|
1668
|
+
include: this.defaultTitleInclude(),
|
|
1669
|
+
});
|
|
1670
|
+
});
|
|
1671
|
+
if (!updatedTitle) {
|
|
1672
|
+
throw new common_1.NotFoundException('Financial title not found');
|
|
1125
1673
|
}
|
|
1126
|
-
|
|
1127
|
-
|
|
1674
|
+
return this.mapTitleToFront(updatedTitle);
|
|
1675
|
+
}
|
|
1676
|
+
async updateTitleTags(titleId, titleType, tagIds, locale) {
|
|
1677
|
+
const title = await this.getTitleById(titleId, titleType, locale);
|
|
1678
|
+
const installmentIds = (title.financial_installment || []).map((installment) => installment.id);
|
|
1679
|
+
if (installmentIds.length === 0) {
|
|
1680
|
+
throw new common_1.BadRequestException('Financial title has no installments');
|
|
1681
|
+
}
|
|
1682
|
+
const normalizedTagIds = [
|
|
1683
|
+
...new Set((tagIds || [])
|
|
1684
|
+
.map((tagId) => Number(tagId))
|
|
1685
|
+
.filter((tagId) => Number.isInteger(tagId) && tagId > 0)),
|
|
1686
|
+
];
|
|
1687
|
+
if (normalizedTagIds.length > 0) {
|
|
1688
|
+
const existingTags = await this.prisma.tag.findMany({
|
|
1689
|
+
where: {
|
|
1690
|
+
id: {
|
|
1691
|
+
in: normalizedTagIds,
|
|
1692
|
+
},
|
|
1693
|
+
},
|
|
1694
|
+
select: {
|
|
1695
|
+
id: true,
|
|
1696
|
+
},
|
|
1697
|
+
});
|
|
1698
|
+
const existingTagIds = new Set(existingTags.map((tag) => tag.id));
|
|
1699
|
+
const invalidTagIds = normalizedTagIds.filter((tagId) => !existingTagIds.has(tagId));
|
|
1700
|
+
if (invalidTagIds.length > 0) {
|
|
1701
|
+
throw new common_1.BadRequestException(`Invalid tag IDs: ${invalidTagIds.join(', ')}`);
|
|
1702
|
+
}
|
|
1703
|
+
}
|
|
1704
|
+
await this.prisma.$transaction(async (tx) => {
|
|
1705
|
+
if (normalizedTagIds.length === 0) {
|
|
1706
|
+
await tx.financial_installment_tag.deleteMany({
|
|
1707
|
+
where: {
|
|
1708
|
+
installment_id: {
|
|
1709
|
+
in: installmentIds,
|
|
1710
|
+
},
|
|
1711
|
+
},
|
|
1712
|
+
});
|
|
1713
|
+
return;
|
|
1714
|
+
}
|
|
1715
|
+
await tx.financial_installment_tag.deleteMany({
|
|
1716
|
+
where: {
|
|
1717
|
+
installment_id: {
|
|
1718
|
+
in: installmentIds,
|
|
1719
|
+
},
|
|
1720
|
+
tag_id: {
|
|
1721
|
+
notIn: normalizedTagIds,
|
|
1722
|
+
},
|
|
1723
|
+
},
|
|
1724
|
+
});
|
|
1725
|
+
const newRelations = installmentIds.flatMap((installmentId) => normalizedTagIds.map((tagId) => ({
|
|
1726
|
+
installment_id: installmentId,
|
|
1727
|
+
tag_id: tagId,
|
|
1728
|
+
})));
|
|
1729
|
+
await tx.financial_installment_tag.createMany({
|
|
1730
|
+
data: newRelations,
|
|
1731
|
+
skipDuplicates: true,
|
|
1732
|
+
});
|
|
1733
|
+
});
|
|
1734
|
+
const updatedTitle = await this.getTitleById(titleId, titleType, locale);
|
|
1735
|
+
return this.mapTitleToFront(updatedTitle);
|
|
1128
1736
|
}
|
|
1129
1737
|
async loadTitles(type) {
|
|
1130
1738
|
const titles = await this.prisma.financial_title.findMany({
|
|
@@ -1213,6 +1821,124 @@ let FinanceService = FinanceService_1 = class FinanceService {
|
|
|
1213
1821
|
data: log.created_at.toISOString(),
|
|
1214
1822
|
}));
|
|
1215
1823
|
}
|
|
1824
|
+
async resolvePaymentMethodId(tx, paymentChannel) {
|
|
1825
|
+
const paymentType = this.mapPaymentMethodFromPt(paymentChannel);
|
|
1826
|
+
if (!paymentType) {
|
|
1827
|
+
return null;
|
|
1828
|
+
}
|
|
1829
|
+
const paymentMethod = await tx.payment_method.findFirst({
|
|
1830
|
+
where: {
|
|
1831
|
+
type: paymentType,
|
|
1832
|
+
status: 'active',
|
|
1833
|
+
},
|
|
1834
|
+
select: {
|
|
1835
|
+
id: true,
|
|
1836
|
+
},
|
|
1837
|
+
});
|
|
1838
|
+
return (paymentMethod === null || paymentMethod === void 0 ? void 0 : paymentMethod.id) || null;
|
|
1839
|
+
}
|
|
1840
|
+
async assertDateNotInClosedPeriod(tx, competenceDate, operation) {
|
|
1841
|
+
if (!competenceDate) {
|
|
1842
|
+
return;
|
|
1843
|
+
}
|
|
1844
|
+
const closedPeriod = await tx.period_close.findFirst({
|
|
1845
|
+
where: {
|
|
1846
|
+
status: 'closed',
|
|
1847
|
+
period_start: {
|
|
1848
|
+
lte: competenceDate,
|
|
1849
|
+
},
|
|
1850
|
+
period_end: {
|
|
1851
|
+
gte: competenceDate,
|
|
1852
|
+
},
|
|
1853
|
+
},
|
|
1854
|
+
select: {
|
|
1855
|
+
id: true,
|
|
1856
|
+
},
|
|
1857
|
+
});
|
|
1858
|
+
if (closedPeriod) {
|
|
1859
|
+
throw new common_1.BadRequestException(`Cannot ${operation}: competence is in a closed period`);
|
|
1860
|
+
}
|
|
1861
|
+
}
|
|
1862
|
+
resolveInstallmentStatus(amountCents, openAmountCents, dueDate, currentStatus) {
|
|
1863
|
+
if (currentStatus === 'canceled') {
|
|
1864
|
+
return 'canceled';
|
|
1865
|
+
}
|
|
1866
|
+
if (openAmountCents <= 0) {
|
|
1867
|
+
return 'settled';
|
|
1868
|
+
}
|
|
1869
|
+
if (openAmountCents < amountCents) {
|
|
1870
|
+
return 'partial';
|
|
1871
|
+
}
|
|
1872
|
+
const today = new Date();
|
|
1873
|
+
today.setHours(0, 0, 0, 0);
|
|
1874
|
+
const dueDateOnly = new Date(dueDate);
|
|
1875
|
+
dueDateOnly.setHours(0, 0, 0, 0);
|
|
1876
|
+
if (dueDateOnly < today) {
|
|
1877
|
+
return 'overdue';
|
|
1878
|
+
}
|
|
1879
|
+
return 'open';
|
|
1880
|
+
}
|
|
1881
|
+
deriveTitleStatusFromInstallments(installments) {
|
|
1882
|
+
if (installments.length === 0) {
|
|
1883
|
+
return 'open';
|
|
1884
|
+
}
|
|
1885
|
+
const effectiveStatuses = installments.map((installment) => this.resolveInstallmentStatus(installment.amount_cents, installment.open_amount_cents, installment.due_date, installment.status));
|
|
1886
|
+
if (effectiveStatuses.every((status) => status === 'settled')) {
|
|
1887
|
+
return 'settled';
|
|
1888
|
+
}
|
|
1889
|
+
const hasPayment = installments.some((installment) => installment.open_amount_cents < installment.amount_cents);
|
|
1890
|
+
if (hasPayment) {
|
|
1891
|
+
return 'partial';
|
|
1892
|
+
}
|
|
1893
|
+
return 'open';
|
|
1894
|
+
}
|
|
1895
|
+
async recalculateTitleStatus(tx, titleId) {
|
|
1896
|
+
const title = await tx.financial_title.findUnique({
|
|
1897
|
+
where: {
|
|
1898
|
+
id: titleId,
|
|
1899
|
+
},
|
|
1900
|
+
select: {
|
|
1901
|
+
id: true,
|
|
1902
|
+
status: true,
|
|
1903
|
+
financial_installment: {
|
|
1904
|
+
select: {
|
|
1905
|
+
amount_cents: true,
|
|
1906
|
+
open_amount_cents: true,
|
|
1907
|
+
due_date: true,
|
|
1908
|
+
status: true,
|
|
1909
|
+
},
|
|
1910
|
+
},
|
|
1911
|
+
},
|
|
1912
|
+
});
|
|
1913
|
+
if (!title) {
|
|
1914
|
+
throw new common_1.NotFoundException('Financial title not found');
|
|
1915
|
+
}
|
|
1916
|
+
const nextStatus = this.deriveTitleStatusFromInstallments(title.financial_installment);
|
|
1917
|
+
if (title.status !== nextStatus) {
|
|
1918
|
+
await tx.financial_title.update({
|
|
1919
|
+
where: {
|
|
1920
|
+
id: title.id,
|
|
1921
|
+
},
|
|
1922
|
+
data: {
|
|
1923
|
+
status: nextStatus,
|
|
1924
|
+
},
|
|
1925
|
+
});
|
|
1926
|
+
}
|
|
1927
|
+
return nextStatus;
|
|
1928
|
+
}
|
|
1929
|
+
async createAuditLog(tx, data) {
|
|
1930
|
+
await tx.audit_log.create({
|
|
1931
|
+
data: {
|
|
1932
|
+
actor_user_id: data.actorUserId || null,
|
|
1933
|
+
action: data.action,
|
|
1934
|
+
entity_table: data.entityTable,
|
|
1935
|
+
entity_id: data.entityId,
|
|
1936
|
+
summary: data.summary || null,
|
|
1937
|
+
before_data: data.beforeData || null,
|
|
1938
|
+
after_data: data.afterData || null,
|
|
1939
|
+
},
|
|
1940
|
+
});
|
|
1941
|
+
}
|
|
1216
1942
|
mapTitleToFront(title, paymentChannelOverride) {
|
|
1217
1943
|
var _a;
|
|
1218
1944
|
const allocations = title.financial_installment.flatMap((installment) => installment.installment_allocation);
|
|
@@ -1233,28 +1959,40 @@ let FinanceService = FinanceService_1 = class FinanceService {
|
|
|
1233
1959
|
numero: installment.installment_number,
|
|
1234
1960
|
vencimento: installment.due_date.toISOString(),
|
|
1235
1961
|
valor: this.fromCents(installment.amount_cents),
|
|
1236
|
-
|
|
1962
|
+
valorAberto: this.fromCents(installment.open_amount_cents),
|
|
1963
|
+
status: this.mapStatusToPt(this.resolveInstallmentStatus(installment.amount_cents, installment.open_amount_cents, installment.due_date, installment.status)),
|
|
1237
1964
|
metodoPagamento: this.mapPaymentMethodToPt((_c = (_b = (_a = installment.settlement_allocation[0]) === null || _a === void 0 ? void 0 : _a.settlement) === null || _b === void 0 ? void 0 : _b.payment_method) === null || _c === void 0 ? void 0 : _c.type) || paymentChannelOverride || 'transferencia',
|
|
1238
1965
|
liquidacoes: installment.settlement_allocation.map((allocation) => {
|
|
1239
|
-
var _a, _b, _c, _d, _e;
|
|
1966
|
+
var _a, _b, _c, _d, _e, _f, _g;
|
|
1240
1967
|
return ({
|
|
1241
1968
|
id: String(allocation.id),
|
|
1242
|
-
|
|
1969
|
+
settlementId: ((_a = allocation.settlement) === null || _a === void 0 ? void 0 : _a.id)
|
|
1970
|
+
? String(allocation.settlement.id)
|
|
1971
|
+
: null,
|
|
1972
|
+
data: (_c = (_b = allocation.settlement) === null || _b === void 0 ? void 0 : _b.settled_at) === null || _c === void 0 ? void 0 : _c.toISOString(),
|
|
1243
1973
|
valor: this.fromCents(allocation.allocated_amount_cents),
|
|
1244
1974
|
juros: this.fromCents(allocation.interest_cents || 0),
|
|
1245
1975
|
desconto: this.fromCents(allocation.discount_cents || 0),
|
|
1246
1976
|
multa: this.fromCents(allocation.penalty_cents || 0),
|
|
1247
|
-
contaBancariaId: ((
|
|
1977
|
+
contaBancariaId: ((_d = allocation.settlement) === null || _d === void 0 ? void 0 : _d.bank_account_id)
|
|
1248
1978
|
? String(allocation.settlement.bank_account_id)
|
|
1249
1979
|
: null,
|
|
1250
|
-
|
|
1980
|
+
status: ((_e = allocation.settlement) === null || _e === void 0 ? void 0 : _e.status) || null,
|
|
1981
|
+
metodo: this.mapPaymentMethodToPt((_g = (_f = allocation.settlement) === null || _f === void 0 ? void 0 : _f.payment_method) === null || _g === void 0 ? void 0 : _g.type) || 'transferencia',
|
|
1251
1982
|
});
|
|
1252
1983
|
}),
|
|
1253
1984
|
});
|
|
1254
1985
|
});
|
|
1986
|
+
const attachmentDetails = title.financial_title_attachment.map((attachment) => {
|
|
1987
|
+
var _a, _b;
|
|
1988
|
+
return ({
|
|
1989
|
+
id: String(attachment.file_id),
|
|
1990
|
+
nome: ((_a = attachment.file) === null || _a === void 0 ? void 0 : _a.filename) || ((_b = attachment.file) === null || _b === void 0 ? void 0 : _b.path),
|
|
1991
|
+
});
|
|
1992
|
+
});
|
|
1255
1993
|
return Object.assign({ id: String(title.id), documento: title.document_number || `TIT-${title.id}`, descricao: title.description || '', competencia: title.competence_date
|
|
1256
1994
|
? title.competence_date.toISOString().slice(0, 7)
|
|
1257
|
-
: '', valorTotal: this.fromCents(title.total_amount_cents), status: this.mapStatusToPt(title.status), criadoEm: title.created_at.toISOString(), categoriaId: title.finance_category_id ? String(title.finance_category_id) : null, centroCustoId: firstCostCenter ? String(firstCostCenter) : null, anexos:
|
|
1995
|
+
: '', valorTotal: this.fromCents(title.total_amount_cents), status: this.mapStatusToPt(title.status), criadoEm: title.created_at.toISOString(), categoriaId: title.finance_category_id ? String(title.finance_category_id) : null, centroCustoId: firstCostCenter ? String(firstCostCenter) : null, anexos: attachmentDetails.map((attachment) => attachment.nome), anexosDetalhes: attachmentDetails, tags, parcelas: mappedInstallments, canal: this.mapPaymentMethodToPt(channelFromSettlement) ||
|
|
1258
1996
|
paymentChannelOverride ||
|
|
1259
1997
|
'transferencia' }, (title.title_type === 'payable'
|
|
1260
1998
|
? { fornecedorId: String(title.person_id) }
|
|
@@ -1300,6 +2038,18 @@ let FinanceService = FinanceService_1 = class FinanceService {
|
|
|
1300
2038
|
};
|
|
1301
2039
|
return statusMap[status] || 'aberto';
|
|
1302
2040
|
}
|
|
2041
|
+
normalizeTagSlug(value) {
|
|
2042
|
+
if (!value) {
|
|
2043
|
+
return '';
|
|
2044
|
+
}
|
|
2045
|
+
return value
|
|
2046
|
+
.normalize('NFD')
|
|
2047
|
+
.replace(/[\u0300-\u036f]/g, '')
|
|
2048
|
+
.toLowerCase()
|
|
2049
|
+
.trim()
|
|
2050
|
+
.replace(/[^a-z0-9]+/g, '-')
|
|
2051
|
+
.replace(/(^-|-$)+/g, '');
|
|
2052
|
+
}
|
|
1303
2053
|
mapStatusFromPt(status) {
|
|
1304
2054
|
if (!status || status === 'all') {
|
|
1305
2055
|
return undefined;
|
|
@@ -1327,6 +2077,27 @@ let FinanceService = FinanceService_1 = class FinanceService {
|
|
|
1327
2077
|
};
|
|
1328
2078
|
return paymentMethodMap[paymentMethodType] || undefined;
|
|
1329
2079
|
}
|
|
2080
|
+
mapPaymentMethodFromPt(paymentMethodType) {
|
|
2081
|
+
if (!paymentMethodType) {
|
|
2082
|
+
return undefined;
|
|
2083
|
+
}
|
|
2084
|
+
const paymentMethodMap = {
|
|
2085
|
+
boleto: 'boleto',
|
|
2086
|
+
pix: 'pix',
|
|
2087
|
+
transferencia: 'ted',
|
|
2088
|
+
transferência: 'ted',
|
|
2089
|
+
ted: 'ted',
|
|
2090
|
+
doc: 'doc',
|
|
2091
|
+
cartao: 'card',
|
|
2092
|
+
cartão: 'card',
|
|
2093
|
+
dinheiro: 'cash',
|
|
2094
|
+
cheque: 'other',
|
|
2095
|
+
cash: 'cash',
|
|
2096
|
+
card: 'card',
|
|
2097
|
+
other: 'other',
|
|
2098
|
+
};
|
|
2099
|
+
return paymentMethodMap[(paymentMethodType || '').toLowerCase()];
|
|
2100
|
+
}
|
|
1330
2101
|
mapAccountTypeToPt(accountType) {
|
|
1331
2102
|
const accountTypeMap = {
|
|
1332
2103
|
checking: 'corrente',
|