@hed-hog/finance 0.0.252 → 0.0.256
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 +106 -4
- 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 +104 -2
- package/dist/finance.service.d.ts.map +1 -1
- package/dist/finance.service.js +366 -121
- package/dist/finance.service.js.map +1 -1
- package/hedhog/data/route.yaml +27 -0
- package/hedhog/frontend/app/_components/finance-entity-field-with-create.tsx.ejs +572 -0
- package/hedhog/frontend/app/_components/finance-title-actions-menu.tsx.ejs +244 -0
- package/hedhog/frontend/app/_components/person-field-with-create.tsx.ejs +143 -51
- package/hedhog/frontend/app/_lib/title-action-rules.ts.ejs +36 -0
- package/hedhog/frontend/app/accounts-payable/installments/[id]/page.tsx.ejs +449 -293
- package/hedhog/frontend/app/accounts-payable/installments/page.tsx.ejs +1189 -545
- package/hedhog/frontend/app/accounts-receivable/installments/[id]/page.tsx.ejs +176 -133
- package/hedhog/frontend/app/accounts-receivable/installments/page.tsx.ejs +1459 -312
- package/hedhog/frontend/app/page.tsx.ejs +15 -4
- package/hedhog/frontend/messages/en.json +294 -5
- package/hedhog/frontend/messages/pt.json +294 -5
- 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 +7 -7
- package/src/dto/reverse-settlement.dto.ts +4 -0
- package/src/finance-installments.controller.ts +45 -12
- package/src/finance.service.ts +521 -146
package/src/finance.service.ts
CHANGED
|
@@ -1,15 +1,19 @@
|
|
|
1
1
|
import { getLocaleText } from '@hed-hog/api-locale';
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
PageOrderDirection,
|
|
4
|
+
PaginationDTO,
|
|
5
|
+
PaginationService,
|
|
6
|
+
} from '@hed-hog/api-pagination';
|
|
3
7
|
import { PrismaService } from '@hed-hog/api-prisma';
|
|
4
8
|
import { AiService, FileService } from '@hed-hog/core';
|
|
5
9
|
import {
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
10
|
+
BadRequestException,
|
|
11
|
+
ConflictException,
|
|
12
|
+
forwardRef,
|
|
13
|
+
Inject,
|
|
14
|
+
Injectable,
|
|
15
|
+
Logger,
|
|
16
|
+
NotFoundException,
|
|
13
17
|
} from '@nestjs/common';
|
|
14
18
|
import { createHash } from 'node:crypto';
|
|
15
19
|
import { readFile } from 'node:fs/promises';
|
|
@@ -1067,6 +1071,192 @@ export class FinanceService {
|
|
|
1067
1071
|
);
|
|
1068
1072
|
}
|
|
1069
1073
|
|
|
1074
|
+
async getTitleSettlementsHistory(titleId: number, locale: string) {
|
|
1075
|
+
const title = await this.prisma.financial_title.findUnique({
|
|
1076
|
+
where: { id: titleId },
|
|
1077
|
+
select: { id: true },
|
|
1078
|
+
});
|
|
1079
|
+
|
|
1080
|
+
if (!title) {
|
|
1081
|
+
throw new NotFoundException(
|
|
1082
|
+
getLocaleText(
|
|
1083
|
+
'itemNotFound',
|
|
1084
|
+
locale,
|
|
1085
|
+
`Financial title with ID ${titleId} not found`,
|
|
1086
|
+
).replace('{{item}}', 'Financial title'),
|
|
1087
|
+
);
|
|
1088
|
+
}
|
|
1089
|
+
|
|
1090
|
+
const rows = await this.prisma.$queryRaw<
|
|
1091
|
+
Array<{
|
|
1092
|
+
normal_id: number;
|
|
1093
|
+
normal_paid_at: Date;
|
|
1094
|
+
normal_amount_cents: number;
|
|
1095
|
+
normal_method: string | null;
|
|
1096
|
+
normal_account_id: number | null;
|
|
1097
|
+
normal_account_name: string | null;
|
|
1098
|
+
normal_created_at: Date;
|
|
1099
|
+
normal_created_by: string | null;
|
|
1100
|
+
normal_memo: string | null;
|
|
1101
|
+
reconciliation_id: number | null;
|
|
1102
|
+
reconciliation_status: string | null;
|
|
1103
|
+
installment_id: number;
|
|
1104
|
+
installment_seq: number;
|
|
1105
|
+
allocation_amount_cents: number;
|
|
1106
|
+
reversal_id: number | null;
|
|
1107
|
+
reversal_paid_at: Date | null;
|
|
1108
|
+
reversal_amount_cents: number | null;
|
|
1109
|
+
reversal_created_at: Date | null;
|
|
1110
|
+
reversal_created_by: string | null;
|
|
1111
|
+
reversal_memo: string | null;
|
|
1112
|
+
}>
|
|
1113
|
+
>`
|
|
1114
|
+
SELECT
|
|
1115
|
+
s.id AS normal_id,
|
|
1116
|
+
s.settled_at AS normal_paid_at,
|
|
1117
|
+
s.amount_cents AS normal_amount_cents,
|
|
1118
|
+
pm.type::text AS normal_method,
|
|
1119
|
+
s.bank_account_id AS normal_account_id,
|
|
1120
|
+
ba.name AS normal_account_name,
|
|
1121
|
+
s.created_at AS normal_created_at,
|
|
1122
|
+
u.name AS normal_created_by,
|
|
1123
|
+
s.description AS normal_memo,
|
|
1124
|
+
br.id AS reconciliation_id,
|
|
1125
|
+
br.status::text AS reconciliation_status,
|
|
1126
|
+
fi.id AS installment_id,
|
|
1127
|
+
fi.installment_number AS installment_seq,
|
|
1128
|
+
COALESCE(sa.amount_cents, sa.allocated_amount_cents) AS allocation_amount_cents,
|
|
1129
|
+
r.id AS reversal_id,
|
|
1130
|
+
r.settled_at AS reversal_paid_at,
|
|
1131
|
+
r.amount_cents AS reversal_amount_cents,
|
|
1132
|
+
r.created_at AS reversal_created_at,
|
|
1133
|
+
ur.name AS reversal_created_by,
|
|
1134
|
+
r.description AS reversal_memo
|
|
1135
|
+
FROM settlement s
|
|
1136
|
+
INNER JOIN settlement_allocation sa ON sa.settlement_id = s.id
|
|
1137
|
+
INNER JOIN financial_installment fi ON fi.id = sa.installment_id
|
|
1138
|
+
LEFT JOIN payment_method pm ON pm.id = s.payment_method_id
|
|
1139
|
+
LEFT JOIN bank_account ba ON ba.id = s.bank_account_id
|
|
1140
|
+
LEFT JOIN "user" u ON u.id = s.created_by_user_id
|
|
1141
|
+
LEFT JOIN bank_reconciliation br ON br.settlement_id = s.id
|
|
1142
|
+
LEFT JOIN settlement r ON r.reverses_settlement_id = s.id
|
|
1143
|
+
LEFT JOIN "user" ur ON ur.id = r.created_by_user_id
|
|
1144
|
+
WHERE fi.title_id = ${titleId}
|
|
1145
|
+
AND COALESCE(s.entry_type::text, 'normal') = 'normal'
|
|
1146
|
+
ORDER BY s.settled_at DESC, s.id DESC, fi.installment_number ASC
|
|
1147
|
+
`;
|
|
1148
|
+
|
|
1149
|
+
const groups = new Map<string, any>();
|
|
1150
|
+
|
|
1151
|
+
for (const row of rows) {
|
|
1152
|
+
const key = String(row.normal_id);
|
|
1153
|
+
const existing = groups.get(key);
|
|
1154
|
+
|
|
1155
|
+
if (!existing) {
|
|
1156
|
+
groups.set(key, {
|
|
1157
|
+
normal: {
|
|
1158
|
+
id: key,
|
|
1159
|
+
paidAt: row.normal_paid_at?.toISOString?.() || null,
|
|
1160
|
+
amountCents: Number(row.normal_amount_cents || 0),
|
|
1161
|
+
type: 'NORMAL',
|
|
1162
|
+
method: this.mapPaymentMethodToPt(row.normal_method) || row.normal_method,
|
|
1163
|
+
account: row.normal_account_name || null,
|
|
1164
|
+
accountId: row.normal_account_id
|
|
1165
|
+
? String(row.normal_account_id)
|
|
1166
|
+
: null,
|
|
1167
|
+
createdAt: row.normal_created_at?.toISOString?.() || null,
|
|
1168
|
+
createdBy: row.normal_created_by || null,
|
|
1169
|
+
memo: row.normal_memo || null,
|
|
1170
|
+
reconciled: row.reconciliation_status === 'reconciled',
|
|
1171
|
+
reconciliationId: row.reconciliation_id
|
|
1172
|
+
? String(row.reconciliation_id)
|
|
1173
|
+
: null,
|
|
1174
|
+
},
|
|
1175
|
+
reversal: row.reversal_id
|
|
1176
|
+
? {
|
|
1177
|
+
id: String(row.reversal_id),
|
|
1178
|
+
paidAt: row.reversal_paid_at?.toISOString?.() || null,
|
|
1179
|
+
amountCents: Number(row.reversal_amount_cents || 0),
|
|
1180
|
+
type: 'REVERSAL',
|
|
1181
|
+
createdAt: row.reversal_created_at?.toISOString?.() || null,
|
|
1182
|
+
createdBy: row.reversal_created_by || null,
|
|
1183
|
+
memo: row.reversal_memo || null,
|
|
1184
|
+
}
|
|
1185
|
+
: null,
|
|
1186
|
+
allocations: [],
|
|
1187
|
+
statusLabel: row.reversal_id ? 'ESTORNADO' : 'ATIVO',
|
|
1188
|
+
});
|
|
1189
|
+
}
|
|
1190
|
+
|
|
1191
|
+
groups.get(key).allocations.push({
|
|
1192
|
+
installmentId: String(row.installment_id),
|
|
1193
|
+
installmentSeq: Number(row.installment_seq || 0),
|
|
1194
|
+
amountCents: Number(row.allocation_amount_cents || 0),
|
|
1195
|
+
});
|
|
1196
|
+
}
|
|
1197
|
+
|
|
1198
|
+
return Array.from(groups.values());
|
|
1199
|
+
}
|
|
1200
|
+
|
|
1201
|
+
async reverseSettlementById(
|
|
1202
|
+
settlementId: number,
|
|
1203
|
+
data: ReverseSettlementDto,
|
|
1204
|
+
locale: string,
|
|
1205
|
+
userId?: number,
|
|
1206
|
+
) {
|
|
1207
|
+
const updatedTitle = await this.reverseSettlementInternal(
|
|
1208
|
+
settlementId,
|
|
1209
|
+
data,
|
|
1210
|
+
locale,
|
|
1211
|
+
userId,
|
|
1212
|
+
);
|
|
1213
|
+
|
|
1214
|
+
return this.mapTitleToFront(updatedTitle);
|
|
1215
|
+
}
|
|
1216
|
+
|
|
1217
|
+
async unreconcileBankReconciliation(id: number, userId?: number) {
|
|
1218
|
+
const reconciliation = await this.prisma.bank_reconciliation.findUnique({
|
|
1219
|
+
where: { id },
|
|
1220
|
+
select: {
|
|
1221
|
+
id: true,
|
|
1222
|
+
settlement_id: true,
|
|
1223
|
+
bank_statement_line_id: true,
|
|
1224
|
+
},
|
|
1225
|
+
});
|
|
1226
|
+
|
|
1227
|
+
if (!reconciliation) {
|
|
1228
|
+
throw new NotFoundException('Conciliação bancária não encontrada');
|
|
1229
|
+
}
|
|
1230
|
+
|
|
1231
|
+
await this.prisma.$transaction(async (tx) => {
|
|
1232
|
+
await tx.bank_reconciliation.delete({
|
|
1233
|
+
where: { id: reconciliation.id },
|
|
1234
|
+
});
|
|
1235
|
+
|
|
1236
|
+
await tx.bank_statement_line.updateMany({
|
|
1237
|
+
where: {
|
|
1238
|
+
id: reconciliation.bank_statement_line_id,
|
|
1239
|
+
status: {
|
|
1240
|
+
in: ['reconciled', 'adjusted'],
|
|
1241
|
+
},
|
|
1242
|
+
},
|
|
1243
|
+
data: {
|
|
1244
|
+
status: 'pending',
|
|
1245
|
+
},
|
|
1246
|
+
});
|
|
1247
|
+
|
|
1248
|
+
await this.createAuditLog(tx, {
|
|
1249
|
+
action: 'UNRECONCILE_SETTLEMENT',
|
|
1250
|
+
entityTable: 'bank_reconciliation',
|
|
1251
|
+
entityId: String(reconciliation.id),
|
|
1252
|
+
actorUserId: userId,
|
|
1253
|
+
summary: `Unreconciled settlement ${reconciliation.settlement_id}`,
|
|
1254
|
+
});
|
|
1255
|
+
});
|
|
1256
|
+
|
|
1257
|
+
return { success: true };
|
|
1258
|
+
}
|
|
1259
|
+
|
|
1070
1260
|
async createTag(data: CreateFinanceTagDto) {
|
|
1071
1261
|
const slug = this.normalizeTagSlug(data.name);
|
|
1072
1262
|
|
|
@@ -2213,6 +2403,7 @@ export class FinanceService {
|
|
|
2213
2403
|
status?: string,
|
|
2214
2404
|
) {
|
|
2215
2405
|
const prismaStatus = this.mapStatusFromPt(status);
|
|
2406
|
+
const search = paginationParams?.search?.trim();
|
|
2216
2407
|
const where: any = {
|
|
2217
2408
|
title_type: titleType,
|
|
2218
2409
|
};
|
|
@@ -2221,15 +2412,45 @@ export class FinanceService {
|
|
|
2221
2412
|
where.status = prismaStatus;
|
|
2222
2413
|
}
|
|
2223
2414
|
|
|
2224
|
-
|
|
2415
|
+
if (search) {
|
|
2416
|
+
where.OR = [
|
|
2417
|
+
{
|
|
2418
|
+
document_number: {
|
|
2419
|
+
contains: search,
|
|
2420
|
+
mode: 'insensitive',
|
|
2421
|
+
},
|
|
2422
|
+
},
|
|
2423
|
+
{
|
|
2424
|
+
person: {
|
|
2425
|
+
name: {
|
|
2426
|
+
contains: search,
|
|
2427
|
+
mode: 'insensitive',
|
|
2428
|
+
},
|
|
2429
|
+
},
|
|
2430
|
+
},
|
|
2431
|
+
];
|
|
2432
|
+
}
|
|
2433
|
+
|
|
2434
|
+
const normalizedPaginationParams: PaginationDTO = {
|
|
2435
|
+
...paginationParams,
|
|
2436
|
+
sortField: paginationParams?.sortField || 'created_at',
|
|
2437
|
+
sortOrder: paginationParams?.sortOrder || PageOrderDirection.Desc,
|
|
2438
|
+
};
|
|
2439
|
+
|
|
2440
|
+
const paginated = await this.paginationService.paginate(
|
|
2225
2441
|
this.prisma.financial_title,
|
|
2226
|
-
|
|
2442
|
+
normalizedPaginationParams,
|
|
2227
2443
|
{
|
|
2228
2444
|
where,
|
|
2229
2445
|
include: this.defaultTitleInclude(),
|
|
2230
2446
|
orderBy: { created_at: 'desc' },
|
|
2231
2447
|
},
|
|
2232
2448
|
);
|
|
2449
|
+
|
|
2450
|
+
return {
|
|
2451
|
+
...paginated,
|
|
2452
|
+
data: (paginated.data || []).map((title) => this.mapTitleToFront(title)),
|
|
2453
|
+
};
|
|
2233
2454
|
}
|
|
2234
2455
|
|
|
2235
2456
|
private async getTitleById(id: number, titleType: TitleType, locale: string) {
|
|
@@ -2914,21 +3135,20 @@ export class FinanceService {
|
|
|
2914
3135
|
throw new BadRequestException('Title cannot be canceled in current status');
|
|
2915
3136
|
}
|
|
2916
3137
|
|
|
2917
|
-
const
|
|
2918
|
-
|
|
2919
|
-
|
|
2920
|
-
|
|
2921
|
-
|
|
2922
|
-
settlement
|
|
2923
|
-
|
|
2924
|
-
|
|
2925
|
-
|
|
2926
|
-
|
|
2927
|
-
|
|
2928
|
-
|
|
2929
|
-
|
|
2930
|
-
|
|
2931
|
-
});
|
|
3138
|
+
const activeSettlements = await tx.$queryRaw<Array<{ has_active: boolean }>>`
|
|
3139
|
+
SELECT EXISTS (
|
|
3140
|
+
SELECT 1
|
|
3141
|
+
FROM settlement_allocation sa
|
|
3142
|
+
INNER JOIN financial_installment fi ON fi.id = sa.installment_id
|
|
3143
|
+
INNER JOIN settlement s ON s.id = sa.settlement_id
|
|
3144
|
+
LEFT JOIN settlement r ON r.reverses_settlement_id = s.id
|
|
3145
|
+
WHERE fi.title_id = ${title.id}
|
|
3146
|
+
AND COALESCE(s.entry_type::text, 'normal') = 'normal'
|
|
3147
|
+
AND r.id IS NULL
|
|
3148
|
+
) AS has_active
|
|
3149
|
+
`;
|
|
3150
|
+
|
|
3151
|
+
const hasActiveSettlements = activeSettlements[0]?.has_active;
|
|
2932
3152
|
|
|
2933
3153
|
if (hasActiveSettlements) {
|
|
2934
3154
|
throw new ConflictException(
|
|
@@ -3101,9 +3321,18 @@ export class FinanceService {
|
|
|
3101
3321
|
|
|
3102
3322
|
await tx.settlement_allocation.create({
|
|
3103
3323
|
data: {
|
|
3104
|
-
|
|
3105
|
-
|
|
3324
|
+
settlement: {
|
|
3325
|
+
connect: {
|
|
3326
|
+
id: settlement.id,
|
|
3327
|
+
},
|
|
3328
|
+
},
|
|
3329
|
+
financial_installment: {
|
|
3330
|
+
connect: {
|
|
3331
|
+
id: installment.id,
|
|
3332
|
+
},
|
|
3333
|
+
},
|
|
3106
3334
|
allocated_amount_cents: amountCents,
|
|
3335
|
+
amount_cents: amountCents,
|
|
3107
3336
|
discount_cents: this.toCents(data.discount || 0),
|
|
3108
3337
|
interest_cents: this.toCents(data.interest || 0),
|
|
3109
3338
|
penalty_cents: this.toCents(data.penalty || 0),
|
|
@@ -3227,97 +3456,251 @@ export class FinanceService {
|
|
|
3227
3456
|
locale: string,
|
|
3228
3457
|
userId?: number,
|
|
3229
3458
|
) {
|
|
3230
|
-
const updatedTitle = await this.
|
|
3231
|
-
|
|
3232
|
-
|
|
3233
|
-
|
|
3234
|
-
|
|
3235
|
-
|
|
3236
|
-
|
|
3237
|
-
|
|
3238
|
-
|
|
3239
|
-
|
|
3240
|
-
},
|
|
3241
|
-
});
|
|
3459
|
+
const updatedTitle = await this.reverseSettlementInternal(
|
|
3460
|
+
settlementId,
|
|
3461
|
+
data,
|
|
3462
|
+
locale,
|
|
3463
|
+
userId,
|
|
3464
|
+
{
|
|
3465
|
+
titleId,
|
|
3466
|
+
titleType,
|
|
3467
|
+
},
|
|
3468
|
+
);
|
|
3242
3469
|
|
|
3243
|
-
|
|
3244
|
-
|
|
3245
|
-
|
|
3246
|
-
|
|
3247
|
-
|
|
3248
|
-
|
|
3249
|
-
|
|
3250
|
-
|
|
3470
|
+
if (!updatedTitle) {
|
|
3471
|
+
throw new NotFoundException('Financial title not found');
|
|
3472
|
+
}
|
|
3473
|
+
|
|
3474
|
+
return this.mapTitleToFront(updatedTitle);
|
|
3475
|
+
}
|
|
3476
|
+
|
|
3477
|
+
private async reverseSettlementInternal(
|
|
3478
|
+
settlementId: number,
|
|
3479
|
+
data: ReverseSettlementDto,
|
|
3480
|
+
locale: string,
|
|
3481
|
+
userId?: number,
|
|
3482
|
+
scope?: {
|
|
3483
|
+
titleId?: number;
|
|
3484
|
+
titleType?: TitleType;
|
|
3485
|
+
},
|
|
3486
|
+
) {
|
|
3487
|
+
const { title } = await this.prisma.$transaction(async (tx) => {
|
|
3488
|
+
const settlementRows = await tx.$queryRaw<
|
|
3489
|
+
Array<{
|
|
3490
|
+
id: number;
|
|
3491
|
+
settlement_type: string;
|
|
3492
|
+
settled_at: Date;
|
|
3493
|
+
amount_cents: number;
|
|
3494
|
+
description: string | null;
|
|
3495
|
+
person_id: number | null;
|
|
3496
|
+
bank_account_id: number | null;
|
|
3497
|
+
payment_method_id: number | null;
|
|
3498
|
+
created_by_user_id: number | null;
|
|
3499
|
+
entry_type: string | null;
|
|
3500
|
+
title_id: number;
|
|
3501
|
+
title_type: string;
|
|
3502
|
+
title_status: string;
|
|
3503
|
+
title_competence_date: Date | null;
|
|
3504
|
+
}>
|
|
3505
|
+
>`
|
|
3506
|
+
SELECT
|
|
3507
|
+
s.id,
|
|
3508
|
+
s.settlement_type::text,
|
|
3509
|
+
s.settled_at,
|
|
3510
|
+
s.amount_cents,
|
|
3511
|
+
s.description,
|
|
3512
|
+
s.person_id,
|
|
3513
|
+
s.bank_account_id,
|
|
3514
|
+
s.payment_method_id,
|
|
3515
|
+
s.created_by_user_id,
|
|
3516
|
+
COALESCE(s.entry_type::text, 'normal') AS entry_type,
|
|
3517
|
+
ft.id AS title_id,
|
|
3518
|
+
ft.title_type::text AS title_type,
|
|
3519
|
+
ft.status::text AS title_status,
|
|
3520
|
+
ft.competence_date AS title_competence_date
|
|
3521
|
+
FROM settlement s
|
|
3522
|
+
INNER JOIN settlement_allocation sa ON sa.settlement_id = s.id
|
|
3523
|
+
INNER JOIN financial_installment fi ON fi.id = sa.installment_id
|
|
3524
|
+
INNER JOIN financial_title ft ON ft.id = fi.title_id
|
|
3525
|
+
WHERE s.id = ${settlementId}
|
|
3526
|
+
LIMIT 1
|
|
3527
|
+
FOR UPDATE OF s
|
|
3528
|
+
`;
|
|
3529
|
+
|
|
3530
|
+
const settlement = settlementRows[0];
|
|
3531
|
+
|
|
3532
|
+
if (!settlement) {
|
|
3533
|
+
throw new NotFoundException('Settlement not found');
|
|
3534
|
+
}
|
|
3535
|
+
|
|
3536
|
+
if (scope?.titleId && settlement.title_id !== scope.titleId) {
|
|
3537
|
+
throw new NotFoundException('Settlement not found for this title');
|
|
3538
|
+
}
|
|
3539
|
+
|
|
3540
|
+
if (scope?.titleType && settlement.title_type !== scope.titleType) {
|
|
3541
|
+
throw new NotFoundException('Settlement not found for this title type');
|
|
3542
|
+
}
|
|
3543
|
+
|
|
3544
|
+
if (settlement.entry_type !== 'normal') {
|
|
3545
|
+
throw new BadRequestException('Somente liquidações normais podem ser estornadas');
|
|
3251
3546
|
}
|
|
3252
3547
|
|
|
3253
3548
|
await this.assertDateNotInClosedPeriod(
|
|
3254
3549
|
tx,
|
|
3255
|
-
|
|
3550
|
+
settlement.title_competence_date,
|
|
3256
3551
|
'reverse settlement',
|
|
3257
3552
|
);
|
|
3258
3553
|
|
|
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
|
-
});
|
|
3554
|
+
const alreadyReversed = await tx.$queryRaw<Array<{ id: number }>>`
|
|
3555
|
+
SELECT id
|
|
3556
|
+
FROM settlement
|
|
3557
|
+
WHERE reverses_settlement_id = ${settlement.id}
|
|
3558
|
+
LIMIT 1
|
|
3559
|
+
`;
|
|
3287
3560
|
|
|
3288
|
-
if (
|
|
3289
|
-
throw new
|
|
3561
|
+
if (alreadyReversed.length > 0) {
|
|
3562
|
+
throw new ConflictException('Liquidação já estornada.');
|
|
3290
3563
|
}
|
|
3291
3564
|
|
|
3292
|
-
|
|
3293
|
-
|
|
3565
|
+
const isReconciled = await tx.$queryRaw<Array<{ id: number }>>`
|
|
3566
|
+
SELECT id
|
|
3567
|
+
FROM bank_reconciliation
|
|
3568
|
+
WHERE settlement_id = ${settlement.id}
|
|
3569
|
+
AND status = 'reconciled'
|
|
3570
|
+
LIMIT 1
|
|
3571
|
+
`;
|
|
3572
|
+
|
|
3573
|
+
if (isReconciled.length > 0) {
|
|
3574
|
+
throw new ConflictException('Desconciliar primeiro');
|
|
3294
3575
|
}
|
|
3295
3576
|
|
|
3296
|
-
|
|
3297
|
-
|
|
3577
|
+
const allocations = await tx.$queryRaw<
|
|
3578
|
+
Array<{
|
|
3579
|
+
id: number;
|
|
3580
|
+
installment_id: number;
|
|
3581
|
+
allocated_amount_cents: number;
|
|
3582
|
+
amount_cents: number | null;
|
|
3583
|
+
discount_cents: number;
|
|
3584
|
+
interest_cents: number;
|
|
3585
|
+
penalty_cents: number;
|
|
3586
|
+
installment_amount_cents: number;
|
|
3587
|
+
installment_open_amount_cents: number;
|
|
3588
|
+
installment_due_date: Date;
|
|
3589
|
+
installment_status: string;
|
|
3590
|
+
}>
|
|
3591
|
+
>`
|
|
3592
|
+
SELECT
|
|
3593
|
+
sa.id,
|
|
3594
|
+
sa.installment_id,
|
|
3595
|
+
sa.allocated_amount_cents,
|
|
3596
|
+
sa.amount_cents,
|
|
3597
|
+
sa.discount_cents,
|
|
3598
|
+
sa.interest_cents,
|
|
3599
|
+
sa.penalty_cents,
|
|
3600
|
+
fi.amount_cents AS installment_amount_cents,
|
|
3601
|
+
fi.open_amount_cents AS installment_open_amount_cents,
|
|
3602
|
+
fi.due_date AS installment_due_date,
|
|
3603
|
+
fi.status::text AS installment_status
|
|
3604
|
+
FROM settlement_allocation sa
|
|
3605
|
+
INNER JOIN financial_installment fi ON fi.id = sa.installment_id
|
|
3606
|
+
WHERE sa.settlement_id = ${settlement.id}
|
|
3607
|
+
FOR UPDATE OF fi
|
|
3608
|
+
`;
|
|
3609
|
+
|
|
3610
|
+
if (allocations.length === 0) {
|
|
3611
|
+
throw new BadRequestException('Settlement has no allocations to reverse');
|
|
3612
|
+
}
|
|
3298
3613
|
|
|
3299
|
-
|
|
3300
|
-
|
|
3301
|
-
|
|
3614
|
+
const reversalMemo = data.reason?.trim() || data.memo?.trim() || 'Estorno';
|
|
3615
|
+
const reversalAmountCents = -Math.abs(Number(settlement.amount_cents || 0));
|
|
3616
|
+
|
|
3617
|
+
const reversalResult = await tx.$queryRaw<Array<{ id: number }>>`
|
|
3618
|
+
INSERT INTO settlement (
|
|
3619
|
+
person_id,
|
|
3620
|
+
bank_account_id,
|
|
3621
|
+
payment_method_id,
|
|
3622
|
+
settlement_type,
|
|
3623
|
+
entry_type,
|
|
3624
|
+
status,
|
|
3625
|
+
settled_at,
|
|
3626
|
+
amount_cents,
|
|
3627
|
+
description,
|
|
3628
|
+
external_reference,
|
|
3629
|
+
created_by_user_id,
|
|
3630
|
+
reverses_settlement_id,
|
|
3631
|
+
created_at,
|
|
3632
|
+
updated_at
|
|
3633
|
+
)
|
|
3634
|
+
VALUES (
|
|
3635
|
+
${settlement.person_id},
|
|
3636
|
+
${settlement.bank_account_id},
|
|
3637
|
+
${settlement.payment_method_id},
|
|
3638
|
+
${settlement.settlement_type}::settlement_settlement_type_enum,
|
|
3639
|
+
'reversal'::settlement_entry_type_enum,
|
|
3640
|
+
'confirmed'::settlement_status_enum,
|
|
3641
|
+
NOW(),
|
|
3642
|
+
${reversalAmountCents},
|
|
3643
|
+
${reversalMemo},
|
|
3644
|
+
NULL,
|
|
3645
|
+
${userId || settlement.created_by_user_id || null},
|
|
3646
|
+
${settlement.id},
|
|
3647
|
+
NOW(),
|
|
3648
|
+
NOW()
|
|
3649
|
+
)
|
|
3650
|
+
RETURNING id
|
|
3651
|
+
`;
|
|
3652
|
+
|
|
3653
|
+
const reversalId = reversalResult[0]?.id;
|
|
3654
|
+
|
|
3655
|
+
if (!reversalId) {
|
|
3656
|
+
throw new BadRequestException('Could not create reversal settlement');
|
|
3657
|
+
}
|
|
3658
|
+
|
|
3659
|
+
for (const allocation of allocations) {
|
|
3660
|
+
const originalAmount = Number(
|
|
3661
|
+
allocation.amount_cents ?? allocation.allocated_amount_cents ?? 0,
|
|
3662
|
+
);
|
|
3663
|
+
|
|
3664
|
+
await tx.settlement_allocation.create({
|
|
3665
|
+
data: {
|
|
3666
|
+
settlement: {
|
|
3667
|
+
connect: {
|
|
3668
|
+
id: reversalId,
|
|
3669
|
+
},
|
|
3670
|
+
},
|
|
3671
|
+
financial_installment: {
|
|
3672
|
+
connect: {
|
|
3673
|
+
id: allocation.installment_id,
|
|
3674
|
+
},
|
|
3675
|
+
},
|
|
3676
|
+
allocated_amount_cents: -Math.abs(originalAmount),
|
|
3677
|
+
amount_cents: -Math.abs(originalAmount),
|
|
3678
|
+
discount_cents: -Math.abs(allocation.discount_cents || 0),
|
|
3679
|
+
interest_cents: -Math.abs(allocation.interest_cents || 0),
|
|
3680
|
+
penalty_cents: -Math.abs(allocation.penalty_cents || 0),
|
|
3681
|
+
},
|
|
3682
|
+
});
|
|
3302
3683
|
|
|
3303
3684
|
const nextOpenAmountCents =
|
|
3304
|
-
|
|
3685
|
+
Number(allocation.installment_open_amount_cents || 0) +
|
|
3686
|
+
Math.abs(originalAmount);
|
|
3305
3687
|
|
|
3306
|
-
if (nextOpenAmountCents >
|
|
3307
|
-
throw new
|
|
3308
|
-
`
|
|
3688
|
+
if (nextOpenAmountCents > Number(allocation.installment_amount_cents || 0)) {
|
|
3689
|
+
throw new ConflictException(
|
|
3690
|
+
`Estorno excederia o valor original da parcela ${allocation.installment_id}`,
|
|
3309
3691
|
);
|
|
3310
3692
|
}
|
|
3311
3693
|
|
|
3312
3694
|
const nextInstallmentStatus = this.resolveInstallmentStatus(
|
|
3313
|
-
|
|
3695
|
+
Number(allocation.installment_amount_cents || 0),
|
|
3314
3696
|
nextOpenAmountCents,
|
|
3315
|
-
|
|
3697
|
+
new Date(allocation.installment_due_date),
|
|
3698
|
+
allocation.installment_status,
|
|
3316
3699
|
);
|
|
3317
3700
|
|
|
3318
3701
|
await tx.financial_installment.update({
|
|
3319
3702
|
where: {
|
|
3320
|
-
id:
|
|
3703
|
+
id: allocation.installment_id,
|
|
3321
3704
|
},
|
|
3322
3705
|
data: {
|
|
3323
3706
|
open_amount_cents: nextOpenAmountCents,
|
|
@@ -3326,76 +3709,47 @@ export class FinanceService {
|
|
|
3326
3709
|
});
|
|
3327
3710
|
}
|
|
3328
3711
|
|
|
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);
|
|
3712
|
+
const nextTitleStatus = await this.recalculateTitleStatus(
|
|
3713
|
+
tx,
|
|
3714
|
+
settlement.title_id,
|
|
3715
|
+
);
|
|
3368
3716
|
|
|
3369
3717
|
await this.createAuditLog(tx, {
|
|
3370
3718
|
action: 'REVERSE_SETTLEMENT',
|
|
3371
3719
|
entityTable: 'financial_title',
|
|
3372
|
-
entityId: String(
|
|
3720
|
+
entityId: String(settlement.title_id),
|
|
3373
3721
|
actorUserId: userId,
|
|
3374
|
-
summary: `
|
|
3722
|
+
summary: `Created reversal ${reversalId} for settlement ${settlement.id}`,
|
|
3375
3723
|
beforeData: JSON.stringify({
|
|
3376
|
-
title_status:
|
|
3377
|
-
|
|
3724
|
+
title_status: settlement.title_status,
|
|
3725
|
+
settlement_id: settlement.id,
|
|
3726
|
+
settlement_entry_type: settlement.entry_type,
|
|
3378
3727
|
}),
|
|
3379
3728
|
afterData: JSON.stringify({
|
|
3380
3729
|
title_status: nextTitleStatus,
|
|
3381
|
-
|
|
3730
|
+
settlement_id: settlement.id,
|
|
3731
|
+
reversal_settlement_id: reversalId,
|
|
3382
3732
|
}),
|
|
3383
3733
|
});
|
|
3384
3734
|
|
|
3385
|
-
|
|
3735
|
+
const updatedTitle = await tx.financial_title.findFirst({
|
|
3386
3736
|
where: {
|
|
3387
|
-
id:
|
|
3388
|
-
title_type:
|
|
3737
|
+
id: settlement.title_id,
|
|
3738
|
+
title_type: settlement.title_type as TitleType,
|
|
3389
3739
|
},
|
|
3390
3740
|
include: this.defaultTitleInclude(),
|
|
3391
3741
|
});
|
|
3392
|
-
});
|
|
3393
3742
|
|
|
3394
|
-
|
|
3395
|
-
|
|
3396
|
-
|
|
3743
|
+
if (!updatedTitle) {
|
|
3744
|
+
throw new NotFoundException('Financial title not found');
|
|
3745
|
+
}
|
|
3397
3746
|
|
|
3398
|
-
|
|
3747
|
+
return {
|
|
3748
|
+
title: updatedTitle,
|
|
3749
|
+
};
|
|
3750
|
+
});
|
|
3751
|
+
|
|
3752
|
+
return title;
|
|
3399
3753
|
}
|
|
3400
3754
|
|
|
3401
3755
|
private async updateTitleTags(
|
|
@@ -4213,7 +4567,28 @@ export class FinanceService {
|
|
|
4213
4567
|
return Math.round(value * 100);
|
|
4214
4568
|
}
|
|
4215
4569
|
|
|
4216
|
-
private fromCents(value: number) {
|
|
4217
|
-
|
|
4570
|
+
private fromCents(value: number | bigint | string | null | undefined) {
|
|
4571
|
+
if (value === null || value === undefined) {
|
|
4572
|
+
return 0;
|
|
4573
|
+
}
|
|
4574
|
+
|
|
4575
|
+
if (typeof value === 'bigint') {
|
|
4576
|
+
const isNegative = value < BigInt(0);
|
|
4577
|
+
const absoluteValue = isNegative ? -value : value;
|
|
4578
|
+
const whole = absoluteValue / BigInt(100);
|
|
4579
|
+
const cents = absoluteValue % BigInt(100);
|
|
4580
|
+
const composedValue = Number(whole) + Number(cents) / 100;
|
|
4581
|
+
|
|
4582
|
+
return Number((isNegative ? -composedValue : composedValue).toFixed(2));
|
|
4583
|
+
}
|
|
4584
|
+
|
|
4585
|
+
const numericValue =
|
|
4586
|
+
typeof value === 'string' ? Number(value) : Number(value || 0);
|
|
4587
|
+
|
|
4588
|
+
if (!Number.isFinite(numericValue)) {
|
|
4589
|
+
return 0;
|
|
4590
|
+
}
|
|
4591
|
+
|
|
4592
|
+
return Number((numericValue / 100).toFixed(2));
|
|
4218
4593
|
}
|
|
4219
4594
|
}
|