feat: implement partner licenses module with pagination and filtering capabilities

This commit is contained in:
2026-04-26 09:54:48 +03:30
parent b72e6c7194
commit 34793295c9
24 changed files with 459 additions and 76 deletions
@@ -37,12 +37,12 @@ export class PartnerConsumersService {
partner_id,
})
async findAll(partner_id: string, page = 1, pageSize = 10) {
const [consumers, count] = await this.prisma.$transaction(async tx => [
async findAll(partner_id: string, page = 1, perPage = 10) {
const [consumers, total] = await this.prisma.$transaction(async tx => [
await tx.consumer.findMany({
where: this.defaultWhere(partner_id),
select: this.defaultSelect,
skip: (page - 1) * pageSize,
skip: (page - 1) * perPage,
take: 10,
}),
await tx.consumer.count({
@@ -50,9 +50,9 @@ export class PartnerConsumersService {
}),
])
return ResponseMapper.paginate(consumers.map(mapConsumerWithLicenseUtil), {
count,
total,
page,
pageSize,
perPage,
})
}
@@ -0,0 +1,140 @@
import { ApiProperty } from '@nestjs/swagger'
import { Paginated } from 'common/response/response-mapper'
export interface PartnerLicenseActivationConsumerResponseDto {
id: string
first_name: string
last_name: string
mobile_number: string
}
export interface PartnerLicenseActivationBusinessActivityResponseDto {
id: string
name: string
consumer: PartnerLicenseActivationConsumerResponseDto
}
export interface PartnerLicenseActivationChargeTransactionResponseDto {
id: string
tracking_code: string
activation_expires_at: Date
}
export interface PartnerLicenseActivationLicenseResponseDto {
id: string
accounts_limit: number
charge_transaction: PartnerLicenseActivationChargeTransactionResponseDto
}
export interface PartnerLicenseActivationCountResponseDto {
account_allocations: number
}
export interface PartnerLicenseActivationResponseDto {
id: string
starts_at: Date
expires_at: Date
created_at: Date
updated_at: Date
business_activity: PartnerLicenseActivationBusinessActivityResponseDto
license: PartnerLicenseActivationLicenseResponseDto
_count: PartnerLicenseActivationCountResponseDto
}
export type PartnerLicenseActivationPaginatedResponseDto =
Paginated<PartnerLicenseActivationResponseDto>
export class PartnerLicenseActivationConsumerResponseSchema {
@ApiProperty()
id: string
@ApiProperty()
first_name: string
@ApiProperty()
last_name: string
@ApiProperty()
mobile_number: string
}
export class PartnerLicenseActivationBusinessActivityResponseSchema {
@ApiProperty()
id: string
@ApiProperty()
name: string
@ApiProperty({ type: () => PartnerLicenseActivationConsumerResponseSchema })
consumer: PartnerLicenseActivationConsumerResponseSchema
}
export class PartnerLicenseActivationChargeTransactionResponseSchema {
@ApiProperty()
id: string
@ApiProperty()
tracking_code: string
@ApiProperty()
activation_expires_at: Date
}
export class PartnerLicenseActivationLicenseResponseSchema {
@ApiProperty()
id: string
@ApiProperty()
accounts_limit: number
@ApiProperty({ type: () => PartnerLicenseActivationChargeTransactionResponseSchema })
charge_transaction: PartnerLicenseActivationChargeTransactionResponseSchema
}
export class PartnerLicenseActivationCountResponseSchema {
@ApiProperty()
account_allocations: number
}
export class PartnerLicenseActivationResponseSchema {
@ApiProperty()
id: string
@ApiProperty()
starts_at: Date
@ApiProperty()
expires_at: Date
@ApiProperty()
created_at: Date
@ApiProperty()
updated_at: Date
@ApiProperty({ type: () => PartnerLicenseActivationBusinessActivityResponseSchema })
business_activity: PartnerLicenseActivationBusinessActivityResponseSchema
@ApiProperty({ type: () => PartnerLicenseActivationLicenseResponseSchema })
license: PartnerLicenseActivationLicenseResponseSchema
@ApiProperty({ type: () => PartnerLicenseActivationCountResponseSchema })
_count: PartnerLicenseActivationCountResponseSchema
}
export class PartnerLicenseActivationPaginatedResponseSchema {
@ApiProperty({ example: 'paginate' })
__mapped: string
@ApiProperty({ type: () => [PartnerLicenseActivationResponseSchema] })
items: PartnerLicenseActivationResponseSchema[]
@ApiProperty()
total: number
@ApiProperty()
page: number
@ApiProperty()
perPage: number
}
@@ -0,0 +1,49 @@
import { PartnerInfo } from '@/common/decorators/partnerInfo.decorator'
import { Controller, Get, Query } from '@nestjs/common'
import { ApiOkResponse, ApiQuery, ApiTags } from '@nestjs/swagger'
import { PartnerLicenseActivationPaginatedResponseSchema } from './dto/licenses-response.dto'
import { PartnerLicensesService } from './licenses.service'
@ApiTags('PartnerLicenses')
@Controller('partner/licenses')
export class PartnerLicensesController {
constructor(private readonly service: PartnerLicensesService) {}
@Get()
@ApiQuery({ name: 'page', required: false, type: Number, example: 1 })
@ApiQuery({
name: 'perPage',
required: false,
type: Number,
example: 10,
description: 'Max value is 50.',
})
@ApiQuery({ name: 'consumer_name', required: false, type: String })
@ApiQuery({ name: 'consumer_mobile', required: false, type: String })
@ApiQuery({
name: 'expires_from',
required: false,
type: String,
example: '2026-01-01',
})
@ApiQuery({ name: 'expires_to', required: false, type: String, example: '2026-12-31' })
@ApiOkResponse({ type: PartnerLicenseActivationPaginatedResponseSchema })
async findAll(
@PartnerInfo('id') partnerId: string,
@Query('page') page = '1',
@Query('perPage') perPage = '10',
@Query('consumer_name') consumer_name?: string,
@Query('consumer_mobile') consumer_mobile?: string,
@Query('expires_from') expires_from?: string,
@Query('expires_to') expires_to?: string,
) {
return this.service.findAll(partnerId, {
page: Number(page) || 1,
perPage: Number(perPage) || 10,
consumer_name,
consumer_mobile,
expires_from,
expires_to,
})
}
}
@@ -0,0 +1,11 @@
import { PrismaModule } from '@/prisma/prisma.module'
import { Module } from '@nestjs/common'
import { PartnerLicensesController } from './licenses.controller'
import { PartnerLicensesService } from './licenses.service'
@Module({
imports: [PrismaModule],
controllers: [PartnerLicensesController],
providers: [PartnerLicensesService],
})
export class PartnerLicensesModule {}
@@ -0,0 +1,167 @@
import { PrismaService } from '@/prisma/prisma.service'
import { BadRequestException, Injectable } from '@nestjs/common'
import { ResponseMapper } from 'common/response/response-mapper'
import {
PartnerLicenseActivationPaginatedResponseDto,
PartnerLicenseActivationResponseDto,
} from './dto/licenses-response.dto'
type FindPartnerLicensesParams = {
perPage?: number
page?: number
consumer_name?: string
consumer_mobile?: string
expires_from?: string
expires_to?: string
}
@Injectable()
export class PartnerLicensesService {
private readonly defaultPerPage = 10
private readonly maxPerPage = 50
constructor(private readonly prisma: PrismaService) {}
async findAll(
partner_id: string,
params: FindPartnerLicensesParams = {},
): Promise<PartnerLicenseActivationPaginatedResponseDto> {
const { perPage, page, consumer_name, consumer_mobile, expires_from, expires_to } =
params
const normalizedPage = Number.isFinite(page)
? Math.max(1, Math.floor(page as number))
: 1
const requestedPerPage = Number.isFinite(perPage)
? Math.max(1, Math.floor(perPage as number))
: this.defaultPerPage
const normalizedPerPage = Math.min(requestedPerPage, this.maxPerPage)
const andWhere: any[] = [
{
license: {
charge_transaction: {
partner_id,
},
},
},
]
const consumerName = consumer_name?.trim()
const consumerMobile = consumer_mobile?.trim()
if (consumerName || consumerMobile) {
andWhere.push({
business_activity: {
consumer: {
OR: [
{ first_name: { contains: consumerName } },
{ last_name: { contains: consumerName } },
],
},
},
})
}
if (consumerMobile) {
andWhere.push({
business_activity: {
consumer: {
mobile_number: {
contains: consumerMobile,
},
},
},
})
}
if (expires_from || expires_to) {
const expiresAtFilter: {
gte?: Date
lte?: Date
} = {}
if (expires_from) {
const fromDate = new Date(expires_from)
if (Number.isNaN(fromDate.getTime())) {
throw new BadRequestException('مقدار expires_from نامعتبر است.')
}
expiresAtFilter.gte = fromDate
}
if (expires_to) {
const toDate = new Date(expires_to)
if (Number.isNaN(toDate.getTime())) {
throw new BadRequestException('مقدار expires_to نامعتبر است.')
}
expiresAtFilter.lte = toDate
}
andWhere.push({
expires_at: expiresAtFilter,
})
}
const where = { AND: andWhere }
const [licenseActivations, total] = await this.prisma.$transaction(async tx => [
await tx.licenseActivation.findMany({
where,
orderBy: {
created_at: 'desc',
},
skip: (normalizedPage - 1) * normalizedPerPage,
take: normalizedPerPage,
select: {
id: true,
starts_at: true,
expires_at: true,
created_at: true,
updated_at: true,
business_activity: {
select: {
id: true,
name: true,
consumer: {
select: {
id: true,
first_name: true,
last_name: true,
mobile_number: true,
},
},
},
},
license: {
select: {
id: true,
accounts_limit: true,
charge_transaction: {
select: {
id: true,
tracking_code: true,
activation_expires_at: true,
},
},
},
},
_count: {
select: {
account_allocations: true,
},
},
},
}),
await tx.licenseActivation.count({ where }),
])
return ResponseMapper.paginate<PartnerLicenseActivationResponseDto>(
licenseActivations,
{
total,
page: normalizedPage,
perPage: normalizedPerPage,
},
)
}
}
+2 -1
View File
@@ -2,13 +2,14 @@ import { MiddlewareConsumer, Module, NestModule, RequestMethod } from '@nestjs/c
import { JwtService } from '@nestjs/jwt'
import { PartnerAccountsModule } from './accounts/accounts.module'
import { PartnerCustomersModule } from './consumers/consumers.module'
import { PartnerLicensesModule } from './licenses/licenses.module'
import { PartnerController } from './partners.controller'
import { PartnerMiddleware } from './partners.middleware'
import { PartnerService } from './partners.service'
@Module({
controllers: [PartnerController],
imports: [PartnerAccountsModule, PartnerCustomersModule],
imports: [PartnerAccountsModule, PartnerCustomersModule, PartnerLicensesModule],
providers: [JwtService, PartnerService],
})
export class PartnerModule implements NestModule {