diff options
Diffstat (limited to 'lib/compliance')
| -rw-r--r-- | lib/compliance/red-flag-resolution.ts | 217 |
1 files changed, 184 insertions, 33 deletions
diff --git a/lib/compliance/red-flag-resolution.ts b/lib/compliance/red-flag-resolution.ts index 63057523..47a805bb 100644 --- a/lib/compliance/red-flag-resolution.ts +++ b/lib/compliance/red-flag-resolution.ts @@ -26,7 +26,121 @@ export type ContractSummary = { * 새로운 코드는 `requestRedFlagResolutionWithApproval`을 사용하세요. */ export async function requestRedFlagResolution(contractIds: number[]): Promise<ApprovalResult> { - return await requestRedFlagResolutionWithApproval({ contractIds }) + 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가 설정되지 않았습니다.") + } + + // 준법 응답 및 중복 해소요청 여부를 먼저 확인 (Heavy query 전에 선행) + const responses = await db + .select({ + basicContractId: complianceResponses.basicContractId, + redFlagResolvedAt: complianceResponses.redFlagResolvedAt, + redFlagResolutionApprovalId: complianceResponses.redFlagResolutionApprovalId, + }) + .from(complianceResponses) + .where(inArray(complianceResponses.basicContractId, uniqueContractIds)) + + const blockedContracts = responses + .filter((response) => response.redFlagResolutionApprovalId && !response.redFlagResolvedAt) + .map((response) => response.basicContractId) + + if (blockedContracts.length > 0) { + const blockedSummaries = await fetchContractVendorSummaries(blockedContracts) + const blockedNames = blockedSummaries.map( + (contract) => contract.vendorName ?? `계약 ${contract.contractId}` + ) + + const preview = + blockedNames.length > 2 + ? `${blockedNames.slice(0, 2).join(", ")} 외 ${blockedNames.length - 2}건` + : blockedNames.join(", ") + + throw new Error(`이미 해소요청이 진행 중인 계약서가 있습니다: ${preview}`) + } + + const contractSummaries = await fetchContractsWithFlags(uniqueContractIds) + if (contractSummaries.length === 0) { + throw new Error("선택한 계약서에 RED FLAG가 존재하지 않습니다.") + } + + const validContractIds = contractSummaries.map((contract) => contract.contractId) + + const missingResponses = validContractIds.filter( + (contractId) => !responses.some((response) => response.basicContractId === contractId) + ) + + if (missingResponses.length > 0) { + throw new Error("준법 응답 정보를 찾을 수 없는 계약서가 포함되어 있습니다.") + } + + 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 } /** @@ -95,45 +209,82 @@ export async function fetchContractsWithFlags(contractIds: number[]): Promise<Co return withFlags.filter((contract) => contract.triggeredFlags.length > 0) } -/** - * RED FLAG 해소요청 검증 (중복 요청 방지) - */ -export async function validateRedFlagResolutionRequest( - validContractIds: number[], - contractSummaries: ContractSummary[] -): Promise<void> { - // 중복 해소요청 방지 (진행 중인 결재가 있는지 확인) - const responses = await db +async function fetchContractVendorSummaries( + contractIds: number[] +): Promise<Array<{ contractId: number; vendorName: string | null }>> { + if (contractIds.length === 0) { + return [] + } + + return db .select({ - basicContractId: complianceResponses.basicContractId, - redFlagResolvedAt: complianceResponses.redFlagResolvedAt, - redFlagResolutionApprovalId: complianceResponses.redFlagResolutionApprovalId, + contractId: basicContract.id, + vendorName: vendors.vendorName, }) - .from(complianceResponses) - .where(inArray(complianceResponses.basicContractId, validContractIds)) + .from(basicContract) + .leftJoin(vendors, eq(basicContract.vendorId, vendors.id)) + .where(inArray(basicContract.id, contractIds)) +} - const missingResponses = validContractIds.filter( - (contractId) => !responses.some((response) => response.basicContractId === contractId) - ) +async function buildTemplateVariables( + contracts: ContractSummary[], + meta: { requesterName: string; requestedAt: Date } +): Promise<Record<string, string>> { + const summaryRows = contracts.map((contract) => ({ + contractId: contract.contractId, + vendorName: contract.vendorName ?? "-", + templateName: contract.templateName ?? "-", + redFlagCount: contract.triggeredFlags.length, + })) - if (missingResponses.length > 0) { - throw new Error("준법 응답 정보를 찾을 수 없는 계약서가 포함되어 있습니다.") - } + const summaryTable = await htmlTableConverter(summaryRows, [ + { key: "contractId", label: "계약 ID" }, + { key: "vendorName", label: "업체명" }, + { key: "templateName", label: "템플릿" }, + { key: "redFlagCount", label: "RED FLAG 수" }, + ]) - const blockedContracts = responses - .filter((response) => response.redFlagResolutionApprovalId && !response.redFlagResolvedAt) - .map((response) => response.basicContractId) + 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}` + }) - if (blockedContracts.length > 0) { - const blockedSummaries = contractSummaries - .filter((contract) => blockedContracts.includes(contract.contractId)) - .map((contract) => contract.vendorName ?? `계약 ${contract.contractId}`) + const listHtml = await htmlListConverter(questionList) + return ` + <div style="margin-bottom: 24px;"> + <div style="font-weight:600;margin-bottom:8px;"> + 계약 ID: ${contract.contractId} / ${contract.vendorName ?? "-"} + </div> + <div>${listHtml}</div> + </div> + ` + }) + ) - const preview = - blockedSummaries.length > 2 - ? `${blockedSummaries.slice(0, 2).join(", ")} 외 ${blockedSummaries.length - 2}건` - : blockedSummaries.join(", ") + const detailHtml = detailSections.join("") + const formattedDate = new Intl.DateTimeFormat("ko-KR", { + dateStyle: "medium", + timeStyle: "short", + }).format(meta.requestedAt) - throw new Error(`이미 해소요청이 진행 중인 계약서가 있습니다: ${preview}`) + 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}건` +} |
