"use server" import db from "@/db/db" import { and, eq, inArray } from "drizzle-orm" import { complianceResponses, redFlagManagers } from "@/db/schema/compliance" import { basicContract, basicContractTemplates } from "@/db/schema/basicContractDocumnet" import { vendors } from "@/db/schema/vendors" import { users } from "@/db/schema" import { getTriggeredRedFlagQuestions, type TriggeredRedFlagInfo } from "./red-flag-notifier" import { getServerSession } from "next-auth" import { authOptions } from "@/app/api/auth/[...nextauth]/route" import { ApprovalSubmissionSaga } from "@/lib/approval" import { htmlListConverter, htmlTableConverter } from "@/lib/approval/template-utils" import type { ApprovalResult } from "@/lib/approval/types" import { revalidatePath } from "next/cache" type ContractSummary = { contractId: number vendorName: string | null vendorCode: string | null templateName: string | null createdAt: Date | null triggeredFlags: TriggeredRedFlagInfo[] } /** * RED FLAG 해소요청 - Approval Saga를 통해 상신 */ export async function requestRedFlagResolution(contractIds: number[]): Promise { if (!contractIds || contractIds.length === 0) { throw new Error("RED FLAG 해소요청을 위한 계약서를 선택해주세요.") } const uniqueContractIds = Array.from(new Set(contractIds)) const session = await getServerSession(authOptions) if (!session?.user) { throw new Error("인증이 필요합니다.") } const currentUser = session.user if (!currentUser.epId) { throw new Error("Knox EP ID가 필요합니다.") } const currentUserId = Number(currentUser.id) if (Number.isNaN(currentUserId)) { throw new Error("유효한 사용자 정보가 필요합니다.") } const purchasingManagerEpId = await getPurchasingManagerEpId() if (!purchasingManagerEpId) { throw new Error("구매기획 담당자의 EP ID가 설정되지 않았습니다.") } const contractSummaries = await fetchContractsWithFlags(uniqueContractIds) if (contractSummaries.length === 0) { throw new Error("선택한 계약서에 RED FLAG가 존재하지 않습니다.") } const validContractIds = contractSummaries.map((contract) => contract.contractId) // 중복 해소요청 방지 (진행 중인 결재가 있는지 확인) const responses = await db .select({ basicContractId: complianceResponses.basicContractId, redFlagResolvedAt: complianceResponses.redFlagResolvedAt, redFlagResolutionApprovalId: complianceResponses.redFlagResolutionApprovalId, }) .from(complianceResponses) .where(inArray(complianceResponses.basicContractId, validContractIds)) const missingResponses = validContractIds.filter( (contractId) => !responses.some((response) => response.basicContractId === contractId) ) if (missingResponses.length > 0) { throw new Error("준법 응답 정보를 찾을 수 없는 계약서가 포함되어 있습니다.") } const blockedContracts = responses .filter((response) => response.redFlagResolutionApprovalId && !response.redFlagResolvedAt) .map((response) => response.basicContractId) if (blockedContracts.length > 0) { const blockedSummaries = contractSummaries .filter((contract) => blockedContracts.includes(contract.contractId)) .map((contract) => contract.vendorName ?? `계약 ${contract.contractId}`) const preview = blockedSummaries.length > 2 ? `${blockedSummaries.slice(0, 2).join(", ")} 외 ${blockedSummaries.length - 2}건` : blockedSummaries.join(", ") throw new Error(`이미 해소요청이 진행 중인 계약서가 있습니다: ${preview}`) } const now = new Date() const variables = await buildTemplateVariables(contractSummaries, { requesterName: currentUser.name || currentUser.email || "요청자", requestedAt: now, }) const title = buildApprovalTitle(contractSummaries) const saga = new ApprovalSubmissionSaga( "compliance_red_flag_resolution", { contractIds: validContractIds, requestedBy: currentUserId, requestedAt: now.toISOString(), }, { title, description: "컴플라이언스 Red Flag 해소요청", templateName: "컴플라이언스 Red Flag 해소요청", variables, approvers: [purchasingManagerEpId], currentUser: { id: currentUserId, epId: currentUser.epId, email: currentUser.email ?? undefined, }, } ) const result = await saga.execute() if (result.status === "pending_approval") { await db .update(complianceResponses) .set({ redFlagResolutionApprovalId: result.approvalId, redFlagResolvedAt: null, updatedAt: new Date(), }) .where(inArray(complianceResponses.basicContractId, validContractIds)) await revalidatePath("/evcp/basic-contract") await revalidatePath("/evcp/compliance") } return result } /** * RED FLAG 해소 처리 (승인 후 실행) */ export async function resolveRedFlag( contractId: number, options: { approvalId?: string; revalidate?: boolean } = {} ): Promise<{ success: boolean; message: string }> { const conditions = [eq(complianceResponses.basicContractId, contractId)] if (options.approvalId) { conditions.push(eq(complianceResponses.redFlagResolutionApprovalId, options.approvalId)) } const [updated] = await db .update(complianceResponses) .set({ redFlagResolvedAt: new Date(), updatedAt: new Date(), }) .where(and(...conditions)) .returning({ id: complianceResponses.id }) if (!updated) { throw new Error("해소요청 정보를 찾을 수 없습니다.") } if (options.revalidate !== false) { await revalidatePath("/evcp/basic-contract") await revalidatePath("/evcp/compliance") } return { success: true, message: "RED FLAG가 해제되었습니다.", } } async function getPurchasingManagerEpId(): Promise { const [manager] = await db .select({ purchasingManagerId: redFlagManagers.purchasingManagerId, }) .from(redFlagManagers) .orderBy(redFlagManagers.createdAt) .limit(1) if (!manager?.purchasingManagerId) { return null } const [user] = await db .select({ epId: users.epId, }) .from(users) .where(eq(users.id, manager.purchasingManagerId)) .limit(1) return user?.epId ?? null } async function fetchContractsWithFlags(contractIds: number[]): Promise { const contracts = await db .select({ contractId: basicContract.id, vendorName: vendors.vendorName, vendorCode: vendors.vendorCode, templateName: basicContractTemplates.templateName, createdAt: basicContract.createdAt, }) .from(basicContract) .leftJoin(vendors, eq(basicContract.vendorId, vendors.id)) .leftJoin(basicContractTemplates, eq(basicContract.templateId, basicContractTemplates.id)) .where(inArray(basicContract.id, contractIds)) const withFlags = await Promise.all( contracts.map(async (contract) => { const triggeredFlags = await getTriggeredRedFlagQuestions(contract.contractId) return { ...contract, triggeredFlags, } }) ) return withFlags.filter((contract) => contract.triggeredFlags.length > 0) } async function buildTemplateVariables( contracts: ContractSummary[], meta: { requesterName: string; requestedAt: Date } ): Promise> { const summaryRows = contracts.map((contract) => ({ contractId: contract.contractId, vendorName: contract.vendorName ?? "-", templateName: contract.templateName ?? "-", redFlagCount: contract.triggeredFlags.length, })) const summaryTable = await htmlTableConverter(summaryRows, [ { key: "contractId", label: "계약 ID" }, { key: "vendorName", label: "업체명" }, { key: "templateName", label: "템플릿" }, { key: "redFlagCount", label: "RED FLAG 수" }, ]) const detailSections = await Promise.all( contracts.map(async (contract) => { const questionList = contract.triggeredFlags.map((flag, index) => { const prefix = flag.questionNumber || `${index + 1}` return `${prefix}. ${flag.questionText}` }) const listHtml = await htmlListConverter(questionList) return `
계약 ID: ${contract.contractId} / ${contract.vendorName ?? "-"}
${listHtml}
` }) ) const detailHtml = detailSections.join("") const formattedDate = new Intl.DateTimeFormat("ko-KR", { dateStyle: "medium", timeStyle: "short", }).format(meta.requestedAt) return { 요청자이름: meta.requesterName, 요청일시: formattedDate, 요청사유: "컴플라이언스 Red Flag 해소를 위해 구매기획 합의를 요청드립니다.", RedFlag요약테이블: summaryTable, RedFlag상세내역: detailHtml, } } function buildApprovalTitle(contracts: ContractSummary[]) { if (contracts.length === 0) return "컴플라이언스 Red Flag 해소요청" const firstVendor = contracts[0].vendorName ?? `계약 ${contracts[0].contractId}` if (contracts.length === 1) { return `Red Flag 해소요청 - ${firstVendor}` } return `Red Flag 해소요청 - ${firstVendor} 외 ${contracts.length - 1}건` }