summaryrefslogtreecommitdiff
path: root/lib/compliance
diff options
context:
space:
mode:
Diffstat (limited to 'lib/compliance')
-rw-r--r--lib/compliance/red-flag-resolution.ts217
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}건`
+}