@hed-hog/operations 0.0.300 → 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 (73) 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 +2442 -119
  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 +34 -0
  25. package/hedhog/data/role_route.yaml +39 -0
  26. package/hedhog/data/route.yaml +130 -0
  27. package/hedhog/frontend/app/_components/collaborator-details-screen.tsx.ejs +8 -6
  28. package/hedhog/frontend/app/_components/collaborator-form-screen.tsx.ejs +1163 -327
  29. package/hedhog/frontend/app/_components/collaborator-select-with-create.tsx.ejs +256 -0
  30. package/hedhog/frontend/app/_components/contract-content-editor.tsx.ejs +258 -0
  31. package/hedhog/frontend/app/_components/contract-creation-wizard.tsx.ejs +631 -0
  32. package/hedhog/frontend/app/_components/contract-details-screen.tsx.ejs +353 -27
  33. package/hedhog/frontend/app/_components/contract-form-screen.tsx.ejs +1926 -87
  34. package/hedhog/frontend/app/_components/contract-template-form-screen.tsx.ejs +526 -0
  35. package/hedhog/frontend/app/_components/contract-template-select-with-create.tsx.ejs +247 -0
  36. package/hedhog/frontend/app/_components/contract-wizard-sheet.tsx.ejs +3520 -0
  37. package/hedhog/frontend/app/_components/department-select-with-create.tsx.ejs +370 -0
  38. package/hedhog/frontend/app/_components/person-select-with-create.tsx.ejs +826 -0
  39. package/hedhog/frontend/app/_components/project-form-screen.tsx.ejs +1251 -364
  40. package/hedhog/frontend/app/_components/section-card.tsx.ejs +48 -13
  41. package/hedhog/frontend/app/_lib/api.ts.ejs +2 -5
  42. package/hedhog/frontend/app/_lib/types.ts.ejs +76 -33
  43. package/hedhog/frontend/app/_lib/utils/format.ts.ejs +85 -8
  44. package/hedhog/frontend/app/approvals/page.tsx.ejs +90 -54
  45. package/hedhog/frontend/app/collaborators/[id]/edit/page.tsx.ejs +2 -2
  46. package/hedhog/frontend/app/collaborators/[id]/page.tsx.ejs +2 -2
  47. package/hedhog/frontend/app/collaborators/page.tsx.ejs +597 -140
  48. package/hedhog/frontend/app/contracts/[id]/edit/page.tsx.ejs +2 -2
  49. package/hedhog/frontend/app/contracts/[id]/page.tsx.ejs +2 -2
  50. package/hedhog/frontend/app/contracts/page.tsx.ejs +941 -262
  51. package/hedhog/frontend/app/contracts/templates/page.tsx.ejs +384 -0
  52. package/hedhog/frontend/app/departments/page.tsx.ejs +442 -0
  53. package/hedhog/frontend/app/page.tsx.ejs +36 -12
  54. package/hedhog/frontend/app/projects/[id]/edit/page.tsx.ejs +2 -2
  55. package/hedhog/frontend/app/projects/new/page.tsx.ejs +2 -2
  56. package/hedhog/frontend/app/projects/page.tsx.ejs +264 -102
  57. package/hedhog/frontend/app/schedule-adjustments/page.tsx.ejs +50 -28
  58. package/hedhog/frontend/app/time-off/page.tsx.ejs +57 -31
  59. package/hedhog/frontend/app/timesheets/page.tsx.ejs +85 -42
  60. package/hedhog/frontend/messages/en.json +473 -12
  61. package/hedhog/frontend/messages/pt.json +528 -66
  62. package/hedhog/table/operations_collaborator.yaml +20 -0
  63. package/hedhog/table/operations_contract.yaml +22 -1
  64. package/hedhog/table/operations_contract_document.yaml +33 -16
  65. package/hedhog/table/operations_contract_template.yaml +58 -0
  66. package/hedhog/table/operations_department.yaml +24 -0
  67. package/package.json +6 -4
  68. package/src/operations.controller.ts +122 -0
  69. package/src/operations.module.ts +6 -2
  70. package/src/operations.proposal.subscriber.spec.ts +121 -0
  71. package/src/operations.proposal.subscriber.ts +86 -0
  72. package/src/operations.service.spec.ts +210 -0
  73. package/src/operations.service.ts +3934 -212
@@ -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
+ });