import { and, asc, eq, inArray, isNull } from 'drizzle-orm'; import { crmQuotations, memberships, users } from '@/db/schema'; import { db } from '@/lib/db'; import { AuthError } from '@/lib/auth/session'; import { getApprovalRequest, listApprovalRequests } from '@/features/foundation/approval/server/service'; import type { ApprovalDetailRecord, ApprovalTimelineItem } from '@/features/foundation/approval/types'; import { getSignaturePositionLookup, resolveSignaturePositionLabel } from '@/features/crm/quotations/document/signature-position'; import type { SignatureBlock, SignatureParty } from '@/features/crm/quotations/document/signature.types'; const EMPTY_SIGNATURE_PARTY: SignatureParty = { name: '-', position: '-', actedAt: null, userId: null, roleCode: null }; const APPROVED_BY_ROLE_CODES = new Set(['sales_manager', 'department_manager']); type ApprovalActionWithRole = ApprovalTimelineItem & { roleCode: string | null; }; function toSignatureParty(input?: Partial | null): SignatureParty { return { name: input?.name?.trim() || '-', position: input?.position?.trim() || '-', actedAt: input?.actedAt ?? null, userId: input?.userId ?? null, roleCode: input?.roleCode ?? null }; } function mapApprovalActions(detail: ApprovalDetailRecord | null): ApprovalActionWithRole[] { if (!detail) { return []; } return detail.timeline .filter((item) => item.action === 'approve') .map((item) => ({ ...item, roleCode: detail.steps.find((step) => step.stepNumber === item.stepNumber)?.roleCode ?? null })); } function resolveApprovedByAction(actions: ApprovalActionWithRole[], finalRoleCode: string | null) { const candidates = actions.filter( (item) => item.roleCode !== finalRoleCode && item.roleCode !== null && APPROVED_BY_ROLE_CODES.has(item.roleCode) ); return candidates.at(-1) ?? null; } function resolveAuthorizedByAction( actions: ApprovalActionWithRole[], finalRoleCode: string | null ) { if (!finalRoleCode) { return null; } return actions.filter((item) => item.roleCode === finalRoleCode).at(-1) ?? null; } export async function resolveQuotationSignatures( quotationId: string, organizationId: string ): Promise { const [quotation] = await db .select({ id: crmQuotations.id, organizationId: crmQuotations.organizationId, salesmanId: crmQuotations.salesmanId, createdBy: crmQuotations.createdBy, createdAt: crmQuotations.createdAt }) .from(crmQuotations) .where( and( eq(crmQuotations.id, quotationId), eq(crmQuotations.organizationId, organizationId), isNull(crmQuotations.deletedAt) ) ) .limit(1); if (!quotation) { throw new AuthError('Quotation not found', 404); } const approvalRequests = await listApprovalRequests(organizationId, { entityType: 'quotation', entityId: quotationId, limit: 20 }); const approvalRequest = approvalRequests.items.find((item) => item.status === 'approved') ?? approvalRequests.items[0] ?? null; const approvalDetail = approvalRequest ? await getApprovalRequest(approvalRequest.id, organizationId) : null; const finalStep = approvalDetail?.steps.at(-1) ?? null; const approvalActions = mapApprovalActions(approvalDetail); const approvedByAction = resolveApprovedByAction(approvalActions, finalStep?.roleCode ?? null); const authorizedByAction = resolveAuthorizedByAction( approvalActions, finalStep?.roleCode ?? null ); const preparedByUserId = quotation.salesmanId ?? quotation.createdBy; const userIds = [ preparedByUserId, approvedByAction?.actedBy ?? null, authorizedByAction?.actedBy ?? null ].filter((value): value is string => Boolean(value)); const uniqueUserIds = [...new Set(userIds)]; const [userRows, membershipRows, positionLookup] = await Promise.all([ uniqueUserIds.length ? db .select({ id: users.id, name: users.name }) .from(users) .where(inArray(users.id, uniqueUserIds)) : Promise.resolve([]), uniqueUserIds.length ? db .select({ userId: memberships.userId, businessRole: memberships.businessRole }) .from(memberships) .where( and( eq(memberships.organizationId, organizationId), inArray(memberships.userId, uniqueUserIds) ) ) : Promise.resolve([]), getSignaturePositionLookup(organizationId) ]); const userMap = new Map(userRows.map((row) => [row.id, row.name])); const membershipMap = new Map(membershipRows.map((row) => [row.userId, row.businessRole])); const createPartyFromUser = (args: { userId: string | null | undefined; actedAt?: string | null; roleCode?: string | null; fallbackName?: string | null; }) => { if (!args.userId && !args.fallbackName) { return toSignatureParty(EMPTY_SIGNATURE_PARTY); } const businessRole = (args.userId ? membershipMap.get(args.userId) : null) ?? args.roleCode ?? null; return toSignatureParty({ name: (args.userId ? userMap.get(args.userId) : null) ?? args.fallbackName?.trim() ?? EMPTY_SIGNATURE_PARTY.name, position: resolveSignaturePositionLabel(businessRole, positionLookup), actedAt: args.actedAt ?? null, userId: args.userId ?? null, roleCode: businessRole }); }; return { preparedBy: createPartyFromUser({ userId: preparedByUserId, actedAt: quotation.createdAt.toISOString() }), approvedBy: createPartyFromUser({ userId: approvedByAction?.actedBy, actedAt: approvedByAction?.actedAt ?? null, roleCode: approvedByAction?.roleCode ?? null, fallbackName: approvedByAction?.actorName ?? null }), authorizedBy: createPartyFromUser({ userId: authorizedByAction?.actedBy, actedAt: authorizedByAction?.actedAt ?? null, roleCode: authorizedByAction?.roleCode ?? finalStep?.roleCode ?? null, fallbackName: authorizedByAction?.actorName ?? null }) }; }