import { Inject, Injectable } from '@nestjs/common' import { OrderStatus, Prisma, Role, ServiceStatus } from '@prisma/client' import { Pothos, PothosRef, PothosSchema, SchemaBuilderToken } from '@smatch-corp/nestjs-pothos' import { CenterSchema } from 'src/Center/center.schema' import { Builder } from 'src/Graphql/graphql.builder' import { OrderSchema } from 'src/Order/order.schema' import { PrismaService } from 'src/Prisma/prisma.service' import { ServiceSchema } from 'src/Service/service.schema' import { DateTimeUtils } from 'src/common/utils/datetime.utils' @Injectable() export class AnalyticSchema extends PothosSchema { constructor( @Inject(SchemaBuilderToken) private readonly builder: Builder, private readonly prisma: PrismaService, private readonly serviceSchema: ServiceSchema, private readonly centerSchema: CenterSchema, private readonly orderSchema: OrderSchema, ) { super() } @PothosRef() customerAnalytic() { return this.builder.simpleObject('CustomerAnalytic', { description: 'A customer analytic in the system.', fields: (t) => ({ userId: t.string({ description: 'The ID of the user.', }), activeServiceCount: t.int({ description: 'The number of active services.', }), totalServiceCount: t.int({ description: 'The total number of services.', }), totalSpent: t.float({ description: 'The total amount spent.', }), updatedAt: t.field({ type: 'DateTime', description: 'The date the analytic was last updated.', }), }), }) } @PothosRef() mentorAnalytic() { return this.builder.simpleObject('MentorAnalytic', { description: 'A mentor analytic in the system.', fields: (t) => ({ userId: t.string({ description: 'The ID of the mentor.', }), }), }) } @PothosRef() centerAnalytic() { return this.builder.simpleObject('CenterAnalytic', { description: 'A center analytic in the system.', fields: (t) => ({ centerId: t.string({ description: 'The ID of the center.', }), activeMentorCount: t.int({ description: 'The number of active mentors.', }), activeServiceCount: t.int({ description: 'The number of active services.', }), totalServiceCount: t.int({ description: 'The total number of services.', }), revenue: t.int({ description: 'The total revenue.', }), rating: t.float({ description: 'The average rating.', nullable: true, }), updatedAt: t.field({ type: 'DateTime', description: 'The date the analytic was last updated.', }), }), }) } @PothosRef() platformAnalytic() { return this.builder.simpleObject('PlatformAnalytic', { description: 'A platform analytic in the system.', fields: (t) => ({ topServices: t.field({ type: [this.serviceSchema.service()], description: 'The top services by revenue.', }), topCenters: t.field({ type: [this.centerSchema.center()], description: 'The top centers by revenue.', }), pendingRefunds: t.field({ type: [this.orderSchema.order()], description: 'The pending refunds.', }), activeCenterCount: t.int({ description: 'The number of active centers.', }), totalCenterCount: t.int({ description: 'The total number of centers.', }), totalUserCount: t.int({ description: 'The total number of users.', }), activeMentorCount: t.int({ description: 'The number of active mentors.', }), totalMentorCount: t.int({ description: 'The total number of mentors.', }), totalServiceCount: t.int({ description: 'The total number of services.', }), totalWorkshopCount: t.int({ description: 'The total number of workshops.', }), revenue: t.int({ description: 'The total revenue.', }), approvedServiceCount: t.int({ description: 'The number of approved services.', }), rejectedServiceCount: t.int({ description: 'The number of rejected services.', }), updatedAt: t.field({ type: 'DateTime', description: 'The date the analytic was last updated.', }), }), }) } @PothosRef() timeframes() { return this.builder.enumType('Timeframe', { values: ['day', 'week', 'month', 'year'], }) } @PothosRef() serviceSortBy() { return this.builder.enumType('ServiceSortBy', { values: ['order', 'rating'], }) } @PothosRef() centerSortBy() { return this.builder.enumType('CenterSortBy', { values: ['revenue', 'rating', 'services'], }) } @Pothos() init(): void { this.builder.queryFields((t) => ({ customerAnalytic: t.field({ type: this.customerAnalytic(), description: 'Retrieve a single customer analytic.', resolve: async (_parent, _args, ctx, _info) => { if (!ctx.me) { throw new Error('Unauthorized') } if (ctx.me.role !== Role.CUSTOMER) { throw new Error('Only customers can access this data') } // calculate analytic const activeServiceCount = await this.prisma.order.count({ where: { userId: ctx.me.id, status: OrderStatus.PAID, schedule: { dates: { some: { end: { gte: DateTimeUtils.now().toJSDate(), }, }, }, }, }, }) const totalServiceCount = await this.prisma.order.count({ where: { userId: ctx.me.id, }, }) const totalSpent = await this.prisma.order.aggregate({ where: { userId: ctx.me.id, status: OrderStatus.PAID, }, _sum: { total: true, }, }) return { userId: ctx.me.id, activeServiceCount: activeServiceCount, totalServiceCount: totalServiceCount, totalSpent: totalSpent._sum.total, updatedAt: DateTimeUtils.now(), } }, }), mentorAnalytic: t.field({ type: this.mentorAnalytic(), description: 'Retrieve a single mentor analytic.', resolve: async (_parent, _args, ctx, _info) => { if (!ctx.me) { throw new Error('Unauthorized') } if (ctx.me.role !== Role.CENTER_MENTOR) { throw new Error('Only center mentors can access this data') } // calculate analytic return { userId: ctx.me.id, } }, }), centerAnalytic: t.field({ type: this.centerAnalytic(), description: 'Retrieve a single center analytic.', resolve: async (_parent, _args, ctx, _info) => { if (!ctx.me) { throw new Error('Unauthorized') } if (ctx.me.role !== Role.CENTER_OWNER) { throw new Error('Only center owners can access this data') } // get center by owner id const center = await this.prisma.center.findUnique({ where: { centerOwnerId: ctx.me.id, }, }) if (!center) { throw new Error('Center not found') } // calculate analytic // active mentor include center owner const activeMentorCount = await this.prisma.user.count({ where: { center: { centerOwnerId: ctx.me.id, }, banned: false, }, }) const activeServiceCount = await this.prisma.service.count({ where: { centerId: center.id, status: ServiceStatus.APPROVED, }, }) const totalServiceCount = await this.prisma.service.count({ where: { centerId: center.id, }, }) // calculate revenue from orders of services in the center and factor in commission percentage // query all orders of services in the center and calculate actual revenue of each order // then sum up the revenue let revenue = 0 const orders = await this.prisma.order.findMany({ where: { service: { centerId: center.id }, status: OrderStatus.PAID, }, }) for (const order of orders) { const service = await this.prisma.service.findUnique({ where: { id: order.serviceId }, }) if (!service) { continue } const commission = service.commission const actualRevenue = (order.total || 0) - (order.total || 0) * commission revenue += actualRevenue } return { centerId: center.id, activeMentorCount: activeMentorCount, activeServiceCount: activeServiceCount, totalServiceCount: totalServiceCount, revenue: revenue, updatedAt: DateTimeUtils.now(), } }, }), platformAnalytic: t.field({ type: this.platformAnalytic(), args: { take: t.arg({ type: 'Int', description: 'The number of services to take.', required: true, }), serviceSortBy: t.arg({ type: this.serviceSortBy(), description: 'The field to sort by.', required: true, }), centerSortBy: t.arg({ type: this.centerSortBy(), description: 'The field to sort by.', required: true, }), timeframes: t.arg({ type: this.timeframes(), description: 'The frame of time Eg day, week, month, year.', required: true, }), }, description: 'Retrieve a single platform analytic.', resolve: async (_parent, args, ctx, _info) => { if (!ctx.me) { throw new Error('Unauthorized') } if (ctx.me.role !== Role.ADMIN && ctx.me.role !== Role.MODERATOR) { throw new Error('Only admins and moderators can access this data') } // calculate analytic for services sorted by args.serviceSortBy and args.timeframes const topServices = await this.prisma.service.findMany({ where: { status: ServiceStatus.APPROVED, }, orderBy: { [args.serviceSortBy]: { _count: Prisma.SortOrder.desc, }, }, take: args.take, }) // get top centers by args.centerSortBy const topCenters = await this.prisma.center.findMany({ orderBy: { [args.centerSortBy]: { _count: Prisma.SortOrder.desc, }, }, take: args.take, }) // get pending refunds const pendingRefunds = await this.prisma.order.findMany({ where: { status: OrderStatus.PENDING_REFUND, }, }) // get active center count by center owner not banned and have schedule with dates in the future const activeCenterCount = await this.prisma.center.count({ where: { centerOwner: { banned: false, }, centerMentors: { some: { managedService: { some: { schedule: { some: { dates: { some: { end: { gte: DateTimeUtils.now().toJSDate(), }, }, }, }, }, }, }, }, }, }, }) // get total center count const totalCenterCount = await this.prisma.center.count() // get total user count const totalUserCount = await this.prisma.user.count() // get active mentor count const activeMentorCount = await this.prisma.user.count({ where: { role: Role.CENTER_MENTOR, banned: false, }, }) // get total mentor count const totalMentorCount = await this.prisma.user.count({ where: { role: Role.CENTER_MENTOR, }, }) // get approved service count const approvedServiceCount = await this.prisma.service.count({ where: { status: ServiceStatus.APPROVED, }, }) // get rejected service count const rejectedServiceCount = await this.prisma.service.count({ where: { status: ServiceStatus.REJECTED, }, }) // get total workshop count const totalWorkshopCount = await this.prisma.workshop.count() // get total order count const totalOrderCount = await this.prisma.order.count() // get total service count const totalServiceCount = await this.prisma.service.count() // get revenue let revenue = 0 // query all orders of services in all centers in the past args.timeframes and calculate actual revenue of each order by convert commission percentage to float // convert args.timeframes to number of days const timeframes = DateTimeUtils.subtractDaysFromTimeframe(args.timeframes) const orders = await this.prisma.order.findMany({ where: { status: OrderStatus.PAID, createdAt: { gte: timeframes.toJSDate(), }, }, }) for (const order of orders) { const service = await this.prisma.service.findUnique({ where: { id: order.serviceId }, }) if (!service) { continue } const commission = service.commission const actualRevenue = (order.total || 0) - (order.total || 0) * commission revenue += actualRevenue } // return analytic return { topServices: topServices, topCenters: topCenters, pendingRefunds: pendingRefunds, activeCenterCount: activeCenterCount, totalCenterCount: totalCenterCount, totalUserCount: totalUserCount, activeMentorCount: activeMentorCount, totalMentorCount: totalMentorCount, revenue: revenue, approvedServiceCount: approvedServiceCount, rejectedServiceCount: rejectedServiceCount, totalWorkshopCount: totalWorkshopCount, totalOrderCount: totalOrderCount, totalServiceCount: totalServiceCount, updatedAt: DateTimeUtils.now(), } }, }), })) } }