Phase 6. 업그레이드 신청 관리 구현
This commit is contained in:
@@ -0,0 +1,325 @@
|
||||
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<string, string | string[] | undefined>;
|
||||
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<UpgradeRequestListResult> {
|
||||
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<void> {
|
||||
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<string, string | undefined> {
|
||||
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
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user