@hed-hog/finance 0.0.257 → 0.0.261
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.
- package/dist/dto/create-bank-statement-adjustment.dto.d.ts +8 -0
- package/dist/dto/create-bank-statement-adjustment.dto.d.ts.map +1 -0
- package/dist/dto/create-bank-statement-adjustment.dto.js +50 -0
- package/dist/dto/create-bank-statement-adjustment.dto.js.map +1 -0
- package/dist/dto/create-transfer.dto.d.ts +8 -0
- package/dist/dto/create-transfer.dto.d.ts.map +1 -0
- package/dist/dto/create-transfer.dto.js +52 -0
- package/dist/dto/create-transfer.dto.js.map +1 -0
- package/dist/dto/register-collection-agreement.dto.d.ts +7 -0
- package/dist/dto/register-collection-agreement.dto.d.ts.map +1 -0
- package/dist/dto/register-collection-agreement.dto.js +37 -0
- package/dist/dto/register-collection-agreement.dto.js.map +1 -0
- package/dist/dto/send-collection.dto.d.ts +5 -0
- package/dist/dto/send-collection.dto.d.ts.map +1 -0
- package/dist/dto/send-collection.dto.js +29 -0
- package/dist/dto/send-collection.dto.js.map +1 -0
- package/dist/dto/settle-installment.dto.d.ts +1 -0
- package/dist/dto/settle-installment.dto.d.ts.map +1 -1
- package/dist/dto/settle-installment.dto.js +6 -0
- package/dist/dto/settle-installment.dto.js.map +1 -1
- package/dist/finance-collections.controller.d.ts +35 -0
- package/dist/finance-collections.controller.d.ts.map +1 -0
- package/dist/finance-collections.controller.js +65 -0
- package/dist/finance-collections.controller.js.map +1 -0
- package/dist/finance-data.controller.d.ts +4 -0
- package/dist/finance-data.controller.d.ts.map +1 -1
- package/dist/finance-installments.controller.d.ts +44 -0
- package/dist/finance-installments.controller.d.ts.map +1 -1
- package/dist/finance-statements.controller.d.ts +16 -2
- package/dist/finance-statements.controller.d.ts.map +1 -1
- package/dist/finance-statements.controller.js +34 -6
- package/dist/finance-statements.controller.js.map +1 -1
- package/dist/finance-transfers.controller.d.ts +23 -0
- package/dist/finance-transfers.controller.d.ts.map +1 -0
- package/dist/finance-transfers.controller.js +56 -0
- package/dist/finance-transfers.controller.js.map +1 -0
- package/dist/finance.module.d.ts.map +1 -1
- package/dist/finance.module.js +4 -0
- package/dist/finance.module.js.map +1 -1
- package/dist/finance.service.d.ts +115 -2
- package/dist/finance.service.d.ts.map +1 -1
- package/dist/finance.service.js +632 -8
- package/dist/finance.service.js.map +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/hedhog/data/route.yaml +63 -0
- package/hedhog/frontend/app/accounts-receivable/collections-default/page.tsx.ejs +643 -440
- package/hedhog/frontend/app/cash-and-banks/bank-reconciliation/page.tsx.ejs +825 -477
- package/hedhog/frontend/app/cash-and-banks/statements/page.tsx.ejs +367 -43
- package/hedhog/frontend/app/cash-and-banks/transfers/page.tsx.ejs +315 -75
- package/package.json +4 -4
- package/src/dto/create-bank-statement-adjustment.dto.ts +38 -0
- package/src/dto/create-transfer.dto.ts +46 -0
- package/src/dto/register-collection-agreement.dto.ts +27 -0
- package/src/dto/send-collection.dto.ts +14 -0
- package/src/dto/settle-installment.dto.ts +5 -0
- package/src/finance-collections.controller.ts +34 -0
- package/src/finance-statements.controller.ts +29 -1
- package/src/finance-transfers.controller.ts +26 -0
- package/src/finance.module.ts +4 -0
- package/src/finance.service.ts +775 -5
- package/src/index.ts +2 -0
package/dist/finance.service.js
CHANGED
|
@@ -587,6 +587,242 @@ let FinanceService = FinanceService_1 = class FinanceService {
|
|
|
587
587
|
periodoAberto: openPeriod,
|
|
588
588
|
};
|
|
589
589
|
}
|
|
590
|
+
async getAccountsReceivableCollectionsDefault() {
|
|
591
|
+
var _a;
|
|
592
|
+
const today = this.startOfDay(new Date());
|
|
593
|
+
const overdueInstallments = await this.prisma.financial_installment.findMany({
|
|
594
|
+
where: {
|
|
595
|
+
open_amount_cents: {
|
|
596
|
+
gt: 0,
|
|
597
|
+
},
|
|
598
|
+
due_date: {
|
|
599
|
+
lt: today,
|
|
600
|
+
},
|
|
601
|
+
financial_title: {
|
|
602
|
+
title_type: 'receivable',
|
|
603
|
+
},
|
|
604
|
+
},
|
|
605
|
+
select: {
|
|
606
|
+
due_date: true,
|
|
607
|
+
open_amount_cents: true,
|
|
608
|
+
amount_cents: true,
|
|
609
|
+
financial_title: {
|
|
610
|
+
select: {
|
|
611
|
+
person_id: true,
|
|
612
|
+
person: {
|
|
613
|
+
select: {
|
|
614
|
+
name: true,
|
|
615
|
+
},
|
|
616
|
+
},
|
|
617
|
+
},
|
|
618
|
+
},
|
|
619
|
+
},
|
|
620
|
+
});
|
|
621
|
+
const byCustomer = new Map();
|
|
622
|
+
for (const installment of overdueInstallments) {
|
|
623
|
+
const personId = installment.financial_title.person_id;
|
|
624
|
+
const personName = ((_a = installment.financial_title.person) === null || _a === void 0 ? void 0 : _a.name) || `Cliente ${personId}`;
|
|
625
|
+
const openAmount = installment.open_amount_cents > 0
|
|
626
|
+
? installment.open_amount_cents
|
|
627
|
+
: installment.amount_cents;
|
|
628
|
+
const amount = this.fromCents(openAmount);
|
|
629
|
+
const diffDays = Math.floor((today.getTime() - this.startOfDay(installment.due_date).getTime()) /
|
|
630
|
+
86400000);
|
|
631
|
+
if (!byCustomer.has(personId)) {
|
|
632
|
+
byCustomer.set(personId, {
|
|
633
|
+
clienteId: String(personId),
|
|
634
|
+
cliente: personName,
|
|
635
|
+
bucket0_30: 0,
|
|
636
|
+
bucket31_60: 0,
|
|
637
|
+
bucket61_90: 0,
|
|
638
|
+
bucket90plus: 0,
|
|
639
|
+
total: 0,
|
|
640
|
+
});
|
|
641
|
+
}
|
|
642
|
+
const customer = byCustomer.get(personId);
|
|
643
|
+
if (diffDays <= 30) {
|
|
644
|
+
customer.bucket0_30 += amount;
|
|
645
|
+
}
|
|
646
|
+
else if (diffDays <= 60) {
|
|
647
|
+
customer.bucket31_60 += amount;
|
|
648
|
+
}
|
|
649
|
+
else if (diffDays <= 90) {
|
|
650
|
+
customer.bucket61_90 += amount;
|
|
651
|
+
}
|
|
652
|
+
else {
|
|
653
|
+
customer.bucket90plus += amount;
|
|
654
|
+
}
|
|
655
|
+
customer.total += amount;
|
|
656
|
+
}
|
|
657
|
+
const agingInadimplencia = Array.from(byCustomer.values())
|
|
658
|
+
.map((item) => (Object.assign(Object.assign({}, item), { bucket0_30: Number(item.bucket0_30.toFixed(2)), bucket31_60: Number(item.bucket31_60.toFixed(2)), bucket61_90: Number(item.bucket61_90.toFixed(2)), bucket90plus: Number(item.bucket90plus.toFixed(2)), total: Number(item.total.toFixed(2)) })))
|
|
659
|
+
.sort((a, b) => b.total - a.total);
|
|
660
|
+
const contactHistory = await this.prisma.audit_log.findMany({
|
|
661
|
+
where: {
|
|
662
|
+
entity_table: {
|
|
663
|
+
in: ['financial_collection_contact', 'financial_collection_agreement'],
|
|
664
|
+
},
|
|
665
|
+
},
|
|
666
|
+
include: {
|
|
667
|
+
user: {
|
|
668
|
+
select: {
|
|
669
|
+
name: true,
|
|
670
|
+
},
|
|
671
|
+
},
|
|
672
|
+
},
|
|
673
|
+
orderBy: {
|
|
674
|
+
created_at: 'desc',
|
|
675
|
+
},
|
|
676
|
+
take: 500,
|
|
677
|
+
});
|
|
678
|
+
const historicoContatos = contactHistory.map((log) => {
|
|
679
|
+
var _a;
|
|
680
|
+
const afterData = this.parseAiJson(log.after_data || '{}');
|
|
681
|
+
return {
|
|
682
|
+
clienteId: log.entity_id,
|
|
683
|
+
tipo: this.mapCollectionActionToType(log.action, afterData === null || afterData === void 0 ? void 0 : afterData.channel),
|
|
684
|
+
data: log.created_at.toISOString(),
|
|
685
|
+
descricao: log.summary || '',
|
|
686
|
+
responsavel: ((_a = log.user) === null || _a === void 0 ? void 0 : _a.name) || 'Sistema',
|
|
687
|
+
};
|
|
688
|
+
});
|
|
689
|
+
return {
|
|
690
|
+
agingInadimplencia,
|
|
691
|
+
historicoContatos,
|
|
692
|
+
};
|
|
693
|
+
}
|
|
694
|
+
async sendCollection(personId, data, actorUserId) {
|
|
695
|
+
const person = await this.prisma.person.findUnique({
|
|
696
|
+
where: {
|
|
697
|
+
id: personId,
|
|
698
|
+
},
|
|
699
|
+
select: {
|
|
700
|
+
id: true,
|
|
701
|
+
name: true,
|
|
702
|
+
},
|
|
703
|
+
});
|
|
704
|
+
if (!person) {
|
|
705
|
+
throw new common_1.NotFoundException('Person not found');
|
|
706
|
+
}
|
|
707
|
+
const log = await this.prisma.audit_log.create({
|
|
708
|
+
data: {
|
|
709
|
+
actor_user_id: actorUserId || null,
|
|
710
|
+
action: 'collection_sent',
|
|
711
|
+
entity_table: 'financial_collection_contact',
|
|
712
|
+
entity_id: String(person.id),
|
|
713
|
+
summary: `Cobrança enviada via e-mail para ${person.name}`,
|
|
714
|
+
after_data: JSON.stringify({
|
|
715
|
+
message: data.message,
|
|
716
|
+
subject: data.subject || null,
|
|
717
|
+
}),
|
|
718
|
+
},
|
|
719
|
+
});
|
|
720
|
+
return {
|
|
721
|
+
id: String(log.id),
|
|
722
|
+
success: true,
|
|
723
|
+
};
|
|
724
|
+
}
|
|
725
|
+
async registerCollectionAgreement(personId, data, actorUserId) {
|
|
726
|
+
const person = await this.prisma.person.findUnique({
|
|
727
|
+
where: {
|
|
728
|
+
id: personId,
|
|
729
|
+
},
|
|
730
|
+
select: {
|
|
731
|
+
id: true,
|
|
732
|
+
name: true,
|
|
733
|
+
},
|
|
734
|
+
});
|
|
735
|
+
if (!person) {
|
|
736
|
+
throw new common_1.NotFoundException('Person not found');
|
|
737
|
+
}
|
|
738
|
+
const firstDueDate = new Date(data.first_due_date);
|
|
739
|
+
if (Number.isNaN(firstDueDate.getTime())) {
|
|
740
|
+
throw new common_1.BadRequestException('Invalid first due date');
|
|
741
|
+
}
|
|
742
|
+
const totalAmountCents = this.toCents(Number(data.amount));
|
|
743
|
+
if (totalAmountCents <= 0) {
|
|
744
|
+
throw new common_1.BadRequestException('Invalid agreement amount');
|
|
745
|
+
}
|
|
746
|
+
const baseInstallmentCents = Math.floor(totalAmountCents / data.installments);
|
|
747
|
+
const remainder = totalAmountCents % data.installments;
|
|
748
|
+
const created = await this.prisma.$transaction(async (tx) => {
|
|
749
|
+
const title = await tx.financial_title.create({
|
|
750
|
+
data: {
|
|
751
|
+
person_id: person.id,
|
|
752
|
+
title_type: 'receivable',
|
|
753
|
+
status: 'open',
|
|
754
|
+
document_number: `ACD-${Date.now()}`,
|
|
755
|
+
description: data.notes || 'Acordo de cobrança',
|
|
756
|
+
competence_date: firstDueDate,
|
|
757
|
+
issue_date: new Date(),
|
|
758
|
+
total_amount_cents: totalAmountCents,
|
|
759
|
+
created_by_user_id: actorUserId || null,
|
|
760
|
+
},
|
|
761
|
+
select: {
|
|
762
|
+
id: true,
|
|
763
|
+
},
|
|
764
|
+
});
|
|
765
|
+
for (let index = 0; index < data.installments; index += 1) {
|
|
766
|
+
const dueDate = this.addMonths(firstDueDate, index);
|
|
767
|
+
const amountCents = baseInstallmentCents + (index === data.installments - 1 ? remainder : 0);
|
|
768
|
+
await tx.financial_installment.create({
|
|
769
|
+
data: {
|
|
770
|
+
title_id: title.id,
|
|
771
|
+
installment_number: index + 1,
|
|
772
|
+
competence_date: dueDate,
|
|
773
|
+
due_date: dueDate,
|
|
774
|
+
amount_cents: amountCents,
|
|
775
|
+
open_amount_cents: amountCents,
|
|
776
|
+
status: this.resolveInstallmentStatus(amountCents, amountCents, dueDate, 'open'),
|
|
777
|
+
notes: data.notes || null,
|
|
778
|
+
},
|
|
779
|
+
});
|
|
780
|
+
}
|
|
781
|
+
const log = await tx.audit_log.create({
|
|
782
|
+
data: {
|
|
783
|
+
actor_user_id: actorUserId || null,
|
|
784
|
+
action: 'collection_agreement_registered',
|
|
785
|
+
entity_table: 'financial_collection_agreement',
|
|
786
|
+
entity_id: String(person.id),
|
|
787
|
+
summary: `Acordo registrado para ${person.name}: ${data.installments}x`,
|
|
788
|
+
after_data: JSON.stringify({
|
|
789
|
+
title_id: title.id,
|
|
790
|
+
installments: data.installments,
|
|
791
|
+
amount: data.amount,
|
|
792
|
+
first_due_date: data.first_due_date,
|
|
793
|
+
notes: data.notes || null,
|
|
794
|
+
}),
|
|
795
|
+
},
|
|
796
|
+
select: {
|
|
797
|
+
id: true,
|
|
798
|
+
},
|
|
799
|
+
});
|
|
800
|
+
return {
|
|
801
|
+
titleId: title.id,
|
|
802
|
+
auditLogId: log.id,
|
|
803
|
+
};
|
|
804
|
+
});
|
|
805
|
+
return {
|
|
806
|
+
success: true,
|
|
807
|
+
titleId: String(created.titleId),
|
|
808
|
+
auditLogId: String(created.auditLogId),
|
|
809
|
+
};
|
|
810
|
+
}
|
|
811
|
+
mapCollectionActionToType(action, channel) {
|
|
812
|
+
if (action === 'collection_agreement_registered') {
|
|
813
|
+
return 'Acordo';
|
|
814
|
+
}
|
|
815
|
+
if (channel) {
|
|
816
|
+
const normalized = String(channel).toLowerCase();
|
|
817
|
+
if (normalized === 'email')
|
|
818
|
+
return 'E-mail';
|
|
819
|
+
if (normalized === 'whatsapp')
|
|
820
|
+
return 'WhatsApp';
|
|
821
|
+
if (normalized === 'sms')
|
|
822
|
+
return 'SMS';
|
|
823
|
+
}
|
|
824
|
+
return 'Contato';
|
|
825
|
+
}
|
|
590
826
|
calculateDashboardKpis(payables, receivables, bankAccounts) {
|
|
591
827
|
const today = this.startOfDay(new Date());
|
|
592
828
|
const day7 = this.addDays(today, 7);
|
|
@@ -637,6 +873,11 @@ let FinanceService = FinanceService_1 = class FinanceService {
|
|
|
637
873
|
next.setDate(next.getDate() + days);
|
|
638
874
|
return next;
|
|
639
875
|
}
|
|
876
|
+
addMonths(date, months) {
|
|
877
|
+
const next = new Date(date);
|
|
878
|
+
next.setMonth(next.getMonth() + months);
|
|
879
|
+
return next;
|
|
880
|
+
}
|
|
640
881
|
async listAccountsPayableInstallments(paginationParams, status) {
|
|
641
882
|
return this.listTitles('payable', paginationParams, status);
|
|
642
883
|
}
|
|
@@ -882,6 +1123,184 @@ let FinanceService = FinanceService_1 = class FinanceService {
|
|
|
882
1123
|
});
|
|
883
1124
|
return bankAccounts.map((bankAccount) => this.mapBankAccountToFront(bankAccount));
|
|
884
1125
|
}
|
|
1126
|
+
async listTransfers(filters) {
|
|
1127
|
+
var _a;
|
|
1128
|
+
const search = (_a = filters === null || filters === void 0 ? void 0 : filters.search) === null || _a === void 0 ? void 0 : _a.trim();
|
|
1129
|
+
const parsedBankAccountId = (filters === null || filters === void 0 ? void 0 : filters.bank_account_id)
|
|
1130
|
+
? Number.parseInt(filters.bank_account_id, 10)
|
|
1131
|
+
: undefined;
|
|
1132
|
+
const bankAccountId = parsedBankAccountId && !Number.isNaN(parsedBankAccountId)
|
|
1133
|
+
? parsedBankAccountId
|
|
1134
|
+
: undefined;
|
|
1135
|
+
let transferKeys;
|
|
1136
|
+
if (search || bankAccountId) {
|
|
1137
|
+
const filteredLines = await this.prisma.bank_statement_line.findMany({
|
|
1138
|
+
where: Object.assign(Object.assign({ external_id: {
|
|
1139
|
+
startsWith: 'transfer:',
|
|
1140
|
+
} }, (search
|
|
1141
|
+
? {
|
|
1142
|
+
description: {
|
|
1143
|
+
contains: search,
|
|
1144
|
+
mode: 'insensitive',
|
|
1145
|
+
},
|
|
1146
|
+
}
|
|
1147
|
+
: {})), (bankAccountId ? { bank_account_id: bankAccountId } : {})),
|
|
1148
|
+
select: {
|
|
1149
|
+
external_id: true,
|
|
1150
|
+
},
|
|
1151
|
+
});
|
|
1152
|
+
transferKeys = Array.from(new Set(filteredLines
|
|
1153
|
+
.map((line) => line.external_id)
|
|
1154
|
+
.filter((externalId) => !!externalId)));
|
|
1155
|
+
if (transferKeys.length === 0) {
|
|
1156
|
+
return [];
|
|
1157
|
+
}
|
|
1158
|
+
}
|
|
1159
|
+
const transferLines = await this.prisma.bank_statement_line.findMany({
|
|
1160
|
+
where: Object.assign({}, (transferKeys
|
|
1161
|
+
? {
|
|
1162
|
+
external_id: {
|
|
1163
|
+
in: transferKeys,
|
|
1164
|
+
},
|
|
1165
|
+
}
|
|
1166
|
+
: {
|
|
1167
|
+
external_id: {
|
|
1168
|
+
startsWith: 'transfer:',
|
|
1169
|
+
},
|
|
1170
|
+
})),
|
|
1171
|
+
select: {
|
|
1172
|
+
id: true,
|
|
1173
|
+
external_id: true,
|
|
1174
|
+
bank_account_id: true,
|
|
1175
|
+
posted_date: true,
|
|
1176
|
+
amount_cents: true,
|
|
1177
|
+
description: true,
|
|
1178
|
+
},
|
|
1179
|
+
orderBy: [{ posted_date: 'desc' }, { id: 'desc' }],
|
|
1180
|
+
});
|
|
1181
|
+
const groupedByTransfer = new Map();
|
|
1182
|
+
for (const line of transferLines) {
|
|
1183
|
+
const transferKey = line.external_id;
|
|
1184
|
+
if (!transferKey) {
|
|
1185
|
+
continue;
|
|
1186
|
+
}
|
|
1187
|
+
const current = groupedByTransfer.get(transferKey) || [];
|
|
1188
|
+
current.push(line);
|
|
1189
|
+
groupedByTransfer.set(transferKey, current);
|
|
1190
|
+
}
|
|
1191
|
+
const transfers = Array.from(groupedByTransfer.entries())
|
|
1192
|
+
.map(([transferKey, lines]) => {
|
|
1193
|
+
const sourceLine = lines.find((line) => line.amount_cents < 0);
|
|
1194
|
+
const destinationLine = lines.find((line) => line.amount_cents > 0);
|
|
1195
|
+
if (!sourceLine || !destinationLine) {
|
|
1196
|
+
return null;
|
|
1197
|
+
}
|
|
1198
|
+
return {
|
|
1199
|
+
id: transferKey.replace('transfer:', ''),
|
|
1200
|
+
contaOrigemId: String(sourceLine.bank_account_id),
|
|
1201
|
+
contaDestinoId: String(destinationLine.bank_account_id),
|
|
1202
|
+
data: sourceLine.posted_date.toISOString(),
|
|
1203
|
+
valor: this.fromCents(Math.abs(sourceLine.amount_cents)),
|
|
1204
|
+
descricao: sourceLine.description || destinationLine.description || '',
|
|
1205
|
+
};
|
|
1206
|
+
})
|
|
1207
|
+
.filter(Boolean);
|
|
1208
|
+
return transfers;
|
|
1209
|
+
}
|
|
1210
|
+
async createTransfer(data, userId) {
|
|
1211
|
+
var _a;
|
|
1212
|
+
const sourceAccountId = Number(data.source_account_id);
|
|
1213
|
+
const destinationAccountId = Number(data.destination_account_id);
|
|
1214
|
+
const amount = Number(data.amount);
|
|
1215
|
+
const postedDate = new Date(data.date);
|
|
1216
|
+
if (Number.isNaN(sourceAccountId) ||
|
|
1217
|
+
Number.isNaN(destinationAccountId) ||
|
|
1218
|
+
sourceAccountId <= 0 ||
|
|
1219
|
+
destinationAccountId <= 0) {
|
|
1220
|
+
throw new common_1.BadRequestException('Invalid bank account ids');
|
|
1221
|
+
}
|
|
1222
|
+
if (sourceAccountId === destinationAccountId) {
|
|
1223
|
+
throw new common_1.BadRequestException('Source and destination accounts must be different');
|
|
1224
|
+
}
|
|
1225
|
+
if (Number.isNaN(amount) || amount <= 0) {
|
|
1226
|
+
throw new common_1.BadRequestException('amount must be greater than zero');
|
|
1227
|
+
}
|
|
1228
|
+
if (Number.isNaN(postedDate.getTime())) {
|
|
1229
|
+
throw new common_1.BadRequestException('Invalid transfer date');
|
|
1230
|
+
}
|
|
1231
|
+
const accounts = await this.prisma.bank_account.findMany({
|
|
1232
|
+
where: {
|
|
1233
|
+
id: {
|
|
1234
|
+
in: [sourceAccountId, destinationAccountId],
|
|
1235
|
+
},
|
|
1236
|
+
},
|
|
1237
|
+
select: {
|
|
1238
|
+
id: true,
|
|
1239
|
+
},
|
|
1240
|
+
});
|
|
1241
|
+
if (accounts.length !== 2) {
|
|
1242
|
+
throw new common_1.NotFoundException('Bank account not found');
|
|
1243
|
+
}
|
|
1244
|
+
const amountCents = this.toCents(amount);
|
|
1245
|
+
const description = ((_a = data.description) === null || _a === void 0 ? void 0 : _a.trim()) || 'Transferência bancária';
|
|
1246
|
+
const transferReference = `transfer:${Date.now()}-${Math.round(Math.random() * 1000000)}`;
|
|
1247
|
+
await this.prisma.$transaction(async (tx) => {
|
|
1248
|
+
const sourceStatement = await tx.bank_statement.create({
|
|
1249
|
+
data: {
|
|
1250
|
+
bank_account_id: sourceAccountId,
|
|
1251
|
+
source_type: 'manual',
|
|
1252
|
+
imported_at: new Date(),
|
|
1253
|
+
imported_by_user_id: userId,
|
|
1254
|
+
idempotency_key: `${transferReference}:source`,
|
|
1255
|
+
period_start: postedDate,
|
|
1256
|
+
period_end: postedDate,
|
|
1257
|
+
},
|
|
1258
|
+
});
|
|
1259
|
+
const destinationStatement = await tx.bank_statement.create({
|
|
1260
|
+
data: {
|
|
1261
|
+
bank_account_id: destinationAccountId,
|
|
1262
|
+
source_type: 'manual',
|
|
1263
|
+
imported_at: new Date(),
|
|
1264
|
+
imported_by_user_id: userId,
|
|
1265
|
+
idempotency_key: `${transferReference}:destination`,
|
|
1266
|
+
period_start: postedDate,
|
|
1267
|
+
period_end: postedDate,
|
|
1268
|
+
},
|
|
1269
|
+
});
|
|
1270
|
+
await tx.bank_statement_line.create({
|
|
1271
|
+
data: {
|
|
1272
|
+
bank_statement_id: sourceStatement.id,
|
|
1273
|
+
bank_account_id: sourceAccountId,
|
|
1274
|
+
external_id: transferReference,
|
|
1275
|
+
posted_date: postedDate,
|
|
1276
|
+
amount_cents: -Math.abs(amountCents),
|
|
1277
|
+
description,
|
|
1278
|
+
status: 'reconciled',
|
|
1279
|
+
dedupe_key: `${transferReference}:source`,
|
|
1280
|
+
},
|
|
1281
|
+
});
|
|
1282
|
+
await tx.bank_statement_line.create({
|
|
1283
|
+
data: {
|
|
1284
|
+
bank_statement_id: destinationStatement.id,
|
|
1285
|
+
bank_account_id: destinationAccountId,
|
|
1286
|
+
external_id: transferReference,
|
|
1287
|
+
posted_date: postedDate,
|
|
1288
|
+
amount_cents: Math.abs(amountCents),
|
|
1289
|
+
description,
|
|
1290
|
+
status: 'reconciled',
|
|
1291
|
+
dedupe_key: `${transferReference}:destination`,
|
|
1292
|
+
},
|
|
1293
|
+
});
|
|
1294
|
+
});
|
|
1295
|
+
return {
|
|
1296
|
+
id: transferReference.replace('transfer:', ''),
|
|
1297
|
+
contaOrigemId: String(sourceAccountId),
|
|
1298
|
+
contaDestinoId: String(destinationAccountId),
|
|
1299
|
+
data: postedDate.toISOString(),
|
|
1300
|
+
valor: amount,
|
|
1301
|
+
descricao: description,
|
|
1302
|
+
};
|
|
1303
|
+
}
|
|
885
1304
|
async listCostCenters() {
|
|
886
1305
|
const costCenters = await this.prisma.cost_center.findMany({
|
|
887
1306
|
orderBy: [{ code: 'asc' }, { name: 'asc' }],
|
|
@@ -1035,9 +1454,17 @@ let FinanceService = FinanceService_1 = class FinanceService {
|
|
|
1035
1454
|
});
|
|
1036
1455
|
return Object.assign(Object.assign({}, paginated), { data: (paginated.data || []).map((period) => this.mapPeriodCloseToFront(period)) });
|
|
1037
1456
|
}
|
|
1038
|
-
async listBankStatements(bankAccountId) {
|
|
1457
|
+
async listBankStatements(bankAccountId, search) {
|
|
1458
|
+
const trimmedSearch = search === null || search === void 0 ? void 0 : search.trim();
|
|
1039
1459
|
const statements = await this.prisma.bank_statement_line.findMany({
|
|
1040
|
-
where: Object.assign({}, (bankAccountId ? { bank_account_id: bankAccountId } : {})),
|
|
1460
|
+
where: Object.assign(Object.assign({}, (bankAccountId ? { bank_account_id: bankAccountId } : {})), (trimmedSearch
|
|
1461
|
+
? {
|
|
1462
|
+
description: {
|
|
1463
|
+
contains: trimmedSearch,
|
|
1464
|
+
mode: 'insensitive',
|
|
1465
|
+
},
|
|
1466
|
+
}
|
|
1467
|
+
: {})),
|
|
1041
1468
|
include: {
|
|
1042
1469
|
bank_account: {
|
|
1043
1470
|
select: {
|
|
@@ -1057,8 +1484,74 @@ let FinanceService = FinanceService_1 = class FinanceService {
|
|
|
1057
1484
|
statusConciliacao: this.mapStatementStatusToPt(statement.status),
|
|
1058
1485
|
}));
|
|
1059
1486
|
}
|
|
1060
|
-
async
|
|
1061
|
-
const
|
|
1487
|
+
async getBankReconciliationSummary(bankAccountId) {
|
|
1488
|
+
const pendingStatements = await this.prisma.bank_statement_line.findMany({
|
|
1489
|
+
where: {
|
|
1490
|
+
bank_account_id: bankAccountId,
|
|
1491
|
+
status: {
|
|
1492
|
+
in: ['pending', 'imported'],
|
|
1493
|
+
},
|
|
1494
|
+
},
|
|
1495
|
+
select: {
|
|
1496
|
+
amount_cents: true,
|
|
1497
|
+
},
|
|
1498
|
+
});
|
|
1499
|
+
const openInstallments = await this.prisma.financial_installment.findMany({
|
|
1500
|
+
where: {
|
|
1501
|
+
open_amount_cents: {
|
|
1502
|
+
gt: 0,
|
|
1503
|
+
},
|
|
1504
|
+
status: {
|
|
1505
|
+
in: ['open', 'partial', 'overdue'],
|
|
1506
|
+
},
|
|
1507
|
+
financial_title: {
|
|
1508
|
+
status: {
|
|
1509
|
+
in: ['open', 'partial', 'overdue'],
|
|
1510
|
+
},
|
|
1511
|
+
title_type: {
|
|
1512
|
+
in: ['payable', 'receivable'],
|
|
1513
|
+
},
|
|
1514
|
+
},
|
|
1515
|
+
},
|
|
1516
|
+
select: {
|
|
1517
|
+
open_amount_cents: true,
|
|
1518
|
+
financial_title: {
|
|
1519
|
+
select: {
|
|
1520
|
+
title_type: true,
|
|
1521
|
+
},
|
|
1522
|
+
},
|
|
1523
|
+
},
|
|
1524
|
+
});
|
|
1525
|
+
const payableAmounts = new Set();
|
|
1526
|
+
const receivableAmounts = new Set();
|
|
1527
|
+
for (const installment of openInstallments) {
|
|
1528
|
+
const amount = Math.abs(installment.open_amount_cents);
|
|
1529
|
+
if (installment.financial_title.title_type === 'payable') {
|
|
1530
|
+
payableAmounts.add(amount);
|
|
1531
|
+
}
|
|
1532
|
+
else if (installment.financial_title.title_type === 'receivable') {
|
|
1533
|
+
receivableAmounts.add(amount);
|
|
1534
|
+
}
|
|
1535
|
+
}
|
|
1536
|
+
let discrepancyCount = 0;
|
|
1537
|
+
let differenceCents = 0;
|
|
1538
|
+
for (const statement of pendingStatements) {
|
|
1539
|
+
const normalizedAmount = Math.abs(statement.amount_cents);
|
|
1540
|
+
differenceCents += statement.amount_cents;
|
|
1541
|
+
const hasPossibleMatch = statement.amount_cents < 0
|
|
1542
|
+
? payableAmounts.has(normalizedAmount)
|
|
1543
|
+
: receivableAmounts.has(normalizedAmount);
|
|
1544
|
+
if (!hasPossibleMatch) {
|
|
1545
|
+
discrepancyCount += 1;
|
|
1546
|
+
}
|
|
1547
|
+
}
|
|
1548
|
+
return {
|
|
1549
|
+
discrepancies: discrepancyCount,
|
|
1550
|
+
difference: this.fromCents(differenceCents),
|
|
1551
|
+
};
|
|
1552
|
+
}
|
|
1553
|
+
async exportBankStatementsCsv(bankAccountId, search) {
|
|
1554
|
+
const statements = await this.listBankStatements(bankAccountId, search);
|
|
1062
1555
|
const headers = [
|
|
1063
1556
|
'id',
|
|
1064
1557
|
'data',
|
|
@@ -1089,6 +1582,66 @@ let FinanceService = FinanceService_1 = class FinanceService {
|
|
|
1089
1582
|
fileName,
|
|
1090
1583
|
};
|
|
1091
1584
|
}
|
|
1585
|
+
async createBankStatementAdjustment(data, userId) {
|
|
1586
|
+
var _a;
|
|
1587
|
+
const bankAccountId = Number(data.bank_account_id);
|
|
1588
|
+
const amount = Number(data.amount);
|
|
1589
|
+
const postedAt = data.date ? new Date(data.date) : new Date();
|
|
1590
|
+
if (Number.isNaN(bankAccountId) || bankAccountId <= 0) {
|
|
1591
|
+
throw new common_1.BadRequestException('bank_account_id is required');
|
|
1592
|
+
}
|
|
1593
|
+
if (Number.isNaN(amount) || amount <= 0) {
|
|
1594
|
+
throw new common_1.BadRequestException('amount must be greater than zero');
|
|
1595
|
+
}
|
|
1596
|
+
if (Number.isNaN(postedAt.getTime())) {
|
|
1597
|
+
throw new common_1.BadRequestException('Invalid adjustment date');
|
|
1598
|
+
}
|
|
1599
|
+
const bankAccount = await this.prisma.bank_account.findUnique({
|
|
1600
|
+
where: { id: bankAccountId },
|
|
1601
|
+
select: { id: true },
|
|
1602
|
+
});
|
|
1603
|
+
if (!bankAccount) {
|
|
1604
|
+
throw new common_1.NotFoundException('Bank account not found');
|
|
1605
|
+
}
|
|
1606
|
+
const adjustedAmountCents = -Math.abs(this.toCents(amount));
|
|
1607
|
+
const description = ((_a = data.description) === null || _a === void 0 ? void 0 : _a.trim()) || `Ajuste: ${data.type}`;
|
|
1608
|
+
const reference = `adjustment:${Date.now()}-${Math.round(Math.random() * 1000000)}`;
|
|
1609
|
+
const created = await this.prisma.$transaction(async (tx) => {
|
|
1610
|
+
const statement = await tx.bank_statement.create({
|
|
1611
|
+
data: {
|
|
1612
|
+
bank_account_id: bankAccountId,
|
|
1613
|
+
source_type: 'manual',
|
|
1614
|
+
imported_at: new Date(),
|
|
1615
|
+
imported_by_user_id: userId,
|
|
1616
|
+
idempotency_key: reference,
|
|
1617
|
+
period_start: postedAt,
|
|
1618
|
+
period_end: postedAt,
|
|
1619
|
+
},
|
|
1620
|
+
});
|
|
1621
|
+
const line = await tx.bank_statement_line.create({
|
|
1622
|
+
data: {
|
|
1623
|
+
bank_statement_id: statement.id,
|
|
1624
|
+
bank_account_id: bankAccountId,
|
|
1625
|
+
external_id: reference,
|
|
1626
|
+
posted_date: postedAt,
|
|
1627
|
+
amount_cents: adjustedAmountCents,
|
|
1628
|
+
description,
|
|
1629
|
+
status: 'adjusted',
|
|
1630
|
+
dedupe_key: reference,
|
|
1631
|
+
},
|
|
1632
|
+
});
|
|
1633
|
+
return line;
|
|
1634
|
+
});
|
|
1635
|
+
return {
|
|
1636
|
+
id: String(created.id),
|
|
1637
|
+
contaBancariaId: String(created.bank_account_id),
|
|
1638
|
+
data: created.posted_date.toISOString(),
|
|
1639
|
+
descricao: created.description,
|
|
1640
|
+
valor: this.fromCents(created.amount_cents),
|
|
1641
|
+
tipo: created.amount_cents >= 0 ? 'entrada' : 'saida',
|
|
1642
|
+
statusConciliacao: this.mapStatementStatusToPt(created.status),
|
|
1643
|
+
};
|
|
1644
|
+
}
|
|
1092
1645
|
async importBankStatements(bankAccountId, file, locale, userId) {
|
|
1093
1646
|
if (!file) {
|
|
1094
1647
|
throw new common_1.BadRequestException('File is required');
|
|
@@ -2327,6 +2880,67 @@ let FinanceService = FinanceService_1 = class FinanceService {
|
|
|
2327
2880
|
created_by_user_id: userId,
|
|
2328
2881
|
},
|
|
2329
2882
|
});
|
|
2883
|
+
let reconciliationId = null;
|
|
2884
|
+
if (data.bank_statement_line_id) {
|
|
2885
|
+
const statementLine = await tx.bank_statement_line.findUnique({
|
|
2886
|
+
where: {
|
|
2887
|
+
id: data.bank_statement_line_id,
|
|
2888
|
+
},
|
|
2889
|
+
select: {
|
|
2890
|
+
id: true,
|
|
2891
|
+
bank_account_id: true,
|
|
2892
|
+
amount_cents: true,
|
|
2893
|
+
status: true,
|
|
2894
|
+
},
|
|
2895
|
+
});
|
|
2896
|
+
if (!statementLine) {
|
|
2897
|
+
throw new common_1.NotFoundException('Bank statement line not found');
|
|
2898
|
+
}
|
|
2899
|
+
if (data.bank_account_id &&
|
|
2900
|
+
statementLine.bank_account_id !== data.bank_account_id) {
|
|
2901
|
+
throw new common_1.ConflictException('Bank statement line does not belong to informed bank account');
|
|
2902
|
+
}
|
|
2903
|
+
if (Math.abs(statementLine.amount_cents) !== amountCents) {
|
|
2904
|
+
throw new common_1.ConflictException('Bank statement amount and settlement amount must match');
|
|
2905
|
+
}
|
|
2906
|
+
const hasReconciliation = await tx.bank_reconciliation.findFirst({
|
|
2907
|
+
where: {
|
|
2908
|
+
bank_statement_line_id: statementLine.id,
|
|
2909
|
+
status: 'reconciled',
|
|
2910
|
+
},
|
|
2911
|
+
select: {
|
|
2912
|
+
id: true,
|
|
2913
|
+
},
|
|
2914
|
+
});
|
|
2915
|
+
if (hasReconciliation) {
|
|
2916
|
+
throw new common_1.ConflictException('Bank statement line already reconciled');
|
|
2917
|
+
}
|
|
2918
|
+
const createdReconciliation = await tx.bank_reconciliation.create({
|
|
2919
|
+
data: {
|
|
2920
|
+
bank_statement_line_id: statementLine.id,
|
|
2921
|
+
settlement_id: settlement.id,
|
|
2922
|
+
status: 'reconciled',
|
|
2923
|
+
matched_at: settledAt,
|
|
2924
|
+
reconciled_at: settledAt,
|
|
2925
|
+
matched_by_user_id: userId || null,
|
|
2926
|
+
reconciled_by_user_id: userId || null,
|
|
2927
|
+
},
|
|
2928
|
+
select: {
|
|
2929
|
+
id: true,
|
|
2930
|
+
},
|
|
2931
|
+
});
|
|
2932
|
+
reconciliationId = createdReconciliation.id;
|
|
2933
|
+
if (statementLine.status !== 'reconciled') {
|
|
2934
|
+
await tx.bank_statement_line.update({
|
|
2935
|
+
where: {
|
|
2936
|
+
id: statementLine.id,
|
|
2937
|
+
},
|
|
2938
|
+
data: {
|
|
2939
|
+
status: 'reconciled',
|
|
2940
|
+
},
|
|
2941
|
+
});
|
|
2942
|
+
}
|
|
2943
|
+
}
|
|
2330
2944
|
await tx.settlement_allocation.create({
|
|
2331
2945
|
data: {
|
|
2332
2946
|
settlement: {
|
|
@@ -2404,6 +3018,7 @@ let FinanceService = FinanceService_1 = class FinanceService {
|
|
|
2404
3018
|
title_status: nextTitleStatus,
|
|
2405
3019
|
installment_open_amount_cents: updatedInstallment.open_amount_cents,
|
|
2406
3020
|
settlement_id: settlement.id,
|
|
3021
|
+
bank_reconciliation_id: reconciliationId,
|
|
2407
3022
|
}),
|
|
2408
3023
|
});
|
|
2409
3024
|
const updatedTitle = await tx.financial_title.findFirst({
|
|
@@ -2419,9 +3034,12 @@ let FinanceService = FinanceService_1 = class FinanceService {
|
|
|
2419
3034
|
return {
|
|
2420
3035
|
title: updatedTitle,
|
|
2421
3036
|
settlementId: settlement.id,
|
|
3037
|
+
reconciliationId,
|
|
2422
3038
|
};
|
|
2423
3039
|
});
|
|
2424
|
-
return Object.assign(Object.assign({}, this.mapTitleToFront(result.title)), { settlementId: String(result.settlementId)
|
|
3040
|
+
return Object.assign(Object.assign({}, this.mapTitleToFront(result.title)), { settlementId: String(result.settlementId), reconciliationId: result.reconciliationId
|
|
3041
|
+
? String(result.reconciliationId)
|
|
3042
|
+
: null });
|
|
2425
3043
|
}
|
|
2426
3044
|
catch (error) {
|
|
2427
3045
|
const message = String((error === null || error === void 0 ? void 0 : error.message) || '');
|
|
@@ -2923,7 +3541,7 @@ let FinanceService = FinanceService_1 = class FinanceService {
|
|
|
2923
3541
|
});
|
|
2924
3542
|
}
|
|
2925
3543
|
mapTitleToFront(title, paymentChannelOverride) {
|
|
2926
|
-
var _a;
|
|
3544
|
+
var _a, _b, _c;
|
|
2927
3545
|
const allocations = title.financial_installment.flatMap((installment) => installment.installment_allocation);
|
|
2928
3546
|
const firstCostCenter = (_a = allocations[0]) === null || _a === void 0 ? void 0 : _a.cost_center_id;
|
|
2929
3547
|
const tags = [
|
|
@@ -2978,8 +3596,14 @@ let FinanceService = FinanceService_1 = class FinanceService {
|
|
|
2978
3596
|
: '', valorTotal: this.fromCents(title.total_amount_cents), status: this.mapStatusToPt(title.status), criadoEm: title.created_at.toISOString(), categoriaId: title.finance_category_id ? String(title.finance_category_id) : null, centroCustoId: firstCostCenter ? String(firstCostCenter) : null, anexos: attachmentDetails.map((attachment) => attachment.nome), anexosDetalhes: attachmentDetails, tags, parcelas: mappedInstallments, canal: this.mapPaymentMethodToPt(channelFromSettlement) ||
|
|
2979
3597
|
paymentChannelOverride ||
|
|
2980
3598
|
'transferencia' }, (title.title_type === 'payable'
|
|
2981
|
-
? {
|
|
2982
|
-
|
|
3599
|
+
? {
|
|
3600
|
+
fornecedorId: String(title.person_id),
|
|
3601
|
+
fornecedor: ((_b = title.person) === null || _b === void 0 ? void 0 : _b.name) || '',
|
|
3602
|
+
}
|
|
3603
|
+
: {
|
|
3604
|
+
clienteId: String(title.person_id),
|
|
3605
|
+
cliente: ((_c = title.person) === null || _c === void 0 ? void 0 : _c.name) || '',
|
|
3606
|
+
}));
|
|
2983
3607
|
}
|
|
2984
3608
|
defaultTitleInclude() {
|
|
2985
3609
|
return {
|