From 41a6f7f1c8cfb4ff381c744f78465b6b6a77640b Mon Sep 17 00:00:00 2001 From: Matt Date: Thu, 12 Jun 2025 17:04:45 +0200 Subject: [PATCH] feat: Implement EOI document validation and orphaned data cleanup - Add new /api/eoi/validate-document endpoint to check document existence - Automatically clean up orphaned database records when documents don't exist in Documenso - Update EOISection component to validate documents on mount - Enhanced delete-generated-document endpoint to handle already-deleted documents - Updated check-signature-status endpoint with validation logic - Prevents EOI section from showing when document no longer exists - Self-healing system that fixes data inconsistencies automatically Key improvements: - Validates document existence before showing EOI management UI - Cleans up documensoID, signature links, and status fields when document is missing - Graceful handling of 404 errors from Documenso API - Background validation with user-friendly notifications - Prevents phantom EOI states that appear generated but don't exist --- components/EOISection.vue | 51 +++++++- server/api/eoi/check-signature-status.ts | 48 ++++++-- server/api/eoi/delete-generated-document.ts | 32 ++++- server/api/eoi/validate-document.ts | 129 ++++++++++++++++++++ 4 files changed, 247 insertions(+), 13 deletions(-) create mode 100644 server/api/eoi/validate-document.ts diff --git a/components/EOISection.vue b/components/EOISection.vue index 41e25a5..faeb6aa 100644 --- a/components/EOISection.vue +++ b/components/EOISection.vue @@ -520,6 +520,7 @@ const isDeleting = ref(false); const isDeletingGenerated = ref(false); const signatureStatus = ref(null); const isCheckingSignatures = ref(false); +const isValidatingDocument = ref(false); const hasGeneratedEOI = computed(() => { // Primary check: documensoID must exist for a generated EOI @@ -857,10 +858,56 @@ const deleteGeneratedEOI = async () => { } }; +const validateDocument = async () => { + if (!props.interest['documensoID']) return; + + isValidatingDocument.value = true; + + try { + console.log('[EOI Section] Validating document for interest:', props.interest.Id); + + const response = await $fetch<{ + success: boolean; + valid: boolean; + cleaned: boolean; + message: string; + documentStatus?: string; + }>('/api/eoi/validate-document', { + headers: { + 'x-tag': '094ut234' + }, + params: { + interestId: props.interest.Id.toString() + } + }); + + if (response.success) { + if (response.cleaned) { + console.log('[EOI Section] Document validation cleaned up orphaned records'); + toast.info('Detected and cleaned up orphaned EOI data'); + emit('update'); // Refresh parent to reflect cleaned state + } else { + console.log('[EOI Section] Document validation passed'); + } + } + } catch (error: any) { + console.error('[EOI Section] Failed to validate document:', error); + // Don't show error to user - validation is a background process + } finally { + isValidatingDocument.value = false; + } +}; + // Check signature status on mount if EOI is generated onMounted(() => { - if (hasGeneratedEOI.value && !isEOISigned.value) { - checkSignatureStatus(); + if (hasGeneratedEOI.value) { + // First validate the document exists + validateDocument().then(() => { + // Only check signature status if document is still valid after validation + if (hasGeneratedEOI.value && !isEOISigned.value) { + checkSignatureStatus(); + } + }); } }); diff --git a/server/api/eoi/check-signature-status.ts b/server/api/eoi/check-signature-status.ts index c5e4d29..992748e 100644 --- a/server/api/eoi/check-signature-status.ts +++ b/server/api/eoi/check-signature-status.ts @@ -1,5 +1,6 @@ import { getDocumesoDocumentByExternalId, checkDocumentSignatureStatus } from '~/server/utils/documeso'; -import { getInterestById } from '~/server/utils/nocodb'; +import { getInterestById, updateInterest } from '~/server/utils/nocodb'; +import type { InterestSalesProcessLevel, EOIStatus } from '~/utils/types'; export default defineEventHandler(async (event) => { const xTagHeader = getRequestHeader(event, "x-tag"); @@ -44,12 +45,45 @@ export default defineEventHandler(async (event) => { // If we have a stored documensoID, use it directly if (interest && interest['documensoID']) { console.log('[check-signature-status] Using stored documensoID:', interest['documensoID']); - const status = await checkDocumentSignatureStatus(parseInt(interest['documensoID'])); - return { - success: true, - documentId: parseInt(interest['documensoID']), - ...status - }; + try { + const status = await checkDocumentSignatureStatus(parseInt(interest['documensoID'])); + return { + success: true, + documentId: parseInt(interest['documensoID']), + ...status + }; + } catch (error: any) { + console.error('[check-signature-status] Failed to check status for stored documensoID:', error); + + // If document not found (404), clean up the database + if (error.status === 404 || error.statusCode === 404 || error.message?.includes('404')) { + console.log('[check-signature-status] Document not found - cleaning up orphaned database records'); + + const updateData = { + 'EOI Status': 'Awaiting Further Details' as EOIStatus, + 'Sales Process Level': 'Specific Qualified Interest' as InterestSalesProcessLevel, + 'EOI Time Sent': undefined, + 'Signature Link Client': undefined, + 'Signature Link CC': undefined, + 'Signature Link Developer': undefined, + 'EmbeddedSignatureLinkClient': undefined, + 'EmbeddedSignatureLinkCC': undefined, + 'EmbeddedSignatureLinkDeveloper': undefined, + 'documensoID': undefined, + 'reminder_enabled': false + }; + + await updateInterest(interestId, updateData); + + throw createError({ + statusCode: 404, + statusMessage: 'Document not found - database cleaned up', + }); + } + + // Re-throw other errors + throw error; + } } // Otherwise, try to find by external ID (using interestId) - fallback method diff --git a/server/api/eoi/delete-generated-document.ts b/server/api/eoi/delete-generated-document.ts index e51ebf7..0c813b7 100644 --- a/server/api/eoi/delete-generated-document.ts +++ b/server/api/eoi/delete-generated-document.ts @@ -74,6 +74,8 @@ export default defineEventHandler(async (event) => { } console.log('[Delete Generated EOI] Deleting document from Documenso'); + let documensoDeleteSuccessful = false; + try { const deleteResponse = await fetch(`${documensoBaseUrl}/api/v1/documents/${documensoID}`, { method: 'DELETE', @@ -86,15 +88,37 @@ export default defineEventHandler(async (event) => { if (!deleteResponse.ok) { const errorText = await deleteResponse.text(); console.error('[Delete Generated EOI] Documenso deletion failed:', errorText); - throw new Error(`Failed to delete document from Documenso: ${deleteResponse.statusText}`); + + // If it's a 404, the document is already gone, which is what we want + if (deleteResponse.status === 404) { + console.log('[Delete Generated EOI] Document already deleted from Documenso (404) - proceeding with database cleanup'); + documensoDeleteSuccessful = true; + } else { + throw new Error(`Failed to delete document from Documenso: ${deleteResponse.statusText}`); + } + } else { + console.log('[Delete Generated EOI] Successfully deleted document from Documenso'); + documensoDeleteSuccessful = true; } - - console.log('[Delete Generated EOI] Successfully deleted document from Documenso'); } catch (error: any) { console.error('[Delete Generated EOI] Documenso deletion error:', error); + + // Check if it's a network error or 404 - in those cases, proceed with cleanup + if (error.message?.includes('404') || error.status === 404) { + console.log('[Delete Generated EOI] Document not found in Documenso - proceeding with database cleanup'); + documensoDeleteSuccessful = true; + } else { + throw createError({ + statusCode: 500, + statusMessage: `Failed to delete document from Documenso: ${error.message}`, + }); + } + } + + if (!documensoDeleteSuccessful) { throw createError({ statusCode: 500, - statusMessage: `Failed to delete document from Documenso: ${error.message}`, + statusMessage: 'Failed to delete document from Documenso', }); } diff --git a/server/api/eoi/validate-document.ts b/server/api/eoi/validate-document.ts new file mode 100644 index 0000000..b71cc2f --- /dev/null +++ b/server/api/eoi/validate-document.ts @@ -0,0 +1,129 @@ +import { getInterestById, updateInterest } from '~/server/utils/nocodb'; +import { getDocumesoDocument } from '~/server/utils/documeso'; +import type { InterestSalesProcessLevel, EOIStatus } from '~/utils/types'; + +export default defineEventHandler(async (event) => { + const xTagHeader = getRequestHeader(event, "x-tag"); + + console.log('[Validate Document] Request received with x-tag:', xTagHeader); + + if (!xTagHeader || (xTagHeader !== "094ut234" && xTagHeader !== "pjnvü1230")) { + console.error('[Validate Document] Authentication failed - invalid x-tag'); + throw createError({ statusCode: 401, statusMessage: "unauthenticated" }); + } + + try { + const query = getQuery(event); + const { interestId } = query; + + console.log('[Validate Document] Interest ID:', interestId); + + if (!interestId) { + console.error('[Validate Document] No interest ID provided'); + throw createError({ + statusCode: 400, + statusMessage: 'Interest ID is required', + }); + } + + // Get current interest data + const interest = await getInterestById(interestId.toString()); + if (!interest) { + throw createError({ + statusCode: 404, + statusMessage: 'Interest not found', + }); + } + + const documensoID = interest['documensoID']; + console.log('[Validate Document] Documenso ID:', documensoID); + + // If no documensoID, the state is already clean + if (!documensoID) { + console.log('[Validate Document] No documensoID found - state is clean'); + return { + success: true, + valid: true, + message: 'No document ID found - state is clean', + cleaned: false + }; + } + + // Try to fetch the document from Documenso + let documentExists = true; + let documentStatus = null; + + try { + console.log('[Validate Document] Checking document existence in Documenso'); + const document = await getDocumesoDocument(parseInt(documensoID)); + documentStatus = document.status; + console.log('[Validate Document] Document exists with status:', documentStatus); + } catch (error: any) { + console.log('[Validate Document] Document check failed:', error.message); + + // If it's a 404 or similar error, the document doesn't exist + if (error.status === 404 || error.statusCode === 404 || error.message?.includes('404')) { + documentExists = false; + console.log('[Validate Document] Document not found in Documenso - will clean up database'); + } else { + // For other errors, we'll assume the document exists but there's a connection issue + console.warn('[Validate Document] Unable to verify document existence due to API error:', error.message); + return { + success: true, + valid: true, + message: 'Unable to verify document existence due to API error', + cleaned: false, + error: error.message + }; + } + } + + // If document doesn't exist, clean up the database + if (!documentExists) { + console.log('[Validate Document] Cleaning up orphaned database records'); + + const updateData = { + 'EOI Status': 'Awaiting Further Details' as EOIStatus, + 'Sales Process Level': 'Specific Qualified Interest' as InterestSalesProcessLevel, + 'EOI Time Sent': undefined, + 'Signature Link Client': undefined, + 'Signature Link CC': undefined, + 'Signature Link Developer': undefined, + 'EmbeddedSignatureLinkClient': undefined, + 'EmbeddedSignatureLinkCC': undefined, + 'EmbeddedSignatureLinkDeveloper': undefined, + 'documensoID': undefined, + 'reminder_enabled': false + }; + + // Update the interest + await updateInterest(interestId.toString(), updateData); + + console.log('[Validate Document] Database cleanup completed'); + + return { + success: true, + valid: false, + message: 'Document not found in Documenso - database cleaned up', + cleaned: true + }; + } + + // Document exists and is valid + return { + success: true, + valid: true, + message: 'Document exists and is valid', + cleaned: false, + documentStatus + }; + + } catch (error: any) { + console.error('[Validate Document] Failed to validate document:', error); + console.error('[Validate Document] Error stack:', error.stack); + throw createError({ + statusCode: error.statusCode || 500, + statusMessage: error.statusMessage || error.message || 'Failed to validate document', + }); + } +});