@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
@@ -3606,6 +3606,7 @@ let FinanceService = FinanceService_1 = class FinanceService {
3606
3606
  return title;
3607
3607
  }
3608
3608
  async createTitle(data, titleType, locale, userId) {
3609
+ var _a, _b;
3609
3610
  const rule = data.recurrence_rule;
3610
3611
  if (rule && !rule.end_date && !rule.max_occurrences) {
3611
3612
  throw new common_1.BadRequestException((0, api_locale_1.getLocaleText)('recurrenceEndOrCountRequired', locale, 'Provide end_date or max_occurrences for recurring titles'));
@@ -3614,12 +3615,42 @@ let FinanceService = FinanceService_1 = class FinanceService {
3614
3615
  throw new common_1.BadRequestException((0, api_locale_1.getLocaleText)('recurringCannotBePaid', locale, 'Recurring titles cannot be created as already paid'));
3615
3616
  }
3616
3617
  const isRecurring = Boolean(rule);
3617
- const installments = isRecurring
3618
- ? this.buildRecurrenceInstallments(data.due_date, rule.frequency, this.toCents(data.total_amount), rule.end_date, rule.max_occurrences)
3619
- : this.normalizeAndValidateInstallments(data, locale);
3620
- const hasPaidInstallments = installments.some((installment) => installment.paid);
3618
+ // Recurring titles are NOT installment titles: each occurrence becomes an
3619
+ // independent title (with a single installment), all linked by a shared
3620
+ // recurrence_group_id. Installment titles remain a single title with many
3621
+ // installments.
3622
+ let titleSpecs;
3623
+ if (isRecurring) {
3624
+ const occurrences = this.buildRecurrenceInstallments(data.due_date, rule.frequency, this.toCents(data.total_amount), rule.end_date, rule.max_occurrences);
3625
+ const recurrenceGroupId = (0, node_crypto_1.randomUUID)();
3626
+ const baseDocument = ((_a = data.document_number) === null || _a === void 0 ? void 0 : _a.trim()) || null;
3627
+ const documentMode = rule.document_number_mode || 'same';
3628
+ titleSpecs = occurrences.map((occurrence, index) => ({
3629
+ documentNumber: this.resolveRecurrenceDocumentNumber(baseDocument, documentMode, index + 1, occurrences.length),
3630
+ recurrenceGroupId,
3631
+ totalAmountCents: occurrence.amount_cents,
3632
+ installments: [
3633
+ {
3634
+ installment_number: 1,
3635
+ due_date: occurrence.due_date,
3636
+ amount_cents: occurrence.amount_cents,
3637
+ },
3638
+ ],
3639
+ }));
3640
+ }
3641
+ else {
3642
+ titleSpecs = [
3643
+ {
3644
+ documentNumber: ((_b = data.document_number) === null || _b === void 0 ? void 0 : _b.trim()) || null,
3645
+ recurrenceGroupId: null,
3646
+ totalAmountCents: this.toCents(data.total_amount),
3647
+ installments: this.normalizeAndValidateInstallments(data, locale),
3648
+ },
3649
+ ];
3650
+ }
3651
+ const hasPaidInstallments = titleSpecs.some((spec) => spec.installments.some((installment) => installment.paid));
3621
3652
  const createResult = await this.prisma.$transaction(async (tx) => {
3622
- var _a, _b, _c, _d;
3653
+ var _a, _b, _c;
3623
3654
  const person = await tx.person.findUnique({
3624
3655
  where: { id: data.person_id },
3625
3656
  select: { id: true },
@@ -3674,122 +3705,124 @@ let FinanceService = FinanceService_1 = class FinanceService {
3674
3705
  }
3675
3706
  await this.assertDateNotInClosedPeriod(tx, data.competence_date
3676
3707
  ? this.parseLocalDate(data.competence_date)
3677
- : this.parseLocalDate(installments[0].due_date), 'create title');
3678
- const totalAmountCents = isRecurring
3679
- ? this.toCents(data.total_amount) * installments.length
3680
- : this.toCents(data.total_amount);
3681
- const title = await tx.financial_title.create({
3682
- data: {
3683
- person_id: data.person_id,
3684
- title_type: titleType,
3685
- status: 'draft',
3686
- document_number: ((_a = data.document_number) === null || _a === void 0 ? void 0 : _a.trim()) || null,
3687
- description: data.description,
3688
- competence_date: data.competence_date
3689
- ? this.parseLocalDate(data.competence_date)
3690
- : null,
3691
- issue_date: data.issue_date ? this.parseLocalDate(data.issue_date) : null,
3692
- total_amount_cents: totalAmountCents,
3693
- is_recurring: isRecurring,
3694
- recurrence_frequency: (_b = rule === null || rule === void 0 ? void 0 : rule.frequency) !== null && _b !== void 0 ? _b : null,
3695
- recurrence_end_date: (rule === null || rule === void 0 ? void 0 : rule.end_date) ? this.parseLocalDate(rule.end_date) : null,
3696
- finance_category_id: data.finance_category_id,
3697
- created_by_user_id: userId,
3698
- },
3699
- });
3700
- if (attachmentFileIds.length > 0) {
3701
- await tx.financial_title_attachment.createMany({
3702
- data: attachmentFileIds.map((fileId) => ({
3703
- title_id: title.id,
3704
- file_id: fileId,
3705
- uploaded_by_user_id: userId,
3706
- })),
3707
- });
3708
- }
3708
+ : this.parseLocalDate(titleSpecs[0].installments[0].due_date), 'create title');
3709
+ const createdTitleIds = [];
3709
3710
  const appliedSettlements = [];
3710
- for (let index = 0; index < installments.length; index++) {
3711
- const installment = installments[index];
3712
- const amountCents = installment.amount_cents;
3713
- const createdInstallment = await tx.financial_installment.create({
3711
+ for (const spec of titleSpecs) {
3712
+ const title = await tx.financial_title.create({
3714
3713
  data: {
3715
- title_id: title.id,
3716
- installment_number: installment.installment_number,
3714
+ person_id: data.person_id,
3715
+ title_type: titleType,
3716
+ status: 'draft',
3717
+ document_number: spec.documentNumber,
3718
+ description: data.description,
3717
3719
  competence_date: data.competence_date
3718
3720
  ? this.parseLocalDate(data.competence_date)
3719
- : this.parseLocalDate(installment.due_date),
3720
- due_date: this.parseLocalDate(installment.due_date),
3721
- amount_cents: amountCents,
3722
- open_amount_cents: amountCents,
3723
- status: this.resolveInstallmentStatus(amountCents, amountCents, this.parseLocalDate(installment.due_date)),
3724
- notes: data.description,
3721
+ : null,
3722
+ issue_date: data.issue_date ? this.parseLocalDate(data.issue_date) : null,
3723
+ total_amount_cents: spec.totalAmountCents,
3724
+ is_recurring: isRecurring,
3725
+ recurrence_frequency: (_a = rule === null || rule === void 0 ? void 0 : rule.frequency) !== null && _a !== void 0 ? _a : null,
3726
+ recurrence_end_date: (rule === null || rule === void 0 ? void 0 : rule.end_date) ? this.parseLocalDate(rule.end_date) : null,
3727
+ recurrence_group_id: spec.recurrenceGroupId,
3728
+ finance_category_id: data.finance_category_id,
3729
+ created_by_user_id: userId,
3725
3730
  },
3726
3731
  });
3727
- if (data.cost_center_id) {
3728
- await tx.installment_allocation.create({
3729
- data: {
3730
- installment_id: createdInstallment.id,
3731
- cost_center_id: data.cost_center_id,
3732
- allocated_amount_cents: amountCents,
3733
- },
3732
+ createdTitleIds.push(title.id);
3733
+ if (attachmentFileIds.length > 0) {
3734
+ await tx.financial_title_attachment.createMany({
3735
+ data: attachmentFileIds.map((fileId) => ({
3736
+ title_id: title.id,
3737
+ file_id: fileId,
3738
+ uploaded_by_user_id: userId,
3739
+ })),
3734
3740
  });
3735
3741
  }
3736
- if (installment.paid) {
3737
- const appliedSettlement = await this.applyInstallmentSettlement(tx, {
3738
- title: { id: title.id, person_id: data.person_id },
3739
- titleType,
3740
- installmentId: createdInstallment.id,
3741
- amountCents: (_c = installment.paid_amount_cents) !== null && _c !== void 0 ? _c : amountCents,
3742
- settledAt: this.parseLocalDate(installment.paid_at || installment.due_date),
3743
- bankAccountId: (_d = data.bank_account_id) !== null && _d !== void 0 ? _d : null,
3744
- paymentChannel: data.payment_channel,
3745
- discountCents: installment.discount_cents,
3746
- interestCents: installment.interest_cents,
3747
- penaltyCents: installment.penalty_cents,
3748
- userId,
3742
+ for (const installment of spec.installments) {
3743
+ const amountCents = installment.amount_cents;
3744
+ const createdInstallment = await tx.financial_installment.create({
3745
+ data: {
3746
+ title_id: title.id,
3747
+ installment_number: installment.installment_number,
3748
+ competence_date: data.competence_date
3749
+ ? this.parseLocalDate(data.competence_date)
3750
+ : this.parseLocalDate(installment.due_date),
3751
+ due_date: this.parseLocalDate(installment.due_date),
3752
+ amount_cents: amountCents,
3753
+ open_amount_cents: amountCents,
3754
+ status: this.resolveInstallmentStatus(amountCents, amountCents, this.parseLocalDate(installment.due_date)),
3755
+ notes: data.description,
3756
+ },
3749
3757
  });
3750
- appliedSettlements.push(appliedSettlement);
3758
+ if (data.cost_center_id) {
3759
+ await tx.installment_allocation.create({
3760
+ data: {
3761
+ installment_id: createdInstallment.id,
3762
+ cost_center_id: data.cost_center_id,
3763
+ allocated_amount_cents: amountCents,
3764
+ },
3765
+ });
3766
+ }
3767
+ if (installment.paid) {
3768
+ const appliedSettlement = await this.applyInstallmentSettlement(tx, {
3769
+ title: { id: title.id, person_id: data.person_id },
3770
+ titleType,
3771
+ installmentId: createdInstallment.id,
3772
+ amountCents: (_b = installment.paid_amount_cents) !== null && _b !== void 0 ? _b : amountCents,
3773
+ settledAt: this.parseLocalDate(installment.paid_at || installment.due_date),
3774
+ bankAccountId: (_c = data.bank_account_id) !== null && _c !== void 0 ? _c : null,
3775
+ paymentChannel: data.payment_channel,
3776
+ discountCents: installment.discount_cents,
3777
+ interestCents: installment.interest_cents,
3778
+ penaltyCents: installment.penalty_cents,
3779
+ userId,
3780
+ });
3781
+ appliedSettlements.push(Object.assign(Object.assign({}, appliedSettlement), { titleId: title.id }));
3782
+ }
3751
3783
  }
3752
- }
3753
- const finalStatus = appliedSettlements.length > 0
3754
- ? await this.recalculateTitleStatus(tx, title.id)
3755
- : 'draft';
3756
- await this.createAuditLog(tx, {
3757
- action: 'CREATE_TITLE',
3758
- entityTable: 'financial_title',
3759
- entityId: String(title.id),
3760
- actorUserId: userId,
3761
- summary: `Created ${titleType} title ${title.id} in ${finalStatus}`,
3762
- afterData: JSON.stringify({
3763
- status: finalStatus,
3764
- total_amount_cents: this.toCents(data.total_amount),
3765
- }),
3766
- });
3767
- for (const appliedSettlement of appliedSettlements) {
3784
+ const titleSettlements = appliedSettlements.filter((s) => s.titleId === title.id);
3785
+ const finalStatus = titleSettlements.length > 0
3786
+ ? await this.recalculateTitleStatus(tx, title.id)
3787
+ : 'draft';
3768
3788
  await this.createAuditLog(tx, {
3769
- action: 'SETTLE_INSTALLMENT',
3789
+ action: 'CREATE_TITLE',
3770
3790
  entityTable: 'financial_title',
3771
3791
  entityId: String(title.id),
3772
3792
  actorUserId: userId,
3773
- summary: `Settled installment ${appliedSettlement.installmentId} of title ${title.id}`,
3774
- beforeData: JSON.stringify({
3775
- title_status: 'draft',
3776
- installment_open_amount_cents: appliedSettlement.openAmountCentsBefore,
3777
- }),
3793
+ summary: `Created ${titleType} title ${title.id} in ${finalStatus}`,
3778
3794
  afterData: JSON.stringify({
3779
- title_status: finalStatus,
3780
- installment_open_amount_cents: appliedSettlement.openAmountCentsAfter,
3781
- settlement_id: appliedSettlement.settlementId,
3782
- bank_reconciliation_id: null,
3795
+ status: finalStatus,
3796
+ total_amount_cents: spec.totalAmountCents,
3783
3797
  }),
3784
3798
  });
3799
+ for (const appliedSettlement of titleSettlements) {
3800
+ await this.createAuditLog(tx, {
3801
+ action: 'SETTLE_INSTALLMENT',
3802
+ entityTable: 'financial_title',
3803
+ entityId: String(title.id),
3804
+ actorUserId: userId,
3805
+ summary: `Settled installment ${appliedSettlement.installmentId} of title ${title.id}`,
3806
+ beforeData: JSON.stringify({
3807
+ title_status: 'draft',
3808
+ installment_open_amount_cents: appliedSettlement.openAmountCentsBefore,
3809
+ }),
3810
+ afterData: JSON.stringify({
3811
+ title_status: finalStatus,
3812
+ installment_open_amount_cents: appliedSettlement.openAmountCentsAfter,
3813
+ settlement_id: appliedSettlement.settlementId,
3814
+ bank_reconciliation_id: null,
3815
+ }),
3816
+ });
3817
+ }
3785
3818
  }
3786
3819
  return {
3787
- titleId: title.id,
3788
- settlementIds: appliedSettlements.map((appliedSettlement) => appliedSettlement.settlementId),
3820
+ titleIds: createdTitleIds,
3821
+ settlementIds: appliedSettlements.map((s) => s.settlementId),
3789
3822
  };
3790
3823
  });
3791
- const createdTitle = await this.getTitleById(createResult.titleId, titleType, locale);
3792
- this.financeRealtime.publish({ domain: 'installment', type: 'created', entityId: createResult.titleId });
3824
+ const createdTitle = await this.getTitleById(createResult.titleIds[0], titleType, locale);
3825
+ this.financeRealtime.publish({ domain: 'installment', type: 'created', entityId: createResult.titleIds[0] });
3793
3826
  for (const settlementId of createResult.settlementIds) {
3794
3827
  this.financeRealtime.publish({ domain: 'settlement', type: 'settled', entityId: settlementId });
3795
3828
  }
@@ -3801,8 +3834,16 @@ let FinanceService = FinanceService_1 = class FinanceService {
3801
3834
  throw new common_1.BadRequestException((0, api_locale_1.getLocaleText)('recurrenceEndOrCountRequired', locale, 'Provide end_date or max_occurrences for recurring titles'));
3802
3835
  }
3803
3836
  const isRecurring = Boolean(rule);
3837
+ // When editing a recurring title we only update this single occurrence
3838
+ // (one installment). Re-building the full series is out of scope for edits.
3804
3839
  const installments = isRecurring
3805
- ? this.buildRecurrenceInstallments(data.due_date, rule.frequency, this.toCents(data.total_amount), rule.end_date, rule.max_occurrences)
3840
+ ? [
3841
+ {
3842
+ installment_number: 1,
3843
+ due_date: data.due_date,
3844
+ amount_cents: this.toCents(data.total_amount),
3845
+ },
3846
+ ]
3806
3847
  : this.normalizeAndValidateInstallments(data, locale);
3807
3848
  const updatedTitle = await this.prisma.$transaction(async (tx) => {
3808
3849
  var _a, _b;
@@ -3887,9 +3928,7 @@ let FinanceService = FinanceService_1 = class FinanceService {
3887
3928
  throw new common_1.BadRequestException('Cannot edit title with settled installments');
3888
3929
  }
3889
3930
  }
3890
- const totalAmountCents = isRecurring
3891
- ? this.toCents(data.total_amount) * installments.length
3892
- : this.toCents(data.total_amount);
3931
+ const totalAmountCents = this.toCents(data.total_amount);
3893
3932
  await tx.financial_title.update({
3894
3933
  where: { id: title.id },
3895
3934
  data: {
@@ -3904,7 +3943,8 @@ let FinanceService = FinanceService_1 = class FinanceService {
3904
3943
  is_recurring: isRecurring,
3905
3944
  recurrence_frequency: (_b = rule === null || rule === void 0 ? void 0 : rule.frequency) !== null && _b !== void 0 ? _b : null,
3906
3945
  recurrence_end_date: (rule === null || rule === void 0 ? void 0 : rule.end_date) ? this.parseLocalDate(rule.end_date) : null,
3907
- finance_category_id: data.finance_category_id,
3946
+ // Preserve recurrence_group_id — editing a single occurrence must not
3947
+ // break the link to the rest of the series.
3908
3948
  },
3909
3949
  });
3910
3950
  if (installmentIds.length > 0) {
@@ -4038,6 +4078,13 @@ let FinanceService = FinanceService_1 = class FinanceService {
4038
4078
  }
4039
4079
  return installments;
4040
4080
  }
4081
+ resolveRecurrenceDocumentNumber(base, mode, index, total) {
4082
+ if (mode === 'none')
4083
+ return null;
4084
+ if (mode === 'sequence')
4085
+ return base ? `${base} ${index}/${total}` : `${index}/${total}`;
4086
+ return base; // 'same'
4087
+ }
4041
4088
  normalizeAndValidateInstallments(data, locale) {
4042
4089
  const fallbackDueDate = data.due_date;
4043
4090
  const totalAmountCents = this.toCents(data.total_amount);
@@ -5054,7 +5101,7 @@ let FinanceService = FinanceService_1 = class FinanceService {
5054
5101
  });
5055
5102
  }
5056
5103
  mapTitleToFront(title, paymentChannelOverride) {
5057
- var _a, _b, _c, _d, _e;
5104
+ var _a, _b, _c, _d, _e, _f;
5058
5105
  const allocations = title.financial_installment.flatMap((installment) => installment.installment_allocation);
5059
5106
  const firstCostCenter = (_a = allocations[0]) === null || _a === void 0 ? void 0 : _a.cost_center_id;
5060
5107
  const tags = [
@@ -5116,14 +5163,14 @@ let FinanceService = FinanceService_1 = class FinanceService {
5116
5163
  paymentChannelOverride ||
5117
5164
  'transferencia', isRecurring: (_b = title.is_recurring) !== null && _b !== void 0 ? _b : false, recurrenceFrequency: (_c = title.recurrence_frequency) !== null && _c !== void 0 ? _c : null, recurrenceEndDate: title.recurrence_end_date
5118
5165
  ? title.recurrence_end_date.toISOString().slice(0, 10)
5119
- : null }, (title.title_type === 'payable'
5166
+ : null, recurrenceGroupId: (_d = title.recurrence_group_id) !== null && _d !== void 0 ? _d : null }, (title.title_type === 'payable'
5120
5167
  ? {
5121
5168
  fornecedorId: String(title.person_id),
5122
- fornecedor: ((_d = title.person) === null || _d === void 0 ? void 0 : _d.name) || '',
5169
+ fornecedor: ((_e = title.person) === null || _e === void 0 ? void 0 : _e.name) || '',
5123
5170
  }
5124
5171
  : {
5125
5172
  clienteId: String(title.person_id),
5126
- cliente: ((_e = title.person) === null || _e === void 0 ? void 0 : _e.name) || '',
5173
+ cliente: ((_f = title.person) === null || _f === void 0 ? void 0 : _f.name) || '',
5127
5174
  }));
5128
5175
  }
5129
5176
  defaultTitleInclude() {