0

Whilst trying to deploy an Apex class and test to our production org, I get this error without any context. The class runs fine in sandbox and the test passes with 92% coverage.

✘ Deploying Metadata 1m 8.71s ▸ Components: 1/1 (100%)

Test Results Summary
Passing: 0
Failing: 0
Total: 0
Error (10): stringWidth is not a function

This 'stringWidth' variable or method is not used or referred to anywhere in this class or any of the other classes being called, or even in our entire repo for that matter.

I'm wondering if it's part of the Regex or something that I've used to manipulate strings in some places, however I've done some testing by commenting out these blocks of code and it still gives the same error.

So I'm stumped. Any ideas greatly appreciated.

Here's the class:

global class HSEDataScraperBatch implements Database.Batchable<String>, Database.AllowsCallouts {
    global List<String> allCaseNumbers = new List<String>();
    global List<String> companyMatchers = new List<String>{
        'Limited', 'Ltd', 'Council', 'Association', 'Trust', 'Society','University', 'College', 'School','Institute', 
        'Academy', 'Centre','Group', 'Partnership', 'Services','Service', 'Company', 'Corporation','Contractor', '('
    };
    

    global Iterable<String> start(Database.BatchableContext BC) {
        String url = 'https://resources.hse.gov.uk/convictions/breach/breach_list.asp?PN=1&ST=B&SO=DHD';
        String htmlBody = makeHttpCall(url);

        if (String.isNotBlank(htmlBody)) {
            Integer totalPages = getTotalPages(htmlBody);

            for (Integer page = 1; page <= totalPages; page++) {
                String paginatedUrl = url.replace('PN=1', 'PN=' + page);
                String paginatedHtml = makeHttpCall(paginatedUrl);

                if (String.isNotBlank(paginatedHtml)) {
                    allCaseNumbers.addAll(extractFirstColumnValues(paginatedHtml));
                }
            }
        }

        if(Test.isRunningTest()) {
            Integer maxCasesInTest = 20;
            List<String> limitedCaseNumbers = new List<String>();
            for (Integer i = 0; i < Math.min(maxCasesInTest, allCaseNumbers.size()); i++) {
                limitedCaseNumbers.add(allCaseNumbers[i]);
            }
            return limitedCaseNumbers;
        }

        return allCaseNumbers;
    }

    global void execute(Database.BatchableContext BC, List<String> caseNumbers) {

        Set<String> existingCaseNumbers = new Set<String>();
        Set<String> newCaseNumbers = new Set<String>();
        List<Lead> leadsToEnrich = new List<Lead>();
        List<Lead> newLeads = new List<Lead>();

        for (Lead lead : [SELECT HSE_Case_No__c FROM Lead WHERE HSE_Case_No__c != null]) {
            existingCaseNumbers.add(lead.HSE_Case_No__c);
        }

        for (String caseNo : caseNumbers) {
            String simplifiedCaseNo = caseNo.substringBefore('/');
            if (!newCaseNumbers.contains(simplifiedCaseNo) && !existingCaseNumbers.contains(simplifiedCaseNo)) {
                String detailsUrl = 'https://resources.hse.gov.uk/convictions/breach/breach_details.asp?SF=BID&SV=' + caseNo.replace('/', '');
                String detailsHtml = makeHttpCall(detailsUrl);
                newCaseNumbers.add(simplifiedCaseNo);
                if (String.isNotBlank(detailsHtml)) {
                    CaseDetails caseDetails = extractCaseDetails(detailsHtml, simplifiedCaseNo);
                    Lead newLead = (createLead(caseDetails));
                    // Seperating the leads with and without postcode/company, then merging them for insert. To avoid modifying the enrichment class, as other classes use it.
                    if(newLead.PostalCode != null && newLead.Company != null) {
                        leadsToEnrich.add(newLead);
                    }
                    else {
                        newLeads.add(newLead);
                    }
                }
            }
        }

        if (!leadsToEnrich.isEmpty()) {
            List<Lead> enrichedLeads = GooglePlacesLeadEnrichment.googlePlacesSearch(leadsToEnrich);
            newLeads.addAll(enrichedLeads);
            insert newLeads;
        }
        else if (!newLeads.isEmpty()) {
            insert newLeads;
        }
        
    }

    global void finish(Database.BatchableContext BC) {
        System.debug('Batch job completed.');
    }
    
    private String makeHttpCall(String url) {
        try {
            Http http = new Http();
            HttpRequest request = new HttpRequest();
            request.setEndpoint(url);
            request.setMethod('GET');
            HttpResponse response = http.send(request);

            if (response.getStatusCode() == 200) {
                return response.getBody();
            } else {
                System.debug('HTTP call failed with status code: ' + response.getStatusCode());
            }
        } catch (Exception e) {
            System.debug('Exception during HTTP call: ' + e.getMessage());
        }
        return null;
    }

    private Integer getTotalPages(String html) {
        Integer totalPages = 0;
        String pageTextStart = 'Showing Page ';
        String pageTextEnd = ', results';

        Integer startIdx = html.indexOf(pageTextStart);
        Integer endIdx = html.indexOf(pageTextEnd, startIdx);

        if (startIdx != -1 && endIdx != -1) {
            String pageText = html.substring(startIdx, endIdx);
            Integer ofIdx = pageText.indexOf('of ');
            if (ofIdx != -1) {
                String totalPagesText = pageText.substring(ofIdx + 3).trim();
                totalPages = Integer.valueOf(totalPagesText);
            }
        }
        return totalPages;
    }

    private List<String> extractFirstColumnValues(String htmlBody) {
        List<String> firstColumnValues = new List<String>();
        Integer startIdx = htmlBody.indexOf('<table');
        Integer endIdx = htmlBody.indexOf('</table>', startIdx) + 8;

        if (startIdx != -1 && endIdx != -1) {
            String tableHtml = htmlBody.substring(startIdx, endIdx);
            List<String> rows = tableHtml.split('<tr>');
            for (String row : rows) {
                if (row.contains('<td>')) {
                    Integer tdStart = row.indexOf('<td>');
                    Integer tdEnd = row.indexOf('</td>', tdStart);
                    if (tdStart != -1 && tdEnd != -1) {
                        String cellData = row.substring(tdStart + 4, tdEnd).replaceAll('<.*?>', '').trim();
                        if (!cellData.contains('&')) {
                            firstColumnValues.add(cellData);
                        }
                    }
                }
            }
        }
        return firstColumnValues;
    }

    private CaseDetails extractCaseDetails(String html, String caseNo) {
        CaseDetails caseDetails = new CaseDetails();
        caseDetails.caseNumber = caseNo;
        caseDetails.defendant = extractValue(html, '<td><strong>Defendant</strong></td>\\s*<td[^>]*>([\\s\\S]*?)</td>');
        caseDetails.courtName = extractValue(html, '<td><strong>Court Name</strong></td>\\s*<td[^>]*>([\\s\\S]*?)</td>');
        caseDetails.courtLevel = extractValue(html, '<td><strong>Court Level</strong></td>\\s*<td[^>]*>([\\s\\S]*?)</td>');
        caseDetails.act = extractValue(html, '<td><strong>Act</strong></td>\\s*<td[^>]*>([\\s\\S]*?)</td>');
        caseDetails.regulation = extractValue(html, '<td><strong>Regulation</strong></td>\\s*<td[^>]*>([\\s\\S]*?)</td>');
        caseDetails.dateOfHearing = extractValue(html, '<td><strong>Date of Hearing</strong></td>\\s*<td[^>]*>([\\s\\S]*?)</td>');
        caseDetails.result = extractValue(html, '<td><strong>Result</strong></td>\\s*<td[^>]*>([\\s\\S]*?)</td>');
        caseDetails.fine = extractValue(html, '<td><strong>Fine</strong></td>\\s*<td[^>]*>([\\s\\S]*?)</td>');
        caseDetails.industry = extractValue(html, '<td><strong>Industry</strong></td>\\s*<td[^>]*>([\\s\\S]*?)</td>');
        caseDetails.address = extractValue(html, '<td rowspan="5"><strong>Address</strong></td>\\s*<td rowspan="5">([\\s\\S]*?)</td>').replace('<BR>', ', ');

        return caseDetails;
    }

    private Lead createLead(CaseDetails caseDetails) {
        Lead newLead = new Lead();
        
        if (companyMatchers.contains(caseDetails.defendant)
        ) {
            newLead.LastName = 'Not Known'; 
        } else {
            List<String> nameParts = caseDetails.defendant.split(' ');

            if (nameParts.size() > 1) {
                newLead.FirstName = nameParts[0]; 
                newLead.LastName = nameParts[nameParts.size() - 1]; 
            } else {
                newLead.LastName = caseDetails.defendant;
            }
        }

        if (caseDetails.address != null) {
            newLead.PostalCode = extractValue(caseDetails.address, '([A-Z]{1,2}[0-9R][0-9A-Z]? [0-9][A-Z]{2})');
        }

        newLead.Company = caseDetails.defendant.replace('&amp;', '&');
        newLead.HSE_Case_No__c = caseDetails.caseNumber;
        newLead.LeadSource = 'Health & Safety Executive';
        newLead.Industry = caseDetails.industry != null ? caseDetails.industry : '';
        newLead.Description = 'Case No: ' + caseDetails.caseNumber + '\n' +
            'Court Name: ' + (caseDetails.courtName != null ? caseDetails.courtName : '') + '\n' +
            'Court Level: ' + (caseDetails.courtLevel != null ? caseDetails.courtLevel : '') + '\n' +
            'Act/Regulation: ' + (caseDetails.act != null ? caseDetails.act : caseDetails.regulation != null ? caseDetails.regulation : '') + '\n' +  
            'Date of Hearing: ' + caseDetails.dateOfHearing + '\n' +
            'Result: ' + caseDetails.result + '\n' +
            'Fine: ' + caseDetails.fine + '\n';

        return newLead;
    }

    private String extractValue(String html, String regex) {
        Pattern pattern = Pattern.compile(regex);
        Matcher matcher = pattern.matcher(html);
        if (matcher.find()) {
            return matcher.group(1).trim().replaceAll('<.*?>', '');
        }
        return null;
    }

    global class CaseDetails {
        public String caseNumber;
        public String defendant;
        public String courtName;
        public String courtLevel;
        public String act;
        public String regulation;
        public String dateOfHearing;
        public String result;
        public String fine;
        public String industry;
        public String address;
    }
}
2
  • Just an update on this: My colleague tried to deploy and it went through no problems. I made a couple of changes then redeployed and it also went through fine. What gives? Commented Feb 6 at 9:53
  • I encountered this deployment error again today. Just updating the Salesforce CLI fixed it. Commented Apr 11 at 10:54

0

You must log in to answer this question.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.