diff --git a/.forestadmin-schema.json b/.forestadmin-schema.json index 6b66ef74c..056dd8187 100644 --- a/.forestadmin-schema.json +++ b/.forestadmin-schema.json @@ -2742,6 +2742,24 @@ "type": "Number", "validations": [] }, + { + "defaultValue": null, + "enums": null, + "field": "type", + "integration": null, + "inverseOf": null, + "isFilterable": true, + "isPrimaryKey": false, + "isReadOnly": false, + "isRequired": true, + "isSortable": true, + "isVirtual": false, + "reference": null, + "type": "String", + "validations": [ + {"type": "is present", "message": "Failed validation rule: 'Present'"} + ] + }, { "defaultValue": null, "enums": null, diff --git a/apps/web-api/prisma/migrations/20240731093436_member_interaction_follow_up/migration.sql b/apps/web-api/prisma/migrations/20240731093436_member_interaction_follow_up/migration.sql new file mode 100644 index 000000000..a4aaa0297 --- /dev/null +++ b/apps/web-api/prisma/migrations/20240731093436_member_interaction_follow_up/migration.sql @@ -0,0 +1,83 @@ +-- CreateEnum +CREATE TYPE "MemberFollowUpStatus" AS ENUM ('PENDING', 'COMPLETED'); + +-- CreateEnum +CREATE TYPE "MemberFeedbackResponseType" AS ENUM ('POSITIVE', 'NEGATIVE', 'NEUTRAL'); + +-- CreateTable +CREATE TABLE "MemberInteraction" ( + "id" SERIAL NOT NULL, + "uid" TEXT NOT NULL, + "type" TEXT NOT NULL, + "data" JSONB, + "hasFollowUp" BOOLEAN NOT NULL DEFAULT false, + "sourceMemberUid" TEXT NOT NULL, + "targetMemberUid" TEXT, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "MemberInteraction_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "MemberFollowUp" ( + "id" SERIAL NOT NULL, + "uid" TEXT NOT NULL, + "status" "MemberFollowUpStatus" NOT NULL, + "type" TEXT NOT NULL, + "data" JSONB, + "isDelayed" BOOLEAN NOT NULL DEFAULT false, + "interactionUid" TEXT, + "createdBy" TEXT NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "MemberFollowUp_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "MemberFeedback" ( + "id" SERIAL NOT NULL, + "uid" TEXT NOT NULL, + "type" TEXT NOT NULL, + "data" JSONB, + "rating" INTEGER, + "comments" TEXT[], + "response" "MemberFeedbackResponseType" NOT NULL, + "followUpUid" TEXT NOT NULL, + "createdBy" TEXT NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "MemberFeedback_pkey" PRIMARY KEY ("id") +); + +-- CreateIndex +CREATE UNIQUE INDEX "MemberInteraction_uid_key" ON "MemberInteraction"("uid"); + +-- CreateIndex +CREATE UNIQUE INDEX "MemberFollowUp_uid_key" ON "MemberFollowUp"("uid"); + +-- CreateIndex +CREATE UNIQUE INDEX "MemberFeedback_uid_key" ON "MemberFeedback"("uid"); + +-- CreateIndex +CREATE UNIQUE INDEX "MemberFeedback_followUpUid_key" ON "MemberFeedback"("followUpUid"); + +-- AddForeignKey +ALTER TABLE "MemberInteraction" ADD CONSTRAINT "MemberInteraction_sourceMemberUid_fkey" FOREIGN KEY ("sourceMemberUid") REFERENCES "Member"("uid") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "MemberInteraction" ADD CONSTRAINT "MemberInteraction_targetMemberUid_fkey" FOREIGN KEY ("targetMemberUid") REFERENCES "Member"("uid") ON DELETE SET NULL ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "MemberFollowUp" ADD CONSTRAINT "MemberFollowUp_interactionUid_fkey" FOREIGN KEY ("interactionUid") REFERENCES "MemberInteraction"("uid") ON DELETE SET NULL ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "MemberFollowUp" ADD CONSTRAINT "MemberFollowUp_createdBy_fkey" FOREIGN KEY ("createdBy") REFERENCES "Member"("uid") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "MemberFeedback" ADD CONSTRAINT "MemberFeedback_followUpUid_fkey" FOREIGN KEY ("followUpUid") REFERENCES "MemberFollowUp"("uid") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "MemberFeedback" ADD CONSTRAINT "MemberFeedback_createdBy_fkey" FOREIGN KEY ("createdBy") REFERENCES "Member"("uid") ON DELETE RESTRICT ON UPDATE CASCADE; diff --git a/apps/web-api/prisma/schema.prisma b/apps/web-api/prisma/schema.prisma index 941545a4e..67762eb92 100644 --- a/apps/web-api/prisma/schema.prisma +++ b/apps/web-api/prisma/schema.prisma @@ -451,6 +451,7 @@ model TeamFocusAreaVersionHistory { model MemberInteraction { id Int @id @default(autoincrement()) uid String @unique @default(cuid()) + type String data Json? hasFollowUp Boolean @default(false) sourceMemberUid String diff --git a/apps/web-api/src/member-feedbacks/member-feedbacks.service.ts b/apps/web-api/src/member-feedbacks/member-feedbacks.service.ts index 5120926e3..6732fecda 100644 --- a/apps/web-api/src/member-feedbacks/member-feedbacks.service.ts +++ b/apps/web-api/src/member-feedbacks/member-feedbacks.service.ts @@ -20,16 +20,21 @@ export class MemberFeedbacksService { private followUpService: MemberFollowUpsService ) {} - async createFeedback(feedback: Prisma.MemberFeedbackUncheckedCreateInput, loggedInMember, followUp) { + async createFeedback( + feedback: Prisma.MemberFeedbackUncheckedCreateInput, + loggedInMember, + followUp, + tx?: Prisma.TransactionClient + ) { try { - const result = await this.prisma.memberFeedback.create({ + const result = await (tx || this.prisma).memberFeedback.create({ data: { ...feedback, createdBy: loggedInMember.uid, followUpUid: followUp.uid } }); - await this.followUpService.updateFollowUpStatusByUid(followUp.uid, MemberFollowUpStatus.Enum.COMPLETED) + await this.followUpService.updateFollowUpStatusByUid(followUp.uid, MemberFollowUpStatus.Enum.COMPLETED, tx) return result; } catch(error) { this.handleErrors(error); diff --git a/apps/web-api/src/member-follow-ups/member-follow-ups.service.ts b/apps/web-api/src/member-follow-ups/member-follow-ups.service.ts index 8f2a0399e..fddbb6487 100644 --- a/apps/web-api/src/member-follow-ups/member-follow-ups.service.ts +++ b/apps/web-api/src/member-follow-ups/member-follow-ups.service.ts @@ -15,9 +15,13 @@ export class MemberFollowUpsService { private logger: LogService ) {} - async createFollowUp(followUp: Prisma.MemberFollowUpUncheckedCreateInput, interaction) { + async createFollowUp( + followUp: Prisma.MemberFollowUpUncheckedCreateInput, + interaction, + tx?: Prisma.TransactionClient + ) { try { - await this.prisma.memberFollowUp.create({ + await (tx || this.prisma).memberFollowUp.create({ data: { ...followUp } @@ -27,23 +31,44 @@ export class MemberFollowUpsService { } } - async getFollowUps(query: Prisma.MemberFollowUpFindManyArgs) { + async getFollowUps( + query: Prisma.MemberFollowUpFindManyArgs, + tx?: Prisma.TransactionClient + ) { try { - const followUps = await this.prisma.memberFollowUp.findMany({ + return await (tx || this.prisma).memberFollowUp.findMany({ ...query, include: { - interaction: true + interaction: { + select: { + uid: true, + type: true, + sourceMember: { + select: { + name:true + } + }, + targetMember: { + select: { + name:true + } + } + } + } } }); - return followUps; } catch(error) { this.handleErrors(error); } } - async updateFollowUpStatusByUid(uid: string, status) { + async updateFollowUpStatusByUid( + uid: string, + status, + tx?: Prisma.TransactionClient + ) { try { - return await this.prisma.memberFollowUp.update({ + return await (tx || this.prisma).memberFollowUp.update({ where: { uid }, @@ -93,4 +118,4 @@ export class MemberFollowUpsService { } throw error; }; -} \ No newline at end of file +} 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 295fdc4d4..186fa4dfe 100644 --- a/apps/web-api/src/office-hours/office-hours.controller.ts +++ b/apps/web-api/src/office-hours/office-hours.controller.ts @@ -50,7 +50,6 @@ export class OfficeHoursController { @UseGuards(UserTokenValidation) @NoCache() async findAll( - @Param('uid') interactionUid: string, @Req() request: Request ) { const queryableFields = prismaQueryableFieldsFromZod( 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 ef486ced9..0095624ca 100644 --- a/apps/web-api/src/office-hours/office-hours.service.ts +++ b/apps/web-api/src/office-hours/office-hours.service.ts @@ -30,25 +30,30 @@ export class OfficeHoursService { private readonly feedbackService: MemberFeedbacksService ) {} - async createInteraction(interaction: Prisma.MemberInteractionUncheckedCreateInput, loggedInMember) { + async createInteraction( + interaction: Prisma.MemberInteractionUncheckedCreateInput, + loggedInMember + ) { try { - const result = await this.prisma.memberInteraction.create({ - data:{ - ...interaction, - sourceMemberUid: loggedInMember?.uid - } + return this.prisma.$transaction(async(tx) => { + const result = await tx.memberInteraction.create({ + data:{ + ...interaction, + sourceMemberUid: loggedInMember?.uid + } + }); + if (result?.hasFollowUp) { + await this.createInteractionFollowUp(result, loggedInMember, MemberFollowUpType.Enum.MEETING_INITIATED, tx); + await this.createInteractionFollowUp(result, loggedInMember, MemberFollowUpType.Enum.MEETING_SCHEDULED, tx); + }; + return result; }); - if (result?.hasFollowUp) { - await this.createInteractionFollowUp(result, loggedInMember, MemberFollowUpType.Enum.MEETING_INITIATED); - await this.createInteractionFollowUp(result, loggedInMember, MemberFollowUpType.Enum.MEETING_SCHEDULED); - }; - return result; } catch(exception) { this.handleErrors(exception); } } - async createInteractionFollowUp(interaction, loggedInMember, type, scheduledAt?) { + async createInteractionFollowUp(interaction, loggedInMember, type, tx?, scheduledAt?) { const followUp: any = { status: MemberFollowUpStatus.Enum.PENDING, interactionUid: interaction?.uid, @@ -62,37 +67,55 @@ export class OfficeHoursService { if (scheduledAt != null) { followUp.createdAt = scheduledAt; } - return await this.followUpService.createFollowUp(followUp, interaction); + return await this.followUpService.createFollowUp(followUp, interaction, tx); } async createInteractionFeedback(feedback, member, followUp) { feedback.comments = feedback.comments?.map(comment => InteractionFailureReasons[comment]) || []; - if ( - followUp.type === MemberFollowUpType.Enum.MEETING_INITIATED && - feedback.response === MemberFeedbackResponseType.Enum.NEGATIVE - ) {} - if ( - feedback.response === MemberFeedbackResponseType.Enum.NEGATIVE && - feedback.comments?.includes('IFR0004') - ) { - await this.createInteractionFollowUp( - followUp.interaction, - member, - MemberFollowUpType.Enum.MEETING_YET_TO_HAPPEN - ); - } - if ( - feedback.response === MemberFeedbackResponseType.Enum.NEGATIVE && - feedback.comments?.includes('IFR0005') - ) { - await this.createInteractionFollowUp( - followUp.interaction, - member, - MemberFollowUpType.Enum.MEETING_RESCHEDULED, - feedback?.data?.scheduledAt - ); - } - return await this.feedbackService.createFeedback(feedback, member, followUp); + return await this.prisma.$transaction(async (tx) => { + if ( + followUp.type === MemberFollowUpType.Enum.MEETING_INITIATED && + feedback.response === MemberFeedbackResponseType.Enum.NEGATIVE + ) { + const delayedFollowUps = await this.followUpService.getFollowUps({ + where: { + interactionUid: followUp.interactionUid, + type: MemberFollowUpType.Enum.MEETING_SCHEDULED + } + }, tx); + if (delayedFollowUps?.length) { + await this.followUpService.updateFollowUpStatusByUid( + delayedFollowUps[0]?.uid, + MemberFollowUpStatus.Enum.COMPLETED, + tx + ); + } + } + if ( + feedback.response === MemberFeedbackResponseType.Enum.NEGATIVE && + feedback.comments?.includes('IFR0004') + ) { + await this.createInteractionFollowUp( + followUp.interaction, + member, + MemberFollowUpType.Enum.MEETING_YET_TO_HAPPEN, + tx + ); + } + if ( + feedback.response === MemberFeedbackResponseType.Enum.NEGATIVE && + feedback.comments?.includes('IFR0005') + ) { + await this.createInteractionFollowUp( + followUp.interaction, + member, + MemberFollowUpType.Enum.MEETING_RESCHEDULED, + tx, + feedback?.data?.scheduledAt + ); + } + return await this.feedbackService.createFeedback(feedback, member, followUp, tx); + }); } private handleErrors(error, message?) { diff --git a/libs/contracts/src/lib/contract-member-interaction.ts b/libs/contracts/src/lib/contract-member-interaction.ts index b65950294..3c4a7659b 100644 --- a/libs/contracts/src/lib/contract-member-interaction.ts +++ b/libs/contracts/src/lib/contract-member-interaction.ts @@ -19,7 +19,7 @@ export const apiMemberInteractions = contract.router({ }, getMemberInteractionFollowUps: { method: 'GET', - path: `${getAPIVersionAsPath('1')}/members/:memberUid/interactions/:uid/follow-ups`, + path: `${getAPIVersionAsPath('1')}/members/:memberUid/interactions/follow-ups`, query: MemberFollowUpQueryParams, responses: { 200: ResponseMemberFollowUpWithRelationsSchema.array(), diff --git a/libs/contracts/src/schema/member-interaction.ts b/libs/contracts/src/schema/member-interaction.ts index 1404363c4..ccbba869f 100644 --- a/libs/contracts/src/schema/member-interaction.ts +++ b/libs/contracts/src/schema/member-interaction.ts @@ -1,9 +1,14 @@ import { z, } from "zod"; import { createZodDto } from '@abitia/zod-dto'; +export const MemberInteractionType = z.enum([ + "SCHEDULE_MEETING" +]); + const MemberInteractionSchema = z.object({ id: z.number().int(), uid: z.string(), + type: MemberInteractionType, data: z.any().optional(), hasFollowUp: z.boolean().optional(), createdAt: z.string(),