- Reformatted import statements and code structure for consistency and clarity across the AnalyticSchema. - Enhanced field descriptions to ensure uniformity and better understanding of the analytics data. - Updated error messages for improved user feedback during analytic data retrieval. - Improved organization of query fields for better maintainability and readability. These changes aim to enhance code readability, maintainability, and user experience within the analytic features.
487 lines
16 KiB
TypeScript
487 lines
16 KiB
TypeScript
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(),
|
|
};
|
|
},
|
|
}),
|
|
}));
|
|
}
|
|
}
|