@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.
@@ -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
- const result = await this.prisma.$transaction(async (tx) => {
2080
- var _a;
2081
- const title = await tx.financial_title.findFirst({
2082
- where: {
2083
- id: titleId,
2084
- title_type: titleType,
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
- data: {
2155
- open_amount_cents: {
2156
- decrement: amountCents,
2251
+ select: {
2252
+ id: true,
2253
+ person_id: true,
2254
+ status: true,
2255
+ competence_date: true,
2157
2256
  },
2158
- },
2159
- });
2160
- if (decrementResult.count !== 1) {
2161
- throw new common_1.BadRequestException('Installment was updated concurrently, please try again');
2162
- }
2163
- const updatedInstallment = await tx.financial_installment.findUnique({
2164
- where: {
2165
- id: installment.id,
2166
- },
2167
- select: {
2168
- id: true,
2169
- amount_cents: true,
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: updatedInstallment.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
- status: nextInstallmentStatus,
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
- const previousTitleStatus = title.status;
2190
- const nextTitleStatus = await this.recalculateTitleStatus(tx, title.id);
2191
- await this.createAuditLog(tx, {
2192
- action: 'SETTLE_INSTALLMENT',
2193
- entityTable: 'financial_title',
2194
- entityId: String(title.id),
2195
- actorUserId: userId,
2196
- summary: `Settled installment ${installment.id} of title ${title.id}`,
2197
- beforeData: JSON.stringify({
2198
- title_status: previousTitleStatus,
2199
- installment_open_amount_cents: installment.open_amount_cents,
2200
- }),
2201
- afterData: JSON.stringify({
2202
- title_status: nextTitleStatus,
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
- include: {
2254
- settlement_allocation: {
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.status === 'reversed') {
2273
- throw new common_1.BadRequestException('This settlement is already reversed');
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
- for (const allocation of settlement.settlement_allocation) {
2276
- const installment = allocation.financial_installment;
2277
- if (!installment) {
2278
- continue;
2279
- }
2280
- const nextOpenAmountCents = installment.open_amount_cents + allocation.allocated_amount_cents;
2281
- if (nextOpenAmountCents > installment.amount_cents) {
2282
- throw new common_1.BadRequestException(`Reverse would exceed installment amount for installment ${installment.id}`);
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(installment.amount_cents, nextOpenAmountCents, installment.due_date);
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: installment.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.settlement.update({
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(title.id),
2566
+ entityId: String(settlement.title_id),
2315
2567
  actorUserId: userId,
2316
- summary: `Reversed settlement ${settlement.id} from title ${title.id}`,
2568
+ summary: `Created reversal ${reversalId} for settlement ${settlement.id}`,
2317
2569
  beforeData: JSON.stringify({
2318
- title_status: previousTitleStatus,
2319
- settlement_status: settlement.status,
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
- settlement_status: 'reversed',
2576
+ settlement_id: settlement.id,
2577
+ reversal_settlement_id: reversalId,
2324
2578
  }),
2325
2579
  });
2326
- return tx.financial_title.findFirst({
2580
+ const updatedTitle = await tx.financial_title.findFirst({
2327
2581
  where: {
2328
- id: title.id,
2329
- title_type: titleType,
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
- if (!updatedTitle) {
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);