@hed-hog/contact 0.0.301 → 0.0.302
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/person/person.service.d.ts +2 -0
- package/dist/person/person.service.d.ts.map +1 -1
- package/dist/person/person.service.js +111 -127
- package/dist/person/person.service.js.map +1 -1
- package/dist/person/person.service.spec.d.ts +2 -0
- package/dist/person/person.service.spec.d.ts.map +1 -0
- package/dist/person/person.service.spec.js +106 -0
- package/dist/person/person.service.spec.js.map +1 -0
- package/dist/proposal/proposal.service.d.ts +5 -0
- package/dist/proposal/proposal.service.d.ts.map +1 -1
- package/dist/proposal/proposal.service.js +242 -19
- package/dist/proposal/proposal.service.js.map +1 -1
- package/dist/proposal/proposal.service.spec.js +153 -165
- package/dist/proposal/proposal.service.spec.js.map +1 -1
- package/hedhog/data/menu.yaml +35 -18
- package/hedhog/frontend/app/accounts/_components/account-form-sheet.tsx.ejs +517 -346
- package/hedhog/frontend/app/activities/_components/activity-detail-sheet.tsx.ejs +42 -17
- package/hedhog/frontend/app/activities/_components/activity-types.ts.ejs +1 -1
- package/hedhog/frontend/app/activities/page.tsx.ejs +315 -101
- package/hedhog/frontend/app/follow-ups/page.tsx.ejs +172 -22
- package/hedhog/frontend/app/page.tsx.ejs +1 -1
- package/hedhog/frontend/app/person/_components/person-form-sheet.tsx.ejs +1 -1
- package/hedhog/frontend/app/pipeline/_components/lead-detail-sheet.tsx.ejs +1 -1
- package/hedhog/frontend/app/pipeline/_components/lead-proposals-tab.tsx.ejs +505 -428
- package/hedhog/frontend/app/pipeline/page.tsx.ejs +30 -4
- package/hedhog/frontend/app/proposals/_components/proposals-management-page.tsx.ejs +773 -0
- package/hedhog/frontend/app/proposals/approvals/page.tsx.ejs +5 -0
- package/hedhog/frontend/app/proposals/page.tsx.ejs +5 -0
- package/hedhog/frontend/app/reports/page.tsx.ejs +431 -375
- package/hedhog/frontend/messages/en.json +100 -1
- package/hedhog/frontend/messages/pt.json +100 -1
- package/package.json +6 -6
- package/src/person/person.service.spec.ts +143 -0
- package/src/person/person.service.ts +147 -158
- package/src/proposal/proposal.service.spec.ts +196 -0
- package/src/proposal/proposal.service.ts +348 -18
|
@@ -4,39 +4,40 @@ import { PaginationDTO, PaginationService } from '@hed-hog/api-pagination';
|
|
|
4
4
|
import { Prisma, PrismaService } from '@hed-hog/api-prisma';
|
|
5
5
|
import { FileService, SettingService } from '@hed-hog/core';
|
|
6
6
|
import {
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
7
|
+
BadRequestException,
|
|
8
|
+
Inject,
|
|
9
|
+
Injectable,
|
|
10
|
+
Logger,
|
|
11
|
+
NotFoundException,
|
|
12
|
+
forwardRef,
|
|
12
13
|
} from '@nestjs/common';
|
|
13
14
|
import {
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
15
|
+
ACCOUNT_LIFECYCLE_STAGES,
|
|
16
|
+
type AccountLifecycleStage,
|
|
17
|
+
type CreateAccountDTO,
|
|
18
|
+
type UpdateAccountDTO,
|
|
18
19
|
} from './dto/account.dto';
|
|
19
20
|
import {
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
21
|
+
type ActivityListQueryDTO,
|
|
22
|
+
type CrmActivityPriority,
|
|
23
|
+
type CrmActivitySourceKind,
|
|
24
|
+
type CrmActivityStatus,
|
|
25
|
+
type CrmActivityType,
|
|
25
26
|
} from './dto/activity.dto';
|
|
26
27
|
import { CreateFollowupDTO } from './dto/create-followup.dto';
|
|
27
28
|
import {
|
|
28
|
-
|
|
29
|
-
|
|
29
|
+
CreateInteractionDTO,
|
|
30
|
+
PersonInteractionTypeDTO,
|
|
30
31
|
} from './dto/create-interaction.dto';
|
|
31
32
|
import { CreateDTO } from './dto/create.dto';
|
|
32
33
|
import {
|
|
33
|
-
|
|
34
|
-
|
|
34
|
+
type CrmDashboardPeriod,
|
|
35
|
+
type DashboardQueryDTO,
|
|
35
36
|
} from './dto/dashboard-query.dto';
|
|
36
37
|
import { CheckPersonDuplicatesQueryDTO } from './dto/duplicates-query.dto';
|
|
37
38
|
import {
|
|
38
|
-
|
|
39
|
-
|
|
39
|
+
FollowupListQueryDTO,
|
|
40
|
+
FollowupStatsQueryDTO,
|
|
40
41
|
} from './dto/followup-query.dto';
|
|
41
42
|
import { MergePersonDTO } from './dto/merge.dto';
|
|
42
43
|
import { ReportsQueryDTO, type CrmReportGroupBy } from './dto/reports-query.dto';
|
|
@@ -286,6 +287,8 @@ type CrmReportPayload = {
|
|
|
286
287
|
|
|
287
288
|
@Injectable()
|
|
288
289
|
export class PersonService {
|
|
290
|
+
private readonly logger = new Logger(PersonService.name);
|
|
291
|
+
|
|
289
292
|
constructor(
|
|
290
293
|
@Inject(forwardRef(() => PrismaService))
|
|
291
294
|
private readonly prismaService: PrismaService,
|
|
@@ -2055,17 +2058,17 @@ export class PersonService {
|
|
|
2055
2058
|
LIFECYCLE_STAGE_METADATA_KEY,
|
|
2056
2059
|
data.lifecycle_stage,
|
|
2057
2060
|
);
|
|
2058
|
-
|
|
2059
|
-
if (nextLifecycleStage && currentLifecycleStage !== nextLifecycleStage) {
|
|
2060
|
-
await this.registerStageTransition(tx, {
|
|
2061
|
-
personId: person.id,
|
|
2062
|
-
fromStage: currentLifecycleStage,
|
|
2063
|
-
toStage: nextLifecycleStage,
|
|
2064
|
-
changedByUserId: actorUserId,
|
|
2065
|
-
});
|
|
2066
|
-
}
|
|
2067
2061
|
});
|
|
2068
2062
|
|
|
2063
|
+
if (nextLifecycleStage && currentLifecycleStage !== nextLifecycleStage) {
|
|
2064
|
+
await this.registerStageTransitionSafely(this.prismaService, {
|
|
2065
|
+
personId: person.id,
|
|
2066
|
+
fromStage: currentLifecycleStage,
|
|
2067
|
+
toStage: nextLifecycleStage,
|
|
2068
|
+
changedByUserId: actorUserId,
|
|
2069
|
+
});
|
|
2070
|
+
}
|
|
2071
|
+
|
|
2069
2072
|
return {
|
|
2070
2073
|
success: true,
|
|
2071
2074
|
lifecycle_stage: data.lifecycle_stage,
|
|
@@ -2137,6 +2140,15 @@ export class PersonService {
|
|
|
2137
2140
|
|
|
2138
2141
|
const currentLifecycleStage = await this.getPersonLifecycleStage(id);
|
|
2139
2142
|
const nextLifecycleStage = this.normalizeTextOrNull(data.lifecycle_stage);
|
|
2143
|
+
const stageTransitionParams =
|
|
2144
|
+
nextLifecycleStage && currentLifecycleStage !== nextLifecycleStage
|
|
2145
|
+
? {
|
|
2146
|
+
personId: id,
|
|
2147
|
+
fromStage: currentLifecycleStage,
|
|
2148
|
+
toStage: nextLifecycleStage,
|
|
2149
|
+
changedByUserId: this.coerceNumber(user?.id) || null,
|
|
2150
|
+
}
|
|
2151
|
+
: null;
|
|
2140
2152
|
|
|
2141
2153
|
const incomingContacts = Array.isArray(data.contacts) ? data.contacts : [];
|
|
2142
2154
|
const incomingAddresses = Array.isArray(data.addresses) ? data.addresses : [];
|
|
@@ -2182,18 +2194,16 @@ export class PersonService {
|
|
|
2182
2194
|
await this.syncAddresses(tx, id, incomingAddresses, locale);
|
|
2183
2195
|
await this.syncDocuments(tx, id, incomingDocuments);
|
|
2184
2196
|
|
|
2185
|
-
if (nextLifecycleStage && currentLifecycleStage !== nextLifecycleStage) {
|
|
2186
|
-
await this.registerStageTransition(tx, {
|
|
2187
|
-
personId: id,
|
|
2188
|
-
fromStage: currentLifecycleStage,
|
|
2189
|
-
toStage: nextLifecycleStage,
|
|
2190
|
-
changedByUserId: this.coerceNumber(user?.id) || null,
|
|
2191
|
-
});
|
|
2192
|
-
}
|
|
2193
|
-
|
|
2194
2197
|
return { success: true };
|
|
2195
2198
|
})
|
|
2196
2199
|
.then(async (result) => {
|
|
2200
|
+
if (stageTransitionParams) {
|
|
2201
|
+
await this.registerStageTransitionSafely(
|
|
2202
|
+
this.prismaService,
|
|
2203
|
+
stageTransitionParams,
|
|
2204
|
+
);
|
|
2205
|
+
}
|
|
2206
|
+
|
|
2197
2207
|
await this.cleanupReplacedAvatar(locale, person.avatar_id, data.avatar_id);
|
|
2198
2208
|
return result;
|
|
2199
2209
|
});
|
|
@@ -3068,8 +3078,28 @@ export class PersonService {
|
|
|
3068
3078
|
return this.metadataToString(metadata?.value) ?? 'new';
|
|
3069
3079
|
}
|
|
3070
3080
|
|
|
3081
|
+
private async registerStageTransitionSafely(
|
|
3082
|
+
client: any,
|
|
3083
|
+
params: {
|
|
3084
|
+
personId: number;
|
|
3085
|
+
fromStage: string | null;
|
|
3086
|
+
toStage: string;
|
|
3087
|
+
changedByUserId: number | null;
|
|
3088
|
+
},
|
|
3089
|
+
) {
|
|
3090
|
+
try {
|
|
3091
|
+
await this.registerStageTransition(client, params);
|
|
3092
|
+
} catch (error) {
|
|
3093
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
3094
|
+
|
|
3095
|
+
this.logger.warn(
|
|
3096
|
+
`Failed to persist CRM stage history for person ${params.personId} (${params.fromStage ?? 'null'} -> ${params.toStage}). Continuing without audit entry. Reason: ${reason}`,
|
|
3097
|
+
);
|
|
3098
|
+
}
|
|
3099
|
+
}
|
|
3100
|
+
|
|
3071
3101
|
private async registerStageTransition(
|
|
3072
|
-
|
|
3102
|
+
client: any,
|
|
3073
3103
|
{
|
|
3074
3104
|
personId,
|
|
3075
3105
|
fromStage,
|
|
@@ -3086,32 +3116,15 @@ export class PersonService {
|
|
|
3086
3116
|
return;
|
|
3087
3117
|
}
|
|
3088
3118
|
|
|
3089
|
-
|
|
3090
|
-
|
|
3091
|
-
|
|
3092
|
-
|
|
3093
|
-
|
|
3094
|
-
|
|
3095
|
-
|
|
3096
|
-
|
|
3097
|
-
|
|
3098
|
-
to_stage,
|
|
3099
|
-
changed_by_user_id,
|
|
3100
|
-
changed_at,
|
|
3101
|
-
created_at,
|
|
3102
|
-
updated_at
|
|
3103
|
-
)
|
|
3104
|
-
VALUES (
|
|
3105
|
-
${personId},
|
|
3106
|
-
${fromStageSql},
|
|
3107
|
-
CAST(${toStage} AS crm_stage_history_to_stage_enum),
|
|
3108
|
-
${changedByUserId},
|
|
3109
|
-
NOW(),
|
|
3110
|
-
NOW(),
|
|
3111
|
-
NOW()
|
|
3112
|
-
)
|
|
3113
|
-
`,
|
|
3114
|
-
);
|
|
3119
|
+
await client.crm_stage_history.create({
|
|
3120
|
+
data: {
|
|
3121
|
+
person_id: personId,
|
|
3122
|
+
from_stage: fromStage ?? null,
|
|
3123
|
+
to_stage: toStage,
|
|
3124
|
+
changed_by_user_id: changedByUserId,
|
|
3125
|
+
changed_at: new Date(),
|
|
3126
|
+
},
|
|
3127
|
+
});
|
|
3115
3128
|
}
|
|
3116
3129
|
|
|
3117
3130
|
private async syncPersonSubtypeData(
|
|
@@ -4047,69 +4060,59 @@ export class PersonService {
|
|
|
4047
4060
|
actorUserId: number | null;
|
|
4048
4061
|
},
|
|
4049
4062
|
) {
|
|
4050
|
-
const
|
|
4051
|
-
|
|
4052
|
-
|
|
4053
|
-
|
|
4054
|
-
|
|
4055
|
-
|
|
4056
|
-
|
|
4057
|
-
|
|
4058
|
-
|
|
4059
|
-
|
|
4060
|
-
)) as Array<{ id: number }>;
|
|
4061
|
-
const existing = existingRows[0];
|
|
4063
|
+
const existing = await tx.crm_activity.findFirst({
|
|
4064
|
+
where: {
|
|
4065
|
+
person_id: personId,
|
|
4066
|
+
source_kind: 'followup',
|
|
4067
|
+
completed_at: null,
|
|
4068
|
+
},
|
|
4069
|
+
orderBy: {
|
|
4070
|
+
id: 'desc',
|
|
4071
|
+
},
|
|
4072
|
+
});
|
|
4062
4073
|
|
|
4063
4074
|
const normalizedNotes = this.normalizeTextOrNull(notes);
|
|
4075
|
+
const subject = this.getFollowupActivitySubject();
|
|
4076
|
+
const dueAtDate = new Date(dueAt);
|
|
4077
|
+
const priority: CrmActivityPriority = 'medium';
|
|
4078
|
+
const type: CrmActivityType = 'task';
|
|
4064
4079
|
|
|
4065
4080
|
if (existing) {
|
|
4066
|
-
await tx
|
|
4067
|
-
|
|
4068
|
-
|
|
4069
|
-
|
|
4070
|
-
|
|
4071
|
-
|
|
4072
|
-
subject = ${this.getFollowupActivitySubject()},
|
|
4073
|
-
notes = ${normalizedNotes},
|
|
4074
|
-
due_at = CAST(${dueAt} AS TIMESTAMPTZ),
|
|
4075
|
-
priority = CAST(${'medium'} AS crm_activity_priority_enum),
|
|
4076
|
-
updated_at = NOW()
|
|
4077
|
-
WHERE id = ${existing.id}
|
|
4078
|
-
`,
|
|
4079
|
-
);
|
|
4080
|
-
return;
|
|
4081
|
-
}
|
|
4082
|
-
|
|
4083
|
-
await tx.$executeRaw(
|
|
4084
|
-
Prisma.sql`
|
|
4085
|
-
INSERT INTO crm_activity (
|
|
4086
|
-
person_id,
|
|
4087
|
-
owner_user_id,
|
|
4088
|
-
created_by_user_id,
|
|
4081
|
+
await tx.crm_activity.update({
|
|
4082
|
+
where: {
|
|
4083
|
+
id: existing.id,
|
|
4084
|
+
},
|
|
4085
|
+
data: {
|
|
4086
|
+
owner_user_id: ownerUserId,
|
|
4089
4087
|
type,
|
|
4090
4088
|
subject,
|
|
4091
|
-
notes,
|
|
4092
|
-
due_at,
|
|
4089
|
+
notes: normalizedNotes,
|
|
4090
|
+
due_at: dueAtDate,
|
|
4093
4091
|
priority,
|
|
4094
|
-
|
|
4095
|
-
|
|
4096
|
-
|
|
4097
|
-
|
|
4098
|
-
|
|
4099
|
-
|
|
4100
|
-
|
|
4101
|
-
|
|
4102
|
-
|
|
4103
|
-
|
|
4104
|
-
|
|
4105
|
-
|
|
4106
|
-
|
|
4107
|
-
|
|
4108
|
-
|
|
4109
|
-
|
|
4110
|
-
|
|
4111
|
-
|
|
4112
|
-
|
|
4092
|
+
updated_at: new Date(),
|
|
4093
|
+
},
|
|
4094
|
+
});
|
|
4095
|
+
return;
|
|
4096
|
+
}
|
|
4097
|
+
|
|
4098
|
+
const sourceKind: CrmActivitySourceKind = 'followup';
|
|
4099
|
+
const now = new Date();
|
|
4100
|
+
|
|
4101
|
+
await tx.crm_activity.create({
|
|
4102
|
+
data: {
|
|
4103
|
+
person_id: personId,
|
|
4104
|
+
owner_user_id: ownerUserId,
|
|
4105
|
+
created_by_user_id: actorUserId,
|
|
4106
|
+
type,
|
|
4107
|
+
subject,
|
|
4108
|
+
notes: normalizedNotes,
|
|
4109
|
+
due_at: dueAtDate,
|
|
4110
|
+
priority,
|
|
4111
|
+
source_kind: sourceKind,
|
|
4112
|
+
created_at: now,
|
|
4113
|
+
updated_at: now,
|
|
4114
|
+
},
|
|
4115
|
+
});
|
|
4113
4116
|
}
|
|
4114
4117
|
|
|
4115
4118
|
private async createCompletedInteractionActivity(
|
|
@@ -4127,41 +4130,27 @@ export class PersonService {
|
|
|
4127
4130
|
},
|
|
4128
4131
|
) {
|
|
4129
4132
|
const completedAt = new Date(interaction.created_at);
|
|
4133
|
+
const type: CrmActivityType = interaction.type;
|
|
4134
|
+
const priority: CrmActivityPriority = 'medium';
|
|
4135
|
+
const sourceKind: CrmActivitySourceKind = 'interaction';
|
|
4130
4136
|
|
|
4131
|
-
await tx
|
|
4132
|
-
|
|
4133
|
-
|
|
4134
|
-
|
|
4135
|
-
|
|
4136
|
-
|
|
4137
|
-
|
|
4138
|
-
|
|
4139
|
-
|
|
4140
|
-
|
|
4141
|
-
|
|
4142
|
-
|
|
4143
|
-
|
|
4144
|
-
|
|
4145
|
-
|
|
4146
|
-
|
|
4147
|
-
|
|
4148
|
-
VALUES (
|
|
4149
|
-
${personId},
|
|
4150
|
-
${ownerUserId},
|
|
4151
|
-
${actorUserId},
|
|
4152
|
-
${actorUserId},
|
|
4153
|
-
CAST(${interaction.type} AS crm_activity_type_enum),
|
|
4154
|
-
${this.getInteractionActivitySubject(interaction.type)},
|
|
4155
|
-
${this.normalizeTextOrNull(interaction.notes)},
|
|
4156
|
-
CAST(${interaction.created_at} AS TIMESTAMPTZ),
|
|
4157
|
-
CAST(${interaction.created_at} AS TIMESTAMPTZ),
|
|
4158
|
-
CAST(${'medium'} AS crm_activity_priority_enum),
|
|
4159
|
-
CAST(${'interaction'} AS crm_activity_source_kind_enum),
|
|
4160
|
-
CAST(${interaction.created_at} AS TIMESTAMPTZ),
|
|
4161
|
-
NOW()
|
|
4162
|
-
)
|
|
4163
|
-
`,
|
|
4164
|
-
);
|
|
4137
|
+
await tx.crm_activity.create({
|
|
4138
|
+
data: {
|
|
4139
|
+
person_id: personId,
|
|
4140
|
+
owner_user_id: ownerUserId,
|
|
4141
|
+
created_by_user_id: actorUserId,
|
|
4142
|
+
completed_by_user_id: actorUserId,
|
|
4143
|
+
type,
|
|
4144
|
+
subject: this.getInteractionActivitySubject(interaction.type),
|
|
4145
|
+
notes: this.normalizeTextOrNull(interaction.notes),
|
|
4146
|
+
due_at: completedAt,
|
|
4147
|
+
completed_at: completedAt,
|
|
4148
|
+
priority,
|
|
4149
|
+
source_kind: sourceKind,
|
|
4150
|
+
created_at: completedAt,
|
|
4151
|
+
updated_at: new Date(),
|
|
4152
|
+
},
|
|
4153
|
+
});
|
|
4165
4154
|
}
|
|
4166
4155
|
|
|
4167
4156
|
private getFollowupActivitySubject() {
|
|
@@ -4494,7 +4483,7 @@ export class PersonService {
|
|
|
4494
4483
|
|
|
4495
4484
|
if (lifecycleStage && lifecycleStage !== 'all') {
|
|
4496
4485
|
filters.push(
|
|
4497
|
-
Prisma.sql`AND pc.account_lifecycle_stage =
|
|
4486
|
+
Prisma.sql`AND pc.account_lifecycle_stage = ${lifecycleStage}`,
|
|
4498
4487
|
);
|
|
4499
4488
|
}
|
|
4500
4489
|
|
|
@@ -4602,12 +4591,12 @@ export class PersonService {
|
|
|
4602
4591
|
}
|
|
4603
4592
|
|
|
4604
4593
|
if (type && type !== 'all') {
|
|
4605
|
-
filters.push(Prisma.sql`AND a.type =
|
|
4594
|
+
filters.push(Prisma.sql`AND a.type = ${type}`);
|
|
4606
4595
|
}
|
|
4607
4596
|
|
|
4608
4597
|
if (priority && priority !== 'all') {
|
|
4609
4598
|
filters.push(
|
|
4610
|
-
Prisma.sql`AND a.priority =
|
|
4599
|
+
Prisma.sql`AND a.priority = ${priority}`,
|
|
4611
4600
|
);
|
|
4612
4601
|
}
|
|
4613
4602
|
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
/// <reference types="jest" />
|
|
2
|
+
import { ProposalService } from './proposal.service';
|
|
3
|
+
|
|
4
|
+
describe('ProposalService regression coverage', () => {
|
|
5
|
+
let prisma: any;
|
|
6
|
+
let service: ProposalService;
|
|
7
|
+
|
|
8
|
+
beforeEach(() => {
|
|
9
|
+
prisma = {
|
|
10
|
+
proposal: {
|
|
11
|
+
findMany: jest.fn().mockImplementation(({ include }) => {
|
|
12
|
+
if (
|
|
13
|
+
include?.person?.select?.trade_name ||
|
|
14
|
+
include?.person?.select?.email ||
|
|
15
|
+
include?.person?.select?.phone
|
|
16
|
+
) {
|
|
17
|
+
throw new Error(
|
|
18
|
+
'Unknown field selected directly on model `person`.',
|
|
19
|
+
);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
return Promise.resolve([
|
|
23
|
+
{
|
|
24
|
+
id: 101,
|
|
25
|
+
person_id: 8,
|
|
26
|
+
person: {
|
|
27
|
+
id: 8,
|
|
28
|
+
name: 'Ana Cliente',
|
|
29
|
+
email: 'contato@cliente.test',
|
|
30
|
+
phone: '+55 11 99999-0000',
|
|
31
|
+
},
|
|
32
|
+
proposal_revision: [],
|
|
33
|
+
},
|
|
34
|
+
]);
|
|
35
|
+
}),
|
|
36
|
+
count: jest.fn().mockResolvedValue(1),
|
|
37
|
+
aggregate: jest.fn().mockResolvedValue({
|
|
38
|
+
_sum: {
|
|
39
|
+
total_amount_cents: 0,
|
|
40
|
+
},
|
|
41
|
+
}),
|
|
42
|
+
},
|
|
43
|
+
person_company: {
|
|
44
|
+
findMany: jest.fn().mockResolvedValue([
|
|
45
|
+
{
|
|
46
|
+
id: 8,
|
|
47
|
+
trade_name: 'Cliente SA',
|
|
48
|
+
},
|
|
49
|
+
]),
|
|
50
|
+
},
|
|
51
|
+
contact: {
|
|
52
|
+
findMany: jest.fn().mockResolvedValue([
|
|
53
|
+
{
|
|
54
|
+
id: 1,
|
|
55
|
+
person_id: 8,
|
|
56
|
+
value: 'contato@cliente.test',
|
|
57
|
+
is_primary: true,
|
|
58
|
+
contact_type: {
|
|
59
|
+
code: 'EMAIL',
|
|
60
|
+
},
|
|
61
|
+
},
|
|
62
|
+
{
|
|
63
|
+
id: 2,
|
|
64
|
+
person_id: 8,
|
|
65
|
+
value: '+55 11 99999-0000',
|
|
66
|
+
is_primary: true,
|
|
67
|
+
contact_type: {
|
|
68
|
+
code: 'PHONE',
|
|
69
|
+
},
|
|
70
|
+
},
|
|
71
|
+
]),
|
|
72
|
+
},
|
|
73
|
+
document: {
|
|
74
|
+
findMany: jest.fn().mockResolvedValue([
|
|
75
|
+
{
|
|
76
|
+
id: 3,
|
|
77
|
+
person_id: 8,
|
|
78
|
+
value: '12.345.678/0001-90',
|
|
79
|
+
},
|
|
80
|
+
]),
|
|
81
|
+
},
|
|
82
|
+
person_metadata: {
|
|
83
|
+
findFirst: jest.fn().mockResolvedValue(null),
|
|
84
|
+
update: jest.fn().mockResolvedValue(null),
|
|
85
|
+
create: jest.fn().mockResolvedValue(null),
|
|
86
|
+
delete: jest.fn().mockResolvedValue(null),
|
|
87
|
+
},
|
|
88
|
+
$transaction: jest.fn(),
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
service = new ProposalService(
|
|
92
|
+
prisma,
|
|
93
|
+
{
|
|
94
|
+
findLinksBySource: jest.fn(),
|
|
95
|
+
publishEvent: jest.fn(),
|
|
96
|
+
} as any,
|
|
97
|
+
{
|
|
98
|
+
upload: jest.fn(),
|
|
99
|
+
} as any,
|
|
100
|
+
{
|
|
101
|
+
getSettingValues: jest.fn(),
|
|
102
|
+
} as any,
|
|
103
|
+
);
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
it('lists proposals without selecting a missing person.trade_name column', async () => {
|
|
107
|
+
const result = await service.list({
|
|
108
|
+
search: 'cliente',
|
|
109
|
+
take: 20,
|
|
110
|
+
skip: 0,
|
|
111
|
+
pageSize: 20,
|
|
112
|
+
} as any);
|
|
113
|
+
|
|
114
|
+
expect(prisma.proposal.findMany).toHaveBeenCalledWith(
|
|
115
|
+
expect.objectContaining({
|
|
116
|
+
include: expect.objectContaining({
|
|
117
|
+
person: expect.objectContaining({
|
|
118
|
+
select: expect.not.objectContaining({
|
|
119
|
+
trade_name: true,
|
|
120
|
+
}),
|
|
121
|
+
}),
|
|
122
|
+
}),
|
|
123
|
+
}),
|
|
124
|
+
);
|
|
125
|
+
expect(prisma.person_company.findMany).toHaveBeenCalledWith({
|
|
126
|
+
where: {
|
|
127
|
+
id: {
|
|
128
|
+
in: [8],
|
|
129
|
+
},
|
|
130
|
+
},
|
|
131
|
+
select: {
|
|
132
|
+
id: true,
|
|
133
|
+
trade_name: true,
|
|
134
|
+
},
|
|
135
|
+
});
|
|
136
|
+
expect(prisma.contact.findMany).toHaveBeenCalled();
|
|
137
|
+
expect(prisma.document.findMany).toHaveBeenCalled();
|
|
138
|
+
expect(result.data).toEqual(
|
|
139
|
+
expect.arrayContaining([
|
|
140
|
+
expect.objectContaining({
|
|
141
|
+
person: expect.objectContaining({
|
|
142
|
+
trade_name: 'Cliente SA',
|
|
143
|
+
email: 'contato@cliente.test',
|
|
144
|
+
phone: '+55 11 99999-0000',
|
|
145
|
+
document: '12.345.678/0001-90',
|
|
146
|
+
}),
|
|
147
|
+
}),
|
|
148
|
+
]),
|
|
149
|
+
);
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
it('syncs person deal value metadata from approved and converted proposal totals', async () => {
|
|
153
|
+
prisma.proposal.aggregate.mockResolvedValue({
|
|
154
|
+
_sum: {
|
|
155
|
+
total_amount_cents: 125050,
|
|
156
|
+
},
|
|
157
|
+
});
|
|
158
|
+
prisma.person_metadata.findFirst.mockResolvedValue({ id: 77 });
|
|
159
|
+
|
|
160
|
+
await (service as any).syncPersonDealValueFromApprovedProposals(prisma, 8);
|
|
161
|
+
|
|
162
|
+
expect(prisma.proposal.aggregate).toHaveBeenCalledWith({
|
|
163
|
+
where: {
|
|
164
|
+
deleted_at: null,
|
|
165
|
+
person_id: 8,
|
|
166
|
+
status: {
|
|
167
|
+
in: ['approved', 'contract_generated'],
|
|
168
|
+
},
|
|
169
|
+
},
|
|
170
|
+
_sum: {
|
|
171
|
+
total_amount_cents: true,
|
|
172
|
+
},
|
|
173
|
+
});
|
|
174
|
+
expect(prisma.person_metadata.update).toHaveBeenCalledWith({
|
|
175
|
+
where: { id: 77 },
|
|
176
|
+
data: { value: '1250.50' },
|
|
177
|
+
});
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
it('clears person deal value metadata when there are no approved proposals left', async () => {
|
|
181
|
+
prisma.proposal.aggregate.mockResolvedValue({
|
|
182
|
+
_sum: {
|
|
183
|
+
total_amount_cents: 0,
|
|
184
|
+
},
|
|
185
|
+
});
|
|
186
|
+
prisma.person_metadata.findFirst.mockResolvedValue({ id: 88 });
|
|
187
|
+
|
|
188
|
+
await (service as any).syncPersonDealValueFromApprovedProposals(prisma, 8);
|
|
189
|
+
|
|
190
|
+
expect(prisma.person_metadata.delete).toHaveBeenCalledWith({
|
|
191
|
+
where: { id: 88 },
|
|
192
|
+
});
|
|
193
|
+
expect(prisma.person_metadata.create).not.toHaveBeenCalled();
|
|
194
|
+
expect(prisma.person_metadata.update).not.toHaveBeenCalled();
|
|
195
|
+
});
|
|
196
|
+
});
|