diff --git a/artifacts/doc_inspect/downloaded.docx b/artifacts/doc_inspect/downloaded.docx new file mode 100644 index 0000000..9ebd491 --- /dev/null +++ b/artifacts/doc_inspect/downloaded.docx @@ -0,0 +1 @@ +ù¾¸²æì¶¸§‚'§t‡q \ No newline at end of file diff --git a/docs/AppraiserReviewLetter_Template.docx b/docs/AppraiserReviewLetter_Template.docx index b64c79f..d84fdb7 100644 Binary files a/docs/AppraiserReviewLetter_Template.docx and b/docs/AppraiserReviewLetter_Template.docx differ diff --git a/force-app/main/default/classes/CLMDocGenCallout.cls b/force-app/main/default/classes/CLMDocGenCallout.cls index d281801..6c93912 100644 --- a/force-app/main/default/classes/CLMDocGenCallout.cls +++ b/force-app/main/default/classes/CLMDocGenCallout.cls @@ -112,20 +112,25 @@ public class CLMDocGenCallout { private static String buildDataXml(Map payload) { String xml = ''; + // Emit flat fields first for (String key : payload.keySet()) { if (key == 'DeficiencyList') continue; xml += '<' + key + '>' + escapeXml(String.valueOf(payload.get(key))) + ''; } + // Emit DeficiencyList as a nested list so templates can iterate dynamically List deficiencies = (List) payload.get('DeficiencyList'); - if (deficiencies != null) { + if (deficiencies != null && !deficiencies.isEmpty()) { + xml += ''; for (Integer i = 0; i < deficiencies.size(); i++) { Map d = (Map) deficiencies[i]; - String p = 'Deficiency_' + (i + 1) + '_'; - xml += '<' + p + 'Number>' + escapeXml(String.valueOf(d.get('deficiencyNumber'))) + ''; - xml += '<' + p + 'Description>' + escapeXml(String.valueOf(d.get('description'))) + ''; - xml += '<' + p + 'Resolution>' + escapeXml(String.valueOf(d.get('resolution'))) + ''; + xml += ''; + xml += '' + escapeXml(String.valueOf(d.get('deficiencyNumber'))) + ''; + xml += '' + escapeXml(String.valueOf(d.get('description'))) + ''; + xml += '' + escapeXml(String.valueOf(d.get('resolution'))) + ''; + xml += ''; } + xml += ''; xml += '' + deficiencies.size() + ''; } diff --git a/force-app/main/default/namedCredentials/CLMuatDownload.namedCredential-meta.xml b/force-app/main/default/namedCredentials/CLMuatDownload.namedCredential-meta.xml new file mode 100644 index 0000000..d418ff2 --- /dev/null +++ b/force-app/main/default/namedCredentials/CLMuatDownload.namedCredential-meta.xml @@ -0,0 +1,19 @@ + + + false + false + Enabled + true + + + Url + Url + https://apidownloaduatna11.springcm.com + + + DocusignJWT + ExternalCredential + Authentication + + SecuredEndpoint + diff --git a/force-app/main/default/namedCredentials/CLMuatDownloadNamedCreds.namedCredential-meta.xml b/force-app/main/default/namedCredentials/CLMuatDownloadNamedCreds.namedCredential-meta.xml new file mode 100644 index 0000000..56f5031 --- /dev/null +++ b/force-app/main/default/namedCredentials/CLMuatDownloadNamedCreds.namedCredential-meta.xml @@ -0,0 +1,19 @@ + + + false + false + Enabled + true + + + Url + Url + https://apidownloaduatna11.springcm.com + + + DocusignJWT + ExternalCredential + Authentication + + SecuredEndpoint + diff --git a/scripts/compare_apex_postman_json.py b/scripts/compare_apex_postman_json.py new file mode 100644 index 0000000..0fb71d9 --- /dev/null +++ b/scripts/compare_apex_postman_json.py @@ -0,0 +1,99 @@ +#!/usr/bin/env python3 +import json, difflib + +postman_json_text = '''{ + "TemplateDocument": { + "Href": "https://apiuatna11.springcm.com/v2/bccae332-c7db-4892-ab85-257df0f70fea/documents/a0cbc0e6-d87d-459e-8d63-66baa47878f3" }, + "DataXML": "AC-0000102/04/2026123 Main St, Denver, CO 802021Missing comparable sale adjustment detail.Added adjustment rationale and supporting calculations.2Neighborhood trend explanation insufficient.Expanded market trend narrative with MLS evidence.3Photo date stamps were not included.Re-uploaded photos with date metadata and captions.3", + "DestinationDocumentName": "Review_AC-00001.docx", + "DestinationFolder": { + "Href": "https://apiuatna11.springcm.com/v2/bccae332-c7db-4892-ab85-257df0f70fea/folders/12220442-b12e-f111-84fc-88e9a4bd0d9c" + } +} +''' + +# Parse the Postman JSON +postman = json.loads(postman_json_text) + +# Reconstruct the Apex payload (simulate AppraiserCasePayloadBuilder output) +payload = { + 'AppraiserCaseNumber': 'AC-00001', + 'AppraiserFieldReviewDate': '02/04/2026', + 'PropertyAddress': '123 Main St, Denver, CO 80202', + 'DeficiencyList': [ + {'deficiencyNumber': '1', 'description': 'Missing comparable sale adjustment detail.', 'resolution': 'Added adjustment rationale and supporting calculations.'}, + {'deficiencyNumber': '2', 'description': 'Neighborhood trend explanation insufficient.', 'resolution': 'Expanded market trend narrative with MLS evidence.'}, + {'deficiencyNumber': '3', 'description': 'Photo date stamps were not included.', 'resolution': 'Re-uploaded photos with date metadata and captions.'} + ] +} + +# Function to mimic buildDataXml from Apex implementation +def escape_xml(s): + if s is None: + return '' + return s.replace('&','&').replace('<','<').replace('>','>').replace('"','"').replace("'","'") + +def build_data_xml(payload): + xml = '' + # Emit flat fields first (Apex iterates payload.keySet() which is not ordered in Python dict, but we'll follow this order) + # We'll follow the same ordering as in Postman: AppraiserCaseNumber, AppraiserFieldReviewDate, PropertyAddress + for key in ['AppraiserCaseNumber','AppraiserFieldReviewDate','PropertyAddress']: + v = payload.get(key) + xml += f'<{key}>' + escape_xml(str(v) if v is not None else '') + f'' + + # Emit DeficiencyList nested structure + deficiencies = payload.get('DeficiencyList') + if deficiencies: + xml += '' + for d in deficiencies: + xml += '' + xml += '' + escape_xml(str(d.get('deficiencyNumber'))) + '' + xml += '' + escape_xml(str(d.get('description'))) + '' + xml += '' + escape_xml(str(d.get('resolution'))) + '' + xml += '' + xml += '' + xml += '' + str(len(deficiencies)) + '' + xml += '' + return xml + +apex_data_xml = build_data_xml(payload) + +apex_request = { + 'TemplateDocument': {'Href': postman['TemplateDocument']['Href']}, + 'DataXML': apex_data_xml, + 'DestinationDocumentName': postman['DestinationDocumentName'], + 'DestinationFolder': {'Href': postman['DestinationFolder']['Href']} +} + +# Compare postman vs apex +print('Comparing top-level fields:') +for key in ['TemplateDocument','DestinationDocumentName','DestinationFolder']: + p = postman.get(key) + a = apex_request.get(key) + equal = (p == a) + print(f' - {key}:', 'MATCH' if equal else 'DIFFER') + +# Compare DataXML +postman_xml = postman['DataXML'] +apex_xml = apex_request['DataXML'] + +if postman_xml == apex_xml: + print('\nDataXML: EXACT MATCH') +else: + print('\nDataXML: DIFFER') + # show diff + pd = postman_xml.split('><') + ad = apex_xml.split('><') + diff = difflib.unified_diff(pd, ad, fromfile='postman_xml', tofile='apex_xml', lineterm='') + print('\n'.join(diff)) + # also show small head/tail + print('\n-- Postman head 200 chars --\n', postman_xml[:200]) + print('\n-- Apex head 200 chars --\n', apex_xml[:200]) + +# Additionally print whether other top-level objects match (TemplateDocument Href and Folder Href) +print('\nTemplateDocument.Href match:', postman['TemplateDocument']['Href'] == apex_request['TemplateDocument']['Href']) +print('DestinationFolder.Href match:', postman['DestinationFolder']['Href'] == apex_request['DestinationFolder']['Href']) + +# Print apex_data_xml for inspection +print('\n--- Apex DataXML ---\n') +print(apex_data_xml) diff --git a/scripts/reconstruct_and_inspect.py b/scripts/reconstruct_and_inspect.py new file mode 100644 index 0000000..fec8a54 --- /dev/null +++ b/scripts/reconstruct_and_inspect.py @@ -0,0 +1,100 @@ +#!/usr/bin/env python3 +import os, re, base64, zipfile, xml.etree.ElementTree as ET, sys +log_path = '/home/paulh/.vscode-server/data/User/workspaceStorage/79b924110cb5ff6de49811d445e59969-1/GitHub.copilot-chat/chat-session-resources/90e6dae0-2184-412b-af0b-eac258be98c5/call_gY8uUyzuGvFiN46d4ZPVFtjz__vscode-1775271381281/content.txt' +out_dir = 'artifacts/doc_inspect' +os.makedirs(out_dir, exist_ok=True) +start=False +chunks = [] +with open(log_path, 'r', errors='replace') as f: + for line in f: + if 'BASE64_BEGIN' in line: + start = True + continue + if 'BASE64_END' in line: + break + if start: + if 'BASE64_CHUNK:' in line: + parts = line.split('BASE64_CHUNK:',1)[1].strip() + chunks.append(parts) + else: + # fallback: if line looks like base64 (long and only base64 chars + =), take it + s = line.strip() + if len(s) > 100 and re.fullmatch(r'[A-Za-z0-9+/=\n\r]+', s): + chunks.append(s) + +if not chunks: + print('ERROR: no base64 chunks found in log at', log_path) + sys.exit(2) + +b64 = ''.join(chunks) +# sanitize (remove any DEBUG prefixes that snuck in) +b64 = re.sub(r'\s+', '', b64) +try: + data = base64.b64decode(b64) +except Exception as e: + print('ERROR decoding base64:', e) + sys.exit(3) + +docx_path = os.path.join(out_dir, 'downloaded.docx') +with open(docx_path, 'wb') as f: + f.write(data) +print('WROTE_DOCX:', docx_path, 'size=', os.path.getsize(docx_path)) + +# unzip +unzip_dir = os.path.join(out_dir, 'unzipped') +os.makedirs(unzip_dir, exist_ok=True) +try: + with zipfile.ZipFile(docx_path, 'r') as z: + z.extractall(unzip_dir) +except Exception as e: + print('ERROR unzipping docx:', e) + sys.exit(4) + +doc_xml = os.path.join(unzip_dir, 'word', 'document.xml') +if not os.path.exists(doc_xml): + print('ERROR: word/document.xml not found in the docx') + sys.exit(5) + +# parse XML and extract tables +ns = {'w':'http://schemas.openxmlformats.org/wordprocessingml/2006/main'} +ET.register_namespace('w', ns['w']) +try: + tree = ET.parse(doc_xml) + root = tree.getroot() +except Exception as e: + print('ERROR parsing document.xml:', e) + sys.exit(6) + +tables = root.findall('.//w:tbl', ns) +print('TABLE_COUNT:', len(tables)) +# For each table, collect row texts (limit output to first 5 tables and 20 rows each) +found_def_texts = [] +for ti, tbl in enumerate(tables[:5], start=1): + rows = tbl.findall('.//w:tr', ns) + print('\n--- TABLE', ti, 'rows=', len(rows), '---') + for ri, tr in enumerate(rows[:20], start=1): + texts = [t.text for t in tr.findall('.//w:t', ns) if t.text] + joined = ' | '.join(texts).strip() + if joined: + print('ROW %d:'%ri, repr(joined)) + # heuristic: look for keywords + if any(k.lower() in joined.lower() for k in ('deficiency','description','defect','ac-','AC-','DeficiencyList')): + found_def_texts.append(joined) + else: + print('ROW %d: '%ri) + +# Also search whole document.xml for certain keywords +full_xml = open(doc_xml,'r',encoding='utf-8',errors='replace').read() +keywords = ['Deficiency','DeficiencyList','Description','