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