@hed-hog/finance 0.0.252 → 0.0.253

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -690,6 +690,138 @@ let FinanceService = FinanceService_1 = class FinanceService {
690
690
  async reverseAccountsReceivableSettlement(id, settlementId, data, locale, userId) {
691
691
  return this.reverseTitleSettlement(id, settlementId, data, 'receivable', locale, userId);
692
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
+ }
693
825
  async createTag(data) {
694
826
  const slug = this.normalizeTagSlug(data.name);
695
827
  if (!slug) {
@@ -2022,6 +2154,7 @@ let FinanceService = FinanceService_1 = class FinanceService {
2022
2154
  }
2023
2155
  async cancelTitle(titleId, data, titleType, locale, userId) {
2024
2156
  const updatedTitle = await this.prisma.$transaction(async (tx) => {
2157
+ var _a;
2025
2158
  const title = await tx.financial_title.findFirst({
2026
2159
  where: {
2027
2160
  id: titleId,
@@ -2039,21 +2172,19 @@ let FinanceService = FinanceService_1 = class FinanceService {
2039
2172
  if (title.status === 'settled' || title.status === 'canceled') {
2040
2173
  throw new common_1.BadRequestException('Title cannot be canceled in current status');
2041
2174
  }
2042
- const hasActiveSettlements = await tx.settlement_allocation.findFirst({
2043
- where: {
2044
- financial_installment: {
2045
- title_id: title.id,
2046
- },
2047
- settlement: {
2048
- status: {
2049
- not: 'reversed',
2050
- },
2051
- },
2052
- },
2053
- select: {
2054
- id: true,
2055
- },
2056
- });
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;
2057
2188
  if (hasActiveSettlements) {
2058
2189
  throw new common_1.ConflictException('Não é possível cancelar enquanto houver liquidações ativas. Estorne primeiro.');
2059
2190
  }
@@ -2270,69 +2401,157 @@ let FinanceService = FinanceService_1 = class FinanceService {
2270
2401
  }
2271
2402
  }
2272
2403
  async reverseTitleSettlement(titleId, settlementId, data, titleType, locale, userId) {
2273
- const updatedTitle = await this.prisma.$transaction(async (tx) => {
2274
- const title = await tx.financial_title.findFirst({
2275
- where: {
2276
- id: titleId,
2277
- title_type: titleType,
2278
- },
2279
- select: {
2280
- id: true,
2281
- status: true,
2282
- competence_date: true,
2283
- },
2284
- });
2285
- if (!title) {
2286
- throw new common_1.NotFoundException((0, api_locale_1.getLocaleText)('itemNotFound', locale, `Financial title with ID ${titleId} not found`).replace('{{item}}', 'Financial title'));
2287
- }
2288
- await this.assertDateNotInClosedPeriod(tx, title.competence_date, 'reverse settlement');
2289
- const settlement = await tx.settlement.findFirst({
2290
- where: {
2291
- id: settlementId,
2292
- settlement_type: titleType,
2293
- settlement_allocation: {
2294
- some: {
2295
- financial_installment: {
2296
- title_id: title.id,
2297
- },
2298
- },
2299
- },
2300
- },
2301
- include: {
2302
- settlement_allocation: {
2303
- include: {
2304
- financial_installment: {
2305
- select: {
2306
- id: true,
2307
- amount_cents: true,
2308
- open_amount_cents: true,
2309
- due_date: true,
2310
- status: true,
2311
- },
2312
- },
2313
- },
2314
- },
2315
- },
2316
- });
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];
2317
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) {
2318
2444
  throw new common_1.NotFoundException('Settlement not found for this title');
2319
2445
  }
2320
- if (settlement.status === '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');
2448
+ }
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) {
2321
2460
  throw new common_1.ConflictException('Liquidação já estornada.');
2322
2461
  }
2323
- for (const allocation of settlement.settlement_allocation) {
2324
- const installment = allocation.financial_installment;
2325
- if (!installment) {
2326
- continue;
2327
- }
2328
- const nextOpenAmountCents = installment.open_amount_cents + allocation.allocated_amount_cents;
2329
- if (nextOpenAmountCents > installment.amount_cents) {
2330
- throw new common_1.BadRequestException(`Reverse would exceed installment amount for installment ${installment.id}`);
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}`);
2331
2550
  }
2332
- 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);
2333
2552
  await tx.financial_installment.update({
2334
2553
  where: {
2335
- id: installment.id,
2554
+ id: allocation.installment_id,
2336
2555
  },
2337
2556
  data: {
2338
2557
  open_amount_cents: nextOpenAmountCents,
@@ -2340,69 +2559,39 @@ let FinanceService = FinanceService_1 = class FinanceService {
2340
2559
  },
2341
2560
  });
2342
2561
  }
2343
- await tx.settlement.update({
2344
- where: {
2345
- id: settlement.id,
2346
- },
2347
- data: {
2348
- status: 'reversed',
2349
- description: [
2350
- settlement.description,
2351
- data.reason ? `Reversed: ${data.reason.trim()}` : 'Reversed',
2352
- ]
2353
- .filter(Boolean)
2354
- .join(' | '),
2355
- },
2356
- });
2357
- await tx.bank_reconciliation.updateMany({
2358
- where: {
2359
- settlement_id: settlement.id,
2360
- status: 'pending',
2361
- },
2362
- data: {
2363
- status: 'reversed',
2364
- },
2365
- });
2366
- await tx.bank_reconciliation.updateMany({
2367
- where: {
2368
- settlement_id: settlement.id,
2369
- status: {
2370
- in: ['reconciled', 'adjusted'],
2371
- },
2372
- },
2373
- data: {
2374
- status: 'adjusted',
2375
- },
2376
- });
2377
- const previousTitleStatus = title.status;
2378
- const nextTitleStatus = await this.recalculateTitleStatus(tx, title.id);
2562
+ const nextTitleStatus = await this.recalculateTitleStatus(tx, settlement.title_id);
2379
2563
  await this.createAuditLog(tx, {
2380
2564
  action: 'REVERSE_SETTLEMENT',
2381
2565
  entityTable: 'financial_title',
2382
- entityId: String(title.id),
2566
+ entityId: String(settlement.title_id),
2383
2567
  actorUserId: userId,
2384
- summary: `Reversed settlement ${settlement.id} from title ${title.id}`,
2568
+ summary: `Created reversal ${reversalId} for settlement ${settlement.id}`,
2385
2569
  beforeData: JSON.stringify({
2386
- title_status: previousTitleStatus,
2387
- settlement_status: settlement.status,
2570
+ title_status: settlement.title_status,
2571
+ settlement_id: settlement.id,
2572
+ settlement_entry_type: settlement.entry_type,
2388
2573
  }),
2389
2574
  afterData: JSON.stringify({
2390
2575
  title_status: nextTitleStatus,
2391
- settlement_status: 'reversed',
2576
+ settlement_id: settlement.id,
2577
+ reversal_settlement_id: reversalId,
2392
2578
  }),
2393
2579
  });
2394
- return tx.financial_title.findFirst({
2580
+ const updatedTitle = await tx.financial_title.findFirst({
2395
2581
  where: {
2396
- id: title.id,
2397
- title_type: titleType,
2582
+ id: settlement.title_id,
2583
+ title_type: settlement.title_type,
2398
2584
  },
2399
2585
  include: this.defaultTitleInclude(),
2400
2586
  });
2587
+ if (!updatedTitle) {
2588
+ throw new common_1.NotFoundException('Financial title not found');
2589
+ }
2590
+ return {
2591
+ title: updatedTitle,
2592
+ };
2401
2593
  });
2402
- if (!updatedTitle) {
2403
- throw new common_1.NotFoundException('Financial title not found');
2404
- }
2405
- return this.mapTitleToFront(updatedTitle);
2594
+ return title;
2406
2595
  }
2407
2596
  async updateTitleTags(titleId, titleType, tagIds, locale) {
2408
2597
  const title = await this.getTitleById(titleId, titleType, locale);