@hed-hog/finance 0.0.252 → 0.0.253
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/reverse-settlement.dto.d.ts +1 -0
- package/dist/dto/reverse-settlement.dto.d.ts.map +1 -1
- package/dist/dto/reverse-settlement.dto.js +5 -0
- package/dist/dto/reverse-settlement.dto.js.map +1 -1
- package/dist/finance-installments.controller.d.ts +40 -2
- package/dist/finance-installments.controller.d.ts.map +1 -1
- package/dist/finance-installments.controller.js +38 -2
- package/dist/finance-installments.controller.js.map +1 -1
- package/dist/finance.service.d.ts +38 -0
- package/dist/finance.service.d.ts.map +1 -1
- package/dist/finance.service.js +307 -118
- package/dist/finance.service.js.map +1 -1
- package/hedhog/data/route.yaml +27 -0
- package/hedhog/frontend/app/accounts-payable/installments/[id]/page.tsx.ejs +308 -99
- package/hedhog/query/settlement-auditability.sql +175 -0
- package/hedhog/table/bank_reconciliation.yaml +11 -0
- package/hedhog/table/settlement.yaml +17 -1
- package/hedhog/table/settlement_allocation.yaml +3 -0
- package/package.json +5 -5
- package/src/dto/reverse-settlement.dto.ts +4 -0
- package/src/finance-installments.controller.ts +45 -12
- package/src/finance.service.ts +439 -139
package/src/finance.service.ts
CHANGED
|
@@ -3,13 +3,13 @@ import { PaginationDTO, PaginationService } from '@hed-hog/api-pagination';
|
|
|
3
3
|
import { PrismaService } from '@hed-hog/api-prisma';
|
|
4
4
|
import { AiService, FileService } from '@hed-hog/core';
|
|
5
5
|
import {
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
6
|
+
BadRequestException,
|
|
7
|
+
ConflictException,
|
|
8
|
+
forwardRef,
|
|
9
|
+
Inject,
|
|
10
|
+
Injectable,
|
|
11
|
+
Logger,
|
|
12
|
+
NotFoundException,
|
|
13
13
|
} from '@nestjs/common';
|
|
14
14
|
import { createHash } from 'node:crypto';
|
|
15
15
|
import { readFile } from 'node:fs/promises';
|
|
@@ -1067,6 +1067,192 @@ export class FinanceService {
|
|
|
1067
1067
|
);
|
|
1068
1068
|
}
|
|
1069
1069
|
|
|
1070
|
+
async getTitleSettlementsHistory(titleId: number, locale: string) {
|
|
1071
|
+
const title = await this.prisma.financial_title.findUnique({
|
|
1072
|
+
where: { id: titleId },
|
|
1073
|
+
select: { id: true },
|
|
1074
|
+
});
|
|
1075
|
+
|
|
1076
|
+
if (!title) {
|
|
1077
|
+
throw new NotFoundException(
|
|
1078
|
+
getLocaleText(
|
|
1079
|
+
'itemNotFound',
|
|
1080
|
+
locale,
|
|
1081
|
+
`Financial title with ID ${titleId} not found`,
|
|
1082
|
+
).replace('{{item}}', 'Financial title'),
|
|
1083
|
+
);
|
|
1084
|
+
}
|
|
1085
|
+
|
|
1086
|
+
const rows = await this.prisma.$queryRaw<
|
|
1087
|
+
Array<{
|
|
1088
|
+
normal_id: number;
|
|
1089
|
+
normal_paid_at: Date;
|
|
1090
|
+
normal_amount_cents: number;
|
|
1091
|
+
normal_method: string | null;
|
|
1092
|
+
normal_account_id: number | null;
|
|
1093
|
+
normal_account_name: string | null;
|
|
1094
|
+
normal_created_at: Date;
|
|
1095
|
+
normal_created_by: string | null;
|
|
1096
|
+
normal_memo: string | null;
|
|
1097
|
+
reconciliation_id: number | null;
|
|
1098
|
+
reconciliation_status: string | null;
|
|
1099
|
+
installment_id: number;
|
|
1100
|
+
installment_seq: number;
|
|
1101
|
+
allocation_amount_cents: number;
|
|
1102
|
+
reversal_id: number | null;
|
|
1103
|
+
reversal_paid_at: Date | null;
|
|
1104
|
+
reversal_amount_cents: number | null;
|
|
1105
|
+
reversal_created_at: Date | null;
|
|
1106
|
+
reversal_created_by: string | null;
|
|
1107
|
+
reversal_memo: string | null;
|
|
1108
|
+
}>
|
|
1109
|
+
>`
|
|
1110
|
+
SELECT
|
|
1111
|
+
s.id AS normal_id,
|
|
1112
|
+
s.settled_at AS normal_paid_at,
|
|
1113
|
+
s.amount_cents AS normal_amount_cents,
|
|
1114
|
+
pm.type::text AS normal_method,
|
|
1115
|
+
s.bank_account_id AS normal_account_id,
|
|
1116
|
+
ba.name AS normal_account_name,
|
|
1117
|
+
s.created_at AS normal_created_at,
|
|
1118
|
+
u.name AS normal_created_by,
|
|
1119
|
+
s.description AS normal_memo,
|
|
1120
|
+
br.id AS reconciliation_id,
|
|
1121
|
+
br.status::text AS reconciliation_status,
|
|
1122
|
+
fi.id AS installment_id,
|
|
1123
|
+
fi.installment_number AS installment_seq,
|
|
1124
|
+
COALESCE(sa.amount_cents, sa.allocated_amount_cents) AS allocation_amount_cents,
|
|
1125
|
+
r.id AS reversal_id,
|
|
1126
|
+
r.settled_at AS reversal_paid_at,
|
|
1127
|
+
r.amount_cents AS reversal_amount_cents,
|
|
1128
|
+
r.created_at AS reversal_created_at,
|
|
1129
|
+
ur.name AS reversal_created_by,
|
|
1130
|
+
r.description AS reversal_memo
|
|
1131
|
+
FROM settlement s
|
|
1132
|
+
INNER JOIN settlement_allocation sa ON sa.settlement_id = s.id
|
|
1133
|
+
INNER JOIN financial_installment fi ON fi.id = sa.installment_id
|
|
1134
|
+
LEFT JOIN payment_method pm ON pm.id = s.payment_method_id
|
|
1135
|
+
LEFT JOIN bank_account ba ON ba.id = s.bank_account_id
|
|
1136
|
+
LEFT JOIN "user" u ON u.id = s.created_by_user_id
|
|
1137
|
+
LEFT JOIN bank_reconciliation br ON br.settlement_id = s.id
|
|
1138
|
+
LEFT JOIN settlement r ON r.reverses_settlement_id = s.id
|
|
1139
|
+
LEFT JOIN "user" ur ON ur.id = r.created_by_user_id
|
|
1140
|
+
WHERE fi.title_id = ${titleId}
|
|
1141
|
+
AND COALESCE(s.entry_type::text, 'normal') = 'normal'
|
|
1142
|
+
ORDER BY s.settled_at DESC, s.id DESC, fi.installment_number ASC
|
|
1143
|
+
`;
|
|
1144
|
+
|
|
1145
|
+
const groups = new Map<string, any>();
|
|
1146
|
+
|
|
1147
|
+
for (const row of rows) {
|
|
1148
|
+
const key = String(row.normal_id);
|
|
1149
|
+
const existing = groups.get(key);
|
|
1150
|
+
|
|
1151
|
+
if (!existing) {
|
|
1152
|
+
groups.set(key, {
|
|
1153
|
+
normal: {
|
|
1154
|
+
id: key,
|
|
1155
|
+
paidAt: row.normal_paid_at?.toISOString?.() || null,
|
|
1156
|
+
amountCents: Number(row.normal_amount_cents || 0),
|
|
1157
|
+
type: 'NORMAL',
|
|
1158
|
+
method: this.mapPaymentMethodToPt(row.normal_method) || row.normal_method,
|
|
1159
|
+
account: row.normal_account_name || null,
|
|
1160
|
+
accountId: row.normal_account_id
|
|
1161
|
+
? String(row.normal_account_id)
|
|
1162
|
+
: null,
|
|
1163
|
+
createdAt: row.normal_created_at?.toISOString?.() || null,
|
|
1164
|
+
createdBy: row.normal_created_by || null,
|
|
1165
|
+
memo: row.normal_memo || null,
|
|
1166
|
+
reconciled: row.reconciliation_status === 'reconciled',
|
|
1167
|
+
reconciliationId: row.reconciliation_id
|
|
1168
|
+
? String(row.reconciliation_id)
|
|
1169
|
+
: null,
|
|
1170
|
+
},
|
|
1171
|
+
reversal: row.reversal_id
|
|
1172
|
+
? {
|
|
1173
|
+
id: String(row.reversal_id),
|
|
1174
|
+
paidAt: row.reversal_paid_at?.toISOString?.() || null,
|
|
1175
|
+
amountCents: Number(row.reversal_amount_cents || 0),
|
|
1176
|
+
type: 'REVERSAL',
|
|
1177
|
+
createdAt: row.reversal_created_at?.toISOString?.() || null,
|
|
1178
|
+
createdBy: row.reversal_created_by || null,
|
|
1179
|
+
memo: row.reversal_memo || null,
|
|
1180
|
+
}
|
|
1181
|
+
: null,
|
|
1182
|
+
allocations: [],
|
|
1183
|
+
statusLabel: row.reversal_id ? 'ESTORNADO' : 'ATIVO',
|
|
1184
|
+
});
|
|
1185
|
+
}
|
|
1186
|
+
|
|
1187
|
+
groups.get(key).allocations.push({
|
|
1188
|
+
installmentId: String(row.installment_id),
|
|
1189
|
+
installmentSeq: Number(row.installment_seq || 0),
|
|
1190
|
+
amountCents: Number(row.allocation_amount_cents || 0),
|
|
1191
|
+
});
|
|
1192
|
+
}
|
|
1193
|
+
|
|
1194
|
+
return Array.from(groups.values());
|
|
1195
|
+
}
|
|
1196
|
+
|
|
1197
|
+
async reverseSettlementById(
|
|
1198
|
+
settlementId: number,
|
|
1199
|
+
data: ReverseSettlementDto,
|
|
1200
|
+
locale: string,
|
|
1201
|
+
userId?: number,
|
|
1202
|
+
) {
|
|
1203
|
+
const updatedTitle = await this.reverseSettlementInternal(
|
|
1204
|
+
settlementId,
|
|
1205
|
+
data,
|
|
1206
|
+
locale,
|
|
1207
|
+
userId,
|
|
1208
|
+
);
|
|
1209
|
+
|
|
1210
|
+
return this.mapTitleToFront(updatedTitle);
|
|
1211
|
+
}
|
|
1212
|
+
|
|
1213
|
+
async unreconcileBankReconciliation(id: number, userId?: number) {
|
|
1214
|
+
const reconciliation = await this.prisma.bank_reconciliation.findUnique({
|
|
1215
|
+
where: { id },
|
|
1216
|
+
select: {
|
|
1217
|
+
id: true,
|
|
1218
|
+
settlement_id: true,
|
|
1219
|
+
bank_statement_line_id: true,
|
|
1220
|
+
},
|
|
1221
|
+
});
|
|
1222
|
+
|
|
1223
|
+
if (!reconciliation) {
|
|
1224
|
+
throw new NotFoundException('Conciliação bancária não encontrada');
|
|
1225
|
+
}
|
|
1226
|
+
|
|
1227
|
+
await this.prisma.$transaction(async (tx) => {
|
|
1228
|
+
await tx.bank_reconciliation.delete({
|
|
1229
|
+
where: { id: reconciliation.id },
|
|
1230
|
+
});
|
|
1231
|
+
|
|
1232
|
+
await tx.bank_statement_line.updateMany({
|
|
1233
|
+
where: {
|
|
1234
|
+
id: reconciliation.bank_statement_line_id,
|
|
1235
|
+
status: {
|
|
1236
|
+
in: ['reconciled', 'adjusted'],
|
|
1237
|
+
},
|
|
1238
|
+
},
|
|
1239
|
+
data: {
|
|
1240
|
+
status: 'pending',
|
|
1241
|
+
},
|
|
1242
|
+
});
|
|
1243
|
+
|
|
1244
|
+
await this.createAuditLog(tx, {
|
|
1245
|
+
action: 'UNRECONCILE_SETTLEMENT',
|
|
1246
|
+
entityTable: 'bank_reconciliation',
|
|
1247
|
+
entityId: String(reconciliation.id),
|
|
1248
|
+
actorUserId: userId,
|
|
1249
|
+
summary: `Unreconciled settlement ${reconciliation.settlement_id}`,
|
|
1250
|
+
});
|
|
1251
|
+
});
|
|
1252
|
+
|
|
1253
|
+
return { success: true };
|
|
1254
|
+
}
|
|
1255
|
+
|
|
1070
1256
|
async createTag(data: CreateFinanceTagDto) {
|
|
1071
1257
|
const slug = this.normalizeTagSlug(data.name);
|
|
1072
1258
|
|
|
@@ -2914,21 +3100,20 @@ export class FinanceService {
|
|
|
2914
3100
|
throw new BadRequestException('Title cannot be canceled in current status');
|
|
2915
3101
|
}
|
|
2916
3102
|
|
|
2917
|
-
const
|
|
2918
|
-
|
|
2919
|
-
|
|
2920
|
-
|
|
2921
|
-
|
|
2922
|
-
settlement
|
|
2923
|
-
|
|
2924
|
-
|
|
2925
|
-
|
|
2926
|
-
|
|
2927
|
-
|
|
2928
|
-
|
|
2929
|
-
|
|
2930
|
-
|
|
2931
|
-
});
|
|
3103
|
+
const activeSettlements = await tx.$queryRaw<Array<{ has_active: boolean }>>`
|
|
3104
|
+
SELECT EXISTS (
|
|
3105
|
+
SELECT 1
|
|
3106
|
+
FROM settlement_allocation sa
|
|
3107
|
+
INNER JOIN financial_installment fi ON fi.id = sa.installment_id
|
|
3108
|
+
INNER JOIN settlement s ON s.id = sa.settlement_id
|
|
3109
|
+
LEFT JOIN settlement r ON r.reverses_settlement_id = s.id
|
|
3110
|
+
WHERE fi.title_id = ${title.id}
|
|
3111
|
+
AND COALESCE(s.entry_type::text, 'normal') = 'normal'
|
|
3112
|
+
AND r.id IS NULL
|
|
3113
|
+
) AS has_active
|
|
3114
|
+
`;
|
|
3115
|
+
|
|
3116
|
+
const hasActiveSettlements = activeSettlements[0]?.has_active;
|
|
2932
3117
|
|
|
2933
3118
|
if (hasActiveSettlements) {
|
|
2934
3119
|
throw new ConflictException(
|
|
@@ -3227,97 +3412,241 @@ export class FinanceService {
|
|
|
3227
3412
|
locale: string,
|
|
3228
3413
|
userId?: number,
|
|
3229
3414
|
) {
|
|
3230
|
-
const updatedTitle = await this.
|
|
3231
|
-
|
|
3232
|
-
|
|
3233
|
-
|
|
3234
|
-
|
|
3235
|
-
|
|
3236
|
-
|
|
3237
|
-
|
|
3238
|
-
|
|
3239
|
-
|
|
3240
|
-
},
|
|
3241
|
-
});
|
|
3415
|
+
const updatedTitle = await this.reverseSettlementInternal(
|
|
3416
|
+
settlementId,
|
|
3417
|
+
data,
|
|
3418
|
+
locale,
|
|
3419
|
+
userId,
|
|
3420
|
+
{
|
|
3421
|
+
titleId,
|
|
3422
|
+
titleType,
|
|
3423
|
+
},
|
|
3424
|
+
);
|
|
3242
3425
|
|
|
3243
|
-
|
|
3244
|
-
|
|
3245
|
-
|
|
3246
|
-
|
|
3247
|
-
|
|
3248
|
-
|
|
3249
|
-
|
|
3250
|
-
|
|
3426
|
+
if (!updatedTitle) {
|
|
3427
|
+
throw new NotFoundException('Financial title not found');
|
|
3428
|
+
}
|
|
3429
|
+
|
|
3430
|
+
return this.mapTitleToFront(updatedTitle);
|
|
3431
|
+
}
|
|
3432
|
+
|
|
3433
|
+
private async reverseSettlementInternal(
|
|
3434
|
+
settlementId: number,
|
|
3435
|
+
data: ReverseSettlementDto,
|
|
3436
|
+
locale: string,
|
|
3437
|
+
userId?: number,
|
|
3438
|
+
scope?: {
|
|
3439
|
+
titleId?: number;
|
|
3440
|
+
titleType?: TitleType;
|
|
3441
|
+
},
|
|
3442
|
+
) {
|
|
3443
|
+
const { title } = await this.prisma.$transaction(async (tx) => {
|
|
3444
|
+
const settlementRows = await tx.$queryRaw<
|
|
3445
|
+
Array<{
|
|
3446
|
+
id: number;
|
|
3447
|
+
settlement_type: string;
|
|
3448
|
+
settled_at: Date;
|
|
3449
|
+
amount_cents: number;
|
|
3450
|
+
description: string | null;
|
|
3451
|
+
person_id: number | null;
|
|
3452
|
+
bank_account_id: number | null;
|
|
3453
|
+
payment_method_id: number | null;
|
|
3454
|
+
created_by_user_id: number | null;
|
|
3455
|
+
entry_type: string | null;
|
|
3456
|
+
title_id: number;
|
|
3457
|
+
title_type: string;
|
|
3458
|
+
title_status: string;
|
|
3459
|
+
title_competence_date: Date | null;
|
|
3460
|
+
}>
|
|
3461
|
+
>`
|
|
3462
|
+
SELECT DISTINCT
|
|
3463
|
+
s.id,
|
|
3464
|
+
s.settlement_type::text,
|
|
3465
|
+
s.settled_at,
|
|
3466
|
+
s.amount_cents,
|
|
3467
|
+
s.description,
|
|
3468
|
+
s.person_id,
|
|
3469
|
+
s.bank_account_id,
|
|
3470
|
+
s.payment_method_id,
|
|
3471
|
+
s.created_by_user_id,
|
|
3472
|
+
COALESCE(s.entry_type::text, 'normal') AS entry_type,
|
|
3473
|
+
ft.id AS title_id,
|
|
3474
|
+
ft.title_type::text AS title_type,
|
|
3475
|
+
ft.status::text AS title_status,
|
|
3476
|
+
ft.competence_date AS title_competence_date
|
|
3477
|
+
FROM settlement s
|
|
3478
|
+
INNER JOIN settlement_allocation sa ON sa.settlement_id = s.id
|
|
3479
|
+
INNER JOIN financial_installment fi ON fi.id = sa.installment_id
|
|
3480
|
+
INNER JOIN financial_title ft ON ft.id = fi.title_id
|
|
3481
|
+
WHERE s.id = ${settlementId}
|
|
3482
|
+
FOR UPDATE OF s
|
|
3483
|
+
`;
|
|
3484
|
+
|
|
3485
|
+
const settlement = settlementRows[0];
|
|
3486
|
+
|
|
3487
|
+
if (!settlement) {
|
|
3488
|
+
throw new NotFoundException('Settlement not found');
|
|
3489
|
+
}
|
|
3490
|
+
|
|
3491
|
+
if (scope?.titleId && settlement.title_id !== scope.titleId) {
|
|
3492
|
+
throw new NotFoundException('Settlement not found for this title');
|
|
3493
|
+
}
|
|
3494
|
+
|
|
3495
|
+
if (scope?.titleType && settlement.title_type !== scope.titleType) {
|
|
3496
|
+
throw new NotFoundException('Settlement not found for this title type');
|
|
3497
|
+
}
|
|
3498
|
+
|
|
3499
|
+
if (settlement.entry_type !== 'normal') {
|
|
3500
|
+
throw new BadRequestException('Somente liquidações normais podem ser estornadas');
|
|
3251
3501
|
}
|
|
3252
3502
|
|
|
3253
3503
|
await this.assertDateNotInClosedPeriod(
|
|
3254
3504
|
tx,
|
|
3255
|
-
|
|
3505
|
+
settlement.title_competence_date,
|
|
3256
3506
|
'reverse settlement',
|
|
3257
3507
|
);
|
|
3258
3508
|
|
|
3259
|
-
const
|
|
3260
|
-
|
|
3261
|
-
|
|
3262
|
-
|
|
3263
|
-
|
|
3264
|
-
|
|
3265
|
-
financial_installment: {
|
|
3266
|
-
title_id: title.id,
|
|
3267
|
-
},
|
|
3268
|
-
},
|
|
3269
|
-
},
|
|
3270
|
-
},
|
|
3271
|
-
include: {
|
|
3272
|
-
settlement_allocation: {
|
|
3273
|
-
include: {
|
|
3274
|
-
financial_installment: {
|
|
3275
|
-
select: {
|
|
3276
|
-
id: true,
|
|
3277
|
-
amount_cents: true,
|
|
3278
|
-
open_amount_cents: true,
|
|
3279
|
-
due_date: true,
|
|
3280
|
-
status: true,
|
|
3281
|
-
},
|
|
3282
|
-
},
|
|
3283
|
-
},
|
|
3284
|
-
},
|
|
3285
|
-
},
|
|
3286
|
-
});
|
|
3509
|
+
const alreadyReversed = await tx.$queryRaw<Array<{ id: number }>>`
|
|
3510
|
+
SELECT id
|
|
3511
|
+
FROM settlement
|
|
3512
|
+
WHERE reverses_settlement_id = ${settlement.id}
|
|
3513
|
+
LIMIT 1
|
|
3514
|
+
`;
|
|
3287
3515
|
|
|
3288
|
-
if (
|
|
3289
|
-
throw new
|
|
3516
|
+
if (alreadyReversed.length > 0) {
|
|
3517
|
+
throw new ConflictException('Liquidação já estornada.');
|
|
3290
3518
|
}
|
|
3291
3519
|
|
|
3292
|
-
|
|
3293
|
-
|
|
3520
|
+
const isReconciled = await tx.$queryRaw<Array<{ id: number }>>`
|
|
3521
|
+
SELECT id
|
|
3522
|
+
FROM bank_reconciliation
|
|
3523
|
+
WHERE settlement_id = ${settlement.id}
|
|
3524
|
+
AND status = 'reconciled'
|
|
3525
|
+
LIMIT 1
|
|
3526
|
+
`;
|
|
3527
|
+
|
|
3528
|
+
if (isReconciled.length > 0) {
|
|
3529
|
+
throw new ConflictException('Desconciliar primeiro');
|
|
3294
3530
|
}
|
|
3295
3531
|
|
|
3296
|
-
|
|
3297
|
-
|
|
3532
|
+
const allocations = await tx.$queryRaw<
|
|
3533
|
+
Array<{
|
|
3534
|
+
id: number;
|
|
3535
|
+
installment_id: number;
|
|
3536
|
+
allocated_amount_cents: number;
|
|
3537
|
+
amount_cents: number | null;
|
|
3538
|
+
discount_cents: number;
|
|
3539
|
+
interest_cents: number;
|
|
3540
|
+
penalty_cents: number;
|
|
3541
|
+
installment_amount_cents: number;
|
|
3542
|
+
installment_open_amount_cents: number;
|
|
3543
|
+
installment_due_date: Date;
|
|
3544
|
+
installment_status: string;
|
|
3545
|
+
}>
|
|
3546
|
+
>`
|
|
3547
|
+
SELECT
|
|
3548
|
+
sa.id,
|
|
3549
|
+
sa.installment_id,
|
|
3550
|
+
sa.allocated_amount_cents,
|
|
3551
|
+
sa.amount_cents,
|
|
3552
|
+
sa.discount_cents,
|
|
3553
|
+
sa.interest_cents,
|
|
3554
|
+
sa.penalty_cents,
|
|
3555
|
+
fi.amount_cents AS installment_amount_cents,
|
|
3556
|
+
fi.open_amount_cents AS installment_open_amount_cents,
|
|
3557
|
+
fi.due_date AS installment_due_date,
|
|
3558
|
+
fi.status::text AS installment_status
|
|
3559
|
+
FROM settlement_allocation sa
|
|
3560
|
+
INNER JOIN financial_installment fi ON fi.id = sa.installment_id
|
|
3561
|
+
WHERE sa.settlement_id = ${settlement.id}
|
|
3562
|
+
FOR UPDATE OF fi
|
|
3563
|
+
`;
|
|
3564
|
+
|
|
3565
|
+
if (allocations.length === 0) {
|
|
3566
|
+
throw new BadRequestException('Settlement has no allocations to reverse');
|
|
3567
|
+
}
|
|
3298
3568
|
|
|
3299
|
-
|
|
3300
|
-
|
|
3301
|
-
|
|
3569
|
+
const reversalMemo = data.reason?.trim() || data.memo?.trim() || 'Estorno';
|
|
3570
|
+
const reversalAmountCents = -Math.abs(Number(settlement.amount_cents || 0));
|
|
3571
|
+
|
|
3572
|
+
const reversalResult = await tx.$queryRaw<Array<{ id: number }>>`
|
|
3573
|
+
INSERT INTO settlement (
|
|
3574
|
+
person_id,
|
|
3575
|
+
bank_account_id,
|
|
3576
|
+
payment_method_id,
|
|
3577
|
+
settlement_type,
|
|
3578
|
+
entry_type,
|
|
3579
|
+
status,
|
|
3580
|
+
settled_at,
|
|
3581
|
+
amount_cents,
|
|
3582
|
+
description,
|
|
3583
|
+
external_reference,
|
|
3584
|
+
created_by_user_id,
|
|
3585
|
+
reverses_settlement_id,
|
|
3586
|
+
created_at,
|
|
3587
|
+
updated_at
|
|
3588
|
+
)
|
|
3589
|
+
VALUES (
|
|
3590
|
+
${settlement.person_id},
|
|
3591
|
+
${settlement.bank_account_id},
|
|
3592
|
+
${settlement.payment_method_id},
|
|
3593
|
+
${settlement.settlement_type}::settlement_settlement_type_enum,
|
|
3594
|
+
'reversal'::settlement_entry_type_enum,
|
|
3595
|
+
'confirmed'::settlement_status_enum,
|
|
3596
|
+
NOW(),
|
|
3597
|
+
${reversalAmountCents},
|
|
3598
|
+
${reversalMemo},
|
|
3599
|
+
NULL,
|
|
3600
|
+
${userId || settlement.created_by_user_id || null},
|
|
3601
|
+
${settlement.id},
|
|
3602
|
+
NOW(),
|
|
3603
|
+
NOW()
|
|
3604
|
+
)
|
|
3605
|
+
RETURNING id
|
|
3606
|
+
`;
|
|
3607
|
+
|
|
3608
|
+
const reversalId = reversalResult[0]?.id;
|
|
3609
|
+
|
|
3610
|
+
if (!reversalId) {
|
|
3611
|
+
throw new BadRequestException('Could not create reversal settlement');
|
|
3612
|
+
}
|
|
3613
|
+
|
|
3614
|
+
for (const allocation of allocations) {
|
|
3615
|
+
const originalAmount = Number(
|
|
3616
|
+
allocation.amount_cents ?? allocation.allocated_amount_cents ?? 0,
|
|
3617
|
+
);
|
|
3618
|
+
|
|
3619
|
+
await tx.settlement_allocation.create({
|
|
3620
|
+
data: {
|
|
3621
|
+
settlement_id: reversalId,
|
|
3622
|
+
installment_id: allocation.installment_id,
|
|
3623
|
+
allocated_amount_cents: -Math.abs(originalAmount),
|
|
3624
|
+
discount_cents: -Math.abs(allocation.discount_cents || 0),
|
|
3625
|
+
interest_cents: -Math.abs(allocation.interest_cents || 0),
|
|
3626
|
+
penalty_cents: -Math.abs(allocation.penalty_cents || 0),
|
|
3627
|
+
},
|
|
3628
|
+
});
|
|
3302
3629
|
|
|
3303
3630
|
const nextOpenAmountCents =
|
|
3304
|
-
|
|
3631
|
+
Number(allocation.installment_open_amount_cents || 0) +
|
|
3632
|
+
Math.abs(originalAmount);
|
|
3305
3633
|
|
|
3306
|
-
if (nextOpenAmountCents >
|
|
3307
|
-
throw new
|
|
3308
|
-
`
|
|
3634
|
+
if (nextOpenAmountCents > Number(allocation.installment_amount_cents || 0)) {
|
|
3635
|
+
throw new ConflictException(
|
|
3636
|
+
`Estorno excederia o valor original da parcela ${allocation.installment_id}`,
|
|
3309
3637
|
);
|
|
3310
3638
|
}
|
|
3311
3639
|
|
|
3312
3640
|
const nextInstallmentStatus = this.resolveInstallmentStatus(
|
|
3313
|
-
|
|
3641
|
+
Number(allocation.installment_amount_cents || 0),
|
|
3314
3642
|
nextOpenAmountCents,
|
|
3315
|
-
|
|
3643
|
+
new Date(allocation.installment_due_date),
|
|
3644
|
+
allocation.installment_status,
|
|
3316
3645
|
);
|
|
3317
3646
|
|
|
3318
3647
|
await tx.financial_installment.update({
|
|
3319
3648
|
where: {
|
|
3320
|
-
id:
|
|
3649
|
+
id: allocation.installment_id,
|
|
3321
3650
|
},
|
|
3322
3651
|
data: {
|
|
3323
3652
|
open_amount_cents: nextOpenAmountCents,
|
|
@@ -3326,76 +3655,47 @@ export class FinanceService {
|
|
|
3326
3655
|
});
|
|
3327
3656
|
}
|
|
3328
3657
|
|
|
3329
|
-
await
|
|
3330
|
-
|
|
3331
|
-
|
|
3332
|
-
|
|
3333
|
-
data: {
|
|
3334
|
-
status: 'reversed',
|
|
3335
|
-
description: [
|
|
3336
|
-
settlement.description,
|
|
3337
|
-
data.reason ? `Reversed: ${data.reason.trim()}` : 'Reversed',
|
|
3338
|
-
]
|
|
3339
|
-
.filter(Boolean)
|
|
3340
|
-
.join(' | '),
|
|
3341
|
-
},
|
|
3342
|
-
});
|
|
3343
|
-
|
|
3344
|
-
await tx.bank_reconciliation.updateMany({
|
|
3345
|
-
where: {
|
|
3346
|
-
settlement_id: settlement.id,
|
|
3347
|
-
status: 'pending',
|
|
3348
|
-
},
|
|
3349
|
-
data: {
|
|
3350
|
-
status: 'reversed',
|
|
3351
|
-
},
|
|
3352
|
-
});
|
|
3353
|
-
|
|
3354
|
-
await tx.bank_reconciliation.updateMany({
|
|
3355
|
-
where: {
|
|
3356
|
-
settlement_id: settlement.id,
|
|
3357
|
-
status: {
|
|
3358
|
-
in: ['reconciled', 'adjusted'],
|
|
3359
|
-
},
|
|
3360
|
-
},
|
|
3361
|
-
data: {
|
|
3362
|
-
status: 'adjusted',
|
|
3363
|
-
},
|
|
3364
|
-
});
|
|
3365
|
-
|
|
3366
|
-
const previousTitleStatus = title.status;
|
|
3367
|
-
const nextTitleStatus = await this.recalculateTitleStatus(tx, title.id);
|
|
3658
|
+
const nextTitleStatus = await this.recalculateTitleStatus(
|
|
3659
|
+
tx,
|
|
3660
|
+
settlement.title_id,
|
|
3661
|
+
);
|
|
3368
3662
|
|
|
3369
3663
|
await this.createAuditLog(tx, {
|
|
3370
3664
|
action: 'REVERSE_SETTLEMENT',
|
|
3371
3665
|
entityTable: 'financial_title',
|
|
3372
|
-
entityId: String(
|
|
3666
|
+
entityId: String(settlement.title_id),
|
|
3373
3667
|
actorUserId: userId,
|
|
3374
|
-
summary: `
|
|
3668
|
+
summary: `Created reversal ${reversalId} for settlement ${settlement.id}`,
|
|
3375
3669
|
beforeData: JSON.stringify({
|
|
3376
|
-
title_status:
|
|
3377
|
-
|
|
3670
|
+
title_status: settlement.title_status,
|
|
3671
|
+
settlement_id: settlement.id,
|
|
3672
|
+
settlement_entry_type: settlement.entry_type,
|
|
3378
3673
|
}),
|
|
3379
3674
|
afterData: JSON.stringify({
|
|
3380
3675
|
title_status: nextTitleStatus,
|
|
3381
|
-
|
|
3676
|
+
settlement_id: settlement.id,
|
|
3677
|
+
reversal_settlement_id: reversalId,
|
|
3382
3678
|
}),
|
|
3383
3679
|
});
|
|
3384
3680
|
|
|
3385
|
-
|
|
3681
|
+
const updatedTitle = await tx.financial_title.findFirst({
|
|
3386
3682
|
where: {
|
|
3387
|
-
id:
|
|
3388
|
-
title_type:
|
|
3683
|
+
id: settlement.title_id,
|
|
3684
|
+
title_type: settlement.title_type as TitleType,
|
|
3389
3685
|
},
|
|
3390
3686
|
include: this.defaultTitleInclude(),
|
|
3391
3687
|
});
|
|
3392
|
-
});
|
|
3393
3688
|
|
|
3394
|
-
|
|
3395
|
-
|
|
3396
|
-
|
|
3689
|
+
if (!updatedTitle) {
|
|
3690
|
+
throw new NotFoundException('Financial title not found');
|
|
3691
|
+
}
|
|
3397
3692
|
|
|
3398
|
-
|
|
3693
|
+
return {
|
|
3694
|
+
title: updatedTitle,
|
|
3695
|
+
};
|
|
3696
|
+
});
|
|
3697
|
+
|
|
3698
|
+
return title;
|
|
3399
3699
|
}
|
|
3400
3700
|
|
|
3401
3701
|
private async updateTitleTags(
|