From d54ef20168472ef83f310a1979342da7877ba7f1 Mon Sep 17 00:00:00 2001 From: Navaneethakrishnan Date: Wed, 7 Aug 2024 15:44:11 +0530 Subject: [PATCH] feat: introduced API to close member interaction follow up --- .forestadmin-schema.json | 290 +++++------------- .../migration.sql | 2 + apps/web-api/prisma/schema.prisma | 1 + .../office-hours/office-hours.controller.ts | 57 +++- .../src/office-hours/office-hours.service.ts | 13 +- apps/web-api/src/utils/constants.ts | 14 +- .../src/lib/contract-member-interaction.ts | 9 + libs/contracts/src/schema/member-follow-up.ts | 2 +- 8 files changed, 152 insertions(+), 236 deletions(-) create mode 100644 apps/web-api/prisma/migrations/20240807080204_member_interaction_followup_type/migration.sql diff --git a/.forestadmin-schema.json b/.forestadmin-schema.json index 78f3ca265..10094ba49 100644 --- a/.forestadmin-schema.json +++ b/.forestadmin-schema.json @@ -1100,17 +1100,17 @@ { "defaultValue": null, "enums": null, - "field": "Teams", + "field": "_IndustryTagToTeams", "integration": null, - "inverseOf": "IndustryTags", + "inverseOf": "IndustryTag", "isFilterable": false, "isPrimaryKey": false, "isReadOnly": false, "isRequired": false, "isSortable": false, "isVirtual": false, - "reference": "Team.id", - "relationship": "BelongsToMany", + "reference": "_IndustryTagToTeam.id", + "relationship": "HasMany", "type": ["Number"], "validations": [] }, @@ -1796,24 +1796,24 @@ { "defaultValue": null, "enums": null, - "field": "Skills", + "field": "TeamFocusAreaVersionHistories", "integration": null, - "inverseOf": "Members", + "inverseOf": "Member", "isFilterable": false, "isPrimaryKey": false, "isReadOnly": false, "isRequired": false, "isSortable": false, "isVirtual": false, - "reference": "Skill.id", - "relationship": "BelongsToMany", - "type": ["Number"], + "reference": "TeamFocusAreaVersionHistory.uid", + "relationship": "HasMany", + "type": ["String"], "validations": [] }, { "defaultValue": null, "enums": null, - "field": "TeamFocusAreaVersionHistories", + "field": "TeamMemberRoles", "integration": null, "inverseOf": "Member", "isFilterable": false, @@ -1822,7 +1822,7 @@ "isRequired": false, "isSortable": false, "isVirtual": false, - "reference": "TeamFocusAreaVersionHistory.uid", + "reference": "TeamMemberRole.uid", "relationship": "HasMany", "type": ["String"], "validations": [] @@ -1830,7 +1830,7 @@ { "defaultValue": null, "enums": null, - "field": "TeamMemberRoles", + "field": "Teams", "integration": null, "inverseOf": "Member", "isFilterable": false, @@ -1839,7 +1839,7 @@ "isRequired": false, "isSortable": false, "isVirtual": false, - "reference": "TeamMemberRole.uid", + "reference": "Team.uid", "relationship": "HasMany", "type": ["String"], "validations": [] @@ -1847,7 +1847,7 @@ { "defaultValue": null, "enums": null, - "field": "Teams", + "field": "_MemberToMemberRoles", "integration": null, "inverseOf": "Member", "isFilterable": false, @@ -1856,15 +1856,15 @@ "isRequired": false, "isSortable": false, "isVirtual": false, - "reference": "Team.uid", + "reference": "_MemberToMemberRole.id", "relationship": "HasMany", - "type": ["String"], + "type": ["Number"], "validations": [] }, { "defaultValue": null, "enums": null, - "field": "_MemberToMemberRoles", + "field": "_MemberToSkills", "integration": null, "inverseOf": "Member", "isFilterable": false, @@ -1873,7 +1873,7 @@ "isRequired": false, "isSortable": false, "isVirtual": false, - "reference": "_MemberToMemberRole.id", + "reference": "_MemberToSkill.id", "relationship": "HasMany", "type": ["Number"], "validations": [] @@ -2541,7 +2541,7 @@ }, { "defaultValue": null, - "enums": ["PENDING", "COMPLETED"], + "enums": ["PENDING", "COMPLETED", "CLOSED"], "field": "status", "integration": null, "inverseOf": null, @@ -2926,17 +2926,17 @@ { "defaultValue": null, "enums": null, - "field": "Teams", + "field": "_MembershipSourceToTeams", "integration": null, - "inverseOf": "MembershipSources", + "inverseOf": "MembershipSource", "isFilterable": false, "isPrimaryKey": false, "isReadOnly": false, "isRequired": false, "isSortable": false, "isVirtual": false, - "reference": "Team.id", - "relationship": "BelongsToMany", + "reference": "_MembershipSourceToTeam.id", + "relationship": "HasMany", "type": ["Number"], "validations": [] }, @@ -4432,17 +4432,17 @@ { "defaultValue": null, "enums": null, - "field": "Members", + "field": "_MemberToSkills", "integration": null, - "inverseOf": "Skills", + "inverseOf": "Skill", "isFilterable": false, "isPrimaryKey": false, "isReadOnly": false, "isRequired": false, "isSortable": false, "isVirtual": false, - "reference": "Member.id", - "relationship": "BelongsToMany", + "reference": "_MemberToSkill.id", + "relationship": "HasMany", "type": ["Number"], "validations": [] }, @@ -4610,58 +4610,58 @@ { "defaultValue": null, "enums": null, - "field": "IndustryTags", + "field": "Member", "integration": null, "inverseOf": "Teams", - "isFilterable": false, + "isFilterable": true, "isPrimaryKey": false, "isReadOnly": false, "isRequired": false, - "isSortable": false, + "isSortable": true, "isVirtual": false, - "reference": "IndustryTag.id", - "relationship": "BelongsToMany", - "type": ["Number"], + "reference": "Member.uid", + "relationship": "BelongsTo", + "type": "String", "validations": [] }, { "defaultValue": null, "enums": null, - "field": "Member", + "field": "PLEventGuests", "integration": null, - "inverseOf": "Teams", - "isFilterable": true, + "inverseOf": "Team", + "isFilterable": false, "isPrimaryKey": false, "isReadOnly": false, "isRequired": false, - "isSortable": true, + "isSortable": false, "isVirtual": false, - "reference": "Member.uid", - "relationship": "BelongsTo", - "type": "String", + "reference": "PLEventGuest.uid", + "relationship": "HasMany", + "type": ["String"], "validations": [] }, { "defaultValue": null, "enums": null, - "field": "MembershipSources", + "field": "Projects", "integration": null, - "inverseOf": "Teams", + "inverseOf": "Team", "isFilterable": false, "isPrimaryKey": false, "isReadOnly": false, "isRequired": false, "isSortable": false, "isVirtual": false, - "reference": "MembershipSource.id", - "relationship": "BelongsToMany", - "type": ["Number"], + "reference": "Project.uid", + "relationship": "HasMany", + "type": ["String"], "validations": [] }, { "defaultValue": null, "enums": null, - "field": "PLEventGuests", + "field": "TeamFocusAreaVersionHistories", "integration": null, "inverseOf": "Team", "isFilterable": false, @@ -4670,7 +4670,7 @@ "isRequired": false, "isSortable": false, "isVirtual": false, - "reference": "PLEventGuest.uid", + "reference": "TeamFocusAreaVersionHistory.uid", "relationship": "HasMany", "type": ["String"], "validations": [] @@ -4678,7 +4678,7 @@ { "defaultValue": null, "enums": null, - "field": "Projects", + "field": "TeamFocusAreas", "integration": null, "inverseOf": "Team", "isFilterable": false, @@ -4687,7 +4687,7 @@ "isRequired": false, "isSortable": false, "isVirtual": false, - "reference": "Project.uid", + "reference": "TeamFocusArea.uid", "relationship": "HasMany", "type": ["String"], "validations": [] @@ -4695,7 +4695,7 @@ { "defaultValue": null, "enums": null, - "field": "TeamFocusAreaVersionHistories", + "field": "TeamMemberRoles", "integration": null, "inverseOf": "Team", "isFilterable": false, @@ -4704,7 +4704,7 @@ "isRequired": false, "isSortable": false, "isVirtual": false, - "reference": "TeamFocusAreaVersionHistory.uid", + "reference": "TeamMemberRole.uid", "relationship": "HasMany", "type": ["String"], "validations": [] @@ -4712,7 +4712,7 @@ { "defaultValue": null, "enums": null, - "field": "TeamFocusAreas", + "field": "_IndustryTagToTeams", "integration": null, "inverseOf": "Team", "isFilterable": false, @@ -4721,15 +4721,15 @@ "isRequired": false, "isSortable": false, "isVirtual": false, - "reference": "TeamFocusArea.uid", + "reference": "_IndustryTagToTeam.id", "relationship": "HasMany", - "type": ["String"], + "type": ["Number"], "validations": [] }, { "defaultValue": null, "enums": null, - "field": "TeamMemberRoles", + "field": "_MembershipSourceToTeams", "integration": null, "inverseOf": "Team", "isFilterable": false, @@ -4738,25 +4738,25 @@ "isRequired": false, "isSortable": false, "isVirtual": false, - "reference": "TeamMemberRole.uid", + "reference": "_MembershipSourceToTeam.id", "relationship": "HasMany", - "type": ["String"], + "type": ["Number"], "validations": [] }, { "defaultValue": null, "enums": null, - "field": "Technologies", + "field": "_TeamToTechnologies", "integration": null, - "inverseOf": "Teams", + "inverseOf": "Team", "isFilterable": false, "isPrimaryKey": false, "isReadOnly": false, "isRequired": false, "isSortable": false, "isVirtual": false, - "reference": "Technology.id", - "relationship": "BelongsToMany", + "reference": "_TeamToTechnology.id", + "relationship": "HasMany", "type": ["Number"], "validations": [] }, @@ -5534,17 +5534,17 @@ { "defaultValue": null, "enums": null, - "field": "Teams", + "field": "_TeamToTechnologies", "integration": null, - "inverseOf": "Technologies", + "inverseOf": "Technology", "isFilterable": false, "isPrimaryKey": false, "isReadOnly": false, "isRequired": false, "isSortable": false, "isVirtual": false, - "reference": "Team.id", - "relationship": "BelongsToMany", + "reference": "_TeamToTechnology.id", + "relationship": "HasMany", "type": ["Number"], "validations": [] }, @@ -5646,48 +5646,12 @@ { "actions": [], "fields": [ - { - "defaultValue": null, - "enums": null, - "field": "A", - "integration": null, - "inverseOf": null, - "isFilterable": true, - "isPrimaryKey": true, - "isReadOnly": true, - "isRequired": true, - "isSortable": true, - "isVirtual": false, - "reference": null, - "type": "Number", - "validations": [ - {"type": "is present", "message": "Failed validation rule: 'Present'"} - ] - }, - { - "defaultValue": null, - "enums": null, - "field": "B", - "integration": null, - "inverseOf": null, - "isFilterable": true, - "isPrimaryKey": true, - "isReadOnly": true, - "isRequired": true, - "isSortable": true, - "isVirtual": false, - "reference": null, - "type": "Number", - "validations": [ - {"type": "is present", "message": "Failed validation rule: 'Present'"} - ] - }, { "defaultValue": null, "enums": null, "field": "IndustryTag", "integration": null, - "inverseOf": null, + "inverseOf": "_IndustryTagToTeams", "isFilterable": true, "isPrimaryKey": false, "isReadOnly": false, @@ -5706,7 +5670,7 @@ "enums": null, "field": "Team", "integration": null, - "inverseOf": null, + "inverseOf": "_IndustryTagToTeams", "isFilterable": true, "isPrimaryKey": false, "isReadOnly": false, @@ -5786,48 +5750,12 @@ { "actions": [], "fields": [ - { - "defaultValue": null, - "enums": null, - "field": "A", - "integration": null, - "inverseOf": null, - "isFilterable": true, - "isPrimaryKey": true, - "isReadOnly": true, - "isRequired": true, - "isSortable": true, - "isVirtual": false, - "reference": null, - "type": "Number", - "validations": [ - {"type": "is present", "message": "Failed validation rule: 'Present'"} - ] - }, - { - "defaultValue": null, - "enums": null, - "field": "B", - "integration": null, - "inverseOf": null, - "isFilterable": true, - "isPrimaryKey": true, - "isReadOnly": true, - "isRequired": true, - "isSortable": true, - "isVirtual": false, - "reference": null, - "type": "Number", - "validations": [ - {"type": "is present", "message": "Failed validation rule: 'Present'"} - ] - }, { "defaultValue": null, "enums": null, "field": "Member", "integration": null, - "inverseOf": null, + "inverseOf": "_MemberToSkills", "isFilterable": true, "isPrimaryKey": false, "isReadOnly": false, @@ -5846,7 +5774,7 @@ "enums": null, "field": "Skill", "integration": null, - "inverseOf": null, + "inverseOf": "_MemberToSkills", "isFilterable": true, "isPrimaryKey": false, "isReadOnly": false, @@ -5874,48 +5802,12 @@ { "actions": [], "fields": [ - { - "defaultValue": null, - "enums": null, - "field": "A", - "integration": null, - "inverseOf": null, - "isFilterable": true, - "isPrimaryKey": true, - "isReadOnly": true, - "isRequired": true, - "isSortable": true, - "isVirtual": false, - "reference": null, - "type": "Number", - "validations": [ - {"type": "is present", "message": "Failed validation rule: 'Present'"} - ] - }, - { - "defaultValue": null, - "enums": null, - "field": "B", - "integration": null, - "inverseOf": null, - "isFilterable": true, - "isPrimaryKey": true, - "isReadOnly": true, - "isRequired": true, - "isSortable": true, - "isVirtual": false, - "reference": null, - "type": "Number", - "validations": [ - {"type": "is present", "message": "Failed validation rule: 'Present'"} - ] - }, { "defaultValue": null, "enums": null, "field": "MembershipSource", "integration": null, - "inverseOf": null, + "inverseOf": "_MembershipSourceToTeams", "isFilterable": true, "isPrimaryKey": false, "isReadOnly": false, @@ -5934,7 +5826,7 @@ "enums": null, "field": "Team", "integration": null, - "inverseOf": null, + "inverseOf": "_MembershipSourceToTeams", "isFilterable": true, "isPrimaryKey": false, "isReadOnly": false, @@ -5962,48 +5854,12 @@ { "actions": [], "fields": [ - { - "defaultValue": null, - "enums": null, - "field": "A", - "integration": null, - "inverseOf": null, - "isFilterable": true, - "isPrimaryKey": true, - "isReadOnly": true, - "isRequired": true, - "isSortable": true, - "isVirtual": false, - "reference": null, - "type": "Number", - "validations": [ - {"type": "is present", "message": "Failed validation rule: 'Present'"} - ] - }, - { - "defaultValue": null, - "enums": null, - "field": "B", - "integration": null, - "inverseOf": null, - "isFilterable": true, - "isPrimaryKey": true, - "isReadOnly": true, - "isRequired": true, - "isSortable": true, - "isVirtual": false, - "reference": null, - "type": "Number", - "validations": [ - {"type": "is present", "message": "Failed validation rule: 'Present'"} - ] - }, { "defaultValue": null, "enums": null, "field": "Team", "integration": null, - "inverseOf": null, + "inverseOf": "_TeamToTechnologies", "isFilterable": true, "isPrimaryKey": false, "isReadOnly": false, @@ -6022,7 +5878,7 @@ "enums": null, "field": "Technology", "integration": null, - "inverseOf": null, + "inverseOf": "_TeamToTechnologies", "isFilterable": true, "isPrimaryKey": false, "isReadOnly": false, diff --git a/apps/web-api/prisma/migrations/20240807080204_member_interaction_followup_type/migration.sql b/apps/web-api/prisma/migrations/20240807080204_member_interaction_followup_type/migration.sql new file mode 100644 index 000000000..041c7cb15 --- /dev/null +++ b/apps/web-api/prisma/migrations/20240807080204_member_interaction_followup_type/migration.sql @@ -0,0 +1,2 @@ +-- AlterEnum +ALTER TYPE "MemberFollowUpStatus" ADD VALUE 'CLOSED'; diff --git a/apps/web-api/prisma/schema.prisma b/apps/web-api/prisma/schema.prisma index 67762eb92..626aaaf08 100644 --- a/apps/web-api/prisma/schema.prisma +++ b/apps/web-api/prisma/schema.prisma @@ -116,6 +116,7 @@ enum PLEventType { enum MemberFollowUpStatus { PENDING COMPLETED + CLOSED } enum MemberFeedbackResponseType { diff --git a/apps/web-api/src/office-hours/office-hours.controller.ts b/apps/web-api/src/office-hours/office-hours.controller.ts index b58a722ea..ca1ac4707 100644 --- a/apps/web-api/src/office-hours/office-hours.controller.ts +++ b/apps/web-api/src/office-hours/office-hours.controller.ts @@ -13,7 +13,8 @@ import { CreateMemberFeedbackSchemaDto, MemberFollowUpQueryParams, ResponseMemberFollowUpWithRelationsSchema, - MemberFollowUpStatus + MemberFollowUpStatus, + MemberFollowUpType } from 'libs/contracts/src/schema'; import { ApiQueryFromZod } from '../decorators/api-query-from-zod'; import { ApiOkResponseFromZod } from '../decorators/api-response-from-zod'; @@ -40,20 +41,33 @@ export class OfficeHoursController { ): Promise { const member: any = await this.memberService.findMemberByEmail(request["userEmail"]); const interval = parseInt(process.env.INTERACTION_INTERVAL_DELAY_IN_MILLISECONDS || '1800000') - const result = await this.interactionService.findInteractions({ + const result: any = await this.interactionService.findInteractions({ where: { sourceMemberUid: member?.uid, targetMemberUid: body?.targetMemberUid, createdAt: { gte: new Date(new Date().getTime() - interval) } + }, + include: { + interactionFollowUps: { + where: { + type: MemberFollowUpType.Enum.MEETING_INITIATED, + status: { + in: [ MemberFollowUpStatus.Enum.PENDING, MemberFollowUpStatus.Enum.CLOSED ] + } + } + } + }, + orderBy: { + createdAt: 'desc' } }); - if (result && result.length > 0) { + if (result && result.length > 0 && result[0]?.interactionFollowUps?.length > 0) { throw new ForbiddenException(`Interaction with same user within ${interval / (60 * 1000)} minutes is forbidden`); } if (member.uid === body.targetMemberUid) { - throw new ForbiddenException('Interacte with yourself is forbidden'); + throw new ForbiddenException('Interaction with yourself is forbidden'); } return await this.interactionService.createInteraction(body as any, member); } @@ -69,6 +83,8 @@ export class OfficeHoursController { const queryableFields = prismaQueryableFieldsFromZod( ResponseMemberFollowUpWithRelationsSchema ); + const { status } : any = request.query; + delete request.query.status; const builder = new PrismaQueryBuilder(queryableFields); const builtQuery = builder.build(request.query); const member: any = await this.memberService.findMemberByEmail(request["userEmail"]); @@ -77,7 +93,7 @@ export class OfficeHoursController { builtQuery.where, { createdBy: member?.uid, - status: MemberFollowUpStatus.Enum.PENDING + status: status ? { in: status.split(',') } : {} }, this.followUpService.buildDelayedFollowUpQuery() ] @@ -98,12 +114,39 @@ export class OfficeHoursController { where: { uid : interactionFollowUpUid, createdBy: member?.uid, - status: MemberFollowUpStatus.Enum.PENDING + status: { + in: [ MemberFollowUpStatus.Enum.PENDING, MemberFollowUpStatus.Enum.CLOSED ] + } } }); + console.log(followUps) if (followUps && followUps.length === 0) { throw new NotFoundException(`There is no follow-up associated with the given ID: ${interactionFollowUpUid}`); } return await this.interactionService.createInteractionFeedback(body as any, member, followUps?.[0]); } -} \ No newline at end of file + + @Api(server.route.closeMemberInteractionFollowUp) + @UsePipes(ZodValidationPipe) + @UseGuards(UserTokenValidation) + async closeMemberInteractionFollowUp( + @Param('interactionUid') interactionUid: string, + @Param('followUpUid') followUpUid: string, + @Req() request: Request + ): Promise { + const member: any = await this.memberService.findMemberByEmail(request["userEmail"]); + const followUps = await this.followUpService.getFollowUps({ + where: { + uid : followUpUid, + interactionUid, + createdBy: member?.uid, + status: MemberFollowUpStatus.Enum.PENDING + } + }); + if (followUps && followUps.length === 0) { + throw new NotFoundException(`No pending follow-up found for the given ID: ${followUpUid}. + It may have been closed or does not exist.`); + } + return await this.interactionService.closeMemberInteractionFollowUpByID(followUpUid); + } +} diff --git a/apps/web-api/src/office-hours/office-hours.service.ts b/apps/web-api/src/office-hours/office-hours.service.ts index c94b4587a..643c38d19 100644 --- a/apps/web-api/src/office-hours/office-hours.service.ts +++ b/apps/web-api/src/office-hours/office-hours.service.ts @@ -56,10 +56,7 @@ export class OfficeHoursService { async findInteractions(queryOptions: Prisma.MemberInteractionFindManyArgs) { try { return await this.prisma.memberInteraction.findMany({ - ...queryOptions, - include: { - interactionFollowUps: true - } + ...queryOptions }); } catch(exception) { this.handleErrors(exception); @@ -130,6 +127,14 @@ export class OfficeHoursService { }); } + async closeMemberInteractionFollowUpByID(followUpUid) { + try { + return await this.followUpService.updateFollowUpStatusByUid(followUpUid, MemberFollowUpStatus.Enum.CLOSED); + } catch(error) { + this.handleErrors(error, followUpUid); + } + } + private handleErrors(error, message?) { this.logger.error(error); if (error instanceof Prisma.PrismaClientKnownRequestError) { diff --git a/apps/web-api/src/utils/constants.ts b/apps/web-api/src/utils/constants.ts index 46c446aa4..1ef6ff1b5 100644 --- a/apps/web-api/src/utils/constants.ts +++ b/apps/web-api/src/utils/constants.ts @@ -101,13 +101,13 @@ export const PROJECT = 'Project'; export const TEAM = 'Team'; export const InteractionFailureReasons: { [key: string]: string } = { - "Broken Link": "IFR0001", + "Link is broken": "IFR0001", "I plan to schedule soon": "IFR0002", - "Preferred slot not available": "IFR0003", - "Meeting yet to happen": "IFR0004", - "Got Rescheduled": "IFR0005", - "Got Cancelled" : "IFR0006", + "Preferred slot is not available": "IFR0003", + "Got rescheduled": "IFR0005", + "Got cancelled" : "IFR0006", "Member didn’t show up": "IFR0007", - "I did not show up":"IFR0008", - "Other": "IFR0009" + "I could not make it":"IFR0008", + "Call quality issues": "IFR0009", + "Meeting link didn't work": "IFR00010" }; diff --git a/libs/contracts/src/lib/contract-member-interaction.ts b/libs/contracts/src/lib/contract-member-interaction.ts index 3c4a7659b..8c4c721e9 100644 --- a/libs/contracts/src/lib/contract-member-interaction.ts +++ b/libs/contracts/src/lib/contract-member-interaction.ts @@ -26,6 +26,15 @@ export const apiMemberInteractions = contract.router({ }, summary: 'Get member interaction follow ups', }, + closeMemberInteractionFollowUp: { + method: 'PATCH', + path: `${getAPIVersionAsPath('1')}/members/:uid/interactions/:interactionUid/follow-ups/:followUpUid`, + body: contract.body(), + responses: { + 200: contract.response(), + }, + summary: 'close a member interaction follow up' + }, createMemberInteractionFeedback: { method: 'POST', path: `${getAPIVersionAsPath('1')}/members/:uid/follow-ups/:uid/feedbacks`, diff --git a/libs/contracts/src/schema/member-follow-up.ts b/libs/contracts/src/schema/member-follow-up.ts index 27fb213a5..924f972ba 100644 --- a/libs/contracts/src/schema/member-follow-up.ts +++ b/libs/contracts/src/schema/member-follow-up.ts @@ -10,7 +10,7 @@ export const MemberFollowUpType = z.enum([ "MEETING_RESCHEDULED" ]); -export const MemberFollowUpStatus = z.enum(["PENDING", "COMPLETED"]); +export const MemberFollowUpStatus = z.enum(["PENDING", "COMPLETED", "CLOSED"]); const MemberFollowUpSchema = z.object({ id: z.number().int(),