Skip to content

Commit 07aed90

Browse files
authored
Introduce Feedback System (PalisadoesFoundation#1387)
* Add feedback model and typedefs * Add field level resolvers * Add mutation and queries * Add tests for field level resolvers * Complete testing for feedback system
1 parent 3973d69 commit 07aed90

27 files changed

+909
-0
lines changed

codegen.ts

+2
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,8 @@ const config: CodegenConfig = {
4444

4545
EventProject: "../models/EventProject#InterfaceEventProject",
4646

47+
Feedback: "../models/Feedback#InterfaceFeedback",
48+
4749
// File: '../models/File#InterfaceFile',
4850

4951
Group: "../models/Group#InterfaceGroup",

schema.graphql

+18
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ type CheckIn {
3131
allotedRoom: String
3232
allotedSeat: String
3333
event: Event!
34+
feedbackSubmitted: Boolean!
3435
time: DateTime!
3536
user: User!
3637
}
@@ -145,10 +146,12 @@ type Event {
145146
allDay: Boolean!
146147
attendees: [User!]!
147148
attendeesCheckInStatus: [CheckInStatus!]!
149+
averageFeedbackScore: Float
148150
creator: User!
149151
description: String!
150152
endDate: Date!
151153
endTime: Time
154+
feedback: [Feedback!]!
152155
isPublic: Boolean!
153156
isRegisterable: Boolean!
154157
latitude: Latitude
@@ -257,6 +260,19 @@ type ExtendSession {
257260
refreshToken: String!
258261
}
259262

263+
type Feedback {
264+
_id: ID!
265+
event: Event!
266+
rating: Int!
267+
review: String
268+
}
269+
270+
input FeedbackInput {
271+
eventId: ID!
272+
rating: Int!
273+
review: String
274+
}
275+
260276
interface FieldError {
261277
message: String!
262278
path: [String!]!
@@ -393,6 +409,7 @@ type Mutation {
393409
acceptAdmin(id: ID!): Boolean!
394410
acceptMembershipRequest(membershipRequestId: ID!): MembershipRequest!
395411
addEventAttendee(data: EventAttendeeInput!): User!
412+
addFeedback(data: FeedbackInput!): Feedback!
396413
addLanguageTranslation(data: LanguageInput!): Language!
397414
addOrganizationImage(file: String!, organizationId: String!): Organization!
398415
addUserImage(file: String!): User!
@@ -710,6 +727,7 @@ type Query {
710727
getDonationByOrgIdConnection(first: Int, orgId: ID!, skip: Int, where: DonationWhereInput): [Donation!]!
711728
getPlugins: [Plugin]
712729
getlanguage(lang_code: String!): [Translation]
730+
hasSubmittedFeedback(eventId: ID!, userId: ID!): Boolean
713731
joinedOrganizations(id: ID): [Organization]
714732
me: User!
715733
myLanguage: String

src/constants.ts

+13
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,13 @@ export const EVENT_PROJECT_NOT_FOUND_ERROR = {
3131
MESSAGE: "eventProject.notFound",
3232
PARAM: "eventProject",
3333
};
34+
35+
export const FEEDBACK_ALREADY_SUBMITTED = {
36+
MESSAGE: "The user has already submitted a feedback for this event.",
37+
CODE: "feedback.alreadySubmitted",
38+
PARAM: "feedback.alreadySubmitted",
39+
};
40+
3441
export const INVALID_OTP = "Invalid OTP";
3542

3643
export const IN_PRODUCTION = process.env.NODE_ENV === "production";
@@ -146,6 +153,12 @@ export const USER_NOT_REGISTERED_FOR_EVENT = {
146153
PARAM: "user.notRegistered",
147154
};
148155

156+
export const USER_NOT_CHECKED_IN = {
157+
MESSAGE: "The user did not check in for the event.",
158+
CODE: "user.notCheckedIn",
159+
PARAM: "user.notCheckedIn",
160+
};
161+
149162
export const USER_NOT_ORGANIZATION_ADMIN = {
150163
MESSAGE: "Error: User must be an ADMIN",
151164
CODE: "role.notValid.admin",

src/models/CheckIn.ts

+6
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ export interface InterfaceCheckIn {
1515
time: Date;
1616
allotedRoom: string;
1717
allotedSeat: string;
18+
feedbackSubmitted: boolean;
1819
}
1920

2021
const checkInSchema = new Schema({
@@ -36,6 +37,11 @@ const checkInSchema = new Schema({
3637
type: String,
3738
required: false,
3839
},
40+
feedbackSubmitted: {
41+
type: Boolean,
42+
required: true,
43+
default: false,
44+
},
3945
});
4046

4147
// We will also create an index here for faster database querying

src/models/Feedback.ts

+41
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
import type { Types, PopulatedDoc, Document, Model } from "mongoose";
2+
import { Schema, model, models } from "mongoose";
3+
import type { InterfaceEvent } from "./Event";
4+
5+
export interface InterfaceFeedback {
6+
_id: Types.ObjectId;
7+
eventId: PopulatedDoc<InterfaceEvent & Document>;
8+
rating: number;
9+
review: string | null;
10+
}
11+
12+
const feedbackSchema = new Schema({
13+
eventId: {
14+
type: Schema.Types.ObjectId,
15+
ref: "Event",
16+
required: true,
17+
},
18+
rating: {
19+
type: Number,
20+
required: true,
21+
default: 0,
22+
max: 10,
23+
},
24+
review: {
25+
type: String,
26+
required: false,
27+
},
28+
});
29+
30+
// We will also create an index here for faster database querying
31+
feedbackSchema.index({
32+
eventId: 1,
33+
});
34+
35+
const feedbackModel = (): Model<InterfaceFeedback> =>
36+
model<InterfaceFeedback>("Feedback", feedbackSchema);
37+
38+
// This syntax is needed to prevent Mongoose OverwriteModelError while running tests.
39+
export const Feedback = (models.Feedback || feedbackModel()) as ReturnType<
40+
typeof feedbackModel
41+
>;

src/models/index.ts

+1
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ export * from "./Donation";
77
export * from "./Event";
88
export * from "./EventAttendee";
99
export * from "./EventProject";
10+
export * from "./Feedback";
1011
export * from "./File";
1112
export * from "./Group";
1213
export * from "./GroupChat";

src/resolvers/Event/attendees.ts

+1
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ export const attendees: EventResolvers["attendees"] = async (parent) => {
77
})
88
.populate("userId")
99
.lean();
10+
1011
return eventAttendeeObjects.map(
1112
(eventAttendeeObject) => eventAttendeeObject.userId
1213
);
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import type { EventResolvers } from "../../types/generatedGraphQLTypes";
2+
import { Feedback } from "../../models";
3+
4+
export const averageFeedbackScore: EventResolvers["averageFeedbackScore"] =
5+
async (parent) => {
6+
const feedbacks = await Feedback.find({
7+
eventId: parent._id,
8+
})
9+
.select("rating")
10+
.lean();
11+
12+
// Return null if no feedback has been submitted
13+
if (feedbacks.length === 0) return null;
14+
15+
// Return the average feedback score
16+
const sum = feedbacks.reduce(
17+
(accumulator, feedback) => accumulator + feedback.rating,
18+
0
19+
);
20+
return sum / feedbacks.length;
21+
};

src/resolvers/Event/feedback.ts

+8
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
import type { EventResolvers } from "../../types/generatedGraphQLTypes";
2+
import { Feedback } from "../../models";
3+
4+
export const feedback: EventResolvers["feedback"] = async (parent) => {
5+
return Feedback.find({
6+
eventId: parent._id,
7+
}).lean();
8+
};

src/resolvers/Event/index.ts

+4
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,16 @@
11
import type { EventResolvers } from "../../types/generatedGraphQLTypes";
22
import { attendees } from "./attendees";
33
import { attendeesCheckInStatus } from "./attendeesCheckInStatus";
4+
import { averageFeedbackScore } from "./averageFeedbackScore";
5+
import { feedback } from "./feedback";
46
import { organization } from "./organization";
57
import { projects } from "./projects";
68

79
export const Event: EventResolvers = {
810
attendees,
911
attendeesCheckInStatus,
12+
averageFeedbackScore,
13+
feedback,
1014
organization,
1115
projects,
1216
};

src/resolvers/Feedback/event.ts

+8
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
import type { FeedbackResolvers } from "../../types/generatedGraphQLTypes";
2+
import { Event } from "../../models";
3+
4+
export const event: FeedbackResolvers["event"] = async (parent) => {
5+
return await Event.findOne({
6+
_id: parent.eventId,
7+
}).lean();
8+
};

src/resolvers/Feedback/index.ts

+6
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
import type { FeedbackResolvers } from "../../types/generatedGraphQLTypes";
2+
import { event } from "./event";
3+
4+
export const Feedback: FeedbackResolvers = {
5+
event,
6+
};

src/resolvers/Mutation/addFeedback.ts

+79
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
import {
2+
EVENT_NOT_FOUND_ERROR,
3+
USER_NOT_FOUND_ERROR,
4+
USER_NOT_CHECKED_IN,
5+
USER_NOT_REGISTERED_FOR_EVENT,
6+
FEEDBACK_ALREADY_SUBMITTED,
7+
} from "../../constants";
8+
import type { MutationResolvers } from "../../types/generatedGraphQLTypes";
9+
import { errors, requestContext } from "../../libraries";
10+
import { User, Event, EventAttendee, CheckIn, Feedback } from "../../models";
11+
12+
export const addFeedback: MutationResolvers["addFeedback"] = async (
13+
_parent,
14+
args,
15+
context
16+
) => {
17+
const currentUserExists = await User.exists({
18+
_id: context.userId,
19+
});
20+
21+
if (!currentUserExists) {
22+
throw new errors.NotFoundError(
23+
requestContext.translate(USER_NOT_FOUND_ERROR.MESSAGE),
24+
USER_NOT_FOUND_ERROR.CODE,
25+
USER_NOT_FOUND_ERROR.PARAM
26+
);
27+
}
28+
29+
const currentEventExists = await Event.exists({
30+
_id: args.data.eventId,
31+
});
32+
33+
if (!currentEventExists) {
34+
throw new errors.NotFoundError(
35+
requestContext.translate(EVENT_NOT_FOUND_ERROR.MESSAGE),
36+
EVENT_NOT_FOUND_ERROR.CODE,
37+
EVENT_NOT_FOUND_ERROR.PARAM
38+
);
39+
}
40+
41+
const eventAttendeeObject = await EventAttendee.findOne({
42+
eventId: args.data.eventId,
43+
userId: context.userId,
44+
})
45+
.populate("checkInId")
46+
.lean();
47+
48+
if (eventAttendeeObject === null) {
49+
throw new errors.ConflictError(
50+
requestContext.translate(USER_NOT_REGISTERED_FOR_EVENT.MESSAGE),
51+
USER_NOT_REGISTERED_FOR_EVENT.CODE,
52+
USER_NOT_REGISTERED_FOR_EVENT.PARAM
53+
);
54+
}
55+
56+
if (eventAttendeeObject.checkInId === null) {
57+
throw new errors.ConflictError(
58+
requestContext.translate(USER_NOT_CHECKED_IN.MESSAGE),
59+
USER_NOT_CHECKED_IN.CODE,
60+
USER_NOT_CHECKED_IN.PARAM
61+
);
62+
}
63+
64+
if (eventAttendeeObject.checkInId.feedbackSubmitted) {
65+
throw new errors.ConflictError(
66+
requestContext.translate(FEEDBACK_ALREADY_SUBMITTED.MESSAGE),
67+
FEEDBACK_ALREADY_SUBMITTED.CODE,
68+
FEEDBACK_ALREADY_SUBMITTED.PARAM
69+
);
70+
}
71+
72+
await CheckIn.findByIdAndUpdate(eventAttendeeObject.checkInId, {
73+
feedbackSubmitted: true,
74+
});
75+
76+
const feedback = await Feedback.create({ ...args.data });
77+
78+
return feedback;
79+
};

src/resolvers/Mutation/index.ts

+2
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import type { MutationResolvers } from "../../types/generatedGraphQLTypes";
22
import { acceptAdmin } from "./acceptAdmin";
33
import { acceptMembershipRequest } from "./acceptMembershipRequest";
44
import { addEventAttendee } from "./addEventAttendee";
5+
import { addFeedback } from "./addFeedback";
56
import { addLanguageTranslation } from "./addLanguageTranslation";
67
import { addOrganizationImage } from "./addOrganizationImage";
78
import { addUserImage } from "./addUserImage";
@@ -85,6 +86,7 @@ export const Mutation: MutationResolvers = {
8586
acceptAdmin,
8687
acceptMembershipRequest,
8788
addEventAttendee,
89+
addFeedback,
8890
addLanguageTranslation,
8991
addOrganizationImage,
9092
addUserImage,
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
import type { QueryResolvers } from "../../types/generatedGraphQLTypes";
2+
import { User, Event, EventAttendee } from "../../models";
3+
import { errors, requestContext } from "../../libraries";
4+
import {
5+
EVENT_NOT_FOUND_ERROR,
6+
USER_NOT_FOUND_ERROR,
7+
USER_NOT_CHECKED_IN,
8+
USER_NOT_REGISTERED_FOR_EVENT,
9+
} from "../../constants";
10+
11+
export const hasSubmittedFeedback: QueryResolvers["hasSubmittedFeedback"] =
12+
async (_parent, args) => {
13+
const currentUserExists = await User.exists({
14+
_id: args.userId,
15+
});
16+
17+
if (!currentUserExists) {
18+
throw new errors.NotFoundError(
19+
requestContext.translate(USER_NOT_FOUND_ERROR.MESSAGE),
20+
USER_NOT_FOUND_ERROR.CODE,
21+
USER_NOT_FOUND_ERROR.PARAM
22+
);
23+
}
24+
25+
const currentEventExists = await Event.exists({
26+
_id: args.eventId,
27+
});
28+
29+
if (!currentEventExists) {
30+
throw new errors.NotFoundError(
31+
requestContext.translate(EVENT_NOT_FOUND_ERROR.MESSAGE),
32+
EVENT_NOT_FOUND_ERROR.CODE,
33+
EVENT_NOT_FOUND_ERROR.PARAM
34+
);
35+
}
36+
37+
const eventAttendeeObject = await EventAttendee.findOne({
38+
...args,
39+
})
40+
.populate("checkInId")
41+
.lean();
42+
43+
if (eventAttendeeObject === null) {
44+
throw new errors.ConflictError(
45+
requestContext.translate(USER_NOT_REGISTERED_FOR_EVENT.MESSAGE),
46+
USER_NOT_REGISTERED_FOR_EVENT.CODE,
47+
USER_NOT_REGISTERED_FOR_EVENT.PARAM
48+
);
49+
}
50+
51+
if (eventAttendeeObject.checkInId === null) {
52+
throw new errors.ConflictError(
53+
requestContext.translate(USER_NOT_CHECKED_IN.MESSAGE),
54+
USER_NOT_CHECKED_IN.CODE,
55+
USER_NOT_CHECKED_IN.PARAM
56+
);
57+
}
58+
59+
return eventAttendeeObject.checkInId.feedbackSubmitted;
60+
};

0 commit comments

Comments
 (0)