update code ngu

This commit is contained in:
2024-11-25 22:06:47 +07:00
parent 84a09375da
commit 26dd46c7a0
7 changed files with 489 additions and 56 deletions

View File

@@ -0,0 +1,13 @@
import { Module } from '@nestjs/common'
import { AnalyticSchema } from './analytic.schema'
import { ServiceSchema } from 'src/Service/service.schema'
import { CenterSchema } from 'src/Center/center.schema'
import { OrderSchema } from 'src/Order/order.schema'
import { PayosModule } from 'src/Payos/payos.module'
@Module({
imports: [PayosModule],
providers: [AnalyticSchema, ServiceSchema, CenterSchema, OrderSchema],
exports: [AnalyticSchema, ServiceSchema, CenterSchema, OrderSchema],
})
export class AnalyticModule {}

View File

@@ -0,0 +1,458 @@
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 { DateTimeUtils } from 'src/common/utils/datetime.utils'
import { Builder } from 'src/Graphql/graphql.builder'
import { PrismaService } from 'src/Prisma/prisma.service'
import { ServiceSchema } from 'src/Service/service.schema'
import { CenterSchema } from 'src/Center/center.schema'
import { OrderSchema } from 'src/Order/order.schema'
@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.',
}),
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.isSubscription) throw new Error('Not allowed')
if (!ctx.http.me) throw new Error('Unauthorized')
if (ctx.http.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.http.me.id,
status: OrderStatus.PAID,
schedule: {
dates: {
some: {
end: {
gte: DateTimeUtils.now().toJSDate(),
},
},
},
},
},
})
const totalServiceCount = await this.prisma.order.count({
where: {
userId: ctx.http.me.id,
},
})
const totalSpent = await this.prisma.order.aggregate({
where: {
userId: ctx.http.me.id,
status: OrderStatus.PAID,
},
_sum: {
total: true,
},
})
return {
userId: ctx.http.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.isSubscription) throw new Error('Not allowed')
if (!ctx.http.me) throw new Error('Unauthorized')
if (ctx.http.me.role !== Role.CENTER_MENTOR)
throw new Error('Only center mentors can access this data')
// calculate analytic
return {
userId: ctx.http.me.id,
}
},
}),
centerAnalytic: t.field({
type: this.centerAnalytic(),
description: 'Retrieve a single center analytic.',
resolve: async (_parent, _args, ctx, _info) => {
if (ctx.isSubscription) throw new Error('Not allowed')
if (!ctx.http.me) throw new Error('Unauthorized')
if (ctx.http.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.http.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.http.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.isSubscription) throw new Error('Not allowed')
if (!ctx.http.me) throw new Error('Unauthorized')
if (
ctx.http.me.role !== Role.ADMIN &&
ctx.http.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 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,
updatedAt: DateTimeUtils.now(),
}
},
}),
}))
}
}

View File

@@ -80,7 +80,7 @@ export class CronService {
}
// cron every 1 minute to check payment status where created_at is more than 15 minutes
@Cron(CronExpression.EVERY_MINUTE)
@Cron(CronExpression.EVERY_10_MINUTES)
async checkPaymentStatus() {
Logger.log('Checking payment status', 'checkPaymentStatus')
const payments = await this.prisma.payment.findMany({

View File

@@ -1,8 +0,0 @@
import { Module } from '@nestjs/common'
import { FinanceSchema } from './finance.schema'
@Module({
providers: [FinanceSchema],
exports: [FinanceSchema],
})
export class FinanceModule {}

View File

@@ -1,45 +0,0 @@
import { Inject, Injectable } from '@nestjs/common'
import { Pothos, PothosRef, PothosSchema, SchemaBuilderToken } from '@smatch-corp/nestjs-pothos'
import { Builder } from 'src/Graphql/graphql.builder'
import { PrismaService } from 'src/Prisma/prisma.service'
@Injectable()
export class FinanceSchema extends PothosSchema {
constructor(
@Inject(SchemaBuilderToken) private readonly builder: Builder,
private readonly prisma: PrismaService,
) {
super()
}
@PothosRef()
finance() {
return this.builder.simpleObject('Finance', {
description: 'A finance in the system.',
fields: (t) => ({
id: t.string({
description: 'The ID of the finance.',
}),
amount: t.string({
description: 'The amount of the finance.',
}),
}),
})
}
@PothosRef()
init(): void {
this.builder.queryFields((t) => ({
finance: t.field({
type: this.finance(),
description: 'Retrieve a single finance by its unique identifier.',
resolve: async () => {
return {
id: '1',
amount: '100',
}
},
}),
}))
}
}

View File

@@ -42,7 +42,7 @@ import { initContextCache } from '@pothos/core'
import { RedisPubSub } from 'graphql-redis-subscriptions'
import { DocumentModule } from 'src/Document/document.module'
import { Context } from 'graphql-ws'
import { FinanceModule } from 'src/Finance/finance.module'
import { AnalyticModule } from 'src/Analytic/analytic.module'
import { MeetingRoomModule } from 'src/MeetingRoom/meetingroom.module'
@Global()
@@ -79,7 +79,7 @@ import { MeetingRoomModule } from 'src/MeetingRoom/meetingroom.module'
WorkshopMeetingRoomModule,
AdminNoteModule,
DocumentModule,
FinanceModule,
AnalyticModule,
MeetingRoomModule,
PothosModule.forRoot({
builder: {

View File

@@ -142,4 +142,19 @@ export class DateTimeUtils {
static getTimeFromDateTime(dateTime: DateTime): TimeType {
return this.toTime(`${dateTime.hour}:${dateTime.minute}:${dateTime.second}`)
}
static subtractDaysFromTimeframe(timeframe: string): DateTime {
// convert timeframe to number of days: 'day' -> 1, 'week' -> 7, 'month' -> 30, 'year' -> 365
const daysMap: Record<string, number> = {
day: 1,
week: 7,
month: 30,
year: 365,
}
const days = daysMap[timeframe.toLowerCase()]
if (days === undefined) {
throw new Error(`Invalid timeframe: ${timeframe}`)
}
return DateTime.now().minus({ days })
}
}