272 lines
11 KiB
OpenEdge ABL
272 lines
11 KiB
OpenEdge ABL
|
|
/**
|
||
|
|
* 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<String> 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<ImportResult> importFromCSV(List<ImportRequest> requests) {
|
||
|
|
List<ImportResult> results = new List<ImportResult>();
|
||
|
|
|
||
|
|
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<ContentVersion> 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<String> lines = csvContent.split('\n');
|
||
|
|
List<SIM_Inventory__c> simsToInsert = new List<SIM_Inventory__c>();
|
||
|
|
List<String> errors = new List<String>();
|
||
|
|
|
||
|
|
// 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<String> existingPhoneNumbers = new Set<String>();
|
||
|
|
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<String> phoneNumbersInBatch = new Set<String>();
|
||
|
|
|
||
|
|
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<String> 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<String> firstTen = new List<String>();
|
||
|
|
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<String> parseCSVLine(String line) {
|
||
|
|
List<String> result = new List<String>();
|
||
|
|
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;
|
||
|
|
}
|
||
|
|
}
|