diff --git a/src/backend/src/prisma-query-args/announcements.query.args.ts b/src/backend/src/prisma-query-args/announcements.query.args.ts index 2f0e1ba294..b88c9fbf1d 100644 --- a/src/backend/src/prisma-query-args/announcements.query.args.ts +++ b/src/backend/src/prisma-query-args/announcements.query.args.ts @@ -6,7 +6,6 @@ export type AnnouncementQueryArgs = ReturnType; export const getAnnouncementQueryArgs = (organizationId: string) => Prisma.validator()({ include: { - usersReceived: getUserQueryArgs(organizationId), - userCreated: getUserQueryArgs(organizationId) + usersReceived: getUserQueryArgs(organizationId) } }); diff --git a/src/backend/src/prisma/migrations/20241218044032_homepage_updates/migration.sql b/src/backend/src/prisma/migrations/20241219125602_homepage_updates/migration.sql similarity index 90% rename from src/backend/src/prisma/migrations/20241218044032_homepage_updates/migration.sql rename to src/backend/src/prisma/migrations/20241219125602_homepage_updates/migration.sql index f35a709d3f..37c468e58e 100644 --- a/src/backend/src/prisma/migrations/20241218044032_homepage_updates/migration.sql +++ b/src/backend/src/prisma/migrations/20241219125602_homepage_updates/migration.sql @@ -9,7 +9,8 @@ CREATE TABLE "Announcement" ( "announcementId" TEXT NOT NULL, "text" TEXT NOT NULL, "dateCreated" TIMESTAMP(3) NOT NULL, - "userCreatedId" TEXT NOT NULL, + "dateDeleted" TIMESTAMP(3), + "senderName" TEXT NOT NULL, "slackEventId" TEXT NOT NULL, "slackChannelName" TEXT NOT NULL, @@ -38,6 +39,9 @@ CREATE TABLE "_userNotifications" ( "B" TEXT NOT NULL ); +-- CreateIndex +CREATE UNIQUE INDEX "Announcement_slackEventId_key" ON "Announcement"("slackEventId"); + -- CreateIndex CREATE UNIQUE INDEX "_receivedAnnouncements_AB_unique" ON "_receivedAnnouncements"("A", "B"); @@ -53,9 +57,6 @@ CREATE INDEX "_userNotifications_B_index" ON "_userNotifications"("B"); -- AddForeignKey ALTER TABLE "Project" ADD CONSTRAINT "Project_organizationId_fkey" FOREIGN KEY ("organizationId") REFERENCES "Organization"("organizationId") ON DELETE SET NULL ON UPDATE CASCADE; --- AddForeignKey -ALTER TABLE "Announcement" ADD CONSTRAINT "Announcement_userCreatedId_fkey" FOREIGN KEY ("userCreatedId") REFERENCES "User"("userId") ON DELETE RESTRICT ON UPDATE CASCADE; - -- AddForeignKey ALTER TABLE "_receivedAnnouncements" ADD CONSTRAINT "_receivedAnnouncements_A_fkey" FOREIGN KEY ("A") REFERENCES "Announcement"("announcementId") ON DELETE CASCADE ON UPDATE CASCADE; diff --git a/src/backend/src/prisma/schema.prisma b/src/backend/src/prisma/schema.prisma index f70d812b2a..bb8247266d 100644 --- a/src/backend/src/prisma/schema.prisma +++ b/src/backend/src/prisma/schema.prisma @@ -181,7 +181,6 @@ model User { createdMilestones Milestone[] @relation(name: "milestoneCreator") deletedMilestones Milestone[] @relation(name: "milestoneDeleter") unreadAnnouncements Announcement[] @relation(name: "receivedAnnouncements") - createdAnnouncements Announcement[] @relation(name: "createdAnnouncements") unreadNotifications Notification[] @relation(name: "userNotifications") } @@ -932,13 +931,13 @@ model Milestone { } model Announcement { - announcementId String @id @default(uuid()) + announcementId String @id @default(uuid()) text String - usersReceived User[] @relation("receivedAnnouncements") + usersReceived User[] @relation("receivedAnnouncements") dateCreated DateTime - userCreatedId String - userCreated User @relation("createdAnnouncements", fields: [userCreatedId], references: [userId]) - slackEventId String + dateDeleted DateTime? + senderName String + slackEventId String @unique slackChannelName String } diff --git a/src/backend/src/prisma/seed.ts b/src/backend/src/prisma/seed.ts index fdb5ffefdb..ddd4e190cb 100644 --- a/src/backend/src/prisma/seed.ts +++ b/src/backend/src/prisma/seed.ts @@ -33,7 +33,7 @@ import { writeFileSync } from 'fs'; import WorkPackageTemplatesService from '../services/work-package-template.services'; import RecruitmentServices from '../services/recruitment.services'; import OrganizationsService from '../services/organizations.services'; -import NotificationsService from '../services/notifications.services'; +import AnnouncementService from '../services/announcement.service'; const prisma = new PrismaClient(); @@ -1894,6 +1894,16 @@ const performSeed: () => Promise = async () => { await RecruitmentServices.createFaq(batman, 'When was FinishLine created?', 'FinishLine was created in 2019', ner); await RecruitmentServices.createFaq(batman, 'How many developers are working on FinishLine?', '178 as of 2024', ner); + + await AnnouncementService.createAnnouncement( + 'Welcome to Finishline!', + [regina.userId], + new Date(), + 'Thomas Emrax', + '1', + 'software', + ner.organizationId + ); }; performSeed() diff --git a/src/backend/src/services/announcement.service.ts b/src/backend/src/services/announcement.service.ts new file mode 100644 index 0000000000..eb0d9a5f1b --- /dev/null +++ b/src/backend/src/services/announcement.service.ts @@ -0,0 +1,46 @@ +import { Announcement } from 'shared'; +import prisma from '../prisma/prisma'; +import { getAnnouncementQueryArgs } from '../prisma-query-args/announcements.query.args'; +import announcementTransformer from '../transformers/announcements.transformer'; + +export default class AnnouncementService { + /** + * Creates an announcement that is sent to users + * this data is populated from slack events + * @param text slack message text + * @param usersReceivedIds users to send announcements to + * @param dateCreated date created of slack message + * @param senderName name of user who sent slack message + * @param slackEventId id of slack event (provided by slack api) + * @param slackChannelName name of channel message was sent in + * @param organizationId id of organization of users + * @returns the created announcement + */ + static async createAnnouncement( + text: string, + usersReceivedIds: string[], + dateCreated: Date, + senderName: string, + slackEventId: string, + slackChannelName: string, + organizationId: string + ): Promise { + const announcement = await prisma.announcement.create({ + data: { + text, + usersReceived: { + connect: usersReceivedIds.map((id) => ({ + userId: id + })) + }, + dateCreated, + senderName, + slackEventId, + slackChannelName + }, + ...getAnnouncementQueryArgs(organizationId) + }); + + return announcementTransformer(announcement); + } +} diff --git a/src/backend/src/transformers/announcements.transformer.ts b/src/backend/src/transformers/announcements.transformer.ts index 2a8f77e6b0..4fee90eac2 100644 --- a/src/backend/src/transformers/announcements.transformer.ts +++ b/src/backend/src/transformers/announcements.transformer.ts @@ -7,10 +7,12 @@ const announcementTransformer = (announcement: Prisma.AnnouncementGetPayload { await prisma.frequentlyAskedQuestion.deleteMany(); await prisma.organization.deleteMany(); await prisma.user.deleteMany(); + await prisma.announcement.deleteMany(); }; export const createFinanceTeamAndLead = async (organization?: Organization) => { diff --git a/src/backend/tests/unmocked/users.test.ts b/src/backend/tests/unmocked/users.test.ts index 512a651b90..789109a2ae 100644 --- a/src/backend/tests/unmocked/users.test.ts +++ b/src/backend/tests/unmocked/users.test.ts @@ -4,6 +4,7 @@ import { batmanAppAdmin } from '../test-data/users.test-data'; import UsersService from '../../src/services/users.services'; import { NotFoundException } from '../../src/utils/errors.utils'; import NotificationsService from '../../src/services/notifications.services'; +import AnnouncementService from '../../src/services/announcement.service'; describe('User Tests', () => { let orgId: string; @@ -81,7 +82,7 @@ describe('User Tests', () => { ).rejects.toThrow(new NotFoundException('User', '1')); }); - it('Succeeds and gets user notifications', async () => { + it('Succeeds and removes user notification', async () => { const testBatman = await createTestUser(batmanAppAdmin, orgId); await NotificationsService.sendNotifcationToUsers('test1', 'test1', [testBatman.userId], orgId); await NotificationsService.sendNotifcationToUsers('test2', 'test2', [testBatman.userId], orgId); @@ -102,4 +103,40 @@ describe('User Tests', () => { expect(updatedNotifications[0].text).toBe('test2'); }); }); + + describe('Get Announcements', () => { + it('fails on invalid user id', async () => { + await expect(async () => await UsersService.getUserUnreadAnnouncements('1', organization)).rejects.toThrow( + new NotFoundException('User', '1') + ); + }); + + it('Succeeds and gets user announcements', async () => { + const testBatman = await createTestUser(batmanAppAdmin, orgId); + await AnnouncementService.createAnnouncement( + 'test1', + [testBatman.userId], + new Date(), + 'Thomas Emrax', + '1', + 'software', + organization.organizationId + ); + await AnnouncementService.createAnnouncement( + 'test2', + [testBatman.userId], + new Date(), + 'Superman', + '50', + 'mechanical', + organization.organizationId + ); + + const announcements = await UsersService.getUserUnreadAnnouncements(testBatman.userId, organization); + + expect(announcements).toHaveLength(2); + expect(announcements[0].text).toBe('test1'); + expect(announcements[1].text).toBe('test2'); + }); + }); }); diff --git a/src/shared/src/types/announcements.types.ts b/src/shared/src/types/announcements.types.ts index 9315535c86..c0e2d615a7 100644 --- a/src/shared/src/types/announcements.types.ts +++ b/src/shared/src/types/announcements.types.ts @@ -3,8 +3,10 @@ import { User } from './user-types'; export interface Announcement { announcementId: string; text: string; - userCreated: User; + usersReceived: User[]; + senderName: string; dateCreated: Date; slackEventId: string; slackChannelName: string; + dateDeleted?: Date; }