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
This commit is contained in:
parent
b25e93d2a0
commit
41a6f7f1c8
|
|
@ -520,6 +520,7 @@ const isDeleting = ref(false);
|
||||||
const isDeletingGenerated = ref(false);
|
const isDeletingGenerated = ref(false);
|
||||||
const signatureStatus = ref<any>(null);
|
const signatureStatus = ref<any>(null);
|
||||||
const isCheckingSignatures = ref(false);
|
const isCheckingSignatures = ref(false);
|
||||||
|
const isValidatingDocument = ref(false);
|
||||||
|
|
||||||
const hasGeneratedEOI = computed(() => {
|
const hasGeneratedEOI = computed(() => {
|
||||||
// Primary check: documensoID must exist for a generated EOI
|
// 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
|
// Check signature status on mount if EOI is generated
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
if (hasGeneratedEOI.value && !isEOISigned.value) {
|
if (hasGeneratedEOI.value) {
|
||||||
checkSignatureStatus();
|
// First validate the document exists
|
||||||
|
validateDocument().then(() => {
|
||||||
|
// Only check signature status if document is still valid after validation
|
||||||
|
if (hasGeneratedEOI.value && !isEOISigned.value) {
|
||||||
|
checkSignatureStatus();
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
import { getDocumesoDocumentByExternalId, checkDocumentSignatureStatus } from '~/server/utils/documeso';
|
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) => {
|
export default defineEventHandler(async (event) => {
|
||||||
const xTagHeader = getRequestHeader(event, "x-tag");
|
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 we have a stored documensoID, use it directly
|
||||||
if (interest && interest['documensoID']) {
|
if (interest && interest['documensoID']) {
|
||||||
console.log('[check-signature-status] Using stored documensoID:', interest['documensoID']);
|
console.log('[check-signature-status] Using stored documensoID:', interest['documensoID']);
|
||||||
const status = await checkDocumentSignatureStatus(parseInt(interest['documensoID']));
|
try {
|
||||||
return {
|
const status = await checkDocumentSignatureStatus(parseInt(interest['documensoID']));
|
||||||
success: true,
|
return {
|
||||||
documentId: parseInt(interest['documensoID']),
|
success: true,
|
||||||
...status
|
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
|
// Otherwise, try to find by external ID (using interestId) - fallback method
|
||||||
|
|
|
||||||
|
|
@ -74,6 +74,8 @@ export default defineEventHandler(async (event) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log('[Delete Generated EOI] Deleting document from Documenso');
|
console.log('[Delete Generated EOI] Deleting document from Documenso');
|
||||||
|
let documensoDeleteSuccessful = false;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const deleteResponse = await fetch(`${documensoBaseUrl}/api/v1/documents/${documensoID}`, {
|
const deleteResponse = await fetch(`${documensoBaseUrl}/api/v1/documents/${documensoID}`, {
|
||||||
method: 'DELETE',
|
method: 'DELETE',
|
||||||
|
|
@ -86,15 +88,37 @@ export default defineEventHandler(async (event) => {
|
||||||
if (!deleteResponse.ok) {
|
if (!deleteResponse.ok) {
|
||||||
const errorText = await deleteResponse.text();
|
const errorText = await deleteResponse.text();
|
||||||
console.error('[Delete Generated EOI] Documenso deletion failed:', errorText);
|
console.error('[Delete Generated EOI] Documenso deletion failed:', errorText);
|
||||||
throw new Error(`Failed to delete document from Documenso: ${deleteResponse.statusText}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log('[Delete Generated EOI] Successfully deleted document from Documenso');
|
// 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;
|
||||||
|
}
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
console.error('[Delete Generated EOI] Documenso deletion error:', error);
|
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({
|
throw createError({
|
||||||
statusCode: 500,
|
statusCode: 500,
|
||||||
statusMessage: `Failed to delete document from Documenso: ${error.message}`,
|
statusMessage: 'Failed to delete document from Documenso',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
Loading…
Reference in New Issue