@hed-hog/operations 0.0.299 → 0.0.301

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 (97) hide show
  1. package/dist/operations.controller.d.ts +713 -31
  2. package/dist/operations.controller.d.ts.map +1 -1
  3. package/dist/operations.controller.js +157 -0
  4. package/dist/operations.controller.js.map +1 -1
  5. package/dist/operations.module.d.ts.map +1 -1
  6. package/dist/operations.module.js +5 -1
  7. package/dist/operations.module.js.map +1 -1
  8. package/dist/operations.proposal.subscriber.d.ts +11 -0
  9. package/dist/operations.proposal.subscriber.d.ts.map +1 -0
  10. package/dist/operations.proposal.subscriber.js +80 -0
  11. package/dist/operations.proposal.subscriber.js.map +1 -0
  12. package/dist/operations.proposal.subscriber.spec.d.ts +2 -0
  13. package/dist/operations.proposal.subscriber.spec.d.ts.map +1 -0
  14. package/dist/operations.proposal.subscriber.spec.js +88 -0
  15. package/dist/operations.proposal.subscriber.spec.js.map +1 -0
  16. package/dist/operations.service.d.ts +490 -46
  17. package/dist/operations.service.d.ts.map +1 -1
  18. package/dist/operations.service.js +3590 -1267
  19. package/dist/operations.service.js.map +1 -1
  20. package/dist/operations.service.spec.d.ts +2 -0
  21. package/dist/operations.service.spec.d.ts.map +1 -0
  22. package/dist/operations.service.spec.js +159 -0
  23. package/dist/operations.service.spec.js.map +1 -0
  24. package/hedhog/data/menu.yaml +232 -198
  25. package/hedhog/data/role.yaml +23 -23
  26. package/hedhog/data/role_route.yaml +39 -0
  27. package/hedhog/data/route.yaml +447 -317
  28. package/hedhog/frontend/app/_components/collaborator-details-screen.tsx.ejs +8 -6
  29. package/hedhog/frontend/app/_components/collaborator-form-screen.tsx.ejs +1163 -327
  30. package/hedhog/frontend/app/_components/collaborator-select-with-create.tsx.ejs +256 -0
  31. package/hedhog/frontend/app/_components/contract-content-editor.tsx.ejs +258 -0
  32. package/hedhog/frontend/app/_components/contract-creation-wizard.tsx.ejs +631 -0
  33. package/hedhog/frontend/app/_components/contract-details-screen.tsx.ejs +353 -27
  34. package/hedhog/frontend/app/_components/contract-form-screen.tsx.ejs +1926 -87
  35. package/hedhog/frontend/app/_components/contract-template-form-screen.tsx.ejs +526 -0
  36. package/hedhog/frontend/app/_components/contract-template-select-with-create.tsx.ejs +247 -0
  37. package/hedhog/frontend/app/_components/contract-wizard-sheet.tsx.ejs +3520 -0
  38. package/hedhog/frontend/app/_components/department-select-with-create.tsx.ejs +370 -0
  39. package/hedhog/frontend/app/_components/person-select-with-create.tsx.ejs +826 -0
  40. package/hedhog/frontend/app/_components/project-form-screen.tsx.ejs +1251 -364
  41. package/hedhog/frontend/app/_components/section-card.tsx.ejs +48 -13
  42. package/hedhog/frontend/app/_lib/api.ts.ejs +2 -5
  43. package/hedhog/frontend/app/_lib/types.ts.ejs +76 -33
  44. package/hedhog/frontend/app/_lib/utils/format.ts.ejs +85 -8
  45. package/hedhog/frontend/app/approvals/page.tsx.ejs +90 -54
  46. package/hedhog/frontend/app/collaborators/[id]/edit/page.tsx.ejs +2 -2
  47. package/hedhog/frontend/app/collaborators/[id]/page.tsx.ejs +2 -2
  48. package/hedhog/frontend/app/collaborators/page.tsx.ejs +597 -140
  49. package/hedhog/frontend/app/contracts/[id]/edit/page.tsx.ejs +2 -2
  50. package/hedhog/frontend/app/contracts/[id]/page.tsx.ejs +2 -2
  51. package/hedhog/frontend/app/contracts/page.tsx.ejs +941 -262
  52. package/hedhog/frontend/app/contracts/templates/page.tsx.ejs +384 -0
  53. package/hedhog/frontend/app/departments/page.tsx.ejs +442 -0
  54. package/hedhog/frontend/app/page.tsx.ejs +36 -12
  55. package/hedhog/frontend/app/projects/[id]/edit/page.tsx.ejs +2 -2
  56. package/hedhog/frontend/app/projects/new/page.tsx.ejs +2 -2
  57. package/hedhog/frontend/app/projects/page.tsx.ejs +264 -102
  58. package/hedhog/frontend/app/schedule-adjustments/page.tsx.ejs +50 -28
  59. package/hedhog/frontend/app/time-off/page.tsx.ejs +57 -31
  60. package/hedhog/frontend/app/timesheets/page.tsx.ejs +85 -42
  61. package/hedhog/frontend/messages/en.json +473 -12
  62. package/hedhog/frontend/messages/pt.json +528 -66
  63. package/hedhog/table/operations_approval.yaml +49 -49
  64. package/hedhog/table/operations_approval_history.yaml +29 -29
  65. package/hedhog/table/operations_collaborator.yaml +87 -67
  66. package/hedhog/table/operations_collaborator_schedule_day.yaml +34 -34
  67. package/hedhog/table/operations_contract.yaml +121 -100
  68. package/hedhog/table/operations_contract_document.yaml +40 -23
  69. package/hedhog/table/operations_contract_financial_term.yaml +40 -40
  70. package/hedhog/table/operations_contract_history.yaml +27 -27
  71. package/hedhog/table/operations_contract_party.yaml +46 -46
  72. package/hedhog/table/operations_contract_revision.yaml +38 -38
  73. package/hedhog/table/operations_contract_signature.yaml +38 -38
  74. package/hedhog/table/operations_contract_template.yaml +58 -0
  75. package/hedhog/table/operations_department.yaml +24 -0
  76. package/hedhog/table/operations_project.yaml +54 -54
  77. package/hedhog/table/operations_project_assignment.yaml +55 -55
  78. package/hedhog/table/operations_schedule_adjustment_day.yaml +34 -34
  79. package/hedhog/table/operations_schedule_adjustment_request.yaml +53 -53
  80. package/hedhog/table/operations_time_off_request.yaml +57 -57
  81. package/hedhog/table/operations_timesheet.yaml +41 -41
  82. package/hedhog/table/operations_timesheet_entry.yaml +40 -40
  83. package/package.json +5 -3
  84. package/src/operations.controller.ts +304 -182
  85. package/src/operations.module.ts +26 -22
  86. package/src/operations.proposal.subscriber.spec.ts +121 -0
  87. package/src/operations.proposal.subscriber.ts +86 -0
  88. package/src/operations.service.spec.ts +210 -0
  89. package/src/operations.service.ts +7317 -3595
  90. package/dist/operations-data.controller.d.ts +0 -139
  91. package/dist/operations-data.controller.d.ts.map +0 -1
  92. package/dist/operations-data.controller.js +0 -113
  93. package/dist/operations-data.controller.js.map +0 -1
  94. package/dist/operations-growth.controller.d.ts +0 -48
  95. package/dist/operations-growth.controller.d.ts.map +0 -1
  96. package/dist/operations-growth.controller.js +0 -90
  97. package/dist/operations-growth.controller.js.map +0 -1
@@ -0,0 +1,86 @@
1
+ import {
2
+ IntegrationDeveloperApiService,
3
+ LinkType,
4
+ } from '@hed-hog/core';
5
+ import { Injectable, Logger, OnModuleInit } from '@nestjs/common';
6
+ import { OperationsService } from './operations.service';
7
+
8
+ @Injectable()
9
+ export class OperationsProposalSubscriber implements OnModuleInit {
10
+ private readonly logger = new Logger(OperationsProposalSubscriber.name);
11
+
12
+ constructor(
13
+ private readonly integrationApi: IntegrationDeveloperApiService,
14
+ private readonly operationsService: OperationsService,
15
+ ) {}
16
+
17
+ onModuleInit(): void {
18
+ const handler = async (event) => {
19
+ const payload = (event.payload || {}) as {
20
+ proposalId?: number;
21
+ };
22
+ const sourceEntityType =
23
+ String(event.aggregateType || '').trim() || 'proposal';
24
+ const sourceEntityId =
25
+ String(payload.proposalId || event.aggregateId || '').trim() ||
26
+ event.aggregateId;
27
+
28
+ if (!sourceEntityId) {
29
+ throw new Error(`Missing proposal aggregate id for ${event.eventName}.`);
30
+ }
31
+
32
+ const existingLinks = await this.integrationApi.findLinksBySource({
33
+ module: event.sourceModule,
34
+ entityType: sourceEntityType,
35
+ entityId: sourceEntityId,
36
+ });
37
+
38
+ const alreadyLinked = existingLinks.some(
39
+ (link) =>
40
+ link.targetModule === 'operations' &&
41
+ link.targetEntityType === 'contract',
42
+ );
43
+
44
+ if (alreadyLinked) {
45
+ this.logger.debug(
46
+ `Skipping duplicate contract generation for ${sourceEntityType}:${sourceEntityId} from ${event.eventName}`,
47
+ );
48
+ return;
49
+ }
50
+
51
+ const createdContract =
52
+ await this.operationsService.createContractFromProposalIntegration(
53
+ event.payload || {},
54
+ );
55
+
56
+ await this.integrationApi.createLink({
57
+ sourceModule: event.sourceModule,
58
+ sourceEntityType,
59
+ sourceEntityId,
60
+ targetModule: 'operations',
61
+ targetEntityType: 'contract',
62
+ targetEntityId: String(createdContract.id),
63
+ linkType: LinkType.REFERENCE,
64
+ metadata: {
65
+ eventName: event.eventName,
66
+ contractCode: createdContract.code,
67
+ },
68
+ });
69
+ };
70
+
71
+ this.integrationApi.subscribeMany([
72
+ {
73
+ eventName: 'contact.proposal.convert_requested',
74
+ consumerName: 'operations.contract-from-proposal',
75
+ priority: 10,
76
+ handler,
77
+ },
78
+ {
79
+ eventName: 'contact.proposal.approved',
80
+ consumerName: 'operations.contract-from-proposal-legacy',
81
+ priority: 5,
82
+ handler,
83
+ },
84
+ ]);
85
+ }
86
+ }
@@ -0,0 +1,210 @@
1
+ /// <reference types="jest" />
2
+
3
+ import { BadRequestException } from '@nestjs/common';
4
+ import { OperationsService } from './operations.service';
5
+
6
+ describe('OperationsService proposal integration', () => {
7
+ let service: OperationsService;
8
+ let prisma: { $transaction: jest.Mock };
9
+ let integrationApi: { publishEvent: jest.Mock };
10
+
11
+ beforeEach(() => {
12
+ prisma = {
13
+ $transaction: jest.fn(),
14
+ };
15
+
16
+ integrationApi = {
17
+ publishEvent: jest.fn().mockResolvedValue(undefined),
18
+ };
19
+
20
+ service = new OperationsService(
21
+ prisma as any,
22
+ {} as any,
23
+ integrationApi as any,
24
+ {} as any,
25
+ {} as any,
26
+ );
27
+
28
+ jest.spyOn(service as any, 'generateContractCode').mockResolvedValue('CTR-001');
29
+ jest.spyOn(service as any, 'replaceContractParties').mockResolvedValue(undefined);
30
+ jest
31
+ .spyOn(service as any, 'replaceContractFinancialTerms')
32
+ .mockResolvedValue(undefined);
33
+ jest.spyOn(service as any, 'replaceContractRevisions').mockResolvedValue(undefined);
34
+ jest.spyOn(service as any, 'insertContractHistory').mockResolvedValue(undefined);
35
+ });
36
+
37
+ afterEach(() => {
38
+ jest.restoreAllMocks();
39
+ });
40
+
41
+ it('throws when proposalId is missing', async () => {
42
+ await expect(
43
+ service.createContractFromProposalIntegration({}),
44
+ ).rejects.toThrow(BadRequestException);
45
+ });
46
+
47
+ it('returns the existing contract when the CRM proposal was already converted', async () => {
48
+ const tx = {
49
+ $queryRawUnsafe: jest
50
+ .fn()
51
+ .mockResolvedValueOnce([])
52
+ .mockResolvedValueOnce([{ id: 42, code: 'CTR-EXISTING' }]),
53
+ };
54
+
55
+ prisma.$transaction.mockImplementation(async (callback: (client: unknown) => unknown) =>
56
+ callback(tx),
57
+ );
58
+
59
+ const result = await service.createContractFromProposalIntegration({
60
+ proposalId: 1001,
61
+ title: 'Already converted proposal',
62
+ });
63
+
64
+ expect(result).toEqual({
65
+ id: 42,
66
+ code: 'CTR-EXISTING',
67
+ });
68
+ expect((service as any).generateContractCode).not.toHaveBeenCalled();
69
+ expect((service as any).replaceContractParties).not.toHaveBeenCalled();
70
+ expect(integrationApi.publishEvent).not.toHaveBeenCalled();
71
+ });
72
+
73
+ it('creates a draft contract with CRM origin metadata and emits an event', async () => {
74
+ const tx = {
75
+ $queryRawUnsafe: jest
76
+ .fn()
77
+ .mockResolvedValueOnce([])
78
+ .mockResolvedValueOnce([])
79
+ .mockResolvedValueOnce([{ id: 77 }]),
80
+ };
81
+
82
+ prisma.$transaction.mockImplementation(async (callback: (client: unknown) => unknown) =>
83
+ callback(tx),
84
+ );
85
+
86
+ const payload = {
87
+ proposalId: 1001,
88
+ proposalRevisionId: 21,
89
+ personId: 5,
90
+ approvedByUserId: 9,
91
+ correlationId: 'proposal:1001',
92
+ code: 'P-1001',
93
+ title: 'Implementation Proposal',
94
+ total: 1234,
95
+ currency: 'USD',
96
+ locale: 'en',
97
+ commercialTerms: {
98
+ contractCategory: 'client' as const,
99
+ contractType: 'service_agreement' as const,
100
+ billingModel: 'monthly_retainer' as const,
101
+ validFrom: '2025-01-10',
102
+ validUntil: '2025-12-31',
103
+ notes: 'Annual support agreement',
104
+ },
105
+ person: {
106
+ id: 5,
107
+ name: 'Acme Contact',
108
+ tradeName: 'Acme Ltd',
109
+ email: 'ops@acme.test',
110
+ phone: '555-0100',
111
+ document: '12.345.678/0001-90',
112
+ },
113
+ revision: {
114
+ id: 21,
115
+ title: 'Revision 1',
116
+ summary: 'Approved commercial terms',
117
+ contentHtml: '<p>draft</p>',
118
+ },
119
+ items: [
120
+ {
121
+ name: 'Monthly retainer',
122
+ description: 'Support hours',
123
+ amount: 1000,
124
+ recurrence: 'monthly' as const,
125
+ },
126
+ {
127
+ name: 'Setup fee',
128
+ amount: 234,
129
+ recurrence: 'one_time' as const,
130
+ },
131
+ ],
132
+ };
133
+
134
+ const result = await service.createContractFromProposalIntegration(payload);
135
+
136
+ expect(result).toEqual({
137
+ id: 77,
138
+ code: 'CTR-001',
139
+ });
140
+
141
+ const insertCall = tx.$queryRawUnsafe.mock.calls[2];
142
+ expect(insertCall[0]).toContain('INSERT INTO operations_contract');
143
+ expect(insertCall).toContain('crm_proposal');
144
+ expect(insertCall).toContain('1001');
145
+
146
+ expect((service as any).replaceContractParties).toHaveBeenCalledWith(
147
+ tx,
148
+ 77,
149
+ [
150
+ expect.objectContaining({
151
+ partyRole: 'client',
152
+ displayName: 'Acme Ltd',
153
+ isPrimary: true,
154
+ }),
155
+ ],
156
+ );
157
+ expect((service as any).replaceContractFinancialTerms).toHaveBeenCalledWith(
158
+ tx,
159
+ 77,
160
+ expect.arrayContaining([
161
+ expect.objectContaining({
162
+ label: 'Monthly retainer',
163
+ amount: 1000,
164
+ recurrence: 'monthly',
165
+ }),
166
+ expect.objectContaining({
167
+ label: 'Setup fee',
168
+ amount: 234,
169
+ recurrence: 'one_time',
170
+ }),
171
+ ]),
172
+ );
173
+ expect((service as any).replaceContractRevisions).toHaveBeenCalledWith(
174
+ tx,
175
+ 77,
176
+ [
177
+ expect.objectContaining({
178
+ title: 'Revision 1',
179
+ status: 'draft',
180
+ }),
181
+ ],
182
+ );
183
+ expect((service as any).insertContractHistory).toHaveBeenCalledWith(
184
+ tx,
185
+ 77,
186
+ 9,
187
+ 'created',
188
+ 'Draft contract generated from CRM proposal P-1001.',
189
+ expect.any(String),
190
+ );
191
+
192
+ expect(integrationApi.publishEvent).toHaveBeenCalledWith(
193
+ expect.objectContaining({
194
+ eventName: 'operations.contract.created',
195
+ payload: expect.objectContaining({
196
+ proposalId: 1001,
197
+ contract: expect.objectContaining({
198
+ id: 77,
199
+ originType: 'crm_proposal',
200
+ originId: '1001',
201
+ contractType: 'service_agreement',
202
+ }),
203
+ }),
204
+ }),
205
+ expect.objectContaining({
206
+ persistenceClient: tx,
207
+ }),
208
+ );
209
+ });
210
+ });