@hed-hog/finance 0.0.238 → 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/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/finance-data.controller.d.ts +13 -5
- package/dist/finance-data.controller.d.ts.map +1 -1
- package/dist/finance-installments.controller.d.ts +248 -12
- package/dist/finance-installments.controller.d.ts.map +1 -1
- package/dist/finance-installments.controller.js +92 -0
- package/dist/finance-installments.controller.js.map +1 -1
- package/dist/finance.service.d.ts +275 -17
- package/dist/finance.service.d.ts.map +1 -1
- package/dist/finance.service.js +666 -78
- package/dist/finance.service.js.map +1 -1
- package/hedhog/data/route.yaml +63 -0
- package/hedhog/frontend/app/accounts-payable/approvals/page.tsx.ejs +92 -12
- package/hedhog/frontend/app/accounts-payable/installments/[id]/page.tsx.ejs +355 -4
- package/hedhog/frontend/app/accounts-payable/installments/page.tsx.ejs +440 -16
- package/hedhog/frontend/app/accounts-receivable/installments/[id]/page.tsx.ejs +396 -49
- package/hedhog/frontend/app/accounts-receivable/installments/page.tsx.ejs +432 -14
- package/package.json +5 -5
- 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/finance-installments.controller.ts +102 -0
- package/src/finance.service.ts +1007 -82
package/dist/finance.service.js
CHANGED
|
@@ -540,6 +540,17 @@ let FinanceService = FinanceService_1 = class FinanceService {
|
|
|
540
540
|
if (auditLogsResult.status === 'rejected') {
|
|
541
541
|
this.logger.error('Failed to load finance audit logs', auditLogsResult.reason);
|
|
542
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
|
+
}));
|
|
543
554
|
return {
|
|
544
555
|
kpis: {
|
|
545
556
|
saldoCaixa: 0,
|
|
@@ -557,7 +568,7 @@ let FinanceService = FinanceService_1 = class FinanceService {
|
|
|
557
568
|
pessoas: people,
|
|
558
569
|
categorias: categories,
|
|
559
570
|
centrosCusto: costCenters,
|
|
560
|
-
aprovacoesPendentes
|
|
571
|
+
aprovacoesPendentes,
|
|
561
572
|
agingInadimplencia: [],
|
|
562
573
|
cenarios: [],
|
|
563
574
|
transferencias: [],
|
|
@@ -587,9 +598,30 @@ let FinanceService = FinanceService_1 = class FinanceService {
|
|
|
587
598
|
async createAccountsPayableTitle(data, locale, userId) {
|
|
588
599
|
return this.createTitle(data, 'payable', locale, userId);
|
|
589
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
|
+
}
|
|
590
613
|
async createAccountsReceivableTitle(data, locale, userId) {
|
|
591
614
|
return this.createTitle(data, 'receivable', locale, userId);
|
|
592
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
|
+
}
|
|
593
625
|
async createTag(data) {
|
|
594
626
|
const slug = this.normalizeTagSlug(data.name);
|
|
595
627
|
if (!slug) {
|
|
@@ -1124,110 +1156,522 @@ let FinanceService = FinanceService_1 = class FinanceService {
|
|
|
1124
1156
|
return title;
|
|
1125
1157
|
}
|
|
1126
1158
|
async createTitle(data, titleType, locale, userId) {
|
|
1127
|
-
const
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
if (!person) {
|
|
1132
|
-
throw new common_1.BadRequestException((0, api_locale_1.getLocaleText)('personNotFound', locale, 'Person not found'));
|
|
1133
|
-
}
|
|
1134
|
-
if (data.finance_category_id) {
|
|
1135
|
-
const category = await this.prisma.finance_category.findUnique({
|
|
1136
|
-
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 },
|
|
1137
1163
|
select: { id: true },
|
|
1138
1164
|
});
|
|
1139
|
-
if (!
|
|
1140
|
-
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'));
|
|
1141
1167
|
}
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
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
|
+
},
|
|
1147
1222
|
});
|
|
1148
|
-
if (
|
|
1149
|
-
|
|
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
|
+
});
|
|
1150
1231
|
}
|
|
1151
|
-
|
|
1152
|
-
|
|
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
|
|
1153
1279
|
? data.installments
|
|
1154
1280
|
: [
|
|
1155
1281
|
{
|
|
1156
1282
|
installment_number: 1,
|
|
1157
|
-
due_date:
|
|
1283
|
+
due_date: fallbackDueDate,
|
|
1158
1284
|
amount: data.total_amount,
|
|
1159
1285
|
},
|
|
1160
1286
|
];
|
|
1161
|
-
const
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
},
|
|
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
|
+
}
|
|
1292
|
+
const amountCents = this.toCents(installment.amount);
|
|
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
|
+
};
|
|
1176
1301
|
});
|
|
1177
|
-
const
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
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({
|
|
1182
1311
|
where: {
|
|
1183
|
-
id:
|
|
1312
|
+
id: titleId,
|
|
1313
|
+
title_type: titleType,
|
|
1184
1314
|
},
|
|
1185
1315
|
select: {
|
|
1186
1316
|
id: true,
|
|
1317
|
+
status: true,
|
|
1318
|
+
competence_date: true,
|
|
1187
1319
|
},
|
|
1188
1320
|
});
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
if (invalidFileIds.length > 0) {
|
|
1192
|
-
throw new common_1.BadRequestException(`Invalid attachment file IDs: ${invalidFileIds.join(', ')}`);
|
|
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'));
|
|
1193
1323
|
}
|
|
1194
|
-
await this.
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
}
|
|
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 },
|
|
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' }),
|
|
1200
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');
|
|
1201
1353
|
}
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
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 },
|
|
1206
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,
|
|
1207
1440
|
title_id: title.id,
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
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,
|
|
1213
1469
|
amount_cents: amountCents,
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
notes: data.description,
|
|
1470
|
+
description: ((_a = data.description) === null || _a === void 0 ? void 0 : _a.trim()) || null,
|
|
1471
|
+
created_by_user_id: userId,
|
|
1217
1472
|
},
|
|
1218
1473
|
});
|
|
1219
|
-
|
|
1220
|
-
|
|
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
|
+
},
|
|
1221
1521
|
data: {
|
|
1222
|
-
|
|
1223
|
-
cost_center_id: data.cost_center_id,
|
|
1224
|
-
allocated_amount_cents: amountCents,
|
|
1522
|
+
status: nextInstallmentStatus,
|
|
1225
1523
|
},
|
|
1226
1524
|
});
|
|
1227
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');
|
|
1228
1673
|
}
|
|
1229
|
-
|
|
1230
|
-
return this.mapTitleToFront(createdTitle, data.payment_channel);
|
|
1674
|
+
return this.mapTitleToFront(updatedTitle);
|
|
1231
1675
|
}
|
|
1232
1676
|
async updateTitleTags(titleId, titleType, tagIds, locale) {
|
|
1233
1677
|
const title = await this.getTitleById(titleId, titleType, locale);
|
|
@@ -1377,6 +1821,124 @@ let FinanceService = FinanceService_1 = class FinanceService {
|
|
|
1377
1821
|
data: log.created_at.toISOString(),
|
|
1378
1822
|
}));
|
|
1379
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
|
+
}
|
|
1380
1942
|
mapTitleToFront(title, paymentChannelOverride) {
|
|
1381
1943
|
var _a;
|
|
1382
1944
|
const allocations = title.financial_installment.flatMap((installment) => installment.installment_allocation);
|
|
@@ -1397,21 +1959,26 @@ let FinanceService = FinanceService_1 = class FinanceService {
|
|
|
1397
1959
|
numero: installment.installment_number,
|
|
1398
1960
|
vencimento: installment.due_date.toISOString(),
|
|
1399
1961
|
valor: this.fromCents(installment.amount_cents),
|
|
1400
|
-
|
|
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)),
|
|
1401
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',
|
|
1402
1965
|
liquidacoes: installment.settlement_allocation.map((allocation) => {
|
|
1403
|
-
var _a, _b, _c, _d, _e;
|
|
1966
|
+
var _a, _b, _c, _d, _e, _f, _g;
|
|
1404
1967
|
return ({
|
|
1405
1968
|
id: String(allocation.id),
|
|
1406
|
-
|
|
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(),
|
|
1407
1973
|
valor: this.fromCents(allocation.allocated_amount_cents),
|
|
1408
1974
|
juros: this.fromCents(allocation.interest_cents || 0),
|
|
1409
1975
|
desconto: this.fromCents(allocation.discount_cents || 0),
|
|
1410
1976
|
multa: this.fromCents(allocation.penalty_cents || 0),
|
|
1411
|
-
contaBancariaId: ((
|
|
1977
|
+
contaBancariaId: ((_d = allocation.settlement) === null || _d === void 0 ? void 0 : _d.bank_account_id)
|
|
1412
1978
|
? String(allocation.settlement.bank_account_id)
|
|
1413
1979
|
: null,
|
|
1414
|
-
|
|
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',
|
|
1415
1982
|
});
|
|
1416
1983
|
}),
|
|
1417
1984
|
});
|
|
@@ -1510,6 +2077,27 @@ let FinanceService = FinanceService_1 = class FinanceService {
|
|
|
1510
2077
|
};
|
|
1511
2078
|
return paymentMethodMap[paymentMethodType] || undefined;
|
|
1512
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
|
+
}
|
|
1513
2101
|
mapAccountTypeToPt(accountType) {
|
|
1514
2102
|
const accountTypeMap = {
|
|
1515
2103
|
checking: 'corrente',
|