import { z } from "zod"; import { db } from "@/lib/db"; import { ACTIVATE_STATUSES, REQUEST_STATUSES } from "@/lib/constants"; import type { Prisma } from "@/lib/generated/prisma/client"; import { calculateUpgradePrice, getAccountTypeLabel, getAccountTypePrice, getTrialAccountTypeLabel, } from "@/lib/utils"; const pageSizeOptions = [10, 20, 50, 100] as const; const upgradeRequestSearchSchema = z.object({ page: z.coerce.number().int().min(1).catch(1), pageSize: z.coerce .number() .int() .refine((value) => pageSizeOptions.includes(value as PageSize)) .catch(10), q: z.string().trim().catch(""), status: z .union([z.literal("all"), z.coerce.number().int().min(0)]) .optional() .catch(REQUEST_STATUSES.REQUESTED), }); type PageSize = (typeof pageSizeOptions)[number]; export type RawSearchParams = | URLSearchParams | Record; export type UpgradeRequestSearchParams = z.infer< typeof upgradeRequestSearchSchema >; export type UpgradeRequestListItem = { maestroUpgradeID: number; maestroID: number; maestroName: string; registeredActivateStatus: number; registeredAccountType: number; requestedAccountType: number; requestedDateTime: string; availableActivateDateTime: string; status: number; additionalPrice: number; }; export type UpgradeRequestListResult = { items: UpgradeRequestListItem[]; page: number; pageSize: number; totalCount: number; totalPages: number; q: string; status?: number | "all"; }; export class UpgradeRequestActionError extends Error { constructor( message: string, readonly statusCode: number ) { super(message); this.name = "UpgradeRequestActionError"; } } export async function getUpgradeRequests( rawSearchParams: RawSearchParams = {} ): Promise { const params = parseUpgradeRequestSearchParams(rawSearchParams); const where = buildUpgradeRequestWhere(params); const skip = (params.page - 1) * params.pageSize; const [totalCount, requests] = await Promise.all([ db.maestro_upgrade.count({ where }), db.maestro_upgrade.findMany({ where, orderBy: { MaestroUpgradeID: "desc" }, skip, take: params.pageSize, select: { MaestroUpgradeID: true, MaestroID: true, RegisteredActivateStatus: true, RegisteredAccountType: true, RequestedAccountType: true, RequestedDateTime: true, Status: true, maestro: { select: { Name: true, AvailableActivateDateTime: true, }, }, }, }), ]); return { items: requests.map(toUpgradeRequestListItem), page: params.page, pageSize: params.pageSize, totalCount, totalPages: Math.max(1, Math.ceil(totalCount / params.pageSize)), q: params.q, status: params.status, }; } export async function approveUpgradeRequest( maestroUpgradeID: number ): Promise<{ availableActivateDateTime: string }> { return db.$transaction(async (tx) => { const request = await getActionTarget(tx, maestroUpgradeID); const availableActivateDateTime = calculateUpgradeAvailableDate(); await tx.maestro.update({ where: { MaestroID: request.MaestroID }, data: { AccountType: request.RequestedAccountType, ActivateStatus: ACTIVATE_STATUSES.ACTIVE, AvailableActivateDateTime: availableActivateDateTime, }, }); await tx.maestro_upgrade.updateMany({ where: { MaestroID: request.MaestroID, Status: REQUEST_STATUSES.REQUESTED, }, data: { Status: REQUEST_STATUSES.CLOSED }, }); await tx.maestro_upgrade.update({ where: { MaestroUpgradeID: request.MaestroUpgradeID }, data: { Status: REQUEST_STATUSES.APPLIED }, }); await tx.maestro_log.create({ data: { Type: "upgrade_maestro", MaestroID: request.MaestroID, LogDateTime: new Date(), Remark: buildUpgradeLogRemark( request.RegisteredActivateStatus, request.RegisteredAccountType, request.RequestedAccountType ), }, }); return { availableActivateDateTime: availableActivateDateTime.toISOString(), }; }); } export async function rejectUpgradeRequest( maestroUpgradeID: number ): Promise { await db.$transaction(async (tx) => { const request = await getActionTarget(tx, maestroUpgradeID); await tx.maestro_upgrade.update({ where: { MaestroUpgradeID: request.MaestroUpgradeID }, data: { Status: REQUEST_STATUSES.CLOSED }, }); await tx.maestro_log.create({ data: { Type: "reject_upgrade_maestro", MaestroID: request.MaestroID, LogDateTime: new Date(), Remark: buildUpgradeLogRemark( request.RegisteredActivateStatus, request.RegisteredAccountType, request.RequestedAccountType ), }, }); }); } export function parseUpgradeRequestSearchParams( rawSearchParams: RawSearchParams ): UpgradeRequestSearchParams { return upgradeRequestSearchSchema.parse( searchParamsToRecord(rawSearchParams) ); } async function getActionTarget( tx: Prisma.TransactionClient, maestroUpgradeID: number ) { const request = await tx.maestro_upgrade.findUnique({ where: { MaestroUpgradeID: maestroUpgradeID }, }); if (!request) { throw new UpgradeRequestActionError( "업그레이드 신청을 찾을 수 없습니다.", 404 ); } if (request.Status !== REQUEST_STATUSES.REQUESTED) { throw new UpgradeRequestActionError( "이미 처리된 업그레이드 신청입니다.", 409 ); } return request; } function buildUpgradeRequestWhere( params: UpgradeRequestSearchParams ): Prisma.maestro_upgradeWhereInput { const conditions: Prisma.maestro_upgradeWhereInput[] = []; if (params.q) { const keywordConditions: Prisma.maestro_upgradeWhereInput[] = [ { maestro: { Name: { contains: params.q } } }, ]; const numericQuery = Number(params.q); if (Number.isInteger(numericQuery) && numericQuery > 0) { keywordConditions.unshift( { MaestroUpgradeID: numericQuery }, { MaestroID: numericQuery } ); } conditions.push({ OR: keywordConditions }); } if (typeof params.status === "number" && Number.isInteger(params.status)) { conditions.push({ Status: params.status }); } return conditions.length > 0 ? { AND: conditions } : {}; } function searchParamsToRecord( rawSearchParams: RawSearchParams ): Record { if (rawSearchParams instanceof URLSearchParams) { return Object.fromEntries(rawSearchParams.entries()); } return Object.fromEntries( Object.entries(rawSearchParams).map(([key, value]) => [ key, Array.isArray(value) ? value[0] : value, ]) ); } function toUpgradeRequestListItem(request: { MaestroUpgradeID: number; MaestroID: number; RegisteredActivateStatus: number; RegisteredAccountType: number; RequestedAccountType: number; RequestedDateTime: Date; Status: number; maestro: { Name: string; AvailableActivateDateTime: Date; }; }): UpgradeRequestListItem { return { maestroUpgradeID: request.MaestroUpgradeID, maestroID: request.MaestroID, maestroName: request.maestro.Name.trim(), registeredActivateStatus: request.RegisteredActivateStatus, registeredAccountType: request.RegisteredAccountType, requestedAccountType: request.RequestedAccountType, requestedDateTime: request.RequestedDateTime.toISOString(), availableActivateDateTime: request.maestro.AvailableActivateDateTime.toISOString(), status: request.Status, additionalPrice: calculateAdditionalPrice( request.RegisteredActivateStatus, request.RegisteredAccountType, request.RequestedAccountType ), }; } function calculateAdditionalPrice( registeredActivateStatus: number, registeredAccountType: number, requestedAccountType: number ) { if (registeredActivateStatus === ACTIVATE_STATUSES.TRIAL) { return getAccountTypePrice(requestedAccountType); } return calculateUpgradePrice(requestedAccountType, registeredAccountType); } function calculateUpgradeAvailableDate(now = new Date()) { const availableDate = new Date(now); availableDate.setFullYear(availableDate.getFullYear() + 1); return availableDate; } function buildUpgradeLogRemark( registeredActivateStatus: number, registeredAccountType: number, requestedAccountType: number ) { const registeredLabel = registeredActivateStatus === ACTIVATE_STATUSES.TRIAL ? getTrialAccountTypeLabel(registeredAccountType) : getAccountTypeLabel(registeredAccountType); return `${registeredLabel} -> ${getAccountTypeLabel(requestedAccountType)}`.slice( 0, 100 ); }