summaryrefslogtreecommitdiff
path: root/lib/compliance
diff options
context:
space:
mode:
Diffstat (limited to 'lib/compliance')
-rw-r--r--lib/compliance/red-flag-resolution.ts271
1 files changed, 181 insertions, 90 deletions
diff --git a/lib/compliance/red-flag-resolution.ts b/lib/compliance/red-flag-resolution.ts
index 47a805bb..02e0179b 100644
--- a/lib/compliance/red-flag-resolution.ts
+++ b/lib/compliance/red-flag-resolution.ts
@@ -2,15 +2,19 @@
import db from "@/db/db"
import { and, eq, inArray } from "drizzle-orm"
-import { complianceResponses } from "@/db/schema/compliance"
+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 { revalidatePath } from "next/cache"
-import { requestRedFlagResolutionWithApproval } from "./approval-actions"
+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"
-export type ContractSummary = {
+type ContractSummary = {
contractId: number
vendorName: string | null
vendorCode: string | null
@@ -19,107 +23,83 @@ export type ContractSummary = {
triggeredFlags: TriggeredRedFlagInfo[]
}
-/**
- * RED FLAG 해소요청 - Approval Saga를 통해 상신
- *
- * @deprecated 이 함수는 호환성을 위해 유지됩니다.
- * 새로운 코드는 `requestRedFlagResolutionWithApproval`을 사용하세요.
- */
-export async function requestRedFlagResolution(contractIds: number[]): Promise<ApprovalResult> {
- if (!contractIds || contractIds.length === 0) {
- throw new Error("RED FLAG 해소요청을 위한 계약서를 선택해주세요.")
- }
+const RED_FLAG_TEMPLATE_NAME = "컴플라이언스 Red Flag 해소요청"
- const uniqueContractIds = Array.from(new Set(contractIds))
+type SessionResult = Awaited<ReturnType<typeof getServerSession>>
+type SessionUser = (SessionResult extends { user: infer U } ? NonNullable<U> : never) & {
+ epId?: string | null
+ id?: string | number
+}
- const session = await getServerSession(authOptions)
- if (!session?.user) {
- throw new Error("인증이 필요합니다.")
- }
+type RedFlagResolutionRequestInput = {
+ contractIds: number[]
+ approvers?: string[]
+ title?: string
+}
- const currentUser = session.user
- if (!currentUser.epId) {
- throw new Error("Knox EP ID가 필요합니다.")
- }
+type RedFlagResolutionPreparationResult = {
+ currentUser: SessionUser
+ currentUserId: number
+ validContractIds: number[]
+ variables: Record<string, string>
+ title: string
+ defaultApprovers: string[]
+ now: Date
+}
- const currentUserId = Number(currentUser.id)
- if (Number.isNaN(currentUserId)) {
- throw new Error("유효한 사용자 정보가 필요합니다.")
- }
+/**
+ * RED FLAG 해소요청 미리보기 데이터 준비
+ */
+export async function prepareRedFlagResolutionApproval(contractIds: number[]) {
+ const context = await prepareRedFlagResolutionContext(contractIds)
- const purchasingManagerEpId = await getPurchasingManagerEpId()
- if (!purchasingManagerEpId) {
- throw new Error("구매기획 담당자의 EP ID가 설정되지 않았습니다.")
+ return {
+ contractIds: context.validContractIds,
+ templateName: RED_FLAG_TEMPLATE_NAME,
+ title: context.title,
+ variables: context.variables,
+ defaultApprovers: context.defaultApprovers,
}
+}
- // 준법 응답 및 중복 해소요청 여부를 먼저 확인 (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(", ")
+/**
+ * RED FLAG 해소요청 - Approval Saga를 통해 상신
+ */
+export async function requestRedFlagResolution(
+ input: RedFlagResolutionRequestInput
+): Promise<ApprovalResult> {
+ const context = await prepareRedFlagResolutionContext(input.contractIds)
- throw new Error(`이미 해소요청이 진행 중인 계약서가 있습니다: ${preview}`)
- }
+ const requestedTitle = input.title?.trim() || context.title
- const contractSummaries = await fetchContractsWithFlags(uniqueContractIds)
- if (contractSummaries.length === 0) {
- throw new Error("선택한 계약서에 RED FLAG가 존재하지 않습니다.")
- }
+ const baseApprovers =
+ input.approvers && input.approvers.length > 0 ? input.approvers : context.defaultApprovers
- const validContractIds = contractSummaries.map((contract) => contract.contractId)
-
- const missingResponses = validContractIds.filter(
- (contractId) => !responses.some((response) => response.basicContractId === contractId)
+ const approverEpIds = Array.from(
+ new Set(baseApprovers.filter((epId): epId is string => typeof epId === "string" && epId.length > 0))
)
- if (missingResponses.length > 0) {
- throw new Error("준법 응답 정보를 찾을 수 없는 계약서가 포함되어 있습니다.")
+ if (approverEpIds.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(),
+ contractIds: context.validContractIds,
+ requestedBy: context.currentUserId,
+ requestedAt: context.now.toISOString(),
},
{
- title,
- description: "컴플라이언스 Red Flag 해소요청",
- templateName: "컴플라이언스 Red Flag 해소요청",
- variables,
- approvers: [purchasingManagerEpId],
+ title: requestedTitle,
+ description: RED_FLAG_TEMPLATE_NAME,
+ templateName: RED_FLAG_TEMPLATE_NAME,
+ variables: context.variables,
+ approvers: approverEpIds,
currentUser: {
- id: currentUserId,
- epId: currentUser.epId,
- email: currentUser.email ?? undefined,
+ id: context.currentUserId,
+ epId: context.currentUser.epId!,
+ email: context.currentUser.email ?? undefined,
},
}
)
@@ -134,7 +114,7 @@ export async function requestRedFlagResolution(contractIds: number[]): Promise<A
redFlagResolvedAt: null,
updatedAt: new Date(),
})
- .where(inArray(complianceResponses.basicContractId, validContractIds))
+ .where(inArray(complianceResponses.basicContractId, context.validContractIds))
await revalidatePath("/evcp/basic-contract")
await revalidatePath("/evcp/compliance")
@@ -179,10 +159,31 @@ export async function resolveRedFlag(
}
}
-/**
- * 계약서와 RED FLAG 정보를 함께 조회
- */
-export async function fetchContractsWithFlags(contractIds: number[]): Promise<ContractSummary[]> {
+async function getPurchasingManagerEpId(): Promise<string | null> {
+ 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<ContractSummary[]> {
const contracts = await db
.select({
contractId: basicContract.id,
@@ -288,3 +289,93 @@ function buildApprovalTitle(contracts: ContractSummary[]) {
return `Red Flag 해소요청 - ${firstVendor} 외 ${contracts.length - 1}건`
}
+
+async function prepareRedFlagResolutionContext(
+ contractIds: number[]
+): Promise<RedFlagResolutionPreparationResult> {
+ 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 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)
+
+ return {
+ currentUser,
+ currentUserId,
+ validContractIds,
+ variables,
+ title,
+ defaultApprovers: [purchasingManagerEpId],
+ now,
+ }
+}