@hed-hog/finance 0.0.366 → 0.0.370

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.
Files changed (38) hide show
  1. package/dist/dto/create-financial-title.dto.d.ts +1 -0
  2. package/dist/dto/create-financial-title.dto.d.ts.map +1 -1
  3. package/dist/dto/create-financial-title.dto.js +6 -0
  4. package/dist/dto/create-financial-title.dto.js.map +1 -1
  5. package/dist/finance-data.controller.d.ts +4 -0
  6. package/dist/finance-data.controller.d.ts.map +1 -1
  7. package/dist/finance-installments.controller.d.ts +40 -0
  8. package/dist/finance-installments.controller.d.ts.map +1 -1
  9. package/dist/finance-statements.controller.d.ts +2 -0
  10. package/dist/finance-statements.controller.d.ts.map +1 -1
  11. package/dist/finance.service.d.ts +47 -0
  12. package/dist/finance.service.d.ts.map +1 -1
  13. package/dist/finance.service.js +156 -109
  14. package/dist/finance.service.js.map +1 -1
  15. package/dist/mcp-tools/finance-installments.mcp-tools.d.ts.map +1 -1
  16. package/dist/mcp-tools/finance-installments.mcp-tools.js +12 -2
  17. package/dist/mcp-tools/finance-installments.mcp-tools.js.map +1 -1
  18. package/hedhog/frontend/app/_components/bank-account-picker-field.tsx.ejs +3 -0
  19. package/hedhog/frontend/app/_components/bank-account-sheet.tsx.ejs +902 -0
  20. package/hedhog/frontend/app/_components/finance-picker.tsx.ejs +95 -0
  21. package/hedhog/frontend/app/accounts-payable/installments/page.tsx.ejs +117 -43
  22. package/hedhog/frontend/app/accounts-receivable/collections-default/page.tsx.ejs +8 -2
  23. package/hedhog/frontend/app/accounts-receivable/installments/page.tsx.ejs +114 -43
  24. package/hedhog/frontend/app/administration/categories/page.tsx.ejs +4 -1
  25. package/hedhog/frontend/app/administration/cost-centers/page.tsx.ejs +4 -1
  26. package/hedhog/frontend/app/administration/currencies/page.tsx.ejs +4 -1
  27. package/hedhog/frontend/app/administration/period-close/page.tsx.ejs +4 -1
  28. package/hedhog/frontend/app/cash-and-banks/bank-accounts/page.tsx.ejs +6 -893
  29. package/hedhog/frontend/app/cash-and-banks/bank-reconciliation/page.tsx.ejs +4 -1
  30. package/hedhog/frontend/app/cash-and-banks/statements/page.tsx.ejs +8 -2
  31. package/hedhog/frontend/app/cash-and-banks/transfers/page.tsx.ejs +4 -1
  32. package/hedhog/frontend/messages/en.json +14 -1
  33. package/hedhog/frontend/messages/pt.json +14 -1
  34. package/hedhog/table/financial_title.yaml +6 -1
  35. package/package.json +6 -6
  36. package/src/dto/create-financial-title.dto.ts +5 -0
  37. package/src/finance.service.ts +187 -134
  38. package/src/mcp-tools/finance-installments.mcp-tools.ts +12 -2
@@ -348,7 +348,10 @@ function CriarAjusteSheet({
348
348
  <Button
349
349
  type="button"
350
350
  variant="outline"
351
- onClick={() => setOpen(false)}
351
+ onClick={() => {
352
+ clearDraft();
353
+ setOpen(false);
354
+ }}
352
355
  >
353
356
  {t('common.cancel')}
354
357
  </Button>
@@ -511,7 +511,10 @@ function NovaContaBancariaSheet({
511
511
  <Button
512
512
  type="button"
513
513
  variant="outline"
514
- onClick={() => onOpenChange(false)}
514
+ onClick={() => {
515
+ clearDraft();
516
+ onOpenChange(false);
517
+ }}
515
518
  >
516
519
  {tBank('common.cancel')}
517
520
  </Button>
@@ -764,7 +767,10 @@ function ImportarExtratoSheet({
764
767
  <Button
765
768
  type="button"
766
769
  variant="outline"
767
- onClick={() => handleOpenChange(false)}
770
+ onClick={() => {
771
+ clearDraft();
772
+ handleOpenChange(false);
773
+ }}
768
774
  >
769
775
  {t('common.cancel')}
770
776
  </Button>
@@ -378,7 +378,10 @@ function NovaTransferenciaSheet({
378
378
  <Button
379
379
  type="button"
380
380
  variant="outline"
381
- onClick={() => setOpen(false)}
381
+ onClick={() => {
382
+ clearDraft();
383
+ setOpen(false);
384
+ }}
382
385
  >
383
386
  {t('common.cancel')}
384
387
  </Button>
@@ -162,7 +162,8 @@
162
162
  "cancel": "Cancel",
163
163
  "save": "Save",
164
164
  "createCategoryAria": "Create new category",
165
- "createCostCenterAria": "Create new cost center"
165
+ "createCostCenterAria": "Create new cost center",
166
+ "createBankAccountAria": "Create new bank account"
166
167
  },
167
168
  "categorySheet": {
168
169
  "title": "New category",
@@ -255,6 +256,12 @@
255
256
  "quarterly": "Quarterly",
256
257
  "semiannual": "Semi-annual",
257
258
  "annual": "Annual"
259
+ },
260
+ "documentNumberModeLabel": "Document numbering",
261
+ "documentNumberModes": {
262
+ "same": "Same number on all",
263
+ "sequence": "Number in sequence (1/N)",
264
+ "none": "No number"
258
265
  }
259
266
  },
260
267
  "newTitle": {
@@ -767,6 +774,12 @@
767
774
  "quarterly": "Quarterly",
768
775
  "semiannual": "Semi-annual",
769
776
  "annual": "Annual"
777
+ },
778
+ "documentNumberModeLabel": "Document numbering",
779
+ "documentNumberModes": {
780
+ "same": "Same number on all",
781
+ "sequence": "Number in sequence (1/N)",
782
+ "none": "No number"
770
783
  }
771
784
  },
772
785
  "newTitle": {
@@ -147,7 +147,8 @@
147
147
  "cancel": "Cancelar",
148
148
  "save": "Salvar",
149
149
  "createCategoryAria": "Criar nova categoria",
150
- "createCostCenterAria": "Criar novo centro de custo"
150
+ "createCostCenterAria": "Criar novo centro de custo",
151
+ "createBankAccountAria": "Criar nova conta bancária"
151
152
  },
152
153
  "categorySheet": {
153
154
  "title": "Nova categoria",
@@ -240,6 +241,12 @@
240
241
  "quarterly": "Trimestral",
241
242
  "semiannual": "Semestral",
242
243
  "annual": "Anual"
244
+ },
245
+ "documentNumberModeLabel": "Numeração do documento",
246
+ "documentNumberModes": {
247
+ "same": "Mesmo número em todos",
248
+ "sequence": "Numerar em sequência (1/N)",
249
+ "none": "Sem número"
243
250
  }
244
251
  },
245
252
  "newTitle": {
@@ -752,6 +759,12 @@
752
759
  "quarterly": "Trimestral",
753
760
  "semiannual": "Semestral",
754
761
  "annual": "Anual"
762
+ },
763
+ "documentNumberModeLabel": "Numeração do documento",
764
+ "documentNumberModes": {
765
+ "same": "Mesmo número em todos",
766
+ "sequence": "Numerar em sequência (1/N)",
767
+ "none": "Sem número"
755
768
  }
756
769
  },
757
770
  "newTitle": {
@@ -38,6 +38,10 @@ columns:
38
38
  - name: recurrence_end_date
39
39
  type: date
40
40
  isNullable: true
41
+ - name: recurrence_group_id
42
+ type: varchar
43
+ length: 36
44
+ isNullable: true
41
45
  - name: finance_category_id
42
46
  type: fk
43
47
  isNullable: true
@@ -60,4 +64,5 @@ columns:
60
64
  indices:
61
65
  - columns: [title_type, status]
62
66
  - columns: [person_id]
63
- - columns: [competence_date]
67
+ - columns: [competence_date]
68
+ - columns: [recurrence_group_id]
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hed-hog/finance",
3
- "version": "0.0.366",
3
+ "version": "0.0.370",
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-locale": "0.0.14",
12
13
  "@hed-hog/api": "0.0.8",
14
+ "@hed-hog/api-prisma": "0.0.6",
15
+ "@hed-hog/core": "0.0.370",
13
16
  "@hed-hog/api-pagination": "0.0.7",
17
+ "@hed-hog/tag": "0.0.370",
14
18
  "@hed-hog/api-types": "0.0.1",
15
- "@hed-hog/api-prisma": "0.0.6",
16
- "@hed-hog/crm": "0.0.366",
17
- "@hed-hog/core": "0.0.366",
18
- "@hed-hog/api-locale": "0.0.14",
19
- "@hed-hog/tag": "0.0.366"
19
+ "@hed-hog/crm": "0.0.370"
20
20
  },
21
21
  "exports": {
22
22
  ".": {
@@ -28,6 +28,11 @@ export class CreateRecurrenceRuleDto {
28
28
  @Min(1)
29
29
  @Max(600)
30
30
  max_occurrences?: number;
31
+
32
+ @IsOptional()
33
+ @IsString()
34
+ @IsIn(['same', 'sequence', 'none'])
35
+ document_number_mode?: string;
31
36
  }
32
37
 
33
38
  export class CreateFinancialInstallmentDto {
@@ -16,7 +16,7 @@ import {
16
16
  Logger,
17
17
  NotFoundException,
18
18
  } from '@nestjs/common';
19
- import { createHash } from 'node:crypto';
19
+ import { createHash, randomUUID } from 'node:crypto';
20
20
  import { readFile } from 'node:fs/promises';
21
21
  import { CreateBankAccountDto } from './dto/create-bank-account.dto';
22
22
  import { CreateBankReconciliationDto } from './dto/create-bank-reconciliation.dto';
@@ -5138,18 +5138,59 @@ export class FinanceService {
5138
5138
  interest_cents?: number;
5139
5139
  penalty_cents?: number;
5140
5140
  };
5141
- const installments: NormalizedInstallment[] = isRecurring
5142
- ? this.buildRecurrenceInstallments(
5143
- data.due_date,
5144
- rule!.frequency,
5145
- this.toCents(data.total_amount),
5146
- rule!.end_date,
5147
- rule!.max_occurrences,
5148
- )
5149
- : this.normalizeAndValidateInstallments(data, locale);
5141
+ type TitleSpec = {
5142
+ documentNumber: string | null;
5143
+ recurrenceGroupId: string | null;
5144
+ totalAmountCents: number;
5145
+ installments: NormalizedInstallment[];
5146
+ };
5147
+
5148
+ // Recurring titles are NOT installment titles: each occurrence becomes an
5149
+ // independent title (with a single installment), all linked by a shared
5150
+ // recurrence_group_id. Installment titles remain a single title with many
5151
+ // installments.
5152
+ let titleSpecs: TitleSpec[];
5153
+ if (isRecurring) {
5154
+ const occurrences = this.buildRecurrenceInstallments(
5155
+ data.due_date,
5156
+ rule!.frequency,
5157
+ this.toCents(data.total_amount),
5158
+ rule!.end_date,
5159
+ rule!.max_occurrences,
5160
+ );
5161
+ const recurrenceGroupId = randomUUID();
5162
+ const baseDocument = data.document_number?.trim() || null;
5163
+ const documentMode = rule!.document_number_mode || 'same';
5164
+ titleSpecs = occurrences.map((occurrence, index) => ({
5165
+ documentNumber: this.resolveRecurrenceDocumentNumber(
5166
+ baseDocument,
5167
+ documentMode,
5168
+ index + 1,
5169
+ occurrences.length,
5170
+ ),
5171
+ recurrenceGroupId,
5172
+ totalAmountCents: occurrence.amount_cents,
5173
+ installments: [
5174
+ {
5175
+ installment_number: 1,
5176
+ due_date: occurrence.due_date,
5177
+ amount_cents: occurrence.amount_cents,
5178
+ },
5179
+ ],
5180
+ }));
5181
+ } else {
5182
+ titleSpecs = [
5183
+ {
5184
+ documentNumber: data.document_number?.trim() || null,
5185
+ recurrenceGroupId: null,
5186
+ totalAmountCents: this.toCents(data.total_amount),
5187
+ installments: this.normalizeAndValidateInstallments(data, locale),
5188
+ },
5189
+ ];
5190
+ }
5150
5191
 
5151
- const hasPaidInstallments = installments.some(
5152
- (installment) => installment.paid,
5192
+ const hasPaidInstallments = titleSpecs.some((spec) =>
5193
+ spec.installments.some((installment) => installment.paid),
5153
5194
  );
5154
5195
 
5155
5196
  const createResult = await this.prisma.$transaction(async (tx) => {
@@ -5233,158 +5274,157 @@ export class FinanceService {
5233
5274
  tx,
5234
5275
  data.competence_date
5235
5276
  ? this.parseLocalDate(data.competence_date)
5236
- : this.parseLocalDate(installments[0].due_date),
5277
+ : this.parseLocalDate(titleSpecs[0].installments[0].due_date),
5237
5278
  'create title',
5238
5279
  );
5239
5280
 
5240
- const totalAmountCents = isRecurring
5241
- ? this.toCents(data.total_amount) * installments.length
5242
- : this.toCents(data.total_amount);
5243
-
5244
- const title = await tx.financial_title.create({
5245
- data: {
5246
- person_id: data.person_id,
5247
- title_type: titleType,
5248
- status: 'draft',
5249
- document_number: data.document_number?.trim() || null,
5250
- description: data.description,
5251
- competence_date: data.competence_date
5252
- ? this.parseLocalDate(data.competence_date)
5253
- : null,
5254
- issue_date: data.issue_date ? this.parseLocalDate(data.issue_date) : null,
5255
- total_amount_cents: totalAmountCents,
5256
- is_recurring: isRecurring,
5257
- recurrence_frequency: rule?.frequency ?? null,
5258
- recurrence_end_date: rule?.end_date ? this.parseLocalDate(rule.end_date) : null,
5259
- finance_category_id: data.finance_category_id,
5260
- created_by_user_id: userId,
5261
- },
5262
- });
5263
-
5264
- if (attachmentFileIds.length > 0) {
5265
- await tx.financial_title_attachment.createMany({
5266
- data: attachmentFileIds.map((fileId) => ({
5267
- title_id: title.id,
5268
- file_id: fileId,
5269
- uploaded_by_user_id: userId,
5270
- })),
5271
- });
5272
- }
5273
-
5281
+ const createdTitleIds: number[] = [];
5274
5282
  const appliedSettlements: Array<{
5275
5283
  settlementId: number;
5276
5284
  installmentId: number;
5277
5285
  openAmountCentsBefore: number;
5278
5286
  openAmountCentsAfter: number;
5287
+ titleId: number;
5279
5288
  }> = [];
5280
5289
 
5281
- for (let index = 0; index < installments.length; index++) {
5282
- const installment = installments[index];
5283
- const amountCents = installment.amount_cents;
5284
-
5285
- const createdInstallment = await tx.financial_installment.create({
5290
+ for (const spec of titleSpecs) {
5291
+ const title = await tx.financial_title.create({
5286
5292
  data: {
5287
- title_id: title.id,
5288
- installment_number: installment.installment_number,
5293
+ person_id: data.person_id,
5294
+ title_type: titleType,
5295
+ status: 'draft',
5296
+ document_number: spec.documentNumber,
5297
+ description: data.description,
5289
5298
  competence_date: data.competence_date
5290
5299
  ? this.parseLocalDate(data.competence_date)
5291
- : this.parseLocalDate(installment.due_date),
5292
- due_date: this.parseLocalDate(installment.due_date),
5293
- amount_cents: amountCents,
5294
- open_amount_cents: amountCents,
5295
- status: this.resolveInstallmentStatus(
5296
- amountCents,
5297
- amountCents,
5298
- this.parseLocalDate(installment.due_date),
5299
- ),
5300
- notes: data.description,
5300
+ : null,
5301
+ issue_date: data.issue_date ? this.parseLocalDate(data.issue_date) : null,
5302
+ total_amount_cents: spec.totalAmountCents,
5303
+ is_recurring: isRecurring,
5304
+ recurrence_frequency: rule?.frequency ?? null,
5305
+ recurrence_end_date: rule?.end_date ? this.parseLocalDate(rule.end_date) : null,
5306
+ recurrence_group_id: spec.recurrenceGroupId,
5307
+ finance_category_id: data.finance_category_id,
5308
+ created_by_user_id: userId,
5301
5309
  },
5302
5310
  });
5303
5311
 
5304
- if (data.cost_center_id) {
5305
- await tx.installment_allocation.create({
5306
- data: {
5307
- installment_id: createdInstallment.id,
5308
- cost_center_id: data.cost_center_id,
5309
- allocated_amount_cents: amountCents,
5310
- },
5312
+ createdTitleIds.push(title.id);
5313
+
5314
+ if (attachmentFileIds.length > 0) {
5315
+ await tx.financial_title_attachment.createMany({
5316
+ data: attachmentFileIds.map((fileId) => ({
5317
+ title_id: title.id,
5318
+ file_id: fileId,
5319
+ uploaded_by_user_id: userId,
5320
+ })),
5311
5321
  });
5312
5322
  }
5313
5323
 
5314
- if (installment.paid) {
5315
- const appliedSettlement = await this.applyInstallmentSettlement(tx, {
5316
- title: { id: title.id, person_id: data.person_id },
5317
- titleType,
5318
- installmentId: createdInstallment.id,
5319
- amountCents: installment.paid_amount_cents ?? amountCents,
5320
- settledAt: this.parseLocalDate(
5321
- installment.paid_at || installment.due_date,
5322
- ),
5323
- bankAccountId: data.bank_account_id ?? null,
5324
- paymentChannel: data.payment_channel,
5325
- discountCents: installment.discount_cents,
5326
- interestCents: installment.interest_cents,
5327
- penaltyCents: installment.penalty_cents,
5328
- userId,
5324
+ for (const installment of spec.installments) {
5325
+ const amountCents = installment.amount_cents;
5326
+
5327
+ const createdInstallment = await tx.financial_installment.create({
5328
+ data: {
5329
+ title_id: title.id,
5330
+ installment_number: installment.installment_number,
5331
+ competence_date: data.competence_date
5332
+ ? this.parseLocalDate(data.competence_date)
5333
+ : this.parseLocalDate(installment.due_date),
5334
+ due_date: this.parseLocalDate(installment.due_date),
5335
+ amount_cents: amountCents,
5336
+ open_amount_cents: amountCents,
5337
+ status: this.resolveInstallmentStatus(
5338
+ amountCents,
5339
+ amountCents,
5340
+ this.parseLocalDate(installment.due_date),
5341
+ ),
5342
+ notes: data.description,
5343
+ },
5329
5344
  });
5330
5345
 
5331
- appliedSettlements.push(appliedSettlement);
5332
- }
5333
- }
5346
+ if (data.cost_center_id) {
5347
+ await tx.installment_allocation.create({
5348
+ data: {
5349
+ installment_id: createdInstallment.id,
5350
+ cost_center_id: data.cost_center_id,
5351
+ allocated_amount_cents: amountCents,
5352
+ },
5353
+ });
5354
+ }
5334
5355
 
5335
- const finalStatus =
5336
- appliedSettlements.length > 0
5337
- ? await this.recalculateTitleStatus(tx, title.id)
5338
- : 'draft';
5356
+ if (installment.paid) {
5357
+ const appliedSettlement = await this.applyInstallmentSettlement(tx, {
5358
+ title: { id: title.id, person_id: data.person_id },
5359
+ titleType,
5360
+ installmentId: createdInstallment.id,
5361
+ amountCents: installment.paid_amount_cents ?? amountCents,
5362
+ settledAt: this.parseLocalDate(
5363
+ installment.paid_at || installment.due_date,
5364
+ ),
5365
+ bankAccountId: data.bank_account_id ?? null,
5366
+ paymentChannel: data.payment_channel,
5367
+ discountCents: installment.discount_cents,
5368
+ interestCents: installment.interest_cents,
5369
+ penaltyCents: installment.penalty_cents,
5370
+ userId,
5371
+ });
5372
+
5373
+ appliedSettlements.push({ ...appliedSettlement, titleId: title.id });
5374
+ }
5375
+ }
5339
5376
 
5340
- await this.createAuditLog(tx, {
5341
- action: 'CREATE_TITLE',
5342
- entityTable: 'financial_title',
5343
- entityId: String(title.id),
5344
- actorUserId: userId,
5345
- summary: `Created ${titleType} title ${title.id} in ${finalStatus}`,
5346
- afterData: JSON.stringify({
5347
- status: finalStatus,
5348
- total_amount_cents: this.toCents(data.total_amount),
5349
- }),
5350
- });
5377
+ const titleSettlements = appliedSettlements.filter((s) => s.titleId === title.id);
5378
+ const finalStatus =
5379
+ titleSettlements.length > 0
5380
+ ? await this.recalculateTitleStatus(tx, title.id)
5381
+ : 'draft';
5351
5382
 
5352
- for (const appliedSettlement of appliedSettlements) {
5353
5383
  await this.createAuditLog(tx, {
5354
- action: 'SETTLE_INSTALLMENT',
5384
+ action: 'CREATE_TITLE',
5355
5385
  entityTable: 'financial_title',
5356
5386
  entityId: String(title.id),
5357
5387
  actorUserId: userId,
5358
- summary: `Settled installment ${appliedSettlement.installmentId} of title ${title.id}`,
5359
- beforeData: JSON.stringify({
5360
- title_status: 'draft',
5361
- installment_open_amount_cents:
5362
- appliedSettlement.openAmountCentsBefore,
5363
- }),
5388
+ summary: `Created ${titleType} title ${title.id} in ${finalStatus}`,
5364
5389
  afterData: JSON.stringify({
5365
- title_status: finalStatus,
5366
- installment_open_amount_cents:
5367
- appliedSettlement.openAmountCentsAfter,
5368
- settlement_id: appliedSettlement.settlementId,
5369
- bank_reconciliation_id: null,
5390
+ status: finalStatus,
5391
+ total_amount_cents: spec.totalAmountCents,
5370
5392
  }),
5371
5393
  });
5394
+
5395
+ for (const appliedSettlement of titleSettlements) {
5396
+ await this.createAuditLog(tx, {
5397
+ action: 'SETTLE_INSTALLMENT',
5398
+ entityTable: 'financial_title',
5399
+ entityId: String(title.id),
5400
+ actorUserId: userId,
5401
+ summary: `Settled installment ${appliedSettlement.installmentId} of title ${title.id}`,
5402
+ beforeData: JSON.stringify({
5403
+ title_status: 'draft',
5404
+ installment_open_amount_cents: appliedSettlement.openAmountCentsBefore,
5405
+ }),
5406
+ afterData: JSON.stringify({
5407
+ title_status: finalStatus,
5408
+ installment_open_amount_cents: appliedSettlement.openAmountCentsAfter,
5409
+ settlement_id: appliedSettlement.settlementId,
5410
+ bank_reconciliation_id: null,
5411
+ }),
5412
+ });
5413
+ }
5372
5414
  }
5373
5415
 
5374
5416
  return {
5375
- titleId: title.id,
5376
- settlementIds: appliedSettlements.map(
5377
- (appliedSettlement) => appliedSettlement.settlementId,
5378
- ),
5417
+ titleIds: createdTitleIds,
5418
+ settlementIds: appliedSettlements.map((s) => s.settlementId),
5379
5419
  };
5380
5420
  });
5381
5421
 
5382
5422
  const createdTitle = await this.getTitleById(
5383
- createResult.titleId,
5423
+ createResult.titleIds[0],
5384
5424
  titleType,
5385
5425
  locale,
5386
5426
  );
5387
- this.financeRealtime.publish({ domain: 'installment', type: 'created', entityId: createResult.titleId });
5427
+ this.financeRealtime.publish({ domain: 'installment', type: 'created', entityId: createResult.titleIds[0] });
5388
5428
  for (const settlementId of createResult.settlementIds) {
5389
5429
  this.financeRealtime.publish({ domain: 'settlement', type: 'settled', entityId: settlementId });
5390
5430
  }
@@ -5406,14 +5446,16 @@ export class FinanceService {
5406
5446
  }
5407
5447
 
5408
5448
  const isRecurring = Boolean(rule);
5449
+ // When editing a recurring title we only update this single occurrence
5450
+ // (one installment). Re-building the full series is out of scope for edits.
5409
5451
  const installments = isRecurring
5410
- ? this.buildRecurrenceInstallments(
5411
- data.due_date,
5412
- rule!.frequency,
5413
- this.toCents(data.total_amount),
5414
- rule!.end_date,
5415
- rule!.max_occurrences,
5416
- )
5452
+ ? [
5453
+ {
5454
+ installment_number: 1,
5455
+ due_date: data.due_date,
5456
+ amount_cents: this.toCents(data.total_amount),
5457
+ },
5458
+ ]
5417
5459
  : this.normalizeAndValidateInstallments(data, locale);
5418
5460
 
5419
5461
  const updatedTitle = await this.prisma.$transaction(async (tx) => {
@@ -5537,9 +5579,7 @@ export class FinanceService {
5537
5579
  }
5538
5580
  }
5539
5581
 
5540
- const totalAmountCents = isRecurring
5541
- ? this.toCents(data.total_amount) * installments.length
5542
- : this.toCents(data.total_amount);
5582
+ const totalAmountCents = this.toCents(data.total_amount);
5543
5583
 
5544
5584
  await tx.financial_title.update({
5545
5585
  where: { id: title.id },
@@ -5555,7 +5595,8 @@ export class FinanceService {
5555
5595
  is_recurring: isRecurring,
5556
5596
  recurrence_frequency: rule?.frequency ?? null,
5557
5597
  recurrence_end_date: rule?.end_date ? this.parseLocalDate(rule.end_date) : null,
5558
- finance_category_id: data.finance_category_id,
5598
+ // Preserve recurrence_group_id — editing a single occurrence must not
5599
+ // break the link to the rest of the series.
5559
5600
  },
5560
5601
  });
5561
5602
 
@@ -5703,6 +5744,17 @@ export class FinanceService {
5703
5744
  return installments;
5704
5745
  }
5705
5746
 
5747
+ private resolveRecurrenceDocumentNumber(
5748
+ base: string | null,
5749
+ mode: string,
5750
+ index: number,
5751
+ total: number,
5752
+ ): string | null {
5753
+ if (mode === 'none') return null;
5754
+ if (mode === 'sequence') return base ? `${base} ${index}/${total}` : `${index}/${total}`;
5755
+ return base; // 'same'
5756
+ }
5757
+
5706
5758
  private normalizeAndValidateInstallments(
5707
5759
  data: CreateFinancialTitleDto,
5708
5760
  locale: string,
@@ -7251,6 +7303,7 @@ export class FinanceService {
7251
7303
  recurrenceEndDate: title.recurrence_end_date
7252
7304
  ? title.recurrence_end_date.toISOString().slice(0, 10)
7253
7305
  : null,
7306
+ recurrenceGroupId: title.recurrence_group_id ?? null,
7254
7307
  ...(title.title_type === 'payable'
7255
7308
  ? {
7256
7309
  fornecedorId: String(title.person_id),
@@ -90,7 +90,7 @@ export class FinanceInstallmentsMcpTools {
90
90
  },
91
91
  recurrence_rule: {
92
92
  type: 'object',
93
- description: 'Recurrence rule. When provided, creates a recurring title (same amount each period) instead of a split installment title.',
93
+ description: 'Recurrence rule. When provided, each occurrence becomes an independent title (one installment each) linked by a recurrence_group_id.',
94
94
  properties: {
95
95
  frequency: {
96
96
  type: 'string',
@@ -99,6 +99,11 @@ export class FinanceInstallmentsMcpTools {
99
99
  },
100
100
  end_date: { type: 'string', description: 'End date (YYYY-MM-DD) — stop generating after this date' },
101
101
  max_occurrences: { type: 'number', description: 'Maximum number of repetitions' },
102
+ document_number_mode: {
103
+ type: 'string',
104
+ enum: ['same', 'sequence', 'none'],
105
+ description: 'How to set document_number on each title: same = copy as-is, sequence = append i/N suffix, none = leave blank (default: same)',
106
+ },
102
107
  },
103
108
  required: ['frequency'],
104
109
  },
@@ -358,7 +363,7 @@ export class FinanceInstallmentsMcpTools {
358
363
  },
359
364
  recurrence_rule: {
360
365
  type: 'object',
361
- description: 'Recurrence rule. When provided, creates a recurring title (same amount each period) instead of a split installment title.',
366
+ description: 'Recurrence rule. When provided, each occurrence becomes an independent title (one installment each) linked by a recurrence_group_id.',
362
367
  properties: {
363
368
  frequency: {
364
369
  type: 'string',
@@ -367,6 +372,11 @@ export class FinanceInstallmentsMcpTools {
367
372
  },
368
373
  end_date: { type: 'string', description: 'End date (YYYY-MM-DD) — stop generating after this date' },
369
374
  max_occurrences: { type: 'number', description: 'Maximum number of repetitions' },
375
+ document_number_mode: {
376
+ type: 'string',
377
+ enum: ['same', 'sequence', 'none'],
378
+ description: 'How to set document_number on each title: same = copy as-is, sequence = append i/N suffix, none = leave blank (default: same)',
379
+ },
370
380
  },
371
381
  required: ['frequency'],
372
382
  },