@hed-hog/finance 0.0.251 → 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 +39 -0
- package/dist/finance.service.d.ts.map +1 -1
- package/dist/finance.service.js +476 -219
- 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 +457 -142
- package/hedhog/frontend/app/accounts-payable/installments/page.tsx.ejs +100 -5
- package/hedhog/frontend/app/accounts-receivable/installments/[id]/page.tsx.ejs +19 -20
- package/hedhog/frontend/app/page.tsx.ejs +11 -4
- package/hedhog/frontend/messages/en.json +44 -0
- package/hedhog/frontend/messages/pt.json +44 -0
- package/hedhog/query/constraints.sql +3 -1
- 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 +2 -2
- package/src/dto/reverse-settlement.dto.ts +4 -0
- package/src/finance-installments.controller.ts +45 -12
- package/src/finance.service.ts +498 -114
package/dist/finance.service.js
CHANGED
|
@@ -591,7 +591,7 @@ let FinanceService = FinanceService_1 = class FinanceService {
|
|
|
591
591
|
const today = this.startOfDay(new Date());
|
|
592
592
|
const day7 = this.addDays(today, 7);
|
|
593
593
|
const day30 = this.addDays(today, 30);
|
|
594
|
-
const payableInstallments = this.extractOpenInstallments(payables);
|
|
594
|
+
const payableInstallments = this.extractOpenInstallments((payables || []).filter((title) => this.isPayableTitleApproved(title)));
|
|
595
595
|
const receivableInstallments = this.extractOpenInstallments(receivables);
|
|
596
596
|
const saldoCaixa = (bankAccounts || [])
|
|
597
597
|
.filter((account) => (account === null || account === void 0 ? void 0 : account.ativo) !== false)
|
|
@@ -617,6 +617,10 @@ let FinanceService = FinanceService_1 = class FinanceService {
|
|
|
617
617
|
(installment === null || installment === void 0 ? void 0 : installment.status) === 'vencido' ||
|
|
618
618
|
(installment === null || installment === void 0 ? void 0 : installment.status) === 'parcial'));
|
|
619
619
|
}
|
|
620
|
+
isPayableTitleApproved(title) {
|
|
621
|
+
const status = String((title === null || title === void 0 ? void 0 : title.status) || '').toLowerCase();
|
|
622
|
+
return status !== 'rascunho' && status !== 'cancelado';
|
|
623
|
+
}
|
|
620
624
|
sumInstallmentsDueBetween(installments, startDate, endDate) {
|
|
621
625
|
return (installments || [])
|
|
622
626
|
.filter((installment) => {
|
|
@@ -686,6 +690,138 @@ let FinanceService = FinanceService_1 = class FinanceService {
|
|
|
686
690
|
async reverseAccountsReceivableSettlement(id, settlementId, data, locale, userId) {
|
|
687
691
|
return this.reverseTitleSettlement(id, settlementId, data, 'receivable', locale, userId);
|
|
688
692
|
}
|
|
693
|
+
async getTitleSettlementsHistory(titleId, locale) {
|
|
694
|
+
var _a, _b, _c, _d, _e, _f, _g, _h;
|
|
695
|
+
const title = await this.prisma.financial_title.findUnique({
|
|
696
|
+
where: { id: titleId },
|
|
697
|
+
select: { id: true },
|
|
698
|
+
});
|
|
699
|
+
if (!title) {
|
|
700
|
+
throw new common_1.NotFoundException((0, api_locale_1.getLocaleText)('itemNotFound', locale, `Financial title with ID ${titleId} not found`).replace('{{item}}', 'Financial title'));
|
|
701
|
+
}
|
|
702
|
+
const rows = await this.prisma.$queryRaw `
|
|
703
|
+
SELECT
|
|
704
|
+
s.id AS normal_id,
|
|
705
|
+
s.settled_at AS normal_paid_at,
|
|
706
|
+
s.amount_cents AS normal_amount_cents,
|
|
707
|
+
pm.type::text AS normal_method,
|
|
708
|
+
s.bank_account_id AS normal_account_id,
|
|
709
|
+
ba.name AS normal_account_name,
|
|
710
|
+
s.created_at AS normal_created_at,
|
|
711
|
+
u.name AS normal_created_by,
|
|
712
|
+
s.description AS normal_memo,
|
|
713
|
+
br.id AS reconciliation_id,
|
|
714
|
+
br.status::text AS reconciliation_status,
|
|
715
|
+
fi.id AS installment_id,
|
|
716
|
+
fi.installment_number AS installment_seq,
|
|
717
|
+
COALESCE(sa.amount_cents, sa.allocated_amount_cents) AS allocation_amount_cents,
|
|
718
|
+
r.id AS reversal_id,
|
|
719
|
+
r.settled_at AS reversal_paid_at,
|
|
720
|
+
r.amount_cents AS reversal_amount_cents,
|
|
721
|
+
r.created_at AS reversal_created_at,
|
|
722
|
+
ur.name AS reversal_created_by,
|
|
723
|
+
r.description AS reversal_memo
|
|
724
|
+
FROM settlement s
|
|
725
|
+
INNER JOIN settlement_allocation sa ON sa.settlement_id = s.id
|
|
726
|
+
INNER JOIN financial_installment fi ON fi.id = sa.installment_id
|
|
727
|
+
LEFT JOIN payment_method pm ON pm.id = s.payment_method_id
|
|
728
|
+
LEFT JOIN bank_account ba ON ba.id = s.bank_account_id
|
|
729
|
+
LEFT JOIN "user" u ON u.id = s.created_by_user_id
|
|
730
|
+
LEFT JOIN bank_reconciliation br ON br.settlement_id = s.id
|
|
731
|
+
LEFT JOIN settlement r ON r.reverses_settlement_id = s.id
|
|
732
|
+
LEFT JOIN "user" ur ON ur.id = r.created_by_user_id
|
|
733
|
+
WHERE fi.title_id = ${titleId}
|
|
734
|
+
AND COALESCE(s.entry_type::text, 'normal') = 'normal'
|
|
735
|
+
ORDER BY s.settled_at DESC, s.id DESC, fi.installment_number ASC
|
|
736
|
+
`;
|
|
737
|
+
const groups = new Map();
|
|
738
|
+
for (const row of rows) {
|
|
739
|
+
const key = String(row.normal_id);
|
|
740
|
+
const existing = groups.get(key);
|
|
741
|
+
if (!existing) {
|
|
742
|
+
groups.set(key, {
|
|
743
|
+
normal: {
|
|
744
|
+
id: key,
|
|
745
|
+
paidAt: ((_b = (_a = row.normal_paid_at) === null || _a === void 0 ? void 0 : _a.toISOString) === null || _b === void 0 ? void 0 : _b.call(_a)) || null,
|
|
746
|
+
amountCents: Number(row.normal_amount_cents || 0),
|
|
747
|
+
type: 'NORMAL',
|
|
748
|
+
method: this.mapPaymentMethodToPt(row.normal_method) || row.normal_method,
|
|
749
|
+
account: row.normal_account_name || null,
|
|
750
|
+
accountId: row.normal_account_id
|
|
751
|
+
? String(row.normal_account_id)
|
|
752
|
+
: null,
|
|
753
|
+
createdAt: ((_d = (_c = row.normal_created_at) === null || _c === void 0 ? void 0 : _c.toISOString) === null || _d === void 0 ? void 0 : _d.call(_c)) || null,
|
|
754
|
+
createdBy: row.normal_created_by || null,
|
|
755
|
+
memo: row.normal_memo || null,
|
|
756
|
+
reconciled: row.reconciliation_status === 'reconciled',
|
|
757
|
+
reconciliationId: row.reconciliation_id
|
|
758
|
+
? String(row.reconciliation_id)
|
|
759
|
+
: null,
|
|
760
|
+
},
|
|
761
|
+
reversal: row.reversal_id
|
|
762
|
+
? {
|
|
763
|
+
id: String(row.reversal_id),
|
|
764
|
+
paidAt: ((_f = (_e = row.reversal_paid_at) === null || _e === void 0 ? void 0 : _e.toISOString) === null || _f === void 0 ? void 0 : _f.call(_e)) || null,
|
|
765
|
+
amountCents: Number(row.reversal_amount_cents || 0),
|
|
766
|
+
type: 'REVERSAL',
|
|
767
|
+
createdAt: ((_h = (_g = row.reversal_created_at) === null || _g === void 0 ? void 0 : _g.toISOString) === null || _h === void 0 ? void 0 : _h.call(_g)) || null,
|
|
768
|
+
createdBy: row.reversal_created_by || null,
|
|
769
|
+
memo: row.reversal_memo || null,
|
|
770
|
+
}
|
|
771
|
+
: null,
|
|
772
|
+
allocations: [],
|
|
773
|
+
statusLabel: row.reversal_id ? 'ESTORNADO' : 'ATIVO',
|
|
774
|
+
});
|
|
775
|
+
}
|
|
776
|
+
groups.get(key).allocations.push({
|
|
777
|
+
installmentId: String(row.installment_id),
|
|
778
|
+
installmentSeq: Number(row.installment_seq || 0),
|
|
779
|
+
amountCents: Number(row.allocation_amount_cents || 0),
|
|
780
|
+
});
|
|
781
|
+
}
|
|
782
|
+
return Array.from(groups.values());
|
|
783
|
+
}
|
|
784
|
+
async reverseSettlementById(settlementId, data, locale, userId) {
|
|
785
|
+
const updatedTitle = await this.reverseSettlementInternal(settlementId, data, locale, userId);
|
|
786
|
+
return this.mapTitleToFront(updatedTitle);
|
|
787
|
+
}
|
|
788
|
+
async unreconcileBankReconciliation(id, userId) {
|
|
789
|
+
const reconciliation = await this.prisma.bank_reconciliation.findUnique({
|
|
790
|
+
where: { id },
|
|
791
|
+
select: {
|
|
792
|
+
id: true,
|
|
793
|
+
settlement_id: true,
|
|
794
|
+
bank_statement_line_id: true,
|
|
795
|
+
},
|
|
796
|
+
});
|
|
797
|
+
if (!reconciliation) {
|
|
798
|
+
throw new common_1.NotFoundException('Conciliação bancária não encontrada');
|
|
799
|
+
}
|
|
800
|
+
await this.prisma.$transaction(async (tx) => {
|
|
801
|
+
await tx.bank_reconciliation.delete({
|
|
802
|
+
where: { id: reconciliation.id },
|
|
803
|
+
});
|
|
804
|
+
await tx.bank_statement_line.updateMany({
|
|
805
|
+
where: {
|
|
806
|
+
id: reconciliation.bank_statement_line_id,
|
|
807
|
+
status: {
|
|
808
|
+
in: ['reconciled', 'adjusted'],
|
|
809
|
+
},
|
|
810
|
+
},
|
|
811
|
+
data: {
|
|
812
|
+
status: 'pending',
|
|
813
|
+
},
|
|
814
|
+
});
|
|
815
|
+
await this.createAuditLog(tx, {
|
|
816
|
+
action: 'UNRECONCILE_SETTLEMENT',
|
|
817
|
+
entityTable: 'bank_reconciliation',
|
|
818
|
+
entityId: String(reconciliation.id),
|
|
819
|
+
actorUserId: userId,
|
|
820
|
+
summary: `Unreconciled settlement ${reconciliation.settlement_id}`,
|
|
821
|
+
});
|
|
822
|
+
});
|
|
823
|
+
return { success: true };
|
|
824
|
+
}
|
|
689
825
|
async createTag(data) {
|
|
690
826
|
const slug = this.normalizeTagSlug(data.name);
|
|
691
827
|
if (!slug) {
|
|
@@ -2018,6 +2154,7 @@ let FinanceService = FinanceService_1 = class FinanceService {
|
|
|
2018
2154
|
}
|
|
2019
2155
|
async cancelTitle(titleId, data, titleType, locale, userId) {
|
|
2020
2156
|
const updatedTitle = await this.prisma.$transaction(async (tx) => {
|
|
2157
|
+
var _a;
|
|
2021
2158
|
const title = await tx.financial_title.findFirst({
|
|
2022
2159
|
where: {
|
|
2023
2160
|
id: titleId,
|
|
@@ -2035,7 +2172,34 @@ let FinanceService = FinanceService_1 = class FinanceService {
|
|
|
2035
2172
|
if (title.status === 'settled' || title.status === 'canceled') {
|
|
2036
2173
|
throw new common_1.BadRequestException('Title cannot be canceled in current status');
|
|
2037
2174
|
}
|
|
2175
|
+
const activeSettlements = await tx.$queryRaw `
|
|
2176
|
+
SELECT EXISTS (
|
|
2177
|
+
SELECT 1
|
|
2178
|
+
FROM settlement_allocation sa
|
|
2179
|
+
INNER JOIN financial_installment fi ON fi.id = sa.installment_id
|
|
2180
|
+
INNER JOIN settlement s ON s.id = sa.settlement_id
|
|
2181
|
+
LEFT JOIN settlement r ON r.reverses_settlement_id = s.id
|
|
2182
|
+
WHERE fi.title_id = ${title.id}
|
|
2183
|
+
AND COALESCE(s.entry_type::text, 'normal') = 'normal'
|
|
2184
|
+
AND r.id IS NULL
|
|
2185
|
+
) AS has_active
|
|
2186
|
+
`;
|
|
2187
|
+
const hasActiveSettlements = (_a = activeSettlements[0]) === null || _a === void 0 ? void 0 : _a.has_active;
|
|
2188
|
+
if (hasActiveSettlements) {
|
|
2189
|
+
throw new common_1.ConflictException('Não é possível cancelar enquanto houver liquidações ativas. Estorne primeiro.');
|
|
2190
|
+
}
|
|
2038
2191
|
await this.assertDateNotInClosedPeriod(tx, title.competence_date, 'cancel title');
|
|
2192
|
+
await tx.financial_installment.updateMany({
|
|
2193
|
+
where: {
|
|
2194
|
+
title_id: title.id,
|
|
2195
|
+
status: {
|
|
2196
|
+
not: 'canceled',
|
|
2197
|
+
},
|
|
2198
|
+
},
|
|
2199
|
+
data: {
|
|
2200
|
+
status: 'canceled',
|
|
2201
|
+
},
|
|
2202
|
+
});
|
|
2039
2203
|
await tx.financial_title.update({
|
|
2040
2204
|
where: { id: title.id },
|
|
2041
2205
|
data: {
|
|
@@ -2076,215 +2240,318 @@ let FinanceService = FinanceService_1 = class FinanceService {
|
|
|
2076
2240
|
if (Number.isNaN(settledAt.getTime())) {
|
|
2077
2241
|
throw new common_1.BadRequestException('Invalid settlement date');
|
|
2078
2242
|
}
|
|
2079
|
-
|
|
2080
|
-
|
|
2081
|
-
|
|
2082
|
-
|
|
2083
|
-
|
|
2084
|
-
|
|
2085
|
-
|
|
2086
|
-
select: {
|
|
2087
|
-
id: true,
|
|
2088
|
-
person_id: true,
|
|
2089
|
-
status: true,
|
|
2090
|
-
competence_date: true,
|
|
2091
|
-
},
|
|
2092
|
-
});
|
|
2093
|
-
if (!title) {
|
|
2094
|
-
throw new common_1.NotFoundException((0, api_locale_1.getLocaleText)('itemNotFound', locale, `Financial title with ID ${titleId} not found`).replace('{{item}}', 'Financial title'));
|
|
2095
|
-
}
|
|
2096
|
-
if (!['open', 'partial'].includes(title.status)) {
|
|
2097
|
-
throw new common_1.BadRequestException('Only open/partial titles can be settled');
|
|
2098
|
-
}
|
|
2099
|
-
await this.assertDateNotInClosedPeriod(tx, title.competence_date, 'settle installment');
|
|
2100
|
-
const installment = await tx.financial_installment.findFirst({
|
|
2101
|
-
where: {
|
|
2102
|
-
id: data.installment_id,
|
|
2103
|
-
title_id: title.id,
|
|
2104
|
-
},
|
|
2105
|
-
select: {
|
|
2106
|
-
id: true,
|
|
2107
|
-
title_id: true,
|
|
2108
|
-
amount_cents: true,
|
|
2109
|
-
open_amount_cents: true,
|
|
2110
|
-
due_date: true,
|
|
2111
|
-
status: true,
|
|
2112
|
-
},
|
|
2113
|
-
});
|
|
2114
|
-
if (!installment) {
|
|
2115
|
-
throw new common_1.BadRequestException('Installment not found for this title');
|
|
2116
|
-
}
|
|
2117
|
-
if (installment.status === 'settled' || installment.status === 'canceled') {
|
|
2118
|
-
throw new common_1.BadRequestException('This installment cannot be settled');
|
|
2119
|
-
}
|
|
2120
|
-
if (amountCents > installment.open_amount_cents) {
|
|
2121
|
-
throw new common_1.BadRequestException('Settlement amount exceeds open amount');
|
|
2122
|
-
}
|
|
2123
|
-
const paymentMethodId = await this.resolvePaymentMethodId(tx, data.payment_channel);
|
|
2124
|
-
const settlement = await tx.settlement.create({
|
|
2125
|
-
data: {
|
|
2126
|
-
person_id: title.person_id,
|
|
2127
|
-
bank_account_id: data.bank_account_id || null,
|
|
2128
|
-
payment_method_id: paymentMethodId,
|
|
2129
|
-
settlement_type: titleType,
|
|
2130
|
-
status: 'confirmed',
|
|
2131
|
-
settled_at: settledAt,
|
|
2132
|
-
amount_cents: amountCents,
|
|
2133
|
-
description: ((_a = data.description) === null || _a === void 0 ? void 0 : _a.trim()) || null,
|
|
2134
|
-
created_by_user_id: userId,
|
|
2135
|
-
},
|
|
2136
|
-
});
|
|
2137
|
-
await tx.settlement_allocation.create({
|
|
2138
|
-
data: {
|
|
2139
|
-
settlement_id: settlement.id,
|
|
2140
|
-
installment_id: installment.id,
|
|
2141
|
-
allocated_amount_cents: amountCents,
|
|
2142
|
-
discount_cents: this.toCents(data.discount || 0),
|
|
2143
|
-
interest_cents: this.toCents(data.interest || 0),
|
|
2144
|
-
penalty_cents: this.toCents(data.penalty || 0),
|
|
2145
|
-
},
|
|
2146
|
-
});
|
|
2147
|
-
const decrementResult = await tx.financial_installment.updateMany({
|
|
2148
|
-
where: {
|
|
2149
|
-
id: installment.id,
|
|
2150
|
-
open_amount_cents: {
|
|
2151
|
-
gte: amountCents,
|
|
2243
|
+
try {
|
|
2244
|
+
const result = await this.prisma.$transaction(async (tx) => {
|
|
2245
|
+
var _a;
|
|
2246
|
+
const title = await tx.financial_title.findFirst({
|
|
2247
|
+
where: {
|
|
2248
|
+
id: titleId,
|
|
2249
|
+
title_type: titleType,
|
|
2152
2250
|
},
|
|
2153
|
-
|
|
2154
|
-
|
|
2155
|
-
|
|
2156
|
-
|
|
2251
|
+
select: {
|
|
2252
|
+
id: true,
|
|
2253
|
+
person_id: true,
|
|
2254
|
+
status: true,
|
|
2255
|
+
competence_date: true,
|
|
2157
2256
|
},
|
|
2158
|
-
}
|
|
2159
|
-
|
|
2160
|
-
|
|
2161
|
-
|
|
2162
|
-
|
|
2163
|
-
|
|
2164
|
-
|
|
2165
|
-
|
|
2166
|
-
|
|
2167
|
-
|
|
2168
|
-
|
|
2169
|
-
|
|
2170
|
-
open_amount_cents: true,
|
|
2171
|
-
due_date: true,
|
|
2172
|
-
status: true,
|
|
2173
|
-
},
|
|
2174
|
-
});
|
|
2175
|
-
if (!updatedInstallment) {
|
|
2176
|
-
throw new common_1.NotFoundException('Installment not found');
|
|
2177
|
-
}
|
|
2178
|
-
const nextInstallmentStatus = this.resolveInstallmentStatus(updatedInstallment.amount_cents, updatedInstallment.open_amount_cents, updatedInstallment.due_date);
|
|
2179
|
-
if (updatedInstallment.status !== nextInstallmentStatus) {
|
|
2180
|
-
await tx.financial_installment.update({
|
|
2257
|
+
});
|
|
2258
|
+
if (!title) {
|
|
2259
|
+
throw new common_1.NotFoundException((0, api_locale_1.getLocaleText)('itemNotFound', locale, `Financial title with ID ${titleId} not found`).replace('{{item}}', 'Financial title'));
|
|
2260
|
+
}
|
|
2261
|
+
if (!['open', 'partial'].includes(title.status)) {
|
|
2262
|
+
if (title.status === 'canceled') {
|
|
2263
|
+
throw new common_1.ConflictException('Título cancelado não pode receber baixa');
|
|
2264
|
+
}
|
|
2265
|
+
throw new common_1.BadRequestException('Only open/partial titles can be settled');
|
|
2266
|
+
}
|
|
2267
|
+
await this.assertDateNotInClosedPeriod(tx, settledAt, 'settle installment');
|
|
2268
|
+
const installment = await tx.financial_installment.findFirst({
|
|
2181
2269
|
where: {
|
|
2182
|
-
id:
|
|
2270
|
+
id: data.installment_id,
|
|
2271
|
+
title_id: title.id,
|
|
2183
2272
|
},
|
|
2273
|
+
select: {
|
|
2274
|
+
id: true,
|
|
2275
|
+
title_id: true,
|
|
2276
|
+
amount_cents: true,
|
|
2277
|
+
open_amount_cents: true,
|
|
2278
|
+
due_date: true,
|
|
2279
|
+
status: true,
|
|
2280
|
+
},
|
|
2281
|
+
});
|
|
2282
|
+
if (!installment) {
|
|
2283
|
+
throw new common_1.BadRequestException('Installment not found for this title');
|
|
2284
|
+
}
|
|
2285
|
+
if (installment.status === 'settled' || installment.status === 'canceled') {
|
|
2286
|
+
if (installment.status === 'settled') {
|
|
2287
|
+
throw new common_1.ConflictException('Parcela já liquidada');
|
|
2288
|
+
}
|
|
2289
|
+
throw new common_1.ConflictException('Parcela cancelada não pode receber baixa');
|
|
2290
|
+
}
|
|
2291
|
+
if (amountCents > installment.open_amount_cents) {
|
|
2292
|
+
throw new common_1.ConflictException('Settlement amount exceeds open amount');
|
|
2293
|
+
}
|
|
2294
|
+
const paymentMethodId = await this.resolvePaymentMethodId(tx, data.payment_channel);
|
|
2295
|
+
const settlement = await tx.settlement.create({
|
|
2184
2296
|
data: {
|
|
2185
|
-
|
|
2297
|
+
person_id: title.person_id,
|
|
2298
|
+
bank_account_id: data.bank_account_id || null,
|
|
2299
|
+
payment_method_id: paymentMethodId,
|
|
2300
|
+
settlement_type: titleType,
|
|
2301
|
+
status: 'confirmed',
|
|
2302
|
+
settled_at: settledAt,
|
|
2303
|
+
amount_cents: amountCents,
|
|
2304
|
+
description: ((_a = data.description) === null || _a === void 0 ? void 0 : _a.trim()) || null,
|
|
2305
|
+
created_by_user_id: userId,
|
|
2186
2306
|
},
|
|
2187
2307
|
});
|
|
2188
|
-
|
|
2189
|
-
|
|
2190
|
-
|
|
2191
|
-
|
|
2192
|
-
|
|
2193
|
-
|
|
2194
|
-
|
|
2195
|
-
|
|
2196
|
-
|
|
2197
|
-
|
|
2198
|
-
|
|
2199
|
-
|
|
2200
|
-
|
|
2201
|
-
|
|
2202
|
-
|
|
2203
|
-
installment_open_amount_cents: updatedInstallment.open_amount_cents,
|
|
2204
|
-
settlement_id: settlement.id,
|
|
2205
|
-
}),
|
|
2206
|
-
});
|
|
2207
|
-
const updatedTitle = await tx.financial_title.findFirst({
|
|
2208
|
-
where: {
|
|
2209
|
-
id: title.id,
|
|
2210
|
-
title_type: titleType,
|
|
2211
|
-
},
|
|
2212
|
-
include: this.defaultTitleInclude(),
|
|
2213
|
-
});
|
|
2214
|
-
if (!updatedTitle) {
|
|
2215
|
-
throw new common_1.NotFoundException('Financial title not found');
|
|
2216
|
-
}
|
|
2217
|
-
return {
|
|
2218
|
-
title: updatedTitle,
|
|
2219
|
-
settlementId: settlement.id,
|
|
2220
|
-
};
|
|
2221
|
-
});
|
|
2222
|
-
return Object.assign(Object.assign({}, this.mapTitleToFront(result.title)), { settlementId: String(result.settlementId) });
|
|
2223
|
-
}
|
|
2224
|
-
async reverseTitleSettlement(titleId, settlementId, data, titleType, locale, userId) {
|
|
2225
|
-
const updatedTitle = await this.prisma.$transaction(async (tx) => {
|
|
2226
|
-
const title = await tx.financial_title.findFirst({
|
|
2227
|
-
where: {
|
|
2228
|
-
id: titleId,
|
|
2229
|
-
title_type: titleType,
|
|
2230
|
-
},
|
|
2231
|
-
select: {
|
|
2232
|
-
id: true,
|
|
2233
|
-
status: true,
|
|
2234
|
-
competence_date: true,
|
|
2235
|
-
},
|
|
2236
|
-
});
|
|
2237
|
-
if (!title) {
|
|
2238
|
-
throw new common_1.NotFoundException((0, api_locale_1.getLocaleText)('itemNotFound', locale, `Financial title with ID ${titleId} not found`).replace('{{item}}', 'Financial title'));
|
|
2239
|
-
}
|
|
2240
|
-
await this.assertDateNotInClosedPeriod(tx, title.competence_date, 'reverse settlement');
|
|
2241
|
-
const settlement = await tx.settlement.findFirst({
|
|
2242
|
-
where: {
|
|
2243
|
-
id: settlementId,
|
|
2244
|
-
settlement_type: titleType,
|
|
2245
|
-
settlement_allocation: {
|
|
2246
|
-
some: {
|
|
2247
|
-
financial_installment: {
|
|
2248
|
-
title_id: title.id,
|
|
2249
|
-
},
|
|
2308
|
+
await tx.settlement_allocation.create({
|
|
2309
|
+
data: {
|
|
2310
|
+
settlement_id: settlement.id,
|
|
2311
|
+
installment_id: installment.id,
|
|
2312
|
+
allocated_amount_cents: amountCents,
|
|
2313
|
+
discount_cents: this.toCents(data.discount || 0),
|
|
2314
|
+
interest_cents: this.toCents(data.interest || 0),
|
|
2315
|
+
penalty_cents: this.toCents(data.penalty || 0),
|
|
2316
|
+
},
|
|
2317
|
+
});
|
|
2318
|
+
const decrementResult = await tx.financial_installment.updateMany({
|
|
2319
|
+
where: {
|
|
2320
|
+
id: installment.id,
|
|
2321
|
+
open_amount_cents: {
|
|
2322
|
+
gte: amountCents,
|
|
2250
2323
|
},
|
|
2251
2324
|
},
|
|
2252
|
-
|
|
2253
|
-
|
|
2254
|
-
|
|
2255
|
-
include: {
|
|
2256
|
-
financial_installment: {
|
|
2257
|
-
select: {
|
|
2258
|
-
id: true,
|
|
2259
|
-
amount_cents: true,
|
|
2260
|
-
open_amount_cents: true,
|
|
2261
|
-
due_date: true,
|
|
2262
|
-
status: true,
|
|
2263
|
-
},
|
|
2264
|
-
},
|
|
2325
|
+
data: {
|
|
2326
|
+
open_amount_cents: {
|
|
2327
|
+
decrement: amountCents,
|
|
2265
2328
|
},
|
|
2266
2329
|
},
|
|
2267
|
-
}
|
|
2330
|
+
});
|
|
2331
|
+
if (decrementResult.count !== 1) {
|
|
2332
|
+
throw new common_1.BadRequestException('Installment was updated concurrently, please try again');
|
|
2333
|
+
}
|
|
2334
|
+
const updatedInstallment = await tx.financial_installment.findUnique({
|
|
2335
|
+
where: {
|
|
2336
|
+
id: installment.id,
|
|
2337
|
+
},
|
|
2338
|
+
select: {
|
|
2339
|
+
id: true,
|
|
2340
|
+
amount_cents: true,
|
|
2341
|
+
open_amount_cents: true,
|
|
2342
|
+
due_date: true,
|
|
2343
|
+
status: true,
|
|
2344
|
+
},
|
|
2345
|
+
});
|
|
2346
|
+
if (!updatedInstallment) {
|
|
2347
|
+
throw new common_1.NotFoundException('Installment not found');
|
|
2348
|
+
}
|
|
2349
|
+
const nextInstallmentStatus = this.resolveInstallmentStatus(updatedInstallment.amount_cents, updatedInstallment.open_amount_cents, updatedInstallment.due_date);
|
|
2350
|
+
if (updatedInstallment.status !== nextInstallmentStatus) {
|
|
2351
|
+
await tx.financial_installment.update({
|
|
2352
|
+
where: {
|
|
2353
|
+
id: updatedInstallment.id,
|
|
2354
|
+
},
|
|
2355
|
+
data: {
|
|
2356
|
+
status: nextInstallmentStatus,
|
|
2357
|
+
},
|
|
2358
|
+
});
|
|
2359
|
+
}
|
|
2360
|
+
const previousTitleStatus = title.status;
|
|
2361
|
+
const nextTitleStatus = await this.recalculateTitleStatus(tx, title.id);
|
|
2362
|
+
await this.createAuditLog(tx, {
|
|
2363
|
+
action: 'SETTLE_INSTALLMENT',
|
|
2364
|
+
entityTable: 'financial_title',
|
|
2365
|
+
entityId: String(title.id),
|
|
2366
|
+
actorUserId: userId,
|
|
2367
|
+
summary: `Settled installment ${installment.id} of title ${title.id}`,
|
|
2368
|
+
beforeData: JSON.stringify({
|
|
2369
|
+
title_status: previousTitleStatus,
|
|
2370
|
+
installment_open_amount_cents: installment.open_amount_cents,
|
|
2371
|
+
}),
|
|
2372
|
+
afterData: JSON.stringify({
|
|
2373
|
+
title_status: nextTitleStatus,
|
|
2374
|
+
installment_open_amount_cents: updatedInstallment.open_amount_cents,
|
|
2375
|
+
settlement_id: settlement.id,
|
|
2376
|
+
}),
|
|
2377
|
+
});
|
|
2378
|
+
const updatedTitle = await tx.financial_title.findFirst({
|
|
2379
|
+
where: {
|
|
2380
|
+
id: title.id,
|
|
2381
|
+
title_type: titleType,
|
|
2382
|
+
},
|
|
2383
|
+
include: this.defaultTitleInclude(),
|
|
2384
|
+
});
|
|
2385
|
+
if (!updatedTitle) {
|
|
2386
|
+
throw new common_1.NotFoundException('Financial title not found');
|
|
2387
|
+
}
|
|
2388
|
+
return {
|
|
2389
|
+
title: updatedTitle,
|
|
2390
|
+
settlementId: settlement.id,
|
|
2391
|
+
};
|
|
2268
2392
|
});
|
|
2393
|
+
return Object.assign(Object.assign({}, this.mapTitleToFront(result.title)), { settlementId: String(result.settlementId) });
|
|
2394
|
+
}
|
|
2395
|
+
catch (error) {
|
|
2396
|
+
const message = String((error === null || error === void 0 ? void 0 : error.message) || '');
|
|
2397
|
+
if (message.includes('Soma de settlement_allocation')) {
|
|
2398
|
+
throw new common_1.ConflictException('Não foi possível registrar a baixa. Existe alocação ativa acima do limite da parcela.');
|
|
2399
|
+
}
|
|
2400
|
+
throw error;
|
|
2401
|
+
}
|
|
2402
|
+
}
|
|
2403
|
+
async reverseTitleSettlement(titleId, settlementId, data, titleType, locale, userId) {
|
|
2404
|
+
const updatedTitle = await this.reverseSettlementInternal(settlementId, data, locale, userId, {
|
|
2405
|
+
titleId,
|
|
2406
|
+
titleType,
|
|
2407
|
+
});
|
|
2408
|
+
if (!updatedTitle) {
|
|
2409
|
+
throw new common_1.NotFoundException('Financial title not found');
|
|
2410
|
+
}
|
|
2411
|
+
return this.mapTitleToFront(updatedTitle);
|
|
2412
|
+
}
|
|
2413
|
+
async reverseSettlementInternal(settlementId, data, locale, userId, scope) {
|
|
2414
|
+
const { title } = await this.prisma.$transaction(async (tx) => {
|
|
2415
|
+
var _a, _b, _c, _d, _e;
|
|
2416
|
+
const settlementRows = await tx.$queryRaw `
|
|
2417
|
+
SELECT DISTINCT
|
|
2418
|
+
s.id,
|
|
2419
|
+
s.settlement_type::text,
|
|
2420
|
+
s.settled_at,
|
|
2421
|
+
s.amount_cents,
|
|
2422
|
+
s.description,
|
|
2423
|
+
s.person_id,
|
|
2424
|
+
s.bank_account_id,
|
|
2425
|
+
s.payment_method_id,
|
|
2426
|
+
s.created_by_user_id,
|
|
2427
|
+
COALESCE(s.entry_type::text, 'normal') AS entry_type,
|
|
2428
|
+
ft.id AS title_id,
|
|
2429
|
+
ft.title_type::text AS title_type,
|
|
2430
|
+
ft.status::text AS title_status,
|
|
2431
|
+
ft.competence_date AS title_competence_date
|
|
2432
|
+
FROM settlement s
|
|
2433
|
+
INNER JOIN settlement_allocation sa ON sa.settlement_id = s.id
|
|
2434
|
+
INNER JOIN financial_installment fi ON fi.id = sa.installment_id
|
|
2435
|
+
INNER JOIN financial_title ft ON ft.id = fi.title_id
|
|
2436
|
+
WHERE s.id = ${settlementId}
|
|
2437
|
+
FOR UPDATE OF s
|
|
2438
|
+
`;
|
|
2439
|
+
const settlement = settlementRows[0];
|
|
2269
2440
|
if (!settlement) {
|
|
2441
|
+
throw new common_1.NotFoundException('Settlement not found');
|
|
2442
|
+
}
|
|
2443
|
+
if ((scope === null || scope === void 0 ? void 0 : scope.titleId) && settlement.title_id !== scope.titleId) {
|
|
2270
2444
|
throw new common_1.NotFoundException('Settlement not found for this title');
|
|
2271
2445
|
}
|
|
2272
|
-
if (settlement.
|
|
2273
|
-
throw new common_1.
|
|
2446
|
+
if ((scope === null || scope === void 0 ? void 0 : scope.titleType) && settlement.title_type !== scope.titleType) {
|
|
2447
|
+
throw new common_1.NotFoundException('Settlement not found for this title type');
|
|
2274
2448
|
}
|
|
2275
|
-
|
|
2276
|
-
|
|
2277
|
-
|
|
2278
|
-
|
|
2279
|
-
|
|
2280
|
-
|
|
2281
|
-
|
|
2282
|
-
|
|
2449
|
+
if (settlement.entry_type !== 'normal') {
|
|
2450
|
+
throw new common_1.BadRequestException('Somente liquidações normais podem ser estornadas');
|
|
2451
|
+
}
|
|
2452
|
+
await this.assertDateNotInClosedPeriod(tx, settlement.title_competence_date, 'reverse settlement');
|
|
2453
|
+
const alreadyReversed = await tx.$queryRaw `
|
|
2454
|
+
SELECT id
|
|
2455
|
+
FROM settlement
|
|
2456
|
+
WHERE reverses_settlement_id = ${settlement.id}
|
|
2457
|
+
LIMIT 1
|
|
2458
|
+
`;
|
|
2459
|
+
if (alreadyReversed.length > 0) {
|
|
2460
|
+
throw new common_1.ConflictException('Liquidação já estornada.');
|
|
2461
|
+
}
|
|
2462
|
+
const isReconciled = await tx.$queryRaw `
|
|
2463
|
+
SELECT id
|
|
2464
|
+
FROM bank_reconciliation
|
|
2465
|
+
WHERE settlement_id = ${settlement.id}
|
|
2466
|
+
AND status = 'reconciled'
|
|
2467
|
+
LIMIT 1
|
|
2468
|
+
`;
|
|
2469
|
+
if (isReconciled.length > 0) {
|
|
2470
|
+
throw new common_1.ConflictException('Desconciliar primeiro');
|
|
2471
|
+
}
|
|
2472
|
+
const allocations = await tx.$queryRaw `
|
|
2473
|
+
SELECT
|
|
2474
|
+
sa.id,
|
|
2475
|
+
sa.installment_id,
|
|
2476
|
+
sa.allocated_amount_cents,
|
|
2477
|
+
sa.amount_cents,
|
|
2478
|
+
sa.discount_cents,
|
|
2479
|
+
sa.interest_cents,
|
|
2480
|
+
sa.penalty_cents,
|
|
2481
|
+
fi.amount_cents AS installment_amount_cents,
|
|
2482
|
+
fi.open_amount_cents AS installment_open_amount_cents,
|
|
2483
|
+
fi.due_date AS installment_due_date,
|
|
2484
|
+
fi.status::text AS installment_status
|
|
2485
|
+
FROM settlement_allocation sa
|
|
2486
|
+
INNER JOIN financial_installment fi ON fi.id = sa.installment_id
|
|
2487
|
+
WHERE sa.settlement_id = ${settlement.id}
|
|
2488
|
+
FOR UPDATE OF fi
|
|
2489
|
+
`;
|
|
2490
|
+
if (allocations.length === 0) {
|
|
2491
|
+
throw new common_1.BadRequestException('Settlement has no allocations to reverse');
|
|
2492
|
+
}
|
|
2493
|
+
const reversalMemo = ((_a = data.reason) === null || _a === void 0 ? void 0 : _a.trim()) || ((_b = data.memo) === null || _b === void 0 ? void 0 : _b.trim()) || 'Estorno';
|
|
2494
|
+
const reversalAmountCents = -Math.abs(Number(settlement.amount_cents || 0));
|
|
2495
|
+
const reversalResult = await tx.$queryRaw `
|
|
2496
|
+
INSERT INTO settlement (
|
|
2497
|
+
person_id,
|
|
2498
|
+
bank_account_id,
|
|
2499
|
+
payment_method_id,
|
|
2500
|
+
settlement_type,
|
|
2501
|
+
entry_type,
|
|
2502
|
+
status,
|
|
2503
|
+
settled_at,
|
|
2504
|
+
amount_cents,
|
|
2505
|
+
description,
|
|
2506
|
+
external_reference,
|
|
2507
|
+
created_by_user_id,
|
|
2508
|
+
reverses_settlement_id,
|
|
2509
|
+
created_at,
|
|
2510
|
+
updated_at
|
|
2511
|
+
)
|
|
2512
|
+
VALUES (
|
|
2513
|
+
${settlement.person_id},
|
|
2514
|
+
${settlement.bank_account_id},
|
|
2515
|
+
${settlement.payment_method_id},
|
|
2516
|
+
${settlement.settlement_type}::settlement_settlement_type_enum,
|
|
2517
|
+
'reversal'::settlement_entry_type_enum,
|
|
2518
|
+
'confirmed'::settlement_status_enum,
|
|
2519
|
+
NOW(),
|
|
2520
|
+
${reversalAmountCents},
|
|
2521
|
+
${reversalMemo},
|
|
2522
|
+
NULL,
|
|
2523
|
+
${userId || settlement.created_by_user_id || null},
|
|
2524
|
+
${settlement.id},
|
|
2525
|
+
NOW(),
|
|
2526
|
+
NOW()
|
|
2527
|
+
)
|
|
2528
|
+
RETURNING id
|
|
2529
|
+
`;
|
|
2530
|
+
const reversalId = (_c = reversalResult[0]) === null || _c === void 0 ? void 0 : _c.id;
|
|
2531
|
+
if (!reversalId) {
|
|
2532
|
+
throw new common_1.BadRequestException('Could not create reversal settlement');
|
|
2533
|
+
}
|
|
2534
|
+
for (const allocation of allocations) {
|
|
2535
|
+
const originalAmount = Number((_e = (_d = allocation.amount_cents) !== null && _d !== void 0 ? _d : allocation.allocated_amount_cents) !== null && _e !== void 0 ? _e : 0);
|
|
2536
|
+
await tx.settlement_allocation.create({
|
|
2537
|
+
data: {
|
|
2538
|
+
settlement_id: reversalId,
|
|
2539
|
+
installment_id: allocation.installment_id,
|
|
2540
|
+
allocated_amount_cents: -Math.abs(originalAmount),
|
|
2541
|
+
discount_cents: -Math.abs(allocation.discount_cents || 0),
|
|
2542
|
+
interest_cents: -Math.abs(allocation.interest_cents || 0),
|
|
2543
|
+
penalty_cents: -Math.abs(allocation.penalty_cents || 0),
|
|
2544
|
+
},
|
|
2545
|
+
});
|
|
2546
|
+
const nextOpenAmountCents = Number(allocation.installment_open_amount_cents || 0) +
|
|
2547
|
+
Math.abs(originalAmount);
|
|
2548
|
+
if (nextOpenAmountCents > Number(allocation.installment_amount_cents || 0)) {
|
|
2549
|
+
throw new common_1.ConflictException(`Estorno excederia o valor original da parcela ${allocation.installment_id}`);
|
|
2283
2550
|
}
|
|
2284
|
-
const nextInstallmentStatus = this.resolveInstallmentStatus(
|
|
2551
|
+
const nextInstallmentStatus = this.resolveInstallmentStatus(Number(allocation.installment_amount_cents || 0), nextOpenAmountCents, new Date(allocation.installment_due_date), allocation.installment_status);
|
|
2285
2552
|
await tx.financial_installment.update({
|
|
2286
2553
|
where: {
|
|
2287
|
-
id:
|
|
2554
|
+
id: allocation.installment_id,
|
|
2288
2555
|
},
|
|
2289
2556
|
data: {
|
|
2290
2557
|
open_amount_cents: nextOpenAmountCents,
|
|
@@ -2292,49 +2559,39 @@ let FinanceService = FinanceService_1 = class FinanceService {
|
|
|
2292
2559
|
},
|
|
2293
2560
|
});
|
|
2294
2561
|
}
|
|
2295
|
-
await tx
|
|
2296
|
-
where: {
|
|
2297
|
-
id: settlement.id,
|
|
2298
|
-
},
|
|
2299
|
-
data: {
|
|
2300
|
-
status: 'reversed',
|
|
2301
|
-
description: [
|
|
2302
|
-
settlement.description,
|
|
2303
|
-
data.reason ? `Reversed: ${data.reason.trim()}` : 'Reversed',
|
|
2304
|
-
]
|
|
2305
|
-
.filter(Boolean)
|
|
2306
|
-
.join(' | '),
|
|
2307
|
-
},
|
|
2308
|
-
});
|
|
2309
|
-
const previousTitleStatus = title.status;
|
|
2310
|
-
const nextTitleStatus = await this.recalculateTitleStatus(tx, title.id);
|
|
2562
|
+
const nextTitleStatus = await this.recalculateTitleStatus(tx, settlement.title_id);
|
|
2311
2563
|
await this.createAuditLog(tx, {
|
|
2312
2564
|
action: 'REVERSE_SETTLEMENT',
|
|
2313
2565
|
entityTable: 'financial_title',
|
|
2314
|
-
entityId: String(
|
|
2566
|
+
entityId: String(settlement.title_id),
|
|
2315
2567
|
actorUserId: userId,
|
|
2316
|
-
summary: `
|
|
2568
|
+
summary: `Created reversal ${reversalId} for settlement ${settlement.id}`,
|
|
2317
2569
|
beforeData: JSON.stringify({
|
|
2318
|
-
title_status:
|
|
2319
|
-
|
|
2570
|
+
title_status: settlement.title_status,
|
|
2571
|
+
settlement_id: settlement.id,
|
|
2572
|
+
settlement_entry_type: settlement.entry_type,
|
|
2320
2573
|
}),
|
|
2321
2574
|
afterData: JSON.stringify({
|
|
2322
2575
|
title_status: nextTitleStatus,
|
|
2323
|
-
|
|
2576
|
+
settlement_id: settlement.id,
|
|
2577
|
+
reversal_settlement_id: reversalId,
|
|
2324
2578
|
}),
|
|
2325
2579
|
});
|
|
2326
|
-
|
|
2580
|
+
const updatedTitle = await tx.financial_title.findFirst({
|
|
2327
2581
|
where: {
|
|
2328
|
-
id:
|
|
2329
|
-
title_type:
|
|
2582
|
+
id: settlement.title_id,
|
|
2583
|
+
title_type: settlement.title_type,
|
|
2330
2584
|
},
|
|
2331
2585
|
include: this.defaultTitleInclude(),
|
|
2332
2586
|
});
|
|
2587
|
+
if (!updatedTitle) {
|
|
2588
|
+
throw new common_1.NotFoundException('Financial title not found');
|
|
2589
|
+
}
|
|
2590
|
+
return {
|
|
2591
|
+
title: updatedTitle,
|
|
2592
|
+
};
|
|
2333
2593
|
});
|
|
2334
|
-
|
|
2335
|
-
throw new common_1.NotFoundException('Financial title not found');
|
|
2336
|
-
}
|
|
2337
|
-
return this.mapTitleToFront(updatedTitle);
|
|
2594
|
+
return title;
|
|
2338
2595
|
}
|
|
2339
2596
|
async updateTitleTags(titleId, titleType, tagIds, locale) {
|
|
2340
2597
|
const title = await this.getTitleById(titleId, titleType, locale);
|