@hed-hog/contact 0.0.300 → 0.0.302

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (78) hide show
  1. package/dist/contact.module.d.ts.map +1 -1
  2. package/dist/contact.module.js +2 -0
  3. package/dist/contact.module.js.map +1 -1
  4. package/dist/index.d.ts +3 -0
  5. package/dist/index.d.ts.map +1 -1
  6. package/dist/index.js +3 -0
  7. package/dist/index.js.map +1 -1
  8. package/dist/person/person.service.d.ts +2 -0
  9. package/dist/person/person.service.d.ts.map +1 -1
  10. package/dist/person/person.service.js +111 -127
  11. package/dist/person/person.service.js.map +1 -1
  12. package/dist/person/person.service.spec.d.ts +2 -0
  13. package/dist/person/person.service.spec.d.ts.map +1 -0
  14. package/dist/person/person.service.spec.js +106 -0
  15. package/dist/person/person.service.spec.js.map +1 -0
  16. package/dist/proposal/dto/proposal.dto.d.ts +152 -0
  17. package/dist/proposal/dto/proposal.dto.d.ts.map +1 -0
  18. package/dist/proposal/dto/proposal.dto.js +396 -0
  19. package/dist/proposal/dto/proposal.dto.js.map +1 -0
  20. package/dist/proposal/proposal-contract.subscriber.d.ts +11 -0
  21. package/dist/proposal/proposal-contract.subscriber.d.ts.map +1 -0
  22. package/dist/proposal/proposal-contract.subscriber.js +51 -0
  23. package/dist/proposal/proposal-contract.subscriber.js.map +1 -0
  24. package/dist/proposal/proposal-event.types.d.ts +122 -0
  25. package/dist/proposal/proposal-event.types.d.ts.map +1 -0
  26. package/dist/proposal/proposal-event.types.js +13 -0
  27. package/dist/proposal/proposal-event.types.js.map +1 -0
  28. package/dist/proposal/proposal.controller.d.ts +56 -0
  29. package/dist/proposal/proposal.controller.d.ts.map +1 -0
  30. package/dist/proposal/proposal.controller.js +191 -0
  31. package/dist/proposal/proposal.controller.js.map +1 -0
  32. package/dist/proposal/proposal.module.d.ts +3 -0
  33. package/dist/proposal/proposal.module.d.ts.map +1 -0
  34. package/dist/proposal/proposal.module.js +32 -0
  35. package/dist/proposal/proposal.module.js.map +1 -0
  36. package/dist/proposal/proposal.service.d.ts +100 -0
  37. package/dist/proposal/proposal.service.d.ts.map +1 -0
  38. package/dist/proposal/proposal.service.js +2137 -0
  39. package/dist/proposal/proposal.service.js.map +1 -0
  40. package/dist/proposal/proposal.service.spec.d.ts +2 -0
  41. package/dist/proposal/proposal.service.spec.d.ts.map +1 -0
  42. package/dist/proposal/proposal.service.spec.js +175 -0
  43. package/dist/proposal/proposal.service.spec.js.map +1 -0
  44. package/hedhog/data/menu.yaml +35 -18
  45. package/hedhog/data/route.yaml +44 -0
  46. package/hedhog/frontend/app/accounts/_components/account-form-sheet.tsx.ejs +517 -346
  47. package/hedhog/frontend/app/activities/_components/activity-detail-sheet.tsx.ejs +42 -17
  48. package/hedhog/frontend/app/activities/_components/activity-types.ts.ejs +1 -1
  49. package/hedhog/frontend/app/activities/page.tsx.ejs +315 -101
  50. package/hedhog/frontend/app/follow-ups/page.tsx.ejs +172 -22
  51. package/hedhog/frontend/app/page.tsx.ejs +1 -1
  52. package/hedhog/frontend/app/person/_components/person-form-sheet.tsx.ejs +1 -1
  53. package/hedhog/frontend/app/pipeline/_components/lead-detail-sheet.tsx.ejs +253 -210
  54. package/hedhog/frontend/app/pipeline/_components/lead-proposals-tab.tsx.ejs +1661 -0
  55. package/hedhog/frontend/app/pipeline/page.tsx.ejs +30 -4
  56. package/hedhog/frontend/app/proposals/_components/proposals-management-page.tsx.ejs +773 -0
  57. package/hedhog/frontend/app/proposals/approvals/page.tsx.ejs +5 -0
  58. package/hedhog/frontend/app/proposals/page.tsx.ejs +5 -0
  59. package/hedhog/frontend/app/reports/page.tsx.ejs +431 -375
  60. package/hedhog/frontend/messages/en.json +236 -43
  61. package/hedhog/frontend/messages/pt.json +235 -42
  62. package/hedhog/table/proposal.yaml +112 -0
  63. package/hedhog/table/proposal_approval.yaml +63 -0
  64. package/hedhog/table/proposal_document.yaml +77 -0
  65. package/hedhog/table/proposal_item.yaml +64 -0
  66. package/hedhog/table/proposal_revision.yaml +78 -0
  67. package/package.json +5 -4
  68. package/src/contact.module.ts +2 -0
  69. package/src/index.ts +3 -0
  70. package/src/person/person.service.spec.ts +143 -0
  71. package/src/person/person.service.ts +147 -158
  72. package/src/proposal/dto/proposal.dto.ts +341 -0
  73. package/src/proposal/proposal-contract.subscriber.ts +43 -0
  74. package/src/proposal/proposal-event.types.ts +130 -0
  75. package/src/proposal/proposal.controller.ts +168 -0
  76. package/src/proposal/proposal.module.ts +19 -0
  77. package/src/proposal/proposal.service.spec.ts +196 -0
  78. package/src/proposal/proposal.service.ts +2855 -0
@@ -0,0 +1,2137 @@
1
+ "use strict";
2
+ var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
3
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
4
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
5
+ else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
6
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
7
+ };
8
+ var __metadata = (this && this.__metadata) || function (k, v) {
9
+ if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
10
+ };
11
+ var __param = (this && this.__param) || function (paramIndex, decorator) {
12
+ return function (target, key) { decorator(target, key, paramIndex); }
13
+ };
14
+ var ProposalService_1;
15
+ Object.defineProperty(exports, "__esModule", { value: true });
16
+ exports.ProposalService = void 0;
17
+ const api_locale_1 = require("@hed-hog/api-locale");
18
+ const api_prisma_1 = require("@hed-hog/api-prisma");
19
+ const core_1 = require("@hed-hog/core");
20
+ const common_1 = require("@nestjs/common");
21
+ const proposal_dto_1 = require("./dto/proposal.dto");
22
+ const proposal_event_types_1 = require("./proposal-event.types");
23
+ let ProposalService = ProposalService_1 = class ProposalService {
24
+ constructor(prisma, integrationApi, fileService, settingService) {
25
+ this.prisma = prisma;
26
+ this.integrationApi = integrationApi;
27
+ this.fileService = fileService;
28
+ this.settingService = settingService;
29
+ this.logger = new common_1.Logger(ProposalService_1.name);
30
+ }
31
+ async list(params) {
32
+ const take = Math.max(1, Number(params.take || params.pageSize || 10));
33
+ const skip = Math.max(0, Number(params.skip || 0));
34
+ const search = String(params.search || '').trim();
35
+ const where = {
36
+ deleted_at: null,
37
+ };
38
+ if (params.person_id) {
39
+ where.person_id = Number(params.person_id);
40
+ }
41
+ if (params.status) {
42
+ where.status = params.status;
43
+ }
44
+ if (search) {
45
+ const matchingCompanyRows = await this.prisma.person_company.findMany({
46
+ where: {
47
+ trade_name: { contains: search, mode: 'insensitive' },
48
+ },
49
+ select: {
50
+ id: true,
51
+ },
52
+ });
53
+ const matchingCompanyPersonIds = matchingCompanyRows
54
+ .map((item) => Number(item.id))
55
+ .filter((id) => id > 0);
56
+ where.OR = [
57
+ { code: { contains: search, mode: 'insensitive' } },
58
+ { title: { contains: search, mode: 'insensitive' } },
59
+ {
60
+ person: {
61
+ is: {
62
+ name: { contains: search, mode: 'insensitive' },
63
+ },
64
+ },
65
+ },
66
+ ...(matchingCompanyPersonIds.length > 0
67
+ ? [{ person_id: { in: matchingCompanyPersonIds } }]
68
+ : []),
69
+ ];
70
+ }
71
+ const [data, total] = await Promise.all([
72
+ this.prisma.proposal.findMany({
73
+ where,
74
+ include: {
75
+ person: {
76
+ select: {
77
+ id: true,
78
+ name: true,
79
+ },
80
+ },
81
+ proposal_revision: {
82
+ where: {
83
+ deleted_at: null,
84
+ is_current: true,
85
+ },
86
+ orderBy: {
87
+ revision_number: 'desc',
88
+ },
89
+ take: 1,
90
+ include: {
91
+ proposal_item: {
92
+ where: { deleted_at: null },
93
+ orderBy: { order: 'asc' },
94
+ },
95
+ },
96
+ },
97
+ },
98
+ orderBy: {
99
+ id: 'desc',
100
+ },
101
+ skip,
102
+ take,
103
+ }),
104
+ this.prisma.proposal.count({ where }),
105
+ ]);
106
+ const normalizedData = await this.attachPersonTradeNames(this.prisma, data);
107
+ const page = Math.floor(skip / take) + 1;
108
+ const lastPage = Math.max(1, Math.ceil(total / take));
109
+ return {
110
+ data: normalizedData,
111
+ total,
112
+ page,
113
+ pageSize: take,
114
+ prev: page > 1 ? page - 1 : null,
115
+ next: page < lastPage ? page + 1 : null,
116
+ lastPage,
117
+ };
118
+ }
119
+ async getStats() {
120
+ const baseWhere = { deleted_at: null };
121
+ const [total, draft, pendingApproval, approved, rejected, contractGenerated,] = await Promise.all([
122
+ this.prisma.proposal.count({ where: baseWhere }),
123
+ this.prisma.proposal.count({
124
+ where: Object.assign(Object.assign({}, baseWhere), { status: proposal_dto_1.ProposalStatus.DRAFT }),
125
+ }),
126
+ this.prisma.proposal.count({
127
+ where: Object.assign(Object.assign({}, baseWhere), { status: proposal_dto_1.ProposalStatus.PENDING_APPROVAL }),
128
+ }),
129
+ this.prisma.proposal.count({
130
+ where: Object.assign(Object.assign({}, baseWhere), { status: proposal_dto_1.ProposalStatus.APPROVED }),
131
+ }),
132
+ this.prisma.proposal.count({
133
+ where: Object.assign(Object.assign({}, baseWhere), { status: proposal_dto_1.ProposalStatus.REJECTED }),
134
+ }),
135
+ this.prisma.proposal.count({
136
+ where: Object.assign(Object.assign({}, baseWhere), { status: proposal_dto_1.ProposalStatus.CONTRACT_GENERATED }),
137
+ }),
138
+ ]);
139
+ return {
140
+ total,
141
+ draft,
142
+ pendingApproval,
143
+ approved,
144
+ rejected,
145
+ contractGenerated,
146
+ };
147
+ }
148
+ async getById(id, locale) {
149
+ const proposal = await this.loadProposalDetail(this.prisma, id);
150
+ if (!proposal) {
151
+ throw new common_1.NotFoundException((0, api_locale_1.getLocaleText)('itemNotFound', locale, 'Proposal not found'));
152
+ }
153
+ const links = await this.integrationApi.findLinksBySource({
154
+ module: 'contact',
155
+ entityType: 'proposal',
156
+ entityId: String(id),
157
+ });
158
+ return Object.assign(Object.assign({}, proposal), { integration_links: links });
159
+ }
160
+ async create(data, locale, userId) {
161
+ await this.assertPersonExists(data.person_id, locale);
162
+ const code = await this.resolveProposalCode(data.code);
163
+ const normalizedItems = this.normalizeItems(data.items);
164
+ const totals = this.resolveTotals(data, normalizedItems);
165
+ const createdByUserId = userId || null;
166
+ const createdProposal = await this.prisma.$transaction(async (tx) => {
167
+ var _a, _b, _c, _d, _e, _f, _g;
168
+ const proposal = await tx.proposal.create({
169
+ data: Object.assign({ person_id: data.person_id, code, title: data.title.trim(), status: (_a = data.status) !== null && _a !== void 0 ? _a : proposal_dto_1.ProposalStatus.DRAFT, contract_category: (_b = data.contract_category) !== null && _b !== void 0 ? _b : 'client', contract_type: (_c = data.contract_type) !== null && _c !== void 0 ? _c : 'service_agreement', billing_model: (_d = data.billing_model) !== null && _d !== void 0 ? _d : 'fixed_price', currency_code: (data.currency_code || 'BRL').trim() || 'BRL', valid_from: data.valid_from ? new Date(data.valid_from) : null, valid_until: data.valid_until ? new Date(data.valid_until) : null, notes: this.normalizeOptionalText(data.notes), owner_user_id: (_e = data.owner_user_id) !== null && _e !== void 0 ? _e : null, created_by_user_id: createdByUserId, updated_by_user_id: createdByUserId, current_revision_number: 1 }, totals),
170
+ });
171
+ const revision = await tx.proposal_revision.create({
172
+ data: Object.assign({ proposal_id: proposal.id, revision_number: 1, status: proposal.status === proposal_dto_1.ProposalStatus.PENDING_APPROVAL
173
+ ? proposal_dto_1.ProposalStatus.PENDING_APPROVAL
174
+ : proposal_dto_1.ProposalStatus.DRAFT, generation_mode: (_f = data.generation_mode) !== null && _f !== void 0 ? _f : 'manual', title: data.title.trim(), summary: this.normalizeOptionalText(data.summary), content_html: this.normalizeOptionalText(data.content_html), snapshot_json: (_g = data.snapshot_json) !== null && _g !== void 0 ? _g : null, valid_until: data.valid_until ? new Date(data.valid_until) : null, is_current: true, created_by_user_id: createdByUserId }, totals),
175
+ });
176
+ if (normalizedItems.length > 0) {
177
+ await tx.proposal_item.createMany({
178
+ data: normalizedItems.map((item, index) => {
179
+ var _a;
180
+ return ({
181
+ proposal_revision_id: revision.id,
182
+ order: index,
183
+ item_type: item.item_type,
184
+ term_type: item.term_type,
185
+ name: item.name,
186
+ description: item.description,
187
+ quantity: item.quantity,
188
+ unit_amount_cents: item.unit_amount_cents,
189
+ total_amount_cents: item.total_amount_cents,
190
+ recurrence: item.recurrence,
191
+ due_day: item.due_day,
192
+ start_date: item.start_date ? new Date(item.start_date) : null,
193
+ end_date: item.end_date ? new Date(item.end_date) : null,
194
+ metadata_json: (_a = item.metadata_json) !== null && _a !== void 0 ? _a : null,
195
+ });
196
+ }),
197
+ });
198
+ }
199
+ const documents = this.normalizeDocuments(data.documents, proposal.id, revision.id, createdByUserId);
200
+ if (documents.length > 0) {
201
+ await tx.proposal_document.createMany({
202
+ data: documents,
203
+ });
204
+ }
205
+ await this.publishProposalLifecycleEvent(tx, proposal_event_types_1.PROPOSAL_EVENT_NAMES.CREATED, proposal.id, locale, createdByUserId);
206
+ return proposal;
207
+ });
208
+ await this.syncPersonDealValueFromApprovedProposals(this.prisma, createdProposal.person_id);
209
+ return this.getById(createdProposal.id, locale);
210
+ }
211
+ async update(id, data, locale, userId) {
212
+ var _a, _b;
213
+ const current = await this.loadProposalDetail(this.prisma, id);
214
+ if (!current) {
215
+ throw new common_1.NotFoundException((0, api_locale_1.getLocaleText)('itemNotFound', locale, 'Proposal not found'));
216
+ }
217
+ this.assertCanEdit(current.status, locale);
218
+ const normalizedItems = data.items
219
+ ? this.normalizeItems(data.items)
220
+ : (((_b = (_a = current.proposal_revision) === null || _a === void 0 ? void 0 : _a.find((revision) => revision.is_current)) === null || _b === void 0 ? void 0 : _b.proposal_item) || []);
221
+ const totals = this.resolveTotals(data, normalizedItems);
222
+ await this.prisma.$transaction(async (tx) => {
223
+ var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p;
224
+ const currentRevision = await tx.proposal_revision.findFirst({
225
+ where: {
226
+ proposal_id: id,
227
+ deleted_at: null,
228
+ is_current: true,
229
+ },
230
+ orderBy: {
231
+ revision_number: 'desc',
232
+ },
233
+ });
234
+ if (!currentRevision) {
235
+ throw new common_1.NotFoundException('Current proposal revision not found.');
236
+ }
237
+ const shouldCreateNewRevision = Boolean(data.create_new_revision);
238
+ await tx.proposal.update({
239
+ where: { id },
240
+ data: Object.assign({ title: (_b = (_a = data.title) === null || _a === void 0 ? void 0 : _a.trim()) !== null && _b !== void 0 ? _b : undefined, contract_category: (_c = data.contract_category) !== null && _c !== void 0 ? _c : undefined, contract_type: (_d = data.contract_type) !== null && _d !== void 0 ? _d : undefined, billing_model: (_e = data.billing_model) !== null && _e !== void 0 ? _e : undefined, currency_code: (_g = (_f = data.currency_code) === null || _f === void 0 ? void 0 : _f.trim()) !== null && _g !== void 0 ? _g : undefined, valid_from: data.valid_from ? new Date(data.valid_from) : undefined, valid_until: data.valid_until ? new Date(data.valid_until) : undefined, notes: data.notes !== undefined ? this.normalizeOptionalText(data.notes) : undefined, owner_user_id: (_h = data.owner_user_id) !== null && _h !== void 0 ? _h : undefined, updated_by_user_id: userId || null, current_revision_number: shouldCreateNewRevision
241
+ ? currentRevision.revision_number + 1
242
+ : currentRevision.revision_number }, totals),
243
+ });
244
+ if (shouldCreateNewRevision) {
245
+ await tx.proposal_revision.updateMany({
246
+ where: {
247
+ proposal_id: id,
248
+ deleted_at: null,
249
+ is_current: true,
250
+ },
251
+ data: {
252
+ is_current: false,
253
+ status: currentRevision.status === proposal_dto_1.ProposalStatus.DRAFT
254
+ ? 'superseded'
255
+ : currentRevision.status,
256
+ },
257
+ });
258
+ const newRevision = await tx.proposal_revision.create({
259
+ data: Object.assign({ proposal_id: id, revision_number: currentRevision.revision_number + 1, status: proposal_dto_1.ProposalStatus.DRAFT, generation_mode: (_j = data.generation_mode) !== null && _j !== void 0 ? _j : currentRevision.generation_mode, title: (_l = (_k = data.title) === null || _k === void 0 ? void 0 : _k.trim()) !== null && _l !== void 0 ? _l : currentRevision.title, summary: data.summary !== undefined
260
+ ? this.normalizeOptionalText(data.summary)
261
+ : currentRevision.summary, content_html: data.content_html !== undefined
262
+ ? this.normalizeOptionalText(data.content_html)
263
+ : currentRevision.content_html, snapshot_json: data.snapshot_json !== undefined
264
+ ? data.snapshot_json
265
+ : currentRevision.snapshot_json, valid_until: data.valid_until !== undefined
266
+ ? data.valid_until
267
+ ? new Date(data.valid_until)
268
+ : null
269
+ : currentRevision.valid_until, is_current: true, created_by_user_id: userId || null }, totals),
270
+ });
271
+ if (data.items) {
272
+ await tx.proposal_item.createMany({
273
+ data: normalizedItems.map((item, index) => {
274
+ var _a;
275
+ return ({
276
+ proposal_revision_id: newRevision.id,
277
+ order: index,
278
+ item_type: item.item_type,
279
+ term_type: item.term_type,
280
+ name: item.name,
281
+ description: item.description,
282
+ quantity: item.quantity,
283
+ unit_amount_cents: item.unit_amount_cents,
284
+ total_amount_cents: item.total_amount_cents,
285
+ recurrence: item.recurrence,
286
+ due_day: item.due_day,
287
+ start_date: item.start_date ? new Date(item.start_date) : null,
288
+ end_date: item.end_date ? new Date(item.end_date) : null,
289
+ metadata_json: (_a = item.metadata_json) !== null && _a !== void 0 ? _a : null,
290
+ });
291
+ }),
292
+ });
293
+ }
294
+ if (data.documents) {
295
+ const documents = this.normalizeDocuments(data.documents, id, newRevision.id, userId || null);
296
+ if (documents.length > 0) {
297
+ await tx.proposal_document.createMany({ data: documents });
298
+ }
299
+ }
300
+ }
301
+ else {
302
+ await tx.proposal_revision.update({
303
+ where: { id: currentRevision.id },
304
+ data: Object.assign({ title: (_o = (_m = data.title) === null || _m === void 0 ? void 0 : _m.trim()) !== null && _o !== void 0 ? _o : undefined, summary: data.summary !== undefined
305
+ ? this.normalizeOptionalText(data.summary)
306
+ : undefined, content_html: data.content_html !== undefined
307
+ ? this.normalizeOptionalText(data.content_html)
308
+ : undefined, snapshot_json: data.snapshot_json !== undefined ? data.snapshot_json : undefined, valid_until: data.valid_until !== undefined
309
+ ? data.valid_until
310
+ ? new Date(data.valid_until)
311
+ : null
312
+ : undefined, generation_mode: (_p = data.generation_mode) !== null && _p !== void 0 ? _p : undefined }, totals),
313
+ });
314
+ if (data.items) {
315
+ await tx.proposal_item.updateMany({
316
+ where: {
317
+ proposal_revision_id: currentRevision.id,
318
+ deleted_at: null,
319
+ },
320
+ data: {
321
+ deleted_at: new Date(),
322
+ },
323
+ });
324
+ if (normalizedItems.length > 0) {
325
+ await tx.proposal_item.createMany({
326
+ data: normalizedItems.map((item, index) => {
327
+ var _a;
328
+ return ({
329
+ proposal_revision_id: currentRevision.id,
330
+ order: index,
331
+ item_type: item.item_type,
332
+ term_type: item.term_type,
333
+ name: item.name,
334
+ description: item.description,
335
+ quantity: item.quantity,
336
+ unit_amount_cents: item.unit_amount_cents,
337
+ total_amount_cents: item.total_amount_cents,
338
+ recurrence: item.recurrence,
339
+ due_day: item.due_day,
340
+ start_date: item.start_date ? new Date(item.start_date) : null,
341
+ end_date: item.end_date ? new Date(item.end_date) : null,
342
+ metadata_json: (_a = item.metadata_json) !== null && _a !== void 0 ? _a : null,
343
+ });
344
+ }),
345
+ });
346
+ }
347
+ }
348
+ if (data.documents) {
349
+ await tx.proposal_document.updateMany({
350
+ where: {
351
+ proposal_id: id,
352
+ proposal_revision_id: currentRevision.id,
353
+ deleted_at: null,
354
+ },
355
+ data: {
356
+ deleted_at: new Date(),
357
+ is_current: false,
358
+ },
359
+ });
360
+ const documents = this.normalizeDocuments(data.documents, id, currentRevision.id, userId || null);
361
+ if (documents.length > 0) {
362
+ await tx.proposal_document.createMany({ data: documents });
363
+ }
364
+ }
365
+ }
366
+ });
367
+ await this.syncPersonDealValueFromApprovedProposals(this.prisma, current.person_id);
368
+ return this.getById(id, locale);
369
+ }
370
+ async submitForApproval(id, data, locale, userId) {
371
+ const current = await this.loadProposalDetail(this.prisma, id);
372
+ if (!current) {
373
+ throw new common_1.NotFoundException((0, api_locale_1.getLocaleText)('itemNotFound', locale, 'Proposal not found'));
374
+ }
375
+ if (current.status === proposal_dto_1.ProposalStatus.APPROVED ||
376
+ current.status === proposal_dto_1.ProposalStatus.CONTRACT_GENERATED) {
377
+ throw new common_1.BadRequestException('Approved proposals cannot be resubmitted.');
378
+ }
379
+ if (current.status === proposal_dto_1.ProposalStatus.PENDING_APPROVAL) {
380
+ return this.getById(id, locale);
381
+ }
382
+ if (current.status === proposal_dto_1.ProposalStatus.CANCELLED) {
383
+ throw new common_1.BadRequestException('Cancelled proposals cannot be resubmitted.');
384
+ }
385
+ this.assertProposalWorkflowReadiness(current, 'submitted for approval');
386
+ await this.prisma.$transaction(async (tx) => {
387
+ var _a, _b;
388
+ const lockedProposal = await this.lockProposalForTransition(tx, id);
389
+ if (!lockedProposal) {
390
+ throw new common_1.NotFoundException('Proposal not found.');
391
+ }
392
+ if (lockedProposal.status === proposal_dto_1.ProposalStatus.APPROVED ||
393
+ lockedProposal.status === proposal_dto_1.ProposalStatus.CONTRACT_GENERATED) {
394
+ throw new common_1.BadRequestException('Approved proposals cannot be resubmitted.');
395
+ }
396
+ if (lockedProposal.status === proposal_dto_1.ProposalStatus.PENDING_APPROVAL) {
397
+ return;
398
+ }
399
+ if (lockedProposal.status === proposal_dto_1.ProposalStatus.CANCELLED) {
400
+ throw new common_1.BadRequestException('Cancelled proposals cannot be resubmitted.');
401
+ }
402
+ this.assertProposalWorkflowReadiness(lockedProposal, 'submitted for approval');
403
+ const currentRevision = this.getCurrentProposalRevision(lockedProposal);
404
+ if (!currentRevision) {
405
+ throw new common_1.NotFoundException('Current proposal revision not found.');
406
+ }
407
+ await tx.proposal.update({
408
+ where: { id },
409
+ data: {
410
+ status: proposal_dto_1.ProposalStatus.PENDING_APPROVAL,
411
+ updated_by_user_id: userId || null,
412
+ },
413
+ });
414
+ await tx.proposal_revision.update({
415
+ where: { id: currentRevision.id },
416
+ data: {
417
+ status: proposal_dto_1.ProposalStatus.PENDING_APPROVAL,
418
+ submitted_at: new Date(),
419
+ },
420
+ });
421
+ const existingApproval = await tx.proposal_approval.findFirst({
422
+ where: {
423
+ proposal_id: id,
424
+ proposal_revision_id: currentRevision.id,
425
+ deleted_at: null,
426
+ step_order: 1,
427
+ },
428
+ });
429
+ if (existingApproval) {
430
+ await tx.proposal_approval.update({
431
+ where: { id: existingApproval.id },
432
+ data: {
433
+ requester_user_id: userId || null,
434
+ approver_user_id: (_a = data.approver_user_id) !== null && _a !== void 0 ? _a : existingApproval.approver_user_id,
435
+ status: 'pending',
436
+ submitted_at: new Date(),
437
+ decided_at: null,
438
+ decision_note: this.normalizeOptionalText(data.note),
439
+ },
440
+ });
441
+ }
442
+ else {
443
+ await tx.proposal_approval.create({
444
+ data: {
445
+ proposal_id: id,
446
+ proposal_revision_id: currentRevision.id,
447
+ requester_user_id: userId || null,
448
+ approver_user_id: (_b = data.approver_user_id) !== null && _b !== void 0 ? _b : null,
449
+ step_order: 1,
450
+ status: 'pending',
451
+ submitted_at: new Date(),
452
+ decision_note: this.normalizeOptionalText(data.note),
453
+ },
454
+ });
455
+ }
456
+ await this.publishProposalLifecycleEvent(tx, proposal_event_types_1.PROPOSAL_EVENT_NAMES.SENT, id, locale, userId || null, {
457
+ note: this.normalizeOptionalText(data.note),
458
+ metadata: {
459
+ trigger: 'submit_for_approval',
460
+ },
461
+ });
462
+ });
463
+ return this.getById(id, locale);
464
+ }
465
+ async approve(id, data, locale, userId) {
466
+ const current = await this.loadProposalDetail(this.prisma, id);
467
+ if (!current) {
468
+ throw new common_1.NotFoundException((0, api_locale_1.getLocaleText)('itemNotFound', locale, 'Proposal not found'));
469
+ }
470
+ if (current.status === proposal_dto_1.ProposalStatus.APPROVED ||
471
+ current.status === proposal_dto_1.ProposalStatus.CONTRACT_GENERATED) {
472
+ return this.getById(id, locale);
473
+ }
474
+ if (current.status !== proposal_dto_1.ProposalStatus.PENDING_APPROVAL) {
475
+ throw new common_1.BadRequestException('Only proposals pending approval can be approved.');
476
+ }
477
+ this.assertProposalWorkflowReadiness(current, 'approved');
478
+ await this.prisma.$transaction(async (tx) => {
479
+ const lockedProposal = await this.lockProposalForTransition(tx, id);
480
+ if (!lockedProposal) {
481
+ throw new common_1.NotFoundException('Proposal not found.');
482
+ }
483
+ if (lockedProposal.status === proposal_dto_1.ProposalStatus.APPROVED ||
484
+ lockedProposal.status === proposal_dto_1.ProposalStatus.CONTRACT_GENERATED) {
485
+ return;
486
+ }
487
+ if (lockedProposal.status !== proposal_dto_1.ProposalStatus.PENDING_APPROVAL) {
488
+ throw new common_1.BadRequestException('Only proposals pending approval can be approved.');
489
+ }
490
+ this.assertProposalWorkflowReadiness(lockedProposal, 'approved');
491
+ const currentRevision = this.getCurrentProposalRevision(lockedProposal);
492
+ if (!currentRevision) {
493
+ throw new common_1.NotFoundException('Current proposal revision not found.');
494
+ }
495
+ const approval = await tx.proposal_approval.findFirst({
496
+ where: {
497
+ proposal_id: id,
498
+ proposal_revision_id: currentRevision.id,
499
+ deleted_at: null,
500
+ step_order: 1,
501
+ },
502
+ });
503
+ const approvedAt = new Date();
504
+ if (approval) {
505
+ await tx.proposal_approval.update({
506
+ where: { id: approval.id },
507
+ data: {
508
+ approver_user_id: userId || approval.approver_user_id || null,
509
+ status: 'approved',
510
+ decided_at: approvedAt,
511
+ decision_note: this.normalizeOptionalText(data.note),
512
+ },
513
+ });
514
+ }
515
+ else {
516
+ await tx.proposal_approval.create({
517
+ data: {
518
+ proposal_id: id,
519
+ proposal_revision_id: currentRevision.id,
520
+ requester_user_id: null,
521
+ approver_user_id: userId || null,
522
+ step_order: 1,
523
+ status: 'approved',
524
+ submitted_at: approvedAt,
525
+ decided_at: approvedAt,
526
+ decision_note: this.normalizeOptionalText(data.note),
527
+ },
528
+ });
529
+ }
530
+ await tx.proposal_revision.update({
531
+ where: { id: currentRevision.id },
532
+ data: {
533
+ status: proposal_dto_1.ProposalStatus.APPROVED,
534
+ approved_at: approvedAt,
535
+ },
536
+ });
537
+ await tx.proposal.update({
538
+ where: { id },
539
+ data: {
540
+ status: proposal_dto_1.ProposalStatus.APPROVED,
541
+ approved_at: approvedAt,
542
+ approved_by_user_id: userId || null,
543
+ updated_by_user_id: userId || null,
544
+ },
545
+ });
546
+ await this.publishProposalLifecycleEvent(tx, proposal_event_types_1.PROPOSAL_EVENT_NAMES.APPROVED, id, locale, userId || null, {
547
+ note: this.normalizeOptionalText(data.note),
548
+ });
549
+ await this.publishProposalLifecycleEvent(tx, proposal_event_types_1.PROPOSAL_EVENT_NAMES.CONVERT_REQUESTED, id, locale, userId || null, {
550
+ note: this.normalizeOptionalText(data.note),
551
+ metadata: {
552
+ trigger: 'approval',
553
+ },
554
+ });
555
+ });
556
+ await this.syncPersonDealValueFromApprovedProposals(this.prisma, current.person_id);
557
+ return this.getById(id, locale);
558
+ }
559
+ async reject(id, data, locale, userId) {
560
+ const current = await this.loadProposalDetail(this.prisma, id);
561
+ if (!current) {
562
+ throw new common_1.NotFoundException((0, api_locale_1.getLocaleText)('itemNotFound', locale, 'Proposal not found'));
563
+ }
564
+ if (current.status === proposal_dto_1.ProposalStatus.REJECTED ||
565
+ current.status === proposal_dto_1.ProposalStatus.CANCELLED ||
566
+ current.status === proposal_dto_1.ProposalStatus.CONTRACT_GENERATED) {
567
+ return this.getById(id, locale);
568
+ }
569
+ if (current.status !== proposal_dto_1.ProposalStatus.PENDING_APPROVAL) {
570
+ throw new common_1.BadRequestException('Only proposals pending approval can be rejected.');
571
+ }
572
+ await this.prisma.$transaction(async (tx) => {
573
+ const lockedProposal = await this.lockProposalForTransition(tx, id);
574
+ if (!lockedProposal) {
575
+ throw new common_1.NotFoundException('Proposal not found.');
576
+ }
577
+ if (lockedProposal.status === proposal_dto_1.ProposalStatus.REJECTED ||
578
+ lockedProposal.status === proposal_dto_1.ProposalStatus.CANCELLED ||
579
+ lockedProposal.status === proposal_dto_1.ProposalStatus.CONTRACT_GENERATED) {
580
+ return;
581
+ }
582
+ if (lockedProposal.status !== proposal_dto_1.ProposalStatus.PENDING_APPROVAL) {
583
+ throw new common_1.BadRequestException('Only proposals pending approval can be rejected.');
584
+ }
585
+ const currentRevision = this.getCurrentProposalRevision(lockedProposal);
586
+ if (!currentRevision) {
587
+ throw new common_1.NotFoundException('Current proposal revision not found.');
588
+ }
589
+ const approval = await tx.proposal_approval.findFirst({
590
+ where: {
591
+ proposal_id: id,
592
+ proposal_revision_id: currentRevision.id,
593
+ deleted_at: null,
594
+ step_order: 1,
595
+ },
596
+ });
597
+ const decidedAt = new Date();
598
+ if (approval) {
599
+ await tx.proposal_approval.update({
600
+ where: { id: approval.id },
601
+ data: {
602
+ approver_user_id: userId || approval.approver_user_id || null,
603
+ status: 'rejected',
604
+ decided_at: decidedAt,
605
+ decision_note: this.normalizeOptionalText(data.note),
606
+ },
607
+ });
608
+ }
609
+ else {
610
+ await tx.proposal_approval.create({
611
+ data: {
612
+ proposal_id: id,
613
+ proposal_revision_id: currentRevision.id,
614
+ requester_user_id: null,
615
+ approver_user_id: userId || null,
616
+ step_order: 1,
617
+ status: 'rejected',
618
+ submitted_at: decidedAt,
619
+ decided_at: decidedAt,
620
+ decision_note: this.normalizeOptionalText(data.note),
621
+ },
622
+ });
623
+ }
624
+ await tx.proposal_revision.update({
625
+ where: { id: currentRevision.id },
626
+ data: {
627
+ status: proposal_dto_1.ProposalStatus.REJECTED,
628
+ },
629
+ });
630
+ await tx.proposal.update({
631
+ where: { id },
632
+ data: {
633
+ status: proposal_dto_1.ProposalStatus.REJECTED,
634
+ updated_by_user_id: userId || null,
635
+ },
636
+ });
637
+ await this.publishProposalLifecycleEvent(tx, proposal_event_types_1.PROPOSAL_EVENT_NAMES.REJECTED, id, locale, userId || null, {
638
+ note: this.normalizeOptionalText(data.note),
639
+ });
640
+ });
641
+ await this.syncPersonDealValueFromApprovedProposals(this.prisma, current.person_id);
642
+ return this.getById(id, locale);
643
+ }
644
+ async cancel(id, data, locale, userId) {
645
+ const current = await this.loadProposalDetail(this.prisma, id);
646
+ if (!current) {
647
+ throw new common_1.NotFoundException((0, api_locale_1.getLocaleText)('itemNotFound', locale, 'Proposal not found'));
648
+ }
649
+ if (current.status === proposal_dto_1.ProposalStatus.CANCELLED) {
650
+ return this.getById(id, locale);
651
+ }
652
+ if (current.status === proposal_dto_1.ProposalStatus.CONTRACT_GENERATED) {
653
+ throw new common_1.BadRequestException('Converted proposals can no longer be cancelled.');
654
+ }
655
+ await this.prisma.$transaction(async (tx) => {
656
+ const currentRevision = await tx.proposal_revision.findFirst({
657
+ where: {
658
+ proposal_id: id,
659
+ deleted_at: null,
660
+ is_current: true,
661
+ },
662
+ orderBy: {
663
+ revision_number: 'desc',
664
+ },
665
+ });
666
+ if (!currentRevision) {
667
+ throw new common_1.NotFoundException('Current proposal revision not found.');
668
+ }
669
+ await tx.proposal_revision.update({
670
+ where: { id: currentRevision.id },
671
+ data: {
672
+ status: proposal_dto_1.ProposalStatus.CANCELLED,
673
+ },
674
+ });
675
+ await tx.proposal_approval.updateMany({
676
+ where: {
677
+ proposal_id: id,
678
+ proposal_revision_id: currentRevision.id,
679
+ deleted_at: null,
680
+ status: 'pending',
681
+ },
682
+ data: {
683
+ status: 'cancelled',
684
+ decided_at: new Date(),
685
+ decision_note: this.normalizeOptionalText(data.note),
686
+ },
687
+ });
688
+ await tx.proposal.update({
689
+ where: { id },
690
+ data: {
691
+ status: proposal_dto_1.ProposalStatus.CANCELLED,
692
+ updated_by_user_id: userId || null,
693
+ },
694
+ });
695
+ await this.publishProposalLifecycleEvent(tx, proposal_event_types_1.PROPOSAL_EVENT_NAMES.CANCELLED, id, locale, userId || null, {
696
+ note: this.normalizeOptionalText(data.note),
697
+ metadata: {
698
+ trigger: 'manual_cancel',
699
+ },
700
+ });
701
+ });
702
+ await this.syncPersonDealValueFromApprovedProposals(this.prisma, current.person_id);
703
+ return this.getById(id, locale);
704
+ }
705
+ async requestConversion(id, data, locale, userId) {
706
+ var _a, _b;
707
+ const current = await this.loadProposalDetail(this.prisma, id);
708
+ if (!current) {
709
+ throw new common_1.NotFoundException((0, api_locale_1.getLocaleText)('itemNotFound', locale, 'Proposal not found'));
710
+ }
711
+ if (current.status === proposal_dto_1.ProposalStatus.CONTRACT_GENERATED) {
712
+ return this.getById(id, locale);
713
+ }
714
+ if (current.status !== proposal_dto_1.ProposalStatus.APPROVED) {
715
+ throw new common_1.BadRequestException('Only approved proposals can request conversion.');
716
+ }
717
+ this.assertProposalWorkflowReadiness(current, 'converted to contracts');
718
+ const existingLinks = await this.integrationApi.findLinksBySource({
719
+ module: 'contact',
720
+ entityType: 'proposal',
721
+ entityId: String(id),
722
+ });
723
+ const existingContractLink = existingLinks.find((link) => link.targetModule === 'operations' &&
724
+ link.targetEntityType === 'contract');
725
+ if (existingContractLink) {
726
+ this.logger.debug(`[proposal-event] conversion already linked for proposal ${id} -> contract ${existingContractLink.targetEntityId}`);
727
+ return this.markConvertedFromIntegration({
728
+ proposalId: id,
729
+ locale,
730
+ createdByUserId: userId || null,
731
+ correlationId: `proposal:${id}:v${current.current_revision_number || 1}`,
732
+ contractId: Number(existingContractLink.targetEntityId || 0) || null,
733
+ contract: {
734
+ id: Number(existingContractLink.targetEntityId || 0) || null,
735
+ code: (_b = (_a = existingContractLink.metadata) === null || _a === void 0 ? void 0 : _a.contractCode) !== null && _b !== void 0 ? _b : null,
736
+ name: null,
737
+ status: 'draft',
738
+ url: null,
739
+ },
740
+ });
741
+ }
742
+ await this.prisma.$transaction(async (tx) => {
743
+ const lockedProposal = await this.lockProposalForTransition(tx, id);
744
+ if (!lockedProposal) {
745
+ throw new common_1.NotFoundException('Proposal not found.');
746
+ }
747
+ if (lockedProposal.status === proposal_dto_1.ProposalStatus.CONTRACT_GENERATED) {
748
+ return;
749
+ }
750
+ if (lockedProposal.status !== proposal_dto_1.ProposalStatus.APPROVED) {
751
+ throw new common_1.BadRequestException('Only approved proposals can request conversion.');
752
+ }
753
+ this.assertProposalWorkflowReadiness(lockedProposal, 'converted to contracts');
754
+ await this.publishProposalLifecycleEvent(tx, proposal_event_types_1.PROPOSAL_EVENT_NAMES.CONVERT_REQUESTED, id, locale, userId || null, {
755
+ note: this.normalizeOptionalText(data.note),
756
+ metadata: {
757
+ trigger: 'manual_conversion_request',
758
+ },
759
+ });
760
+ });
761
+ return this.getById(id, locale);
762
+ }
763
+ async markConvertedFromIntegration(payload) {
764
+ var _a, _b;
765
+ const proposalId = Number((payload === null || payload === void 0 ? void 0 : payload.proposalId) || ((_a = payload === null || payload === void 0 ? void 0 : payload.contract) === null || _a === void 0 ? void 0 : _a.originId) || 0);
766
+ if (!Number.isInteger(proposalId) || proposalId <= 0) {
767
+ this.logger.warn('[proposal-event] ignoring conversion callback without a valid proposalId');
768
+ return null;
769
+ }
770
+ const locale = String((payload === null || payload === void 0 ? void 0 : payload.locale) || 'en').trim() || 'en';
771
+ const current = await this.loadProposalDetail(this.prisma, proposalId);
772
+ if (!current) {
773
+ this.logger.warn(`[proposal-event] proposal ${proposalId} was not found when marking it as converted`);
774
+ return null;
775
+ }
776
+ if (current.status === proposal_dto_1.ProposalStatus.CONTRACT_GENERATED) {
777
+ this.logger.debug(`[proposal-event] proposal ${proposalId} is already marked as converted`);
778
+ return this.getById(proposalId, locale);
779
+ }
780
+ const contractId = Number((payload === null || payload === void 0 ? void 0 : payload.contractId) || ((_b = payload === null || payload === void 0 ? void 0 : payload.contract) === null || _b === void 0 ? void 0 : _b.id) || 0);
781
+ const actorUserId = Number((payload === null || payload === void 0 ? void 0 : payload.createdByUserId) || (payload === null || payload === void 0 ? void 0 : payload.activatedByUserId) || 0) ||
782
+ null;
783
+ await this.prisma.$transaction(async (tx) => {
784
+ var _a, _b, _c, _d, _e, _f, _g, _h;
785
+ const lockedProposal = await this.lockProposalForTransition(tx, proposalId);
786
+ if (!lockedProposal) {
787
+ this.logger.warn(`[proposal-event] proposal ${proposalId} was not found when locking it for conversion`);
788
+ return;
789
+ }
790
+ if (lockedProposal.status === proposal_dto_1.ProposalStatus.CONTRACT_GENERATED) {
791
+ return;
792
+ }
793
+ const previousStatus = String(lockedProposal.status || proposal_dto_1.ProposalStatus.DRAFT);
794
+ await tx.proposal.update({
795
+ where: { id: proposalId },
796
+ data: {
797
+ status: proposal_dto_1.ProposalStatus.CONTRACT_GENERATED,
798
+ contract_generated_at: new Date(),
799
+ updated_by_user_id: actorUserId,
800
+ },
801
+ });
802
+ await this.publishProposalLifecycleEvent(tx, proposal_event_types_1.PROPOSAL_EVENT_NAMES.CONVERTED, proposalId, locale, actorUserId, {
803
+ contract: {
804
+ contractId: Number.isInteger(contractId) && contractId > 0 ? contractId : null,
805
+ code: (_b = (_a = payload === null || payload === void 0 ? void 0 : payload.contract) === null || _a === void 0 ? void 0 : _a.code) !== null && _b !== void 0 ? _b : null,
806
+ name: (_d = (_c = payload === null || payload === void 0 ? void 0 : payload.contract) === null || _c === void 0 ? void 0 : _c.name) !== null && _d !== void 0 ? _d : null,
807
+ status: (_f = (_e = payload === null || payload === void 0 ? void 0 : payload.contract) === null || _e === void 0 ? void 0 : _e.status) !== null && _f !== void 0 ? _f : 'draft',
808
+ url: (_h = (_g = payload === null || payload === void 0 ? void 0 : payload.contract) === null || _g === void 0 ? void 0 : _g.url) !== null && _h !== void 0 ? _h : null,
809
+ },
810
+ correlationId: (payload === null || payload === void 0 ? void 0 : payload.correlationId) || null,
811
+ metadata: {
812
+ trigger: 'operations.contract.created',
813
+ previousStatus,
814
+ contractId: Number.isInteger(contractId) && contractId > 0 ? contractId : null,
815
+ },
816
+ });
817
+ });
818
+ await this.syncPersonDealValueFromApprovedProposals(this.prisma, current.person_id);
819
+ return this.getById(proposalId, locale);
820
+ }
821
+ async generateDocument(id, locale, userId) {
822
+ var _a, _b, _c;
823
+ const proposal = await this.loadProposalDetail(this.prisma, id);
824
+ if (!proposal) {
825
+ throw new common_1.NotFoundException((0, api_locale_1.getLocaleText)('itemNotFound', locale, 'Proposal not found'));
826
+ }
827
+ this.assertProposalDocumentReadiness(proposal);
828
+ const currentRevision = this.getCurrentProposalRevision(proposal);
829
+ if (!(currentRevision === null || currentRevision === void 0 ? void 0 : currentRevision.id)) {
830
+ throw new common_1.NotFoundException('Current proposal revision not found.');
831
+ }
832
+ const html = await this.buildProposalDocumentHtml(proposal, locale);
833
+ const pdfBuffer = await this.renderProposalPdfBuffer(proposal, locale, html);
834
+ const fileName = this.buildProposalDocumentFileName(proposal, currentRevision);
835
+ const uploadedFile = await this.fileService.upload('contact/proposals/generated', {
836
+ fieldname: 'file',
837
+ originalname: fileName,
838
+ encoding: '7bit',
839
+ mimetype: 'application/pdf',
840
+ size: pdfBuffer.length,
841
+ destination: '',
842
+ filename: fileName,
843
+ path: '',
844
+ buffer: pdfBuffer,
845
+ });
846
+ const generatedAt = new Date().toISOString();
847
+ let renderVersion = 1;
848
+ await this.prisma.$transaction(async (tx) => {
849
+ var _a, _b, _c;
850
+ const lockedProposal = await this.lockProposalForTransition(tx, id);
851
+ if (!lockedProposal) {
852
+ throw new common_1.NotFoundException('Proposal not found.');
853
+ }
854
+ this.assertProposalDocumentReadiness(lockedProposal);
855
+ const lockedRevision = this.getCurrentProposalRevision(lockedProposal);
856
+ if (!(lockedRevision === null || lockedRevision === void 0 ? void 0 : lockedRevision.id)) {
857
+ throw new common_1.NotFoundException('Current proposal revision not found.');
858
+ }
859
+ renderVersion = await this.upsertGeneratedProposalDocument(tx, lockedProposal.id, lockedRevision, {
860
+ fileId: uploadedFile.id,
861
+ fileName,
862
+ mimeType: 'application/pdf',
863
+ html,
864
+ uploadedByUserId: Number(userId) || null,
865
+ });
866
+ await tx.proposal_revision.update({
867
+ where: { id: lockedRevision.id },
868
+ data: {
869
+ snapshot_json: this.mergeProposalRevisionSnapshot(lockedRevision.snapshot_json, {
870
+ commercialDocument: {
871
+ template: 'proposal-commercial-v1',
872
+ proposalId: lockedProposal.id,
873
+ proposalRevisionId: lockedRevision.id,
874
+ revisionNumber: Number(lockedRevision.revision_number ||
875
+ lockedProposal.current_revision_number ||
876
+ 1) || 1,
877
+ renderVersion,
878
+ generatedAt,
879
+ generatedByUserId: Number(userId) || null,
880
+ fileId: uploadedFile.id,
881
+ fileName,
882
+ mimeType: 'application/pdf',
883
+ acceptance: {
884
+ status: 'pending',
885
+ acceptedAt: null,
886
+ acceptedByName: ((_a = lockedProposal.person) === null || _a === void 0 ? void 0 : _a.trade_name) ||
887
+ ((_b = lockedProposal.person) === null || _b === void 0 ? void 0 : _b.name) ||
888
+ null,
889
+ acceptedByEmail: ((_c = lockedProposal.person) === null || _c === void 0 ? void 0 : _c.email) || null,
890
+ },
891
+ },
892
+ }),
893
+ },
894
+ });
895
+ });
896
+ return {
897
+ proposalId: proposal.id,
898
+ proposalRevisionId: currentRevision.id,
899
+ version: Number(currentRevision.revision_number || proposal.current_revision_number || 1) ||
900
+ 1,
901
+ renderVersion,
902
+ fileId: uploadedFile.id,
903
+ fileName,
904
+ mimeType: 'application/pdf',
905
+ documentType: 'generated_pdf',
906
+ downloadUrl: `/file/open/${uploadedFile.id}`,
907
+ html,
908
+ acceptance: {
909
+ status: 'pending',
910
+ acceptedAt: null,
911
+ expectedSignerName: ((_a = proposal.person) === null || _a === void 0 ? void 0 : _a.trade_name) || ((_b = proposal.person) === null || _b === void 0 ? void 0 : _b.name) || null,
912
+ expectedSignerEmail: ((_c = proposal.person) === null || _c === void 0 ? void 0 : _c.email) || null,
913
+ },
914
+ };
915
+ }
916
+ async remove(data, locale, userId) {
917
+ const ids = Array.isArray(data === null || data === void 0 ? void 0 : data.ids)
918
+ ? data.ids.map((value) => Number(value)).filter((value) => value > 0)
919
+ : [];
920
+ if (ids.length === 0) {
921
+ throw new common_1.BadRequestException((0, api_locale_1.getLocaleText)('validation.idsMustBeArray', locale, 'No valid proposal ids were informed'));
922
+ }
923
+ const impactedProposals = await this.prisma.proposal.findMany({
924
+ where: {
925
+ id: { in: ids },
926
+ deleted_at: null,
927
+ },
928
+ select: {
929
+ id: true,
930
+ person_id: true,
931
+ },
932
+ });
933
+ const impactedPersonIds = Array.from(new Set(impactedProposals
934
+ .map((proposal) => Number((proposal === null || proposal === void 0 ? void 0 : proposal.person_id) || 0))
935
+ .filter((personId) => personId > 0)));
936
+ await this.prisma.$transaction(async (tx) => {
937
+ const now = new Date();
938
+ await tx.proposal.updateMany({
939
+ where: {
940
+ id: { in: ids },
941
+ deleted_at: null,
942
+ },
943
+ data: {
944
+ deleted_at: now,
945
+ updated_by_user_id: userId || null,
946
+ },
947
+ });
948
+ await tx.proposal_revision.updateMany({
949
+ where: {
950
+ proposal_id: { in: ids },
951
+ deleted_at: null,
952
+ },
953
+ data: {
954
+ deleted_at: now,
955
+ },
956
+ });
957
+ await tx.proposal_item.updateMany({
958
+ where: {
959
+ proposal_revision: {
960
+ proposal_id: { in: ids },
961
+ },
962
+ deleted_at: null,
963
+ },
964
+ data: {
965
+ deleted_at: now,
966
+ },
967
+ });
968
+ await tx.proposal_document.updateMany({
969
+ where: {
970
+ proposal_id: { in: ids },
971
+ deleted_at: null,
972
+ },
973
+ data: {
974
+ deleted_at: now,
975
+ is_current: false,
976
+ },
977
+ });
978
+ await tx.proposal_approval.updateMany({
979
+ where: {
980
+ proposal_id: { in: ids },
981
+ deleted_at: null,
982
+ },
983
+ data: {
984
+ deleted_at: now,
985
+ },
986
+ });
987
+ });
988
+ await Promise.all(impactedPersonIds.map((personId) => this.syncPersonDealValueFromApprovedProposals(this.prisma, personId)));
989
+ return {
990
+ deleted: ids.length,
991
+ ids,
992
+ };
993
+ }
994
+ async publishProposalLifecycleEvent(client, eventName, proposalId, locale, userId, extras) {
995
+ const proposal = await this.loadProposalIntegrationSnapshot(client, proposalId);
996
+ const payload = this.buildProposalLifecyclePayload(proposal, eventName, locale, userId !== null && userId !== void 0 ? userId : null, extras);
997
+ const recentEvents = await client.outbox_event.findMany({
998
+ where: {
999
+ event_name: eventName,
1000
+ source_module: 'contact',
1001
+ aggregate_type: 'proposal',
1002
+ aggregate_id: String(proposalId),
1003
+ },
1004
+ select: {
1005
+ id: true,
1006
+ status: true,
1007
+ payload: true,
1008
+ },
1009
+ orderBy: {
1010
+ id: 'desc',
1011
+ },
1012
+ take: 25,
1013
+ });
1014
+ const duplicated = recentEvents.some((event) => {
1015
+ var _a;
1016
+ return ((_a = event === null || event === void 0 ? void 0 : event.payload) === null || _a === void 0 ? void 0 : _a.eventKey) === payload.eventKey &&
1017
+ !['failed', 'dead_letter'].includes(String((event === null || event === void 0 ? void 0 : event.status) || ''));
1018
+ });
1019
+ if (duplicated) {
1020
+ this.logger.warn(`[proposal-event] skipped duplicate ${eventName} for proposal ${proposalId} (${payload.eventKey})`);
1021
+ return null;
1022
+ }
1023
+ const outboxEvent = await this.integrationApi.publishEvent({
1024
+ eventName,
1025
+ sourceModule: 'contact',
1026
+ aggregateType: 'proposal',
1027
+ aggregateId: String(proposalId),
1028
+ payload,
1029
+ metadata: Object.assign({ producer: 'contact', correlationId: payload.correlationId, eventKey: payload.eventKey, sourceModule: 'contact', sourceEntity: 'proposal', sourceId: String(proposalId), locale, triggeredByUserId: userId !== null && userId !== void 0 ? userId : null }, ((extras === null || extras === void 0 ? void 0 : extras.metadata) || {})),
1030
+ }, {
1031
+ persistenceClient: client,
1032
+ });
1033
+ this.logger.log(`[proposal-event] queued ${eventName} for proposal ${proposalId} as outbox ${outboxEvent.id}`);
1034
+ return outboxEvent;
1035
+ }
1036
+ buildProposalLifecyclePayload(proposal, eventName, locale, userId, extras) {
1037
+ 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, _17;
1038
+ const currentRevision = (_b = (_a = proposal.proposal_revision) === null || _a === void 0 ? void 0 : _a[0]) !== null && _b !== void 0 ? _b : null;
1039
+ const items = ((currentRevision === null || currentRevision === void 0 ? void 0 : currentRevision.proposal_item) || []).map((item) => {
1040
+ var _a, _b, _c, _d, _e;
1041
+ return ({
1042
+ id: Number(item.id || 0) || null,
1043
+ name: item.name,
1044
+ description: (_a = item.description) !== null && _a !== void 0 ? _a : null,
1045
+ itemType: (_b = item.item_type) !== null && _b !== void 0 ? _b : null,
1046
+ termType: (_c = item.term_type) !== null && _c !== void 0 ? _c : null,
1047
+ quantity: Number(item.quantity || 1),
1048
+ unitAmountCents: Number(item.unit_amount_cents || 0),
1049
+ totalAmountCents: Number(item.total_amount_cents || 0),
1050
+ amount: Number(item.total_amount_cents || 0) / 100,
1051
+ recurrence: (_d = item.recurrence) !== null && _d !== void 0 ? _d : null,
1052
+ dueDay: (_e = item.due_day) !== null && _e !== void 0 ? _e : null,
1053
+ });
1054
+ });
1055
+ const version = Number((currentRevision === null || currentRevision === void 0 ? void 0 : currentRevision.revision_number) || proposal.current_revision_number || 1);
1056
+ const correlationId = (extras === null || extras === void 0 ? void 0 : extras.correlationId) || `proposal:${proposal.id}:v${version}`;
1057
+ const contractId = (_d = (_c = extras === null || extras === void 0 ? void 0 : extras.contract) === null || _c === void 0 ? void 0 : _c.contractId) !== null && _d !== void 0 ? _d : null;
1058
+ const eventKeyParts = [eventName, String(proposal.id), `v${version}`];
1059
+ if (contractId) {
1060
+ eventKeyParts.push(`contract:${contractId}`);
1061
+ }
1062
+ const personId = Number(proposal.person_id || ((_e = proposal.person) === null || _e === void 0 ? void 0 : _e.id) || 0) || null;
1063
+ const companyId = String(((_f = proposal.person) === null || _f === void 0 ? void 0 : _f.type) || '') === 'company' ? personId : null;
1064
+ return {
1065
+ eventName,
1066
+ eventKey: eventKeyParts.join(':'),
1067
+ correlationId,
1068
+ proposalId: proposal.id,
1069
+ proposalRevisionId: (_g = currentRevision === null || currentRevision === void 0 ? void 0 : currentRevision.id) !== null && _g !== void 0 ? _g : null,
1070
+ approvedByUserId: userId !== null && userId !== void 0 ? userId : null,
1071
+ dealId: personId,
1072
+ personId,
1073
+ companyId,
1074
+ customerIds: {
1075
+ personId,
1076
+ companyId,
1077
+ },
1078
+ code: (_h = proposal.code) !== null && _h !== void 0 ? _h : null,
1079
+ title: (_k = (_j = proposal.title) !== null && _j !== void 0 ? _j : currentRevision === null || currentRevision === void 0 ? void 0 : currentRevision.title) !== null && _k !== void 0 ? _k : null,
1080
+ version,
1081
+ status: (_m = (_l = proposal.status) !== null && _l !== void 0 ? _l : currentRevision === null || currentRevision === void 0 ? void 0 : currentRevision.status) !== null && _m !== void 0 ? _m : null,
1082
+ approvedAt: proposal.approved_at
1083
+ ? new Date(proposal.approved_at).toISOString()
1084
+ : (currentRevision === null || currentRevision === void 0 ? void 0 : currentRevision.approved_at)
1085
+ ? new Date(currentRevision.approved_at).toISOString()
1086
+ : null,
1087
+ validUntil: proposal.valid_until
1088
+ ? new Date(proposal.valid_until).toISOString()
1089
+ : (currentRevision === null || currentRevision === void 0 ? void 0 : currentRevision.valid_until)
1090
+ ? new Date(currentRevision.valid_until).toISOString()
1091
+ : null,
1092
+ currency: (_o = proposal.currency_code) !== null && _o !== void 0 ? _o : 'BRL',
1093
+ proposal: {
1094
+ code: (_p = proposal.code) !== null && _p !== void 0 ? _p : null,
1095
+ title: (_r = (_q = proposal.title) !== null && _q !== void 0 ? _q : currentRevision === null || currentRevision === void 0 ? void 0 : currentRevision.title) !== null && _r !== void 0 ? _r : null,
1096
+ status: (_t = (_s = proposal.status) !== null && _s !== void 0 ? _s : currentRevision === null || currentRevision === void 0 ? void 0 : currentRevision.status) !== null && _t !== void 0 ? _t : null,
1097
+ contractCategory: (_u = proposal.contract_category) !== null && _u !== void 0 ? _u : null,
1098
+ contractType: (_v = proposal.contract_type) !== null && _v !== void 0 ? _v : null,
1099
+ billingModel: (_w = proposal.billing_model) !== null && _w !== void 0 ? _w : null,
1100
+ currencyCode: (_x = proposal.currency_code) !== null && _x !== void 0 ? _x : 'BRL',
1101
+ validFrom: proposal.valid_from
1102
+ ? new Date(proposal.valid_from).toISOString()
1103
+ : null,
1104
+ validUntil: proposal.valid_until
1105
+ ? new Date(proposal.valid_until).toISOString()
1106
+ : (currentRevision === null || currentRevision === void 0 ? void 0 : currentRevision.valid_until)
1107
+ ? new Date(currentRevision.valid_until).toISOString()
1108
+ : null,
1109
+ subtotalAmountCents: Number(proposal.subtotal_amount_cents || 0),
1110
+ discountAmountCents: Number(proposal.discount_amount_cents || 0),
1111
+ taxAmountCents: Number(proposal.tax_amount_cents || 0),
1112
+ totalAmountCents: Number(proposal.total_amount_cents || 0),
1113
+ totalAmount: Number(proposal.total_amount_cents || 0) / 100,
1114
+ notes: (_z = (_y = extras === null || extras === void 0 ? void 0 : extras.note) !== null && _y !== void 0 ? _y : proposal.notes) !== null && _z !== void 0 ? _z : null,
1115
+ },
1116
+ subtotal: Number(proposal.subtotal_amount_cents || 0) / 100,
1117
+ subtotalCents: Number(proposal.subtotal_amount_cents || 0),
1118
+ discount: Number(proposal.discount_amount_cents || 0) / 100,
1119
+ discountCents: Number(proposal.discount_amount_cents || 0),
1120
+ tax: Number(proposal.tax_amount_cents || 0) / 100,
1121
+ taxCents: Number(proposal.tax_amount_cents || 0),
1122
+ total: Number(proposal.total_amount_cents || 0) / 100,
1123
+ totalCents: Number(proposal.total_amount_cents || 0),
1124
+ commercialTerms: {
1125
+ contractCategory: (_0 = proposal.contract_category) !== null && _0 !== void 0 ? _0 : null,
1126
+ contractType: (_1 = proposal.contract_type) !== null && _1 !== void 0 ? _1 : null,
1127
+ billingModel: (_2 = proposal.billing_model) !== null && _2 !== void 0 ? _2 : null,
1128
+ validFrom: proposal.valid_from
1129
+ ? new Date(proposal.valid_from).toISOString()
1130
+ : null,
1131
+ validUntil: proposal.valid_until
1132
+ ? new Date(proposal.valid_until).toISOString()
1133
+ : (currentRevision === null || currentRevision === void 0 ? void 0 : currentRevision.valid_until)
1134
+ ? new Date(currentRevision.valid_until).toISOString()
1135
+ : null,
1136
+ summary: (_3 = currentRevision === null || currentRevision === void 0 ? void 0 : currentRevision.summary) !== null && _3 !== void 0 ? _3 : null,
1137
+ notes: (_5 = (_4 = extras === null || extras === void 0 ? void 0 : extras.note) !== null && _4 !== void 0 ? _4 : proposal.notes) !== null && _5 !== void 0 ? _5 : null,
1138
+ },
1139
+ itemsSummary: items,
1140
+ items,
1141
+ person: proposal.person
1142
+ ? {
1143
+ id: (_6 = proposal.person.id) !== null && _6 !== void 0 ? _6 : null,
1144
+ name: (_7 = proposal.person.name) !== null && _7 !== void 0 ? _7 : null,
1145
+ tradeName: (_8 = proposal.person.trade_name) !== null && _8 !== void 0 ? _8 : null,
1146
+ email: (_9 = proposal.person.email) !== null && _9 !== void 0 ? _9 : null,
1147
+ phone: (_10 = proposal.person.phone) !== null && _10 !== void 0 ? _10 : null,
1148
+ document: (_11 = proposal.person.document) !== null && _11 !== void 0 ? _11 : null,
1149
+ type: (_12 = proposal.person.type) !== null && _12 !== void 0 ? _12 : null,
1150
+ }
1151
+ : null,
1152
+ revision: currentRevision
1153
+ ? {
1154
+ id: (_13 = currentRevision.id) !== null && _13 !== void 0 ? _13 : null,
1155
+ revisionNumber: version,
1156
+ title: (_14 = currentRevision.title) !== null && _14 !== void 0 ? _14 : null,
1157
+ summary: (_15 = currentRevision.summary) !== null && _15 !== void 0 ? _15 : null,
1158
+ contentHtml: (_16 = currentRevision.content_html) !== null && _16 !== void 0 ? _16 : null,
1159
+ submittedAt: currentRevision.submitted_at
1160
+ ? new Date(currentRevision.submitted_at).toISOString()
1161
+ : null,
1162
+ approvedAt: currentRevision.approved_at
1163
+ ? new Date(currentRevision.approved_at).toISOString()
1164
+ : null,
1165
+ }
1166
+ : null,
1167
+ contract: (_17 = extras === null || extras === void 0 ? void 0 : extras.contract) !== null && _17 !== void 0 ? _17 : null,
1168
+ triggeredByUserId: userId !== null && userId !== void 0 ? userId : null,
1169
+ emittedAt: new Date().toISOString(),
1170
+ locale: locale || null,
1171
+ sourceModule: 'contact',
1172
+ sourceEntity: 'proposal',
1173
+ sourceId: String(proposal.id),
1174
+ source_module: 'contact',
1175
+ source_entity: 'proposal',
1176
+ source_id: String(proposal.id),
1177
+ };
1178
+ }
1179
+ async assertPersonExists(personId, locale) {
1180
+ const person = await this.prisma.person.findUnique({
1181
+ where: { id: Number(personId) },
1182
+ select: { id: true },
1183
+ });
1184
+ if (!person) {
1185
+ throw new common_1.BadRequestException((0, api_locale_1.getLocaleText)('personNotFound', locale, 'Person not found'));
1186
+ }
1187
+ }
1188
+ async syncPersonDealValueFromApprovedProposals(client, personId) {
1189
+ var _a;
1190
+ const normalizedPersonId = Number(personId || 0);
1191
+ if (!Number.isInteger(normalizedPersonId) || normalizedPersonId <= 0) {
1192
+ return;
1193
+ }
1194
+ const aggregation = await client.proposal.aggregate({
1195
+ where: {
1196
+ deleted_at: null,
1197
+ person_id: normalizedPersonId,
1198
+ status: {
1199
+ in: [proposal_dto_1.ProposalStatus.APPROVED, proposal_dto_1.ProposalStatus.CONTRACT_GENERATED],
1200
+ },
1201
+ },
1202
+ _sum: {
1203
+ total_amount_cents: true,
1204
+ },
1205
+ });
1206
+ const totalAmountCents = Number(((_a = aggregation === null || aggregation === void 0 ? void 0 : aggregation._sum) === null || _a === void 0 ? void 0 : _a.total_amount_cents) || 0);
1207
+ const dealValue = totalAmountCents > 0 ? (totalAmountCents / 100).toFixed(2) : null;
1208
+ await this.upsertPersonMetadataValue(client, normalizedPersonId, 'deal_value', dealValue);
1209
+ }
1210
+ async upsertPersonMetadataValue(client, personId, key, value) {
1211
+ if (!client.person_metadata) {
1212
+ return;
1213
+ }
1214
+ const existing = await client.person_metadata.findFirst({
1215
+ where: {
1216
+ person_id: personId,
1217
+ key,
1218
+ },
1219
+ select: { id: true },
1220
+ });
1221
+ const normalizedValue = this.normalizePersonMetadataValue(value);
1222
+ if (normalizedValue == null) {
1223
+ if (existing) {
1224
+ await client.person_metadata.delete({
1225
+ where: { id: existing.id },
1226
+ });
1227
+ }
1228
+ return;
1229
+ }
1230
+ if (existing) {
1231
+ await client.person_metadata.update({
1232
+ where: { id: existing.id },
1233
+ data: { value: normalizedValue },
1234
+ });
1235
+ return;
1236
+ }
1237
+ await client.person_metadata.create({
1238
+ data: {
1239
+ person_id: personId,
1240
+ key,
1241
+ value: normalizedValue,
1242
+ },
1243
+ });
1244
+ }
1245
+ normalizePersonMetadataValue(value) {
1246
+ if (value == null)
1247
+ return null;
1248
+ if (typeof value === 'string') {
1249
+ const trimmed = value.trim();
1250
+ return trimmed.length > 0 ? trimmed : null;
1251
+ }
1252
+ if (typeof value === 'number') {
1253
+ return Number.isFinite(value) ? value : null;
1254
+ }
1255
+ if (typeof value === 'boolean') {
1256
+ return value;
1257
+ }
1258
+ if (Array.isArray(value) || typeof value === 'object') {
1259
+ try {
1260
+ return JSON.parse(JSON.stringify(value));
1261
+ }
1262
+ catch (_a) {
1263
+ return null;
1264
+ }
1265
+ }
1266
+ return null;
1267
+ }
1268
+ async resolveProposalCode(code) {
1269
+ var _a;
1270
+ const normalized = (_a = this.normalizeOptionalText(code)) === null || _a === void 0 ? void 0 : _a.toUpperCase();
1271
+ if (normalized) {
1272
+ const existing = await this.prisma.proposal.findFirst({
1273
+ where: {
1274
+ code: normalized,
1275
+ },
1276
+ select: { id: true },
1277
+ });
1278
+ if (existing) {
1279
+ throw new common_1.BadRequestException('Proposal code already exists.');
1280
+ }
1281
+ return normalized;
1282
+ }
1283
+ for (let attempt = 0; attempt < 5; attempt++) {
1284
+ const stamp = new Date().toISOString().slice(0, 10).replace(/-/g, '');
1285
+ const generated = `PROP-${stamp}-${Math.floor(1000 + Math.random() * 9000)}`;
1286
+ const existing = await this.prisma.proposal.findFirst({
1287
+ where: { code: generated },
1288
+ select: { id: true },
1289
+ });
1290
+ if (!existing) {
1291
+ return generated;
1292
+ }
1293
+ }
1294
+ return `PROP-${Date.now()}`;
1295
+ }
1296
+ normalizeItems(items) {
1297
+ return (items || []).map((item) => {
1298
+ var _a, _b, _c, _d, _e, _f, _g, _h, _j;
1299
+ const quantity = Number((_a = item.quantity) !== null && _a !== void 0 ? _a : 1);
1300
+ const unitAmountCents = Number((_b = item.unit_amount_cents) !== null && _b !== void 0 ? _b : 0);
1301
+ const totalAmountCents = Number.isFinite(Number(item.total_amount_cents))
1302
+ ? Number(item.total_amount_cents)
1303
+ : Math.round(quantity * unitAmountCents);
1304
+ return {
1305
+ item_type: (_c = item.item_type) !== null && _c !== void 0 ? _c : 'service',
1306
+ term_type: (_d = item.term_type) !== null && _d !== void 0 ? _d : 'value',
1307
+ name: item.name.trim(),
1308
+ description: this.normalizeOptionalText(item.description),
1309
+ quantity,
1310
+ unit_amount_cents: unitAmountCents,
1311
+ total_amount_cents: totalAmountCents,
1312
+ recurrence: (_e = item.recurrence) !== null && _e !== void 0 ? _e : 'one_time',
1313
+ due_day: (_f = item.due_day) !== null && _f !== void 0 ? _f : null,
1314
+ start_date: (_g = item.start_date) !== null && _g !== void 0 ? _g : null,
1315
+ end_date: (_h = item.end_date) !== null && _h !== void 0 ? _h : null,
1316
+ metadata_json: (_j = item.metadata_json) !== null && _j !== void 0 ? _j : null,
1317
+ };
1318
+ });
1319
+ }
1320
+ normalizeDocuments(documents, proposalId, proposalRevisionId, uploadedByUserId) {
1321
+ return (documents || []).map((document) => {
1322
+ var _a, _b, _c, _d;
1323
+ return ({
1324
+ proposal_id: proposalId,
1325
+ proposal_revision_id: proposalRevisionId,
1326
+ document_type: (_a = document.document_type) !== null && _a !== void 0 ? _a : 'attachment',
1327
+ file_id: (_b = document.file_id) !== null && _b !== void 0 ? _b : null,
1328
+ file_name: document.file_name.trim(),
1329
+ mime_type: document.mime_type.trim(),
1330
+ file_content_base64: this.normalizeOptionalText(document.file_content_base64),
1331
+ is_current: true,
1332
+ source_kind: (_c = document.source_kind) !== null && _c !== void 0 ? _c : 'manual',
1333
+ extraction_status: (_d = document.extraction_status) !== null && _d !== void 0 ? _d : 'skipped',
1334
+ extraction_summary: this.normalizeOptionalText(document.extraction_summary),
1335
+ notes: this.normalizeOptionalText(document.notes),
1336
+ uploaded_by_user_id: uploadedByUserId,
1337
+ });
1338
+ });
1339
+ }
1340
+ resolveTotals(data, normalizedItems) {
1341
+ const computed = normalizedItems.reduce((acc, item) => {
1342
+ const amount = Number(item.total_amount_cents || 0);
1343
+ if (item.item_type === 'discount') {
1344
+ acc.discount += Math.abs(amount);
1345
+ }
1346
+ else {
1347
+ acc.subtotal += amount;
1348
+ }
1349
+ return acc;
1350
+ }, { subtotal: 0, discount: 0 });
1351
+ const subtotalAmountCents = Number.isFinite(Number(data.subtotal_amount_cents))
1352
+ ? Number(data.subtotal_amount_cents)
1353
+ : computed.subtotal;
1354
+ const discountAmountCents = Number.isFinite(Number(data.discount_amount_cents))
1355
+ ? Number(data.discount_amount_cents)
1356
+ : computed.discount;
1357
+ const taxAmountCents = Number.isFinite(Number(data.tax_amount_cents))
1358
+ ? Number(data.tax_amount_cents)
1359
+ : 0;
1360
+ const totalAmountCents = Number.isFinite(Number(data.total_amount_cents))
1361
+ ? Number(data.total_amount_cents)
1362
+ : subtotalAmountCents - discountAmountCents + taxAmountCents;
1363
+ return {
1364
+ subtotal_amount_cents: subtotalAmountCents,
1365
+ discount_amount_cents: discountAmountCents,
1366
+ tax_amount_cents: taxAmountCents,
1367
+ total_amount_cents: totalAmountCents,
1368
+ };
1369
+ }
1370
+ getCurrentProposalRevision(proposal) {
1371
+ var _a, _b, _c, _d;
1372
+ return ((_d = (_b = (_a = proposal === null || proposal === void 0 ? void 0 : proposal.proposal_revision) === null || _a === void 0 ? void 0 : _a.find((revision) => revision === null || revision === void 0 ? void 0 : revision.is_current)) !== null && _b !== void 0 ? _b : (_c = proposal === null || proposal === void 0 ? void 0 : proposal.proposal_revision) === null || _c === void 0 ? void 0 : _c[0]) !== null && _d !== void 0 ? _d : null);
1373
+ }
1374
+ assertProposalDocumentReadiness(proposal) {
1375
+ const currentRevision = this.getCurrentProposalRevision(proposal);
1376
+ if (!currentRevision) {
1377
+ throw new common_1.NotFoundException('Current proposal revision not found.');
1378
+ }
1379
+ const revisionItems = Array.isArray(currentRevision === null || currentRevision === void 0 ? void 0 : currentRevision.proposal_item)
1380
+ ? currentRevision.proposal_item.filter((item) => item && item.deleted_at == null)
1381
+ : [];
1382
+ const hasPositiveTotal = Number((proposal === null || proposal === void 0 ? void 0 : proposal.total_amount_cents) || 0) > 0;
1383
+ if (revisionItems.length === 0 && !hasPositiveTotal) {
1384
+ throw new common_1.BadRequestException('Proposals without items or value cannot generate a commercial document.');
1385
+ }
1386
+ }
1387
+ async upsertGeneratedProposalDocument(client, proposalId, proposalRevision, document) {
1388
+ var _a, _b;
1389
+ const renderVersion = (await client.proposal_document.count({
1390
+ where: {
1391
+ proposal_id: proposalId,
1392
+ proposal_revision_id: proposalRevision.id,
1393
+ document_type: 'generated_pdf',
1394
+ deleted_at: null,
1395
+ },
1396
+ })) + 1;
1397
+ await client.proposal_document.updateMany({
1398
+ where: {
1399
+ proposal_id: proposalId,
1400
+ proposal_revision_id: proposalRevision.id,
1401
+ document_type: 'generated_pdf',
1402
+ deleted_at: null,
1403
+ },
1404
+ data: {
1405
+ is_current: false,
1406
+ },
1407
+ });
1408
+ await client.proposal_document.create({
1409
+ data: {
1410
+ proposal_id: proposalId,
1411
+ proposal_revision_id: proposalRevision.id,
1412
+ document_type: 'generated_pdf',
1413
+ file_id: (_a = document.fileId) !== null && _a !== void 0 ? _a : null,
1414
+ file_name: document.fileName,
1415
+ mime_type: document.mimeType,
1416
+ file_content_base64: Buffer.from(document.html, 'utf8').toString('base64'),
1417
+ is_current: true,
1418
+ source_kind: 'generated',
1419
+ extraction_status: 'skipped',
1420
+ extraction_summary: `Commercial proposal render version ${renderVersion}.`,
1421
+ notes: 'Commercial proposal PDF generated from proposal data. The base64 snapshot stores the rendered HTML source for preview and future electronic acceptance.',
1422
+ uploaded_by_user_id: (_b = document.uploadedByUserId) !== null && _b !== void 0 ? _b : null,
1423
+ },
1424
+ });
1425
+ return renderVersion;
1426
+ }
1427
+ async renderProposalPdfBuffer(proposal, locale, html) {
1428
+ var _a, _b, _c;
1429
+ const renderedHtml = html !== null && html !== void 0 ? html : (await this.buildProposalDocumentHtml(proposal, locale));
1430
+ let browser = null;
1431
+ try {
1432
+ const importPlaywright = new Function('moduleName', 'return import(moduleName);');
1433
+ const playwright = await importPlaywright('playwright');
1434
+ browser = await playwright.chromium.launch({ headless: true });
1435
+ const page = await browser.newPage();
1436
+ await page.setContent(renderedHtml, { waitUntil: 'networkidle' });
1437
+ return Buffer.from(await page.pdf({
1438
+ format: 'A4',
1439
+ printBackground: true,
1440
+ margin: {
1441
+ top: '72px',
1442
+ right: '18px',
1443
+ bottom: '56px',
1444
+ left: '18px',
1445
+ },
1446
+ }));
1447
+ }
1448
+ catch (error) {
1449
+ const errorMessage = error instanceof Error ? error.message : String(error);
1450
+ const errorStack = error instanceof Error ? ((_a = error.stack) !== null && _a !== void 0 ? _a : error.message) : String(error);
1451
+ 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);
1452
+ this.logger.error(`Failed to generate proposal PDF for proposal ${(_b = proposal === null || proposal === void 0 ? void 0 : proposal.id) !== null && _b !== void 0 ? _b : 'unknown'} (locale=${locale}). ${errorMessage}`, errorStack);
1453
+ throw new common_1.InternalServerErrorException(missingPlaywrightRuntime
1454
+ ? 'PDF generation is unavailable because Playwright/Chromium is not installed on the server. Run `pnpm --filter api run playwright:install` in the API environment.'
1455
+ : 'Failed to generate the PDF document. Check server logs for details.');
1456
+ }
1457
+ finally {
1458
+ await ((_c = browser === null || browser === void 0 ? void 0 : browser.close) === null || _c === void 0 ? void 0 : _c.call(browser));
1459
+ }
1460
+ }
1461
+ async buildProposalDocumentHtml(proposal, locale = 'en') {
1462
+ var _a, _b, _c, _d, _e;
1463
+ const labels = this.getProposalDocumentLabels(locale);
1464
+ const logoUrl = await this.resolveProposalDocumentLogoUrl();
1465
+ const companyName = await this.resolveProposalDocumentCompanyName();
1466
+ const currentRevision = this.getCurrentProposalRevision(proposal);
1467
+ const items = Array.isArray(currentRevision === null || currentRevision === void 0 ? void 0 : currentRevision.proposal_item)
1468
+ ? currentRevision.proposal_item.filter((item) => item && item.deleted_at == null)
1469
+ : [];
1470
+ const bodyHtml = this.sanitizeDocumentHtml(currentRevision === null || currentRevision === void 0 ? void 0 : currentRevision.content_html) ||
1471
+ ((_a = this.normalizeOptionalText(currentRevision === null || currentRevision === void 0 ? void 0 : currentRevision.summary)) === null || _a === void 0 ? void 0 : _a.split(/\n{2,}/).map((paragraph) => `<p>${this.escapeHtml(paragraph)}</p>`).join('')) ||
1472
+ '<p>No additional commercial narrative was provided yet.</p>';
1473
+ const itemsHtml = items.length
1474
+ ? items
1475
+ .map((item) => `
1476
+ <tr>
1477
+ <td>${this.escapeHtml(item.name || 'Item')}</td>
1478
+ <td>${this.escapeHtml(item.description || '—')}</td>
1479
+ <td class="numeric">${this.escapeHtml(String(Number(item.quantity || 1)))}</td>
1480
+ <td>${this.escapeHtml(this.humanizeEnumLabel(item.recurrence || 'one_time'))}</td>
1481
+ <td class="numeric">${this.escapeHtml(this.formatMoney(Number(item.unit_amount_cents || 0) / 100, proposal.currency_code, locale))}</td>
1482
+ <td class="numeric">${this.escapeHtml(this.formatMoney(Number(item.total_amount_cents || 0) / 100, proposal.currency_code, locale))}</td>
1483
+ </tr>`)
1484
+ .join('')
1485
+ : `
1486
+ <tr>
1487
+ <td colspan="6">${this.escapeHtml(labels.emptyItems)}</td>
1488
+ </tr>`;
1489
+ const conditions = [
1490
+ `${labels.validity}: ${this.formatDateLabel(proposal.valid_until, locale, labels.openEnded)}`,
1491
+ `${labels.billingModel}: ${this.humanizeEnumLabel(proposal.billing_model || 'fixed_price')}`,
1492
+ `${labels.proposalType}: ${this.humanizeEnumLabel(proposal.contract_type || 'service_agreement')}`,
1493
+ `${labels.acceptanceReady}`,
1494
+ ]
1495
+ .filter(Boolean)
1496
+ .map((item) => `<li>${this.escapeHtml(item)}</li>`)
1497
+ .join('');
1498
+ const notesHtml = this.normalizeOptionalText(proposal.notes)
1499
+ ? this.normalizeOptionalText(proposal.notes)
1500
+ .split(/\n{2,}/)
1501
+ .map((paragraph) => `<p>${this.escapeHtml(paragraph)}</p>`)
1502
+ .join('')
1503
+ : `<p>${this.escapeHtml(labels.noNotes)}</p>`;
1504
+ return `<!DOCTYPE html>
1505
+ <html lang="${this.escapeHtml(locale || 'en')}">
1506
+ <head>
1507
+ <meta charset="utf-8" />
1508
+ <style>
1509
+ @page {
1510
+ size: A4;
1511
+ margin: 72px 18px 56px 18px;
1512
+ }
1513
+ body {
1514
+ color: #0f172a;
1515
+ font-family: Arial, sans-serif;
1516
+ font-size: 12px;
1517
+ line-height: 1.5;
1518
+ margin: 0;
1519
+ }
1520
+ header {
1521
+ align-items: center;
1522
+ border-bottom: 2px solid #dbeafe;
1523
+ display: flex;
1524
+ justify-content: space-between;
1525
+ gap: 16px;
1526
+ margin-bottom: 24px;
1527
+ padding-bottom: 16px;
1528
+ }
1529
+ .brand { display: flex; align-items: center; gap: 12px; }
1530
+ .brand img { height: 42px; max-width: 140px; object-fit: contain; }
1531
+ .eyebrow {
1532
+ color: #2563eb;
1533
+ font-size: 10px;
1534
+ font-weight: 700;
1535
+ letter-spacing: 0.12em;
1536
+ text-transform: uppercase;
1537
+ }
1538
+ h1 { font-size: 22px; margin: 4px 0 0; }
1539
+ .meta-grid {
1540
+ display: grid;
1541
+ grid-template-columns: repeat(2, minmax(0, 1fr));
1542
+ gap: 12px;
1543
+ margin: 18px 0 20px;
1544
+ }
1545
+ .meta-card, .totals-card {
1546
+ background: #f8fafc;
1547
+ border: 1px solid #dbeafe;
1548
+ border-radius: 12px;
1549
+ padding: 12px;
1550
+ }
1551
+ .meta-card strong, .totals-card strong {
1552
+ display: block;
1553
+ font-size: 10px;
1554
+ letter-spacing: 0.08em;
1555
+ margin-bottom: 4px;
1556
+ text-transform: uppercase;
1557
+ }
1558
+ section { margin-top: 18px; }
1559
+ h2 { color: #1d4ed8; font-size: 14px; margin: 0 0 10px; }
1560
+ .items-table {
1561
+ border-collapse: collapse;
1562
+ width: 100%;
1563
+ }
1564
+ .items-table th, .items-table td {
1565
+ border-bottom: 1px solid #e2e8f0;
1566
+ padding: 8px 10px;
1567
+ text-align: left;
1568
+ vertical-align: top;
1569
+ }
1570
+ .items-table th {
1571
+ background: #eff6ff;
1572
+ font-size: 10px;
1573
+ letter-spacing: 0.08em;
1574
+ text-transform: uppercase;
1575
+ }
1576
+ .numeric { text-align: right; }
1577
+ .content, .notes, .acceptance {
1578
+ border: 1px solid #e2e8f0;
1579
+ border-radius: 14px;
1580
+ padding: 16px 18px;
1581
+ }
1582
+ .totals-grid {
1583
+ display: grid;
1584
+ grid-template-columns: 1.5fr 1fr;
1585
+ gap: 16px;
1586
+ align-items: start;
1587
+ }
1588
+ .totals-card .row {
1589
+ display: flex;
1590
+ justify-content: space-between;
1591
+ margin-bottom: 6px;
1592
+ }
1593
+ .totals-card .row.total {
1594
+ border-top: 1px solid #cbd5e1;
1595
+ font-size: 14px;
1596
+ font-weight: 700;
1597
+ margin-top: 10px;
1598
+ padding-top: 10px;
1599
+ }
1600
+ ul { margin: 0; padding-left: 18px; }
1601
+ footer {
1602
+ border-top: 1px solid #dbeafe;
1603
+ color: #475569;
1604
+ font-size: 10px;
1605
+ margin-top: 28px;
1606
+ padding-top: 12px;
1607
+ }
1608
+ </style>
1609
+ </head>
1610
+ <body>
1611
+ <header>
1612
+ <div class="brand">
1613
+ <img src="${logoUrl}" alt="${this.escapeHtml(companyName)}" />
1614
+ <div>
1615
+ <div class="eyebrow">${this.escapeHtml(labels.documentTag)}</div>
1616
+ <h1>${this.escapeHtml(proposal.title || (currentRevision === null || currentRevision === void 0 ? void 0 : currentRevision.title) || labels.documentTitle)}</h1>
1617
+ <div>${this.escapeHtml(companyName)}</div>
1618
+ </div>
1619
+ </div>
1620
+ <div>
1621
+ <div><strong>${this.escapeHtml(labels.code)}:</strong> ${this.escapeHtml(proposal.code || `PROP-${proposal.id}`)}</div>
1622
+ <div><strong>${this.escapeHtml(labels.version)}:</strong> ${this.escapeHtml(`R${Number((currentRevision === null || currentRevision === void 0 ? void 0 : currentRevision.revision_number) || proposal.current_revision_number || 1) || 1}`)}</div>
1623
+ <div><strong>${this.escapeHtml(labels.status)}:</strong> ${this.escapeHtml(this.humanizeEnumLabel(proposal.status || 'draft'))}</div>
1624
+ </div>
1625
+ </header>
1626
+
1627
+ <div class="meta-grid">
1628
+ <div class="meta-card">
1629
+ <strong>${this.escapeHtml(labels.client)}</strong>
1630
+ ${this.escapeHtml(((_b = proposal.person) === null || _b === void 0 ? void 0 : _b.trade_name) || ((_c = proposal.person) === null || _c === void 0 ? void 0 : _c.name) || labels.notInformed)}
1631
+ </div>
1632
+ <div class="meta-card">
1633
+ <strong>${this.escapeHtml(labels.validity)}</strong>
1634
+ ${this.escapeHtml(this.formatDateLabel(proposal.valid_until, locale, labels.openEnded))}
1635
+ </div>
1636
+ <div class="meta-card">
1637
+ <strong>${this.escapeHtml(labels.email)}</strong>
1638
+ ${this.escapeHtml(((_d = proposal.person) === null || _d === void 0 ? void 0 : _d.email) || labels.notInformed)}
1639
+ </div>
1640
+ <div class="meta-card">
1641
+ <strong>${this.escapeHtml(labels.document)}</strong>
1642
+ ${this.escapeHtml(((_e = proposal.person) === null || _e === void 0 ? void 0 : _e.document) || labels.notInformed)}
1643
+ </div>
1644
+ </div>
1645
+
1646
+ <section>
1647
+ <h2>${this.escapeHtml(labels.executiveSummary)}</h2>
1648
+ <div class="content">${bodyHtml}</div>
1649
+ </section>
1650
+
1651
+ <section>
1652
+ <h2>${this.escapeHtml(labels.items)}</h2>
1653
+ <table class="items-table">
1654
+ <thead>
1655
+ <tr>
1656
+ <th>${this.escapeHtml(labels.item)}</th>
1657
+ <th>${this.escapeHtml(labels.description)}</th>
1658
+ <th class="numeric">${this.escapeHtml(labels.qty)}</th>
1659
+ <th>${this.escapeHtml(labels.recurrence)}</th>
1660
+ <th class="numeric">${this.escapeHtml(labels.unitPrice)}</th>
1661
+ <th class="numeric">${this.escapeHtml(labels.total)}</th>
1662
+ </tr>
1663
+ </thead>
1664
+ <tbody>${itemsHtml}</tbody>
1665
+ </table>
1666
+ </section>
1667
+
1668
+ <section>
1669
+ <div class="totals-grid">
1670
+ <div class="notes">
1671
+ <h2>${this.escapeHtml(labels.commercialConditions)}</h2>
1672
+ <ul>${conditions}</ul>
1673
+ <h2 style="margin-top: 16px;">${this.escapeHtml(labels.notes)}</h2>
1674
+ ${notesHtml}
1675
+ </div>
1676
+ <div class="totals-card">
1677
+ <strong>${this.escapeHtml(labels.totals)}</strong>
1678
+ <div class="row"><span>${this.escapeHtml(labels.subtotal)}</span><span>${this.escapeHtml(this.formatMoney(Number(proposal.subtotal_amount_cents || 0) / 100, proposal.currency_code, locale))}</span></div>
1679
+ <div class="row"><span>${this.escapeHtml(labels.discount)}</span><span>${this.escapeHtml(this.formatMoney(Number(proposal.discount_amount_cents || 0) / 100, proposal.currency_code, locale))}</span></div>
1680
+ <div class="row"><span>${this.escapeHtml(labels.tax)}</span><span>${this.escapeHtml(this.formatMoney(Number(proposal.tax_amount_cents || 0) / 100, proposal.currency_code, locale))}</span></div>
1681
+ <div class="row total"><span>${this.escapeHtml(labels.total)}</span><span>${this.escapeHtml(this.formatMoney(Number(proposal.total_amount_cents || 0) / 100, proposal.currency_code, locale))}</span></div>
1682
+ </div>
1683
+ </div>
1684
+ </section>
1685
+
1686
+ <section>
1687
+ <h2>${this.escapeHtml(labels.acceptanceSection)}</h2>
1688
+ <div class="acceptance">
1689
+ <p>${this.escapeHtml(labels.acceptanceMessage)}</p>
1690
+ <p><strong>${this.escapeHtml(labels.acceptanceStatus)}:</strong> ${this.escapeHtml(labels.pending)}</p>
1691
+ </div>
1692
+ </section>
1693
+
1694
+ <footer>
1695
+ ${this.escapeHtml(labels.generatedOn)} ${this.escapeHtml(this.formatDateLabel(new Date().toISOString(), locale, labels.notInformed))} · ${this.escapeHtml(companyName)}
1696
+ </footer>
1697
+ </body>
1698
+ </html>`;
1699
+ }
1700
+ async resolveProposalDocumentLogoUrl() {
1701
+ var _a;
1702
+ const settings = await this.settingService.getSettingValues([
1703
+ 'image-url',
1704
+ 'icon-url',
1705
+ ]);
1706
+ const configuredUrl = (_a = this.normalizeOptionalText(settings['image-url'])) !== null && _a !== void 0 ? _a : this.normalizeOptionalText(settings['icon-url']);
1707
+ if (configuredUrl && /^https?:\/\//i.test(configuredUrl)) {
1708
+ return configuredUrl;
1709
+ }
1710
+ const inlineSvg = encodeURIComponent(`<svg xmlns="http://www.w3.org/2000/svg" width="240" height="72" viewBox="0 0 240 72">
1711
+ <rect width="240" height="72" rx="18" fill="#0f172a"/>
1712
+ <circle cx="38" cy="36" r="18" fill="#3b82f6"/>
1713
+ <text x="72" y="43" fill="#ffffff" font-size="26" font-family="Arial, sans-serif" font-weight="700">HedHog</text>
1714
+ </svg>`);
1715
+ return `data:image/svg+xml;charset=UTF-8,${inlineSvg}`;
1716
+ }
1717
+ async resolveProposalDocumentCompanyName() {
1718
+ var _a;
1719
+ const settings = await this.settingService.getSettingValues(['system-name']);
1720
+ return (_a = this.normalizeOptionalText(settings['system-name'])) !== null && _a !== void 0 ? _a : 'HedHog';
1721
+ }
1722
+ buildProposalDocumentFileName(proposal, revision) {
1723
+ var _a, _b;
1724
+ const base = (_b = (_a = this.normalizeOptionalText(proposal.code)) !== null && _a !== void 0 ? _a : this.normalizeOptionalText(proposal.title)) !== null && _b !== void 0 ? _b : `proposal-${proposal.id}`;
1725
+ const revisionLabel = Number((revision === null || revision === void 0 ? void 0 : revision.revision_number) || proposal.current_revision_number || 1);
1726
+ return `${base
1727
+ .toLowerCase()
1728
+ .replace(/[^a-z0-9]+/g, '-')
1729
+ .replace(/^-+|-+$/g, '')
1730
+ .slice(0, 72) || `proposal-${proposal.id}`}-r${revisionLabel}.pdf`;
1731
+ }
1732
+ mergeProposalRevisionSnapshot(existingSnapshot, patch) {
1733
+ const base = this.normalizeSnapshotJson(existingSnapshot);
1734
+ return Object.assign(Object.assign(Object.assign({}, base), patch), { commercialDocument: Object.assign(Object.assign({}, (base.commercialDocument || {})), (patch.commercialDocument || {})) });
1735
+ }
1736
+ normalizeSnapshotJson(value) {
1737
+ if (!value) {
1738
+ return {};
1739
+ }
1740
+ if (typeof value === 'string') {
1741
+ try {
1742
+ const parsed = JSON.parse(value);
1743
+ return parsed && typeof parsed === 'object' ? parsed : {};
1744
+ }
1745
+ catch (_a) {
1746
+ return {};
1747
+ }
1748
+ }
1749
+ return typeof value === 'object' && !Array.isArray(value) ? value : {};
1750
+ }
1751
+ getProposalDocumentLabels(locale) {
1752
+ const isPt = String(locale || '').toLowerCase().startsWith('pt');
1753
+ return isPt
1754
+ ? {
1755
+ documentTag: 'Proposta Comercial',
1756
+ documentTitle: 'Proposta Comercial',
1757
+ code: 'Código',
1758
+ version: 'Versão',
1759
+ status: 'Status',
1760
+ client: 'Cliente',
1761
+ validity: 'Validade',
1762
+ email: 'E-mail',
1763
+ document: 'Documento',
1764
+ executiveSummary: 'Resumo Executivo',
1765
+ items: 'Itens da Proposta',
1766
+ item: 'Item',
1767
+ description: 'Descrição',
1768
+ qty: 'Qtd.',
1769
+ recurrence: 'Recorrência',
1770
+ unitPrice: 'Valor Unitário',
1771
+ total: 'Total',
1772
+ totals: 'Totais',
1773
+ subtotal: 'Subtotal',
1774
+ discount: 'Desconto',
1775
+ tax: 'Impostos',
1776
+ commercialConditions: 'Condições Comerciais',
1777
+ notes: 'Observações',
1778
+ noNotes: 'Nenhuma observação adicional informada.',
1779
+ emptyItems: 'Nenhum item comercial foi registrado nesta versão.',
1780
+ notInformed: 'Não informado',
1781
+ openEnded: 'Em aberto',
1782
+ billingModel: 'Modelo de cobrança',
1783
+ proposalType: 'Tipo comercial',
1784
+ acceptanceReady: 'Documento preparado para aceite eletrônico simples em uma etapa futura.',
1785
+ acceptanceSection: 'Aceite Eletrônico (preparação)',
1786
+ acceptanceMessage: 'Esta versão já preserva o HTML da proposta e o PDF assinado visualmente, facilitando a inclusão de um aceite eletrônico simples no futuro.',
1787
+ acceptanceStatus: 'Status do aceite',
1788
+ pending: 'Pendente',
1789
+ generatedOn: 'Gerado em',
1790
+ }
1791
+ : {
1792
+ documentTag: 'Commercial Proposal',
1793
+ documentTitle: 'Commercial Proposal',
1794
+ code: 'Code',
1795
+ version: 'Version',
1796
+ status: 'Status',
1797
+ client: 'Client',
1798
+ validity: 'Validity',
1799
+ email: 'Email',
1800
+ document: 'Document',
1801
+ executiveSummary: 'Executive Summary',
1802
+ items: 'Proposal Items',
1803
+ item: 'Item',
1804
+ description: 'Description',
1805
+ qty: 'Qty.',
1806
+ recurrence: 'Recurrence',
1807
+ unitPrice: 'Unit Price',
1808
+ total: 'Total',
1809
+ totals: 'Totals',
1810
+ subtotal: 'Subtotal',
1811
+ discount: 'Discount',
1812
+ tax: 'Tax',
1813
+ commercialConditions: 'Commercial Conditions',
1814
+ notes: 'Notes',
1815
+ noNotes: 'No additional notes were provided.',
1816
+ emptyItems: 'No commercial items were registered for this version.',
1817
+ notInformed: 'Not informed',
1818
+ openEnded: 'Open ended',
1819
+ billingModel: 'Billing model',
1820
+ proposalType: 'Commercial type',
1821
+ acceptanceReady: 'This document is ready for a future lightweight electronic acceptance step.',
1822
+ acceptanceSection: 'Electronic Acceptance (prepared)',
1823
+ acceptanceMessage: 'This version already preserves the rendered HTML and visual PDF, which will simplify adding a lightweight electronic acceptance flow later.',
1824
+ acceptanceStatus: 'Acceptance status',
1825
+ pending: 'Pending',
1826
+ generatedOn: 'Generated on',
1827
+ };
1828
+ }
1829
+ formatMoney(amount, currencyCode, locale = 'en') {
1830
+ var _a;
1831
+ const currency = (_a = this.normalizeOptionalText(currencyCode)) !== null && _a !== void 0 ? _a : 'BRL';
1832
+ try {
1833
+ return new Intl.NumberFormat(locale || 'en', {
1834
+ style: 'currency',
1835
+ currency,
1836
+ }).format(Number(amount || 0));
1837
+ }
1838
+ catch (_b) {
1839
+ return `${currency} ${Number(amount || 0).toFixed(2)}`;
1840
+ }
1841
+ }
1842
+ formatDateLabel(value, locale = 'en', fallback = '—') {
1843
+ if (!value) {
1844
+ return fallback;
1845
+ }
1846
+ const date = value instanceof Date ? value : new Date(value);
1847
+ if (Number.isNaN(date.getTime())) {
1848
+ return fallback;
1849
+ }
1850
+ try {
1851
+ return new Intl.DateTimeFormat(locale || 'en', {
1852
+ dateStyle: 'medium',
1853
+ }).format(date);
1854
+ }
1855
+ catch (_a) {
1856
+ return date.toISOString().slice(0, 10);
1857
+ }
1858
+ }
1859
+ sanitizeDocumentHtml(value) {
1860
+ const html = this.normalizeOptionalText(value);
1861
+ if (!html) {
1862
+ return null;
1863
+ }
1864
+ return html
1865
+ .replace(/<script[\s\S]*?<\/script>/gi, '')
1866
+ .replace(/\son\w+=("[^"]*"|'[^']*')/gi, '')
1867
+ .replace(/javascript:/gi, '');
1868
+ }
1869
+ humanizeEnumLabel(value) {
1870
+ return String(value !== null && value !== void 0 ? value : '')
1871
+ .split('_')
1872
+ .filter(Boolean)
1873
+ .map((part) => { var _a; return ((_a = part[0]) === null || _a === void 0 ? void 0 : _a.toUpperCase()) + part.slice(1); })
1874
+ .join(' ');
1875
+ }
1876
+ escapeHtml(value) {
1877
+ return String(value !== null && value !== void 0 ? value : '')
1878
+ .replace(/&/g, '&amp;')
1879
+ .replace(/</g, '&lt;')
1880
+ .replace(/>/g, '&gt;')
1881
+ .replace(/"/g, '&quot;')
1882
+ .replace(/'/g, '&#39;');
1883
+ }
1884
+ async lockProposalForTransition(client, proposalId) {
1885
+ const rows = await client.$queryRawUnsafe(`SELECT id
1886
+ FROM proposal
1887
+ WHERE id = $1
1888
+ AND deleted_at IS NULL
1889
+ FOR UPDATE`, proposalId);
1890
+ if (!Array.isArray(rows) || rows.length === 0) {
1891
+ return null;
1892
+ }
1893
+ return this.loadProposalDetail(client, proposalId);
1894
+ }
1895
+ assertProposalWorkflowReadiness(proposal, actionLabel) {
1896
+ var _a, _b;
1897
+ const currentRevision = this.getCurrentProposalRevision(proposal);
1898
+ if (!currentRevision) {
1899
+ throw new common_1.NotFoundException('Current proposal revision not found.');
1900
+ }
1901
+ const validUntil = (_b = (_a = proposal === null || proposal === void 0 ? void 0 : proposal.valid_until) !== null && _a !== void 0 ? _a : currentRevision === null || currentRevision === void 0 ? void 0 : currentRevision.valid_until) !== null && _b !== void 0 ? _b : null;
1902
+ if (validUntil) {
1903
+ const validUntilDate = new Date(validUntil);
1904
+ if (!Number.isNaN(validUntilDate.getTime()) &&
1905
+ validUntilDate.getTime() < Date.now()) {
1906
+ throw new common_1.BadRequestException(`Expired proposals cannot be ${actionLabel}.`);
1907
+ }
1908
+ }
1909
+ const revisionItems = Array.isArray(currentRevision === null || currentRevision === void 0 ? void 0 : currentRevision.proposal_item)
1910
+ ? currentRevision.proposal_item.filter((item) => item && item.deleted_at == null)
1911
+ : [];
1912
+ const hasPositiveTotal = Number((proposal === null || proposal === void 0 ? void 0 : proposal.total_amount_cents) || 0) > 0;
1913
+ if (revisionItems.length === 0 && !hasPositiveTotal) {
1914
+ throw new common_1.BadRequestException(`Proposals without items or value cannot be ${actionLabel}.`);
1915
+ }
1916
+ }
1917
+ assertCanEdit(status, locale) {
1918
+ if (status === proposal_dto_1.ProposalStatus.APPROVED ||
1919
+ status === proposal_dto_1.ProposalStatus.CONTRACT_GENERATED) {
1920
+ throw new common_1.BadRequestException((0, api_locale_1.getLocaleText)('validation.invalidStatus', locale, 'Approved proposals can no longer be edited'));
1921
+ }
1922
+ }
1923
+ normalizeOptionalText(value) {
1924
+ const normalized = String(value || '').trim();
1925
+ return normalized.length > 0 ? normalized : null;
1926
+ }
1927
+ getPrimaryPersonContactValue(contacts, codes) {
1928
+ var _a, _b;
1929
+ const normalizedCodes = new Set(codes.map((code) => code.toUpperCase()));
1930
+ const items = Array.isArray(contacts)
1931
+ ? contacts.filter((contact) => {
1932
+ var _a;
1933
+ return normalizedCodes.has(String(((_a = contact === null || contact === void 0 ? void 0 : contact.contact_type) === null || _a === void 0 ? void 0 : _a.code) || '').toUpperCase());
1934
+ })
1935
+ : [];
1936
+ const primary = items.find((contact) => contact === null || contact === void 0 ? void 0 : contact.is_primary);
1937
+ const fallback = items[0];
1938
+ const value = (_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;
1939
+ return value != null && String(value).trim().length > 0
1940
+ ? String(value).trim()
1941
+ : null;
1942
+ }
1943
+ async attachPersonTradeNames(client, input) {
1944
+ var _a, _b;
1945
+ if (!input) {
1946
+ return input;
1947
+ }
1948
+ const proposals = Array.isArray(input) ? input : [input];
1949
+ const personIds = Array.from(new Set(proposals
1950
+ .map((proposal) => { var _a, _b, _c; return Number((_c = (_b = (_a = proposal === null || proposal === void 0 ? void 0 : proposal.person) === null || _a === void 0 ? void 0 : _a.id) !== null && _b !== void 0 ? _b : proposal === null || proposal === void 0 ? void 0 : proposal.person_id) !== null && _c !== void 0 ? _c : 0); })
1951
+ .filter((id) => id > 0)));
1952
+ if (personIds.length === 0) {
1953
+ return input;
1954
+ }
1955
+ const [companyRows, contactRows, documentRows] = await Promise.all([
1956
+ client.person_company.findMany({
1957
+ where: {
1958
+ id: {
1959
+ in: personIds,
1960
+ },
1961
+ },
1962
+ select: {
1963
+ id: true,
1964
+ trade_name: true,
1965
+ },
1966
+ }),
1967
+ client.contact.findMany({
1968
+ where: {
1969
+ person_id: {
1970
+ in: personIds,
1971
+ },
1972
+ },
1973
+ include: {
1974
+ contact_type: {
1975
+ select: {
1976
+ code: true,
1977
+ },
1978
+ },
1979
+ },
1980
+ orderBy: [{ is_primary: 'desc' }, { id: 'asc' }],
1981
+ }),
1982
+ client.document.findMany({
1983
+ where: {
1984
+ person_id: {
1985
+ in: personIds,
1986
+ },
1987
+ },
1988
+ select: {
1989
+ id: true,
1990
+ person_id: true,
1991
+ value: true,
1992
+ },
1993
+ orderBy: [{ id: 'asc' }],
1994
+ }),
1995
+ ]);
1996
+ const tradeNameByPersonId = new Map(companyRows.map((row) => {
1997
+ var _a;
1998
+ return [
1999
+ row.id,
2000
+ (_a = row.trade_name) !== null && _a !== void 0 ? _a : null,
2001
+ ];
2002
+ }));
2003
+ const contactsByPersonId = new Map();
2004
+ for (const contact of contactRows) {
2005
+ const current = (_a = contactsByPersonId.get(Number(contact.person_id))) !== null && _a !== void 0 ? _a : [];
2006
+ current.push(contact);
2007
+ contactsByPersonId.set(Number(contact.person_id), current);
2008
+ }
2009
+ const documentsByPersonId = new Map();
2010
+ for (const document of documentRows) {
2011
+ const current = (_b = documentsByPersonId.get(Number(document.person_id))) !== null && _b !== void 0 ? _b : [];
2012
+ current.push(document);
2013
+ documentsByPersonId.set(Number(document.person_id), current);
2014
+ }
2015
+ const normalized = proposals.map((proposal) => {
2016
+ var _a, _b, _c, _d;
2017
+ if (!(proposal === null || proposal === void 0 ? void 0 : proposal.person)) {
2018
+ return proposal;
2019
+ }
2020
+ const personId = Number(proposal.person.id);
2021
+ const contacts = (_a = contactsByPersonId.get(personId)) !== null && _a !== void 0 ? _a : [];
2022
+ const documents = (_b = documentsByPersonId.get(personId)) !== null && _b !== void 0 ? _b : [];
2023
+ return Object.assign(Object.assign({}, proposal), { person: Object.assign(Object.assign({}, proposal.person), { trade_name: (_c = tradeNameByPersonId.get(personId)) !== null && _c !== void 0 ? _c : null, email: this.getPrimaryPersonContactValue(contacts, ['EMAIL']), phone: this.getPrimaryPersonContactValue(contacts, [
2024
+ 'PHONE',
2025
+ 'MOBILE',
2026
+ 'WHATSAPP',
2027
+ ]), document: ((_d = documents[0]) === null || _d === void 0 ? void 0 : _d.value) != null &&
2028
+ String(documents[0].value).trim().length > 0
2029
+ ? String(documents[0].value).trim()
2030
+ : null }) });
2031
+ });
2032
+ return Array.isArray(input) ? normalized : normalized[0];
2033
+ }
2034
+ async loadProposalDetail(client, proposalId) {
2035
+ const proposal = await client.proposal.findFirst({
2036
+ where: {
2037
+ id: proposalId,
2038
+ deleted_at: null,
2039
+ },
2040
+ include: {
2041
+ person: {
2042
+ select: {
2043
+ id: true,
2044
+ name: true,
2045
+ },
2046
+ },
2047
+ proposal_revision: {
2048
+ where: {
2049
+ deleted_at: null,
2050
+ },
2051
+ include: {
2052
+ proposal_item: {
2053
+ where: { deleted_at: null },
2054
+ orderBy: { order: 'asc' },
2055
+ },
2056
+ proposal_document: {
2057
+ where: { deleted_at: null },
2058
+ orderBy: { id: 'desc' },
2059
+ },
2060
+ proposal_approval: {
2061
+ where: { deleted_at: null },
2062
+ orderBy: { step_order: 'asc' },
2063
+ },
2064
+ },
2065
+ orderBy: { revision_number: 'desc' },
2066
+ },
2067
+ proposal_document: {
2068
+ where: {
2069
+ deleted_at: null,
2070
+ },
2071
+ orderBy: {
2072
+ id: 'desc',
2073
+ },
2074
+ },
2075
+ proposal_approval: {
2076
+ where: {
2077
+ deleted_at: null,
2078
+ },
2079
+ orderBy: {
2080
+ step_order: 'asc',
2081
+ },
2082
+ },
2083
+ },
2084
+ });
2085
+ return this.attachPersonTradeNames(client, proposal);
2086
+ }
2087
+ async loadProposalIntegrationSnapshot(client, proposalId) {
2088
+ const proposal = await client.proposal.findFirst({
2089
+ where: {
2090
+ id: proposalId,
2091
+ deleted_at: null,
2092
+ },
2093
+ include: {
2094
+ person: {
2095
+ select: {
2096
+ id: true,
2097
+ name: true,
2098
+ type: true,
2099
+ },
2100
+ },
2101
+ proposal_revision: {
2102
+ where: {
2103
+ deleted_at: null,
2104
+ is_current: true,
2105
+ },
2106
+ include: {
2107
+ proposal_item: {
2108
+ where: { deleted_at: null },
2109
+ orderBy: { order: 'asc' },
2110
+ },
2111
+ proposal_document: {
2112
+ where: { deleted_at: null },
2113
+ orderBy: { id: 'desc' },
2114
+ },
2115
+ },
2116
+ orderBy: { revision_number: 'desc' },
2117
+ take: 1,
2118
+ },
2119
+ },
2120
+ });
2121
+ if (!proposal) {
2122
+ throw new common_1.NotFoundException('Proposal snapshot not found.');
2123
+ }
2124
+ return this.attachPersonTradeNames(client, proposal);
2125
+ }
2126
+ };
2127
+ exports.ProposalService = ProposalService;
2128
+ exports.ProposalService = ProposalService = ProposalService_1 = __decorate([
2129
+ (0, common_1.Injectable)(),
2130
+ __param(2, (0, common_1.Inject)((0, common_1.forwardRef)(() => core_1.FileService))),
2131
+ __param(3, (0, common_1.Inject)((0, common_1.forwardRef)(() => core_1.SettingService))),
2132
+ __metadata("design:paramtypes", [api_prisma_1.PrismaService,
2133
+ core_1.IntegrationDeveloperApiService,
2134
+ core_1.FileService,
2135
+ core_1.SettingService])
2136
+ ], ProposalService);
2137
+ //# sourceMappingURL=proposal.service.js.map