@hed-hog/finance 0.0.300 → 0.0.302

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 (41) hide show
  1. package/dist/finance.contract-activated.subscriber.d.ts +24 -0
  2. package/dist/finance.contract-activated.subscriber.d.ts.map +1 -0
  3. package/dist/finance.contract-activated.subscriber.js +519 -0
  4. package/dist/finance.contract-activated.subscriber.js.map +1 -0
  5. package/dist/finance.contract-activated.subscriber.spec.d.ts +2 -0
  6. package/dist/finance.contract-activated.subscriber.spec.d.ts.map +1 -0
  7. package/dist/finance.contract-activated.subscriber.spec.js +302 -0
  8. package/dist/finance.contract-activated.subscriber.spec.js.map +1 -0
  9. package/dist/finance.module.d.ts.map +1 -1
  10. package/dist/finance.module.js +6 -1
  11. package/dist/finance.module.js.map +1 -1
  12. package/hedhog/data/menu.yaml +0 -17
  13. package/hedhog/frontend/app/_components/finance-layout.tsx.ejs +108 -0
  14. package/hedhog/frontend/app/accounts-payable/approvals/page.tsx.ejs +51 -69
  15. package/hedhog/frontend/app/accounts-payable/installments/page.tsx.ejs +1312 -1138
  16. package/hedhog/frontend/app/accounts-receivable/collections-default/page.tsx.ejs +288 -268
  17. package/hedhog/frontend/app/accounts-receivable/installments/page.tsx.ejs +1175 -1016
  18. package/hedhog/frontend/app/administration/audit-logs/page.tsx.ejs +157 -173
  19. package/hedhog/frontend/app/administration/categories/page.tsx.ejs +44 -62
  20. package/hedhog/frontend/app/administration/cost-centers/page.tsx.ejs +62 -80
  21. package/hedhog/frontend/app/administration/period-close/page.tsx.ejs +151 -170
  22. package/hedhog/frontend/app/cash-and-banks/bank-accounts/page.tsx.ejs +369 -322
  23. package/hedhog/frontend/app/cash-and-banks/bank-reconciliation/page.tsx.ejs +204 -226
  24. package/hedhog/frontend/app/cash-and-banks/statements/page.tsx.ejs +122 -140
  25. package/hedhog/frontend/app/cash-and-banks/transfers/page.tsx.ejs +31 -49
  26. package/hedhog/frontend/app/page.tsx.ejs +3 -370
  27. package/hedhog/frontend/app/planning/cash-flow-forecast/page.tsx.ejs +150 -182
  28. package/hedhog/frontend/app/planning/receivables-calendar/page.tsx.ejs +52 -70
  29. package/hedhog/frontend/app/planning/scenarios/page.tsx.ejs +101 -95
  30. package/hedhog/frontend/app/reports/actual-vs-forecast/page.tsx.ejs +100 -125
  31. package/hedhog/frontend/app/reports/aging-default/page.tsx.ejs +77 -105
  32. package/hedhog/frontend/app/reports/cash-position/page.tsx.ejs +99 -134
  33. package/hedhog/frontend/app/reports/overview-results/page.tsx.ejs +147 -182
  34. package/hedhog/frontend/app/reports/top-customers/page.tsx.ejs +49 -61
  35. package/hedhog/frontend/app/reports/top-operational-expenses/page.tsx.ejs +49 -67
  36. package/hedhog/frontend/messages/en.json +176 -68
  37. package/hedhog/frontend/messages/pt.json +176 -68
  38. package/package.json +6 -5
  39. package/src/finance.contract-activated.subscriber.spec.ts +392 -0
  40. package/src/finance.contract-activated.subscriber.ts +780 -0
  41. package/src/finance.module.ts +6 -1
@@ -0,0 +1,392 @@
1
+ /// <reference types="jest" />
2
+
3
+ import { FinanceContractActivatedSubscriber } from './finance.contract-activated.subscriber';
4
+
5
+ describe('FinanceContractActivatedSubscriber', () => {
6
+ let integrationApi: {
7
+ subscribeMany: jest.Mock;
8
+ findLinksBySource: jest.Mock;
9
+ createLink: jest.Mock;
10
+ publishEvent: jest.Mock;
11
+ };
12
+ let financeService: {
13
+ createAccountsReceivableTitle: jest.Mock;
14
+ createAccountsPayableTitle: jest.Mock;
15
+ };
16
+ let prisma: {
17
+ financial_title: {
18
+ findFirst: jest.Mock;
19
+ };
20
+ outbox_event: {
21
+ findMany: jest.Mock;
22
+ };
23
+ };
24
+ let subscriber: FinanceContractActivatedSubscriber;
25
+
26
+ beforeEach(() => {
27
+ integrationApi = {
28
+ subscribeMany: jest.fn(),
29
+ findLinksBySource: jest.fn(),
30
+ createLink: jest.fn().mockResolvedValue(undefined),
31
+ publishEvent: jest.fn().mockResolvedValue(undefined),
32
+ };
33
+
34
+ financeService = {
35
+ createAccountsReceivableTitle: jest.fn().mockResolvedValue({
36
+ id: '91',
37
+ parcelas: [
38
+ {
39
+ id: '501',
40
+ numero: 1,
41
+ vencimento: '2025-02-10T00:00:00.000Z',
42
+ valor: 1000,
43
+ },
44
+ {
45
+ id: '502',
46
+ numero: 2,
47
+ vencimento: '2025-03-10T00:00:00.000Z',
48
+ valor: 1000,
49
+ },
50
+ {
51
+ id: '503',
52
+ numero: 3,
53
+ vencimento: '2025-04-10T00:00:00.000Z',
54
+ valor: 1000,
55
+ },
56
+ ],
57
+ }),
58
+ createAccountsPayableTitle: jest.fn().mockResolvedValue({
59
+ id: '190',
60
+ parcelas: [
61
+ {
62
+ id: '801',
63
+ numero: 1,
64
+ vencimento: '2025-02-15T00:00:00.000Z',
65
+ valor: 450,
66
+ },
67
+ ],
68
+ }),
69
+ };
70
+
71
+ prisma = {
72
+ financial_title: {
73
+ findFirst: jest.fn().mockResolvedValue(null),
74
+ },
75
+ outbox_event: {
76
+ findMany: jest.fn().mockResolvedValue([]),
77
+ },
78
+ };
79
+
80
+ subscriber = new FinanceContractActivatedSubscriber(
81
+ integrationApi as any,
82
+ financeService as any,
83
+ prisma as any,
84
+ );
85
+ });
86
+
87
+ afterEach(() => {
88
+ jest.clearAllMocks();
89
+ });
90
+
91
+ it('registers handlers for contract signing and activation', () => {
92
+ subscriber.onModuleInit();
93
+
94
+ const subscriptions = integrationApi.subscribeMany.mock.calls[0][0];
95
+
96
+ expect(subscriptions).toHaveLength(2);
97
+ expect(
98
+ subscriptions.map((entry: { eventName: string }) => entry.eventName),
99
+ ).toEqual(
100
+ expect.arrayContaining([
101
+ 'operations.contract.signed',
102
+ 'operations.contract.activated',
103
+ ]),
104
+ );
105
+ });
106
+
107
+ it('skips duplicate finance generation when a financial title link already exists', async () => {
108
+ subscriber.onModuleInit();
109
+
110
+ const subscriptions = integrationApi.subscribeMany.mock.calls[0][0];
111
+ const activated = subscriptions.find(
112
+ (entry: { eventName: string }) =>
113
+ entry.eventName === 'operations.contract.activated',
114
+ );
115
+
116
+ integrationApi.findLinksBySource.mockResolvedValue([
117
+ {
118
+ targetModule: 'finance',
119
+ targetEntityType: 'financial_title',
120
+ targetEntityId: '91',
121
+ },
122
+ ]);
123
+
124
+ await activated.handler({
125
+ eventName: 'operations.contract.activated',
126
+ sourceModule: 'operations',
127
+ aggregateType: 'contract',
128
+ aggregateId: '77',
129
+ payload: {
130
+ contractId: 77,
131
+ },
132
+ });
133
+
134
+ expect(financeService.createAccountsReceivableTitle).not.toHaveBeenCalled();
135
+ expect(financeService.createAccountsPayableTitle).not.toHaveBeenCalled();
136
+ expect(integrationApi.createLink).not.toHaveBeenCalled();
137
+ expect(integrationApi.publishEvent).not.toHaveBeenCalled();
138
+ });
139
+
140
+ it('recovers an existing title when the finance record already exists but the integration link is missing', async () => {
141
+ subscriber.onModuleInit();
142
+
143
+ const subscriptions = integrationApi.subscribeMany.mock.calls[0][0];
144
+ const activated = subscriptions.find(
145
+ (entry: { eventName: string }) =>
146
+ entry.eventName === 'operations.contract.activated',
147
+ );
148
+
149
+ integrationApi.findLinksBySource.mockResolvedValue([]);
150
+ prisma.financial_title.findFirst.mockResolvedValue({
151
+ id: 91,
152
+ financial_installment: [
153
+ {
154
+ id: 501,
155
+ installment_number: 1,
156
+ due_date: new Date('2025-02-10T00:00:00.000Z'),
157
+ amount_cents: 100000,
158
+ },
159
+ ],
160
+ });
161
+
162
+ await activated.handler({
163
+ eventName: 'operations.contract.activated',
164
+ sourceModule: 'operations',
165
+ aggregateType: 'contract',
166
+ aggregateId: '77',
167
+ payload: {
168
+ contractId: 77,
169
+ locale: 'en',
170
+ correlationId: 'contract:77',
171
+ activatedByUserId: 9,
172
+ contract: {
173
+ code: 'CTR-077',
174
+ name: 'Recovered Contract',
175
+ contractCategory: 'client',
176
+ contractType: 'service_agreement',
177
+ effectiveDate: '2025-02-01',
178
+ financialTerms: [
179
+ {
180
+ label: 'Recovered installment',
181
+ amount: 1000,
182
+ recurrence: 'one_time',
183
+ },
184
+ ],
185
+ },
186
+ receivable: {
187
+ personId: 5,
188
+ documentNumber: 'CTR-077',
189
+ description: 'Recovered Contract',
190
+ },
191
+ },
192
+ });
193
+
194
+ expect(financeService.createAccountsReceivableTitle).not.toHaveBeenCalled();
195
+ expect(integrationApi.createLink).toHaveBeenCalledWith(
196
+ expect.objectContaining({
197
+ targetEntityType: 'financial_title',
198
+ targetEntityId: '91',
199
+ }),
200
+ );
201
+ });
202
+
203
+ it('creates receivable titles, installment links and a finance success event', async () => {
204
+ subscriber.onModuleInit();
205
+
206
+ const subscriptions = integrationApi.subscribeMany.mock.calls[0][0];
207
+ const signed = subscriptions.find(
208
+ (entry: { eventName: string }) =>
209
+ entry.eventName === 'operations.contract.signed',
210
+ );
211
+
212
+ integrationApi.findLinksBySource.mockResolvedValue([]);
213
+
214
+ await signed.handler({
215
+ eventName: 'operations.contract.signed',
216
+ sourceModule: 'operations',
217
+ aggregateType: 'contract',
218
+ aggregateId: '77',
219
+ payload: {
220
+ contractId: 77,
221
+ proposalId: 12,
222
+ locale: 'en',
223
+ correlationId: 'contract:77',
224
+ signedByUserId: 9,
225
+ contract: {
226
+ code: 'CTR-077',
227
+ name: 'Support Contract',
228
+ contractCategory: 'client',
229
+ contractType: 'service_agreement',
230
+ startDate: '2025-02-01',
231
+ endDate: '2025-04-30',
232
+ signedAt: '2025-02-01',
233
+ effectiveDate: '2025-02-01',
234
+ financialTerms: [
235
+ {
236
+ label: 'Monthly retainer',
237
+ amount: 1000,
238
+ recurrence: 'monthly',
239
+ dueDay: 10,
240
+ },
241
+ ],
242
+ },
243
+ receivable: {
244
+ personId: 5,
245
+ documentNumber: 'CTR-077',
246
+ description: 'Support Contract',
247
+ },
248
+ },
249
+ });
250
+
251
+ expect(financeService.createAccountsReceivableTitle).toHaveBeenCalledWith(
252
+ expect.objectContaining({
253
+ person_id: 5,
254
+ document_number: 'CTR-077',
255
+ total_amount: 3000,
256
+ installments: [
257
+ expect.objectContaining({
258
+ installment_number: 1,
259
+ due_date: '2025-02-10',
260
+ amount: 1000,
261
+ }),
262
+ expect.objectContaining({
263
+ installment_number: 2,
264
+ due_date: '2025-03-10',
265
+ amount: 1000,
266
+ }),
267
+ expect.objectContaining({
268
+ installment_number: 3,
269
+ due_date: '2025-04-10',
270
+ amount: 1000,
271
+ }),
272
+ ],
273
+ }),
274
+ 'en',
275
+ 9,
276
+ );
277
+ expect(financeService.createAccountsPayableTitle).not.toHaveBeenCalled();
278
+
279
+ expect(integrationApi.createLink).toHaveBeenCalledTimes(4);
280
+ expect(integrationApi.createLink).toHaveBeenNthCalledWith(
281
+ 1,
282
+ expect.objectContaining({
283
+ sourceModule: 'operations',
284
+ sourceEntityType: 'contract',
285
+ sourceEntityId: '77',
286
+ targetModule: 'finance',
287
+ targetEntityType: 'financial_title',
288
+ targetEntityId: '91',
289
+ metadata: expect.objectContaining({
290
+ eventName: 'operations.contract.signed',
291
+ titleType: 'receivable',
292
+ source_module: 'operations',
293
+ source_entity: 'contract',
294
+ source_id: '77',
295
+ installmentCount: 3,
296
+ }),
297
+ }),
298
+ );
299
+ expect(integrationApi.createLink).toHaveBeenNthCalledWith(
300
+ 2,
301
+ expect.objectContaining({
302
+ targetEntityType: 'financial_installment',
303
+ targetEntityId: '501',
304
+ }),
305
+ );
306
+ expect(integrationApi.createLink).toHaveBeenNthCalledWith(
307
+ 3,
308
+ expect.objectContaining({
309
+ targetEntityType: 'financial_installment',
310
+ targetEntityId: '502',
311
+ }),
312
+ );
313
+ expect(integrationApi.createLink).toHaveBeenNthCalledWith(
314
+ 4,
315
+ expect.objectContaining({
316
+ targetEntityType: 'financial_installment',
317
+ targetEntityId: '503',
318
+ }),
319
+ );
320
+
321
+ expect(integrationApi.publishEvent).toHaveBeenCalledWith(
322
+ expect.objectContaining({
323
+ eventName: 'finance.receivable.created_from_contract',
324
+ sourceModule: 'finance',
325
+ aggregateType: 'financial_title',
326
+ aggregateId: '91',
327
+ payload: expect.objectContaining({
328
+ proposalId: 12,
329
+ source_module: 'operations',
330
+ source_entity: 'contract',
331
+ source_id: '77',
332
+ triggerEvent: 'operations.contract.signed',
333
+ financialTitle: expect.objectContaining({
334
+ id: '91',
335
+ titleType: 'receivable',
336
+ installmentCount: 3,
337
+ }),
338
+ }),
339
+ }),
340
+ );
341
+ });
342
+
343
+ it('routes supplier contracts to accounts payable', async () => {
344
+ subscriber.onModuleInit();
345
+
346
+ const subscriptions = integrationApi.subscribeMany.mock.calls[0][0];
347
+ const activated = subscriptions.find(
348
+ (entry: { eventName: string }) =>
349
+ entry.eventName === 'operations.contract.activated',
350
+ );
351
+
352
+ integrationApi.findLinksBySource.mockResolvedValue([]);
353
+
354
+ await activated.handler({
355
+ eventName: 'operations.contract.activated',
356
+ sourceModule: 'operations',
357
+ aggregateType: 'contract',
358
+ aggregateId: '88',
359
+ payload: {
360
+ contractId: 88,
361
+ locale: 'en',
362
+ activatedByUserId: 11,
363
+ contract: {
364
+ code: 'CTR-088',
365
+ contractCategory: 'supplier',
366
+ contractType: 'service_agreement',
367
+ effectiveDate: '2025-02-15',
368
+ },
369
+ receivable: {
370
+ personId: 14,
371
+ totalAmount: 450,
372
+ documentNumber: 'CTR-088',
373
+ description: 'Supplier onboarding',
374
+ },
375
+ },
376
+ });
377
+
378
+ expect(financeService.createAccountsPayableTitle).toHaveBeenCalledWith(
379
+ expect.objectContaining({
380
+ person_id: 14,
381
+ total_amount: 450,
382
+ }),
383
+ 'en',
384
+ 11,
385
+ );
386
+ expect(integrationApi.publishEvent).toHaveBeenCalledWith(
387
+ expect.objectContaining({
388
+ eventName: 'finance.payable.created_from_contract',
389
+ }),
390
+ );
391
+ });
392
+ });