@hed-hog/operations 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.
- package/dist/operations.controller.d.ts +713 -31
- package/dist/operations.controller.d.ts.map +1 -1
- package/dist/operations.controller.js +157 -0
- package/dist/operations.controller.js.map +1 -1
- package/dist/operations.module.d.ts.map +1 -1
- package/dist/operations.module.js +5 -1
- package/dist/operations.module.js.map +1 -1
- package/dist/operations.proposal.subscriber.d.ts +11 -0
- package/dist/operations.proposal.subscriber.d.ts.map +1 -0
- package/dist/operations.proposal.subscriber.js +80 -0
- package/dist/operations.proposal.subscriber.js.map +1 -0
- package/dist/operations.proposal.subscriber.spec.d.ts +2 -0
- package/dist/operations.proposal.subscriber.spec.d.ts.map +1 -0
- package/dist/operations.proposal.subscriber.spec.js +88 -0
- package/dist/operations.proposal.subscriber.spec.js.map +1 -0
- package/dist/operations.service.d.ts +491 -46
- package/dist/operations.service.d.ts.map +1 -1
- package/dist/operations.service.js +2484 -121
- package/dist/operations.service.js.map +1 -1
- package/dist/operations.service.spec.d.ts +2 -0
- package/dist/operations.service.spec.d.ts.map +1 -0
- package/dist/operations.service.spec.js +159 -0
- package/dist/operations.service.spec.js.map +1 -0
- package/hedhog/data/menu.yaml +35 -22
- package/hedhog/data/role_route.yaml +39 -0
- package/hedhog/data/route.yaml +130 -0
- package/hedhog/frontend/app/_components/collaborator-details-screen.tsx.ejs +8 -6
- package/hedhog/frontend/app/_components/collaborator-form-screen.tsx.ejs +1163 -327
- package/hedhog/frontend/app/_components/collaborator-select-with-create.tsx.ejs +256 -0
- package/hedhog/frontend/app/_components/contract-content-editor.tsx.ejs +258 -0
- package/hedhog/frontend/app/_components/contract-creation-wizard.tsx.ejs +631 -0
- package/hedhog/frontend/app/_components/contract-details-screen.tsx.ejs +353 -27
- package/hedhog/frontend/app/_components/contract-form-screen.tsx.ejs +1926 -87
- package/hedhog/frontend/app/_components/contract-template-form-screen.tsx.ejs +526 -0
- package/hedhog/frontend/app/_components/contract-template-select-with-create.tsx.ejs +247 -0
- package/hedhog/frontend/app/_components/contract-wizard-sheet.tsx.ejs +3520 -0
- package/hedhog/frontend/app/_components/department-select-with-create.tsx.ejs +370 -0
- package/hedhog/frontend/app/_components/person-select-with-create.tsx.ejs +826 -0
- package/hedhog/frontend/app/_components/project-form-screen.tsx.ejs +1251 -364
- package/hedhog/frontend/app/_components/section-card.tsx.ejs +48 -13
- package/hedhog/frontend/app/_lib/api.ts.ejs +2 -5
- package/hedhog/frontend/app/_lib/types.ts.ejs +76 -33
- package/hedhog/frontend/app/_lib/utils/format.ts.ejs +85 -8
- package/hedhog/frontend/app/approvals/page.tsx.ejs +90 -54
- package/hedhog/frontend/app/collaborators/[id]/edit/page.tsx.ejs +2 -2
- package/hedhog/frontend/app/collaborators/[id]/page.tsx.ejs +2 -2
- package/hedhog/frontend/app/collaborators/page.tsx.ejs +597 -140
- package/hedhog/frontend/app/contracts/[id]/edit/page.tsx.ejs +2 -2
- package/hedhog/frontend/app/contracts/[id]/page.tsx.ejs +2 -2
- package/hedhog/frontend/app/contracts/page.tsx.ejs +941 -262
- package/hedhog/frontend/app/contracts/templates/page.tsx.ejs +384 -0
- package/hedhog/frontend/app/departments/page.tsx.ejs +442 -0
- package/hedhog/frontend/app/page.tsx.ejs +3 -317
- package/hedhog/frontend/app/projects/[id]/edit/page.tsx.ejs +2 -2
- package/hedhog/frontend/app/projects/new/page.tsx.ejs +2 -2
- package/hedhog/frontend/app/projects/page.tsx.ejs +264 -102
- package/hedhog/frontend/app/schedule-adjustments/page.tsx.ejs +50 -28
- package/hedhog/frontend/app/time-off/page.tsx.ejs +57 -31
- package/hedhog/frontend/app/timesheets/page.tsx.ejs +85 -42
- package/hedhog/frontend/messages/en.json +473 -12
- package/hedhog/frontend/messages/pt.json +528 -66
- package/hedhog/table/operations_collaborator.yaml +20 -0
- package/hedhog/table/operations_contract.yaml +22 -1
- package/hedhog/table/operations_contract_document.yaml +33 -16
- package/hedhog/table/operations_contract_template.yaml +58 -0
- package/hedhog/table/operations_department.yaml +24 -0
- package/package.json +7 -5
- package/src/operations.controller.ts +122 -0
- package/src/operations.module.ts +6 -2
- package/src/operations.proposal.subscriber.spec.ts +121 -0
- package/src/operations.proposal.subscriber.ts +86 -0
- package/src/operations.service.spec.ts +210 -0
- package/src/operations.service.ts +4026 -241
|
@@ -8,6 +8,10 @@ var __decorate = (this && this.__decorate) || function (decorators, target, key,
|
|
|
8
8
|
var __metadata = (this && this.__metadata) || function (k, v) {
|
|
9
9
|
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
|
|
10
10
|
};
|
|
11
|
+
var __param = (this && this.__param) || function (paramIndex, decorator) {
|
|
12
|
+
return function (target, key) { decorator(target, key, paramIndex); }
|
|
13
|
+
};
|
|
14
|
+
var OperationsService_1;
|
|
11
15
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
12
16
|
exports.OperationsService = void 0;
|
|
13
17
|
const api_prisma_1 = require("@hed-hog/api-prisma");
|
|
@@ -16,10 +20,99 @@ const common_1 = require("@nestjs/common");
|
|
|
16
20
|
const COLLABORATOR_ROLE = 'admin-operations-collaborator';
|
|
17
21
|
const SUPERVISOR_ROLE = 'admin-operations-supervisor';
|
|
18
22
|
const DIRECTOR_ROLE = 'admin-operations-director';
|
|
19
|
-
|
|
20
|
-
|
|
23
|
+
const CONTRACT_CATEGORY_VALUES = [
|
|
24
|
+
'employee',
|
|
25
|
+
'contractor',
|
|
26
|
+
'client',
|
|
27
|
+
'supplier',
|
|
28
|
+
'vendor',
|
|
29
|
+
'partner',
|
|
30
|
+
'internal',
|
|
31
|
+
'other',
|
|
32
|
+
];
|
|
33
|
+
const CONTRACT_TYPE_VALUES = [
|
|
34
|
+
'clt',
|
|
35
|
+
'pj',
|
|
36
|
+
'freelancer_agreement',
|
|
37
|
+
'service_agreement',
|
|
38
|
+
'fixed_term',
|
|
39
|
+
'recurring_service',
|
|
40
|
+
'nda',
|
|
41
|
+
'amendment',
|
|
42
|
+
'addendum',
|
|
43
|
+
'other',
|
|
44
|
+
];
|
|
45
|
+
const BILLING_MODEL_VALUES = [
|
|
46
|
+
'time_and_material',
|
|
47
|
+
'monthly_retainer',
|
|
48
|
+
'fixed_price',
|
|
49
|
+
];
|
|
50
|
+
const SIGNATURE_STATUS_VALUES = [
|
|
51
|
+
'not_started',
|
|
52
|
+
'pending',
|
|
53
|
+
'partially_signed',
|
|
54
|
+
'signed',
|
|
55
|
+
'expired',
|
|
56
|
+
];
|
|
57
|
+
const CONTRACT_STATUS_VALUES = [
|
|
58
|
+
'draft',
|
|
59
|
+
'under_review',
|
|
60
|
+
'active',
|
|
61
|
+
'renewal',
|
|
62
|
+
'expired',
|
|
63
|
+
'closed',
|
|
64
|
+
'archived',
|
|
65
|
+
];
|
|
66
|
+
const CONTRACT_CREATION_MODE_VALUES = [
|
|
67
|
+
'blank',
|
|
68
|
+
'template',
|
|
69
|
+
'upload',
|
|
70
|
+
'duplicate',
|
|
71
|
+
];
|
|
72
|
+
const ORIGIN_TYPE_VALUES = [
|
|
73
|
+
'manual',
|
|
74
|
+
'employee_hiring',
|
|
75
|
+
'client_project',
|
|
76
|
+
'crm_proposal',
|
|
77
|
+
];
|
|
78
|
+
const CONTRACT_DOCUMENT_TYPE_VALUES = [
|
|
79
|
+
'source_upload',
|
|
80
|
+
'generated_pdf',
|
|
81
|
+
'attachment',
|
|
82
|
+
'other',
|
|
83
|
+
];
|
|
84
|
+
const CONTRACT_DOCUMENT_EXTRACTION_STATUS_VALUES = [
|
|
85
|
+
'pending',
|
|
86
|
+
'processing',
|
|
87
|
+
'completed',
|
|
88
|
+
'failed',
|
|
89
|
+
'skipped',
|
|
90
|
+
];
|
|
91
|
+
const PARTY_ROLE_VALUES = [
|
|
92
|
+
'employee',
|
|
93
|
+
'employer',
|
|
94
|
+
'client',
|
|
95
|
+
'supplier',
|
|
96
|
+
'vendor',
|
|
97
|
+
'partner',
|
|
98
|
+
'witness',
|
|
99
|
+
'internal_owner',
|
|
100
|
+
'other',
|
|
101
|
+
];
|
|
102
|
+
const PARTY_TYPE_VALUES = ['individual', 'company', 'internal_team', 'other'];
|
|
103
|
+
const SIGNATURE_ITEM_STATUS_VALUES = ['pending', 'signed', 'rejected'];
|
|
104
|
+
const FINANCIAL_TERM_TYPE_VALUES = ['value', 'payment', 'revenue', 'fine', 'other'];
|
|
105
|
+
const RECURRENCE_VALUES = ['one_time', 'monthly', 'quarterly', 'yearly', 'other'];
|
|
106
|
+
const REVISION_TYPE_VALUES = ['amendment', 'renewal', 'revision', 'addendum', 'other'];
|
|
107
|
+
const REVISION_STATUS_VALUES = ['draft', 'active', 'completed', 'cancelled'];
|
|
108
|
+
let OperationsService = OperationsService_1 = class OperationsService {
|
|
109
|
+
constructor(prisma, aiService, integrationApi, fileService, settingService) {
|
|
21
110
|
this.prisma = prisma;
|
|
111
|
+
this.aiService = aiService;
|
|
22
112
|
this.integrationApi = integrationApi;
|
|
113
|
+
this.fileService = fileService;
|
|
114
|
+
this.settingService = settingService;
|
|
115
|
+
this.logger = new common_1.Logger(OperationsService_1.name);
|
|
23
116
|
}
|
|
24
117
|
async getDashboard(userId) {
|
|
25
118
|
var _a, _b, _c, _d, _e, _f, _g;
|
|
@@ -97,10 +190,14 @@ let OperationsService = class OperationsService {
|
|
|
97
190
|
const filter = this.buildIdFilter(actor.visibleCollaboratorIds, 'c.id', actor.isDirector);
|
|
98
191
|
return this.queryRows(`SELECT c.id,
|
|
99
192
|
c.user_id AS "userId",
|
|
193
|
+
c.person_id AS "personId",
|
|
100
194
|
c.code,
|
|
101
195
|
c.collaborator_type AS "collaboratorType",
|
|
102
|
-
c.display_name AS "displayName",
|
|
103
|
-
|
|
196
|
+
COALESCE(NULLIF(c.display_name, ''), person_record.name) AS "displayName",
|
|
197
|
+
person_record.name AS "personName",
|
|
198
|
+
person_record.avatar_id AS "personAvatarId",
|
|
199
|
+
c.department_id AS "departmentId",
|
|
200
|
+
COALESCE(NULLIF(department_record.name, ''), NULLIF(c.department, '')) AS "department",
|
|
104
201
|
c.title,
|
|
105
202
|
c.level_label AS "levelLabel",
|
|
106
203
|
c.weekly_capacity_hours AS "weeklyCapacityHours",
|
|
@@ -114,6 +211,11 @@ let OperationsService = class OperationsService {
|
|
|
114
211
|
hiring_contract.status AS "contractStatus",
|
|
115
212
|
COUNT(DISTINCT pa.id)::int AS "activeAssignments"
|
|
116
213
|
FROM operations_collaborator c
|
|
214
|
+
LEFT JOIN person person_record
|
|
215
|
+
ON person_record.id = c.person_id
|
|
216
|
+
LEFT JOIN operations_department department_record
|
|
217
|
+
ON department_record.id = c.department_id
|
|
218
|
+
AND department_record.deleted_at IS NULL
|
|
117
219
|
LEFT JOIN operations_collaborator s
|
|
118
220
|
ON s.id = c.supervisor_collaborator_id
|
|
119
221
|
LEFT JOIN operations_project_assignment pa
|
|
@@ -131,8 +233,8 @@ let OperationsService = class OperationsService {
|
|
|
131
233
|
) hiring_contract ON TRUE
|
|
132
234
|
WHERE c.deleted_at IS NULL
|
|
133
235
|
AND ${filter.clause}
|
|
134
|
-
GROUP BY c.id, s.id, hiring_contract.id, hiring_contract.status
|
|
135
|
-
ORDER BY c.display_name ASC`, filter.params);
|
|
236
|
+
GROUP BY c.id, person_record.id, department_record.id, s.id, hiring_contract.id, hiring_contract.status
|
|
237
|
+
ORDER BY COALESCE(NULLIF(c.display_name, ''), person_record.name) ASC`, filter.params);
|
|
136
238
|
}
|
|
137
239
|
async getMyCollaborator(userId) {
|
|
138
240
|
const actor = await this.getActorContext(userId);
|
|
@@ -169,10 +271,14 @@ let OperationsService = class OperationsService {
|
|
|
169
271
|
const teamFilter = this.buildIdFilter(actor.teamCollaboratorIds, 'c.id', false);
|
|
170
272
|
const teamMembers = await this.queryRows(`SELECT c.id,
|
|
171
273
|
c.user_id AS "userId",
|
|
274
|
+
c.person_id AS "personId",
|
|
172
275
|
c.code,
|
|
173
|
-
c.display_name AS "displayName",
|
|
276
|
+
COALESCE(NULLIF(c.display_name, ''), person_record.name) AS "displayName",
|
|
277
|
+
person_record.name AS "personName",
|
|
278
|
+
person_record.avatar_id AS "personAvatarId",
|
|
174
279
|
c.collaborator_type AS "collaboratorType",
|
|
175
|
-
c.
|
|
280
|
+
c.department_id AS "departmentId",
|
|
281
|
+
COALESCE(NULLIF(department_record.name, ''), NULLIF(c.department, '')) AS "department",
|
|
176
282
|
c.title,
|
|
177
283
|
c.status,
|
|
178
284
|
COUNT(DISTINCT pa.id)::int AS "activeAssignments",
|
|
@@ -180,6 +286,11 @@ let OperationsService = class OperationsService {
|
|
|
180
286
|
COUNT(DISTINCT tor.id) FILTER (WHERE tor.status = 'submitted')::int AS "pendingTimeOffRequests",
|
|
181
287
|
COUNT(DISTINCT sar.id) FILTER (WHERE sar.status = 'submitted')::int AS "pendingScheduleAdjustmentRequests"
|
|
182
288
|
FROM operations_collaborator c
|
|
289
|
+
LEFT JOIN person person_record
|
|
290
|
+
ON person_record.id = c.person_id
|
|
291
|
+
LEFT JOIN operations_department department_record
|
|
292
|
+
ON department_record.id = c.department_id
|
|
293
|
+
AND department_record.deleted_at IS NULL
|
|
183
294
|
LEFT JOIN operations_project_assignment pa
|
|
184
295
|
ON pa.collaborator_id = c.id
|
|
185
296
|
AND pa.deleted_at IS NULL
|
|
@@ -197,8 +308,8 @@ let OperationsService = class OperationsService {
|
|
|
197
308
|
AND sar.deleted_at IS NULL
|
|
198
309
|
AND sar.status = 'submitted'
|
|
199
310
|
WHERE c.deleted_at IS NULL AND ${teamFilter.clause}
|
|
200
|
-
GROUP BY c.id
|
|
201
|
-
ORDER BY c.display_name ASC`, teamFilter.params);
|
|
311
|
+
GROUP BY c.id, person_record.id, department_record.id
|
|
312
|
+
ORDER BY COALESCE(NULLIF(c.display_name, ''), person_record.name) ASC`, teamFilter.params);
|
|
202
313
|
const [teamProjects, pendingApprovalQueue, pendingTimeOffRequests, pendingScheduleAdjustmentRequests] = await Promise.all([
|
|
203
314
|
this.queryRows(`SELECT p.id,
|
|
204
315
|
p.code,
|
|
@@ -341,18 +452,33 @@ let OperationsService = class OperationsService {
|
|
|
341
452
|
};
|
|
342
453
|
}
|
|
343
454
|
async createCollaborator(userId, data) {
|
|
455
|
+
var _a, _b, _c, _d;
|
|
344
456
|
const actor = await this.getActorContext(userId);
|
|
345
457
|
this.ensureDirector(actor);
|
|
346
|
-
|
|
458
|
+
const resolvedPerson = data.personId
|
|
459
|
+
? await this.getPersonById(Number(data.personId))
|
|
460
|
+
: null;
|
|
461
|
+
const resolvedDisplayName = (_d = (_b = (_a = resolvedPerson === null || resolvedPerson === void 0 ? void 0 : resolvedPerson.name) === null || _a === void 0 ? void 0 : _a.trim()) !== null && _b !== void 0 ? _b : (_c = data.displayName) === null || _c === void 0 ? void 0 : _c.trim()) !== null && _d !== void 0 ? _d : '';
|
|
462
|
+
if (!resolvedDisplayName) {
|
|
463
|
+
throw new common_1.BadRequestException('Field "personId" is required.');
|
|
464
|
+
}
|
|
347
465
|
const collaboratorId = await this.prisma.$transaction(async (tx) => {
|
|
348
|
-
var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p, _q, _r, _s, _t;
|
|
466
|
+
var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p, _q, _r, _s, _t, _u, _v, _w, _x, _y;
|
|
467
|
+
const normalizedCode = String((_a = data.code) !== null && _a !== void 0 ? _a : '').trim() ||
|
|
468
|
+
(await this.generateCollaboratorCode(tx));
|
|
469
|
+
const resolvedDepartment = await this.resolveDepartmentReference(tx, {
|
|
470
|
+
departmentId: (_b = data.departmentId) !== null && _b !== void 0 ? _b : null,
|
|
471
|
+
departmentName: data.department,
|
|
472
|
+
});
|
|
349
473
|
const created = (await tx.$queryRawUnsafe(`INSERT INTO operations_collaborator (
|
|
350
474
|
user_id,
|
|
475
|
+
person_id,
|
|
351
476
|
supervisor_collaborator_id,
|
|
352
477
|
code,
|
|
353
478
|
collaborator_type,
|
|
354
479
|
display_name,
|
|
355
480
|
department,
|
|
481
|
+
department_id,
|
|
356
482
|
title,
|
|
357
483
|
level_label,
|
|
358
484
|
weekly_capacity_hours,
|
|
@@ -363,23 +489,26 @@ let OperationsService = class OperationsService {
|
|
|
363
489
|
created_at,
|
|
364
490
|
updated_at
|
|
365
491
|
) VALUES (
|
|
366
|
-
$1, $2, $3,
|
|
367
|
-
|
|
492
|
+
$1, $2, $3, $4,
|
|
493
|
+
$5::operations_collaborator_collaborator_type_7dd7b0ada2_enum,
|
|
494
|
+
$6, $7, $8, $9, $10, $11,
|
|
495
|
+
$12::operations_collaborator_status_ef779877d4_enum,
|
|
496
|
+
$13::date, $14::date, $15, NOW(), NOW()
|
|
368
497
|
)
|
|
369
|
-
RETURNING id`, data.userId, (
|
|
370
|
-
const createdCollaboratorId = (
|
|
498
|
+
RETURNING id`, (_c = data.userId) !== null && _c !== void 0 ? _c : null, (_d = resolvedPerson === null || resolvedPerson === void 0 ? void 0 : resolvedPerson.id) !== null && _d !== void 0 ? _d : null, (_e = data.supervisorCollaboratorId) !== null && _e !== void 0 ? _e : null, normalizedCode, (_f = data.collaboratorType) !== null && _f !== void 0 ? _f : 'other', resolvedDisplayName, (_g = resolvedDepartment === null || resolvedDepartment === void 0 ? void 0 : resolvedDepartment.name) !== null && _g !== void 0 ? _g : null, (_h = resolvedDepartment === null || resolvedDepartment === void 0 ? void 0 : resolvedDepartment.id) !== null && _h !== void 0 ? _h : null, (_j = data.title) !== null && _j !== void 0 ? _j : null, (_k = data.levelLabel) !== null && _k !== void 0 ? _k : null, (_l = data.weeklyCapacityHours) !== null && _l !== void 0 ? _l : null, (_m = data.status) !== null && _m !== void 0 ? _m : 'active', (_o = data.joinedAt) !== null && _o !== void 0 ? _o : null, (_p = data.leftAt) !== null && _p !== void 0 ? _p : null, (_q = data.notes) !== null && _q !== void 0 ? _q : null));
|
|
499
|
+
const createdCollaboratorId = (_r = created[0]) === null || _r === void 0 ? void 0 : _r.id;
|
|
371
500
|
await this.replaceCollaboratorScheduleDays(tx, createdCollaboratorId, data.weeklySchedule);
|
|
372
501
|
if (data.autoGenerateContractDraft !== false) {
|
|
373
502
|
await this.createHiringContractDraft(tx, actor.userId, {
|
|
374
503
|
collaboratorId: createdCollaboratorId,
|
|
375
|
-
collaboratorCode:
|
|
376
|
-
displayName:
|
|
377
|
-
collaboratorType: (
|
|
378
|
-
supervisorCollaboratorId: (
|
|
379
|
-
startDate: (
|
|
380
|
-
weeklyCapacityHours: (
|
|
381
|
-
compensationAmount: (
|
|
382
|
-
description: (
|
|
504
|
+
collaboratorCode: normalizedCode,
|
|
505
|
+
displayName: resolvedDisplayName,
|
|
506
|
+
collaboratorType: (_s = data.collaboratorType) !== null && _s !== void 0 ? _s : 'other',
|
|
507
|
+
supervisorCollaboratorId: (_t = data.supervisorCollaboratorId) !== null && _t !== void 0 ? _t : null,
|
|
508
|
+
startDate: (_u = data.joinedAt) !== null && _u !== void 0 ? _u : null,
|
|
509
|
+
weeklyCapacityHours: (_v = data.weeklyCapacityHours) !== null && _v !== void 0 ? _v : null,
|
|
510
|
+
compensationAmount: (_w = data.compensationAmount) !== null && _w !== void 0 ? _w : null,
|
|
511
|
+
description: (_y = (_x = data.contractDescription) !== null && _x !== void 0 ? _x : data.notes) !== null && _y !== void 0 ? _y : null,
|
|
383
512
|
});
|
|
384
513
|
}
|
|
385
514
|
return createdCollaboratorId;
|
|
@@ -387,24 +516,41 @@ let OperationsService = class OperationsService {
|
|
|
387
516
|
return this.getCollaboratorByIdForUser(userId, collaboratorId);
|
|
388
517
|
}
|
|
389
518
|
async updateCollaborator(userId, collaboratorId, data) {
|
|
519
|
+
var _a, _b, _c, _d, _e;
|
|
390
520
|
const actor = await this.getActorContext(userId);
|
|
391
521
|
this.ensureDirector(actor);
|
|
392
522
|
await this.getCollaboratorById(collaboratorId);
|
|
393
523
|
const updates = [];
|
|
394
524
|
const params = [];
|
|
525
|
+
const resolvedPerson = data.personId !== undefined && data.personId !== null
|
|
526
|
+
? await this.getPersonById(Number(data.personId))
|
|
527
|
+
: null;
|
|
528
|
+
const resolvedDisplayName = (_d = (_b = (_a = resolvedPerson === null || resolvedPerson === void 0 ? void 0 : resolvedPerson.name) === null || _a === void 0 ? void 0 : _a.trim()) !== null && _b !== void 0 ? _b : (_c = data.displayName) === null || _c === void 0 ? void 0 : _c.trim()) !== null && _d !== void 0 ? _d : undefined;
|
|
529
|
+
this.pushUpdate(updates, params, 'user_id', data.userId);
|
|
530
|
+
this.pushUpdate(updates, params, 'person_id', data.personId);
|
|
395
531
|
this.pushUpdate(updates, params, 'supervisor_collaborator_id', data.supervisorCollaboratorId);
|
|
396
|
-
this.pushUpdate(updates, params, 'code', data.code);
|
|
397
|
-
this.pushUpdate(updates, params, 'collaborator_type', data.collaboratorType);
|
|
398
|
-
|
|
399
|
-
|
|
532
|
+
this.pushUpdate(updates, params, 'code', (_e = data.code) === null || _e === void 0 ? void 0 : _e.trim());
|
|
533
|
+
this.pushUpdate(updates, params, 'collaborator_type', data.collaboratorType, 'operations_collaborator_collaborator_type_7dd7b0ada2_enum');
|
|
534
|
+
if (data.personId !== undefined || data.displayName !== undefined) {
|
|
535
|
+
this.pushUpdate(updates, params, 'display_name', resolvedDisplayName !== null && resolvedDisplayName !== void 0 ? resolvedDisplayName : null);
|
|
536
|
+
}
|
|
400
537
|
this.pushUpdate(updates, params, 'title', data.title);
|
|
401
538
|
this.pushUpdate(updates, params, 'level_label', data.levelLabel);
|
|
402
539
|
this.pushUpdate(updates, params, 'weekly_capacity_hours', data.weeklyCapacityHours);
|
|
403
|
-
this.pushUpdate(updates, params, 'status', data.status);
|
|
404
|
-
this.pushUpdate(updates, params, 'joined_at', data.joinedAt);
|
|
405
|
-
this.pushUpdate(updates, params, 'left_at', data.leftAt);
|
|
540
|
+
this.pushUpdate(updates, params, 'status', data.status, 'operations_collaborator_status_ef779877d4_enum');
|
|
541
|
+
this.pushUpdate(updates, params, 'joined_at', data.joinedAt, 'date');
|
|
542
|
+
this.pushUpdate(updates, params, 'left_at', data.leftAt, 'date');
|
|
406
543
|
this.pushUpdate(updates, params, 'notes', data.notes);
|
|
407
544
|
await this.prisma.$transaction(async (tx) => {
|
|
545
|
+
var _a, _b, _c;
|
|
546
|
+
if (data.department !== undefined || data.departmentId !== undefined) {
|
|
547
|
+
const resolvedDepartment = await this.resolveDepartmentReference(tx, {
|
|
548
|
+
departmentId: (_a = data.departmentId) !== null && _a !== void 0 ? _a : null,
|
|
549
|
+
departmentName: data.department,
|
|
550
|
+
});
|
|
551
|
+
this.pushUpdate(updates, params, 'department', (_b = resolvedDepartment === null || resolvedDepartment === void 0 ? void 0 : resolvedDepartment.name) !== null && _b !== void 0 ? _b : null);
|
|
552
|
+
this.pushUpdate(updates, params, 'department_id', (_c = resolvedDepartment === null || resolvedDepartment === void 0 ? void 0 : resolvedDepartment.id) !== null && _c !== void 0 ? _c : null);
|
|
553
|
+
}
|
|
408
554
|
if (updates.length) {
|
|
409
555
|
params.push(collaboratorId);
|
|
410
556
|
await tx.$executeRawUnsafe(`UPDATE operations_collaborator
|
|
@@ -418,6 +564,109 @@ let OperationsService = class OperationsService {
|
|
|
418
564
|
});
|
|
419
565
|
return this.getCollaboratorByIdForUser(userId, collaboratorId);
|
|
420
566
|
}
|
|
567
|
+
async listDepartments(userId) {
|
|
568
|
+
const actor = await this.getActorContext(userId);
|
|
569
|
+
this.ensureCollaborator(actor);
|
|
570
|
+
return this.queryRows(`SELECT d.id,
|
|
571
|
+
d.slug,
|
|
572
|
+
d.code,
|
|
573
|
+
d.name,
|
|
574
|
+
d.description,
|
|
575
|
+
CASE WHEN d.deleted_at IS NULL THEN 'active' ELSE 'inactive' END AS status,
|
|
576
|
+
COUNT(DISTINCT c.id)::int AS "collaboratorCount",
|
|
577
|
+
d.created_at AS "createdAt",
|
|
578
|
+
d.updated_at AS "updatedAt"
|
|
579
|
+
FROM operations_department d
|
|
580
|
+
LEFT JOIN operations_collaborator c
|
|
581
|
+
ON c.deleted_at IS NULL
|
|
582
|
+
AND (
|
|
583
|
+
c.department_id = d.id
|
|
584
|
+
OR (
|
|
585
|
+
c.department_id IS NULL
|
|
586
|
+
AND LOWER(COALESCE(c.department, '')) = LOWER(d.name)
|
|
587
|
+
)
|
|
588
|
+
)
|
|
589
|
+
GROUP BY d.id
|
|
590
|
+
ORDER BY CASE WHEN d.deleted_at IS NULL THEN 0 ELSE 1 END ASC,
|
|
591
|
+
d.name ASC`);
|
|
592
|
+
}
|
|
593
|
+
async createDepartment(userId, data) {
|
|
594
|
+
const actor = await this.getActorContext(userId);
|
|
595
|
+
this.ensureDirector(actor);
|
|
596
|
+
const name = this.normalizeOptionalText(data.name);
|
|
597
|
+
if (!name) {
|
|
598
|
+
throw new common_1.BadRequestException('Department name is required.');
|
|
599
|
+
}
|
|
600
|
+
return this.prisma.$transaction(async (tx) => {
|
|
601
|
+
var _a, _b, _c, _d;
|
|
602
|
+
await this.assertDepartmentNameAvailable(tx, name);
|
|
603
|
+
const normalizedCode = (_b = (_a = this.normalizeOptionalText(data.code)) === null || _a === void 0 ? void 0 : _a.toUpperCase()) !== null && _b !== void 0 ? _b : null;
|
|
604
|
+
if (normalizedCode) {
|
|
605
|
+
await this.assertDepartmentCodeAvailable(tx, normalizedCode);
|
|
606
|
+
}
|
|
607
|
+
const created = (await tx.$queryRawUnsafe(`INSERT INTO operations_department (
|
|
608
|
+
slug,
|
|
609
|
+
code,
|
|
610
|
+
name,
|
|
611
|
+
description,
|
|
612
|
+
deleted_at,
|
|
613
|
+
created_at,
|
|
614
|
+
updated_at
|
|
615
|
+
) VALUES (
|
|
616
|
+
$1, $2, $3, $4,
|
|
617
|
+
CASE WHEN $5 = 'inactive' THEN NOW() ELSE NULL END,
|
|
618
|
+
NOW(), NOW()
|
|
619
|
+
)
|
|
620
|
+
RETURNING id`, await this.generateUniqueDepartmentSlug(tx, name), normalizedCode, name, this.normalizeOptionalText(data.description), (_c = data.status) !== null && _c !== void 0 ? _c : 'active'));
|
|
621
|
+
const createdDepartmentId = (_d = created[0]) === null || _d === void 0 ? void 0 : _d.id;
|
|
622
|
+
if (!createdDepartmentId) {
|
|
623
|
+
throw new common_1.BadRequestException('Unable to create the department.');
|
|
624
|
+
}
|
|
625
|
+
return this.getDepartmentById(tx, createdDepartmentId, true);
|
|
626
|
+
});
|
|
627
|
+
}
|
|
628
|
+
async updateDepartment(userId, departmentId, data) {
|
|
629
|
+
const actor = await this.getActorContext(userId);
|
|
630
|
+
this.ensureDirector(actor);
|
|
631
|
+
return this.prisma.$transaction(async (tx) => {
|
|
632
|
+
var _a, _b, _c, _d, _e, _f;
|
|
633
|
+
const current = await this.getDepartmentById(tx, departmentId, true);
|
|
634
|
+
const nextName = data.name !== undefined
|
|
635
|
+
? this.normalizeOptionalText(data.name)
|
|
636
|
+
: current.name;
|
|
637
|
+
if (!nextName) {
|
|
638
|
+
throw new common_1.BadRequestException('Department name is required.');
|
|
639
|
+
}
|
|
640
|
+
if (String(nextName).toLowerCase() !== String(current.name).toLowerCase()) {
|
|
641
|
+
await this.assertDepartmentNameAvailable(tx, nextName, departmentId);
|
|
642
|
+
}
|
|
643
|
+
const nextCode = data.code !== undefined
|
|
644
|
+
? (_b = (_a = this.normalizeOptionalText(data.code)) === null || _a === void 0 ? void 0 : _a.toUpperCase()) !== null && _b !== void 0 ? _b : null
|
|
645
|
+
: ((_c = current.code) !== null && _c !== void 0 ? _c : null);
|
|
646
|
+
if (nextCode) {
|
|
647
|
+
await this.assertDepartmentCodeAvailable(tx, nextCode, departmentId);
|
|
648
|
+
}
|
|
649
|
+
const nextDescription = data.description !== undefined
|
|
650
|
+
? this.normalizeOptionalText(data.description)
|
|
651
|
+
: ((_d = current.description) !== null && _d !== void 0 ? _d : null);
|
|
652
|
+
const nextStatus = (_f = (_e = data.status) !== null && _e !== void 0 ? _e : current.status) !== null && _f !== void 0 ? _f : 'active';
|
|
653
|
+
const nextSlug = String(nextName).toLowerCase() !== String(current.name).toLowerCase()
|
|
654
|
+
? await this.generateUniqueDepartmentSlug(tx, nextName, departmentId)
|
|
655
|
+
: current.slug;
|
|
656
|
+
await tx.$executeRawUnsafe(`UPDATE operations_department
|
|
657
|
+
SET slug = $1,
|
|
658
|
+
code = $2,
|
|
659
|
+
name = $3,
|
|
660
|
+
description = $4,
|
|
661
|
+
deleted_at = CASE
|
|
662
|
+
WHEN $5 = 'inactive' THEN COALESCE(deleted_at, NOW())
|
|
663
|
+
ELSE NULL
|
|
664
|
+
END,
|
|
665
|
+
updated_at = NOW()
|
|
666
|
+
WHERE id = $6`, nextSlug, nextCode, nextName, nextDescription, nextStatus, departmentId);
|
|
667
|
+
return this.getDepartmentById(tx, departmentId, true);
|
|
668
|
+
});
|
|
669
|
+
}
|
|
421
670
|
async listProjects(userId) {
|
|
422
671
|
const actor = await this.getActorContext(userId);
|
|
423
672
|
const filter = this.buildIdFilter(actor.visibleProjectIds, 'p.id', actor.isDirector);
|
|
@@ -466,7 +715,7 @@ let OperationsService = class OperationsService {
|
|
|
466
715
|
this.ensureDirector(actor);
|
|
467
716
|
this.requireFields(data, ['code', 'name']);
|
|
468
717
|
const createdProjectId = await this.prisma.$transaction(async (tx) => {
|
|
469
|
-
var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p, _q, _r, _s, _t, _u, _v, _w, _x, _y;
|
|
718
|
+
var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p, _q, _r, _s, _t, _u, _v, _w, _x, _y, _z;
|
|
470
719
|
const created = await tx.$queryRawUnsafe(`INSERT INTO operations_project (
|
|
471
720
|
contract_id,
|
|
472
721
|
manager_collaborator_id,
|
|
@@ -483,8 +732,11 @@ let OperationsService = class OperationsService {
|
|
|
483
732
|
created_at,
|
|
484
733
|
updated_at
|
|
485
734
|
) VALUES (
|
|
486
|
-
$1, $2, $3, $4, $5, $6,
|
|
487
|
-
|
|
735
|
+
$1, $2, $3, $4, $5, $6,
|
|
736
|
+
$7::operations_project_status_965e8d4b2d_enum,
|
|
737
|
+
$8,
|
|
738
|
+
$9::operations_project_delivery_model_75ee11b3b7_enum,
|
|
739
|
+
$10, $11::date, $12::date, NOW(), NOW()
|
|
488
740
|
)
|
|
489
741
|
RETURNING id`, (_a = data.contractId) !== null && _a !== void 0 ? _a : null, (_b = data.managerCollaboratorId) !== null && _b !== void 0 ? _b : null, data.code, data.name, (_c = data.clientName) !== null && _c !== void 0 ? _c : null, (_d = data.summary) !== null && _d !== void 0 ? _d : null, (_e = data.status) !== null && _e !== void 0 ? _e : 'planning', (_f = data.progressPercent) !== null && _f !== void 0 ? _f : null, (_g = data.deliveryModel) !== null && _g !== void 0 ? _g : 'project_delivery', (_h = data.budgetAmount) !== null && _h !== void 0 ? _h : null, (_j = data.startDate) !== null && _j !== void 0 ? _j : null, (_k = data.endDate) !== null && _k !== void 0 ? _k : null);
|
|
490
742
|
const projectId = (_l = created[0]) === null || _l === void 0 ? void 0 : _l.id;
|
|
@@ -494,18 +746,19 @@ let OperationsService = class OperationsService {
|
|
|
494
746
|
if (!data.contractId && data.autoGenerateContractDraft !== false) {
|
|
495
747
|
const contractId = await this.createProjectContractDraft(tx, actor.userId, {
|
|
496
748
|
projectId,
|
|
749
|
+
contractTemplateId: (_o = data.contractTemplateId) !== null && _o !== void 0 ? _o : null,
|
|
497
750
|
projectCode: data.code,
|
|
498
751
|
projectName: data.name,
|
|
499
|
-
clientName: (
|
|
500
|
-
managerCollaboratorId: (
|
|
501
|
-
startDate: (
|
|
502
|
-
endDate: (
|
|
503
|
-
budgetAmount: (
|
|
504
|
-
monthlyHourCap: (
|
|
505
|
-
billingModel: (
|
|
506
|
-
contractCode: (
|
|
507
|
-
contractName: (
|
|
508
|
-
description: (
|
|
752
|
+
clientName: (_p = data.clientName) !== null && _p !== void 0 ? _p : data.name,
|
|
753
|
+
managerCollaboratorId: (_q = data.managerCollaboratorId) !== null && _q !== void 0 ? _q : null,
|
|
754
|
+
startDate: (_r = data.startDate) !== null && _r !== void 0 ? _r : null,
|
|
755
|
+
endDate: (_s = data.endDate) !== null && _s !== void 0 ? _s : null,
|
|
756
|
+
budgetAmount: (_t = data.budgetAmount) !== null && _t !== void 0 ? _t : null,
|
|
757
|
+
monthlyHourCap: (_u = data.monthlyHourCap) !== null && _u !== void 0 ? _u : null,
|
|
758
|
+
billingModel: (_v = data.billingModel) !== null && _v !== void 0 ? _v : 'time_and_material',
|
|
759
|
+
contractCode: (_w = data.contractCode) !== null && _w !== void 0 ? _w : null,
|
|
760
|
+
contractName: (_x = data.contractName) !== null && _x !== void 0 ? _x : null,
|
|
761
|
+
description: (_z = (_y = data.contractDescription) !== null && _y !== void 0 ? _y : data.summary) !== null && _z !== void 0 ? _z : null,
|
|
509
762
|
});
|
|
510
763
|
await tx.$executeRawUnsafe(`UPDATE operations_project
|
|
511
764
|
SET contract_id = $1,
|
|
@@ -519,7 +772,7 @@ let OperationsService = class OperationsService {
|
|
|
519
772
|
async updateProject(userId, projectId, data) {
|
|
520
773
|
const actor = await this.getActorContext(userId);
|
|
521
774
|
this.ensureDirector(actor);
|
|
522
|
-
await this.getProjectById(userId, projectId);
|
|
775
|
+
const currentProject = await this.getProjectById(userId, projectId);
|
|
523
776
|
const updates = [];
|
|
524
777
|
const params = [];
|
|
525
778
|
this.pushUpdate(updates, params, 'contract_id', data.contractId);
|
|
@@ -528,13 +781,14 @@ let OperationsService = class OperationsService {
|
|
|
528
781
|
this.pushUpdate(updates, params, 'name', data.name);
|
|
529
782
|
this.pushUpdate(updates, params, 'client_name', data.clientName);
|
|
530
783
|
this.pushUpdate(updates, params, 'summary', data.summary);
|
|
531
|
-
this.pushUpdate(updates, params, 'status', data.status);
|
|
784
|
+
this.pushUpdate(updates, params, 'status', data.status, 'operations_project_status_965e8d4b2d_enum');
|
|
532
785
|
this.pushUpdate(updates, params, 'progress_percent', data.progressPercent);
|
|
533
|
-
this.pushUpdate(updates, params, 'delivery_model', data.deliveryModel);
|
|
786
|
+
this.pushUpdate(updates, params, 'delivery_model', data.deliveryModel, 'operations_project_delivery_model_75ee11b3b7_enum');
|
|
534
787
|
this.pushUpdate(updates, params, 'budget_amount', data.budgetAmount);
|
|
535
|
-
this.pushUpdate(updates, params, 'start_date', data.startDate);
|
|
536
|
-
this.pushUpdate(updates, params, 'end_date', data.endDate);
|
|
788
|
+
this.pushUpdate(updates, params, 'start_date', data.startDate, 'date');
|
|
789
|
+
this.pushUpdate(updates, params, 'end_date', data.endDate, 'date');
|
|
537
790
|
await this.prisma.$transaction(async (tx) => {
|
|
791
|
+
var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p, _q, _r, _s, _t, _u, _v, _w, _x, _y, _z, _0, _1, _2, _3, _4, _5, _6;
|
|
538
792
|
if (updates.length) {
|
|
539
793
|
params.push(projectId);
|
|
540
794
|
await tx.$executeRawUnsafe(`UPDATE operations_project
|
|
@@ -545,9 +799,162 @@ let OperationsService = class OperationsService {
|
|
|
545
799
|
if (data.teamAssignments) {
|
|
546
800
|
await this.replaceProjectAssignments(tx, projectId, data.teamAssignments);
|
|
547
801
|
}
|
|
802
|
+
const nextContractId = data.contractId !== undefined
|
|
803
|
+
? data.contractId
|
|
804
|
+
: ((_a = currentProject.contractId) !== null && _a !== void 0 ? _a : null);
|
|
805
|
+
const shouldGenerateDraft = !nextContractId && data.autoGenerateContractDraft === true;
|
|
806
|
+
if (shouldGenerateDraft) {
|
|
807
|
+
const contractId = await this.createProjectContractDraft(tx, actor.userId, {
|
|
808
|
+
projectId,
|
|
809
|
+
contractTemplateId: (_b = data.contractTemplateId) !== null && _b !== void 0 ? _b : null,
|
|
810
|
+
projectCode: (_c = data.code) !== null && _c !== void 0 ? _c : currentProject.code,
|
|
811
|
+
projectName: (_d = data.name) !== null && _d !== void 0 ? _d : currentProject.name,
|
|
812
|
+
clientName: (_f = (_e = data.clientName) !== null && _e !== void 0 ? _e : currentProject.clientName) !== null && _f !== void 0 ? _f : currentProject.name,
|
|
813
|
+
managerCollaboratorId: (_h = (_g = data.managerCollaboratorId) !== null && _g !== void 0 ? _g : currentProject.managerCollaboratorId) !== null && _h !== void 0 ? _h : null,
|
|
814
|
+
startDate: (_k = (_j = data.startDate) !== null && _j !== void 0 ? _j : currentProject.startDate) !== null && _k !== void 0 ? _k : null,
|
|
815
|
+
endDate: (_m = (_l = data.endDate) !== null && _l !== void 0 ? _l : currentProject.endDate) !== null && _m !== void 0 ? _m : null,
|
|
816
|
+
budgetAmount: (_p = (_o = data.budgetAmount) !== null && _o !== void 0 ? _o : currentProject.budgetAmount) !== null && _p !== void 0 ? _p : null,
|
|
817
|
+
monthlyHourCap: (_s = (_q = data.monthlyHourCap) !== null && _q !== void 0 ? _q : (_r = currentProject.relatedContract) === null || _r === void 0 ? void 0 : _r.monthlyHourCap) !== null && _s !== void 0 ? _s : null,
|
|
818
|
+
billingModel: (_v = (_t = data.billingModel) !== null && _t !== void 0 ? _t : (_u = currentProject.relatedContract) === null || _u === void 0 ? void 0 : _u.billingModel) !== null && _v !== void 0 ? _v : 'time_and_material',
|
|
819
|
+
contractCode: (_y = (_w = data.contractCode) !== null && _w !== void 0 ? _w : (_x = currentProject.relatedContract) === null || _x === void 0 ? void 0 : _x.code) !== null && _y !== void 0 ? _y : null,
|
|
820
|
+
contractName: (_1 = (_z = data.contractName) !== null && _z !== void 0 ? _z : (_0 = currentProject.relatedContract) === null || _0 === void 0 ? void 0 : _0.name) !== null && _1 !== void 0 ? _1 : null,
|
|
821
|
+
description: (_6 = (_5 = (_4 = (_2 = data.contractDescription) !== null && _2 !== void 0 ? _2 : (_3 = currentProject.relatedContract) === null || _3 === void 0 ? void 0 : _3.description) !== null && _4 !== void 0 ? _4 : data.summary) !== null && _5 !== void 0 ? _5 : currentProject.summary) !== null && _6 !== void 0 ? _6 : null,
|
|
822
|
+
});
|
|
823
|
+
await tx.$executeRawUnsafe(`UPDATE operations_project
|
|
824
|
+
SET contract_id = $1,
|
|
825
|
+
updated_at = NOW()
|
|
826
|
+
WHERE id = $2`, contractId, projectId);
|
|
827
|
+
}
|
|
548
828
|
});
|
|
549
829
|
return this.getProjectById(userId, projectId);
|
|
550
830
|
}
|
|
831
|
+
async listContractTemplates(userId) {
|
|
832
|
+
const actor = await this.getActorContext(userId);
|
|
833
|
+
this.ensureDirector(actor);
|
|
834
|
+
return this.queryRows(`SELECT t.id,
|
|
835
|
+
t.slug,
|
|
836
|
+
t.code,
|
|
837
|
+
t.name,
|
|
838
|
+
t.description,
|
|
839
|
+
t.contract_category AS "contractCategory",
|
|
840
|
+
t.contract_type AS "contractType",
|
|
841
|
+
t.billing_model AS "billingModel",
|
|
842
|
+
t.signature_status AS "signatureStatus",
|
|
843
|
+
t.is_active AS "isActive",
|
|
844
|
+
t.status,
|
|
845
|
+
t.content_html AS "contentHtml",
|
|
846
|
+
t.created_at AS "createdAt",
|
|
847
|
+
t.updated_at AS "updatedAt",
|
|
848
|
+
COUNT(DISTINCT c.id)::int AS "usageCount"
|
|
849
|
+
FROM operations_contract_template t
|
|
850
|
+
LEFT JOIN operations_contract c
|
|
851
|
+
ON c.contract_template_id = t.id
|
|
852
|
+
AND c.deleted_at IS NULL
|
|
853
|
+
WHERE t.deleted_at IS NULL
|
|
854
|
+
GROUP BY t.id
|
|
855
|
+
ORDER BY CASE
|
|
856
|
+
WHEN t.status = 'active' THEN 0
|
|
857
|
+
WHEN t.status = 'draft' THEN 1
|
|
858
|
+
WHEN t.status = 'inactive' THEN 2
|
|
859
|
+
ELSE 3
|
|
860
|
+
END,
|
|
861
|
+
t.name ASC`);
|
|
862
|
+
}
|
|
863
|
+
async getContractTemplateById(userId, templateId) {
|
|
864
|
+
const actor = await this.getActorContext(userId);
|
|
865
|
+
this.ensureDirector(actor);
|
|
866
|
+
return this.getContractTemplateRecord(this.prisma, templateId);
|
|
867
|
+
}
|
|
868
|
+
async createContractTemplate(userId, data) {
|
|
869
|
+
const actor = await this.getActorContext(userId);
|
|
870
|
+
this.ensureDirector(actor);
|
|
871
|
+
const name = this.normalizeOptionalText(data.name);
|
|
872
|
+
if (!name) {
|
|
873
|
+
throw new common_1.BadRequestException('Contract template name is required.');
|
|
874
|
+
}
|
|
875
|
+
return this.prisma.$transaction(async (tx) => {
|
|
876
|
+
var _a, _b, _c, _d, _e, _f, _g, _h, _j;
|
|
877
|
+
await this.assertContractTemplateNameAvailable(tx, name);
|
|
878
|
+
const nextCode = (_b = (_a = this.normalizeOptionalText(data.code)) === null || _a === void 0 ? void 0 : _a.toUpperCase()) !== null && _b !== void 0 ? _b : null;
|
|
879
|
+
if (nextCode) {
|
|
880
|
+
await this.assertContractTemplateCodeAvailable(tx, nextCode);
|
|
881
|
+
}
|
|
882
|
+
const nextStatus = (_c = data.status) !== null && _c !== void 0 ? _c : 'active';
|
|
883
|
+
const isActive = (_d = data.isActive) !== null && _d !== void 0 ? _d : !['inactive', 'archived'].includes(nextStatus);
|
|
884
|
+
const created = (await tx.$queryRawUnsafe(`INSERT INTO operations_contract_template (
|
|
885
|
+
slug,
|
|
886
|
+
code,
|
|
887
|
+
name,
|
|
888
|
+
description,
|
|
889
|
+
contract_category,
|
|
890
|
+
contract_type,
|
|
891
|
+
billing_model,
|
|
892
|
+
signature_status,
|
|
893
|
+
is_active,
|
|
894
|
+
status,
|
|
895
|
+
content_html,
|
|
896
|
+
created_at,
|
|
897
|
+
updated_at
|
|
898
|
+
) VALUES (
|
|
899
|
+
$1, $2, $3, $4,
|
|
900
|
+
$5, $6, $7, $8, $9, $10, $11,
|
|
901
|
+
NOW(), NOW()
|
|
902
|
+
)
|
|
903
|
+
RETURNING id`, await this.generateUniqueContractTemplateSlug(tx, name), nextCode, name, this.normalizeOptionalText(data.description), (_e = data.contractCategory) !== null && _e !== void 0 ? _e : 'client', (_f = data.contractType) !== null && _f !== void 0 ? _f : 'service_agreement', (_g = data.billingModel) !== null && _g !== void 0 ? _g : 'time_and_material', (_h = data.signatureStatus) !== null && _h !== void 0 ? _h : 'not_started', isActive, nextStatus, this.normalizeOptionalText(data.contentHtml)));
|
|
904
|
+
const templateId = (_j = created[0]) === null || _j === void 0 ? void 0 : _j.id;
|
|
905
|
+
if (!templateId) {
|
|
906
|
+
throw new common_1.BadRequestException('Unable to create the contract template.');
|
|
907
|
+
}
|
|
908
|
+
return this.getContractTemplateRecord(tx, templateId, true);
|
|
909
|
+
});
|
|
910
|
+
}
|
|
911
|
+
async updateContractTemplate(userId, templateId, data) {
|
|
912
|
+
const actor = await this.getActorContext(userId);
|
|
913
|
+
this.ensureDirector(actor);
|
|
914
|
+
return this.prisma.$transaction(async (tx) => {
|
|
915
|
+
var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p, _q, _r;
|
|
916
|
+
const current = await this.getContractTemplateRecord(tx, templateId, true);
|
|
917
|
+
const nextName = data.name !== undefined
|
|
918
|
+
? this.normalizeOptionalText(data.name)
|
|
919
|
+
: current.name;
|
|
920
|
+
if (!nextName) {
|
|
921
|
+
throw new common_1.BadRequestException('Contract template name is required.');
|
|
922
|
+
}
|
|
923
|
+
if (String(nextName).toLowerCase() !== String(current.name).toLowerCase()) {
|
|
924
|
+
await this.assertContractTemplateNameAvailable(tx, nextName, templateId);
|
|
925
|
+
}
|
|
926
|
+
const nextCode = data.code !== undefined
|
|
927
|
+
? (_b = (_a = this.normalizeOptionalText(data.code)) === null || _a === void 0 ? void 0 : _a.toUpperCase()) !== null && _b !== void 0 ? _b : null
|
|
928
|
+
: ((_c = current.code) !== null && _c !== void 0 ? _c : null);
|
|
929
|
+
if (nextCode) {
|
|
930
|
+
await this.assertContractTemplateCodeAvailable(tx, nextCode, templateId);
|
|
931
|
+
}
|
|
932
|
+
const nextStatus = (_e = (_d = data.status) !== null && _d !== void 0 ? _d : current.status) !== null && _e !== void 0 ? _e : 'active';
|
|
933
|
+
const nextIsActive = (_f = data.isActive) !== null && _f !== void 0 ? _f : !['inactive', 'archived'].includes(nextStatus);
|
|
934
|
+
const nextSlug = String(nextName).toLowerCase() !== String(current.name).toLowerCase()
|
|
935
|
+
? await this.generateUniqueContractTemplateSlug(tx, nextName, templateId)
|
|
936
|
+
: current.slug;
|
|
937
|
+
await tx.$executeRawUnsafe(`UPDATE operations_contract_template
|
|
938
|
+
SET slug = $1,
|
|
939
|
+
code = $2,
|
|
940
|
+
name = $3,
|
|
941
|
+
description = $4,
|
|
942
|
+
contract_category = $5,
|
|
943
|
+
contract_type = $6,
|
|
944
|
+
billing_model = $7,
|
|
945
|
+
signature_status = $8,
|
|
946
|
+
is_active = $9,
|
|
947
|
+
status = $10,
|
|
948
|
+
content_html = $11,
|
|
949
|
+
updated_at = NOW()
|
|
950
|
+
WHERE id = $12`, nextSlug, nextCode, nextName, data.description !== undefined
|
|
951
|
+
? this.normalizeOptionalText(data.description)
|
|
952
|
+
: ((_g = current.description) !== null && _g !== void 0 ? _g : null), (_j = (_h = data.contractCategory) !== null && _h !== void 0 ? _h : current.contractCategory) !== null && _j !== void 0 ? _j : 'client', (_l = (_k = data.contractType) !== null && _k !== void 0 ? _k : current.contractType) !== null && _l !== void 0 ? _l : 'service_agreement', (_o = (_m = data.billingModel) !== null && _m !== void 0 ? _m : current.billingModel) !== null && _o !== void 0 ? _o : 'time_and_material', (_q = (_p = data.signatureStatus) !== null && _p !== void 0 ? _p : current.signatureStatus) !== null && _q !== void 0 ? _q : 'not_started', nextIsActive, nextStatus, data.contentHtml !== undefined
|
|
953
|
+
? this.normalizeOptionalText(data.contentHtml)
|
|
954
|
+
: ((_r = current.contentHtml) !== null && _r !== void 0 ? _r : null), templateId);
|
|
955
|
+
return this.getContractTemplateRecord(tx, templateId, true);
|
|
956
|
+
});
|
|
957
|
+
}
|
|
551
958
|
async listContracts(userId) {
|
|
552
959
|
const actor = await this.getActorContext(userId);
|
|
553
960
|
const params = [];
|
|
@@ -574,6 +981,10 @@ let OperationsService = class OperationsService {
|
|
|
574
981
|
c.billing_model AS "billingModel",
|
|
575
982
|
c.account_manager_collaborator_id AS "accountManagerCollaboratorId",
|
|
576
983
|
c.related_collaborator_id AS "relatedCollaboratorId",
|
|
984
|
+
c.contract_template_id AS "contractTemplateId",
|
|
985
|
+
template.name AS "contractTemplateName",
|
|
986
|
+
template.slug AS "contractTemplateSlug",
|
|
987
|
+
template.code AS "contractTemplateCode",
|
|
577
988
|
c.origin_type AS "originType",
|
|
578
989
|
c.origin_id AS "originId",
|
|
579
990
|
c.start_date AS "startDate",
|
|
@@ -583,19 +994,23 @@ let OperationsService = class OperationsService {
|
|
|
583
994
|
c.budget_amount AS "budgetAmount",
|
|
584
995
|
c.monthly_hour_cap AS "monthlyHourCap",
|
|
585
996
|
c.status,
|
|
997
|
+
c.creation_mode AS "creationMode",
|
|
998
|
+
c.wizard_step AS "wizardStep",
|
|
586
999
|
c.description,
|
|
587
1000
|
m.display_name AS "accountManagerName",
|
|
588
1001
|
linked.display_name AS "relatedCollaboratorName",
|
|
589
|
-
COALESCE(primary_party.display_name, linked.display_name, c.client_name) AS "mainRelatedPartyName",
|
|
590
|
-
COALESCE(financials.value_amount, 0) AS "valueAmount",
|
|
591
|
-
COALESCE(financials.payment_amount, 0) AS "paymentAmount",
|
|
592
|
-
COALESCE(financials.revenue_amount, 0) AS "revenueAmount",
|
|
593
|
-
COALESCE(financials.fine_amount, 0) AS "fineAmount",
|
|
594
|
-
COALESCE(pdf_document.file_name, '') AS "currentPdfFileName",
|
|
1002
|
+
MAX(COALESCE(primary_party.display_name, linked.display_name, c.client_name)) AS "mainRelatedPartyName",
|
|
1003
|
+
MAX(COALESCE(financials.value_amount, 0)) AS "valueAmount",
|
|
1004
|
+
MAX(COALESCE(financials.payment_amount, 0)) AS "paymentAmount",
|
|
1005
|
+
MAX(COALESCE(financials.revenue_amount, 0)) AS "revenueAmount",
|
|
1006
|
+
MAX(COALESCE(financials.fine_amount, 0)) AS "fineAmount",
|
|
1007
|
+
MAX(COALESCE(pdf_document.file_name, '')) AS "currentPdfFileName",
|
|
595
1008
|
COUNT(DISTINCT p.id)::int AS "projectCount"
|
|
596
1009
|
FROM operations_contract c
|
|
597
1010
|
LEFT JOIN operations_collaborator m ON m.id = c.account_manager_collaborator_id
|
|
598
1011
|
LEFT JOIN operations_collaborator linked ON linked.id = c.related_collaborator_id
|
|
1012
|
+
LEFT JOIN operations_contract_template template
|
|
1013
|
+
ON template.id = c.contract_template_id
|
|
599
1014
|
LEFT JOIN LATERAL (
|
|
600
1015
|
SELECT cp.display_name
|
|
601
1016
|
FROM operations_contract_party cp
|
|
@@ -620,7 +1035,7 @@ let OperationsService = class OperationsService {
|
|
|
620
1035
|
WHERE cd.contract_id = c.id
|
|
621
1036
|
AND cd.deleted_at IS NULL
|
|
622
1037
|
AND cd.is_current = true
|
|
623
|
-
AND cd.document_type IN ('
|
|
1038
|
+
AND cd.document_type IN ('source_upload', 'generated_pdf')
|
|
624
1039
|
ORDER BY cd.id DESC
|
|
625
1040
|
LIMIT 1
|
|
626
1041
|
) pdf_document ON TRUE
|
|
@@ -628,8 +1043,8 @@ let OperationsService = class OperationsService {
|
|
|
628
1043
|
ON p.contract_id = c.id
|
|
629
1044
|
AND p.deleted_at IS NULL
|
|
630
1045
|
WHERE ${accessClause}
|
|
631
|
-
GROUP BY c.id, m.id, linked.id
|
|
632
|
-
ORDER BY c.name ASC`, params);
|
|
1046
|
+
GROUP BY c.id, m.id, linked.id, template.id
|
|
1047
|
+
ORDER BY COALESCE(c.name, c.code, CONCAT('draft-', c.id)) ASC`, params);
|
|
633
1048
|
}
|
|
634
1049
|
async getContractById(userId, contractId) {
|
|
635
1050
|
var _a, _b, _c;
|
|
@@ -645,6 +1060,10 @@ let OperationsService = class OperationsService {
|
|
|
645
1060
|
c.billing_model AS "billingModel",
|
|
646
1061
|
c.account_manager_collaborator_id AS "accountManagerCollaboratorId",
|
|
647
1062
|
c.related_collaborator_id AS "relatedCollaboratorId",
|
|
1063
|
+
c.contract_template_id AS "contractTemplateId",
|
|
1064
|
+
template.name AS "contractTemplateName",
|
|
1065
|
+
template.slug AS "contractTemplateSlug",
|
|
1066
|
+
template.code AS "contractTemplateCode",
|
|
648
1067
|
c.origin_type AS "originType",
|
|
649
1068
|
c.origin_id AS "originId",
|
|
650
1069
|
c.start_date AS "startDate",
|
|
@@ -654,6 +1073,8 @@ let OperationsService = class OperationsService {
|
|
|
654
1073
|
c.budget_amount AS "budgetAmount",
|
|
655
1074
|
c.monthly_hour_cap AS "monthlyHourCap",
|
|
656
1075
|
c.status,
|
|
1076
|
+
c.creation_mode AS "creationMode",
|
|
1077
|
+
c.wizard_step AS "wizardStep",
|
|
657
1078
|
c.description,
|
|
658
1079
|
c.content_html AS "contentHtml",
|
|
659
1080
|
m.display_name AS "accountManagerName",
|
|
@@ -661,6 +1082,8 @@ let OperationsService = class OperationsService {
|
|
|
661
1082
|
FROM operations_contract c
|
|
662
1083
|
LEFT JOIN operations_collaborator m ON m.id = c.account_manager_collaborator_id
|
|
663
1084
|
LEFT JOIN operations_collaborator linked ON linked.id = c.related_collaborator_id
|
|
1085
|
+
LEFT JOIN operations_contract_template template
|
|
1086
|
+
ON template.id = c.contract_template_id
|
|
664
1087
|
WHERE c.id = $1
|
|
665
1088
|
AND c.deleted_at IS NULL`, [contractId]);
|
|
666
1089
|
if (!contract) {
|
|
@@ -739,10 +1162,13 @@ let OperationsService = class OperationsService {
|
|
|
739
1162
|
ORDER BY id ASC`, [contractId]),
|
|
740
1163
|
this.queryRows(`SELECT id,
|
|
741
1164
|
document_type AS "documentType",
|
|
1165
|
+
file_id AS "fileId",
|
|
742
1166
|
file_name AS "fileName",
|
|
743
1167
|
mime_type AS "mimeType",
|
|
744
1168
|
file_content_base64 AS "fileContentBase64",
|
|
745
1169
|
is_current AS "isCurrent",
|
|
1170
|
+
extraction_status AS "extractionStatus",
|
|
1171
|
+
extraction_summary AS "extractionSummary",
|
|
746
1172
|
notes,
|
|
747
1173
|
created_at AS "createdAt"
|
|
748
1174
|
FROM operations_contract_document
|
|
@@ -781,9 +1207,9 @@ let OperationsService = class OperationsService {
|
|
|
781
1207
|
async createContract(userId, data) {
|
|
782
1208
|
const actor = await this.getActorContext(userId);
|
|
783
1209
|
this.ensureDirector(actor);
|
|
784
|
-
this.requireFields(data, ['code', 'name', 'clientName', 'startDate']);
|
|
785
1210
|
const createdId = await this.prisma.$transaction(async (tx) => {
|
|
786
|
-
var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p, _q, _r, _s, _t;
|
|
1211
|
+
var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p, _q, _r, _s, _t, _u, _v, _w, _x, _y, _z, _0;
|
|
1212
|
+
const normalizedCode = (_b = (_a = this.normalizeOptionalText(data.code)) === null || _a === void 0 ? void 0 : _a.toUpperCase()) !== null && _b !== void 0 ? _b : (await this.generateContractCode(tx));
|
|
787
1213
|
const created = await tx.$queryRawUnsafe(`INSERT INTO operations_contract (
|
|
788
1214
|
code,
|
|
789
1215
|
name,
|
|
@@ -795,6 +1221,7 @@ let OperationsService = class OperationsService {
|
|
|
795
1221
|
billing_model,
|
|
796
1222
|
account_manager_collaborator_id,
|
|
797
1223
|
related_collaborator_id,
|
|
1224
|
+
contract_template_id,
|
|
798
1225
|
origin_type,
|
|
799
1226
|
origin_id,
|
|
800
1227
|
start_date,
|
|
@@ -804,23 +1231,42 @@ let OperationsService = class OperationsService {
|
|
|
804
1231
|
budget_amount,
|
|
805
1232
|
monthly_hour_cap,
|
|
806
1233
|
status,
|
|
1234
|
+
creation_mode,
|
|
1235
|
+
wizard_step,
|
|
807
1236
|
description,
|
|
808
1237
|
content_html,
|
|
809
1238
|
created_at,
|
|
810
1239
|
updated_at
|
|
811
1240
|
) VALUES (
|
|
812
|
-
$1, $2,
|
|
813
|
-
|
|
814
|
-
$
|
|
1241
|
+
$1, $2,
|
|
1242
|
+
$3::operations_contract_contract_category_70d553ea09_enum,
|
|
1243
|
+
$4::operations_contract_contract_type_48331e2ebf_enum,
|
|
1244
|
+
$5,
|
|
1245
|
+
$6::operations_contract_signature_status_2cb7282a7b_enum,
|
|
1246
|
+
$7,
|
|
1247
|
+
$8::operations_contract_billing_model_409dc7fea2_enum,
|
|
1248
|
+
$9,
|
|
1249
|
+
$10,
|
|
1250
|
+
$11,
|
|
1251
|
+
$12::operations_contract_origin_type_07a7cc2b5d_enum,
|
|
1252
|
+
$13,
|
|
1253
|
+
$14::date,
|
|
1254
|
+
$15::date, $16::date, $17::date, $18, $19,
|
|
1255
|
+
$20::operations_contract_status_a0395962df_enum,
|
|
1256
|
+
$21::operations_contract_creation_mode_98ba669209_enum,
|
|
1257
|
+
$22,
|
|
1258
|
+
$23,
|
|
1259
|
+
$24,
|
|
1260
|
+
NOW(), NOW()
|
|
815
1261
|
)
|
|
816
|
-
RETURNING id`,
|
|
817
|
-
const contractId = (
|
|
1262
|
+
RETURNING id`, normalizedCode, this.normalizeOptionalText(data.name), (_c = data.contractCategory) !== null && _c !== void 0 ? _c : 'client', (_d = data.contractType) !== null && _d !== void 0 ? _d : 'service_agreement', this.normalizeOptionalText(data.clientName), (_e = data.signatureStatus) !== null && _e !== void 0 ? _e : 'not_started', (_f = data.isActive) !== null && _f !== void 0 ? _f : true, (_g = data.billingModel) !== null && _g !== void 0 ? _g : 'time_and_material', (_h = data.accountManagerCollaboratorId) !== null && _h !== void 0 ? _h : null, (_j = data.relatedCollaboratorId) !== null && _j !== void 0 ? _j : null, (_k = data.contractTemplateId) !== null && _k !== void 0 ? _k : null, (_l = data.originType) !== null && _l !== void 0 ? _l : 'manual', (_m = data.originId) !== null && _m !== void 0 ? _m : null, this.normalizeOptionalText((_o = data.startDate) !== null && _o !== void 0 ? _o : null), (_p = data.endDate) !== null && _p !== void 0 ? _p : null, (_q = data.signedAt) !== null && _q !== void 0 ? _q : null, (_s = (_r = data.effectiveDate) !== null && _r !== void 0 ? _r : data.startDate) !== null && _s !== void 0 ? _s : null, (_t = data.budgetAmount) !== null && _t !== void 0 ? _t : null, (_u = data.monthlyHourCap) !== null && _u !== void 0 ? _u : null, (_v = data.status) !== null && _v !== void 0 ? _v : 'draft', (_w = data.creationMode) !== null && _w !== void 0 ? _w : 'blank', (_x = data.wizardStep) !== null && _x !== void 0 ? _x : 0, (_y = data.description) !== null && _y !== void 0 ? _y : null, (_z = data.contentHtml) !== null && _z !== void 0 ? _z : null);
|
|
1263
|
+
const contractId = (_0 = created[0]) === null || _0 === void 0 ? void 0 : _0.id;
|
|
818
1264
|
await this.replaceContractParties(tx, contractId, data.parties);
|
|
819
1265
|
await this.replaceContractSignatures(tx, contractId, data.signatures);
|
|
820
1266
|
await this.replaceContractFinancialTerms(tx, contractId, data.financialTerms);
|
|
821
1267
|
await this.replaceContractRevisions(tx, contractId, data.revisions);
|
|
822
1268
|
if (data.replaceUploadedPdfDocument) {
|
|
823
|
-
await this.
|
|
1269
|
+
await this.replaceContractDocument(tx, contractId, 'source_upload', data.replaceUploadedPdfDocument);
|
|
824
1270
|
}
|
|
825
1271
|
await this.insertContractHistory(tx, contractId, userId, 'created', data.originType === 'manual'
|
|
826
1272
|
? 'Manual contract created from registry.'
|
|
@@ -829,34 +1275,674 @@ let OperationsService = class OperationsService {
|
|
|
829
1275
|
});
|
|
830
1276
|
return this.getContractById(userId, createdId);
|
|
831
1277
|
}
|
|
1278
|
+
async createContractDraft(userId, data) {
|
|
1279
|
+
const actor = await this.getActorContext(userId);
|
|
1280
|
+
this.ensureDirector(actor);
|
|
1281
|
+
const creationMode = this.normalizeEnumValue(data.creationMode, CONTRACT_CREATION_MODE_VALUES, 'blank');
|
|
1282
|
+
const selectedTemplate = data.templateId && data.templateId > 0
|
|
1283
|
+
? await this.getContractTemplateById(userId, data.templateId)
|
|
1284
|
+
: null;
|
|
1285
|
+
const duplicateSource = data.duplicateFromId && data.duplicateFromId > 0
|
|
1286
|
+
? await this.getContractById(userId, data.duplicateFromId)
|
|
1287
|
+
: null;
|
|
1288
|
+
const storedSourceFile = data.sourceFileId && data.sourceFileId > 0
|
|
1289
|
+
? await this.prisma.file.findUnique({
|
|
1290
|
+
where: { id: data.sourceFileId },
|
|
1291
|
+
include: { file_mimetype: true },
|
|
1292
|
+
})
|
|
1293
|
+
: null;
|
|
1294
|
+
if (data.sourceFileId && !storedSourceFile) {
|
|
1295
|
+
throw new common_1.NotFoundException('Source contract file not found.');
|
|
1296
|
+
}
|
|
1297
|
+
const createdId = await this.prisma.$transaction(async (tx) => {
|
|
1298
|
+
var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p, _q, _r, _s, _t, _u, _v, _w, _x, _y, _z, _0, _1, _2, _3, _4, _5, _6, _7, _8, _9, _10, _11, _12, _13, _14, _15, _16;
|
|
1299
|
+
const generatedCode = await this.generateContractCode(tx);
|
|
1300
|
+
const duplicateNameBase = (_b = (_a = this.normalizeOptionalText(duplicateSource === null || duplicateSource === void 0 ? void 0 : duplicateSource.name)) !== null && _a !== void 0 ? _a : this.normalizeOptionalText(duplicateSource === null || duplicateSource === void 0 ? void 0 : duplicateSource.code)) !== null && _b !== void 0 ? _b : 'Contract';
|
|
1301
|
+
const created = await tx.$queryRawUnsafe(`INSERT INTO operations_contract (
|
|
1302
|
+
code,
|
|
1303
|
+
name,
|
|
1304
|
+
contract_category,
|
|
1305
|
+
contract_type,
|
|
1306
|
+
client_name,
|
|
1307
|
+
signature_status,
|
|
1308
|
+
is_active,
|
|
1309
|
+
billing_model,
|
|
1310
|
+
account_manager_collaborator_id,
|
|
1311
|
+
related_collaborator_id,
|
|
1312
|
+
contract_template_id,
|
|
1313
|
+
origin_type,
|
|
1314
|
+
origin_id,
|
|
1315
|
+
start_date,
|
|
1316
|
+
end_date,
|
|
1317
|
+
signed_at,
|
|
1318
|
+
effective_date,
|
|
1319
|
+
budget_amount,
|
|
1320
|
+
monthly_hour_cap,
|
|
1321
|
+
status,
|
|
1322
|
+
creation_mode,
|
|
1323
|
+
wizard_step,
|
|
1324
|
+
description,
|
|
1325
|
+
content_html,
|
|
1326
|
+
created_by_user_id,
|
|
1327
|
+
updated_by_user_id,
|
|
1328
|
+
created_at,
|
|
1329
|
+
updated_at
|
|
1330
|
+
) VALUES (
|
|
1331
|
+
$1,
|
|
1332
|
+
$2,
|
|
1333
|
+
$3::operations_contract_contract_category_70d553ea09_enum,
|
|
1334
|
+
$4::operations_contract_contract_type_48331e2ebf_enum,
|
|
1335
|
+
$5,
|
|
1336
|
+
$6::operations_contract_signature_status_2cb7282a7b_enum,
|
|
1337
|
+
$7,
|
|
1338
|
+
$8::operations_contract_billing_model_409dc7fea2_enum,
|
|
1339
|
+
$9,
|
|
1340
|
+
$10,
|
|
1341
|
+
$11,
|
|
1342
|
+
$12::operations_contract_origin_type_07a7cc2b5d_enum,
|
|
1343
|
+
$13,
|
|
1344
|
+
$14::date,
|
|
1345
|
+
$15::date,
|
|
1346
|
+
$16::date,
|
|
1347
|
+
$17::date,
|
|
1348
|
+
$18,
|
|
1349
|
+
$19,
|
|
1350
|
+
$20::operations_contract_status_a0395962df_enum,
|
|
1351
|
+
$21::operations_contract_creation_mode_98ba669209_enum,
|
|
1352
|
+
$22,
|
|
1353
|
+
$23,
|
|
1354
|
+
$24,
|
|
1355
|
+
$25,
|
|
1356
|
+
$25,
|
|
1357
|
+
NOW(),
|
|
1358
|
+
NOW()
|
|
1359
|
+
)
|
|
1360
|
+
RETURNING id`, generatedCode, creationMode === 'duplicate'
|
|
1361
|
+
? `${duplicateNameBase} Copy`
|
|
1362
|
+
: this.normalizeOptionalText(selectedTemplate === null || selectedTemplate === void 0 ? void 0 : selectedTemplate.name), (_d = (_c = duplicateSource === null || duplicateSource === void 0 ? void 0 : duplicateSource.contractCategory) !== null && _c !== void 0 ? _c : selectedTemplate === null || selectedTemplate === void 0 ? void 0 : selectedTemplate.contractCategory) !== null && _d !== void 0 ? _d : 'client', (_f = (_e = duplicateSource === null || duplicateSource === void 0 ? void 0 : duplicateSource.contractType) !== null && _e !== void 0 ? _e : selectedTemplate === null || selectedTemplate === void 0 ? void 0 : selectedTemplate.contractType) !== null && _f !== void 0 ? _f : 'service_agreement', this.normalizeOptionalText(duplicateSource === null || duplicateSource === void 0 ? void 0 : duplicateSource.clientName), (_h = (_g = duplicateSource === null || duplicateSource === void 0 ? void 0 : duplicateSource.signatureStatus) !== null && _g !== void 0 ? _g : selectedTemplate === null || selectedTemplate === void 0 ? void 0 : selectedTemplate.signatureStatus) !== null && _h !== void 0 ? _h : 'not_started', (_j = duplicateSource === null || duplicateSource === void 0 ? void 0 : duplicateSource.isActive) !== null && _j !== void 0 ? _j : true, (_l = (_k = duplicateSource === null || duplicateSource === void 0 ? void 0 : duplicateSource.billingModel) !== null && _k !== void 0 ? _k : selectedTemplate === null || selectedTemplate === void 0 ? void 0 : selectedTemplate.billingModel) !== null && _l !== void 0 ? _l : 'time_and_material', (_m = duplicateSource === null || duplicateSource === void 0 ? void 0 : duplicateSource.accountManagerCollaboratorId) !== null && _m !== void 0 ? _m : null, (_o = duplicateSource === null || duplicateSource === void 0 ? void 0 : duplicateSource.relatedCollaboratorId) !== null && _o !== void 0 ? _o : null, (_q = (_p = duplicateSource === null || duplicateSource === void 0 ? void 0 : duplicateSource.contractTemplateId) !== null && _p !== void 0 ? _p : selectedTemplate === null || selectedTemplate === void 0 ? void 0 : selectedTemplate.id) !== null && _q !== void 0 ? _q : null, (_r = duplicateSource === null || duplicateSource === void 0 ? void 0 : duplicateSource.originType) !== null && _r !== void 0 ? _r : 'manual', (_s = duplicateSource === null || duplicateSource === void 0 ? void 0 : duplicateSource.originId) !== null && _s !== void 0 ? _s : null, (_t = duplicateSource === null || duplicateSource === void 0 ? void 0 : duplicateSource.startDate) !== null && _t !== void 0 ? _t : null, (_u = duplicateSource === null || duplicateSource === void 0 ? void 0 : duplicateSource.endDate) !== null && _u !== void 0 ? _u : null, (_v = duplicateSource === null || duplicateSource === void 0 ? void 0 : duplicateSource.signedAt) !== null && _v !== void 0 ? _v : null, (_w = duplicateSource === null || duplicateSource === void 0 ? void 0 : duplicateSource.effectiveDate) !== null && _w !== void 0 ? _w : null, (_x = duplicateSource === null || duplicateSource === void 0 ? void 0 : duplicateSource.budgetAmount) !== null && _x !== void 0 ? _x : null, (_y = duplicateSource === null || duplicateSource === void 0 ? void 0 : duplicateSource.monthlyHourCap) !== null && _y !== void 0 ? _y : null, 'draft', creationMode, creationMode === 'upload' ? 0 : creationMode === 'blank' ? 0 : 1, (_0 = (_z = duplicateSource === null || duplicateSource === void 0 ? void 0 : duplicateSource.description) !== null && _z !== void 0 ? _z : selectedTemplate === null || selectedTemplate === void 0 ? void 0 : selectedTemplate.description) !== null && _0 !== void 0 ? _0 : null, (_2 = (_1 = duplicateSource === null || duplicateSource === void 0 ? void 0 : duplicateSource.contentHtml) !== null && _1 !== void 0 ? _1 : selectedTemplate === null || selectedTemplate === void 0 ? void 0 : selectedTemplate.contentHtml) !== null && _2 !== void 0 ? _2 : null, userId);
|
|
1363
|
+
const contractId = (_3 = created[0]) === null || _3 === void 0 ? void 0 : _3.id;
|
|
1364
|
+
if (duplicateSource) {
|
|
1365
|
+
await this.replaceContractParties(tx, contractId, duplicateSource.parties);
|
|
1366
|
+
await this.replaceContractSignatures(tx, contractId, duplicateSource.signatures);
|
|
1367
|
+
await this.replaceContractFinancialTerms(tx, contractId, duplicateSource.financialTerms);
|
|
1368
|
+
await this.replaceContractRevisions(tx, contractId, duplicateSource.revisions);
|
|
1369
|
+
const currentSourceDocument = (_4 = duplicateSource.documents.find((document) => document.isCurrent && document.documentType === 'source_upload')) !== null && _4 !== void 0 ? _4 : null;
|
|
1370
|
+
if (currentSourceDocument) {
|
|
1371
|
+
await this.replaceContractDocument(tx, contractId, 'source_upload', {
|
|
1372
|
+
fileId: (_5 = currentSourceDocument.fileId) !== null && _5 !== void 0 ? _5 : null,
|
|
1373
|
+
fileName: currentSourceDocument.fileName,
|
|
1374
|
+
mimeType: currentSourceDocument.mimeType,
|
|
1375
|
+
fileContentBase64: (_6 = currentSourceDocument.fileContentBase64) !== null && _6 !== void 0 ? _6 : null,
|
|
1376
|
+
notes: (_7 = currentSourceDocument.notes) !== null && _7 !== void 0 ? _7 : null,
|
|
1377
|
+
extractionStatus: (_8 = currentSourceDocument.extractionStatus) !== null && _8 !== void 0 ? _8 : 'skipped',
|
|
1378
|
+
extractionSummary: (_9 = currentSourceDocument.extractionSummary) !== null && _9 !== void 0 ? _9 : null,
|
|
1379
|
+
});
|
|
1380
|
+
}
|
|
1381
|
+
}
|
|
1382
|
+
if (storedSourceFile) {
|
|
1383
|
+
await this.replaceContractDocument(tx, contractId, 'source_upload', {
|
|
1384
|
+
fileId: storedSourceFile.id,
|
|
1385
|
+
fileName: (_10 = this.normalizeOptionalText(data.sourceFileName)) !== null && _10 !== void 0 ? _10 : storedSourceFile.filename,
|
|
1386
|
+
mimeType: (_13 = (_11 = this.normalizeOptionalText(data.sourceMimeType)) !== null && _11 !== void 0 ? _11 : (_12 = storedSourceFile.file_mimetype) === null || _12 === void 0 ? void 0 : _12.name) !== null && _13 !== void 0 ? _13 : 'application/pdf',
|
|
1387
|
+
notes: 'Source contract document uploaded during draft creation.',
|
|
1388
|
+
extractionStatus: 'pending',
|
|
1389
|
+
extractionSummary: null,
|
|
1390
|
+
});
|
|
1391
|
+
}
|
|
1392
|
+
await this.insertContractHistory(tx, contractId, userId, 'draft_created', `Contract draft created with mode ${creationMode}.`, JSON.stringify({
|
|
1393
|
+
creationMode,
|
|
1394
|
+
templateId: (_14 = selectedTemplate === null || selectedTemplate === void 0 ? void 0 : selectedTemplate.id) !== null && _14 !== void 0 ? _14 : null,
|
|
1395
|
+
duplicateFromId: (_15 = duplicateSource === null || duplicateSource === void 0 ? void 0 : duplicateSource.id) !== null && _15 !== void 0 ? _15 : null,
|
|
1396
|
+
sourceFileId: (_16 = storedSourceFile === null || storedSourceFile === void 0 ? void 0 : storedSourceFile.id) !== null && _16 !== void 0 ? _16 : null,
|
|
1397
|
+
}));
|
|
1398
|
+
return contractId;
|
|
1399
|
+
});
|
|
1400
|
+
return this.getContractById(userId, createdId);
|
|
1401
|
+
}
|
|
1402
|
+
async extractContractSource(userId, contractId, data) {
|
|
1403
|
+
var _a, _b;
|
|
1404
|
+
const actor = await this.getActorContext(userId);
|
|
1405
|
+
this.ensureDirector(actor);
|
|
1406
|
+
const contract = await this.getContractById(userId, contractId);
|
|
1407
|
+
const sourceDocument = contract.documents.find((document) => document.isCurrent && document.documentType === 'source_upload');
|
|
1408
|
+
if (!sourceDocument) {
|
|
1409
|
+
throw new common_1.BadRequestException('No source document is attached to this contract draft.');
|
|
1410
|
+
}
|
|
1411
|
+
await this.prisma.$executeRawUnsafe(`UPDATE operations_contract_document
|
|
1412
|
+
SET extraction_status = 'processing',
|
|
1413
|
+
updated_at = NOW()
|
|
1414
|
+
WHERE id = $1`, sourceDocument.id);
|
|
1415
|
+
try {
|
|
1416
|
+
const extracted = await this.extractContractDraft(userId, {
|
|
1417
|
+
contractId,
|
|
1418
|
+
provider: (_a = data.provider) !== null && _a !== void 0 ? _a : null,
|
|
1419
|
+
promptMessage: (_b = data.promptMessage) !== null && _b !== void 0 ? _b : null,
|
|
1420
|
+
});
|
|
1421
|
+
await this.prisma.$executeRawUnsafe(`UPDATE operations_contract_document
|
|
1422
|
+
SET extraction_status = 'completed',
|
|
1423
|
+
extraction_summary = $2,
|
|
1424
|
+
updated_at = NOW()
|
|
1425
|
+
WHERE id = $1`, sourceDocument.id, extracted.summary || extracted.description || extracted.name || null);
|
|
1426
|
+
await this.insertContractHistory(this.prisma, contractId, userId, 'source_extracted', 'Source contract document extracted with AI.');
|
|
1427
|
+
return extracted;
|
|
1428
|
+
}
|
|
1429
|
+
catch (error) {
|
|
1430
|
+
await this.prisma.$executeRawUnsafe(`UPDATE operations_contract_document
|
|
1431
|
+
SET extraction_status = 'failed',
|
|
1432
|
+
updated_at = NOW()
|
|
1433
|
+
WHERE id = $1`, sourceDocument.id);
|
|
1434
|
+
throw error;
|
|
1435
|
+
}
|
|
1436
|
+
}
|
|
1437
|
+
async generateContractContent(userId, contractId, data = {}) {
|
|
1438
|
+
const actor = await this.getActorContext(userId);
|
|
1439
|
+
this.ensureDirector(actor);
|
|
1440
|
+
const contract = await this.getContractById(userId, contractId);
|
|
1441
|
+
const contentHtml = await this.generateContractContentHtml(contract, data);
|
|
1442
|
+
await this.prisma.$transaction(async (tx) => {
|
|
1443
|
+
var _a;
|
|
1444
|
+
await tx.$executeRawUnsafe(`UPDATE operations_contract
|
|
1445
|
+
SET content_html = $2,
|
|
1446
|
+
wizard_step = CASE
|
|
1447
|
+
WHEN COALESCE(wizard_step, 0) < 4 THEN 4
|
|
1448
|
+
ELSE wizard_step
|
|
1449
|
+
END,
|
|
1450
|
+
updated_by_user_id = $3,
|
|
1451
|
+
updated_at = NOW()
|
|
1452
|
+
WHERE id = $1`, contractId, contentHtml, userId);
|
|
1453
|
+
await this.insertContractHistory(tx, contractId, userId, 'content_generated', 'Contract content generated automatically for editing.', JSON.stringify({
|
|
1454
|
+
provider: (_a = data.provider) !== null && _a !== void 0 ? _a : null,
|
|
1455
|
+
overwrite: Boolean(data.overwrite),
|
|
1456
|
+
hasPrompt: Boolean(this.normalizeOptionalText(data.promptMessage)),
|
|
1457
|
+
}));
|
|
1458
|
+
});
|
|
1459
|
+
return this.getContractById(userId, contractId);
|
|
1460
|
+
}
|
|
1461
|
+
async reviewContractLegally(userId, contractId, data = {}) {
|
|
1462
|
+
const actor = await this.getActorContext(userId);
|
|
1463
|
+
this.ensureDirector(actor);
|
|
1464
|
+
const contract = await this.getContractById(userId, contractId);
|
|
1465
|
+
const review = await this.buildContractLegalReview(contract, data);
|
|
1466
|
+
await this.insertContractHistory(this.prisma, contractId, userId, 'legal_reviewed', 'Advisory legal checklist updated for this contract.', JSON.stringify(review));
|
|
1467
|
+
return review;
|
|
1468
|
+
}
|
|
1469
|
+
async generateContractPdf(userId, contractId) {
|
|
1470
|
+
const actor = await this.getActorContext(userId);
|
|
1471
|
+
this.ensureDirector(actor);
|
|
1472
|
+
const contract = await this.getContractById(userId, contractId);
|
|
1473
|
+
const pdfBuffer = await this.renderContractPdfBuffer(contract);
|
|
1474
|
+
const fileName = this.buildContractPdfFileName(contract);
|
|
1475
|
+
const uploadedFile = await this.fileService.upload('operations/contracts/generated', {
|
|
1476
|
+
fieldname: 'file',
|
|
1477
|
+
originalname: fileName,
|
|
1478
|
+
encoding: '7bit',
|
|
1479
|
+
mimetype: 'application/pdf',
|
|
1480
|
+
size: pdfBuffer.length,
|
|
1481
|
+
destination: '',
|
|
1482
|
+
filename: fileName,
|
|
1483
|
+
path: '',
|
|
1484
|
+
buffer: pdfBuffer,
|
|
1485
|
+
});
|
|
1486
|
+
await this.prisma.$transaction(async (tx) => {
|
|
1487
|
+
await this.replaceContractDocument(tx, contractId, 'generated_pdf', {
|
|
1488
|
+
fileId: uploadedFile.id,
|
|
1489
|
+
fileName,
|
|
1490
|
+
mimeType: 'application/pdf',
|
|
1491
|
+
notes: 'PDF generated from contract rich text content.',
|
|
1492
|
+
extractionStatus: 'skipped',
|
|
1493
|
+
extractionSummary: null,
|
|
1494
|
+
});
|
|
1495
|
+
await this.insertContractHistory(tx, contractId, userId, 'pdf_generated', 'Generated a branded PDF version of the contract.');
|
|
1496
|
+
});
|
|
1497
|
+
return {
|
|
1498
|
+
contractId,
|
|
1499
|
+
fileId: uploadedFile.id,
|
|
1500
|
+
fileName,
|
|
1501
|
+
mimeType: 'application/pdf',
|
|
1502
|
+
documentType: 'generated_pdf',
|
|
1503
|
+
downloadUrl: `/file/open/${uploadedFile.id}`,
|
|
1504
|
+
};
|
|
1505
|
+
}
|
|
1506
|
+
async extractContractDraft(userId, data) {
|
|
1507
|
+
var _a;
|
|
1508
|
+
const actor = await this.getActorContext(userId);
|
|
1509
|
+
this.ensureDirector(actor);
|
|
1510
|
+
const uploadFile = await this.resolveContractExtractionFile(userId, data);
|
|
1511
|
+
const aiResult = await this.aiService.chat({
|
|
1512
|
+
provider: data.provider === 'gemini' ? 'gemini' : 'openai',
|
|
1513
|
+
model: data.provider === 'gemini' ? 'gemini-1.5-flash' : 'gpt-4o-mini',
|
|
1514
|
+
message: this.normalizeExtractionString(data.promptMessage) ||
|
|
1515
|
+
'Analyze the attached contract and extract a structured draft for the contract form.',
|
|
1516
|
+
systemPrompt: this.buildContractExtractionSystemPrompt(),
|
|
1517
|
+
}, uploadFile);
|
|
1518
|
+
const parsed = this.parseAiJsonPayload(String((_a = aiResult === null || aiResult === void 0 ? void 0 : aiResult.content) !== null && _a !== void 0 ? _a : ''));
|
|
1519
|
+
const draft = this.normalizeContractExtractDraft(parsed);
|
|
1520
|
+
const warnings = [...draft.warnings];
|
|
1521
|
+
if (uploadFile.mimetype === 'application/msword' ||
|
|
1522
|
+
uploadFile.mimetype ===
|
|
1523
|
+
'application/vnd.openxmlformats-officedocument.wordprocessingml.document') {
|
|
1524
|
+
warnings.push('Word extraction is best-effort. Review names, dates, and values before saving.');
|
|
1525
|
+
}
|
|
1526
|
+
return Object.assign(Object.assign({}, draft), { warnings: Array.from(new Set(warnings.filter(Boolean))) });
|
|
1527
|
+
}
|
|
1528
|
+
async createContractFromProposalIntegration(payload) {
|
|
1529
|
+
var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p, _q, _r, _s, _t, _u, _v, _w, _x, _y, _z;
|
|
1530
|
+
const sourceEntityId = String(payload.proposalId || '').trim();
|
|
1531
|
+
if (!sourceEntityId) {
|
|
1532
|
+
throw new common_1.BadRequestException('proposalId is required for CRM proposal integration.');
|
|
1533
|
+
}
|
|
1534
|
+
const proposal = payload.proposal || {
|
|
1535
|
+
code: (_a = payload.code) !== null && _a !== void 0 ? _a : null,
|
|
1536
|
+
title: (_b = payload.title) !== null && _b !== void 0 ? _b : null,
|
|
1537
|
+
contractCategory: (_d = (_c = payload.commercialTerms) === null || _c === void 0 ? void 0 : _c.contractCategory) !== null && _d !== void 0 ? _d : 'client',
|
|
1538
|
+
contractType: (_f = (_e = payload.commercialTerms) === null || _e === void 0 ? void 0 : _e.contractType) !== null && _f !== void 0 ? _f : 'service_agreement',
|
|
1539
|
+
billingModel: (_h = (_g = payload.commercialTerms) === null || _g === void 0 ? void 0 : _g.billingModel) !== null && _h !== void 0 ? _h : 'fixed_price',
|
|
1540
|
+
validFrom: (_k = (_j = payload.commercialTerms) === null || _j === void 0 ? void 0 : _j.validFrom) !== null && _k !== void 0 ? _k : null,
|
|
1541
|
+
validUntil: (_o = (_l = payload.validUntil) !== null && _l !== void 0 ? _l : (_m = payload.commercialTerms) === null || _m === void 0 ? void 0 : _m.validUntil) !== null && _o !== void 0 ? _o : null,
|
|
1542
|
+
totalAmount: (_p = payload.total) !== null && _p !== void 0 ? _p : null,
|
|
1543
|
+
totalAmountCents: payload.total != null ? Math.round(Number(payload.total) * 100) : null,
|
|
1544
|
+
notes: (_r = (_q = payload.commercialTerms) === null || _q === void 0 ? void 0 : _q.notes) !== null && _r !== void 0 ? _r : null,
|
|
1545
|
+
};
|
|
1546
|
+
const person = payload.person || {};
|
|
1547
|
+
const items = Array.isArray(payload.items) ? payload.items : [];
|
|
1548
|
+
const contractCategory = (_s = proposal.contractCategory) !== null && _s !== void 0 ? _s : 'client';
|
|
1549
|
+
const contractType = (_t = proposal.contractType) !== null && _t !== void 0 ? _t : 'service_agreement';
|
|
1550
|
+
const billingModel = (_u = proposal.billingModel) !== null && _u !== void 0 ? _u : 'fixed_price';
|
|
1551
|
+
const clientName = (_x = (_w = (_v = this.normalizeOptionalText(person.tradeName)) !== null && _v !== void 0 ? _v : this.normalizeOptionalText(person.name)) !== null && _w !== void 0 ? _w : this.normalizeOptionalText(proposal.title)) !== null && _x !== void 0 ? _x : `Proposal ${sourceEntityId}`;
|
|
1552
|
+
const contractName = (_y = this.normalizeOptionalText(proposal.title)) !== null && _y !== void 0 ? _y : `Proposal ${sourceEntityId}`;
|
|
1553
|
+
const financialTerms = items
|
|
1554
|
+
.map((item) => {
|
|
1555
|
+
var _a, _b, _c;
|
|
1556
|
+
return ({
|
|
1557
|
+
termType: this.normalizeEnumValue(item.termType, FINANCIAL_TERM_TYPE_VALUES, 'value'),
|
|
1558
|
+
label: (_a = this.normalizeOptionalText(item.name)) !== null && _a !== void 0 ? _a : 'Commercial term',
|
|
1559
|
+
amount: Number((_b = item.amount) !== null && _b !== void 0 ? _b : Number(item.totalAmountCents || 0) / 100),
|
|
1560
|
+
recurrence: this.normalizeEnumValue(item.recurrence, RECURRENCE_VALUES, 'one_time'),
|
|
1561
|
+
dueDay: (_c = item.dueDay) !== null && _c !== void 0 ? _c : null,
|
|
1562
|
+
notes: this.normalizeOptionalText(item.description),
|
|
1563
|
+
});
|
|
1564
|
+
})
|
|
1565
|
+
.filter((term) => Number.isFinite(term.amount) && term.amount > 0);
|
|
1566
|
+
const fallbackAmount = Number((_z = proposal.totalAmount) !== null && _z !== void 0 ? _z : 0);
|
|
1567
|
+
if (financialTerms.length === 0 && Number.isFinite(fallbackAmount) && fallbackAmount > 0) {
|
|
1568
|
+
financialTerms.push({
|
|
1569
|
+
termType: 'revenue',
|
|
1570
|
+
label: contractName,
|
|
1571
|
+
amount: fallbackAmount,
|
|
1572
|
+
recurrence: 'one_time',
|
|
1573
|
+
dueDay: null,
|
|
1574
|
+
notes: this.normalizeOptionalText(proposal.notes),
|
|
1575
|
+
});
|
|
1576
|
+
}
|
|
1577
|
+
const primaryPartyRole = ['employee', 'contractor'].includes(contractCategory)
|
|
1578
|
+
? 'employee'
|
|
1579
|
+
: ['supplier', 'vendor'].includes(contractCategory)
|
|
1580
|
+
? 'supplier'
|
|
1581
|
+
: contractCategory === 'partner'
|
|
1582
|
+
? 'partner'
|
|
1583
|
+
: 'client';
|
|
1584
|
+
return this.prisma.$transaction(async (tx) => {
|
|
1585
|
+
var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p, _q, _r;
|
|
1586
|
+
await tx.$queryRawUnsafe(`SELECT pg_advisory_xact_lock(hashtext($1))`, `operations:crm_proposal:${sourceEntityId}`);
|
|
1587
|
+
const existingContracts = await tx.$queryRawUnsafe(`SELECT id, code
|
|
1588
|
+
FROM operations_contract
|
|
1589
|
+
WHERE deleted_at IS NULL
|
|
1590
|
+
AND origin_type = $1::operations_contract_origin_type_07a7cc2b5d_enum
|
|
1591
|
+
AND origin_id::text = $2
|
|
1592
|
+
ORDER BY id DESC
|
|
1593
|
+
LIMIT 1`, 'crm_proposal', sourceEntityId);
|
|
1594
|
+
const existingContract = existingContracts[0];
|
|
1595
|
+
if (existingContract === null || existingContract === void 0 ? void 0 : existingContract.id) {
|
|
1596
|
+
return {
|
|
1597
|
+
id: existingContract.id,
|
|
1598
|
+
code: (_a = existingContract.code) !== null && _a !== void 0 ? _a : `CONTRACT-${existingContract.id}`,
|
|
1599
|
+
};
|
|
1600
|
+
}
|
|
1601
|
+
const generatedCode = await this.generateContractCode(tx);
|
|
1602
|
+
const created = await tx.$queryRawUnsafe(`INSERT INTO operations_contract (
|
|
1603
|
+
code,
|
|
1604
|
+
name,
|
|
1605
|
+
contract_category,
|
|
1606
|
+
contract_type,
|
|
1607
|
+
client_name,
|
|
1608
|
+
signature_status,
|
|
1609
|
+
is_active,
|
|
1610
|
+
billing_model,
|
|
1611
|
+
account_manager_collaborator_id,
|
|
1612
|
+
related_collaborator_id,
|
|
1613
|
+
contract_template_id,
|
|
1614
|
+
origin_type,
|
|
1615
|
+
origin_id,
|
|
1616
|
+
start_date,
|
|
1617
|
+
end_date,
|
|
1618
|
+
signed_at,
|
|
1619
|
+
effective_date,
|
|
1620
|
+
budget_amount,
|
|
1621
|
+
monthly_hour_cap,
|
|
1622
|
+
status,
|
|
1623
|
+
creation_mode,
|
|
1624
|
+
wizard_step,
|
|
1625
|
+
description,
|
|
1626
|
+
content_html,
|
|
1627
|
+
created_by_user_id,
|
|
1628
|
+
updated_by_user_id,
|
|
1629
|
+
created_at,
|
|
1630
|
+
updated_at
|
|
1631
|
+
) VALUES (
|
|
1632
|
+
$1,
|
|
1633
|
+
$2,
|
|
1634
|
+
$3::operations_contract_contract_category_70d553ea09_enum,
|
|
1635
|
+
$4::operations_contract_contract_type_48331e2ebf_enum,
|
|
1636
|
+
$5,
|
|
1637
|
+
$6::operations_contract_signature_status_2cb7282a7b_enum,
|
|
1638
|
+
$7,
|
|
1639
|
+
$8::operations_contract_billing_model_409dc7fea2_enum,
|
|
1640
|
+
$9,
|
|
1641
|
+
$10,
|
|
1642
|
+
$11,
|
|
1643
|
+
$12::operations_contract_origin_type_07a7cc2b5d_enum,
|
|
1644
|
+
$13,
|
|
1645
|
+
$14::date,
|
|
1646
|
+
$15::date,
|
|
1647
|
+
$16::date,
|
|
1648
|
+
$17::date,
|
|
1649
|
+
$18,
|
|
1650
|
+
$19,
|
|
1651
|
+
$20::operations_contract_status_a0395962df_enum,
|
|
1652
|
+
$21::operations_contract_creation_mode_98ba669209_enum,
|
|
1653
|
+
$22,
|
|
1654
|
+
$23,
|
|
1655
|
+
$24,
|
|
1656
|
+
$25,
|
|
1657
|
+
$25,
|
|
1658
|
+
NOW(),
|
|
1659
|
+
NOW()
|
|
1660
|
+
)
|
|
1661
|
+
RETURNING id`, generatedCode, contractName, contractCategory, contractType, clientName, 'not_started', true, billingModel, null, null, null, 'crm_proposal', sourceEntityId, (_b = proposal.validFrom) !== null && _b !== void 0 ? _b : null, (_c = proposal.validUntil) !== null && _c !== void 0 ? _c : null, null, (_d = proposal.validFrom) !== null && _d !== void 0 ? _d : null, fallbackAmount > 0 ? fallbackAmount : null, null, 'draft', 'blank', 1, this.normalizeOptionalText(proposal.notes), this.normalizeOptionalText((_e = payload.revision) === null || _e === void 0 ? void 0 : _e.contentHtml), Number(payload.approvedByUserId) || null);
|
|
1662
|
+
const contractId = (_f = created[0]) === null || _f === void 0 ? void 0 : _f.id;
|
|
1663
|
+
if (!contractId) {
|
|
1664
|
+
throw new common_1.BadRequestException('Could not create contract draft from proposal.');
|
|
1665
|
+
}
|
|
1666
|
+
await this.replaceContractParties(tx, contractId, [
|
|
1667
|
+
{
|
|
1668
|
+
partyRole: primaryPartyRole,
|
|
1669
|
+
partyType: person.tradeName ? 'company' : 'individual',
|
|
1670
|
+
displayName: clientName,
|
|
1671
|
+
documentNumber: this.normalizeOptionalText(person.document),
|
|
1672
|
+
email: this.normalizeOptionalText(person.email),
|
|
1673
|
+
phone: this.normalizeOptionalText(person.phone),
|
|
1674
|
+
isPrimary: true,
|
|
1675
|
+
},
|
|
1676
|
+
]);
|
|
1677
|
+
await this.replaceContractFinancialTerms(tx, contractId, financialTerms.map((term) => ({
|
|
1678
|
+
termType: term.termType,
|
|
1679
|
+
label: term.label,
|
|
1680
|
+
amount: term.amount,
|
|
1681
|
+
recurrence: term.recurrence,
|
|
1682
|
+
dueDay: term.dueDay,
|
|
1683
|
+
notes: term.notes,
|
|
1684
|
+
})));
|
|
1685
|
+
await this.replaceContractRevisions(tx, contractId, [
|
|
1686
|
+
{
|
|
1687
|
+
revisionType: 'revision',
|
|
1688
|
+
title: (_h = (_g = payload.revision) === null || _g === void 0 ? void 0 : _g.title) !== null && _h !== void 0 ? _h : contractName,
|
|
1689
|
+
effectiveDate: (_j = proposal.validFrom) !== null && _j !== void 0 ? _j : null,
|
|
1690
|
+
status: 'draft',
|
|
1691
|
+
summary: (_l = this.normalizeOptionalText((_k = payload.revision) === null || _k === void 0 ? void 0 : _k.summary)) !== null && _l !== void 0 ? _l : this.normalizeOptionalText(proposal.notes),
|
|
1692
|
+
},
|
|
1693
|
+
]);
|
|
1694
|
+
await this.insertContractHistory(tx, contractId, Number(payload.approvedByUserId) || null, 'created', `Draft contract generated from CRM proposal ${(_m = proposal.code) !== null && _m !== void 0 ? _m : sourceEntityId}.`, JSON.stringify({
|
|
1695
|
+
correlationId: payload.correlationId || `proposal:${sourceEntityId}`,
|
|
1696
|
+
proposalId: (_o = payload.proposalId) !== null && _o !== void 0 ? _o : (Number(sourceEntityId) > 0 ? Number(sourceEntityId) : null),
|
|
1697
|
+
proposalRevisionId: (_p = payload.proposalRevisionId) !== null && _p !== void 0 ? _p : null,
|
|
1698
|
+
sourceModule: 'contact',
|
|
1699
|
+
sourceEntity: 'proposal',
|
|
1700
|
+
sourceId: sourceEntityId,
|
|
1701
|
+
approvedByUserId: Number(payload.approvedByUserId) || null,
|
|
1702
|
+
}));
|
|
1703
|
+
await this.integrationApi.publishEvent({
|
|
1704
|
+
eventName: 'operations.contract.created',
|
|
1705
|
+
sourceModule: 'operations',
|
|
1706
|
+
aggregateType: 'contract',
|
|
1707
|
+
aggregateId: String(contractId),
|
|
1708
|
+
payload: this.buildContractCreatedEventPayload({
|
|
1709
|
+
id: contractId,
|
|
1710
|
+
code: generatedCode,
|
|
1711
|
+
name: contractName,
|
|
1712
|
+
clientName,
|
|
1713
|
+
contractCategory,
|
|
1714
|
+
contractType,
|
|
1715
|
+
billingModel,
|
|
1716
|
+
originType: 'crm_proposal',
|
|
1717
|
+
originId: sourceEntityId,
|
|
1718
|
+
startDate: (_q = proposal.validFrom) !== null && _q !== void 0 ? _q : null,
|
|
1719
|
+
endDate: (_r = proposal.validUntil) !== null && _r !== void 0 ? _r : null,
|
|
1720
|
+
description: this.normalizeOptionalText(proposal.notes),
|
|
1721
|
+
financialTerms,
|
|
1722
|
+
}, payload, String(payload.locale || '').trim() || 'en', Number(payload.approvedByUserId) || undefined),
|
|
1723
|
+
metadata: {
|
|
1724
|
+
producer: 'operations',
|
|
1725
|
+
correlationId: payload.correlationId || `proposal:${sourceEntityId}`,
|
|
1726
|
+
sourceModule: 'operations',
|
|
1727
|
+
sourceEntity: 'contract',
|
|
1728
|
+
sourceId: String(contractId),
|
|
1729
|
+
originType: 'crm_proposal',
|
|
1730
|
+
originId: sourceEntityId,
|
|
1731
|
+
},
|
|
1732
|
+
}, {
|
|
1733
|
+
persistenceClient: tx,
|
|
1734
|
+
});
|
|
1735
|
+
return {
|
|
1736
|
+
id: contractId,
|
|
1737
|
+
code: generatedCode,
|
|
1738
|
+
};
|
|
1739
|
+
});
|
|
1740
|
+
}
|
|
1741
|
+
buildContractCreatedEventPayload(contract, payload, locale = 'en', createdByUserId) {
|
|
1742
|
+
var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p, _q, _r, _s, _t, _u, _v, _w, _x, _y, _z, _0, _1;
|
|
1743
|
+
return {
|
|
1744
|
+
contractId: contract.id,
|
|
1745
|
+
proposalId: (_a = payload.proposalId) !== null && _a !== void 0 ? _a : null,
|
|
1746
|
+
proposalRevisionId: (_b = payload.proposalRevisionId) !== null && _b !== void 0 ? _b : null,
|
|
1747
|
+
personId: (_e = (_c = payload.personId) !== null && _c !== void 0 ? _c : (_d = payload.person) === null || _d === void 0 ? void 0 : _d.id) !== null && _e !== void 0 ? _e : null,
|
|
1748
|
+
createdByUserId: createdByUserId !== null && createdByUserId !== void 0 ? createdByUserId : null,
|
|
1749
|
+
createdAt: new Date().toISOString(),
|
|
1750
|
+
locale,
|
|
1751
|
+
correlationId: payload.correlationId || `proposal:${(_g = (_f = payload.proposalId) !== null && _f !== void 0 ? _f : contract.originId) !== null && _g !== void 0 ? _g : contract.id}`,
|
|
1752
|
+
sourceModule: 'operations',
|
|
1753
|
+
sourceEntity: 'contract',
|
|
1754
|
+
sourceId: String(contract.id),
|
|
1755
|
+
source_module: 'operations',
|
|
1756
|
+
source_entity: 'contract',
|
|
1757
|
+
source_id: String(contract.id),
|
|
1758
|
+
contract: {
|
|
1759
|
+
id: contract.id,
|
|
1760
|
+
code: (_h = contract.code) !== null && _h !== void 0 ? _h : null,
|
|
1761
|
+
name: (_j = contract.name) !== null && _j !== void 0 ? _j : null,
|
|
1762
|
+
clientName: (_k = contract.clientName) !== null && _k !== void 0 ? _k : null,
|
|
1763
|
+
status: 'draft',
|
|
1764
|
+
contractCategory: (_l = contract.contractCategory) !== null && _l !== void 0 ? _l : 'client',
|
|
1765
|
+
contractType: (_m = contract.contractType) !== null && _m !== void 0 ? _m : 'service_agreement',
|
|
1766
|
+
billingModel: (_o = contract.billingModel) !== null && _o !== void 0 ? _o : 'fixed_price',
|
|
1767
|
+
originType: (_p = contract.originType) !== null && _p !== void 0 ? _p : 'manual',
|
|
1768
|
+
originId: (_q = contract.originId) !== null && _q !== void 0 ? _q : null,
|
|
1769
|
+
startDate: (_r = contract.startDate) !== null && _r !== void 0 ? _r : null,
|
|
1770
|
+
endDate: (_s = contract.endDate) !== null && _s !== void 0 ? _s : null,
|
|
1771
|
+
description: (_t = contract.description) !== null && _t !== void 0 ? _t : null,
|
|
1772
|
+
financialTerms: (_u = contract.financialTerms) !== null && _u !== void 0 ? _u : [],
|
|
1773
|
+
},
|
|
1774
|
+
proposal: (_v = payload.proposal) !== null && _v !== void 0 ? _v : {
|
|
1775
|
+
code: (_w = payload.code) !== null && _w !== void 0 ? _w : null,
|
|
1776
|
+
title: (_x = payload.title) !== null && _x !== void 0 ? _x : null,
|
|
1777
|
+
totalAmount: (_y = payload.total) !== null && _y !== void 0 ? _y : null,
|
|
1778
|
+
notes: (_0 = (_z = payload.commercialTerms) === null || _z === void 0 ? void 0 : _z.notes) !== null && _0 !== void 0 ? _0 : null,
|
|
1779
|
+
},
|
|
1780
|
+
person: (_1 = payload.person) !== null && _1 !== void 0 ? _1 : null,
|
|
1781
|
+
};
|
|
1782
|
+
}
|
|
1783
|
+
async buildContractActivatedEventPayload(contract, locale = 'en', activatedByUserId) {
|
|
1784
|
+
var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p, _q, _r, _s, _t, _u, _v, _w, _x, _y, _z, _0, _1, _2, _3, _4, _5, _6, _7, _8, _9, _10, _11;
|
|
1785
|
+
let personId = null;
|
|
1786
|
+
let proposalId = null;
|
|
1787
|
+
let personRecord = null;
|
|
1788
|
+
if (contract.originType === 'crm_proposal' && contract.originId) {
|
|
1789
|
+
const parsedProposalId = Number(contract.originId);
|
|
1790
|
+
if (Number.isInteger(parsedProposalId) && parsedProposalId > 0) {
|
|
1791
|
+
proposalId = parsedProposalId;
|
|
1792
|
+
const proposal = await this.prisma.proposal.findUnique({
|
|
1793
|
+
where: { id: parsedProposalId },
|
|
1794
|
+
include: {
|
|
1795
|
+
person: {
|
|
1796
|
+
select: {
|
|
1797
|
+
id: true,
|
|
1798
|
+
name: true,
|
|
1799
|
+
},
|
|
1800
|
+
},
|
|
1801
|
+
},
|
|
1802
|
+
});
|
|
1803
|
+
if (proposal) {
|
|
1804
|
+
personId = (_c = (_a = proposal.person_id) !== null && _a !== void 0 ? _a : (_b = proposal.person) === null || _b === void 0 ? void 0 : _b.id) !== null && _c !== void 0 ? _c : null;
|
|
1805
|
+
const [companyRow, contactRows, documentRows] = personId != null
|
|
1806
|
+
? await Promise.all([
|
|
1807
|
+
this.prisma.person_company.findUnique({
|
|
1808
|
+
where: { id: personId },
|
|
1809
|
+
select: { trade_name: true },
|
|
1810
|
+
}),
|
|
1811
|
+
this.prisma.contact.findMany({
|
|
1812
|
+
where: { person_id: personId },
|
|
1813
|
+
include: {
|
|
1814
|
+
contact_type: {
|
|
1815
|
+
select: { code: true },
|
|
1816
|
+
},
|
|
1817
|
+
},
|
|
1818
|
+
orderBy: [{ is_primary: 'desc' }, { id: 'asc' }],
|
|
1819
|
+
}),
|
|
1820
|
+
this.prisma.document.findMany({
|
|
1821
|
+
where: { person_id: personId },
|
|
1822
|
+
select: { value: true },
|
|
1823
|
+
orderBy: [{ id: 'asc' }],
|
|
1824
|
+
}),
|
|
1825
|
+
])
|
|
1826
|
+
: [null, [], []];
|
|
1827
|
+
const resolveContactValue = (codes) => {
|
|
1828
|
+
var _a, _b;
|
|
1829
|
+
const normalizedCodes = new Set(codes.map((code) => code.toUpperCase()));
|
|
1830
|
+
const items = Array.isArray(contactRows)
|
|
1831
|
+
? contactRows.filter((contact) => {
|
|
1832
|
+
var _a;
|
|
1833
|
+
return normalizedCodes.has(String(((_a = contact === null || contact === void 0 ? void 0 : contact.contact_type) === null || _a === void 0 ? void 0 : _a.code) || '').toUpperCase());
|
|
1834
|
+
})
|
|
1835
|
+
: [];
|
|
1836
|
+
const primary = items.find((contact) => contact === null || contact === void 0 ? void 0 : contact.is_primary);
|
|
1837
|
+
const fallback = items[0];
|
|
1838
|
+
return (_b = (_a = primary === null || primary === void 0 ? void 0 : primary.value) !== null && _a !== void 0 ? _a : fallback === null || fallback === void 0 ? void 0 : fallback.value) !== null && _b !== void 0 ? _b : null;
|
|
1839
|
+
};
|
|
1840
|
+
personRecord = proposal.person
|
|
1841
|
+
? Object.assign(Object.assign({}, proposal.person), { trade_name: (_d = companyRow === null || companyRow === void 0 ? void 0 : companyRow.trade_name) !== null && _d !== void 0 ? _d : null, email: resolveContactValue(['EMAIL']), phone: resolveContactValue(['PHONE', 'MOBILE', 'WHATSAPP']), document: (_f = (_e = documentRows[0]) === null || _e === void 0 ? void 0 : _e.value) !== null && _f !== void 0 ? _f : null }) : null;
|
|
1842
|
+
}
|
|
1843
|
+
}
|
|
1844
|
+
}
|
|
1845
|
+
const financialTerms = ((_g = contract.financialTerms) !== null && _g !== void 0 ? _g : []).map((term) => {
|
|
1846
|
+
var _a, _b, _c, _d, _e;
|
|
1847
|
+
return ({
|
|
1848
|
+
label: term.label,
|
|
1849
|
+
termType: (_a = term.termType) !== null && _a !== void 0 ? _a : 'value',
|
|
1850
|
+
amount: Number((_b = term.amount) !== null && _b !== void 0 ? _b : 0),
|
|
1851
|
+
recurrence: (_c = term.recurrence) !== null && _c !== void 0 ? _c : 'one_time',
|
|
1852
|
+
dueDay: (_d = term.dueDay) !== null && _d !== void 0 ? _d : null,
|
|
1853
|
+
notes: (_e = term.notes) !== null && _e !== void 0 ? _e : null,
|
|
1854
|
+
});
|
|
1855
|
+
});
|
|
1856
|
+
const totalAmount = financialTerms.reduce((sum, term) => sum + (Number.isFinite(term.amount) ? term.amount : 0), 0) || Number((_h = contract.budgetAmount) !== null && _h !== void 0 ? _h : 0);
|
|
1857
|
+
return {
|
|
1858
|
+
contractId: contract.id,
|
|
1859
|
+
proposalId,
|
|
1860
|
+
personId,
|
|
1861
|
+
activatedByUserId: activatedByUserId !== null && activatedByUserId !== void 0 ? activatedByUserId : null,
|
|
1862
|
+
signedByUserId: activatedByUserId !== null && activatedByUserId !== void 0 ? activatedByUserId : null,
|
|
1863
|
+
activatedAt: new Date().toISOString(),
|
|
1864
|
+
signedAt: (_l = (_k = (_j = contract.signedAt) !== null && _j !== void 0 ? _j : contract.effectiveDate) !== null && _k !== void 0 ? _k : contract.startDate) !== null && _l !== void 0 ? _l : null,
|
|
1865
|
+
locale,
|
|
1866
|
+
correlationId: proposalId ? `proposal:${proposalId}` : `contract:${contract.id}`,
|
|
1867
|
+
sourceModule: 'operations',
|
|
1868
|
+
sourceEntity: 'contract',
|
|
1869
|
+
sourceId: String(contract.id),
|
|
1870
|
+
source_module: 'operations',
|
|
1871
|
+
source_entity: 'contract',
|
|
1872
|
+
source_id: String(contract.id),
|
|
1873
|
+
contract: {
|
|
1874
|
+
code: (_m = contract.code) !== null && _m !== void 0 ? _m : null,
|
|
1875
|
+
name: (_o = contract.name) !== null && _o !== void 0 ? _o : null,
|
|
1876
|
+
clientName: (_p = contract.clientName) !== null && _p !== void 0 ? _p : null,
|
|
1877
|
+
contractCategory: (_q = contract.contractCategory) !== null && _q !== void 0 ? _q : 'client',
|
|
1878
|
+
contractType: (_r = contract.contractType) !== null && _r !== void 0 ? _r : 'service_agreement',
|
|
1879
|
+
billingModel: (_s = contract.billingModel) !== null && _s !== void 0 ? _s : 'fixed_price',
|
|
1880
|
+
originType: (_t = contract.originType) !== null && _t !== void 0 ? _t : 'manual',
|
|
1881
|
+
originId: (_u = contract.originId) !== null && _u !== void 0 ? _u : null,
|
|
1882
|
+
startDate: (_v = contract.startDate) !== null && _v !== void 0 ? _v : null,
|
|
1883
|
+
endDate: (_w = contract.endDate) !== null && _w !== void 0 ? _w : null,
|
|
1884
|
+
signedAt: (_x = contract.signedAt) !== null && _x !== void 0 ? _x : null,
|
|
1885
|
+
effectiveDate: (_y = contract.effectiveDate) !== null && _y !== void 0 ? _y : null,
|
|
1886
|
+
budgetAmount: (_z = contract.budgetAmount) !== null && _z !== void 0 ? _z : null,
|
|
1887
|
+
description: (_0 = contract.description) !== null && _0 !== void 0 ? _0 : null,
|
|
1888
|
+
financialTerms,
|
|
1889
|
+
parties: (_1 = contract.parties) !== null && _1 !== void 0 ? _1 : [],
|
|
1890
|
+
},
|
|
1891
|
+
person: personRecord
|
|
1892
|
+
? {
|
|
1893
|
+
id: (_2 = personRecord.id) !== null && _2 !== void 0 ? _2 : null,
|
|
1894
|
+
name: (_3 = personRecord.name) !== null && _3 !== void 0 ? _3 : null,
|
|
1895
|
+
tradeName: (_4 = personRecord.trade_name) !== null && _4 !== void 0 ? _4 : null,
|
|
1896
|
+
email: (_5 = personRecord.email) !== null && _5 !== void 0 ? _5 : null,
|
|
1897
|
+
phone: (_6 = personRecord.phone) !== null && _6 !== void 0 ? _6 : null,
|
|
1898
|
+
document: (_7 = personRecord.document) !== null && _7 !== void 0 ? _7 : null,
|
|
1899
|
+
}
|
|
1900
|
+
: null,
|
|
1901
|
+
receivable: {
|
|
1902
|
+
personId,
|
|
1903
|
+
documentNumber: (_8 = contract.code) !== null && _8 !== void 0 ? _8 : `CONTRACT-${contract.id}`,
|
|
1904
|
+
totalAmount,
|
|
1905
|
+
description: (_11 = (_10 = (_9 = contract.description) !== null && _9 !== void 0 ? _9 : contract.name) !== null && _10 !== void 0 ? _10 : contract.code) !== null && _11 !== void 0 ? _11 : null,
|
|
1906
|
+
},
|
|
1907
|
+
};
|
|
1908
|
+
}
|
|
832
1909
|
async updateContract(userId, contractId, data) {
|
|
1910
|
+
var _a, _b;
|
|
833
1911
|
const actor = await this.getActorContext(userId);
|
|
834
1912
|
this.ensureDirector(actor);
|
|
835
|
-
await this.getContractById(userId, contractId);
|
|
1913
|
+
const current = await this.getContractById(userId, contractId);
|
|
1914
|
+
const nextStatus = this.normalizeEnumValue(data.status, CONTRACT_STATUS_VALUES, (_a = current.status) !== null && _a !== void 0 ? _a : 'draft');
|
|
1915
|
+
const nextSignatureStatus = this.normalizeEnumValue(data.signatureStatus, SIGNATURE_STATUS_VALUES, (_b = current.signatureStatus) !== null && _b !== void 0 ? _b : 'not_started');
|
|
1916
|
+
const shouldPublishSigned = current.signatureStatus !== 'signed' && nextSignatureStatus === 'signed';
|
|
1917
|
+
const shouldPublishActivation = current.status !== 'active' && nextStatus === 'active';
|
|
836
1918
|
const updates = [];
|
|
837
1919
|
const params = [];
|
|
838
1920
|
this.pushUpdate(updates, params, 'code', data.code);
|
|
839
1921
|
this.pushUpdate(updates, params, 'name', data.name);
|
|
840
|
-
this.pushUpdate(updates, params, 'contract_category', data.contractCategory);
|
|
841
|
-
this.pushUpdate(updates, params, 'contract_type', data.contractType);
|
|
1922
|
+
this.pushUpdate(updates, params, 'contract_category', data.contractCategory, 'operations_contract_contract_category_70d553ea09_enum');
|
|
1923
|
+
this.pushUpdate(updates, params, 'contract_type', data.contractType, 'operations_contract_contract_type_48331e2ebf_enum');
|
|
842
1924
|
this.pushUpdate(updates, params, 'client_name', data.clientName);
|
|
843
|
-
this.pushUpdate(updates, params, 'signature_status', data.signatureStatus);
|
|
1925
|
+
this.pushUpdate(updates, params, 'signature_status', data.signatureStatus, 'operations_contract_signature_status_2cb7282a7b_enum');
|
|
844
1926
|
this.pushUpdate(updates, params, 'is_active', data.isActive);
|
|
845
|
-
this.pushUpdate(updates, params, 'billing_model', data.billingModel);
|
|
1927
|
+
this.pushUpdate(updates, params, 'billing_model', data.billingModel, 'operations_contract_billing_model_409dc7fea2_enum');
|
|
846
1928
|
this.pushUpdate(updates, params, 'account_manager_collaborator_id', data.accountManagerCollaboratorId);
|
|
847
1929
|
this.pushUpdate(updates, params, 'related_collaborator_id', data.relatedCollaboratorId);
|
|
848
|
-
this.pushUpdate(updates, params, '
|
|
1930
|
+
this.pushUpdate(updates, params, 'contract_template_id', data.contractTemplateId);
|
|
1931
|
+
this.pushUpdate(updates, params, 'origin_type', data.originType, 'operations_contract_origin_type_07a7cc2b5d_enum');
|
|
849
1932
|
this.pushUpdate(updates, params, 'origin_id', data.originId);
|
|
850
|
-
this.pushUpdate(updates, params, 'start_date', data.startDate);
|
|
851
|
-
this.pushUpdate(updates, params, 'end_date', data.endDate);
|
|
852
|
-
this.pushUpdate(updates, params, 'signed_at', data.signedAt);
|
|
853
|
-
this.pushUpdate(updates, params, 'effective_date', data.effectiveDate);
|
|
1933
|
+
this.pushUpdate(updates, params, 'start_date', data.startDate, 'date');
|
|
1934
|
+
this.pushUpdate(updates, params, 'end_date', data.endDate, 'date');
|
|
1935
|
+
this.pushUpdate(updates, params, 'signed_at', data.signedAt, 'date');
|
|
1936
|
+
this.pushUpdate(updates, params, 'effective_date', data.effectiveDate, 'date');
|
|
854
1937
|
this.pushUpdate(updates, params, 'budget_amount', data.budgetAmount);
|
|
855
1938
|
this.pushUpdate(updates, params, 'monthly_hour_cap', data.monthlyHourCap);
|
|
856
|
-
this.pushUpdate(updates, params, 'status', data.status);
|
|
1939
|
+
this.pushUpdate(updates, params, 'status', data.status, 'operations_contract_status_a0395962df_enum');
|
|
1940
|
+
this.pushUpdate(updates, params, 'creation_mode', data.creationMode, 'operations_contract_creation_mode_98ba669209_enum');
|
|
1941
|
+
this.pushUpdate(updates, params, 'wizard_step', data.wizardStep);
|
|
857
1942
|
this.pushUpdate(updates, params, 'description', data.description);
|
|
858
1943
|
this.pushUpdate(updates, params, 'content_html', data.contentHtml);
|
|
859
1944
|
await this.prisma.$transaction(async (tx) => {
|
|
1945
|
+
var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p, _q, _r, _s, _t, _u, _v, _w, _x, _y;
|
|
860
1946
|
if (updates.length) {
|
|
861
1947
|
params.push(contractId);
|
|
862
1948
|
await tx.$executeRawUnsafe(`UPDATE operations_contract
|
|
@@ -877,12 +1963,110 @@ let OperationsService = class OperationsService {
|
|
|
877
1963
|
await this.replaceContractRevisions(tx, contractId, data.revisions);
|
|
878
1964
|
}
|
|
879
1965
|
if (data.replaceUploadedPdfDocument) {
|
|
880
|
-
await this.
|
|
1966
|
+
await this.replaceContractDocument(tx, contractId, 'source_upload', data.replaceUploadedPdfDocument);
|
|
881
1967
|
}
|
|
882
1968
|
await this.insertContractHistory(tx, contractId, userId, 'updated', 'Contract registry data updated.');
|
|
1969
|
+
if (shouldPublishSigned || shouldPublishActivation) {
|
|
1970
|
+
const contractEventPayload = await this.buildContractActivatedEventPayload({
|
|
1971
|
+
id: current.id,
|
|
1972
|
+
code: (_a = data.code) !== null && _a !== void 0 ? _a : current.code,
|
|
1973
|
+
name: (_b = data.name) !== null && _b !== void 0 ? _b : current.name,
|
|
1974
|
+
clientName: (_c = data.clientName) !== null && _c !== void 0 ? _c : current.clientName,
|
|
1975
|
+
contractCategory: (_d = data.contractCategory) !== null && _d !== void 0 ? _d : current.contractCategory,
|
|
1976
|
+
contractType: (_e = data.contractType) !== null && _e !== void 0 ? _e : current.contractType,
|
|
1977
|
+
billingModel: (_f = data.billingModel) !== null && _f !== void 0 ? _f : current.billingModel,
|
|
1978
|
+
originType: (_g = data.originType) !== null && _g !== void 0 ? _g : current.originType,
|
|
1979
|
+
originId: (_h = data.originId) !== null && _h !== void 0 ? _h : current.originId,
|
|
1980
|
+
startDate: (_j = data.startDate) !== null && _j !== void 0 ? _j : current.startDate,
|
|
1981
|
+
endDate: (_k = data.endDate) !== null && _k !== void 0 ? _k : current.endDate,
|
|
1982
|
+
signedAt: (_l = data.signedAt) !== null && _l !== void 0 ? _l : current.signedAt,
|
|
1983
|
+
effectiveDate: (_m = data.effectiveDate) !== null && _m !== void 0 ? _m : current.effectiveDate,
|
|
1984
|
+
budgetAmount: (_o = data.budgetAmount) !== null && _o !== void 0 ? _o : current.budgetAmount,
|
|
1985
|
+
description: (_p = data.description) !== null && _p !== void 0 ? _p : current.description,
|
|
1986
|
+
financialTerms: (_q = data.financialTerms) !== null && _q !== void 0 ? _q : current.financialTerms,
|
|
1987
|
+
parties: (_r = data.parties) !== null && _r !== void 0 ? _r : current.parties,
|
|
1988
|
+
}, 'en', userId);
|
|
1989
|
+
if (shouldPublishSigned) {
|
|
1990
|
+
await this.integrationApi.publishEvent({
|
|
1991
|
+
eventName: 'operations.contract.signed',
|
|
1992
|
+
sourceModule: 'operations',
|
|
1993
|
+
aggregateType: 'contract',
|
|
1994
|
+
aggregateId: String(contractId),
|
|
1995
|
+
payload: Object.assign(Object.assign({}, contractEventPayload), { signedByUserId: userId !== null && userId !== void 0 ? userId : null, signedAt: (_u = (_t = (_s = data.signedAt) !== null && _s !== void 0 ? _s : current.signedAt) !== null && _t !== void 0 ? _t : contractEventPayload.signedAt) !== null && _u !== void 0 ? _u : new Date().toISOString() }),
|
|
1996
|
+
metadata: {
|
|
1997
|
+
producer: 'operations',
|
|
1998
|
+
correlationId: contractEventPayload.correlationId || `contract:${contractId}`,
|
|
1999
|
+
sourceModule: 'operations',
|
|
2000
|
+
sourceEntity: 'contract',
|
|
2001
|
+
sourceId: String(contractId),
|
|
2002
|
+
originType: (_v = data.originType) !== null && _v !== void 0 ? _v : current.originType,
|
|
2003
|
+
originId: (_w = data.originId) !== null && _w !== void 0 ? _w : current.originId,
|
|
2004
|
+
lifecycle: 'signed',
|
|
2005
|
+
},
|
|
2006
|
+
}, {
|
|
2007
|
+
persistenceClient: tx,
|
|
2008
|
+
});
|
|
2009
|
+
}
|
|
2010
|
+
if (shouldPublishActivation) {
|
|
2011
|
+
await this.integrationApi.publishEvent({
|
|
2012
|
+
eventName: 'operations.contract.activated',
|
|
2013
|
+
sourceModule: 'operations',
|
|
2014
|
+
aggregateType: 'contract',
|
|
2015
|
+
aggregateId: String(contractId),
|
|
2016
|
+
payload: contractEventPayload,
|
|
2017
|
+
metadata: {
|
|
2018
|
+
producer: 'operations',
|
|
2019
|
+
correlationId: contractEventPayload.correlationId || `contract:${contractId}`,
|
|
2020
|
+
sourceModule: 'operations',
|
|
2021
|
+
sourceEntity: 'contract',
|
|
2022
|
+
sourceId: String(contractId),
|
|
2023
|
+
originType: (_x = data.originType) !== null && _x !== void 0 ? _x : current.originType,
|
|
2024
|
+
originId: (_y = data.originId) !== null && _y !== void 0 ? _y : current.originId,
|
|
2025
|
+
lifecycle: 'activated',
|
|
2026
|
+
},
|
|
2027
|
+
}, {
|
|
2028
|
+
persistenceClient: tx,
|
|
2029
|
+
});
|
|
2030
|
+
}
|
|
2031
|
+
}
|
|
883
2032
|
});
|
|
884
2033
|
return this.getContractById(userId, contractId);
|
|
885
2034
|
}
|
|
2035
|
+
async removeContract(userId, contractId) {
|
|
2036
|
+
const actor = await this.getActorContext(userId);
|
|
2037
|
+
this.ensureDirector(actor);
|
|
2038
|
+
const current = await this.getContractById(userId, contractId);
|
|
2039
|
+
await this.prisma.$transaction(async (tx) => {
|
|
2040
|
+
var _a, _b;
|
|
2041
|
+
await tx.$executeRawUnsafe(`UPDATE operations_project
|
|
2042
|
+
SET contract_id = NULL,
|
|
2043
|
+
updated_at = NOW()
|
|
2044
|
+
WHERE contract_id = $1
|
|
2045
|
+
AND deleted_at IS NULL`, contractId);
|
|
2046
|
+
for (const tableName of [
|
|
2047
|
+
'operations_contract_party',
|
|
2048
|
+
'operations_contract_signature',
|
|
2049
|
+
'operations_contract_financial_term',
|
|
2050
|
+
'operations_contract_document',
|
|
2051
|
+
'operations_contract_revision',
|
|
2052
|
+
]) {
|
|
2053
|
+
await tx.$executeRawUnsafe(`UPDATE ${tableName}
|
|
2054
|
+
SET deleted_at = NOW(),
|
|
2055
|
+
updated_at = NOW()
|
|
2056
|
+
WHERE contract_id = $1
|
|
2057
|
+
AND deleted_at IS NULL`, contractId);
|
|
2058
|
+
}
|
|
2059
|
+
await this.insertContractHistory(tx, contractId, userId, 'deleted', `Contract "${(_b = (_a = current.name) !== null && _a !== void 0 ? _a : current.code) !== null && _b !== void 0 ? _b : `#${contractId}`}" removed from registry.`);
|
|
2060
|
+
await tx.$executeRawUnsafe(`UPDATE operations_contract
|
|
2061
|
+
SET deleted_at = NOW(),
|
|
2062
|
+
is_active = false,
|
|
2063
|
+
status = 'archived',
|
|
2064
|
+
updated_at = NOW()
|
|
2065
|
+
WHERE id = $1
|
|
2066
|
+
AND deleted_at IS NULL`, contractId);
|
|
2067
|
+
});
|
|
2068
|
+
return { success: true };
|
|
2069
|
+
}
|
|
886
2070
|
async listTimesheets(userId) {
|
|
887
2071
|
const actor = await this.getActorContext(userId);
|
|
888
2072
|
const filter = this.buildIdFilter(actor.visibleCollaboratorIds, 't.collaborator_id', actor.isDirector);
|
|
@@ -958,7 +2142,7 @@ let OperationsService = class OperationsService {
|
|
|
958
2142
|
status,
|
|
959
2143
|
created_at,
|
|
960
2144
|
updated_at
|
|
961
|
-
) VALUES ($1, $2, $3, $4, $5, 'draft', NOW(), NOW())
|
|
2145
|
+
) VALUES ($1, $2, $3::date, $4::date, $5, 'draft', NOW(), NOW())
|
|
962
2146
|
RETURNING id`, collaboratorId, (_a = collaborator.supervisorId) !== null && _a !== void 0 ? _a : null, data.weekStartDate, data.weekEndDate, (_b = data.notes) !== null && _b !== void 0 ? _b : null));
|
|
963
2147
|
const timesheetId = (_c = row[0]) === null || _c === void 0 ? void 0 : _c.id;
|
|
964
2148
|
await this.replaceTimesheetEntries(tx, timesheetId, (_d = data.entries) !== null && _d !== void 0 ? _d : [], collaboratorId);
|
|
@@ -979,8 +2163,8 @@ let OperationsService = class OperationsService {
|
|
|
979
2163
|
await this.prisma.$transaction(async (tx) => {
|
|
980
2164
|
const updates = [];
|
|
981
2165
|
const params = [];
|
|
982
|
-
this.pushUpdate(updates, params, 'week_start_date', data.weekStartDate);
|
|
983
|
-
this.pushUpdate(updates, params, 'week_end_date', data.weekEndDate);
|
|
2166
|
+
this.pushUpdate(updates, params, 'week_start_date', data.weekStartDate, 'date');
|
|
2167
|
+
this.pushUpdate(updates, params, 'week_end_date', data.weekEndDate, 'date');
|
|
984
2168
|
this.pushUpdate(updates, params, 'notes', data.notes);
|
|
985
2169
|
if (updates.length) {
|
|
986
2170
|
params.push(timesheetId);
|
|
@@ -1079,7 +2263,17 @@ let OperationsService = class OperationsService {
|
|
|
1079
2263
|
submitted_at,
|
|
1080
2264
|
created_at,
|
|
1081
2265
|
updated_at
|
|
1082
|
-
) VALUES (
|
|
2266
|
+
) VALUES (
|
|
2267
|
+
$1,
|
|
2268
|
+
$2,
|
|
2269
|
+
$3::operations_time_off_request_request_type_fdcf258f8e_enum,
|
|
2270
|
+
$4::date,
|
|
2271
|
+
$5::date,
|
|
2272
|
+
$6,
|
|
2273
|
+
'submitted',
|
|
2274
|
+
$7,
|
|
2275
|
+
NOW(), NOW(), NOW()
|
|
2276
|
+
)
|
|
1083
2277
|
RETURNING id`, collaboratorId, (_a = collaborator.supervisorId) !== null && _a !== void 0 ? _a : null, (_b = data.requestType) !== null && _b !== void 0 ? _b : 'vacation', data.startDate, data.endDate, (_c = data.totalDays) !== null && _c !== void 0 ? _c : null, (_d = data.reason) !== null && _d !== void 0 ? _d : null));
|
|
1084
2278
|
const requestId = (_e = row[0]) === null || _e === void 0 ? void 0 : _e.id;
|
|
1085
2279
|
await this.upsertApproval(tx, {
|
|
@@ -1188,7 +2382,14 @@ let OperationsService = class OperationsService {
|
|
|
1188
2382
|
created_at,
|
|
1189
2383
|
updated_at
|
|
1190
2384
|
) VALUES (
|
|
1191
|
-
$1,
|
|
2385
|
+
$1,
|
|
2386
|
+
$2,
|
|
2387
|
+
$3::operations_schedule_adjustment_request_request__d3f1202fa6_enum,
|
|
2388
|
+
$4::date,
|
|
2389
|
+
$5::date,
|
|
2390
|
+
'submitted',
|
|
2391
|
+
$6,
|
|
2392
|
+
NOW(), NOW(), NOW()
|
|
1192
2393
|
)
|
|
1193
2394
|
RETURNING id`, collaboratorId, (_a = collaborator.supervisorId) !== null && _a !== void 0 ? _a : null, (_b = data.requestScope) !== null && _b !== void 0 ? _b : 'temporary', data.effectiveStartDate, (_c = data.effectiveEndDate) !== null && _c !== void 0 ? _c : null, (_d = data.reason) !== null && _d !== void 0 ? _d : null));
|
|
1194
2395
|
const requestId = (_e = row[0]) === null || _e === void 0 ? void 0 : _e.id;
|
|
@@ -1202,7 +2403,11 @@ let OperationsService = class OperationsService {
|
|
|
1202
2403
|
break_minutes,
|
|
1203
2404
|
created_at,
|
|
1204
2405
|
updated_at
|
|
1205
|
-
) VALUES (
|
|
2406
|
+
) VALUES (
|
|
2407
|
+
$1,
|
|
2408
|
+
$2::operations_schedule_adjustment_day_weekday_ba6542bf86_enum,
|
|
2409
|
+
$3, $4::time, $5::time, $6, NOW(), NOW()
|
|
2410
|
+
)`, requestId, day.weekday, (_f = day.isWorkingDay) !== null && _f !== void 0 ? _f : true, (_g = day.startTime) !== null && _g !== void 0 ? _g : null, (_h = day.endTime) !== null && _h !== void 0 ? _h : null, (_j = day.breakMinutes) !== null && _j !== void 0 ? _j : null);
|
|
1206
2411
|
}
|
|
1207
2412
|
await this.upsertApproval(tx, {
|
|
1208
2413
|
targetType: 'schedule_adjustment_request',
|
|
@@ -1371,14 +2576,14 @@ let OperationsService = class OperationsService {
|
|
|
1371
2576
|
await this.prisma.$transaction(async (tx) => {
|
|
1372
2577
|
var _a, _b;
|
|
1373
2578
|
await tx.$executeRawUnsafe(`UPDATE operations_approval
|
|
1374
|
-
SET status = $1,
|
|
2579
|
+
SET status = $1::operations_approval_status_747aa313d9_enum,
|
|
1375
2580
|
decided_at = NOW(),
|
|
1376
2581
|
decision_note = $2,
|
|
1377
2582
|
updated_at = NOW()
|
|
1378
2583
|
WHERE id = $3`, nextStatus, (_a = data.note) !== null && _a !== void 0 ? _a : null, approvalId);
|
|
1379
2584
|
if (approval.targetType === 'timesheet') {
|
|
1380
2585
|
await tx.$executeRawUnsafe(`UPDATE operations_timesheet
|
|
1381
|
-
SET status = $1,
|
|
2586
|
+
SET status = $1::operations_timesheet_status_128a8ecd30_enum,
|
|
1382
2587
|
reviewed_at = NOW(),
|
|
1383
2588
|
approver_collaborator_id = $2,
|
|
1384
2589
|
updated_at = NOW()
|
|
@@ -1386,7 +2591,7 @@ let OperationsService = class OperationsService {
|
|
|
1386
2591
|
}
|
|
1387
2592
|
if (approval.targetType === 'time_off_request') {
|
|
1388
2593
|
await tx.$executeRawUnsafe(`UPDATE operations_time_off_request
|
|
1389
|
-
SET status = $1,
|
|
2594
|
+
SET status = $1::operations_time_off_request_status_c0b28799ee_enum,
|
|
1390
2595
|
reviewed_at = NOW(),
|
|
1391
2596
|
approver_collaborator_id = $2,
|
|
1392
2597
|
submitted_at = COALESCE(submitted_at, NOW()),
|
|
@@ -1395,7 +2600,7 @@ let OperationsService = class OperationsService {
|
|
|
1395
2600
|
}
|
|
1396
2601
|
if (approval.targetType === 'schedule_adjustment_request') {
|
|
1397
2602
|
await tx.$executeRawUnsafe(`UPDATE operations_schedule_adjustment_request
|
|
1398
|
-
SET status = $1,
|
|
2603
|
+
SET status = $1::operations_schedule_adjustment_request_status_159e10f34e_enum,
|
|
1399
2604
|
reviewed_at = NOW(),
|
|
1400
2605
|
approver_collaborator_id = $2,
|
|
1401
2606
|
submitted_at = COALESCE(submitted_at, NOW()),
|
|
@@ -1460,10 +2665,12 @@ let OperationsService = class OperationsService {
|
|
|
1460
2665
|
}
|
|
1461
2666
|
async getCollaboratorByUserId(userId) {
|
|
1462
2667
|
return this.querySingle(`SELECT c.id,
|
|
1463
|
-
c.display_name AS "displayName",
|
|
2668
|
+
COALESCE(NULLIF(c.display_name, ''), person_record.name) AS "displayName",
|
|
1464
2669
|
s.id AS "supervisorId",
|
|
1465
2670
|
s.display_name AS "supervisorName"
|
|
1466
2671
|
FROM operations_collaborator c
|
|
2672
|
+
LEFT JOIN person person_record
|
|
2673
|
+
ON person_record.id = c.person_id
|
|
1467
2674
|
LEFT JOIN operations_collaborator s
|
|
1468
2675
|
ON s.id = c.supervisor_collaborator_id
|
|
1469
2676
|
WHERE c.user_id = $1
|
|
@@ -1471,10 +2678,12 @@ let OperationsService = class OperationsService {
|
|
|
1471
2678
|
}
|
|
1472
2679
|
async getCollaboratorById(collaboratorId) {
|
|
1473
2680
|
const collaborator = await this.querySingle(`SELECT c.id,
|
|
1474
|
-
c.display_name AS "displayName",
|
|
2681
|
+
COALESCE(NULLIF(c.display_name, ''), person_record.name) AS "displayName",
|
|
1475
2682
|
s.id AS "supervisorId",
|
|
1476
2683
|
s.display_name AS "supervisorName"
|
|
1477
2684
|
FROM operations_collaborator c
|
|
2685
|
+
LEFT JOIN person person_record
|
|
2686
|
+
ON person_record.id = c.person_id
|
|
1478
2687
|
LEFT JOIN operations_collaborator s
|
|
1479
2688
|
ON s.id = c.supervisor_collaborator_id
|
|
1480
2689
|
WHERE c.id = $1
|
|
@@ -1484,6 +2693,186 @@ let OperationsService = class OperationsService {
|
|
|
1484
2693
|
}
|
|
1485
2694
|
return collaborator;
|
|
1486
2695
|
}
|
|
2696
|
+
async getPersonById(personId) {
|
|
2697
|
+
const person = await this.querySingle(`SELECT id, name, type
|
|
2698
|
+
FROM person
|
|
2699
|
+
WHERE id = $1`, [personId]);
|
|
2700
|
+
if (!person) {
|
|
2701
|
+
throw new common_1.NotFoundException('Person not found.');
|
|
2702
|
+
}
|
|
2703
|
+
return person;
|
|
2704
|
+
}
|
|
2705
|
+
async getDepartmentById(client, departmentId, includeInactive = false) {
|
|
2706
|
+
var _a;
|
|
2707
|
+
const departments = (await client.$queryRawUnsafe(`SELECT id,
|
|
2708
|
+
slug,
|
|
2709
|
+
code,
|
|
2710
|
+
name,
|
|
2711
|
+
description,
|
|
2712
|
+
created_at AS "createdAt",
|
|
2713
|
+
updated_at AS "updatedAt",
|
|
2714
|
+
deleted_at AS "deletedAt",
|
|
2715
|
+
CASE WHEN deleted_at IS NULL THEN 'active' ELSE 'inactive' END AS status
|
|
2716
|
+
FROM operations_department
|
|
2717
|
+
WHERE id = $1
|
|
2718
|
+
AND ($2::boolean OR deleted_at IS NULL)`, departmentId, includeInactive));
|
|
2719
|
+
const department = (_a = departments[0]) !== null && _a !== void 0 ? _a : null;
|
|
2720
|
+
if (!department) {
|
|
2721
|
+
throw new common_1.NotFoundException('Department not found.');
|
|
2722
|
+
}
|
|
2723
|
+
return department;
|
|
2724
|
+
}
|
|
2725
|
+
async assertDepartmentNameAvailable(client, name, excludeDepartmentId) {
|
|
2726
|
+
const existing = (await client.$queryRawUnsafe(`SELECT id
|
|
2727
|
+
FROM operations_department
|
|
2728
|
+
WHERE LOWER(name) = LOWER($1)
|
|
2729
|
+
AND ($2::int IS NULL OR id <> $2)
|
|
2730
|
+
LIMIT 1`, name, excludeDepartmentId !== null && excludeDepartmentId !== void 0 ? excludeDepartmentId : null));
|
|
2731
|
+
if (existing[0]) {
|
|
2732
|
+
throw new common_1.BadRequestException('A department with this name already exists.');
|
|
2733
|
+
}
|
|
2734
|
+
}
|
|
2735
|
+
async assertDepartmentCodeAvailable(client, code, excludeDepartmentId) {
|
|
2736
|
+
const existing = (await client.$queryRawUnsafe(`SELECT id
|
|
2737
|
+
FROM operations_department
|
|
2738
|
+
WHERE UPPER(COALESCE(code, '')) = UPPER($1)
|
|
2739
|
+
AND ($2::int IS NULL OR id <> $2)
|
|
2740
|
+
LIMIT 1`, code, excludeDepartmentId !== null && excludeDepartmentId !== void 0 ? excludeDepartmentId : null));
|
|
2741
|
+
if (existing[0]) {
|
|
2742
|
+
throw new common_1.BadRequestException('A department with this code already exists.');
|
|
2743
|
+
}
|
|
2744
|
+
}
|
|
2745
|
+
async resolveDepartmentReference(client, input) {
|
|
2746
|
+
var _a;
|
|
2747
|
+
if (input.departmentName !== undefined) {
|
|
2748
|
+
const normalizedDepartmentName = this.normalizeOptionalText(input.departmentName);
|
|
2749
|
+
if (!normalizedDepartmentName) {
|
|
2750
|
+
return null;
|
|
2751
|
+
}
|
|
2752
|
+
const existingByName = (await client.$queryRawUnsafe(`SELECT id, name
|
|
2753
|
+
FROM operations_department
|
|
2754
|
+
WHERE deleted_at IS NULL
|
|
2755
|
+
AND LOWER(name) = LOWER($1)
|
|
2756
|
+
ORDER BY id ASC
|
|
2757
|
+
LIMIT 1`, normalizedDepartmentName));
|
|
2758
|
+
if (existingByName[0]) {
|
|
2759
|
+
return existingByName[0];
|
|
2760
|
+
}
|
|
2761
|
+
const slug = await this.generateUniqueDepartmentSlug(client, normalizedDepartmentName);
|
|
2762
|
+
const created = (await client.$queryRawUnsafe(`INSERT INTO operations_department (
|
|
2763
|
+
slug,
|
|
2764
|
+
code,
|
|
2765
|
+
name,
|
|
2766
|
+
description,
|
|
2767
|
+
created_at,
|
|
2768
|
+
updated_at
|
|
2769
|
+
) VALUES ($1, NULL, $2, NULL, NOW(), NOW())
|
|
2770
|
+
RETURNING id, name`, slug, normalizedDepartmentName));
|
|
2771
|
+
return (_a = created[0]) !== null && _a !== void 0 ? _a : null;
|
|
2772
|
+
}
|
|
2773
|
+
if (typeof input.departmentId === 'number' &&
|
|
2774
|
+
Number.isInteger(input.departmentId) &&
|
|
2775
|
+
input.departmentId > 0) {
|
|
2776
|
+
return this.getDepartmentById(client, input.departmentId);
|
|
2777
|
+
}
|
|
2778
|
+
return null;
|
|
2779
|
+
}
|
|
2780
|
+
async generateUniqueDepartmentSlug(client, label, excludeDepartmentId) {
|
|
2781
|
+
const baseSlug = this.slugifyValue(label) || `department-${Date.now().toString(36)}`;
|
|
2782
|
+
for (let attempt = 0; attempt < 25; attempt += 1) {
|
|
2783
|
+
const candidate = attempt === 0 ? baseSlug : `${baseSlug}-${attempt + 1}`;
|
|
2784
|
+
const existing = (await client.$queryRawUnsafe(`SELECT id
|
|
2785
|
+
FROM operations_department
|
|
2786
|
+
WHERE slug = $1
|
|
2787
|
+
AND ($2::int IS NULL OR id <> $2)
|
|
2788
|
+
LIMIT 1`, candidate, excludeDepartmentId !== null && excludeDepartmentId !== void 0 ? excludeDepartmentId : null));
|
|
2789
|
+
if (!existing.length) {
|
|
2790
|
+
return candidate;
|
|
2791
|
+
}
|
|
2792
|
+
}
|
|
2793
|
+
return `${baseSlug}-${Date.now().toString(36)}`;
|
|
2794
|
+
}
|
|
2795
|
+
async getContractTemplateRecord(client, templateId, includeInactive = false) {
|
|
2796
|
+
const template = (await client.$queryRawUnsafe(`SELECT t.id,
|
|
2797
|
+
t.slug,
|
|
2798
|
+
t.code,
|
|
2799
|
+
t.name,
|
|
2800
|
+
t.description,
|
|
2801
|
+
t.contract_category AS "contractCategory",
|
|
2802
|
+
t.contract_type AS "contractType",
|
|
2803
|
+
t.billing_model AS "billingModel",
|
|
2804
|
+
t.signature_status AS "signatureStatus",
|
|
2805
|
+
t.is_active AS "isActive",
|
|
2806
|
+
t.status,
|
|
2807
|
+
t.content_html AS "contentHtml",
|
|
2808
|
+
COUNT(DISTINCT c.id)::int AS "usageCount",
|
|
2809
|
+
t.created_at AS "createdAt",
|
|
2810
|
+
t.updated_at AS "updatedAt"
|
|
2811
|
+
FROM operations_contract_template t
|
|
2812
|
+
LEFT JOIN operations_contract c
|
|
2813
|
+
ON c.contract_template_id = t.id
|
|
2814
|
+
AND c.deleted_at IS NULL
|
|
2815
|
+
WHERE t.id = $1
|
|
2816
|
+
AND ($2::boolean = true OR t.deleted_at IS NULL)
|
|
2817
|
+
GROUP BY t.id
|
|
2818
|
+
LIMIT 1`, templateId, includeInactive));
|
|
2819
|
+
const record = template[0];
|
|
2820
|
+
if (!record) {
|
|
2821
|
+
throw new common_1.NotFoundException('Contract template not found.');
|
|
2822
|
+
}
|
|
2823
|
+
return record;
|
|
2824
|
+
}
|
|
2825
|
+
async assertContractTemplateNameAvailable(client, name, excludeTemplateId) {
|
|
2826
|
+
const existing = (await client.$queryRawUnsafe(`SELECT id
|
|
2827
|
+
FROM operations_contract_template
|
|
2828
|
+
WHERE LOWER(name) = LOWER($1)
|
|
2829
|
+
AND deleted_at IS NULL
|
|
2830
|
+
AND ($2::int IS NULL OR id <> $2)
|
|
2831
|
+
LIMIT 1`, name, excludeTemplateId !== null && excludeTemplateId !== void 0 ? excludeTemplateId : null));
|
|
2832
|
+
if (existing[0]) {
|
|
2833
|
+
throw new common_1.BadRequestException('A contract template with this name already exists.');
|
|
2834
|
+
}
|
|
2835
|
+
}
|
|
2836
|
+
async assertContractTemplateCodeAvailable(client, code, excludeTemplateId) {
|
|
2837
|
+
const existing = (await client.$queryRawUnsafe(`SELECT id
|
|
2838
|
+
FROM operations_contract_template
|
|
2839
|
+
WHERE UPPER(COALESCE(code, '')) = UPPER($1)
|
|
2840
|
+
AND deleted_at IS NULL
|
|
2841
|
+
AND ($2::int IS NULL OR id <> $2)
|
|
2842
|
+
LIMIT 1`, code, excludeTemplateId !== null && excludeTemplateId !== void 0 ? excludeTemplateId : null));
|
|
2843
|
+
if (existing[0]) {
|
|
2844
|
+
throw new common_1.BadRequestException('A contract template with this code already exists.');
|
|
2845
|
+
}
|
|
2846
|
+
}
|
|
2847
|
+
async generateUniqueContractTemplateSlug(client, label, excludeTemplateId) {
|
|
2848
|
+
const baseSlug = this.slugifyValue(label) || `contract-template-${Date.now().toString(36)}`;
|
|
2849
|
+
for (let attempt = 0; attempt < 25; attempt += 1) {
|
|
2850
|
+
const candidate = attempt === 0 ? baseSlug : `${baseSlug}-${attempt + 1}`;
|
|
2851
|
+
const existing = (await client.$queryRawUnsafe(`SELECT id
|
|
2852
|
+
FROM operations_contract_template
|
|
2853
|
+
WHERE slug = $1
|
|
2854
|
+
AND ($2::int IS NULL OR id <> $2)
|
|
2855
|
+
LIMIT 1`, candidate, excludeTemplateId !== null && excludeTemplateId !== void 0 ? excludeTemplateId : null));
|
|
2856
|
+
if (!existing.length) {
|
|
2857
|
+
return candidate;
|
|
2858
|
+
}
|
|
2859
|
+
}
|
|
2860
|
+
return `${baseSlug}-${Date.now().toString(36)}`;
|
|
2861
|
+
}
|
|
2862
|
+
slugifyValue(value) {
|
|
2863
|
+
return (value !== null && value !== void 0 ? value : '')
|
|
2864
|
+
.normalize('NFKD')
|
|
2865
|
+
.replace(/[\u0300-\u036f]/g, '')
|
|
2866
|
+
.toLowerCase()
|
|
2867
|
+
.trim()
|
|
2868
|
+
.replace(/[^a-z0-9]+/g, '-')
|
|
2869
|
+
.replace(/^-+|-+$/g, '')
|
|
2870
|
+
.slice(0, 80);
|
|
2871
|
+
}
|
|
2872
|
+
normalizeOptionalText(value) {
|
|
2873
|
+
const normalized = String(value !== null && value !== void 0 ? value : '').trim();
|
|
2874
|
+
return normalized || null;
|
|
2875
|
+
}
|
|
1487
2876
|
async getProjectDetails(projectId, actorCollaboratorId) {
|
|
1488
2877
|
var _a, _b, _c, _d, _e, _f, _g;
|
|
1489
2878
|
const project = await this.querySingle(`SELECT p.id,
|
|
@@ -1589,10 +2978,14 @@ let OperationsService = class OperationsService {
|
|
|
1589
2978
|
var _a, _b, _c, _d, _e, _f;
|
|
1590
2979
|
const collaborator = await this.querySingle(`SELECT c.id,
|
|
1591
2980
|
c.user_id AS "userId",
|
|
2981
|
+
c.person_id AS "personId",
|
|
2982
|
+
person_record.name AS "personName",
|
|
2983
|
+
person_record.avatar_id AS "personAvatarId",
|
|
1592
2984
|
c.code,
|
|
1593
2985
|
c.collaborator_type AS "collaboratorType",
|
|
1594
|
-
c.display_name AS "displayName",
|
|
1595
|
-
c.
|
|
2986
|
+
COALESCE(NULLIF(c.display_name, ''), person_record.name) AS "displayName",
|
|
2987
|
+
c.department_id AS "departmentId",
|
|
2988
|
+
COALESCE(NULLIF(department_record.name, ''), NULLIF(c.department, '')) AS "department",
|
|
1596
2989
|
c.title,
|
|
1597
2990
|
c.level_label AS "levelLabel",
|
|
1598
2991
|
c.weekly_capacity_hours AS "weeklyCapacityHours",
|
|
@@ -1606,6 +2999,11 @@ let OperationsService = class OperationsService {
|
|
|
1606
2999
|
hiring_contract.status AS "contractStatus",
|
|
1607
3000
|
COUNT(DISTINCT pa.id)::int AS "activeAssignments"
|
|
1608
3001
|
FROM operations_collaborator c
|
|
3002
|
+
LEFT JOIN person person_record
|
|
3003
|
+
ON person_record.id = c.person_id
|
|
3004
|
+
LEFT JOIN operations_department department_record
|
|
3005
|
+
ON department_record.id = c.department_id
|
|
3006
|
+
AND department_record.deleted_at IS NULL
|
|
1609
3007
|
LEFT JOIN operations_collaborator s
|
|
1610
3008
|
ON s.id = c.supervisor_collaborator_id
|
|
1611
3009
|
LEFT JOIN operations_project_assignment pa
|
|
@@ -1623,7 +3021,7 @@ let OperationsService = class OperationsService {
|
|
|
1623
3021
|
) hiring_contract ON TRUE
|
|
1624
3022
|
WHERE c.id = $1
|
|
1625
3023
|
AND c.deleted_at IS NULL
|
|
1626
|
-
GROUP BY c.id, s.id, hiring_contract.id, hiring_contract.status`, [collaboratorId]);
|
|
3024
|
+
GROUP BY c.id, person_record.id, department_record.id, s.id, hiring_contract.id, hiring_contract.status`, [collaboratorId]);
|
|
1627
3025
|
if (!collaborator) {
|
|
1628
3026
|
throw new common_1.NotFoundException('Collaborator not found.');
|
|
1629
3027
|
}
|
|
@@ -1770,7 +3168,7 @@ let OperationsService = class OperationsService {
|
|
|
1770
3168
|
description,
|
|
1771
3169
|
created_at,
|
|
1772
3170
|
updated_at
|
|
1773
|
-
) VALUES ($1, $2, $3, $4, $5, $6, NOW(), NOW())`, timesheetId, (_a = entry.projectAssignmentId) !== null && _a !== void 0 ? _a : null, (_b = entry.activityLabel) !== null && _b !== void 0 ? _b : null, entry.workDate, entry.hours, (_c = entry.description) !== null && _c !== void 0 ? _c : null);
|
|
3171
|
+
) VALUES ($1, $2, $3, $4::date, $5, $6, NOW(), NOW())`, timesheetId, (_a = entry.projectAssignmentId) !== null && _a !== void 0 ? _a : null, (_b = entry.activityLabel) !== null && _b !== void 0 ? _b : null, entry.workDate, entry.hours, (_c = entry.description) !== null && _c !== void 0 ? _c : null);
|
|
1774
3172
|
}
|
|
1775
3173
|
}
|
|
1776
3174
|
async refreshTimesheetTotal(client, timesheetId) {
|
|
@@ -1788,7 +3186,7 @@ let OperationsService = class OperationsService {
|
|
|
1788
3186
|
var _a, _b;
|
|
1789
3187
|
const existing = (await client.$queryRawUnsafe(`SELECT id
|
|
1790
3188
|
FROM operations_approval
|
|
1791
|
-
WHERE target_type = $1
|
|
3189
|
+
WHERE target_type = $1::operations_approval_target_type_32d3f04385_enum
|
|
1792
3190
|
AND target_id = $2
|
|
1793
3191
|
AND deleted_at IS NULL`, input.targetType, input.targetId));
|
|
1794
3192
|
let approvalId = (_a = existing[0]) === null || _a === void 0 ? void 0 : _a.id;
|
|
@@ -1802,7 +3200,10 @@ let OperationsService = class OperationsService {
|
|
|
1802
3200
|
submitted_at,
|
|
1803
3201
|
created_at,
|
|
1804
3202
|
updated_at
|
|
1805
|
-
) VALUES (
|
|
3203
|
+
) VALUES (
|
|
3204
|
+
$1::operations_approval_target_type_32d3f04385_enum,
|
|
3205
|
+
$2, $3, $4, 'pending', NOW(), NOW(), NOW()
|
|
3206
|
+
)
|
|
1806
3207
|
RETURNING id`, input.targetType, input.targetId, input.requesterCollaboratorId, input.approverCollaboratorId));
|
|
1807
3208
|
approvalId = (_b = created[0]) === null || _b === void 0 ? void 0 : _b.id;
|
|
1808
3209
|
await this.insertApprovalHistory(client, approvalId, input.requesterCollaboratorId, 'created', null);
|
|
@@ -1828,7 +3229,13 @@ let OperationsService = class OperationsService {
|
|
|
1828
3229
|
action,
|
|
1829
3230
|
note,
|
|
1830
3231
|
created_at
|
|
1831
|
-
) VALUES (
|
|
3232
|
+
) VALUES (
|
|
3233
|
+
$1,
|
|
3234
|
+
$2,
|
|
3235
|
+
$3::operations_approval_history_action_c8478a05d6_enum,
|
|
3236
|
+
$4,
|
|
3237
|
+
NOW()
|
|
3238
|
+
)`, approvalId, actorCollaboratorId, action, note);
|
|
1832
3239
|
}
|
|
1833
3240
|
async assertProjectAccess(actor, projectId) {
|
|
1834
3241
|
if (actor.isDirector)
|
|
@@ -1941,7 +3348,7 @@ let OperationsService = class OperationsService {
|
|
|
1941
3348
|
break_minutes,
|
|
1942
3349
|
created_at,
|
|
1943
3350
|
updated_at
|
|
1944
|
-
) VALUES ($1, $2, $3, $4, $5, $6, NOW(), NOW())`, collaboratorId, day.weekday, (_a = day.isWorkingDay) !== null && _a !== void 0 ? _a : true, day.isWorkingDay === false ? null : (_b = day.startTime) !== null && _b !== void 0 ? _b : null, day.isWorkingDay === false ? null : (_c = day.endTime) !== null && _c !== void 0 ? _c : null, (_d = day.breakMinutes) !== null && _d !== void 0 ? _d : null);
|
|
3351
|
+
) VALUES ($1, $2::operations_collaborator_schedule_day_weekday_5d79528f6c_enum, $3, $4::time, $5::time, $6, NOW(), NOW())`, collaboratorId, day.weekday, (_a = day.isWorkingDay) !== null && _a !== void 0 ? _a : true, day.isWorkingDay === false ? null : (_b = day.startTime) !== null && _b !== void 0 ? _b : null, day.isWorkingDay === false ? null : (_c = day.endTime) !== null && _c !== void 0 ? _c : null, (_d = day.breakMinutes) !== null && _d !== void 0 ? _d : null);
|
|
1945
3352
|
}
|
|
1946
3353
|
}
|
|
1947
3354
|
async replaceProjectAssignments(client, projectId, teamAssignments) {
|
|
@@ -1965,7 +3372,9 @@ let OperationsService = class OperationsService {
|
|
|
1965
3372
|
created_at,
|
|
1966
3373
|
updated_at
|
|
1967
3374
|
) VALUES (
|
|
1968
|
-
$1, $2, $3, $4, $5, $6, $7, $8,
|
|
3375
|
+
$1, $2, $3, $4, $5, $6, $7::date, $8::date,
|
|
3376
|
+
$9::operations_project_assignment_status_155b459bbf_enum,
|
|
3377
|
+
NOW(), NOW()
|
|
1969
3378
|
)`, projectId, assignment.collaboratorId, (_a = assignment.roleLabel) !== null && _a !== void 0 ? _a : 'Team Member', (_b = assignment.allocationPercent) !== null && _b !== void 0 ? _b : null, (_c = assignment.weeklyHours) !== null && _c !== void 0 ? _c : null, (_d = assignment.isBillable) !== null && _d !== void 0 ? _d : true, (_e = assignment.startDate) !== null && _e !== void 0 ? _e : null, (_f = assignment.endDate) !== null && _f !== void 0 ? _f : null, (_g = assignment.status) !== null && _g !== void 0 ? _g : 'active');
|
|
1970
3379
|
}
|
|
1971
3380
|
}
|
|
@@ -2032,14 +3441,59 @@ let OperationsService = class OperationsService {
|
|
|
2032
3441
|
created_at,
|
|
2033
3442
|
updated_at
|
|
2034
3443
|
) VALUES (
|
|
2035
|
-
$1, $2,
|
|
2036
|
-
|
|
3444
|
+
$1, $2,
|
|
3445
|
+
$3::operations_contract_contract_category_70d553ea09_enum,
|
|
3446
|
+
$4::operations_contract_contract_type_48331e2ebf_enum,
|
|
3447
|
+
$5, 'not_started', true,
|
|
3448
|
+
$6::operations_contract_billing_model_409dc7fea2_enum,
|
|
3449
|
+
$7, $8, 'employee_hiring', $9, $10::date, NULL,
|
|
3450
|
+
NULL, $10::date, $11, $12, 'draft', $13, NULL, $14, $14, NOW(), NOW()
|
|
2037
3451
|
)`, contractCode, contractName, this.mapContractCategoryForCollaboratorType(input.collaboratorType), this.mapContractTypeForCollaboratorType(input.collaboratorType), input.displayName, this.mapBillingModelForCollaboratorType(input.collaboratorType), input.supervisorCollaboratorId, input.collaboratorId, input.collaboratorId, (_a = input.startDate) !== null && _a !== void 0 ? _a : new Date().toISOString().slice(0, 10), (_b = input.compensationAmount) !== null && _b !== void 0 ? _b : null, input.weeklyCapacityHours
|
|
2038
3452
|
? Math.round(Number(input.weeklyCapacityHours) * 4)
|
|
2039
3453
|
: null, (_c = input.description) !== null && _c !== void 0 ? _c : null, createdByUserId);
|
|
2040
3454
|
}
|
|
2041
3455
|
async createProjectContractDraft(client, createdByUserId, input) {
|
|
2042
|
-
var _a, _b, _c, _d, _e, _f, _g, _h;
|
|
3456
|
+
var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p, _q, _r, _s, _t;
|
|
3457
|
+
const templateRows = input.contractTemplateId
|
|
3458
|
+
? (await client.$queryRawUnsafe(`SELECT id,
|
|
3459
|
+
code,
|
|
3460
|
+
name,
|
|
3461
|
+
description,
|
|
3462
|
+
contract_category AS "contractCategory",
|
|
3463
|
+
contract_type AS "contractType",
|
|
3464
|
+
billing_model AS "billingModel",
|
|
3465
|
+
signature_status AS "signatureStatus",
|
|
3466
|
+
content_html AS "contentHtml"
|
|
3467
|
+
FROM operations_contract_template
|
|
3468
|
+
WHERE id = $1
|
|
3469
|
+
AND deleted_at IS NULL
|
|
3470
|
+
LIMIT 1`, input.contractTemplateId))
|
|
3471
|
+
: [];
|
|
3472
|
+
const selectedTemplate = (_a = templateRows[0]) !== null && _a !== void 0 ? _a : null;
|
|
3473
|
+
const templateContext = {
|
|
3474
|
+
project_code: input.projectCode,
|
|
3475
|
+
project_name: input.projectName,
|
|
3476
|
+
client_name: input.clientName,
|
|
3477
|
+
start_date: (_b = input.startDate) !== null && _b !== void 0 ? _b : '',
|
|
3478
|
+
end_date: (_c = input.endDate) !== null && _c !== void 0 ? _c : '',
|
|
3479
|
+
budget_amount: input.budgetAmount !== null && input.budgetAmount !== undefined
|
|
3480
|
+
? String(input.budgetAmount)
|
|
3481
|
+
: '',
|
|
3482
|
+
monthly_hour_cap: input.monthlyHourCap !== null && input.monthlyHourCap !== undefined
|
|
3483
|
+
? String(input.monthlyHourCap)
|
|
3484
|
+
: '',
|
|
3485
|
+
};
|
|
3486
|
+
const applyTemplateVariables = (value) => {
|
|
3487
|
+
const source = value !== null && value !== void 0 ? value : '';
|
|
3488
|
+
return Object.entries(templateContext).reduce((result, [key, replacement]) => result.split(`{{${key}}}`).join(replacement || ''), source);
|
|
3489
|
+
};
|
|
3490
|
+
const templateCodePrefix = this.normalizeOptionalText(selectedTemplate === null || selectedTemplate === void 0 ? void 0 : selectedTemplate.code);
|
|
3491
|
+
const generatedContractCode = ((_e = (_d = this.normalizeOptionalText(input.contractCode)) !== null && _d !== void 0 ? _d : (templateCodePrefix
|
|
3492
|
+
? `${templateCodePrefix}-${input.projectCode}`
|
|
3493
|
+
: null)) !== null && _e !== void 0 ? _e : `PRJ-${input.projectCode}`).slice(0, 40);
|
|
3494
|
+
const generatedContractName = (_g = (_f = this.normalizeOptionalText(input.contractName)) !== null && _f !== void 0 ? _f : this.normalizeOptionalText(applyTemplateVariables(selectedTemplate === null || selectedTemplate === void 0 ? void 0 : selectedTemplate.name))) !== null && _g !== void 0 ? _g : `${input.projectName} Service Agreement`;
|
|
3495
|
+
const generatedDescription = this.normalizeOptionalText(applyTemplateVariables((_h = input.description) !== null && _h !== void 0 ? _h : selectedTemplate === null || selectedTemplate === void 0 ? void 0 : selectedTemplate.description));
|
|
3496
|
+
const generatedContentHtml = this.normalizeOptionalText(applyTemplateVariables(selectedTemplate === null || selectedTemplate === void 0 ? void 0 : selectedTemplate.contentHtml));
|
|
2043
3497
|
const created = await client.$queryRawUnsafe(`INSERT INTO operations_contract (
|
|
2044
3498
|
code,
|
|
2045
3499
|
name,
|
|
@@ -2051,6 +3505,7 @@ let OperationsService = class OperationsService {
|
|
|
2051
3505
|
billing_model,
|
|
2052
3506
|
account_manager_collaborator_id,
|
|
2053
3507
|
related_collaborator_id,
|
|
3508
|
+
contract_template_id,
|
|
2054
3509
|
origin_type,
|
|
2055
3510
|
origin_id,
|
|
2056
3511
|
start_date,
|
|
@@ -2067,11 +3522,35 @@ let OperationsService = class OperationsService {
|
|
|
2067
3522
|
created_at,
|
|
2068
3523
|
updated_at
|
|
2069
3524
|
) VALUES (
|
|
2070
|
-
$1,
|
|
2071
|
-
$
|
|
3525
|
+
$1,
|
|
3526
|
+
$2,
|
|
3527
|
+
$3::operations_contract_contract_category_70d553ea09_enum,
|
|
3528
|
+
$4::operations_contract_contract_type_48331e2ebf_enum,
|
|
3529
|
+
$5,
|
|
3530
|
+
$6::operations_contract_signature_status_2cb7282a7b_enum,
|
|
3531
|
+
true,
|
|
3532
|
+
$7::operations_contract_billing_model_409dc7fea2_enum,
|
|
3533
|
+
$8,
|
|
3534
|
+
NULL,
|
|
3535
|
+
$9,
|
|
3536
|
+
'client_project',
|
|
3537
|
+
$10,
|
|
3538
|
+
$11::date,
|
|
3539
|
+
$12::date,
|
|
3540
|
+
NULL,
|
|
3541
|
+
$11::date,
|
|
3542
|
+
$13,
|
|
3543
|
+
$14,
|
|
3544
|
+
'draft',
|
|
3545
|
+
$15,
|
|
3546
|
+
$16,
|
|
3547
|
+
$17,
|
|
3548
|
+
$17,
|
|
3549
|
+
NOW(),
|
|
3550
|
+
NOW()
|
|
2072
3551
|
)
|
|
2073
|
-
RETURNING id`, (
|
|
2074
|
-
return (
|
|
3552
|
+
RETURNING id`, generatedContractCode, generatedContractName, (_j = selectedTemplate === null || selectedTemplate === void 0 ? void 0 : selectedTemplate.contractCategory) !== null && _j !== void 0 ? _j : 'client', (_k = selectedTemplate === null || selectedTemplate === void 0 ? void 0 : selectedTemplate.contractType) !== null && _k !== void 0 ? _k : 'service_agreement', input.clientName, (_l = selectedTemplate === null || selectedTemplate === void 0 ? void 0 : selectedTemplate.signatureStatus) !== null && _l !== void 0 ? _l : 'not_started', (_m = selectedTemplate === null || selectedTemplate === void 0 ? void 0 : selectedTemplate.billingModel) !== null && _m !== void 0 ? _m : input.billingModel, input.managerCollaboratorId, (_o = selectedTemplate === null || selectedTemplate === void 0 ? void 0 : selectedTemplate.id) !== null && _o !== void 0 ? _o : null, input.projectId, (_p = input.startDate) !== null && _p !== void 0 ? _p : new Date().toISOString().slice(0, 10), (_q = input.endDate) !== null && _q !== void 0 ? _q : null, (_r = input.budgetAmount) !== null && _r !== void 0 ? _r : null, (_s = input.monthlyHourCap) !== null && _s !== void 0 ? _s : null, generatedDescription, generatedContentHtml, createdByUserId);
|
|
3553
|
+
return (_t = created[0]) === null || _t === void 0 ? void 0 : _t.id;
|
|
2075
3554
|
}
|
|
2076
3555
|
async replaceContractParties(client, contractId, parties) {
|
|
2077
3556
|
var _a, _b, _c, _d, _e, _f;
|
|
@@ -2093,7 +3572,10 @@ let OperationsService = class OperationsService {
|
|
|
2093
3572
|
created_at,
|
|
2094
3573
|
updated_at
|
|
2095
3574
|
) VALUES (
|
|
2096
|
-
$1,
|
|
3575
|
+
$1,
|
|
3576
|
+
$2::operations_contract_party_party_role_b66e8515fc_enum,
|
|
3577
|
+
$3::operations_contract_party_party_type_1383bb371c_enum,
|
|
3578
|
+
$4, $5, $6, $7, $8, NOW(), NOW()
|
|
2097
3579
|
)`, contractId, (_a = party.partyRole) !== null && _a !== void 0 ? _a : 'other', (_b = party.partyType) !== null && _b !== void 0 ? _b : 'company', party.displayName, (_c = party.documentNumber) !== null && _c !== void 0 ? _c : null, (_d = party.email) !== null && _d !== void 0 ? _d : null, (_e = party.phone) !== null && _e !== void 0 ? _e : null, (_f = party.isPrimary) !== null && _f !== void 0 ? _f : false);
|
|
2098
3580
|
}
|
|
2099
3581
|
}
|
|
@@ -2115,7 +3597,9 @@ let OperationsService = class OperationsService {
|
|
|
2115
3597
|
created_at,
|
|
2116
3598
|
updated_at
|
|
2117
3599
|
) VALUES (
|
|
2118
|
-
$1, $2, $3, $4,
|
|
3600
|
+
$1, $2, $3, $4,
|
|
3601
|
+
$5::operations_contract_signature_signer_status_1e6fbe2519_enum,
|
|
3602
|
+
$6::timestamp, NOW(), NOW()
|
|
2119
3603
|
)`, contractId, signature.signerName, (_a = signature.signerRole) !== null && _a !== void 0 ? _a : null, (_b = signature.signerEmail) !== null && _b !== void 0 ? _b : null, (_c = signature.status) !== null && _c !== void 0 ? _c : 'pending', (_d = signature.signedAt) !== null && _d !== void 0 ? _d : null);
|
|
2120
3604
|
}
|
|
2121
3605
|
}
|
|
@@ -2138,7 +3622,14 @@ let OperationsService = class OperationsService {
|
|
|
2138
3622
|
created_at,
|
|
2139
3623
|
updated_at
|
|
2140
3624
|
) VALUES (
|
|
2141
|
-
$1,
|
|
3625
|
+
$1,
|
|
3626
|
+
$2::operations_contract_financial_term_term_type_700635c06a_enum,
|
|
3627
|
+
$3,
|
|
3628
|
+
$4,
|
|
3629
|
+
$5::operations_contract_financial_term_recurrence_ba90bbe3bf_enum,
|
|
3630
|
+
$6,
|
|
3631
|
+
$7,
|
|
3632
|
+
NOW(), NOW()
|
|
2142
3633
|
)`, contractId, (_a = term.termType) !== null && _a !== void 0 ? _a : 'value', term.label, term.amount, (_b = term.recurrence) !== null && _b !== void 0 ? _b : 'one_time', (_c = term.dueDay) !== null && _c !== void 0 ? _c : null, (_d = term.notes) !== null && _d !== void 0 ? _d : null);
|
|
2143
3634
|
}
|
|
2144
3635
|
}
|
|
@@ -2160,31 +3651,346 @@ let OperationsService = class OperationsService {
|
|
|
2160
3651
|
created_at,
|
|
2161
3652
|
updated_at
|
|
2162
3653
|
) VALUES (
|
|
2163
|
-
$1,
|
|
3654
|
+
$1,
|
|
3655
|
+
$2::operations_contract_revision_revision_type_cf5ba1a538_enum,
|
|
3656
|
+
$3,
|
|
3657
|
+
$4::date,
|
|
3658
|
+
$5::operations_contract_revision_status_f44f35bb66_enum,
|
|
3659
|
+
$6,
|
|
3660
|
+
NOW(), NOW()
|
|
2164
3661
|
)`, contractId, (_a = revision.revisionType) !== null && _a !== void 0 ? _a : 'revision', revision.title, (_b = revision.effectiveDate) !== null && _b !== void 0 ? _b : null, (_c = revision.status) !== null && _c !== void 0 ? _c : 'draft', (_d = revision.summary) !== null && _d !== void 0 ? _d : null);
|
|
2165
3662
|
}
|
|
2166
3663
|
}
|
|
2167
|
-
async
|
|
2168
|
-
var _a;
|
|
3664
|
+
async replaceContractDocument(client, contractId, documentType, document) {
|
|
3665
|
+
var _a, _b, _c, _d, _e;
|
|
2169
3666
|
await client.$executeRawUnsafe(`UPDATE operations_contract_document
|
|
2170
3667
|
SET is_current = false,
|
|
2171
3668
|
updated_at = NOW()
|
|
2172
3669
|
WHERE contract_id = $1
|
|
2173
3670
|
AND deleted_at IS NULL
|
|
2174
|
-
AND document_type
|
|
3671
|
+
AND document_type = $2`, contractId, documentType);
|
|
2175
3672
|
await client.$executeRawUnsafe(`INSERT INTO operations_contract_document (
|
|
2176
3673
|
contract_id,
|
|
2177
3674
|
document_type,
|
|
3675
|
+
file_id,
|
|
2178
3676
|
file_name,
|
|
2179
3677
|
mime_type,
|
|
2180
3678
|
file_content_base64,
|
|
2181
3679
|
is_current,
|
|
3680
|
+
extraction_status,
|
|
3681
|
+
extraction_summary,
|
|
2182
3682
|
notes,
|
|
2183
3683
|
created_at,
|
|
2184
3684
|
updated_at
|
|
2185
3685
|
) VALUES (
|
|
2186
|
-
$1,
|
|
2187
|
-
)`, contractId, document.fileName, document.mimeType, document.fileContentBase64, (
|
|
3686
|
+
$1, $2, $3, $4, $5, $6, true, $7, $8, $9, NOW(), NOW()
|
|
3687
|
+
)`, contractId, documentType, (_a = document.fileId) !== null && _a !== void 0 ? _a : null, document.fileName, document.mimeType, (_b = document.fileContentBase64) !== null && _b !== void 0 ? _b : null, (_c = document.extractionStatus) !== null && _c !== void 0 ? _c : 'skipped', (_d = document.extractionSummary) !== null && _d !== void 0 ? _d : null, (_e = document.notes) !== null && _e !== void 0 ? _e : null);
|
|
3688
|
+
}
|
|
3689
|
+
async resolveContractExtractionFile(userId, data) {
|
|
3690
|
+
if (data.contractId && data.contractId > 0) {
|
|
3691
|
+
const contract = await this.getContractById(userId, data.contractId);
|
|
3692
|
+
const sourceDocument = contract.documents.find((document) => document.isCurrent && document.documentType === 'source_upload');
|
|
3693
|
+
if (!sourceDocument) {
|
|
3694
|
+
throw new common_1.BadRequestException('No source document is attached to this contract draft.');
|
|
3695
|
+
}
|
|
3696
|
+
if (sourceDocument.fileId) {
|
|
3697
|
+
const { buffer } = await this.fileService.getBuffer(sourceDocument.fileId);
|
|
3698
|
+
return this.buildContractExtractionFile(sourceDocument.fileName, sourceDocument.mimeType, buffer);
|
|
3699
|
+
}
|
|
3700
|
+
const base64 = this.normalizeExtractionString(sourceDocument.fileContentBase64);
|
|
3701
|
+
if (!base64) {
|
|
3702
|
+
throw new common_1.BadRequestException('The current source document does not contain readable file content.');
|
|
3703
|
+
}
|
|
3704
|
+
return this.buildContractExtractionFile(sourceDocument.fileName, sourceDocument.mimeType, Buffer.from(base64, 'base64'));
|
|
3705
|
+
}
|
|
3706
|
+
const fileName = this.normalizeExtractionString(data.fileName);
|
|
3707
|
+
const fileContentBase64 = this.normalizeExtractionString(data.fileContentBase64);
|
|
3708
|
+
const mimeType = this.normalizeContractExtractionMimeType(data.mimeType, fileName);
|
|
3709
|
+
if (!fileName || !fileContentBase64) {
|
|
3710
|
+
throw new common_1.BadRequestException('fileName and fileContentBase64 are required.');
|
|
3711
|
+
}
|
|
3712
|
+
return this.buildContractExtractionFile(fileName, mimeType, Buffer.from(fileContentBase64, 'base64'));
|
|
3713
|
+
}
|
|
3714
|
+
buildContractExtractionFile(fileName, mimeType, buffer) {
|
|
3715
|
+
if (!this.isAllowedContractExtractionMimeType(mimeType)) {
|
|
3716
|
+
throw new common_1.BadRequestException('Only PDF, DOC, and DOCX files are supported for contract extraction.');
|
|
3717
|
+
}
|
|
3718
|
+
if (!buffer.length) {
|
|
3719
|
+
throw new common_1.BadRequestException('Unable to decode the uploaded contract document.');
|
|
3720
|
+
}
|
|
3721
|
+
if (buffer.length > 12 * 1024 * 1024) {
|
|
3722
|
+
throw new common_1.BadRequestException('The uploaded contract document must be smaller than 12 MB.');
|
|
3723
|
+
}
|
|
3724
|
+
return {
|
|
3725
|
+
fieldname: 'file',
|
|
3726
|
+
originalname: fileName,
|
|
3727
|
+
encoding: 'base64',
|
|
3728
|
+
mimetype: mimeType,
|
|
3729
|
+
size: buffer.length,
|
|
3730
|
+
destination: '',
|
|
3731
|
+
filename: fileName,
|
|
3732
|
+
path: '',
|
|
3733
|
+
buffer,
|
|
3734
|
+
};
|
|
3735
|
+
}
|
|
3736
|
+
async renderContractPdfBuffer(contract) {
|
|
3737
|
+
var _a, _b, _c;
|
|
3738
|
+
const html = await this.buildContractPdfHtml(contract);
|
|
3739
|
+
let browser = null;
|
|
3740
|
+
try {
|
|
3741
|
+
const importPlaywright = new Function('moduleName', 'return import(moduleName);');
|
|
3742
|
+
const playwright = await importPlaywright('playwright');
|
|
3743
|
+
browser = await playwright.chromium.launch({ headless: true });
|
|
3744
|
+
const page = await browser.newPage();
|
|
3745
|
+
await page.setContent(html, { waitUntil: 'networkidle' });
|
|
3746
|
+
return Buffer.from(await page.pdf({
|
|
3747
|
+
format: 'A4',
|
|
3748
|
+
printBackground: true,
|
|
3749
|
+
margin: {
|
|
3750
|
+
top: '72px',
|
|
3751
|
+
right: '18px',
|
|
3752
|
+
bottom: '56px',
|
|
3753
|
+
left: '18px',
|
|
3754
|
+
},
|
|
3755
|
+
}));
|
|
3756
|
+
}
|
|
3757
|
+
catch (error) {
|
|
3758
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
3759
|
+
const errorStack = error instanceof Error ? ((_a = error.stack) !== null && _a !== void 0 ? _a : error.message) : String(error);
|
|
3760
|
+
const missingPlaywrightRuntime = /Cannot find package ['"]playwright['"]|Cannot find module ['"]playwright['"]|Executable doesn't exist|browserType\.launch|Failed to launch|Please run the following command to download new browsers/i.test(errorMessage);
|
|
3761
|
+
this.logger.error(`Failed to generate contract PDF for contract ${(_b = contract === null || contract === void 0 ? void 0 : contract.id) !== null && _b !== void 0 ? _b : 'unknown'}. ${errorMessage}`, errorStack);
|
|
3762
|
+
throw new common_1.InternalServerErrorException(missingPlaywrightRuntime
|
|
3763
|
+
? 'PDF generation is unavailable because Playwright/Chromium is not installed on the server. Run `pnpm --filter api run playwright:install` in the API environment.'
|
|
3764
|
+
: 'Failed to generate the PDF document. Check server logs for details.');
|
|
3765
|
+
}
|
|
3766
|
+
finally {
|
|
3767
|
+
await ((_c = browser === null || browser === void 0 ? void 0 : browser.close) === null || _c === void 0 ? void 0 : _c.call(browser));
|
|
3768
|
+
}
|
|
3769
|
+
}
|
|
3770
|
+
async buildContractPdfHtml(contract) {
|
|
3771
|
+
var _a, _b, _c, _d, _e;
|
|
3772
|
+
const logoUrl = await this.resolveContractPdfLogoUrl();
|
|
3773
|
+
const contentHtml = (_c = (_a = this.normalizeOptionalText(contract.contentHtml)) !== null && _a !== void 0 ? _a : (_b = this.normalizeOptionalText(contract.description)) === null || _b === void 0 ? void 0 : _b.split(/\n{2,}/).map((paragraph) => `<p>${this.escapeHtml(paragraph)}</p>`).join('')) !== null && _c !== void 0 ? _c : '<p>No contract body was provided yet.</p>';
|
|
3774
|
+
const partiesHtml = ((_d = contract.parties) !== null && _d !== void 0 ? _d : []).length
|
|
3775
|
+
? contract.parties
|
|
3776
|
+
.map((party) => `
|
|
3777
|
+
<li>
|
|
3778
|
+
<strong>${this.escapeHtml(party.displayName || 'Party')}</strong>
|
|
3779
|
+
<span>${this.escapeHtml([party.partyRole, party.partyType].filter(Boolean).join(' • '))}</span>
|
|
3780
|
+
</li>`)
|
|
3781
|
+
.join('')
|
|
3782
|
+
: '<li><strong>No parties registered yet.</strong><span>Complete this contract later if needed.</span></li>';
|
|
3783
|
+
const financialTermsHtml = ((_e = contract.financialTerms) !== null && _e !== void 0 ? _e : []).length
|
|
3784
|
+
? contract.financialTerms
|
|
3785
|
+
.map((term) => `
|
|
3786
|
+
<li>
|
|
3787
|
+
<strong>${this.escapeHtml(term.label || 'Term')}</strong>
|
|
3788
|
+
<span>${this.escapeHtml([term.termType, term.amount, term.recurrence]
|
|
3789
|
+
.filter((item) => item !== null && item !== undefined && String(item).trim())
|
|
3790
|
+
.join(' • '))}</span>
|
|
3791
|
+
</li>`)
|
|
3792
|
+
.join('')
|
|
3793
|
+
: '<li><strong>No financial terms registered yet.</strong><span>The draft is intentionally lightweight.</span></li>';
|
|
3794
|
+
return `<!DOCTYPE html>
|
|
3795
|
+
<html lang="en">
|
|
3796
|
+
<head>
|
|
3797
|
+
<meta charset="utf-8" />
|
|
3798
|
+
<style>
|
|
3799
|
+
@page {
|
|
3800
|
+
size: A4;
|
|
3801
|
+
margin: 72px 18px 56px 18px;
|
|
3802
|
+
}
|
|
3803
|
+
body {
|
|
3804
|
+
color: #0f172a;
|
|
3805
|
+
font-family: Arial, sans-serif;
|
|
3806
|
+
font-size: 12px;
|
|
3807
|
+
line-height: 1.5;
|
|
3808
|
+
margin: 0;
|
|
3809
|
+
}
|
|
3810
|
+
header {
|
|
3811
|
+
align-items: center;
|
|
3812
|
+
border-bottom: 2px solid #dbeafe;
|
|
3813
|
+
display: flex;
|
|
3814
|
+
gap: 16px;
|
|
3815
|
+
justify-content: space-between;
|
|
3816
|
+
margin-bottom: 24px;
|
|
3817
|
+
padding-bottom: 16px;
|
|
3818
|
+
}
|
|
3819
|
+
.brand {
|
|
3820
|
+
align-items: center;
|
|
3821
|
+
display: flex;
|
|
3822
|
+
gap: 12px;
|
|
3823
|
+
}
|
|
3824
|
+
.brand img {
|
|
3825
|
+
height: 42px;
|
|
3826
|
+
max-width: 140px;
|
|
3827
|
+
object-fit: contain;
|
|
3828
|
+
}
|
|
3829
|
+
.eyebrow {
|
|
3830
|
+
color: #2563eb;
|
|
3831
|
+
font-size: 10px;
|
|
3832
|
+
font-weight: 700;
|
|
3833
|
+
letter-spacing: 0.12em;
|
|
3834
|
+
text-transform: uppercase;
|
|
3835
|
+
}
|
|
3836
|
+
h1 {
|
|
3837
|
+
font-size: 22px;
|
|
3838
|
+
margin: 4px 0 0;
|
|
3839
|
+
}
|
|
3840
|
+
.meta-grid {
|
|
3841
|
+
display: grid;
|
|
3842
|
+
gap: 12px;
|
|
3843
|
+
grid-template-columns: repeat(2, minmax(0, 1fr));
|
|
3844
|
+
margin: 20px 0;
|
|
3845
|
+
}
|
|
3846
|
+
.meta-card {
|
|
3847
|
+
background: #f8fafc;
|
|
3848
|
+
border: 1px solid #dbeafe;
|
|
3849
|
+
border-radius: 12px;
|
|
3850
|
+
padding: 12px;
|
|
3851
|
+
}
|
|
3852
|
+
.meta-card strong {
|
|
3853
|
+
display: block;
|
|
3854
|
+
font-size: 10px;
|
|
3855
|
+
letter-spacing: 0.08em;
|
|
3856
|
+
margin-bottom: 4px;
|
|
3857
|
+
text-transform: uppercase;
|
|
3858
|
+
}
|
|
3859
|
+
section {
|
|
3860
|
+
margin-top: 20px;
|
|
3861
|
+
}
|
|
3862
|
+
h2 {
|
|
3863
|
+
color: #1d4ed8;
|
|
3864
|
+
font-size: 14px;
|
|
3865
|
+
margin: 0 0 10px;
|
|
3866
|
+
}
|
|
3867
|
+
ul {
|
|
3868
|
+
list-style: none;
|
|
3869
|
+
margin: 0;
|
|
3870
|
+
padding: 0;
|
|
3871
|
+
}
|
|
3872
|
+
li {
|
|
3873
|
+
border: 1px solid #e2e8f0;
|
|
3874
|
+
border-radius: 10px;
|
|
3875
|
+
margin-bottom: 8px;
|
|
3876
|
+
padding: 10px 12px;
|
|
3877
|
+
}
|
|
3878
|
+
li strong {
|
|
3879
|
+
display: block;
|
|
3880
|
+
margin-bottom: 2px;
|
|
3881
|
+
}
|
|
3882
|
+
footer {
|
|
3883
|
+
border-top: 1px solid #dbeafe;
|
|
3884
|
+
color: #475569;
|
|
3885
|
+
font-size: 10px;
|
|
3886
|
+
margin-top: 28px;
|
|
3887
|
+
padding-top: 12px;
|
|
3888
|
+
}
|
|
3889
|
+
.content {
|
|
3890
|
+
border: 1px solid #e2e8f0;
|
|
3891
|
+
border-radius: 14px;
|
|
3892
|
+
padding: 18px;
|
|
3893
|
+
}
|
|
3894
|
+
.content p:first-child {
|
|
3895
|
+
margin-top: 0;
|
|
3896
|
+
}
|
|
3897
|
+
</style>
|
|
3898
|
+
</head>
|
|
3899
|
+
<body>
|
|
3900
|
+
<header>
|
|
3901
|
+
<div class="brand">
|
|
3902
|
+
<img src="${logoUrl}" alt="System logo" />
|
|
3903
|
+
<div>
|
|
3904
|
+
<div class="eyebrow">Contract Document</div>
|
|
3905
|
+
<h1>${this.escapeHtml(contract.name || contract.code || 'Contract draft')}</h1>
|
|
3906
|
+
</div>
|
|
3907
|
+
</div>
|
|
3908
|
+
<div>
|
|
3909
|
+
<div><strong>Code:</strong> ${this.escapeHtml(contract.code || 'Draft')}</div>
|
|
3910
|
+
<div><strong>Status:</strong> ${this.escapeHtml(this.humanizeEnumLabel(contract.status || 'draft'))}</div>
|
|
3911
|
+
</div>
|
|
3912
|
+
</header>
|
|
3913
|
+
|
|
3914
|
+
<div class="meta-grid">
|
|
3915
|
+
<div class="meta-card">
|
|
3916
|
+
<strong>Client</strong>
|
|
3917
|
+
${this.escapeHtml(contract.clientName || 'Not informed yet')}
|
|
3918
|
+
</div>
|
|
3919
|
+
<div class="meta-card">
|
|
3920
|
+
<strong>Type</strong>
|
|
3921
|
+
${this.escapeHtml(this.humanizeEnumLabel(contract.contractType || 'service_agreement'))}
|
|
3922
|
+
</div>
|
|
3923
|
+
<div class="meta-card">
|
|
3924
|
+
<strong>Period</strong>
|
|
3925
|
+
${this.escapeHtml([contract.startDate || 'Open start', contract.endDate || 'Open end'].join(' - '))}
|
|
3926
|
+
</div>
|
|
3927
|
+
<div class="meta-card">
|
|
3928
|
+
<strong>Billing</strong>
|
|
3929
|
+
${this.escapeHtml(this.humanizeEnumLabel(contract.billingModel || 'time_and_material'))}
|
|
3930
|
+
</div>
|
|
3931
|
+
</div>
|
|
3932
|
+
|
|
3933
|
+
<section>
|
|
3934
|
+
<h2>Related Parties</h2>
|
|
3935
|
+
<ul>${partiesHtml}</ul>
|
|
3936
|
+
</section>
|
|
3937
|
+
|
|
3938
|
+
<section>
|
|
3939
|
+
<h2>Financial Terms</h2>
|
|
3940
|
+
<ul>${financialTermsHtml}</ul>
|
|
3941
|
+
</section>
|
|
3942
|
+
|
|
3943
|
+
<section>
|
|
3944
|
+
<h2>Contract Body</h2>
|
|
3945
|
+
<div class="content">${contentHtml}</div>
|
|
3946
|
+
</section>
|
|
3947
|
+
|
|
3948
|
+
<footer>
|
|
3949
|
+
Generated on ${this.escapeHtml(new Date().toISOString())} for ${this.escapeHtml(contract.code || 'draft')}.
|
|
3950
|
+
</footer>
|
|
3951
|
+
</body>
|
|
3952
|
+
</html>`;
|
|
3953
|
+
}
|
|
3954
|
+
async resolveContractPdfLogoUrl() {
|
|
3955
|
+
var _a;
|
|
3956
|
+
const settings = await this.settingService.getSettingValues([
|
|
3957
|
+
'image-url',
|
|
3958
|
+
'icon-url',
|
|
3959
|
+
]);
|
|
3960
|
+
const configuredUrl = (_a = this.normalizeOptionalText(settings['image-url'])) !== null && _a !== void 0 ? _a : this.normalizeOptionalText(settings['icon-url']);
|
|
3961
|
+
if (configuredUrl && /^https?:\/\//i.test(configuredUrl)) {
|
|
3962
|
+
return configuredUrl;
|
|
3963
|
+
}
|
|
3964
|
+
const inlineSvg = encodeURIComponent(`<svg xmlns="http://www.w3.org/2000/svg" width="240" height="72" viewBox="0 0 240 72">
|
|
3965
|
+
<rect width="240" height="72" rx="18" fill="#0f172a"/>
|
|
3966
|
+
<circle cx="38" cy="36" r="18" fill="#3b82f6"/>
|
|
3967
|
+
<text x="72" y="43" fill="#ffffff" font-size="26" font-family="Arial, sans-serif" font-weight="700">HedHog</text>
|
|
3968
|
+
</svg>`);
|
|
3969
|
+
return `data:image/svg+xml;charset=UTF-8,${inlineSvg}`;
|
|
3970
|
+
}
|
|
3971
|
+
buildContractPdfFileName(contract) {
|
|
3972
|
+
var _a, _b;
|
|
3973
|
+
const base = (_b = (_a = this.normalizeOptionalText(contract.code)) !== null && _a !== void 0 ? _a : this.normalizeOptionalText(contract.name)) !== null && _b !== void 0 ? _b : `contract-${contract.id}`;
|
|
3974
|
+
return `${base
|
|
3975
|
+
.toLowerCase()
|
|
3976
|
+
.replace(/[^a-z0-9]+/g, '-')
|
|
3977
|
+
.replace(/^-+|-+$/g, '')
|
|
3978
|
+
.slice(0, 80) || `contract-${contract.id}`}.pdf`;
|
|
3979
|
+
}
|
|
3980
|
+
humanizeEnumLabel(value) {
|
|
3981
|
+
return String(value !== null && value !== void 0 ? value : '')
|
|
3982
|
+
.split('_')
|
|
3983
|
+
.filter(Boolean)
|
|
3984
|
+
.map((part) => { var _a; return ((_a = part[0]) === null || _a === void 0 ? void 0 : _a.toUpperCase()) + part.slice(1); })
|
|
3985
|
+
.join(' ');
|
|
3986
|
+
}
|
|
3987
|
+
escapeHtml(value) {
|
|
3988
|
+
return String(value !== null && value !== void 0 ? value : '')
|
|
3989
|
+
.replace(/&/g, '&')
|
|
3990
|
+
.replace(/</g, '<')
|
|
3991
|
+
.replace(/>/g, '>')
|
|
3992
|
+
.replace(/"/g, '"')
|
|
3993
|
+
.replace(/'/g, ''');
|
|
2188
3994
|
}
|
|
2189
3995
|
async insertContractHistory(client, contractId, actorUserId, action, note, metadataJson) {
|
|
2190
3996
|
await client.$executeRawUnsafe(`INSERT INTO operations_contract_history (
|
|
@@ -2198,6 +4004,555 @@ let OperationsService = class OperationsService {
|
|
|
2198
4004
|
$1, $2, $3, $4, $5, NOW()
|
|
2199
4005
|
)`, contractId, actorUserId, action, note, metadataJson !== null && metadataJson !== void 0 ? metadataJson : null);
|
|
2200
4006
|
}
|
|
4007
|
+
async generateCollaboratorCode(client) {
|
|
4008
|
+
for (let attempt = 0; attempt < 5; attempt += 1) {
|
|
4009
|
+
const candidate = `COL-${Date.now().toString(36).toUpperCase()}${Math.random()
|
|
4010
|
+
.toString(36)
|
|
4011
|
+
.slice(2, 6)
|
|
4012
|
+
.toUpperCase()}`;
|
|
4013
|
+
const existing = (await client.$queryRawUnsafe(`SELECT code
|
|
4014
|
+
FROM operations_collaborator
|
|
4015
|
+
WHERE code = $1
|
|
4016
|
+
LIMIT 1`, candidate));
|
|
4017
|
+
if (!existing.length) {
|
|
4018
|
+
return candidate;
|
|
4019
|
+
}
|
|
4020
|
+
}
|
|
4021
|
+
throw new common_1.BadRequestException('Unable to generate collaborator code.');
|
|
4022
|
+
}
|
|
4023
|
+
async generateContractCode(client) {
|
|
4024
|
+
for (let attempt = 0; attempt < 5; attempt += 1) {
|
|
4025
|
+
const candidate = `CTR-${Date.now().toString(36).toUpperCase()}${Math.random()
|
|
4026
|
+
.toString(36)
|
|
4027
|
+
.slice(2, 6)
|
|
4028
|
+
.toUpperCase()}`.slice(0, 40);
|
|
4029
|
+
const existing = (await client.$queryRawUnsafe(`SELECT code
|
|
4030
|
+
FROM operations_contract
|
|
4031
|
+
WHERE code = $1
|
|
4032
|
+
LIMIT 1`, candidate));
|
|
4033
|
+
if (!existing.length) {
|
|
4034
|
+
return candidate;
|
|
4035
|
+
}
|
|
4036
|
+
}
|
|
4037
|
+
throw new common_1.BadRequestException('Unable to generate contract code.');
|
|
4038
|
+
}
|
|
4039
|
+
async generateContractContentHtml(contract, data = {}) {
|
|
4040
|
+
var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p, _q, _r, _s, _t, _u, _v, _w, _x;
|
|
4041
|
+
const currentContent = this.normalizeOptionalText(contract.contentHtml);
|
|
4042
|
+
const primaryParty = (_d = (_b = ((_a = contract.parties) !== null && _a !== void 0 ? _a : []).find((party) => party.isPrimary)) !== null && _b !== void 0 ? _b : ((_c = contract.parties) !== null && _c !== void 0 ? _c : [])[0]) !== null && _d !== void 0 ? _d : null;
|
|
4043
|
+
const templateContext = {
|
|
4044
|
+
contract_code: (_e = this.normalizeOptionalText(contract.code)) !== null && _e !== void 0 ? _e : '',
|
|
4045
|
+
contract_name: (_f = this.normalizeOptionalText(contract.name)) !== null && _f !== void 0 ? _f : '',
|
|
4046
|
+
client_name: (_h = (_g = this.normalizeOptionalText(contract.clientName)) !== null && _g !== void 0 ? _g : this.normalizeOptionalText(primaryParty === null || primaryParty === void 0 ? void 0 : primaryParty.displayName)) !== null && _h !== void 0 ? _h : '',
|
|
4047
|
+
contract_category: (_j = this.normalizeOptionalText(contract.contractCategory)) !== null && _j !== void 0 ? _j : '',
|
|
4048
|
+
contract_type: (_k = this.normalizeOptionalText(contract.contractType)) !== null && _k !== void 0 ? _k : '',
|
|
4049
|
+
start_date: (_l = this.normalizeOptionalText(contract.startDate)) !== null && _l !== void 0 ? _l : '',
|
|
4050
|
+
end_date: (_m = this.normalizeOptionalText(contract.endDate)) !== null && _m !== void 0 ? _m : '',
|
|
4051
|
+
effective_date: (_o = this.normalizeOptionalText(contract.effectiveDate)) !== null && _o !== void 0 ? _o : '',
|
|
4052
|
+
signed_at: (_p = this.normalizeOptionalText(contract.signedAt)) !== null && _p !== void 0 ? _p : '',
|
|
4053
|
+
budget_amount: contract.budgetAmount !== null && contract.budgetAmount !== undefined
|
|
4054
|
+
? String(contract.budgetAmount)
|
|
4055
|
+
: '',
|
|
4056
|
+
monthly_hour_cap: contract.monthlyHourCap !== null &&
|
|
4057
|
+
contract.monthlyHourCap !== undefined
|
|
4058
|
+
? String(contract.monthlyHourCap)
|
|
4059
|
+
: '',
|
|
4060
|
+
primary_party_name: (_q = this.normalizeOptionalText(primaryParty === null || primaryParty === void 0 ? void 0 : primaryParty.displayName)) !== null && _q !== void 0 ? _q : '',
|
|
4061
|
+
primary_party_document: (_r = this.normalizeOptionalText(primaryParty === null || primaryParty === void 0 ? void 0 : primaryParty.documentNumber)) !== null && _r !== void 0 ? _r : '',
|
|
4062
|
+
primary_party_email: (_s = this.normalizeOptionalText(primaryParty === null || primaryParty === void 0 ? void 0 : primaryParty.email)) !== null && _s !== void 0 ? _s : '',
|
|
4063
|
+
primary_party_phone: (_t = this.normalizeOptionalText(primaryParty === null || primaryParty === void 0 ? void 0 : primaryParty.phone)) !== null && _t !== void 0 ? _t : '',
|
|
4064
|
+
description: (_u = this.normalizeOptionalText(contract.description)) !== null && _u !== void 0 ? _u : '',
|
|
4065
|
+
};
|
|
4066
|
+
const applyTemplateVariables = (value) => {
|
|
4067
|
+
const source = value !== null && value !== void 0 ? value : '';
|
|
4068
|
+
return Object.entries(templateContext).reduce((result, [key, replacement]) => result.split(`{{${key}}}`).join(replacement || ''), source);
|
|
4069
|
+
};
|
|
4070
|
+
const currentHtmlWithVariables = this.normalizeOptionalText(applyTemplateVariables(currentContent));
|
|
4071
|
+
if (currentHtmlWithVariables && data.overwrite !== true) {
|
|
4072
|
+
return currentHtmlWithVariables;
|
|
4073
|
+
}
|
|
4074
|
+
const baseHtml = currentHtmlWithVariables !== null && currentHtmlWithVariables !== void 0 ? currentHtmlWithVariables : this.buildFallbackContractContent(contract);
|
|
4075
|
+
const promptMessage = (_v = this.normalizeOptionalText(data.promptMessage)) !== null && _v !== void 0 ? _v : 'Generate an initial contract body using the registered metadata and keep the language concise, professional, and ready for legal review.';
|
|
4076
|
+
try {
|
|
4077
|
+
const aiResult = await this.aiService.chat({
|
|
4078
|
+
provider: data.provider === 'gemini' ? 'gemini' : 'openai',
|
|
4079
|
+
model: data.provider === 'gemini' ? 'gemini-1.5-flash' : 'gpt-4o-mini',
|
|
4080
|
+
message: promptMessage,
|
|
4081
|
+
systemPrompt: [
|
|
4082
|
+
'You draft professional business contract content for an operations back-office system.',
|
|
4083
|
+
'Return ONLY clean HTML suitable for a rich text editor preview.',
|
|
4084
|
+
'Do not include Markdown fences, commentary, or legal disclaimers inside the HTML.',
|
|
4085
|
+
'Keep the output concise, well-structured, and ready for legal revision.',
|
|
4086
|
+
'If a specific detail is missing, preserve placeholders like {{client_name}}, {{start_date}}, {{end_date}}, {{budget_amount}}, and {{monthly_hour_cap}} instead of inventing facts.',
|
|
4087
|
+
`Contract context:\n${JSON.stringify(templateContext, null, 2)}`,
|
|
4088
|
+
baseHtml
|
|
4089
|
+
? `Current base HTML:\n${baseHtml}`
|
|
4090
|
+
: 'There is no current HTML yet.',
|
|
4091
|
+
].join('\n\n'),
|
|
4092
|
+
});
|
|
4093
|
+
return ((_x = this.normalizeOptionalText(String((_w = aiResult === null || aiResult === void 0 ? void 0 : aiResult.content) !== null && _w !== void 0 ? _w : ''))) !== null && _x !== void 0 ? _x : baseHtml);
|
|
4094
|
+
}
|
|
4095
|
+
catch (_y) {
|
|
4096
|
+
return baseHtml;
|
|
4097
|
+
}
|
|
4098
|
+
}
|
|
4099
|
+
buildFallbackContractContent(contract) {
|
|
4100
|
+
var _a, _b, _c, _d, _e, _f, _g;
|
|
4101
|
+
const title = this.escapeHtml((_b = (_a = this.normalizeOptionalText(contract.name)) !== null && _a !== void 0 ? _a : this.normalizeOptionalText(contract.code)) !== null && _b !== void 0 ? _b : 'Contract Draft');
|
|
4102
|
+
const description = this.escapeHtml((_c = this.normalizeOptionalText(contract.description)) !== null && _c !== void 0 ? _c : 'This draft summarizes the main commercial and operational conditions agreed between the parties.');
|
|
4103
|
+
const validityItems = [
|
|
4104
|
+
contract.startDate ? `Start date: ${contract.startDate}` : null,
|
|
4105
|
+
contract.endDate ? `End date: ${contract.endDate}` : null,
|
|
4106
|
+
contract.effectiveDate ? `Effective date: ${contract.effectiveDate}` : null,
|
|
4107
|
+
contract.signedAt ? `Signed at: ${contract.signedAt}` : null,
|
|
4108
|
+
]
|
|
4109
|
+
.filter(Boolean)
|
|
4110
|
+
.map((item) => `<li>${this.escapeHtml(String(item))}</li>`)
|
|
4111
|
+
.join('');
|
|
4112
|
+
const partiesHtml = ((_d = contract.parties) !== null && _d !== void 0 ? _d : []).length
|
|
4113
|
+
? ((_e = contract.parties) !== null && _e !== void 0 ? _e : [])
|
|
4114
|
+
.map((party) => `
|
|
4115
|
+
<li>
|
|
4116
|
+
<strong>${this.escapeHtml(party.displayName || 'Party')}</strong>
|
|
4117
|
+
<span>${this.escapeHtml([party.partyRole, party.partyType, party.documentNumber]
|
|
4118
|
+
.filter(Boolean)
|
|
4119
|
+
.join(' • '))}</span>
|
|
4120
|
+
</li>`)
|
|
4121
|
+
.join('')
|
|
4122
|
+
: '<li><strong>Main party</strong><span>{{client_name}}</span></li>';
|
|
4123
|
+
const financialTermsHtml = ((_f = contract.financialTerms) !== null && _f !== void 0 ? _f : []).length
|
|
4124
|
+
? ((_g = contract.financialTerms) !== null && _g !== void 0 ? _g : [])
|
|
4125
|
+
.map((term) => `
|
|
4126
|
+
<li>
|
|
4127
|
+
<strong>${this.escapeHtml(term.label || 'Financial term')}</strong>
|
|
4128
|
+
<span>${this.escapeHtml([term.termType, term.amount, term.recurrence]
|
|
4129
|
+
.filter((item) => item !== null &&
|
|
4130
|
+
item !== undefined &&
|
|
4131
|
+
String(item).trim())
|
|
4132
|
+
.join(' • '))}</span>
|
|
4133
|
+
</li>`)
|
|
4134
|
+
.join('')
|
|
4135
|
+
: '<li><strong>Commercial conditions</strong><span>Define prices, billing cadence, and penalties before signature.</span></li>';
|
|
4136
|
+
return `
|
|
4137
|
+
<h1>${title}</h1>
|
|
4138
|
+
<p>${description}</p>
|
|
4139
|
+
<h2>1. Parties</h2>
|
|
4140
|
+
<ul>${partiesHtml}</ul>
|
|
4141
|
+
<h2>2. Scope and object</h2>
|
|
4142
|
+
<p>The parties agree to execute the services and obligations described in this contract, according to the agreed operational, technical, and commercial conditions.</p>
|
|
4143
|
+
<h2>3. Validity</h2>
|
|
4144
|
+
${validityItems ? `<ul>${validityItems}</ul>` : '<p>Define start date, term, renewal, and termination conditions.</p>'}
|
|
4145
|
+
<h2>4. Financial terms</h2>
|
|
4146
|
+
<ul>${financialTermsHtml}</ul>
|
|
4147
|
+
<h2>5. General obligations</h2>
|
|
4148
|
+
<p>Review confidentiality, data protection, service levels, termination, and liability clauses before formal approval.</p>
|
|
4149
|
+
`;
|
|
4150
|
+
}
|
|
4151
|
+
async buildContractLegalReview(contract, data = {}) {
|
|
4152
|
+
var _a, _b, _c;
|
|
4153
|
+
const heuristicReview = this.buildHeuristicContractLegalReview(contract);
|
|
4154
|
+
const promptMessage = (_a = this.normalizeOptionalText(data.promptMessage)) !== null && _a !== void 0 ? _a : 'Review this contract draft and return an advisory legal checklist with practical warnings and missing items.';
|
|
4155
|
+
try {
|
|
4156
|
+
const aiResult = await this.aiService.chat({
|
|
4157
|
+
provider: data.provider === 'gemini' ? 'gemini' : 'openai',
|
|
4158
|
+
model: data.provider === 'gemini' ? 'gemini-1.5-flash' : 'gpt-4o-mini',
|
|
4159
|
+
message: promptMessage,
|
|
4160
|
+
systemPrompt: [
|
|
4161
|
+
'You are an advisory legal-review assistant for business contracts.',
|
|
4162
|
+
'Return ONLY valid JSON. Do not include Markdown fences or commentary.',
|
|
4163
|
+
'This review is non-blocking and should help a human reviewer spot likely gaps.',
|
|
4164
|
+
'Expected JSON shape:',
|
|
4165
|
+
JSON.stringify({
|
|
4166
|
+
summary: '',
|
|
4167
|
+
missingFields: [],
|
|
4168
|
+
warnings: [],
|
|
4169
|
+
checklist: [],
|
|
4170
|
+
status: 'attention_required',
|
|
4171
|
+
}, null, 2),
|
|
4172
|
+
`Contract metadata:\n${JSON.stringify({
|
|
4173
|
+
code: contract.code,
|
|
4174
|
+
name: contract.name,
|
|
4175
|
+
clientName: contract.clientName,
|
|
4176
|
+
contractCategory: contract.contractCategory,
|
|
4177
|
+
contractType: contract.contractType,
|
|
4178
|
+
startDate: contract.startDate,
|
|
4179
|
+
endDate: contract.endDate,
|
|
4180
|
+
effectiveDate: contract.effectiveDate,
|
|
4181
|
+
signatureStatus: contract.signatureStatus,
|
|
4182
|
+
parties: contract.parties,
|
|
4183
|
+
financialTerms: contract.financialTerms,
|
|
4184
|
+
}, null, 2)}`,
|
|
4185
|
+
`Contract HTML:\n${(_b = this.normalizeOptionalText(contract.contentHtml)) !== null && _b !== void 0 ? _b : ''}`,
|
|
4186
|
+
].join('\n\n'),
|
|
4187
|
+
});
|
|
4188
|
+
const parsed = this.parseAiJsonPayload(String((_c = aiResult === null || aiResult === void 0 ? void 0 : aiResult.content) !== null && _c !== void 0 ? _c : ''));
|
|
4189
|
+
const missingFields = Array.from(new Set([
|
|
4190
|
+
...heuristicReview.missingFields,
|
|
4191
|
+
...this.normalizeStringList(parsed.missingFields),
|
|
4192
|
+
]));
|
|
4193
|
+
const warnings = Array.from(new Set([
|
|
4194
|
+
...heuristicReview.warnings,
|
|
4195
|
+
...this.normalizeStringList(parsed.warnings),
|
|
4196
|
+
]));
|
|
4197
|
+
const checklist = Array.from(new Set([
|
|
4198
|
+
...this.normalizeStringList(parsed.checklist),
|
|
4199
|
+
...heuristicReview.checklist,
|
|
4200
|
+
]));
|
|
4201
|
+
const parsedStatus = this.normalizeExtractionString(parsed.status);
|
|
4202
|
+
const status = parsedStatus === 'ready_for_revision' ||
|
|
4203
|
+
parsedStatus === 'attention_required'
|
|
4204
|
+
? parsedStatus
|
|
4205
|
+
: missingFields.length || warnings.length
|
|
4206
|
+
? 'attention_required'
|
|
4207
|
+
: 'ready_for_revision';
|
|
4208
|
+
return {
|
|
4209
|
+
summary: this.normalizeExtractionString(parsed.summary) ||
|
|
4210
|
+
heuristicReview.summary,
|
|
4211
|
+
missingFields,
|
|
4212
|
+
warnings,
|
|
4213
|
+
checklist,
|
|
4214
|
+
status,
|
|
4215
|
+
reviewedAt: new Date().toISOString(),
|
|
4216
|
+
};
|
|
4217
|
+
}
|
|
4218
|
+
catch (_d) {
|
|
4219
|
+
return heuristicReview;
|
|
4220
|
+
}
|
|
4221
|
+
}
|
|
4222
|
+
buildHeuristicContractLegalReview(contract) {
|
|
4223
|
+
var _a, _b, _c, _d, _e, _f, _g;
|
|
4224
|
+
const contentText = ((_a = this.normalizeOptionalText(contract.contentHtml)) !== null && _a !== void 0 ? _a : '')
|
|
4225
|
+
.replace(/<[^>]+>/g, ' ')
|
|
4226
|
+
.replace(/\s+/g, ' ')
|
|
4227
|
+
.trim()
|
|
4228
|
+
.toLowerCase();
|
|
4229
|
+
const primaryParty = (_e = (_c = ((_b = contract.parties) !== null && _b !== void 0 ? _b : []).find((party) => party.isPrimary)) !== null && _c !== void 0 ? _c : ((_d = contract.parties) !== null && _d !== void 0 ? _d : [])[0]) !== null && _e !== void 0 ? _e : null;
|
|
4230
|
+
const missingFields = [];
|
|
4231
|
+
const warnings = [];
|
|
4232
|
+
const checklist = [];
|
|
4233
|
+
if (!this.normalizeOptionalText(contract.name)) {
|
|
4234
|
+
missingFields.push('Contract title');
|
|
4235
|
+
checklist.push('Attention: define a clear contract title.');
|
|
4236
|
+
}
|
|
4237
|
+
else {
|
|
4238
|
+
checklist.push('OK: contract title is registered.');
|
|
4239
|
+
}
|
|
4240
|
+
if (!this.normalizeOptionalText(contract.clientName)) {
|
|
4241
|
+
missingFields.push('Client or primary party name');
|
|
4242
|
+
checklist.push('Attention: confirm the client or main party identification.');
|
|
4243
|
+
}
|
|
4244
|
+
else {
|
|
4245
|
+
checklist.push('OK: the main party is identified.');
|
|
4246
|
+
}
|
|
4247
|
+
if (!contract.startDate && !contract.effectiveDate) {
|
|
4248
|
+
missingFields.push('Start or effective date');
|
|
4249
|
+
checklist.push('Attention: define the term start or effective date.');
|
|
4250
|
+
}
|
|
4251
|
+
else {
|
|
4252
|
+
checklist.push('OK: validity dates were provided.');
|
|
4253
|
+
}
|
|
4254
|
+
if (!((_f = contract.parties) !== null && _f !== void 0 ? _f : []).length) {
|
|
4255
|
+
missingFields.push('Related parties');
|
|
4256
|
+
warnings.push('No related parties are registered in the contract draft.');
|
|
4257
|
+
}
|
|
4258
|
+
if (!this.normalizeOptionalText(primaryParty === null || primaryParty === void 0 ? void 0 : primaryParty.documentNumber)) {
|
|
4259
|
+
warnings.push('Consider confirming the primary party document number.');
|
|
4260
|
+
}
|
|
4261
|
+
if (!((_g = contract.financialTerms) !== null && _g !== void 0 ? _g : []).length && contract.budgetAmount == null) {
|
|
4262
|
+
warnings.push('Commercial conditions are still generic; define prices, recurrence, and penalties.');
|
|
4263
|
+
checklist.push('Attention: validate billing, adjustments, and penalties.');
|
|
4264
|
+
}
|
|
4265
|
+
else {
|
|
4266
|
+
checklist.push('OK: there is at least one financial reference to review.');
|
|
4267
|
+
}
|
|
4268
|
+
if (!contentText) {
|
|
4269
|
+
missingFields.push('Contract body content');
|
|
4270
|
+
warnings.push('The contract body is still empty and needs a first draft.');
|
|
4271
|
+
}
|
|
4272
|
+
else {
|
|
4273
|
+
if (contentText.length < 600) {
|
|
4274
|
+
warnings.push('The contract body is still brief; validate scope, obligations, and exceptions in detail.');
|
|
4275
|
+
}
|
|
4276
|
+
if (!/(rescis|terminat|cancel)/.test(contentText)) {
|
|
4277
|
+
warnings.push('Add a termination / rescission clause with notice and penalty rules.');
|
|
4278
|
+
}
|
|
4279
|
+
if (!/(confiden|sigil|data protect|privacy)/.test(contentText)) {
|
|
4280
|
+
warnings.push('Review confidentiality and data protection clauses when applicable.');
|
|
4281
|
+
}
|
|
4282
|
+
if (!/(foro|jurisd|governing law|applicable law)/.test(contentText)) {
|
|
4283
|
+
warnings.push('Review the governing law / forum clause before signature.');
|
|
4284
|
+
}
|
|
4285
|
+
}
|
|
4286
|
+
const summary = missingFields.length || warnings.length
|
|
4287
|
+
? `Advisory legal review found ${missingFields.length} missing item(s) and ${warnings.length} point(s) for attention before final approval.`
|
|
4288
|
+
: 'Advisory legal review found the draft structurally consistent for human legal revision.';
|
|
4289
|
+
return {
|
|
4290
|
+
summary,
|
|
4291
|
+
missingFields: Array.from(new Set(missingFields)),
|
|
4292
|
+
warnings: Array.from(new Set(warnings)),
|
|
4293
|
+
checklist: Array.from(new Set(checklist)),
|
|
4294
|
+
status: missingFields.length || warnings.length
|
|
4295
|
+
? 'attention_required'
|
|
4296
|
+
: 'ready_for_revision',
|
|
4297
|
+
reviewedAt: new Date().toISOString(),
|
|
4298
|
+
};
|
|
4299
|
+
}
|
|
4300
|
+
buildContractExtractionSystemPrompt() {
|
|
4301
|
+
return [
|
|
4302
|
+
'You extract structured draft data from uploaded business contracts.',
|
|
4303
|
+
'Return ONLY valid JSON. Do not include Markdown fences, prose, or comments.',
|
|
4304
|
+
'Use empty strings or empty arrays when the document is unclear; never invent legal facts.',
|
|
4305
|
+
'Dates must use YYYY-MM-DD whenever the document provides enough confidence.',
|
|
4306
|
+
'For contentHtml, return clean HTML suitable for a rich text editor preview.',
|
|
4307
|
+
'Expected JSON shape:',
|
|
4308
|
+
JSON.stringify({
|
|
4309
|
+
code: '',
|
|
4310
|
+
name: '',
|
|
4311
|
+
clientName: '',
|
|
4312
|
+
contractCategory: 'client',
|
|
4313
|
+
contractType: 'service_agreement',
|
|
4314
|
+
signatureStatus: 'not_started',
|
|
4315
|
+
billingModel: 'time_and_material',
|
|
4316
|
+
originType: 'manual',
|
|
4317
|
+
originId: '',
|
|
4318
|
+
startDate: '',
|
|
4319
|
+
endDate: '',
|
|
4320
|
+
signedAt: '',
|
|
4321
|
+
effectiveDate: '',
|
|
4322
|
+
budgetAmount: '',
|
|
4323
|
+
monthlyHourCap: '',
|
|
4324
|
+
status: 'draft',
|
|
4325
|
+
description: '',
|
|
4326
|
+
contentHtml: '',
|
|
4327
|
+
parties: [
|
|
4328
|
+
{
|
|
4329
|
+
displayName: '',
|
|
4330
|
+
partyRole: 'client',
|
|
4331
|
+
partyType: 'company',
|
|
4332
|
+
documentNumber: '',
|
|
4333
|
+
email: '',
|
|
4334
|
+
phone: '',
|
|
4335
|
+
isPrimary: true,
|
|
4336
|
+
},
|
|
4337
|
+
],
|
|
4338
|
+
signatures: [
|
|
4339
|
+
{
|
|
4340
|
+
signerName: '',
|
|
4341
|
+
signerRole: '',
|
|
4342
|
+
signerEmail: '',
|
|
4343
|
+
status: 'pending',
|
|
4344
|
+
signedAt: '',
|
|
4345
|
+
},
|
|
4346
|
+
],
|
|
4347
|
+
financialTerms: [
|
|
4348
|
+
{
|
|
4349
|
+
label: '',
|
|
4350
|
+
termType: 'value',
|
|
4351
|
+
amount: '',
|
|
4352
|
+
recurrence: 'one_time',
|
|
4353
|
+
dueDay: '',
|
|
4354
|
+
notes: '',
|
|
4355
|
+
},
|
|
4356
|
+
],
|
|
4357
|
+
revisions: [
|
|
4358
|
+
{
|
|
4359
|
+
title: '',
|
|
4360
|
+
revisionType: 'revision',
|
|
4361
|
+
effectiveDate: '',
|
|
4362
|
+
status: 'draft',
|
|
4363
|
+
summary: '',
|
|
4364
|
+
},
|
|
4365
|
+
],
|
|
4366
|
+
summary: '',
|
|
4367
|
+
missingFields: [],
|
|
4368
|
+
warnings: [],
|
|
4369
|
+
}, null, 2),
|
|
4370
|
+
].join('\n\n');
|
|
4371
|
+
}
|
|
4372
|
+
parseAiJsonPayload(content) {
|
|
4373
|
+
var _a;
|
|
4374
|
+
const trimmed = String(content !== null && content !== void 0 ? content : '').trim();
|
|
4375
|
+
if (!trimmed) {
|
|
4376
|
+
throw new common_1.BadRequestException('AI returned an empty contract draft response.');
|
|
4377
|
+
}
|
|
4378
|
+
const fencedMatch = trimmed.match(/```(?:json)?\s*([\s\S]*?)\s*```/i);
|
|
4379
|
+
const candidate = ((_a = fencedMatch === null || fencedMatch === void 0 ? void 0 : fencedMatch[1]) === null || _a === void 0 ? void 0 : _a.trim()) || trimmed;
|
|
4380
|
+
try {
|
|
4381
|
+
return JSON.parse(candidate);
|
|
4382
|
+
}
|
|
4383
|
+
catch (_b) {
|
|
4384
|
+
const firstBraceIndex = candidate.indexOf('{');
|
|
4385
|
+
const lastBraceIndex = candidate.lastIndexOf('}');
|
|
4386
|
+
if (firstBraceIndex >= 0 && lastBraceIndex > firstBraceIndex) {
|
|
4387
|
+
try {
|
|
4388
|
+
return JSON.parse(candidate.slice(firstBraceIndex, lastBraceIndex + 1));
|
|
4389
|
+
}
|
|
4390
|
+
catch (_c) {
|
|
4391
|
+
throw new common_1.BadRequestException('AI returned an invalid contract draft payload.');
|
|
4392
|
+
}
|
|
4393
|
+
}
|
|
4394
|
+
throw new common_1.BadRequestException('AI returned an invalid contract draft payload.');
|
|
4395
|
+
}
|
|
4396
|
+
}
|
|
4397
|
+
normalizeContractExtractDraft(raw) {
|
|
4398
|
+
const warnings = this.normalizeStringList(raw.warnings);
|
|
4399
|
+
const missingFields = this.normalizeStringList(raw.missingFields);
|
|
4400
|
+
const draft = {
|
|
4401
|
+
code: this.normalizeExtractionString(raw.code),
|
|
4402
|
+
name: this.normalizeExtractionString(raw.name),
|
|
4403
|
+
clientName: this.normalizeExtractionString(raw.clientName),
|
|
4404
|
+
contractCategory: this.normalizeEnumValue(raw.contractCategory, CONTRACT_CATEGORY_VALUES, 'client'),
|
|
4405
|
+
contractType: this.normalizeEnumValue(raw.contractType, CONTRACT_TYPE_VALUES, 'service_agreement'),
|
|
4406
|
+
signatureStatus: this.normalizeEnumValue(raw.signatureStatus, SIGNATURE_STATUS_VALUES, 'not_started'),
|
|
4407
|
+
isActive: raw.isActive === false ? false : true,
|
|
4408
|
+
billingModel: this.normalizeEnumValue(raw.billingModel, BILLING_MODEL_VALUES, 'time_and_material'),
|
|
4409
|
+
originType: this.normalizeEnumValue(raw.originType, ORIGIN_TYPE_VALUES, 'manual'),
|
|
4410
|
+
originId: this.normalizeExtractionString(raw.originId),
|
|
4411
|
+
startDate: this.normalizeExtractionDate(raw.startDate),
|
|
4412
|
+
endDate: this.normalizeExtractionDate(raw.endDate),
|
|
4413
|
+
signedAt: this.normalizeExtractionDate(raw.signedAt),
|
|
4414
|
+
effectiveDate: this.normalizeExtractionDate(raw.effectiveDate),
|
|
4415
|
+
budgetAmount: this.normalizeExtractionNumber(raw.budgetAmount),
|
|
4416
|
+
monthlyHourCap: this.normalizeExtractionNumber(raw.monthlyHourCap),
|
|
4417
|
+
status: this.normalizeEnumValue(raw.status, CONTRACT_STATUS_VALUES, 'draft'),
|
|
4418
|
+
description: this.normalizeExtractionString(raw.description),
|
|
4419
|
+
contentHtml: this.normalizeExtractionString(raw.contentHtml),
|
|
4420
|
+
summary: this.normalizeExtractionString(raw.summary),
|
|
4421
|
+
parties: this.normalizeObjectList(raw.parties)
|
|
4422
|
+
.map((party) => ({
|
|
4423
|
+
displayName: this.normalizeExtractionString(party.displayName),
|
|
4424
|
+
partyRole: this.normalizeEnumValue(party.partyRole, PARTY_ROLE_VALUES, 'client'),
|
|
4425
|
+
partyType: this.normalizeEnumValue(party.partyType, PARTY_TYPE_VALUES, 'company'),
|
|
4426
|
+
documentNumber: this.normalizeExtractionString(party.documentNumber),
|
|
4427
|
+
email: this.normalizeExtractionString(party.email),
|
|
4428
|
+
phone: this.normalizeExtractionString(party.phone),
|
|
4429
|
+
isPrimary: this.normalizeExtractionBoolean(party.isPrimary),
|
|
4430
|
+
}))
|
|
4431
|
+
.filter((party) => party.displayName),
|
|
4432
|
+
signatures: this.normalizeObjectList(raw.signatures)
|
|
4433
|
+
.map((signature) => ({
|
|
4434
|
+
signerName: this.normalizeExtractionString(signature.signerName),
|
|
4435
|
+
signerRole: this.normalizeExtractionString(signature.signerRole),
|
|
4436
|
+
signerEmail: this.normalizeExtractionString(signature.signerEmail),
|
|
4437
|
+
status: this.normalizeEnumValue(signature.status, SIGNATURE_ITEM_STATUS_VALUES, 'pending'),
|
|
4438
|
+
signedAt: this.normalizeExtractionDate(signature.signedAt),
|
|
4439
|
+
}))
|
|
4440
|
+
.filter((signature) => signature.signerName),
|
|
4441
|
+
financialTerms: this.normalizeObjectList(raw.financialTerms)
|
|
4442
|
+
.map((term) => ({
|
|
4443
|
+
label: this.normalizeExtractionString(term.label),
|
|
4444
|
+
termType: this.normalizeEnumValue(term.termType, FINANCIAL_TERM_TYPE_VALUES, 'value'),
|
|
4445
|
+
amount: this.normalizeExtractionNumber(term.amount),
|
|
4446
|
+
recurrence: this.normalizeEnumValue(term.recurrence, RECURRENCE_VALUES, 'one_time'),
|
|
4447
|
+
dueDay: this.normalizeExtractionNumber(term.dueDay),
|
|
4448
|
+
notes: this.normalizeExtractionString(term.notes),
|
|
4449
|
+
}))
|
|
4450
|
+
.filter((term) => term.label),
|
|
4451
|
+
revisions: this.normalizeObjectList(raw.revisions)
|
|
4452
|
+
.map((revision) => ({
|
|
4453
|
+
title: this.normalizeExtractionString(revision.title),
|
|
4454
|
+
revisionType: this.normalizeEnumValue(revision.revisionType, REVISION_TYPE_VALUES, 'revision'),
|
|
4455
|
+
effectiveDate: this.normalizeExtractionDate(revision.effectiveDate),
|
|
4456
|
+
status: this.normalizeEnumValue(revision.status, REVISION_STATUS_VALUES, 'draft'),
|
|
4457
|
+
summary: this.normalizeExtractionString(revision.summary),
|
|
4458
|
+
}))
|
|
4459
|
+
.filter((revision) => revision.title),
|
|
4460
|
+
};
|
|
4461
|
+
if (!draft.name)
|
|
4462
|
+
missingFields.push('Contract title');
|
|
4463
|
+
if (!draft.clientName)
|
|
4464
|
+
missingFields.push('Client name');
|
|
4465
|
+
if (!draft.startDate)
|
|
4466
|
+
missingFields.push('Start date');
|
|
4467
|
+
if (!draft.contentHtml && !draft.summary && !draft.description) {
|
|
4468
|
+
warnings.push('The contract body was not extracted with enough confidence. Use the editor to complete it.');
|
|
4469
|
+
}
|
|
4470
|
+
if (!draft.parties.length) {
|
|
4471
|
+
warnings.push('No related parties were identified with confidence.');
|
|
4472
|
+
}
|
|
4473
|
+
return Object.assign(Object.assign({}, draft), { summary: draft.summary || draft.description || draft.name, missingFields: Array.from(new Set(missingFields.filter(Boolean))), warnings: Array.from(new Set(warnings.filter(Boolean))) });
|
|
4474
|
+
}
|
|
4475
|
+
normalizeExtractionString(value) {
|
|
4476
|
+
return typeof value === 'string'
|
|
4477
|
+
? value.trim()
|
|
4478
|
+
: value === null || value === undefined
|
|
4479
|
+
? ''
|
|
4480
|
+
: String(value).trim();
|
|
4481
|
+
}
|
|
4482
|
+
normalizeExtractionBoolean(value) {
|
|
4483
|
+
if (typeof value === 'boolean') {
|
|
4484
|
+
return value;
|
|
4485
|
+
}
|
|
4486
|
+
const normalized = this.normalizeExtractionString(value).toLowerCase();
|
|
4487
|
+
return normalized === 'true' || normalized === '1' || normalized === 'yes';
|
|
4488
|
+
}
|
|
4489
|
+
normalizeExtractionNumber(value) {
|
|
4490
|
+
if (typeof value === 'number' && Number.isFinite(value)) {
|
|
4491
|
+
return value;
|
|
4492
|
+
}
|
|
4493
|
+
const normalized = this.normalizeExtractionString(value);
|
|
4494
|
+
if (!normalized) {
|
|
4495
|
+
return '';
|
|
4496
|
+
}
|
|
4497
|
+
const direct = Number(normalized);
|
|
4498
|
+
if (Number.isFinite(direct)) {
|
|
4499
|
+
return direct;
|
|
4500
|
+
}
|
|
4501
|
+
const ptBr = normalized
|
|
4502
|
+
.split('.')
|
|
4503
|
+
.join('')
|
|
4504
|
+
.replace(',', '.');
|
|
4505
|
+
const parsed = Number(ptBr);
|
|
4506
|
+
return Number.isFinite(parsed) ? parsed : normalized;
|
|
4507
|
+
}
|
|
4508
|
+
normalizeExtractionDate(value) {
|
|
4509
|
+
var _a;
|
|
4510
|
+
const normalized = this.normalizeExtractionString(value);
|
|
4511
|
+
if (!normalized) {
|
|
4512
|
+
return '';
|
|
4513
|
+
}
|
|
4514
|
+
const match = normalized.match(/\d{4}-\d{2}-\d{2}/);
|
|
4515
|
+
return (_a = match === null || match === void 0 ? void 0 : match[0]) !== null && _a !== void 0 ? _a : '';
|
|
4516
|
+
}
|
|
4517
|
+
normalizeStringList(value) {
|
|
4518
|
+
return Array.isArray(value)
|
|
4519
|
+
? value
|
|
4520
|
+
.map((item) => this.normalizeExtractionString(item))
|
|
4521
|
+
.filter(Boolean)
|
|
4522
|
+
: [];
|
|
4523
|
+
}
|
|
4524
|
+
normalizeObjectList(value) {
|
|
4525
|
+
if (!Array.isArray(value)) {
|
|
4526
|
+
return [];
|
|
4527
|
+
}
|
|
4528
|
+
return value.filter((item) => Boolean(item) && typeof item === 'object');
|
|
4529
|
+
}
|
|
4530
|
+
normalizeEnumValue(value, allowed, fallback) {
|
|
4531
|
+
const normalized = this.normalizeExtractionString(value);
|
|
4532
|
+
return allowed.includes(normalized) ? normalized : fallback;
|
|
4533
|
+
}
|
|
4534
|
+
normalizeContractExtractionMimeType(mimeType, fileName) {
|
|
4535
|
+
const current = this.normalizeExtractionString(mimeType).toLowerCase();
|
|
4536
|
+
if (current && current !== 'application/octet-stream') {
|
|
4537
|
+
return current;
|
|
4538
|
+
}
|
|
4539
|
+
const name = this.normalizeExtractionString(fileName).toLowerCase();
|
|
4540
|
+
if (name.endsWith('.pdf'))
|
|
4541
|
+
return 'application/pdf';
|
|
4542
|
+
if (name.endsWith('.doc'))
|
|
4543
|
+
return 'application/msword';
|
|
4544
|
+
if (name.endsWith('.docx')) {
|
|
4545
|
+
return 'application/vnd.openxmlformats-officedocument.wordprocessingml.document';
|
|
4546
|
+
}
|
|
4547
|
+
return current || 'application/octet-stream';
|
|
4548
|
+
}
|
|
4549
|
+
isAllowedContractExtractionMimeType(mimeType) {
|
|
4550
|
+
return [
|
|
4551
|
+
'application/pdf',
|
|
4552
|
+
'application/msword',
|
|
4553
|
+
'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
|
|
4554
|
+
].includes(mimeType);
|
|
4555
|
+
}
|
|
2201
4556
|
requireFields(input, required) {
|
|
2202
4557
|
for (const field of required) {
|
|
2203
4558
|
const value = input[field];
|
|
@@ -2223,15 +4578,18 @@ let OperationsService = class OperationsService {
|
|
|
2223
4578
|
...new Set(values.filter((value) => typeof value === 'number')),
|
|
2224
4579
|
];
|
|
2225
4580
|
}
|
|
2226
|
-
pushUpdate(updates, params, column, value) {
|
|
4581
|
+
pushUpdate(updates, params, column, value, castType) {
|
|
2227
4582
|
if (value === undefined)
|
|
2228
4583
|
return;
|
|
2229
4584
|
params.push(value);
|
|
2230
|
-
|
|
4585
|
+
const placeholder = castType
|
|
4586
|
+
? `$${params.length}::${castType}`
|
|
4587
|
+
: `$${params.length}`;
|
|
4588
|
+
updates.push(`${column} = ${placeholder}`);
|
|
2231
4589
|
}
|
|
2232
|
-
param(params, value) {
|
|
4590
|
+
param(params, value, castType) {
|
|
2233
4591
|
params.push(value);
|
|
2234
|
-
return `$${params.length}`;
|
|
4592
|
+
return castType ? `$${params.length}::${castType}` : `$${params.length}`;
|
|
2235
4593
|
}
|
|
2236
4594
|
groupBy(items, key) {
|
|
2237
4595
|
return items.reduce((accumulator, item) => {
|
|
@@ -2255,9 +4613,14 @@ let OperationsService = class OperationsService {
|
|
|
2255
4613
|
}
|
|
2256
4614
|
};
|
|
2257
4615
|
exports.OperationsService = OperationsService;
|
|
2258
|
-
exports.OperationsService = OperationsService = __decorate([
|
|
4616
|
+
exports.OperationsService = OperationsService = OperationsService_1 = __decorate([
|
|
2259
4617
|
(0, common_1.Injectable)(),
|
|
4618
|
+
__param(3, (0, common_1.Inject)((0, common_1.forwardRef)(() => core_1.FileService))),
|
|
4619
|
+
__param(4, (0, common_1.Inject)((0, common_1.forwardRef)(() => core_1.SettingService))),
|
|
2260
4620
|
__metadata("design:paramtypes", [api_prisma_1.PrismaService,
|
|
2261
|
-
core_1.
|
|
4621
|
+
core_1.AiService,
|
|
4622
|
+
core_1.IntegrationDeveloperApiService,
|
|
4623
|
+
core_1.FileService,
|
|
4624
|
+
core_1.SettingService])
|
|
2262
4625
|
], OperationsService);
|
|
2263
4626
|
//# sourceMappingURL=operations.service.js.map
|