diff options
Diffstat (limited to 'lib/po/service.ts')
| -rw-r--r-- | lib/po/service.ts | 431 |
1 files changed, 431 insertions, 0 deletions
diff --git a/lib/po/service.ts b/lib/po/service.ts new file mode 100644 index 00000000..dc398201 --- /dev/null +++ b/lib/po/service.ts @@ -0,0 +1,431 @@ +"use server"; + +import { headers } from "next/headers"; +import db from "@/db/db"; +import { GetPOSchema } from "./validations"; +import { unstable_cache } from "@/lib/unstable-cache"; +import { filterColumns } from "@/lib/filter-columns"; +import { + asc, + desc, + ilike, + inArray, + and, + gte, + lte, + not, + or, + eq, + count, +} from "drizzle-orm"; +import { countPos, selectPos } from "./repository"; + +import { + contractEnvelopes, + contractsDetailView, + contractSigners, + contracts, +} from "@/db/schema/contract"; +import { vendors, vendorContacts } from "@/db/schema/vendors"; +import { revalidatePath } from "next/cache"; +import * as z from "zod"; +import { POContent } from "@/lib/docuSign/types"; + +/** + * PQ 목록 조회 + */ +export async function getPOs(input: GetPOSchema) { + return unstable_cache( + async () => { + try { + const offset = (input.page - 1) * input.perPage; + + // 1. Try a simple query first to make sure the view works at all + try { + const testQuery = await db + .select({ count: count() }) + .from(contractsDetailView); + console.log("Test query result:", testQuery); + } catch (testErr) { + console.error("Test query failed:", testErr); + } + + // 2. Build where clause with more careful handling + let advancedWhere; + try { + advancedWhere = filterColumns({ + table: contractsDetailView, + filters: input.filters, + joinOperator: input.joinOperator, + }); + console.log("Advanced where clause built successfully"); + } catch (whereErr) { + console.error("Error building advanced where:", whereErr); + advancedWhere = undefined; + } + + let globalWhere; + if (input.search) { + try { + const s = `%${input.search}%`; + globalWhere = or( + ilike(contractsDetailView.contractNo, s), + ilike(contractsDetailView.contractName, s) + ); + console.log("Global where clause built successfully"); + } catch (searchErr) { + console.error("Error building search where:", searchErr); + globalWhere = undefined; + } + } + + // 3. Combine where clauses safely + let finalWhere; + if (advancedWhere && globalWhere) { + finalWhere = and(advancedWhere, globalWhere); + } else { + finalWhere = advancedWhere || globalWhere; + } + + // 4. Build order by + let orderBy; + try { + orderBy = + input.sort.length > 0 + ? input.sort.map((item) => + item.desc + ? desc(contractsDetailView[item.id]) + : asc(contractsDetailView[item.id]) + ) + : [asc(contractsDetailView.createdAt)]; + } catch (orderErr) { + console.error("Error building order by:", orderErr); + orderBy = [asc(contractsDetailView.createdAt)]; + } + + // 5. Execute queries with proper error handling + let data = []; + let total = 0; + + try { + // Try without transaction first for better error visibility + const queryBuilder = db.select().from(contractsDetailView); + + // Add where clause if it exists + if (finalWhere) { + queryBuilder.where(finalWhere); + } + + // Add ordering + queryBuilder.orderBy(...orderBy); + + // Add pagination + queryBuilder.offset(offset).limit(input.perPage); + + // Execute query + data = await queryBuilder; + + // Get total count + const countBuilder = db + .select({ count: count() }) + .from(contractsDetailView); + + if (finalWhere) { + countBuilder.where(finalWhere); + } + + const countResult = await countBuilder; + total = countResult[0]?.count || 0; + } catch (queryErr) { + console.error("Query execution failed:", queryErr); + throw queryErr; // Rethrow to be caught by the outer try/catch + } + + const pageCount = Math.ceil(total / input.perPage); + + return { data, pageCount }; + } catch (err) { + // More detailed error logging + console.error("Error in getPOs:", err); + if (err instanceof Error) { + console.error("Error message:", err.message); + console.error("Error stack:", err.stack); + } + return { data: [], pageCount: 0 }; + } + }, + [JSON.stringify(input)], + { + revalidate: 3600, + tags: [`po`], + } + )(); +} + +// Schema for a single signer +const signerSchema = z.object({ + signerEmail: z.string().email(), + signerName: z.string().min(1), + signerPosition: z.string(), + signerType: z.enum(["REQUESTER", "VENDOR"]), + vendorContactId: z.number().optional(), +}); + +// Schema for the entire request +const signatureRequestSchema = z.object({ + contractId: z.number(), + signers: z.array(signerSchema).min(1, "At least one signer is required"), +}); + +/** + * Server action to request electronic signatures for a contract from multiple parties + */ +export async function requestSignatures( + input: z.infer<typeof signatureRequestSchema> +): Promise<{ success: boolean; message: string }> { + try { + // Validate the input + const validatedData = signatureRequestSchema.parse(input); + + const headersList = await headers(); + const host = headersList.get("host"); + const proto = headersList.get("x-forwarded-proto") || "http"; // 기본값은 http + const origin = `${proto}://${host}`; + + // Use a transaction to ensure data consistency + return await db.transaction(async (tx) => { + // Get contract details using standard select + const [contract] = await tx + .select() + .from(contracts) + .where(eq(contracts.id, validatedData.contractId)) + .limit(1); + + if (!contract) { + throw new Error( + `Contract with ID ${validatedData.contractId} not found` + ); + } + + // Generate unique envelope ID + // const envelopeId = `env-${Date.now()}-${Math.floor( + // Math.random() * 1000 + // )}`; + + // Get contract number or fallback + const contractNo = + contract.contractNo || `contract-${validatedData.contractId}`; + + const signer = validatedData.signers.find( + (c) => c.signerType === "REQUESTER" + ); + + const vendor = validatedData.signers.find( + (c) => c.signerType === "VENDOR" + ); + + if (!vendor || !signer) { + return { + success: true, + message: `협력업체 서명자를 확인할 수 없습니다.`, + }; + } + + const { vendorContactId } = vendor; + + if (!vendorContactId) { + return { + success: true, + message: `계약 번호를 확인할 수 없습니다.`, + }; + } + + const [vendorInfoData] = await tx + .select({ + vendorContract: vendorContacts, + vendorInfo: vendors, + }) + .from(vendorContacts) + .leftJoin(vendors, eq(vendorContacts.vendorId, vendors.id)) + .where(eq(vendorContacts.id, vendorContactId)) + .limit(1); + + const { vendorContract, vendorInfo } = vendorInfoData; + + const docuSignTempId = "73b04617-477c-4ec8-8a32-c8da701f6b0c"; + + const { totalAmount = "0", tax = "0" } = contract; + + const totalAmountNum = Number(totalAmount); + const taxNum = Number(tax); + const taxRate = ((taxNum / totalAmountNum) * 100).toFixed(2); + + const contractInfo: POContent = [ + { tabLabel: "po_no", value: contractNo }, + { tabLabel: "vendor_name", value: vendorInfo?.vendorName ?? "" }, + { tabLabel: "po_date", value: contract?.startDate ?? "" }, + { tabLabel: "project_name", value: contract.contractName }, + { tabLabel: "vendor_location", value: vendorInfo?.address ?? "" }, + { tabLabel: "shi_email", value: signer.signerEmail }, + { tabLabel: "vendor_email", value: vendorContract.contactEmail }, + { tabLabel: "po_desc", value: contract.contractName }, + { tabLabel: "qty", value: "1" }, + { tabLabel: "unit_price", value: totalAmountNum.toLocaleString() }, + { tabLabel: "total", value: totalAmountNum.toLocaleString() }, + { + tabLabel: "grand_total_amount", + value: totalAmountNum.toLocaleString(), + }, + { tabLabel: "tax_rate", value: taxRate }, + { tabLabel: "tax_total", value: taxNum.toLocaleString() }, + { + tabLabel: "payment_amount", + value: (totalAmountNum + taxNum).toLocaleString(), + }, + { + tabLabel: "remark", + value: `결제 조건: ${contract.paymentTerms} +납품 조건: ${contract.deliveryTerms} +납품 기한: ${contract.deliveryDate} +납품 장소: ${contract.deliveryLocation} +계약 종료일/유효 기간: ${contract.endDate} +Remarks:${contract.remarks}`, + }, + ]; + + const sendDocuSign = await fetch(`${origin}/api/po/sendDocuSign`, { + method: "POST", + headers: { + "Content-Type": "application/json", // ✅ 이거 꼭 있어야 함! + }, + body: JSON.stringify({ + docuSignTempId, + contractInfo, + contractorInfo: { + email: "dts@dtsolution.co.kr", + name: "삼성중공업", + roleName: "shi", + }, + subcontractorinfo: { + email: vendorContract.contactEmail, + name: vendorInfo?.vendorName, + roleName: "vendor", + }, + ccInfo: [ + // { + // email: "kiman.kim@dtsolution.io", + // name: "김기만", + // roleName: "cc", + // }, + ], + }), + }).then((data) => data.json()); + + const { success: sendDocuSignResult, envelopeId } = sendDocuSign; + + if (!sendDocuSignResult) { + return { + success: false, + message: "DocuSign 전자 서명 발송에 실패하였습니다.", + }; + } + + // Create a single envelope for all signers + const [newEnvelope] = await tx + .insert(contractEnvelopes) + .values({ + contractId: validatedData.contractId, + envelopeId: envelopeId, + envelopeStatus: "sent", + fileName: `${contractNo}-signature.pdf`, // Required field + filePath: `/contracts/${validatedData.contractId}/signatures`, // Required field + // Add any other required fields based on your schema + }) + .returning(); + + // // Check for duplicate emails + const signerEmails = new Set(); + for (const signer of validatedData.signers) { + if (signerEmails.has(signer.signerEmail)) { + throw new Error(`Duplicate signer email: ${signer.signerEmail}`); + } + signerEmails.add(signer.signerEmail); + } + + // Create signer records for each signer + for (const signer of validatedData.signers) { + await tx.insert(contractSigners).values({ + envelopeId: newEnvelope.id, + signerEmail: signer.signerEmail, + signerName: signer.signerName, + signerPosition: signer.signerPosition, + signerStatus: "sent", + signerType: signer.signerType, + // Only include vendorContactId if it's provided and the signer is a vendor + ...(signer.vendorContactId && signer.signerType === "VENDOR" + ? { vendorContactId: signer.vendorContactId } + : {}), + }); + } + + // Update contract status to indicate pending signatures + await tx + .update(contracts) + .set({ status: "PENDING_SIGNATURE" }) + .where(eq(contracts.id, validatedData.contractId)); + + // In a real implementation, you would send the envelope to DocuSign or similar service + // For example: + // const docusignResult = await docusignClient.createEnvelope({ + // recipients: validatedData.signers.map(signer => ({ + // email: signer.signerEmail, + // name: signer.signerName, + // recipientType: signer.signerType === "REQUESTER" ? "signer" : "cc", + // routingOrder: signer.signerType === "REQUESTER" ? 1 : 2, + // })), + // documentId: `contract-${validatedData.contractId}`, + // // other DocuSign-specific parameters + // }); + + // Revalidate the path to refresh the data + revalidatePath("/po"); + + // Return success response + return { + success: true, + message: `Signature requests sent to ${validatedData.signers.length} recipient(s)`, + }; + }); + } catch (error) { + console.error("Error requesting electronic signatures:", error); + return { + success: false, + message: + error instanceof Error + ? error.message + : "Failed to send signature requests", + }; + } +} + +export async function getVendorContacts(vendorId: number) { + try { + const contacts = await db + .select({ + id: vendorContacts.id, + contactName: vendorContacts.contactName, + contactEmail: vendorContacts.contactEmail, + contactPosition: vendorContacts.contactPosition, + contactPhone: vendorContacts.contactPhone, + isPrimary: vendorContacts.isPrimary, + }) + .from(vendorContacts) + .where(eq(vendorContacts.vendorId, vendorId)) + .orderBy(vendorContacts.isPrimary, vendorContacts.contactName); + + return contacts; + } catch (error) { + console.error("Error fetching vendor contacts:", error); + throw new Error("Failed to fetch vendor contacts"); + } +} |
