import { generateTrackingCode, isTrackingCodeUniqueViolation, } from '@/common/utils/tracking-code-generator.util' import { PartnerAccountQuotaChargeTransactionSelect, PartnerAccountQuotaChargeTransactionWhereInput, } from '@/generated/prisma/models' import { PrismaService } from '@/prisma/prisma.service' import { BadRequestException, Injectable } from '@nestjs/common' import { ResponseMapper } from 'common/response/response-mapper' import { ChargeAccountQuotaDto } from './dto/chargeAccountQuotaTransactions.dto' @Injectable() export class PartnerAccountChargeTransactionService { constructor(private readonly prisma: PrismaService) {} private readonly TRACKING_CODE_LENGTH = 8 private readonly TRACKING_CODE_MAX_ATTEMPTS = 5 private readonly QUOTA_BATCH_SIZE = 200 private readonly MAX_QUANTITY_PER_REQUEST = 1000 private readonly mappedTransaction = (transaction: any) => { const { allocations, purchased_count, _count, ...rest } = transaction const activation_count = _count.allocations return { ...rest, charged_license_count: purchased_count, activation_count, remained_license_count: purchased_count - activation_count, } } private readonly defaultSelect: PartnerAccountQuotaChargeTransactionSelect = { id: true, created_at: true, tracking_code: true, activation_expires_at: true, purchased_count: true, _count: { select: { credits: { where: { allocation: { isNot: null, }, }, }, }, }, } async findAll(partner_id: string, page = 1, perPage = 10) { const defaultWhere: PartnerAccountQuotaChargeTransactionWhereInput = { partner_id, } const [transactions, total] = await this.prisma.$transaction(async tx => [ await tx.partnerAccountQuotaChargeTransaction.findMany({ where: defaultWhere, skip: (page - 1) * perPage, take: perPage, select: this.defaultSelect, }), await tx.partnerAccountQuotaChargeTransaction.count({ where: defaultWhere, }), ]) const mappedTransactions = transactions.map(this.mappedTransaction) return ResponseMapper.paginate(mappedTransactions, { page, perPage, total, }) } async findOne(partner_id: string, id: string) { const transaction = this.prisma.partnerAccountQuotaChargeTransaction.findUniqueOrThrow({ where: { id, partner_id, }, select: this.defaultSelect, }) return ResponseMapper.single(this.mappedTransaction(transaction)) } async create(partner_id: string, data: ChargeAccountQuotaDto) { 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 } | null = null for (let attempt = 0; attempt < this.TRACKING_CODE_MAX_ATTEMPTS; attempt++) { try { createdTransaction = await tx.partnerAccountQuotaChargeTransaction.create({ data: { activation_expires_at: data.activated_expires_at, tracking_code: generateTrackingCode('AQC', this.TRACKING_CODE_LENGTH), purchased_count: data.quantity, partner: { connect: { id: partner_id } }, }, select: { id: 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.scheduleQuotaCreditProvisioning(transaction.id, data.quantity) return ResponseMapper.create({ transaction_id: transaction.id, charged_license_count: data.quantity, status: 'QUEUED', }) } catch (error) { throw error } } private scheduleQuotaCreditProvisioning(transactionId: string, quantity: number) { setTimeout(() => { this.provisionQuotaCreditsInBackground(transactionId, quantity).catch(() => null) }, 0) } private async provisionQuotaCreditsInBackground( transactionId: string, quantity: number, ) { let createdCount = 0 while (createdCount < quantity) { const batchSize = Math.min(this.QUOTA_BATCH_SIZE, quantity - createdCount) await this.prisma.partnerAccountQuotaCredit.createMany({ data: Array.from({ length: batchSize }, () => ({ charge_transaction_id: transactionId, })), }) createdCount += batchSize } } }