Skip to content

Commit 2326ebb

Browse files
committed
feat: add project conversations for mentor/team/client messaging
Adds two scoped conversation channels per project anchor (Program B project and Program A application): - PARTICIPANTS: team + mentor + client company - INTERNAL: team + mentor only; company users cannot see or reach it (404, not 403, so the private channel is never disclosed) Includes Prisma models (Conversation, ConversationMessage, ConversationMessageAttachment) + migration, a new src/conversations module (access service, service, repository, controller, DTOs), plain-text messages with author-only edit/soft-delete, file attachments reusing the Files/Storage flow, and unit tests.
1 parent 2a32152 commit 2326ebb

16 files changed

Lines changed: 2031 additions & 4 deletions
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
-- CreateEnum
2+
CREATE TYPE "ConversationChannel" AS ENUM ('INTERNAL', 'PARTICIPANTS');
3+
4+
-- CreateTable
5+
CREATE TABLE "Conversation" (
6+
"id" TEXT NOT NULL,
7+
"channel" "ConversationChannel" NOT NULL,
8+
"programBProjectId" TEXT,
9+
"applicationId" TEXT,
10+
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
11+
"updatedAt" TIMESTAMP(3) NOT NULL,
12+
13+
CONSTRAINT "Conversation_pkey" PRIMARY KEY ("id")
14+
);
15+
16+
-- CreateTable
17+
CREATE TABLE "ConversationMessage" (
18+
"id" TEXT NOT NULL,
19+
"conversationId" TEXT NOT NULL,
20+
"authorUserId" TEXT NOT NULL,
21+
"body" TEXT NOT NULL,
22+
"editedAt" TIMESTAMP(3),
23+
"deletedAt" TIMESTAMP(3),
24+
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
25+
"updatedAt" TIMESTAMP(3) NOT NULL,
26+
27+
CONSTRAINT "ConversationMessage_pkey" PRIMARY KEY ("id")
28+
);
29+
30+
-- CreateTable
31+
CREATE TABLE "ConversationMessageAttachment" (
32+
"id" TEXT NOT NULL,
33+
"messageId" TEXT NOT NULL,
34+
"uploadedFileId" TEXT NOT NULL,
35+
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
36+
37+
CONSTRAINT "ConversationMessageAttachment_pkey" PRIMARY KEY ("id")
38+
);
39+
40+
-- CreateIndex
41+
CREATE INDEX "Conversation_programBProjectId_idx" ON "Conversation"("programBProjectId");
42+
43+
-- CreateIndex
44+
CREATE INDEX "Conversation_applicationId_idx" ON "Conversation"("applicationId");
45+
46+
-- CreateIndex
47+
CREATE UNIQUE INDEX "Conversation_programBProjectId_channel_key" ON "Conversation"("programBProjectId", "channel");
48+
49+
-- CreateIndex
50+
CREATE UNIQUE INDEX "Conversation_applicationId_channel_key" ON "Conversation"("applicationId", "channel");
51+
52+
-- CreateIndex
53+
CREATE INDEX "ConversationMessage_conversationId_createdAt_idx" ON "ConversationMessage"("conversationId", "createdAt");
54+
55+
-- CreateIndex
56+
CREATE INDEX "ConversationMessage_authorUserId_idx" ON "ConversationMessage"("authorUserId");
57+
58+
-- CreateIndex
59+
CREATE INDEX "ConversationMessageAttachment_messageId_idx" ON "ConversationMessageAttachment"("messageId");
60+
61+
-- CreateIndex
62+
CREATE INDEX "ConversationMessageAttachment_uploadedFileId_idx" ON "ConversationMessageAttachment"("uploadedFileId");
63+
64+
-- CreateIndex
65+
CREATE UNIQUE INDEX "ConversationMessageAttachment_messageId_uploadedFileId_key" ON "ConversationMessageAttachment"("messageId", "uploadedFileId");
66+
67+
-- AddForeignKey
68+
ALTER TABLE "Conversation" ADD CONSTRAINT "Conversation_programBProjectId_fkey" FOREIGN KEY ("programBProjectId") REFERENCES "ProgramBProject"("id") ON DELETE CASCADE ON UPDATE CASCADE;
69+
70+
-- AddForeignKey
71+
ALTER TABLE "Conversation" ADD CONSTRAINT "Conversation_applicationId_fkey" FOREIGN KEY ("applicationId") REFERENCES "Application"("id") ON DELETE CASCADE ON UPDATE CASCADE;
72+
73+
-- AddForeignKey
74+
ALTER TABLE "ConversationMessage" ADD CONSTRAINT "ConversationMessage_conversationId_fkey" FOREIGN KEY ("conversationId") REFERENCES "Conversation"("id") ON DELETE CASCADE ON UPDATE CASCADE;
75+
76+
-- AddForeignKey
77+
ALTER TABLE "ConversationMessage" ADD CONSTRAINT "ConversationMessage_authorUserId_fkey" FOREIGN KEY ("authorUserId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
78+
79+
-- AddForeignKey
80+
ALTER TABLE "ConversationMessageAttachment" ADD CONSTRAINT "ConversationMessageAttachment_messageId_fkey" FOREIGN KEY ("messageId") REFERENCES "ConversationMessage"("id") ON DELETE CASCADE ON UPDATE CASCADE;
81+
82+
-- AddForeignKey
83+
ALTER TABLE "ConversationMessageAttachment" ADD CONSTRAINT "ConversationMessageAttachment_uploadedFileId_fkey" FOREIGN KEY ("uploadedFileId") REFERENCES "UploadedFile"("id") ON DELETE RESTRICT ON UPDATE CASCADE;

prisma/schema.prisma

Lines changed: 76 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -215,6 +215,11 @@ enum ProgramBDocumentVisibility {
215215
PARTICIPANTS
216216
}
217217

218+
enum ConversationChannel {
219+
INTERNAL
220+
PARTICIPANTS
221+
}
222+
218223
model User {
219224
id String @id @default(uuid())
220225
firstName String
@@ -273,6 +278,7 @@ model User {
273278
needsInfoRepliesCreated NeedsInfoReply[] @relation("NeedsInfoReplyCreatedBy")
274279
applicationStatusEventsChanged ApplicationStatusEvent[] @relation("ApplicationStatusChangedBy")
275280
programAMentorshipNotesAuthored ProgramAMentorshipNote[] @relation("ProgramAMentorshipNoteAuthor")
281+
conversationMessages ConversationMessage[] @relation("ConversationMessageAuthor")
276282
277283
createdAt DateTime @default(now())
278284
updatedAt DateTime @updatedAt
@@ -308,16 +314,17 @@ model UploadedFile {
308314
uploadedAt DateTime?
309315
failedAt DateTime?
310316
311-
createdAt DateTime @default(now())
312-
updatedAt DateTime @updatedAt
313-
academicEvidenceFor StudentProfile[] @relation("StudentAcademicEvidence")
314-
cvFor StudentProfile[] @relation("StudentCvFile")
317+
createdAt DateTime @default(now())
318+
updatedAt DateTime @updatedAt
319+
academicEvidenceFor StudentProfile[] @relation("StudentAcademicEvidence")
320+
cvFor StudentProfile[] @relation("StudentCvFile")
315321
applicationDocuments ApplicationDocument[]
316322
programBTeamApplications ProgramBTeamApplication[]
317323
programBBacklogDocuments ProgramBBacklogDocument[]
318324
programBProjectDocuments ProgramBProjectDocument[]
319325
programBTeamApplicationCvs ProgramBTeamApplicationCv[]
320326
reportExportJobs ReportExportJob[]
327+
conversationAttachments ConversationMessageAttachment[]
321328
322329
@@index([ownerId, status])
323330
@@index([status, uploadUrlExpiresAt])
@@ -405,6 +412,7 @@ model Application {
405412
mentorshipNotes ProgramAMentorshipNote[]
406413
programAMilestones ProgramAMilestone[]
407414
programBProjects ProgramBProject[]
415+
conversations Conversation[]
408416
409417
status ApplicationStatus @default(DRAFT)
410418
submittedAt DateTime?
@@ -826,6 +834,7 @@ model ProgramBProject {
826834
mentoringNotes ProgramBMentoringNote[]
827835
poReviews ProgramBPoReview[]
828836
documents ProgramBProjectDocument[]
837+
conversations Conversation[]
829838
830839
@@index([teamId])
831840
@@index([backlogItemId])
@@ -921,6 +930,69 @@ model ProgramBMentoringNote {
921930
@@index([projectId, createdAt])
922931
}
923932

933+
/// A single communication channel scoped to one project anchor (Program B project
934+
/// or Program A application) and one ConversationChannel. Created lazily on the
935+
/// first message. INTERNAL = team + mentor only; PARTICIPANTS = also the client.
936+
model Conversation {
937+
id String @id @default(uuid())
938+
channel ConversationChannel
939+
940+
programBProjectId String?
941+
programBProject ProgramBProject? @relation(fields: [programBProjectId], references: [id], onDelete: Cascade)
942+
943+
applicationId String?
944+
application Application? @relation(fields: [applicationId], references: [id], onDelete: Cascade)
945+
946+
messages ConversationMessage[]
947+
948+
createdAt DateTime @default(now())
949+
updatedAt DateTime @updatedAt
950+
951+
@@unique([programBProjectId, channel])
952+
@@unique([applicationId, channel])
953+
@@index([programBProjectId])
954+
@@index([applicationId])
955+
}
956+
957+
model ConversationMessage {
958+
id String @id @default(uuid())
959+
960+
conversationId String
961+
conversation Conversation @relation(fields: [conversationId], references: [id], onDelete: Cascade)
962+
963+
authorUserId String
964+
authorUser User @relation("ConversationMessageAuthor", fields: [authorUserId], references: [id], onDelete: Restrict)
965+
966+
body String
967+
968+
editedAt DateTime?
969+
deletedAt DateTime? // soft delete -> rendered as a tombstone
970+
971+
attachments ConversationMessageAttachment[]
972+
973+
createdAt DateTime @default(now())
974+
updatedAt DateTime @updatedAt
975+
976+
@@index([conversationId, createdAt])
977+
@@index([authorUserId])
978+
}
979+
980+
model ConversationMessageAttachment {
981+
id String @id @default(uuid())
982+
983+
messageId String
984+
message ConversationMessage @relation(fields: [messageId], references: [id], onDelete: Cascade)
985+
986+
uploadedFileId String
987+
uploadedFile UploadedFile @relation(fields: [uploadedFileId], references: [id], onDelete: Restrict)
988+
989+
createdAt DateTime @default(now())
990+
991+
@@unique([messageId, uploadedFileId])
992+
@@index([messageId])
993+
@@index([uploadedFileId])
994+
}
995+
924996
model ProgramBPoReview {
925997
id String @id @default(uuid())
926998

src/app.module.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ import { ProgramBCompanyOverviewModule } from './programs/program-b/company-over
2828
import { ReportExportProcessor } from './reports/report-export/report-export.processor';
2929
import { ReportsModule } from './reports/reports.module';
3030
import { ContactModule } from './contact/contact.module';
31+
import { ConversationsModule } from './conversations/conversations.module';
3132

3233
const queueProcessorProviders =
3334
process.env.RUN_QUEUE_PROCESSORS === 'true'
@@ -67,6 +68,7 @@ const queueProcessorProviders =
6768
ProgramBCompanyOverviewModule,
6869
ReportsModule,
6970
ContactModule,
71+
ConversationsModule,
7072
],
7173
controllers: [AppController],
7274
providers: [

0 commit comments

Comments
 (0)