create license management

This commit is contained in:
2026-04-06 13:31:40 +03:30
parent c02e7afca1
commit 1160d81cdb
42 changed files with 1233 additions and 522 deletions
@@ -1,4 +1,4 @@
import { Body, Controller, Delete, Get, Param, Patch, Post } from '@nestjs/common'
import { Body, Controller, Get, Param, Patch, Post } from '@nestjs/common'
import { AdminUsersService } from './consumers.service'
import { CreateConsumerDto, UpdateConsumerDto } from './dto/consumer.dto'
@@ -25,9 +25,4 @@ export class AdminUsersController {
async update(@Param('id') id: string, @Body() data: UpdateConsumerDto) {
return this.usersService.update(id, data)
}
@Delete(':id')
async delete(@Param('id') id: string) {
return this.usersService.delete(id)
}
}
@@ -4,9 +4,15 @@ import { AdminUserAccountsModule } from './accounts/accounts.module'
import { AdminConsumerBusinessActivitiesModule } from './business-activities/business-activities.module'
import { AdminUsersController } from './consumers.controller'
import { AdminUsersService } from './consumers.service'
import { AdminConsumerLicensesModule } from './licenses/licenses.module'
@Module({
imports: [PrismaModule, AdminUserAccountsModule, AdminConsumerBusinessActivitiesModule],
imports: [
PrismaModule,
AdminUserAccountsModule,
AdminConsumerBusinessActivitiesModule,
AdminConsumerLicensesModule,
],
controllers: [AdminUsersController],
providers: [AdminUsersService],
exports: [AdminUserAccountsModule],
+103 -37
View File
@@ -1,7 +1,14 @@
import { PasswordUtil } from '@/common/utils/password.util'
import { AccountStatus, AccountType, ConsumerRole } from '@/generated/prisma/enums'
import {
AccountStatus,
AccountType,
ConsumerRole,
LicenseStatus,
PartnerStatus,
} from '@/generated/prisma/enums'
import { ConsumerCreateInput, ConsumerSelect } from '@/generated/prisma/models'
import { PrismaService } from '@/prisma/prisma.service'
import { Injectable } from '@nestjs/common'
import { BadRequestException, Injectable } from '@nestjs/common'
import { ResponseMapper } from 'common/response/response-mapper'
import { CreateConsumerDto, UpdateConsumerDto } from './dto/consumer.dto'
@@ -9,17 +16,33 @@ import { CreateConsumerDto, UpdateConsumerDto } from './dto/consumer.dto'
export class AdminUsersService {
constructor(private readonly prisma: PrismaService) {}
private readonly defaultSelect: ConsumerSelect = {
id: true,
first_name: true,
last_name: true,
mobile_number: true,
status: true,
created_at: true,
license: {
select: {
id: true,
starts_at: true,
expires_at: true,
status: true,
partner: {
select: {
id: true,
name: true,
},
},
},
},
}
async findAll() {
const [consumers, count] = await this.prisma.$transaction([
this.prisma.consumer.findMany({
select: {
id: true,
first_name: true,
last_name: true,
mobile_number: true,
status: true,
created_at: true,
},
select: this.defaultSelect,
}),
this.prisma.consumer.count(),
])
@@ -33,16 +56,12 @@ export class AdminUsersService {
}
async findOne(id: string) {
this.prisma.license.findFirst({
where: {},
})
const consumer = await this.prisma.consumer.findUniqueOrThrow({
where: { id },
select: {
id: true,
first_name: true,
last_name: true,
mobile_number: true,
status: true,
created_at: true,
},
select: this.defaultSelect,
})
return ResponseMapper.single({
@@ -52,32 +71,79 @@ export class AdminUsersService {
}
async create(data: CreateConsumerDto) {
const { username, password, ...rest } = data
return this.prisma.consumer.create({
data: {
...rest,
accounts: {
create: {
role: ConsumerRole.OWNER,
account: {
create: {
username,
password: await PasswordUtil.hash(password),
type: AccountType.CONSUMER,
status: AccountStatus.ACTIVE,
},
const { username, password, license, ...rest } = data
const dataToCreate: ConsumerCreateInput = {
...rest,
accounts: {
create: {
role: ConsumerRole.OWNER,
account: {
create: {
username,
password: await PasswordUtil.hash(password),
type: AccountType.CONSUMER,
status: AccountStatus.ACTIVE,
},
},
},
},
}
if (license) {
if (license.partner_id) {
const partner = await this.prisma.partner.findUnique({
where: {
id: license.partner_id,
status: PartnerStatus.ACTIVE,
},
select: {
name: true,
license_quota: true,
_count: {
select: {
licenses: true,
},
},
},
})
if (!partner) {
throw new BadRequestException('متاسفانه شریک تجاری مورد نظر شما یافت نشد')
}
const partnerLicensesRemainsQuota =
partner.license_quota || 0 - partner._count.licenses
if (!partnerLicensesRemainsQuota) {
throw new BadRequestException(
`تعداد لایسنس‌های فعلی شریک تجاری ${partner.name} به پایان رسیده است`,
)
}
}
dataToCreate.license = {
create: {
starts_at: license.starts_at,
expires_at: new Date(
new Date(license.starts_at).getFullYear() + 1,
).toISOString(),
status: LicenseStatus.ACTIVE,
partner: {
connect: {
id: license.partner_id,
},
},
},
}
}
return this.prisma.consumer.create({
data: dataToCreate,
select: this.defaultSelect,
})
}
async update(id: string, data: UpdateConsumerDto) {
return this.prisma.admin.update({ where: { id }, data })
}
async delete(id: string) {
return this.prisma.admin.delete({ where: { id } })
return this.prisma.consumer.update({
where: { id },
data,
})
}
}
@@ -1,6 +1,7 @@
import { ConsumerStatus } from '@/generated/prisma/enums'
import { ApiProperty, PartialType } from '@nestjs/swagger'
import { IsEnum, IsString } from 'class-validator'
import { ApiProperty, OmitType, PartialType } from '@nestjs/swagger'
import { IsEnum, IsObject, IsString } from 'class-validator'
import { CreateLicenseDto } from '../licenses/dto/license.dto'
export class CreateConsumerDto {
@IsString()
@@ -22,8 +23,16 @@ export class CreateConsumerDto {
@IsString()
@ApiProperty({ required: true })
password: string
@IsObject()
@ApiProperty()
license: CreateLicenseDto
}
export class UpdateConsumerDto extends PartialType(CreateConsumerDto) {
export class UpdateConsumerDto extends OmitType(PartialType(CreateConsumerDto), [
'password',
'username',
'license',
]) {
@IsEnum(ConsumerStatus)
@ApiProperty({ enum: ConsumerStatus })
status: ConsumerStatus
@@ -0,0 +1,37 @@
import { LicenseStatus } from '@/generated/prisma/enums'
import { ApiProperty, OmitType, PartialType } from '@nestjs/swagger'
import { IsDateString, IsEnum } from 'class-validator'
export class CreateLicenseDto {
@ApiProperty({ required: true, default: new Date().toISOString() })
@IsDateString(
{ strict: true },
{ message: 'invoice_date must be a valid ISO-8601 string' },
)
starts_at: Date
@ApiProperty()
@IsDateString(
{ strict: true },
{ message: 'invoice_date must be a valid ISO-8601 string' },
)
expires_at?: Date
@ApiProperty()
partner_id: string
}
export class UpdateLicenseDto extends OmitType(PartialType(CreateLicenseDto), [
'starts_at',
]) {
@ApiProperty({ required: true, default: new Date().toISOString() })
@IsDateString(
{ strict: true },
{ message: 'invoice_date must be a valid ISO-8601 string' },
)
starts_at: Date
@ApiProperty({ enum: LicenseStatus })
@IsEnum(LicenseStatus)
status?: LicenseStatus
}
@@ -0,0 +1,25 @@
import { Body, Controller, Param, Patch, Post } from '@nestjs/common'
import { ApiTags } from '@nestjs/swagger'
import { CreateLicenseDto, UpdateLicenseDto } from './dto/license.dto'
import { LicensesService } from './licenses.service'
@ApiTags('AdminConsumerAccounts')
@Controller('admin/consumers/:consumerId/licenses')
export class LicensesController {
constructor(private readonly service: LicensesService) {}
@Post()
async create(@Param('consumerId') consumerId: string, @Body() data: CreateLicenseDto) {
return this.service.create(consumerId, data)
}
@Patch(':id')
async update(@Param('id') id: string, @Body() data: UpdateLicenseDto) {
return this.service.update(id, data)
}
// @Delete(':id')
// async delete(@Param('id') id: string) {
// return this.service.delete(id)
// }
}
@@ -0,0 +1,12 @@
import { PrismaModule } from '@/prisma/prisma.module'
import { Module } from '@nestjs/common'
import { LicensesController } from './licenses.controller'
import { LicensesService } from './licenses.service'
@Module({
imports: [PrismaModule],
controllers: [LicensesController],
providers: [LicensesService],
exports: [LicensesService],
})
export class AdminConsumerLicensesModule {}
@@ -0,0 +1,51 @@
import { LicenseStatus } from '@/generated/prisma/enums'
import { PrismaService } from '@/prisma/prisma.service'
import { Injectable } from '@nestjs/common'
import { ResponseMapper } from 'common/response/response-mapper'
import { CreateLicenseDto, UpdateLicenseDto } from './dto/license.dto'
@Injectable()
export class LicensesService {
constructor(private readonly prisma: PrismaService) {}
private readonly setExpireDate = (starts_at: Date) => {
return new Date(
new Date(starts_at).setFullYear(new Date(starts_at).getFullYear() + 1),
).toISOString()
}
async create(consumerId: string, data: CreateLicenseDto) {
const { starts_at, expires_at } = data
const license = await this.prisma.license.create({
data: {
starts_at,
expires_at: expires_at ?? this.setExpireDate(starts_at),
partner: {
connect: {
id: data.partner_id,
},
},
consumers: {
connect: {
id: consumerId,
},
},
status: LicenseStatus.ACTIVE,
},
})
return ResponseMapper.create(license)
}
async update(id: string, data: UpdateLicenseDto) {
const { starts_at, expires_at } = data
const license = await this.prisma.license.update({
where: { id },
data: {
...data,
starts_at,
expires_at: expires_at ?? this.setExpireDate(starts_at),
},
})
return ResponseMapper.update(license)
}
}
@@ -17,7 +17,7 @@ export class GoodsController {
}
@Post()
create(@Param('guildId') guildId: string, @Body() data: CreateGuildGoodDto) {
return this.goodsService.create(guildId, data)
create(@Body() data: CreateGuildGoodDto) {
return this.goodsService.create(data)
}
}
@@ -67,7 +67,7 @@ export class GoodsService {
return ResponseMapper.single(good)
}
async create(guild_id: string, data: CreateGuildGoodDto) {
async create(data: CreateGuildGoodDto) {
const { category_id, ...rest } = data
const good = await this.prisma.good.create({
@@ -1,5 +1,5 @@
import { Body, Controller, Get, Param, Patch, Post } from '@nestjs/common'
import { CreateLicenseDto, UpdateLicenseDto } from './dto/license.dto'
import { Body, Controller, Get, Param, Patch } from '@nestjs/common'
import { UpdateLicenseDto } from './dto/license.dto'
import { PartnerLicensesService } from './licenses.service'
@Controller('admin/licenses')
@@ -16,11 +16,6 @@ export class LicensesController {
return this.licensesService.findOne(id)
}
@Post()
async create(@Body() data: CreateLicenseDto) {
return this.licensesService.create(data)
}
@Patch(':id')
async update(@Param('id') id: string, @Body() data: UpdateLicenseDto) {
return this.licensesService.update(id, data)
+3 -24
View File
@@ -2,8 +2,7 @@ import { LicenseInclude } from '@/generated/prisma/models'
import { PrismaService } from '@/prisma/prisma.service'
import { Injectable } from '@nestjs/common'
import { ResponseMapper } from 'common/response/response-mapper'
import { LicenseStatus } from 'generated/prisma/enums'
import { CreateLicenseDto, UpdateLicenseDto } from './dto/license.dto'
import { UpdateLicenseDto } from './dto/license.dto'
@Injectable()
export class PartnerLicensesService {
@@ -43,7 +42,6 @@ export class PartnerLicensesService {
this.prisma.license.findMany({
include: this.licenseInclude,
omit: {
pos_id: true,
partner_id: true,
},
}),
@@ -51,7 +49,7 @@ export class PartnerLicensesService {
])
return ResponseMapper.paginate(
licenses.map(license => {
const { pos, ...rest } = license
const { ...rest } = license
// @ts-ignore
const { accounts, ...posRest } = pos
return {
@@ -71,12 +69,11 @@ export class PartnerLicensesService {
where: { id },
include: this.licenseInclude,
omit: {
pos_id: true,
partner_id: true,
},
})
if (license) {
const { pos, ...rest } = license
const { ...rest } = license
// @ts-ignore
const { accounts, ...posRest } = pos
return ResponseMapper.single({
@@ -89,24 +86,6 @@ export class PartnerLicensesService {
return ResponseMapper.single(license)
}
async create(data: CreateLicenseDto) {
const date = new Date(data.starts_at || '')
const expiresAt =
data.expires_at || new Date(date.setFullYear(date.getFullYear() + 1))
const license = await this.prisma.license.create({
data: {
partner_id: data.partner_id,
pos_id: data.pos_id,
starts_at: date,
expires_at: expiresAt,
status: LicenseStatus.ACTIVE,
},
})
return ResponseMapper.create(license)
}
async update(id: string, data: UpdateLicenseDto) {
const license = await this.prisma.license.update({
where: { id },
+11 -2
View File
@@ -1,6 +1,15 @@
import { Controller } from '@nestjs/common'
import { ConsumerInfoInfo } from '@/common/decorators/consumerInfo.decorator'
import { Controller, Get } from '@nestjs/common'
import { ApiTags } from '@nestjs/swagger'
import { ConsumerService } from './consumer.service'
@ApiTags('Consumer')
@Controller('consumer')
export class ConsumerController {}
export class ConsumerController {
constructor(private service: ConsumerService) {}
@Get('')
async findOne(@ConsumerInfoInfo('user_id') userId: string) {
return this.service.getInfo(userId)
}
}
-14
View File
@@ -1,14 +0,0 @@
// import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common'
// import { Observable } from 'rxjs'
// @Injectable()
// export class AdminGuard implements CanActivate {
// canActivate(
// _context: ExecutionContext,
// ): boolean | Promise<boolean> | Observable<boolean> {
// // Replace with real auth/permission checks (e.g., check JWT claims)
// const request = _context.switchToHttp().getRequest()
// const isAdminHeader = request.headers['x-admin']
// return isAdminHeader === 'true' || request.isAdminRequest === true
// }
// }
+39 -9
View File
@@ -1,20 +1,50 @@
import { checkAndDecodeJwtToken } from '@/common/utils/jwt-user.util'
import { AccountType } from '@/generated/prisma/enums'
import { Injectable, NestMiddleware, UnauthorizedException } from '@nestjs/common'
import { PrismaService } from '@/prisma/prisma.service'
import { ForbiddenException, Injectable, NestMiddleware } from '@nestjs/common'
import { JwtService } from '@nestjs/jwt'
import { NextFunction, Request, Response } from 'express'
@Injectable()
export class ConsumerMiddleware implements NestMiddleware {
constructor(private jwtService: JwtService) {}
use(req: Request, _res: Response, next: NextFunction) {
const decodedToken = checkAndDecodeJwtToken(req, this.jwtService)
if (decodedToken?.type === AccountType.CONSUMER) {
req.decodedToken = decodedToken
constructor(
private prisma: PrismaService,
private jwtService: JwtService,
) {}
async use(req: Request, _res: Response, next: NextFunction) {
const doForbidden = () => {
throw new ForbiddenException('شما دسترسی لازم را ندارید')
}
try {
const tokenAccount = checkAndDecodeJwtToken(req, this.jwtService)
if (tokenAccount?.type !== 'CONSUMER') {
return doForbidden()
}
const consumerAccount = await this.prisma.consumerAccount.findUnique({
where: {
id: tokenAccount.account_id,
},
select: {
user_id: true,
account_id: true,
},
})
if (!consumerAccount) {
return doForbidden()
}
req.consumerData = {
account_id: consumerAccount.account_id,
user_id: consumerAccount.user_id,
}
req.decodedToken = tokenAccount
return next()
} catch (error) {
return doForbidden()
}
throw new UnauthorizedException('شما دسترسی لازم را ندارید.')
}
}
+7 -1
View File
@@ -4,11 +4,12 @@ import { ConsumerAccountsModule } from './accounts/accounts.module'
import { ConsumerBusinessActivitiesModule } from './business-activities/business-activities.module'
import { ConsumerController } from './consumer.controller'
import { ConsumerMiddleware } from './consumer.middleware'
import { ConsumerService } from './consumer.service'
@Module({
controllers: [ConsumerController],
imports: [ConsumerAccountsModule, ConsumerBusinessActivitiesModule],
providers: [JwtService],
providers: [JwtService, ConsumerService],
})
export class ConsumerModule implements NestModule {
configure(consumer: MiddlewareConsumer) {
@@ -17,5 +18,10 @@ export class ConsumerModule implements NestModule {
method: RequestMethod.ALL,
version: '1',
})
consumer.apply(ConsumerMiddleware).forRoutes({
path: '/consumer',
method: RequestMethod.ALL,
version: '1',
})
}
}
+40
View File
@@ -0,0 +1,40 @@
import { ResponseMapper } from '@/common/response/response-mapper'
import { PrismaService } from '@/prisma/prisma.service'
import { Injectable } from '@nestjs/common'
@Injectable()
export class ConsumerService {
constructor(private readonly prisma: PrismaService) {}
async getInfo(consumer_id: string) {
const consumer = await this.prisma.consumer.findUnique({
where: {
id: consumer_id,
},
select: {
id: true,
first_name: true,
last_name: true,
mobile_number: true,
status: true,
license: {
select: {
starts_at: true,
expires_at: true,
status: true,
partner: {
select: {
name: true,
},
},
},
},
},
})
return ResponseMapper.single({
...consumer,
full_name: `${consumer?.first_name} ${consumer?.last_name}`,
})
}
}