- Updated MessageSchema to include context-aware filtering for notifications, allowing retrieval based on recipientId for NOTIFICATION and SYSTEM contexts. - Enhanced RefundTicketSchema to notify moderators upon refund requests, improving communication and response times. - Modified ResumeSchema to send notifications to mentors and center owners when a new resume is submitted, ensuring timely updates. - Improved ServiceSchema to notify center owners and mentors about service approvals, enhancing user engagement and awareness. - Implemented role-based access control checks across schemas to ensure only authorized users can perform specific actions, enhancing security and user experience.
486 lines
18 KiB
TypeScript
486 lines
18 KiB
TypeScript
import { Inject, Injectable, Logger } from '@nestjs/common'
|
|
import { Message, MessageContextType, MessageType, Role, ServiceStatus } from '@prisma/client'
|
|
import { Pothos, PothosRef, PothosSchema, SchemaBuilderToken } from '@smatch-corp/nestjs-pothos'
|
|
import { Builder } from '../Graphql/graphql.builder'
|
|
import { MailService } from '../Mail/mail.service'
|
|
import { MinioService } from '../Minio/minio.service'
|
|
import { PrismaService } from '../Prisma/prisma.service'
|
|
import { PubSubEvent } from '../common/pubsub/pubsub-event'
|
|
import { DateTimeUtils } from '../common/utils/datetime.utils'
|
|
@Injectable()
|
|
export class ServiceSchema extends PothosSchema {
|
|
constructor(
|
|
@Inject(SchemaBuilderToken) private readonly builder: Builder,
|
|
private readonly prisma: PrismaService,
|
|
private readonly minioService: MinioService,
|
|
private readonly mailService: MailService,
|
|
) {
|
|
super()
|
|
}
|
|
|
|
@PothosRef()
|
|
service() {
|
|
return this.builder.prismaObject('Service', {
|
|
description: 'A service offered by a center.',
|
|
fields: (t) => ({
|
|
id: t.exposeID('id', {
|
|
description: 'The ID of the service.',
|
|
}),
|
|
name: t.exposeString('name', {
|
|
description: 'The name of the service.',
|
|
}),
|
|
description: t.exposeString('description', {
|
|
description: 'The description of the service.',
|
|
}),
|
|
content: t.exposeString('content', {
|
|
description: 'The content of the service.',
|
|
}),
|
|
centerId: t.exposeID('centerId', {
|
|
description: 'The ID of the center that offers the service.',
|
|
}),
|
|
userId: t.exposeID('userId', {
|
|
description: 'The ID of the user who requested the service.',
|
|
}),
|
|
adminNote: t.relation('adminNote', {
|
|
description: 'The admin note of the service.',
|
|
}),
|
|
price: t.exposeFloat('price', {
|
|
description: 'The price of the service.',
|
|
}),
|
|
commission: t.exposeFloat('commission', {
|
|
description: 'The commission of the service.',
|
|
}),
|
|
rating: t.expose('rating', {
|
|
type: 'Float',
|
|
nullable: true,
|
|
description: 'The rating of the service.',
|
|
}),
|
|
imageFile: t.relation('imageFile', {
|
|
description: 'The image file for the service.',
|
|
}),
|
|
imageFileId: t.exposeID('imageFileId', {
|
|
nullable: true,
|
|
description: 'The ID of the image file for the service.',
|
|
}),
|
|
imageFileUrl: t.string({
|
|
nullable: true,
|
|
description: 'The URL of the image file for the service.',
|
|
resolve: async (service) => {
|
|
// get file id from imageFileUrl
|
|
const imageFileId = service.imageFileUrl?.split('/').pop()?.split('?')[0]
|
|
return await this.minioService.updatePresignUrl(
|
|
imageFileId ?? '',
|
|
'files',
|
|
service.imageFileUrl ?? undefined,
|
|
)
|
|
},
|
|
}),
|
|
status: t.expose('status', {
|
|
type: ServiceStatus,
|
|
description: 'The status of the service.',
|
|
}),
|
|
isActive: t.exposeBoolean('isActive', {
|
|
description: 'Whether the service is active.',
|
|
}),
|
|
createdAt: t.expose('createdAt', {
|
|
type: 'DateTime',
|
|
description: 'The date and time the service was created.',
|
|
}),
|
|
updatedAt: t.expose('updatedAt', {
|
|
type: 'DateTime',
|
|
description: 'The date and time the service was updated.',
|
|
}),
|
|
feedbacks: t.relation('feedbacks', {
|
|
description: 'The feedbacks for the service.',
|
|
}),
|
|
order: t.relation('order', {
|
|
description: 'The order for the service.',
|
|
}),
|
|
center: t.relation('center', {
|
|
description: 'The center that offers the service.',
|
|
}),
|
|
workshop: t.relation('workshop', {
|
|
description: 'The workshop for the service.',
|
|
}),
|
|
serviceAndCategory: t.relation('serviceAndCategory', {
|
|
description: 'The service and category for the service.',
|
|
}),
|
|
workshopOrganization: t.relation('workshopOrganization', {
|
|
description: 'The workshop organization for the service.',
|
|
}),
|
|
user: t.relation('user', {
|
|
description: 'The user who requested the service.',
|
|
}),
|
|
managedService: t.relation('managedService', {
|
|
description: 'The managed service for the service.',
|
|
}),
|
|
feedbacked: t.boolean({
|
|
description: 'Whether the user has already provided feedback for the service.',
|
|
nullable: true,
|
|
resolve: async (service, _args, ctx) => {
|
|
if (ctx.isSubscription) {
|
|
return null
|
|
}
|
|
if (!ctx.http.me) {
|
|
return false
|
|
}
|
|
const serviceFeedbacks = await this.prisma.serviceFeedback.findMany({
|
|
where: {
|
|
serviceId: service.id,
|
|
userId: ctx.http.me.id,
|
|
},
|
|
})
|
|
return serviceFeedbacks.length > 0
|
|
},
|
|
}),
|
|
quiz: t.relation('quiz', {
|
|
description: 'The quiz of the service.',
|
|
}),
|
|
}),
|
|
})
|
|
}
|
|
|
|
@Pothos()
|
|
init() {
|
|
this.builder.queryFields((t) => ({
|
|
testServices: t.prismaConnection(
|
|
{
|
|
description: 'A test connection for services',
|
|
type: this.service(),
|
|
cursor: 'id',
|
|
args: this.builder.generator.findManyArgs('Service'),
|
|
resolve: async (query, _root, _args, _ctx, _info) => {
|
|
return await this.prisma.service.findMany({
|
|
...query,
|
|
})
|
|
},
|
|
totalCount: (query) => {
|
|
return this.prisma.service.count({
|
|
...query,
|
|
})
|
|
},
|
|
},
|
|
{},
|
|
),
|
|
services: t.prismaField({
|
|
description: 'Retrieve a list of services with optional filtering, ordering, and pagination.',
|
|
type: [this.service()],
|
|
args: this.builder.generator.findManyArgs('Service'),
|
|
resolve: async (query, _root, args, _ctx, _info) => {
|
|
return await this.prisma.service.findMany({
|
|
...query,
|
|
where: args.filter ?? undefined,
|
|
orderBy: args.orderBy ?? undefined,
|
|
skip: args.skip ?? undefined,
|
|
take: args.take ?? undefined,
|
|
cursor: args.cursor ?? undefined,
|
|
})
|
|
},
|
|
}),
|
|
servicesByCenter: t.prismaField({
|
|
description: 'Retrieve a list of services with optional filtering, ordering, and pagination.',
|
|
type: [this.service()],
|
|
|
|
args: this.builder.generator.findManyArgs('Service'),
|
|
resolve: async (query, _root, args, ctx, _info) => {
|
|
if (ctx.isSubscription) {
|
|
throw new Error('Not allowed')
|
|
}
|
|
// check role if user is mentor or center owner
|
|
const role = ctx.http.me?.role
|
|
if (role !== Role.CENTER_MENTOR && role !== Role.CENTER_OWNER) {
|
|
throw new Error('Not allowed')
|
|
}
|
|
if (role === Role.CENTER_MENTOR) {
|
|
// load only service belong to center of current user
|
|
const managedServices = await this.prisma.managedService.findMany({
|
|
where: { mentorId: ctx.http.me?.id ?? '' },
|
|
})
|
|
if (!managedServices) {
|
|
throw new Error('Managed services not found')
|
|
}
|
|
// query services that have serviceId in managedServices
|
|
args.filter = {
|
|
id: { in: managedServices.map((service) => service.serviceId) },
|
|
}
|
|
}
|
|
// if role is center owner, load all services belong to center of current user
|
|
if (role === Role.CENTER_OWNER) {
|
|
const center = await this.prisma.center.findUnique({
|
|
where: { centerOwnerId: ctx.http.me?.id ?? '' },
|
|
})
|
|
if (!center) {
|
|
throw new Error('Center not found')
|
|
}
|
|
args.filter = { centerId: { in: [center.id] } }
|
|
}
|
|
|
|
return await this.prisma.service.findMany({
|
|
...query,
|
|
where: args.filter ?? undefined,
|
|
orderBy: args.orderBy ?? undefined,
|
|
skip: args.skip ?? undefined,
|
|
take: args.take ?? undefined,
|
|
cursor: args.cursor ?? undefined,
|
|
})
|
|
},
|
|
}),
|
|
service: t.prismaField({
|
|
description: 'Retrieve a single service by its unique identifier.',
|
|
type: this.service(),
|
|
args: {
|
|
input: t.arg({
|
|
type: this.builder.generator.getWhereUnique('Service'),
|
|
required: true,
|
|
}),
|
|
},
|
|
resolve: async (query, _root, args, _ctx, _info) => {
|
|
return await this.prisma.service.findUnique({
|
|
...query,
|
|
where: args.input,
|
|
include: {
|
|
feedbacks: true,
|
|
},
|
|
})
|
|
},
|
|
}),
|
|
}))
|
|
|
|
// Mutation section
|
|
this.builder.mutationFields((t) => ({
|
|
createService: t.prismaField({
|
|
description: 'Create a new service.',
|
|
type: this.service(),
|
|
args: {
|
|
input: t.arg({
|
|
type: this.builder.generator.getCreateInput('Service', ['user']),
|
|
required: true,
|
|
}),
|
|
},
|
|
resolve: async (query, _root, args, ctx, _info) => {
|
|
if (ctx.isSubscription) {
|
|
throw new Error('Not allowed')
|
|
}
|
|
// replace userId with current user id
|
|
args.input.user = { connect: { id: ctx.http.me?.id ?? '' } }
|
|
const service = await this.prisma.service.create({
|
|
...query,
|
|
data: args.input,
|
|
})
|
|
// send notification to all mentor or center owner for the center
|
|
const center = await this.prisma.center.findUnique({
|
|
where: { id: service.centerId },
|
|
})
|
|
if (!center?.centerOwnerId) {
|
|
throw new Error('Center owner not found')
|
|
}
|
|
const centerOwner = await this.prisma.user.findUnique({
|
|
where: { id: center.centerOwnerId },
|
|
})
|
|
if (!centerOwner) {
|
|
throw new Error('Center owner not found')
|
|
}
|
|
const centerMentor = await this.prisma.centerMentor.findMany({
|
|
where: { centerId: service.centerId },
|
|
})
|
|
const mentorIds = centerMentor.map((mentor) => mentor.mentorId)
|
|
const mentorEmails = await this.prisma.user.findMany({
|
|
where: { id: { in: mentorIds } },
|
|
})
|
|
const emails = [centerOwner.email, ...mentorEmails.map((mentor) => mentor.email)]
|
|
await this.mailService.sendTemplateEmail(emails, 'Thông báo về trạng thái dịch vụ', 'ServiceApproved', {
|
|
SERVICE_NAME: service.name,
|
|
CENTER_NAME: center.name,
|
|
})
|
|
// send notification to all mentor or center owner for the center using context
|
|
const message = await this.prisma.message.create({
|
|
data: {
|
|
senderId: ctx.http.me?.id ?? '',
|
|
recipientId: centerOwner.id,
|
|
type: MessageType.TEXT,
|
|
content: `Dịch vụ ${service.name} của bạn đã được chấp thuận`,
|
|
sentAt: DateTimeUtils.nowAsJSDate(),
|
|
context: MessageContextType.NOTIFICATION,
|
|
},
|
|
})
|
|
ctx.http.pubSub.publish(`${PubSubEvent.NOTIFICATION}.${centerOwner.id}`, message)
|
|
return service
|
|
},
|
|
}),
|
|
updateService: t.prismaField({
|
|
description: 'Update an existing service.',
|
|
type: this.service(),
|
|
args: {
|
|
input: t.arg({
|
|
type: this.builder.generator.getUpdateInput('Service'),
|
|
required: true,
|
|
}),
|
|
where: t.arg({
|
|
type: this.builder.generator.getWhereUnique('Service'),
|
|
required: true,
|
|
}),
|
|
},
|
|
resolve: async (query, _root, args, _ctx, _info) => {
|
|
return await this.prisma.service.update({
|
|
...query,
|
|
where: args.where,
|
|
data: args.input,
|
|
})
|
|
},
|
|
}),
|
|
deleteService: t.prismaField({
|
|
description: 'Delete an existing service.',
|
|
type: this.service(),
|
|
args: {
|
|
where: t.arg({
|
|
type: this.builder.generator.getWhereUnique('Service'),
|
|
required: true,
|
|
}),
|
|
},
|
|
resolve: async (query, _root, args, _ctx, _info) => {
|
|
return await this.prisma.service.delete({
|
|
...query,
|
|
where: args.where,
|
|
})
|
|
},
|
|
}),
|
|
approveOrRejectService: t.prismaField({
|
|
description: 'Approve or reject a service. For moderator only.',
|
|
type: this.service(),
|
|
args: {
|
|
serviceId: t.arg({
|
|
type: 'String',
|
|
required: true,
|
|
}),
|
|
approve: t.arg({
|
|
type: 'Boolean',
|
|
required: true,
|
|
description: 'The approve status of the service.',
|
|
}),
|
|
adminNote: t.arg({
|
|
type: 'String',
|
|
required: false,
|
|
description: 'The admin note of the service.',
|
|
}),
|
|
commission: t.arg({
|
|
type: 'Float',
|
|
required: false,
|
|
description: 'The commission of the service. present as float 0 to 1 and required if approve is true',
|
|
}),
|
|
},
|
|
resolve: async (query, _root, args, ctx, _info) => {
|
|
if (ctx.isSubscription) {
|
|
throw new Error('Not allowed')
|
|
}
|
|
return await this.prisma.$transaction(async (prisma) => {
|
|
// check if service is already approved or rejected
|
|
const service = await prisma.service.findUnique({
|
|
where: { id: args.serviceId },
|
|
})
|
|
if (!service) {
|
|
throw new Error('Service not found')
|
|
}
|
|
if (service.status !== ServiceStatus.PENDING) {
|
|
throw new Error('Service is already approved or rejected')
|
|
}
|
|
let commission = args.commission
|
|
if (args.approve && !commission) {
|
|
commission = 0.05
|
|
}
|
|
// update service status
|
|
const updatedService = await prisma.service.update({
|
|
...query,
|
|
where: { id: args.serviceId },
|
|
data: {
|
|
status: args.approve ? ServiceStatus.APPROVED : ServiceStatus.REJECTED,
|
|
adminNote: {
|
|
create: {
|
|
content: args.adminNote ?? '',
|
|
notedByUserId: ctx.http.me?.id ?? '',
|
|
},
|
|
},
|
|
commission: commission ?? 0,
|
|
},
|
|
})
|
|
// mail to all mentor or center owner for the center
|
|
const center = await prisma.center.findUnique({
|
|
where: { id: service.centerId },
|
|
})
|
|
if (!center?.centerOwnerId) {
|
|
throw new Error('Center owner not found')
|
|
}
|
|
const centerOwner = await prisma.user.findUnique({
|
|
where: { id: center.centerOwnerId },
|
|
})
|
|
if (!centerOwner) {
|
|
throw new Error('Center owner not found')
|
|
}
|
|
const centerMentor = await prisma.centerMentor.findMany({
|
|
where: { centerId: service.centerId },
|
|
})
|
|
const mentorIds = centerMentor.map((mentor) => mentor.mentorId)
|
|
// get mentor emails
|
|
const mentorEmails = await prisma.user.findMany({
|
|
where: { id: { in: mentorIds } },
|
|
})
|
|
const emails = [centerOwner.email, ...mentorEmails.map((mentor) => mentor.email)]
|
|
if (args.approve) {
|
|
await this.mailService.sendTemplateEmail(emails, 'Thông báo về trạng thái dịch vụ', 'ServiceApproved', {
|
|
SERVICE_NAME: service.name,
|
|
CENTER_NAME: center.name,
|
|
})
|
|
// get user ids from mentorIds
|
|
const userIds = mentorIds.map((id) => id)
|
|
// send notification to user using context
|
|
userIds.forEach(async (id) => {
|
|
// add message to database
|
|
const message = await this.prisma.message.create({
|
|
data: {
|
|
senderId: ctx.http.me?.id ?? '',
|
|
recipientId: id,
|
|
type: MessageType.TEXT,
|
|
content: `Dịch vụ ${service.name} của bạn đã được chấp thuận`,
|
|
sentAt: DateTimeUtils.nowAsJSDate(),
|
|
context: MessageContextType.NOTIFICATION,
|
|
metadata: {
|
|
serviceId: service.id,
|
|
},
|
|
},
|
|
})
|
|
ctx.http.pubSub.publish(`${PubSubEvent.NOTIFICATION}.${id}`, message)
|
|
})
|
|
} else {
|
|
await this.mailService.sendTemplateEmail(emails, 'Thông báo về trạng thái dịch vụ', 'ServiceRejected', {
|
|
SERVICE_NAME: service.name,
|
|
CENTER_NAME: center.name,
|
|
ADMIN_NOTE: args.adminNote ?? 'Không có lý do',
|
|
})
|
|
// send notification to user using context
|
|
// get user ids from mentorIds
|
|
const userIds = mentorIds.map((id) => id)
|
|
userIds.forEach(async (id) => {
|
|
// add message to database
|
|
const message = await this.prisma.message.create({
|
|
data: {
|
|
senderId: ctx.http.me?.id ?? '',
|
|
recipientId: id,
|
|
type: MessageType.TEXT,
|
|
content: `Dịch vụ ${service.name} của bạn đã bị từ chối`,
|
|
sentAt: DateTimeUtils.nowAsJSDate(),
|
|
context: MessageContextType.NOTIFICATION,
|
|
metadata: {
|
|
serviceId: service.id,
|
|
},
|
|
},
|
|
})
|
|
ctx.http.pubSub.publish(`${PubSubEvent.NOTIFICATION}.${id}`, message)
|
|
})
|
|
}
|
|
return updatedService
|
|
})
|
|
},
|
|
}),
|
|
}))
|
|
}
|
|
}
|