@hed-hog/finance 0.0.278 → 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,10 +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
- <<<<<<< HEAD
137
- =======
138
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 |
139
- >>>>>>> d51b9b5865563d7f3890f049742d5c20ae326ff9
140
137
  | GET | /finance/statements/export | Sim | Exporta extratos em CSV | Query: `bank_account_id` (obrigatório), `search` | CSV para download | 400 BadRequest, 401 |
141
138
  | POST | /finance/statements/import | Sim | Importa extrato bancário | Multipart file + Body: `bank_account_id` (obrigatório) | Importação realizada | 400 BadRequest, 401 |
142
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 |
@@ -248,6 +245,18 @@ O módulo financeiro gerencia o ciclo completo da gestão financeira corporativa
248
245
  }
249
246
  ```
250
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
+
251
260
  ### CreateBankStatementAdjustmentDto
252
261
 
253
262
  ```ts
@@ -260,24 +269,20 @@ O módulo financeiro gerencia o ciclo completo da gestão financeira corporativa
260
269
  }
261
270
  ```
262
271
 
263
- ### CreateTransferDto
272
+ ### CreateCostCenterDto
264
273
 
265
274
  ```ts
266
275
  {
267
- source_account_id: number; // obrigatório
268
- destination_account_id: number; // obrigatório
269
- date: string; // obrigatório
270
- amount: number; // obrigatório, >= 0.01
271
- description?: string;
276
+ name: string; // obrigatório
272
277
  }
273
278
  ```
274
279
 
275
- ### CreateFinanceTagDto
280
+ ### UpdateCostCenterDto
276
281
 
277
282
  ```ts
278
283
  {
279
- name: string; // obrigatório, string não vazia
280
- color?: string; // opcional, cor hexadecimal válida
284
+ name?: string;
285
+ status?: 'active' | 'inactive';
281
286
  }
282
287
  ```
283
288
 
@@ -311,23 +316,6 @@ O módulo financeiro gerencia o ciclo completo da gestão financeira corporativa
311
316
  }
312
317
  ```
313
318
 
314
- ### CreateCostCenterDto
315
-
316
- ```ts
317
- {
318
- name: string; // obrigatório
319
- }
320
- ```
321
-
322
- ### UpdateCostCenterDto
323
-
324
- ```ts
325
- {
326
- name?: string;
327
- status?: 'active' | 'inactive';
328
- }
329
- ```
330
-
331
319
  ### SendCollectionDto
332
320
 
333
321
  ```ts
@@ -375,6 +363,38 @@ O módulo financeiro gerencia o ciclo completo da gestão financeira corporativa
375
363
  }
376
364
  ```
377
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
+
378
398
  ---
379
399
 
380
400
  ## 6. Erros comuns
@@ -754,6 +774,22 @@ Form-data:
754
774
  - bank_account_id: 10
755
775
  ```
756
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
+
757
793
  ### Estornar liquidação
758
794
 
759
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
+ );