/** * SIMInventoryImporter * Invocable Apex class for importing Physical SIM inventory from CSV files. * Used by Screen Flow to allow employees to bulk import physical SIMs. * * CSV Format Expected (matching ASI_N6_PASI_*.csv): * Row,Phone_Number,PT_Number,OEM_ID,Batch_Date,,,,, * 1,02000002470001,PT0220024700010,PASI,20251229,,,, * * Note: No header row expected. All imports are Physical SIM type. * * @author Customer Portal Team * @version 1.1 */ public with sharing class SIMInventoryImporter { // Hardcoded values for Physical SIM imports private static final String SIM_TYPE = 'Physical SIM'; private static final Boolean SKIP_HEADER = false; /** * Input wrapper for the invocable method */ public class ImportRequest { @InvocableVariable(label='Content Document IDs' description='IDs from File Upload component' required=true) public List contentDocumentIds; } /** * Output wrapper for the invocable method */ public class ImportResult { @InvocableVariable(label='Success') public Boolean success; @InvocableVariable(label='Records Created') public Integer recordsCreated; @InvocableVariable(label='Records Failed') public Integer recordsFailed; @InvocableVariable(label='Error Messages') public String errorMessages; @InvocableVariable(label='Summary Message') public String summaryMessage; } /** * Main invocable method called by Flow */ @InvocableMethod(label='Import SIM Inventory from CSV' description='Parses CSV content and creates SIM_Inventory__c records' category='SIM Management') public static List importFromCSV(List requests) { List results = new List(); for (ImportRequest request : requests) { results.add(processCSV(request)); } return results; } /** * Process a single CSV import request */ private static ImportResult processCSV(ImportRequest request) { ImportResult result = new ImportResult(); result.success = true; result.recordsCreated = 0; result.recordsFailed = 0; result.errorMessages = ''; try { // Get the first Content Document ID from the list if (request.contentDocumentIds == null || request.contentDocumentIds.isEmpty()) { result.success = false; result.errorMessages = 'No file was uploaded. Please select a CSV file.'; result.summaryMessage = 'Import failed: No file uploaded'; return result; } String contentDocumentId = request.contentDocumentIds[0]; // Retrieve file content from ContentVersion List cvList = [ SELECT VersionData FROM ContentVersion WHERE ContentDocumentId = :contentDocumentId AND IsLatest = true LIMIT 1 ]; if (cvList.isEmpty()) { result.success = false; result.errorMessages = 'Could not find the uploaded file. Please try again.'; result.summaryMessage = 'Import failed: File not found'; return result; } String csvContent = cvList[0].VersionData.toString(); // Parse CSV content List lines = csvContent.split('\n'); List simsToInsert = new List(); List errors = new List(); // Start from first row (no header row in Physical SIM CSV files) Integer startIndex = SKIP_HEADER ? 1 : 0; // Collect existing phone numbers to check for duplicates Set existingPhoneNumbers = new Set(); for (SIM_Inventory__c existing : [SELECT Phone_Number__c FROM SIM_Inventory__c WHERE Phone_Number__c != null]) { existingPhoneNumbers.add(existing.Phone_Number__c); } Set phoneNumbersInBatch = new Set(); for (Integer i = startIndex; i < lines.size(); i++) { String line = lines[i].trim(); // Skip empty lines if (String.isBlank(line)) { continue; } // Remove carriage return if present (Windows line endings) line = line.replace('\r', ''); try { // Parse CSV line List columns = parseCSVLine(line); // Expected format: Row,Phone_Number,PT_Number,OEM_ID,Batch_Date,,,,, if (columns.size() < 2) { errors.add('Row ' + (i + 1) + ': Not enough columns (need at least phone number)'); result.recordsFailed++; continue; } String phoneNumber = columns.size() > 1 ? columns[1].trim() : ''; String ptNumber = columns.size() > 2 ? columns[2].trim() : ''; String oemId = columns.size() > 3 ? columns[3].trim() : ''; String batchDateStr = columns.size() > 4 ? columns[4].trim() : ''; // Validate phone number if (String.isBlank(phoneNumber)) { errors.add('Row ' + (i + 1) + ': Phone number is empty'); result.recordsFailed++; continue; } // Check for duplicates in database if (existingPhoneNumbers.contains(phoneNumber)) { errors.add('Row ' + (i + 1) + ': Phone number ' + phoneNumber + ' already exists in database'); result.recordsFailed++; continue; } // Check for duplicates within the CSV if (phoneNumbersInBatch.contains(phoneNumber)) { errors.add('Row ' + (i + 1) + ': Duplicate phone number ' + phoneNumber + ' in CSV file'); result.recordsFailed++; continue; } // Parse batch date (format: YYYYMMDD) Date batchDate = null; if (String.isNotBlank(batchDateStr) && batchDateStr.length() >= 8) { try { Integer year = Integer.valueOf(batchDateStr.substring(0, 4)); Integer month = Integer.valueOf(batchDateStr.substring(4, 6)); Integer day = Integer.valueOf(batchDateStr.substring(6, 8)); batchDate = Date.newInstance(year, month, day); } catch (Exception e) { // Leave as null if parsing fails - not critical } } // Create SIM_Inventory__c record SIM_Inventory__c sim = new SIM_Inventory__c(); sim.Phone_Number__c = phoneNumber; sim.PT_Number__c = ptNumber; sim.OEM_ID__c = oemId; sim.Batch_Date__c = batchDate; sim.Status__c = 'Available'; sim.SIM_Type__c = SIM_TYPE; // Always Physical SIM sim.Name = phoneNumber; // Use phone number as name for easy identification simsToInsert.add(sim); phoneNumbersInBatch.add(phoneNumber); } catch (Exception e) { errors.add('Row ' + (i + 1) + ': ' + e.getMessage()); result.recordsFailed++; } } // Insert records with partial success allowed if (!simsToInsert.isEmpty()) { Database.SaveResult[] saveResults = Database.insert(simsToInsert, false); for (Integer i = 0; i < saveResults.size(); i++) { if (saveResults[i].isSuccess()) { result.recordsCreated++; } else { result.recordsFailed++; for (Database.Error err : saveResults[i].getErrors()) { errors.add('Insert error for ' + simsToInsert[i].Phone_Number__c + ': ' + err.getMessage()); } } } } // Build error message string (limit to first 10 errors for readability) if (!errors.isEmpty()) { if (errors.size() <= 10) { result.errorMessages = String.join(errors, '\n'); } else { List firstTen = new List(); for (Integer i = 0; i < 10; i++) { firstTen.add(errors[i]); } result.errorMessages = String.join(firstTen, '\n') + '\n\n... and ' + (errors.size() - 10) + ' more errors'; } } // Build summary message result.summaryMessage = 'Import completed: ' + result.recordsCreated + ' records created successfully.'; if (result.recordsFailed > 0) { result.summaryMessage += ' ' + result.recordsFailed + ' records failed.'; result.success = (result.recordsCreated > 0); // Partial success if any records created } } catch (Exception e) { result.success = false; result.errorMessages = 'Critical error: ' + e.getMessage() + '\n\nStack trace: ' + e.getStackTraceString(); result.summaryMessage = 'Import failed due to an unexpected error.'; } return result; } /** * Parse a single CSV line, handling quoted fields properly */ private static List parseCSVLine(String line) { List result = new List(); Boolean inQuotes = false; String currentField = ''; for (Integer i = 0; i < line.length(); i++) { String c = line.substring(i, i + 1); if (c == '"') { inQuotes = !inQuotes; } else if (c == ',' && !inQuotes) { result.add(currentField); currentField = ''; } else { currentField += c; } } // Add the last field result.add(currentField); return result; } }