From 1e532029fa3324a8f139214f4411cc6dee5ae957 Mon Sep 17 00:00:00 2001 From: paulh Date: Sun, 5 Apr 2026 14:22:57 -0400 Subject: [PATCH] Add Appraiser Review Letter template and XML merge data --- artifacts/doc_inspect/downloaded.docx | 1 + docs/AppraiserReviewLetter_Template.docx | Bin 37243 -> 37137 bytes .../main/default/classes/CLMDocGenCallout.cls | 15 ++- .../CLMuatDownload.namedCredential-meta.xml | 19 ++++ ...ownloadNamedCreds.namedCredential-meta.xml | 19 ++++ scripts/compare_apex_postman_json.py | 99 +++++++++++++++++ scripts/reconstruct_and_inspect.py | 100 ++++++++++++++++++ 7 files changed, 248 insertions(+), 5 deletions(-) create mode 100644 artifacts/doc_inspect/downloaded.docx create mode 100644 force-app/main/default/namedCredentials/CLMuatDownload.namedCredential-meta.xml create mode 100644 force-app/main/default/namedCredentials/CLMuatDownloadNamedCreds.namedCredential-meta.xml create mode 100644 scripts/compare_apex_postman_json.py create mode 100644 scripts/reconstruct_and_inspect.py 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 b64c79fff0bd91fc5d9f19257677a9e93dc0355e..d84fdb727422042c52606472f02593d16abf5cee 100644 GIT binary patch delta 1590 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 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','