Files
chocoadmin/lib/extension-requests.ts
T

322 lines
8.6 KiB
TypeScript

import { z } from "zod";
import { db } from "@/lib/db";
import { logger } from "@/lib/logger";
import { ApiError } from "@/lib/errors";
import { REQUEST_STATUSES } from "@/lib/constants";
import type { Prisma } from "@/lib/generated/prisma/client";
import {
calculateExtendedAvailableDate,
getAccountTypeLabel,
} from "@/lib/utils";
const pageSizeOptions = [10, 20, 50, 100] as const;
const extensionRequestSearchSchema = 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 ExtensionRequestSearchParams = z.infer<
typeof extensionRequestSearchSchema
>;
export type ExtensionRequestListItem = {
maestroExtensionID: number;
maestroID: number;
maestroName: string;
requestedAccountType: number;
currentAccountType: number;
activateStatus: number;
requestedDateTime: string;
availableActivateDateTime: string;
status: number;
};
export type ExtensionRequestListResult = {
items: ExtensionRequestListItem[];
page: number;
pageSize: number;
totalCount: number;
totalPages: number;
q: string;
status?: number | "all";
};
export async function getExtensionRequests(
rawSearchParams: RawSearchParams = {}
): Promise<ExtensionRequestListResult> {
const t0 = Date.now();
const params = parseExtensionRequestSearchParams(rawSearchParams);
const where = buildExtensionRequestWhere(params);
const skip = (params.page - 1) * params.pageSize;
const [totalCount, requests] = await Promise.all([
db.maestro_extension.count({ where }),
db.maestro_extension.findMany({
where,
orderBy: { MaestroExtensionID: "desc" },
skip,
take: params.pageSize,
select: {
MaestroExtensionID: true,
MaestroID: true,
AccountType: true,
RequestedDateTime: true,
Status: true,
maestro: {
select: {
Name: true,
AccountType: true,
ActivateStatus: true,
AvailableActivateDateTime: true,
},
},
},
}),
]);
logger.info("extension-requests", "list fetched", {
totalCount,
page: params.page,
duration: Date.now() - t0,
});
return {
items: requests.map(toExtensionRequestListItem),
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 approveExtensionRequest(
maestroExtensionID: number
): Promise<{
availableActivateDateTime: string;
maestroName: string;
maestroEmail: string;
}> {
return db.$transaction(async (tx) => {
const request = await getActionTarget(tx, maestroExtensionID);
const availableActivateDateTime = calculateExtendedAvailableDate(
request.maestro.AvailableActivateDateTime
);
await tx.maestro.update({
where: { MaestroID: request.MaestroID },
data: {
ActivateStatus: 2,
AvailableActivateDateTime: availableActivateDateTime,
},
});
await tx.maestro_extension.updateMany({
where: {
MaestroID: request.MaestroID,
Status: REQUEST_STATUSES.REQUESTED,
},
data: { Status: REQUEST_STATUSES.CLOSED },
});
await tx.maestro_extension.update({
where: { MaestroExtensionID: request.MaestroExtensionID },
data: { Status: REQUEST_STATUSES.APPLIED },
});
await tx.maestro_log.create({
data: {
Type: "extension_maestro",
MaestroID: request.MaestroID,
LogDateTime: new Date(),
Remark: buildExtensionLogRemark(
request.maestro.Name,
request.AccountType
),
},
});
return {
availableActivateDateTime: availableActivateDateTime.toISOString(),
maestroName: request.maestro.Name.trim(),
maestroEmail: request.maestro.Email.trim(),
};
});
}
export async function createExtensionRequest(
maestroID: number,
accountType: number
): Promise<{ maestroExtensionID: number; requestedDateTime: string; status: number }> {
const maestro = await db.maestro.findUnique({
where: { MaestroID: maestroID },
select: { MaestroID: true },
});
if (!maestro) {
throw new ApiError("마에스트로를 찾을 수 없습니다.", 404);
}
const request = await db.maestro_extension.create({
data: {
MaestroID: maestroID,
AccountType: accountType,
RequestedDateTime: new Date(),
Status: REQUEST_STATUSES.REQUESTED,
},
select: {
MaestroExtensionID: true,
RequestedDateTime: true,
Status: true,
},
});
return {
maestroExtensionID: request.MaestroExtensionID,
requestedDateTime: request.RequestedDateTime.toISOString(),
status: request.Status,
};
}
export async function cancelExtensionRequest(
maestroExtensionID: number
): Promise<void> {
await db.$transaction(async (tx) => {
const request = await getActionTarget(tx, maestroExtensionID);
await tx.maestro_extension.update({
where: { MaestroExtensionID: request.MaestroExtensionID },
data: { Status: REQUEST_STATUSES.CLOSED },
});
await tx.maestro_log.create({
data: {
Type: "cancel_extension_maestro",
MaestroID: request.MaestroID,
LogDateTime: new Date(),
Remark: buildExtensionLogRemark(
request.maestro.Name,
request.AccountType
),
},
});
});
}
export function parseExtensionRequestSearchParams(
rawSearchParams: RawSearchParams
): ExtensionRequestSearchParams {
return extensionRequestSearchSchema.parse(
searchParamsToRecord(rawSearchParams)
);
}
async function getActionTarget(
tx: Prisma.TransactionClient,
maestroExtensionID: number
) {
const request = await tx.maestro_extension.findUnique({
where: { MaestroExtensionID: maestroExtensionID },
include: { maestro: true },
});
if (!request) {
throw new ApiError("연장 신청을 찾을 수 없습니다.", 404);
}
if (request.Status !== REQUEST_STATUSES.REQUESTED) {
throw new ApiError("이미 처리된 연장 신청입니다.", 409);
}
return request;
}
function buildExtensionRequestWhere(
params: ExtensionRequestSearchParams
): Prisma.maestro_extensionWhereInput {
const conditions: Prisma.maestro_extensionWhereInput[] = [];
if (params.q) {
const keywordConditions: Prisma.maestro_extensionWhereInput[] = [
{ maestro: { Name: { contains: params.q } } },
];
const numericQuery = Number(params.q);
if (Number.isInteger(numericQuery) && numericQuery > 0) {
keywordConditions.unshift(
{ MaestroExtensionID: 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 toExtensionRequestListItem(request: {
MaestroExtensionID: number;
MaestroID: number;
AccountType: number;
RequestedDateTime: Date;
Status: number;
maestro: {
Name: string;
AccountType: number;
ActivateStatus: number;
AvailableActivateDateTime: Date;
};
}): ExtensionRequestListItem {
return {
maestroExtensionID: request.MaestroExtensionID,
maestroID: request.MaestroID,
maestroName: request.maestro.Name.trim(),
requestedAccountType: request.AccountType,
currentAccountType: request.maestro.AccountType,
activateStatus: request.maestro.ActivateStatus,
requestedDateTime: request.RequestedDateTime.toISOString(),
availableActivateDateTime:
request.maestro.AvailableActivateDateTime.toISOString(),
status: request.Status,
};
}
function buildExtensionLogRemark(maestroName: string, accountType: number) {
return `${maestroName.trim()}(${getAccountTypeLabel(accountType)})`.slice(
0,
100
);
}