import { RedisKeyMaker } from '@/common/utils/redisKeyMaker' import { generateTrackingCode, isTrackingCodeUniqueViolation, } from '@/common/utils/tracking-code-generator.util' import { LicenseChargeTransactionSelect, LicenseChargeTransactionWhereInput, } from '@/generated/prisma/models' import { PartnersCacheInvalidationService } from '@/modules/partners/cache/partners-cache-invalidation.service' import { PrismaService } from '@/prisma/prisma.service' import { RedisService } from '@/redis/redis.service' import { BadRequestException, Injectable } from '@nestjs/common' import { ResponseMapper } from 'common/response/response-mapper' import { ChargeLicenseDto } from './dto/chargedLicenseTransactions.dto' @Injectable() export class PartnerLicenseChargeTransactionService { constructor( private readonly prisma: PrismaService, private readonly redisService: RedisService, private readonly cacheInvalidationService: PartnersCacheInvalidationService, ) {} private readonly TRACKING_CODE_LENGTH = 8 private readonly TRACKING_CODE_MAX_ATTEMPTS = 5 private readonly LICENSE_BATCH_SIZE = 200 private readonly MAX_QUANTITY_PER_REQUEST = 1000 private readonly mappedTransaction = (transaction: any) => { const { licenses, purchased_count, _count, ...rest } = transaction const activation_count = _count.licenses return { ...rest, charged_license_count: purchased_count, activation_count, remained_license_count: purchased_count - activation_count, } } private readonly defaultSelect: LicenseChargeTransactionSelect = { id: true, created_at: true, activation_expires_at: true, tracking_code: true, purchased_count: true, _count: { select: { licenses: { where: { activation: { isNot: null, }, }, }, }, }, } async findAll(partner_id: string, page = 1, perPage = 10) { const cacheKey = RedisKeyMaker.partnerLicenseChargeTransactionsList( partner_id, page, perPage, ) const defaultWhere: LicenseChargeTransactionWhereInput = { partner_id, } return await this.redisService.getAndSet(cacheKey, 'paginate', async () => { const [transactions, total] = await this.prisma.$transaction(async tx => [ await tx.licenseChargeTransaction.findMany({ where: defaultWhere, skip: (page - 1) * perPage, take: perPage, select: this.defaultSelect, }), await tx.licenseChargeTransaction.count({ where: defaultWhere, }), ]) const mappedTransactions = transactions.map(this.mappedTransaction) return { items: mappedTransactions, page, perPage, total, } }) } async findOne(partner_id: string, id: string) { const cacheKey = RedisKeyMaker.partnerLicenseChargeTransactionDetail(partner_id, id) return await this.redisService.getAndSet(cacheKey, 'single', async () => { const transaction = await this.prisma.licenseChargeTransaction.findUniqueOrThrow({ where: { id, partner_id, }, select: this.defaultSelect, }) return this.mappedTransaction(transaction) }) } async create(partner_id: string, data: ChargeLicenseDto) { if (data.quantity > this.MAX_QUANTITY_PER_REQUEST) { throw new BadRequestException( `تعداد درخواستی بیش از حد مجاز است. حداکثر ${this.MAX_QUANTITY_PER_REQUEST} عدد مجاز است.`, ) } try { const transaction = await this.prisma.$transaction(async tx => { let createdTransaction: { id: string; purchased_count: number } | null = null for (let attempt = 0; attempt < this.TRACKING_CODE_MAX_ATTEMPTS; attempt++) { try { createdTransaction = await tx.licenseChargeTransaction.create({ data: { activation_expires_at: data.activated_expires_at, tracking_code: generateTrackingCode('LIC', this.TRACKING_CODE_LENGTH), purchased_count: data.quantity, partner: { connect: { id: partner_id } }, }, select: { id: true, purchased_count: true, }, }) break } catch (error) { if ( isTrackingCodeUniqueViolation(error) && attempt < this.TRACKING_CODE_MAX_ATTEMPTS - 1 ) { continue } throw error } } if (!createdTransaction) { throw new BadRequestException('متاسفانه مشکلی پیش آمده است.') } return createdTransaction }) this.scheduleLicenseProvisioning(transaction.id, transaction.purchased_count) await this.cacheInvalidationService.invalidatePartnersList() await this.cacheInvalidationService.invalidatePartnerLicenses(partner_id) return ResponseMapper.create({ transaction_id: transaction.id, charged_license_count: transaction.purchased_count, status: 'QUEUED', }) } catch (error) { throw error } } private scheduleLicenseProvisioning(transactionId: string, quantity: number) { setTimeout(() => { this.provisionLicensesInBackground(transactionId, quantity).catch(() => null) }, 0) } private async provisionLicensesInBackground(transactionId: string, quantity: number) { let createdCount = 0 while (createdCount < quantity) { const batchSize = Math.min(this.LICENSE_BATCH_SIZE, quantity - createdCount) await this.prisma.license.createMany({ data: Array.from({ length: batchSize }, () => ({ charge_transaction_id: transactionId, })), }) createdCount += batchSize } } }