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 orderTotal = Number(order.total ?? 0); const commission = Number(service.commission ?? 0); const actualRevenue = orderTotal * (1 - commission); if (!isNaN(actualRevenue)) { 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(), }; }, }), })); } }