@hed-hog/finance 0.0.251 → 0.0.252

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.
@@ -221,8 +221,12 @@ function Alertas({
221
221
  (e) => e.statusConciliacao === 'pendente'
222
222
  ).length;
223
223
 
224
- const periodoBase = periodoAberto?.inicio ? new Date(periodoAberto.inicio) : new Date();
225
- const mes = new Intl.DateTimeFormat(locale, { month: 'long' }).format(periodoBase);
224
+ const periodoBase = periodoAberto?.inicio
225
+ ? new Date(periodoAberto.inicio)
226
+ : new Date();
227
+ const mes = new Intl.DateTimeFormat(locale, { month: 'long' }).format(
228
+ periodoBase
229
+ );
226
230
  const periodoAtual = `${mes.charAt(0).toUpperCase()}${mes.slice(1)}/${periodoBase.getFullYear()}`;
227
231
 
228
232
  return (
@@ -281,6 +285,9 @@ export default function DashboardPage() {
281
285
  } = data;
282
286
 
283
287
  const getPessoaById = (id?: string) => pessoas.find((p) => p.id === id);
288
+ const titulosPagarAprovados = titulosPagar.filter(
289
+ (titulo) => titulo.status !== 'rascunho' && titulo.status !== 'cancelado'
290
+ );
284
291
 
285
292
  return (
286
293
  <Page>
@@ -335,7 +342,7 @@ export default function DashboardPage() {
335
342
  </CardContent>
336
343
  </Card>
337
344
  <Alertas
338
- titulosPagar={titulosPagar}
345
+ titulosPagar={titulosPagarAprovados}
339
346
  extratos={extratos}
340
347
  periodoAberto={periodoAberto}
341
348
  locale={locale}
@@ -344,7 +351,7 @@ export default function DashboardPage() {
344
351
  </div>
345
352
 
346
353
  <ProximosVencimentos
347
- titulosPagar={titulosPagar}
354
+ titulosPagar={titulosPagarAprovados}
348
355
  titulosReceber={titulosReceber}
349
356
  getPessoaById={getPessoaById}
350
357
  t={t}
@@ -198,6 +198,44 @@
198
198
  "reverse": "Reverse",
199
199
  "cancel": "Cancel"
200
200
  },
201
+ "validation": {
202
+ "installmentRequired": "Installment is required",
203
+ "amountGreaterThanZero": "Amount must be greater than zero"
204
+ },
205
+ "dialogs": {
206
+ "cancel": {
207
+ "title": "Confirm cancellation",
208
+ "description": "This action changes the title status to canceled and does not remove audit records.",
209
+ "cancel": "Cancel",
210
+ "confirm": "Confirm cancellation"
211
+ },
212
+ "reverse": {
213
+ "title": "Confirm reversal",
214
+ "description": "This action reverses the selected settlement, recalculates installment balances, and updates the title status.",
215
+ "cancel": "Cancel",
216
+ "confirm": "Confirm reversal"
217
+ }
218
+ },
219
+ "settleSheet": {
220
+ "title": "Register settlement",
221
+ "description": "Provide the installment and settlement amount. The backend validates status and amount limits automatically.",
222
+ "installmentLabel": "Installment",
223
+ "installmentPlaceholder": "Select",
224
+ "installmentOption": "Installment {number} - open amount: {amount}",
225
+ "amountLabel": "Amount",
226
+ "descriptionLabel": "Description (optional)",
227
+ "confirm": "Confirm settlement"
228
+ },
229
+ "messages": {
230
+ "approveSuccess": "Title approved successfully",
231
+ "approveError": "Could not approve the title",
232
+ "settleSuccess": "Settlement registered successfully",
233
+ "settleError": "Could not register settlement",
234
+ "reverseSuccess": "Reversal completed successfully",
235
+ "reverseError": "Could not reverse the settlement",
236
+ "cancelSuccess": "Title canceled successfully",
237
+ "cancelError": "Could not cancel the title"
238
+ },
201
239
  "documentData": {
202
240
  "title": "Document Data",
203
241
  "supplier": "Supplier",
@@ -254,6 +292,12 @@
254
292
  "fine": "Fine",
255
293
  "account": "Account",
256
294
  "method": "Method",
295
+ "actions": "Actions",
296
+ "reverseButton": "Reverse",
297
+ "reverseDialogTitle": "Confirm reversal",
298
+ "reverseDialogDescription": "This action creates a reversal for the settlement and recalculates balances and status.",
299
+ "reverseDialogCancel": "Cancel",
300
+ "reverseDialogConfirm": "Confirm reversal",
257
301
  "none": "No settlement recorded"
258
302
  },
259
303
  "audit": { "none": "No audit event" }
@@ -198,6 +198,44 @@
198
198
  "reverse": "Estornar",
199
199
  "cancel": "Cancelar"
200
200
  },
201
+ "validation": {
202
+ "installmentRequired": "Parcela obrigatória",
203
+ "amountGreaterThanZero": "Valor deve ser maior que zero"
204
+ },
205
+ "dialogs": {
206
+ "cancel": {
207
+ "title": "Confirmar cancelamento",
208
+ "description": "Essa ação altera o título para cancelado e não remove os registros de auditoria.",
209
+ "cancel": "Cancelar",
210
+ "confirm": "Confirmar cancelamento"
211
+ },
212
+ "reverse": {
213
+ "title": "Confirmar estorno",
214
+ "description": "Esta ação estorna a liquidação selecionada, recalcula os saldos das parcelas e atualiza o status do título.",
215
+ "cancel": "Cancelar",
216
+ "confirm": "Confirmar estorno"
217
+ }
218
+ },
219
+ "settleSheet": {
220
+ "title": "Registrar baixa",
221
+ "description": "Informe a parcela e o valor da baixa. O backend valida os estados e limites de valor automaticamente.",
222
+ "installmentLabel": "Parcela",
223
+ "installmentPlaceholder": "Selecione",
224
+ "installmentOption": "Parcela {number} - em aberto: {amount}",
225
+ "amountLabel": "Valor",
226
+ "descriptionLabel": "Descrição (opcional)",
227
+ "confirm": "Confirmar baixa"
228
+ },
229
+ "messages": {
230
+ "approveSuccess": "Título aprovado com sucesso",
231
+ "approveError": "Não foi possível aprovar o título",
232
+ "settleSuccess": "Baixa registrada com sucesso",
233
+ "settleError": "Não foi possível registrar a baixa",
234
+ "reverseSuccess": "Estorno realizado com sucesso",
235
+ "reverseError": "Não foi possível estornar a liquidação",
236
+ "cancelSuccess": "Título cancelado com sucesso",
237
+ "cancelError": "Não foi possível cancelar o título"
238
+ },
201
239
  "documentData": {
202
240
  "title": "Dados do Documento",
203
241
  "supplier": "Fornecedor",
@@ -254,6 +292,12 @@
254
292
  "fine": "Multa",
255
293
  "account": "Conta",
256
294
  "method": "Método",
295
+ "actions": "Ações",
296
+ "reverseButton": "Estornar",
297
+ "reverseDialogTitle": "Confirmar estorno",
298
+ "reverseDialogDescription": "Esta ação cria o estorno da liquidação e recalcula saldos e status.",
299
+ "reverseDialogCancel": "Cancelar",
300
+ "reverseDialogConfirm": "Confirmar estorno",
257
301
  "none": "Nenhuma liquidação registrada"
258
302
  },
259
303
  "audit": { "none": "Nenhum evento de auditoria" }
@@ -102,7 +102,9 @@ BEGIN
102
102
  SELECT COALESCE(SUM(sa.allocated_amount_cents), 0)
103
103
  INTO allocated_total_cents
104
104
  FROM settlement_allocation sa
105
- WHERE sa.installment_id = target_installment_id;
105
+ INNER JOIN settlement s ON s.id = sa.settlement_id
106
+ WHERE sa.installment_id = target_installment_id
107
+ AND s.status <> 'reversed';
106
108
 
107
109
  IF allocated_total_cents > installment_amount_cents THEN
108
110
  RAISE EXCEPTION
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hed-hog/finance",
3
- "version": "0.0.251",
3
+ "version": "0.0.252",
4
4
  "main": "dist/index.js",
5
5
  "types": "dist/index.d.ts",
6
6
  "dependencies": {
@@ -9,14 +9,14 @@
9
9
  "@nestjs/core": "^11",
10
10
  "@nestjs/jwt": "^11",
11
11
  "@nestjs/mapped-types": "*",
12
- "@hed-hog/api-pagination": "0.0.5",
13
- "@hed-hog/contact": "0.0.251",
14
12
  "@hed-hog/api-prisma": "0.0.4",
13
+ "@hed-hog/contact": "0.0.251",
14
+ "@hed-hog/api-pagination": "0.0.5",
15
15
  "@hed-hog/tag": "0.0.251",
16
16
  "@hed-hog/api-locale": "0.0.11",
17
17
  "@hed-hog/api-types": "0.0.1",
18
- "@hed-hog/api": "0.0.3",
19
- "@hed-hog/core": "0.0.251"
18
+ "@hed-hog/core": "0.0.251",
19
+ "@hed-hog/api": "0.0.3"
20
20
  },
21
21
  "exports": {
22
22
  ".": {
@@ -4,6 +4,7 @@ import { PrismaService } from '@hed-hog/api-prisma';
4
4
  import { AiService, FileService } from '@hed-hog/core';
5
5
  import {
6
6
  BadRequestException,
7
+ ConflictException,
7
8
  forwardRef,
8
9
  Inject,
9
10
  Injectable,
@@ -833,7 +834,9 @@ export class FinanceService {
833
834
  const day7 = this.addDays(today, 7);
834
835
  const day30 = this.addDays(today, 30);
835
836
 
836
- const payableInstallments = this.extractOpenInstallments(payables);
837
+ const payableInstallments = this.extractOpenInstallments(
838
+ (payables || []).filter((title) => this.isPayableTitleApproved(title)),
839
+ );
837
840
  const receivableInstallments = this.extractOpenInstallments(receivables);
838
841
 
839
842
  const saldoCaixa = (bankAccounts || [])
@@ -887,6 +890,11 @@ export class FinanceService {
887
890
  );
888
891
  }
889
892
 
893
+ private isPayableTitleApproved(title: any) {
894
+ const status = String(title?.status || '').toLowerCase();
895
+ return status !== 'rascunho' && status !== 'cancelado';
896
+ }
897
+
890
898
  private sumInstallmentsDueBetween(
891
899
  installments: any[],
892
900
  startDate: Date,
@@ -2906,12 +2914,46 @@ export class FinanceService {
2906
2914
  throw new BadRequestException('Title cannot be canceled in current status');
2907
2915
  }
2908
2916
 
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
+ });
2932
+
2933
+ if (hasActiveSettlements) {
2934
+ throw new ConflictException(
2935
+ 'Não é possível cancelar enquanto houver liquidações ativas. Estorne primeiro.',
2936
+ );
2937
+ }
2938
+
2909
2939
  await this.assertDateNotInClosedPeriod(
2910
2940
  tx,
2911
2941
  title.competence_date,
2912
2942
  'cancel title',
2913
2943
  );
2914
2944
 
2945
+ await tx.financial_installment.updateMany({
2946
+ where: {
2947
+ title_id: title.id,
2948
+ status: {
2949
+ not: 'canceled',
2950
+ },
2951
+ },
2952
+ data: {
2953
+ status: 'canceled',
2954
+ },
2955
+ });
2956
+
2915
2957
  await tx.financial_title.update({
2916
2958
  where: { id: title.id },
2917
2959
  data: {
@@ -2966,8 +3008,9 @@ export class FinanceService {
2966
3008
  throw new BadRequestException('Invalid settlement date');
2967
3009
  }
2968
3010
 
2969
- const result = await this.prisma.$transaction(async (tx) => {
2970
- const title = await tx.financial_title.findFirst({
3011
+ try {
3012
+ const result = await this.prisma.$transaction(async (tx) => {
3013
+ const title = await tx.financial_title.findFirst({
2971
3014
  where: {
2972
3015
  id: titleId,
2973
3016
  title_type: titleType,
@@ -2991,6 +3034,10 @@ export class FinanceService {
2991
3034
  }
2992
3035
 
2993
3036
  if (!['open', 'partial'].includes(title.status)) {
3037
+ if (title.status === 'canceled') {
3038
+ throw new ConflictException('Título cancelado não pode receber baixa');
3039
+ }
3040
+
2994
3041
  throw new BadRequestException(
2995
3042
  'Only open/partial titles can be settled',
2996
3043
  );
@@ -2998,7 +3045,7 @@ export class FinanceService {
2998
3045
 
2999
3046
  await this.assertDateNotInClosedPeriod(
3000
3047
  tx,
3001
- title.competence_date,
3048
+ settledAt,
3002
3049
  'settle installment',
3003
3050
  );
3004
3051
 
@@ -3022,11 +3069,15 @@ export class FinanceService {
3022
3069
  }
3023
3070
 
3024
3071
  if (installment.status === 'settled' || installment.status === 'canceled') {
3025
- throw new BadRequestException('This installment cannot be settled');
3072
+ if (installment.status === 'settled') {
3073
+ throw new ConflictException('Parcela já liquidada');
3074
+ }
3075
+
3076
+ throw new ConflictException('Parcela cancelada não pode receber baixa');
3026
3077
  }
3027
3078
 
3028
3079
  if (amountCents > installment.open_amount_cents) {
3029
- throw new BadRequestException('Settlement amount exceeds open amount');
3080
+ throw new ConflictException('Settlement amount exceeds open amount');
3030
3081
  }
3031
3082
 
3032
3083
  const paymentMethodId = await this.resolvePaymentMethodId(
@@ -3145,16 +3196,27 @@ export class FinanceService {
3145
3196
  throw new NotFoundException('Financial title not found');
3146
3197
  }
3147
3198
 
3199
+ return {
3200
+ title: updatedTitle,
3201
+ settlementId: settlement.id,
3202
+ };
3203
+ });
3204
+
3148
3205
  return {
3149
- title: updatedTitle,
3150
- settlementId: settlement.id,
3206
+ ...this.mapTitleToFront(result.title),
3207
+ settlementId: String(result.settlementId),
3151
3208
  };
3152
- });
3209
+ } catch (error: any) {
3210
+ const message = String(error?.message || '');
3153
3211
 
3154
- return {
3155
- ...this.mapTitleToFront(result.title),
3156
- settlementId: String(result.settlementId),
3157
- };
3212
+ if (message.includes('Soma de settlement_allocation')) {
3213
+ throw new ConflictException(
3214
+ 'Não foi possível registrar a baixa. Existe alocação ativa acima do limite da parcela.',
3215
+ );
3216
+ }
3217
+
3218
+ throw error;
3219
+ }
3158
3220
  }
3159
3221
 
3160
3222
  private async reverseTitleSettlement(
@@ -3228,7 +3290,7 @@ export class FinanceService {
3228
3290
  }
3229
3291
 
3230
3292
  if (settlement.status === 'reversed') {
3231
- throw new BadRequestException('This settlement is already reversed');
3293
+ throw new ConflictException('Liquidação estornada.');
3232
3294
  }
3233
3295
 
3234
3296
  for (const allocation of settlement.settlement_allocation) {
@@ -3279,6 +3341,28 @@ export class FinanceService {
3279
3341
  },
3280
3342
  });
3281
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
+
3282
3366
  const previousTitleStatus = title.status;
3283
3367
  const nextTitleStatus = await this.recalculateTitleStatus(tx, title.id);
3284
3368