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 }) }, }), })) } }