@hed-hog/finance 0.0.276 → 0.0.279

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/README.md CHANGED
@@ -133,6 +133,7 @@ O módulo financeiro gerencia o ciclo completo da gestão financeira corporativa
133
133
  |--------|-------------------------------|------|-------------------------------|-------------------------------------------------------------------------------------------------------|------------------------|----------------------|
134
134
  | GET | /finance/statements | Sim | Lista extratos bancários | Query: `bank_account_id`, `search` | Lista de extratos | 400 BadRequest, 401 |
135
135
  | GET | /finance/bank-reconciliation/summary | Sim | Resumo da conciliação bancária | Query: `bank_account_id` (obrigatório) | Resumo da conciliação | 400 BadRequest, 401 |
136
+ | POST | /finance/bank-reconciliations | Sim | Cria conciliação bancária | Body: `{ title_id: number; installment_id: number; bank_statement_line_id: number; payment_channel?: string; description?: string }` | Conciliação criada | 400 Validação, 401 |
136
137
  | GET | /finance/statements/export | Sim | Exporta extratos em CSV | Query: `bank_account_id` (obrigatório), `search` | CSV para download | 400 BadRequest, 401 |
137
138
  | POST | /finance/statements/import | Sim | Importa extrato bancário | Multipart file + Body: `bank_account_id` (obrigatório) | Importação realizada | 400 BadRequest, 401 |
138
139
  | POST | /finance/statements/adjustments | Sim | Cria ajuste em extrato bancário | Body: `{ bank_account_id: number; amount: number; date?: string; type: string; description?: string }` | Ajuste criado | 400 Validação, 401 |
@@ -244,6 +245,18 @@ O módulo financeiro gerencia o ciclo completo da gestão financeira corporativa
244
245
  }
245
246
  ```
246
247
 
248
+ ### CreateBankReconciliationDto
249
+
250
+ ```ts
251
+ {
252
+ title_id: number; // obrigatório
253
+ installment_id: number; // obrigatório
254
+ bank_statement_line_id: number; // obrigatório
255
+ payment_channel?: string;
256
+ description?: string;
257
+ }
258
+ ```
259
+
247
260
  ### CreateBankStatementAdjustmentDto
248
261
 
249
262
  ```ts
@@ -256,24 +269,20 @@ O módulo financeiro gerencia o ciclo completo da gestão financeira corporativa
256
269
  }
257
270
  ```
258
271
 
259
- ### CreateTransferDto
272
+ ### CreateCostCenterDto
260
273
 
261
274
  ```ts
262
275
  {
263
- source_account_id: number; // obrigatório
264
- destination_account_id: number; // obrigatório
265
- date: string; // obrigatório
266
- amount: number; // obrigatório, >= 0.01
267
- description?: string;
276
+ name: string; // obrigatório
268
277
  }
269
278
  ```
270
279
 
271
- ### CreateFinanceTagDto
280
+ ### UpdateCostCenterDto
272
281
 
273
282
  ```ts
274
283
  {
275
- name: string; // obrigatório, string não vazia
276
- color?: string; // opcional, cor hexadecimal válida
284
+ name?: string;
285
+ status?: 'active' | 'inactive';
277
286
  }
278
287
  ```
279
288
 
@@ -307,23 +316,6 @@ O módulo financeiro gerencia o ciclo completo da gestão financeira corporativa
307
316
  }
308
317
  ```
309
318
 
310
- ### CreateCostCenterDto
311
-
312
- ```ts
313
- {
314
- name: string; // obrigatório
315
- }
316
- ```
317
-
318
- ### UpdateCostCenterDto
319
-
320
- ```ts
321
- {
322
- name?: string;
323
- status?: 'active' | 'inactive';
324
- }
325
- ```
326
-
327
319
  ### SendCollectionDto
328
320
 
329
321
  ```ts
@@ -371,6 +363,38 @@ O módulo financeiro gerencia o ciclo completo da gestão financeira corporativa
371
363
  }
372
364
  ```
373
365
 
366
+ ### CreateTransferDto
367
+
368
+ ```ts
369
+ {
370
+ source_account_id: number; // obrigatório
371
+ destination_account_id: number; // obrigatório
372
+ date: string; // obrigatório
373
+ amount: number; // obrigatório, >= 0.01
374
+ description?: string;
375
+ }
376
+ ```
377
+
378
+ ### CreateFinanceTagDto
379
+
380
+ ```ts
381
+ {
382
+ name: string; // obrigatório, string não vazia
383
+ color?: string; // opcional, cor hexadecimal válida
384
+ }
385
+ ```
386
+
387
+ ### UpdateFinanceScenarioSettingsDto
388
+
389
+ ```ts
390
+ {
391
+ atrasoMedio: number;
392
+ taxaInadimplencia: number;
393
+ crescimentoReceita: number;
394
+ setAsDefault?: boolean;
395
+ }
396
+ ```
397
+
374
398
  ---
375
399
 
376
400
  ## 6. Erros comuns
@@ -750,6 +774,22 @@ Form-data:
750
774
  - bank_account_id: 10
751
775
  ```
752
776
 
777
+ ### Criar conciliação bancária
778
+
779
+ ```http
780
+ POST /finance/bank-reconciliations
781
+ Authorization: Bearer <token>
782
+ Content-Type: application/json
783
+
784
+ {
785
+ "title_id": 123,
786
+ "installment_id": 456,
787
+ "bank_statement_line_id": 789,
788
+ "payment_channel": "PIX",
789
+ "description": "Conciliação automática"
790
+ }
791
+ ```
792
+
753
793
  ### Estornar liquidação
754
794
 
755
795
  ```http
@@ -322,6 +322,52 @@
322
322
  - where:
323
323
  slug: admin-finance
324
324
 
325
+ - menu_id:
326
+ where:
327
+ slug: /finance/reports
328
+ icon: chart-no-axes-combined
329
+ url: /finance/reports/overview-results
330
+ name:
331
+ en: Results Overview
332
+ pt: Visão Geral dos Resultados
333
+ slug: /finance/reports/overview-results
334
+ relations:
335
+ role:
336
+ - where:
337
+ slug: admin
338
+ - where:
339
+ slug: admin-finance
340
+ - menu_id:
341
+ where:
342
+ slug: /finance/reports
343
+ icon: chart-bar-big
344
+ url: /finance/reports/top-customers
345
+ name:
346
+ en: Top Customers
347
+ pt: Principais Clientes
348
+ slug: /finance/reports/top-customers
349
+ relations:
350
+ role:
351
+ - where:
352
+ slug: admin
353
+ - where:
354
+ slug: admin-finance
355
+ - menu_id:
356
+ where:
357
+ slug: /finance/reports
358
+ icon: chart-pie
359
+ url: /finance/reports/top-operational-expenses
360
+ name:
361
+ en: Top Operational Expenses
362
+ pt: Principais Gastos Operacionais
363
+ slug: /finance/reports/top-operational-expenses
364
+ relations:
365
+ role:
366
+ - where:
367
+ slug: admin
368
+ - where:
369
+ slug: admin-finance
370
+
325
371
  - menu_id:
326
372
  where:
327
373
  slug: /finance
@@ -0,0 +1,275 @@
1
+ import {
2
+ customerRevenueMock,
3
+ financialResultsMock,
4
+ operationalExpensesMock,
5
+ type GroupBy,
6
+ } from './report-mocks';
7
+
8
+ const dateFormatter = new Intl.DateTimeFormat('pt-BR');
9
+
10
+ function toDate(value: string) {
11
+ return new Date(`${value}T00:00:00`);
12
+ }
13
+
14
+ function isWithinRange(date: string, from: string, to: string) {
15
+ return date >= from && date <= to;
16
+ }
17
+
18
+ function startOfWeek(date: Date) {
19
+ const current = new Date(date);
20
+ const day = current.getDay();
21
+ const diff = day === 0 ? -6 : 1 - day;
22
+ current.setDate(current.getDate() + diff);
23
+ current.setHours(0, 0, 0, 0);
24
+
25
+ return current;
26
+ }
27
+
28
+ function getIsoWeek(date: Date) {
29
+ const current = new Date(
30
+ Date.UTC(date.getFullYear(), date.getMonth(), date.getDate())
31
+ );
32
+ const dayNum = current.getUTCDay() || 7;
33
+ current.setUTCDate(current.getUTCDate() + 4 - dayNum);
34
+ const yearStart = new Date(Date.UTC(current.getUTCFullYear(), 0, 1));
35
+
36
+ return Math.ceil(
37
+ ((current.getTime() - yearStart.getTime()) / 86400000 + 1) / 7
38
+ );
39
+ }
40
+
41
+ function getBucketKey(date: string, groupBy: GroupBy) {
42
+ const d = toDate(date);
43
+
44
+ if (groupBy === 'day') {
45
+ return date;
46
+ }
47
+
48
+ if (groupBy === 'week') {
49
+ const weekStart = startOfWeek(d);
50
+ const isoWeek = getIsoWeek(weekStart);
51
+ return `${weekStart.getFullYear()}-W${String(isoWeek).padStart(2, '0')}`;
52
+ }
53
+
54
+ if (groupBy === 'month') {
55
+ return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}`;
56
+ }
57
+
58
+ return String(d.getFullYear());
59
+ }
60
+
61
+ export function formatBucketLabel(bucket: string, groupBy: GroupBy) {
62
+ if (groupBy === 'day') {
63
+ return dateFormatter.format(toDate(bucket));
64
+ }
65
+
66
+ if (groupBy === 'week') {
67
+ const [year, week] = bucket.split('-W');
68
+ return `Sem ${Number(week)}/${year}`;
69
+ }
70
+
71
+ if (groupBy === 'month') {
72
+ const [year, month] = bucket.split('-');
73
+ return `${month}/${year}`;
74
+ }
75
+
76
+ return bucket;
77
+ }
78
+
79
+ function sortBuckets(a: string, b: string, groupBy: GroupBy) {
80
+ if (groupBy === 'year' || groupBy === 'month' || groupBy === 'day') {
81
+ return a.localeCompare(b);
82
+ }
83
+
84
+ const [aYear, aWeek] = a.split('-W');
85
+ const [bYear, bWeek] = b.split('-W');
86
+ const yearDiff = Number(aYear) - Number(bYear);
87
+
88
+ if (yearDiff !== 0) {
89
+ return yearDiff;
90
+ }
91
+
92
+ return Number(aWeek) - Number(bWeek);
93
+ }
94
+
95
+ export function aggregateOverview(params: {
96
+ from: string;
97
+ to: string;
98
+ groupBy: GroupBy;
99
+ }) {
100
+ const { from, to, groupBy } = params;
101
+ const grouped = new Map<
102
+ string,
103
+ {
104
+ period: string;
105
+ label: string;
106
+ faturamento: number;
107
+ despesasEmprestimos: number;
108
+ diferenca: number;
109
+ aporteInvestidor: number;
110
+ emprestimoBanco: number;
111
+ despesas: number;
112
+ }
113
+ >();
114
+
115
+ for (const item of financialResultsMock) {
116
+ if (!isWithinRange(item.date, from, to)) {
117
+ continue;
118
+ }
119
+
120
+ const bucket = getBucketKey(item.date, groupBy);
121
+ const current = grouped.get(bucket) || {
122
+ period: bucket,
123
+ label: formatBucketLabel(bucket, groupBy),
124
+ faturamento: 0,
125
+ despesasEmprestimos: 0,
126
+ diferenca: 0,
127
+ aporteInvestidor: 0,
128
+ emprestimoBanco: 0,
129
+ despesas: 0,
130
+ };
131
+
132
+ current.faturamento += item.faturamento;
133
+ current.aporteInvestidor += item.aporteInvestidor;
134
+ current.emprestimoBanco += item.emprestimoBanco;
135
+ current.despesas += item.despesas;
136
+ current.despesasEmprestimos += item.despesas + item.emprestimoBanco;
137
+ current.diferenca = current.faturamento - current.despesasEmprestimos;
138
+
139
+ grouped.set(bucket, current);
140
+ }
141
+
142
+ return Array.from(grouped.values()).sort((a, b) =>
143
+ sortBuckets(a.period, b.period, groupBy)
144
+ );
145
+ }
146
+
147
+ export function aggregateTopCustomers(params: {
148
+ from: string;
149
+ to: string;
150
+ groupBy: GroupBy;
151
+ topN?: number;
152
+ }) {
153
+ const { from, to, groupBy, topN = 20 } = params;
154
+
155
+ const byCustomer = new Map<string, number>();
156
+ const byBucket = new Map<string, number>();
157
+
158
+ for (const item of customerRevenueMock) {
159
+ if (!isWithinRange(item.date, from, to)) {
160
+ continue;
161
+ }
162
+
163
+ const current = byCustomer.get(item.customer) || 0;
164
+ byCustomer.set(item.customer, current + item.value);
165
+
166
+ const bucket = getBucketKey(item.date, groupBy);
167
+ const currentBucket = byBucket.get(bucket) || 0;
168
+ byBucket.set(bucket, currentBucket + item.value);
169
+ }
170
+
171
+ const sorted = Array.from(byCustomer.entries())
172
+ .map(([customer, value]) => ({ customer, value }))
173
+ .sort((a, b) => b.value - a.value);
174
+
175
+ const topCustomers = sorted.slice(0, topN);
176
+ const total = sorted.reduce((acc, row) => acc + row.value, 0);
177
+ const top5 = topCustomers
178
+ .slice(0, 5)
179
+ .reduce((acc, row) => acc + row.value, 0);
180
+
181
+ const pieData = topCustomers.slice(0, 9);
182
+ const othersValue = sorted.slice(9).reduce((acc, row) => acc + row.value, 0);
183
+
184
+ if (othersValue > 0) {
185
+ pieData.push({ customer: 'Outros', value: othersValue });
186
+ }
187
+
188
+ const groupedPeriods = Array.from(byBucket.entries())
189
+ .map(([period, value]) => ({
190
+ period,
191
+ label: formatBucketLabel(period, groupBy),
192
+ value,
193
+ }))
194
+ .sort((a, b) => sortBuckets(a.period, b.period, groupBy));
195
+
196
+ return {
197
+ total,
198
+ top5,
199
+ top5Percent: total > 0 ? (top5 / total) * 100 : 0,
200
+ topCustomers,
201
+ pieData,
202
+ groupedPeriods,
203
+ };
204
+ }
205
+
206
+ export function aggregateOperationalExpenses(params: {
207
+ from: string;
208
+ to: string;
209
+ groupBy: GroupBy;
210
+ topN?: number;
211
+ }) {
212
+ const { from, to, groupBy, topN = 20 } = params;
213
+
214
+ const byCategory = new Map<
215
+ string,
216
+ { category: string; costCenter: string; value: number }
217
+ >();
218
+ const byCostCenter = new Map<string, number>();
219
+ const byBucket = new Map<string, number>();
220
+
221
+ for (const item of operationalExpensesMock) {
222
+ if (!isWithinRange(item.date, from, to)) {
223
+ continue;
224
+ }
225
+
226
+ const currentCategory = byCategory.get(item.category) || {
227
+ category: item.category,
228
+ costCenter: item.costCenter,
229
+ value: 0,
230
+ };
231
+
232
+ currentCategory.value += item.value;
233
+ byCategory.set(item.category, currentCategory);
234
+
235
+ const currentCenter = byCostCenter.get(item.costCenter) || 0;
236
+ byCostCenter.set(item.costCenter, currentCenter + item.value);
237
+
238
+ const bucket = getBucketKey(item.date, groupBy);
239
+ const currentBucket = byBucket.get(bucket) || 0;
240
+ byBucket.set(bucket, currentBucket + item.value);
241
+ }
242
+
243
+ const sorted = Array.from(byCategory.values()).sort(
244
+ (a, b) => b.value - a.value
245
+ );
246
+ const topExpenses = sorted.slice(0, topN);
247
+ const total = sorted.reduce((acc, item) => acc + item.value, 0);
248
+
249
+ const pieData = Array.from(byCostCenter.entries()).map(([name, value]) => ({
250
+ name,
251
+ value,
252
+ }));
253
+
254
+ const groupedPeriods = Array.from(byBucket.entries())
255
+ .map(([period, value]) => ({
256
+ period,
257
+ label: formatBucketLabel(period, groupBy),
258
+ value,
259
+ }))
260
+ .sort((a, b) => sortBuckets(a.period, b.period, groupBy));
261
+
262
+ return {
263
+ total,
264
+ topExpenses,
265
+ pieData,
266
+ groupedPeriods,
267
+ };
268
+ }
269
+
270
+ export function getDefaultDateRange() {
271
+ return {
272
+ from: '2021-01-01',
273
+ to: '2026-12-31',
274
+ };
275
+ }
@@ -0,0 +1,186 @@
1
+ export type GroupBy = 'day' | 'week' | 'month' | 'year';
2
+
3
+ export interface FinancialResultEntry {
4
+ date: string;
5
+ faturamento: number;
6
+ emprestimoBanco: number;
7
+ aporteInvestidor: number;
8
+ despesas: number;
9
+ }
10
+
11
+ export interface CustomerRevenueEntry {
12
+ date: string;
13
+ customer: string;
14
+ value: number;
15
+ }
16
+
17
+ export interface OperationalExpenseEntry {
18
+ date: string;
19
+ category: string;
20
+ costCenter: string;
21
+ value: number;
22
+ }
23
+
24
+ const customerNames = [
25
+ 'VALE',
26
+ 'CALIAN',
27
+ 'ALURA',
28
+ 'FIAP',
29
+ 'UDEMY',
30
+ 'HIREWORK',
31
+ 'PAULO DE TARSO',
32
+ 'BRCLICK',
33
+ 'DISTRITO',
34
+ 'COINBIT CLUB',
35
+ 'IT GLOBAL',
36
+ 'INFINITY LEGACY',
37
+ 'XFX',
38
+ 'CLASS',
39
+ 'IMPACTA',
40
+ 'AFX',
41
+ 'SOLARIUM',
42
+ 'ADVANCED RESOURCES',
43
+ 'BMX SCHOOL',
44
+ 'CODE NATION',
45
+ 'NEXT LEVEL',
46
+ 'MEGA TECH',
47
+ 'EDU PRO',
48
+ 'CONECTA BR',
49
+ 'SABER MAIS',
50
+ ];
51
+
52
+ const topCustomerMultipliers: Record<string, number> = {
53
+ VALE: 4.5,
54
+ CALIAN: 2.4,
55
+ ALURA: 1.65,
56
+ FIAP: 1.45,
57
+ };
58
+
59
+ const expenseCategories = [
60
+ { category: 'Folha de Pagamento', costCenter: 'Pessoas', weight: 4.6 },
61
+ { category: 'Marketing', costCenter: 'Comercial', weight: 3.4 },
62
+ { category: 'Tecnologia SaaS', costCenter: 'TI', weight: 3.1 },
63
+ { category: 'Infraestrutura Cloud', costCenter: 'TI', weight: 2.8 },
64
+ {
65
+ category: 'Jurídico e Compliance',
66
+ costCenter: 'Administrativo',
67
+ weight: 2.1,
68
+ },
69
+ {
70
+ category: 'Facilities e Escritório',
71
+ costCenter: 'Administrativo',
72
+ weight: 1.9,
73
+ },
74
+ { category: 'Treinamentos', costCenter: 'Pessoas', weight: 1.7 },
75
+ { category: 'Viagens e Eventos', costCenter: 'Comercial', weight: 1.5 },
76
+ { category: 'Suporte ao Cliente', costCenter: 'Operações', weight: 2.0 },
77
+ {
78
+ category: 'Ferramentas Financeiras',
79
+ costCenter: 'Financeiro',
80
+ weight: 1.3,
81
+ },
82
+ { category: 'Consultorias', costCenter: 'Operações', weight: 1.2 },
83
+ { category: 'Seguros', costCenter: 'Financeiro', weight: 0.9 },
84
+ ];
85
+
86
+ function clamp(value: number, min: number, max: number) {
87
+ return Math.max(min, Math.min(max, value));
88
+ }
89
+
90
+ function monthSeed(year: number, monthIndex: number) {
91
+ return year * 17 + monthIndex * 31;
92
+ }
93
+
94
+ function buildMonthlyDates() {
95
+ const dates: string[] = [];
96
+
97
+ for (let year = 2021; year <= 2026; year += 1) {
98
+ for (let month = 1; month <= 12; month += 1) {
99
+ dates.push(`${year}-${String(month).padStart(2, '0')}-01`);
100
+ }
101
+ }
102
+
103
+ return dates;
104
+ }
105
+
106
+ const monthlyDates = buildMonthlyDates();
107
+
108
+ export const financialResultsMock: FinancialResultEntry[] = monthlyDates.map(
109
+ (date, index) => {
110
+ const d = new Date(`${date}T00:00:00`);
111
+ const year = d.getFullYear();
112
+ const month = d.getMonth();
113
+ const growthFactor = 1 + (year - 2021) * 0.08;
114
+ const seasonality = Math.sin((month / 12) * Math.PI * 2) * 0.18;
115
+ const trend = 1 + index * 0.006;
116
+ const seed = monthSeed(year, month);
117
+
118
+ const faturamento = Math.round(
119
+ (260000 + seasonality * 70000 + (seed % 13) * 3500) * trend * growthFactor
120
+ );
121
+
122
+ const despesas = Math.round(
123
+ (190000 +
124
+ Math.cos((month / 12) * Math.PI * 2) * 45000 +
125
+ (seed % 7) * 4200) *
126
+ (0.93 + (year - 2021) * 0.035)
127
+ );
128
+
129
+ const emprestimoBanco =
130
+ month % 6 === 1 ? Math.round(22000 + (seed % 9) * 1800) : 0;
131
+
132
+ const aporteInvestidor =
133
+ year === 2023 && (month === 2 || month === 8)
134
+ ? 175000
135
+ : year === 2024 && month === 1
136
+ ? 50000
137
+ : year === 2025 && month === 4
138
+ ? 52000
139
+ : 0;
140
+
141
+ return {
142
+ date,
143
+ faturamento: clamp(faturamento, 120000, 530000),
144
+ emprestimoBanco,
145
+ aporteInvestidor,
146
+ despesas: clamp(despesas, 130000, 430000),
147
+ };
148
+ }
149
+ );
150
+
151
+ export const customerRevenueMock: CustomerRevenueEntry[] = monthlyDates.flatMap(
152
+ (date, monthIndex) => {
153
+ const dateObj = new Date(`${date}T00:00:00`);
154
+ const seasonal =
155
+ 1 + Math.sin((dateObj.getMonth() / 12) * Math.PI * 2) * 0.08;
156
+
157
+ return customerNames.map((customer, index) => {
158
+ const base = Math.max(1800, 46000 - index * 1850);
159
+ const priorityBoost = topCustomerMultipliers[customer] || 1;
160
+ const variation = 1 + ((monthIndex + index * 3) % 9) * 0.023;
161
+
162
+ return {
163
+ date,
164
+ customer,
165
+ value: Math.round(base * priorityBoost * seasonal * variation),
166
+ };
167
+ });
168
+ }
169
+ );
170
+
171
+ export const operationalExpensesMock: OperationalExpenseEntry[] =
172
+ monthlyDates.flatMap((date, monthIndex) =>
173
+ expenseCategories.map((entry, categoryIndex) => {
174
+ const monthPulse = 1 + ((monthIndex + categoryIndex) % 7) * 0.04;
175
+ const quarterEffects = [1.04, 0.97, 1.1, 0.99] as const;
176
+ const quarterEffect =
177
+ quarterEffects[Math.floor((monthIndex % 12) / 3)] ?? 1;
178
+
179
+ return {
180
+ date,
181
+ category: entry.category,
182
+ costCenter: entry.costCenter,
183
+ value: Math.round(14000 * entry.weight * monthPulse * quarterEffect),
184
+ };
185
+ })
186
+ );