From 61141b837ed897d6ef336a6e9d28f805b9215edb Mon Sep 17 00:00:00 2001 From: AAladeen <114360717+AAladeen@users.noreply.github.com> Date: Fri, 24 Oct 2025 10:04:23 +0200 Subject: [PATCH 1/2] Add Commitments to Offerings This BG script loops throw all services and offerings in defined Service Portfolio and add Commitments selected in the array. The Names of these Commitments are first checked for existence. The script offers Dry Run capability to check how many updates will it perform. It can be easily tweaked to run it not on All services in portfolio, but for each service, when we need different commitments on some services/offerings. --- .../Add Commitments to Offerings | 500 ++++++++++++++++++ 1 file changed, 500 insertions(+) create mode 100644 Server-Side Components/Background Scripts/Add Commitments to Offerings diff --git a/Server-Side Components/Background Scripts/Add Commitments to Offerings b/Server-Side Components/Background Scripts/Add Commitments to Offerings new file mode 100644 index 0000000000..b565116148 --- /dev/null +++ b/Server-Side Components/Background Scripts/Add Commitments to Offerings @@ -0,0 +1,500 @@ +// ============================================================================ +// ServiceNow Background Script +// Purpose: Loop through portfolio services, get offerings, and add service +// commitments with dry-run and validation +// ============================================================================ + +(function() { + + // ======================================================================== + // CONFIGURATION SECTION - UPDATE THESE VALUES + // ======================================================================== + + var CONFIG = { + // REQUIRED: Portfolio System ID + // Example: '8c968a94877a6650cc6b1fc83cbb3573' + // Where to find: Navigate to Service Portfolio → Copy sys_id from spm_service_portfolio.list + PORTFOLIO_SYS_ID: '8c968a94877a6650cc6b1fc83cbb3573', + + // REQUIRED: Service Commitments to Add (array of names) + // Example: ['INC P4 Response (1h)', 'INC P4 Resolution (40h)'] + // Where to find: Service Commitment table → copy exact names + SERVICE_COMMITMENTS_TO_ADD: [ + 'INC P4 Response (1h)', + 'INC P4 Resolution (40h)' + ], + + // OPTIONAL: Enable dry-run mode (no database updates) + // true = simulate only, false = execute and save + DRY_RUN: true, + + // OPTIONAL: Filter by operational status + // Set to true to only process operational services/offerings + FILTER_OPERATIONAL_ONLY: false, + + // OPTIONAL: Skip existing commitment relationships + // true = skip if already linked, false = attempt update + SKIP_EXISTING: true + }; + + // ======================================================================== + // EXECUTION CONTEXT & STATE TRACKING + // ======================================================================== + + var ExecutionContext = { + dryRun: CONFIG.DRY_RUN, + startTime: new Date(), + stats: { + servicesFound: 0, + servicesProcessed: 0, + offeringsFound: 0, + offeringsProcessed: 0, + commitmentsFound: 0, + commitmentsCreated: 0, + relationsSkipped: 0, + relationsCreated: 0, + relationsFailed: 0, + errorsEncountered: 0 + }, + validationErrors: [], + processLog: [] + }; + + // ======================================================================== + // VALIDATION FUNCTIONS + // ======================================================================== + + /** + * Validate configuration before execution + */ + function validateConfiguration() { + gs.info('🔍 Validating configuration...'); + + var errors = []; + + // Validate Portfolio System ID + if (!CONFIG.PORTFOLIO_SYS_ID || CONFIG.PORTFOLIO_SYS_ID.trim() === '') { + errors.push('❌ PORTFOLIO_SYS_ID is empty or missing'); + } else if (CONFIG.PORTFOLIO_SYS_ID.length !== 32) { + errors.push('âš ī¸ PORTFOLIO_SYS_ID appears invalid (expected 32 characters, got ' + + CONFIG.PORTFOLIO_SYS_ID.length + ')'); + } + + // Validate Service Commitments Array + if (!CONFIG.SERVICE_COMMITMENTS_TO_ADD || + !Array.isArray(CONFIG.SERVICE_COMMITMENTS_TO_ADD) || + CONFIG.SERVICE_COMMITMENTS_TO_ADD.length === 0) { + errors.push('❌ SERVICE_COMMITMENTS_TO_ADD must be a non-empty array'); + } else { + for (var i = 0; i < CONFIG.SERVICE_COMMITMENTS_TO_ADD.length; i++) { + var commitment = CONFIG.SERVICE_COMMITMENTS_TO_ADD[i]; + if (typeof commitment !== 'string' || commitment.trim() === '') { + errors.push('❌ SERVICE_COMMITMENTS_TO_ADD[' + i + + '] is empty or not a string'); + } + } + } + + // Check if Portfolio exists + if (CONFIG.PORTFOLIO_SYS_ID && CONFIG.PORTFOLIO_SYS_ID.length === 32) { + var portfolioGR = new GlideRecord('spm_portfolio'); + portfolioGR.addQuery('sys_id', CONFIG.PORTFOLIO_SYS_ID); + portfolioGR.query(); + + if (!portfolioGR.next()) { + errors.push('❌ Portfolio not found with sys_id: ' + CONFIG.PORTFOLIO_SYS_ID); + } else { + logInfo('✅ Portfolio found: ' + portfolioGR.getDisplayValue('name')); + } + } + + return errors; + } + + // ======================================================================== + // LOGGING FUNCTIONS + // ======================================================================== + + function logInfo(message) { + gs.info(message); + ExecutionContext.processLog.push('[INFO] ' + message); + } + + function logWarn(message) { + gs.warn(message); + ExecutionContext.processLog.push('[WARN] ' + message); + } + + function logError(message) { + gs.error(message); + ExecutionContext.processLog.push('[ERROR] ' + message); + ExecutionContext.stats.errorsEncountered++; + } + + // ======================================================================== + // CORE BUSINESS LOGIC + // ======================================================================== + + /** + * Main execution function + */ + function executeProcess() { + try { + logInfo('═══════════════════════════════════════════════════════════════'); + logInfo('🚀 Starting Portfolio Offerings Service Commitments Process'); + logInfo('📅 Execution Time: ' + ExecutionContext.startTime); + logInfo('🔄 Mode: ' + (ExecutionContext.dryRun ? 'DRY-RUN (No changes)' : 'LIVE (Changes will be saved)')); + logInfo('═══════════════════════════════════════════════════════════════'); + + // Step 1: Validate configuration + var validationErrors = validateConfiguration(); + if (validationErrors.length > 0) { + logError('Configuration validation failed:'); + for (var i = 0; i < validationErrors.length; i++) { + logError(' ' + validationErrors[i]); + } + return false; + } + logInfo('✅ Configuration validation passed'); + + // Step 2: Get all services from portfolio + var services = getServicesFromPortfolio(CONFIG.PORTFOLIO_SYS_ID); + ExecutionContext.stats.servicesFound = services.length; + + if (services.length === 0) { + logWarn('âš ī¸ No services found in portfolio'); + return true; + } + + logInfo('✅ Found ' + services.length + ' service(s) in portfolio'); + + // Step 3: Process each service and its offerings + for (var i = 0; i < services.length; i++) { + var service = services[i]; + ExecutionContext.stats.servicesProcessed++; + + logInfo(''); + logInfo('đŸ“Ļ [Service ' + (i + 1) + '/' + services.length + '] Processing: ' + + service.name + ' (ID: ' + service.sys_id + ')'); + + // Get offerings for this service + var offerings = getOfferingsForService(service.sys_id); + ExecutionContext.stats.offeringsFound += offerings.length; + + if (offerings.length === 0) { + logInfo(' â„šī¸ No offerings found for this service'); + continue; + } + + logInfo(' ✅ Found ' + offerings.length + ' offering(s)'); + + // Process each offering + for (var j = 0; j < offerings.length; j++) { + var offering = offerings[j]; + ExecutionContext.stats.offeringsProcessed++; + + logInfo(' 📋 [Offering ' + (j + 1) + '/' + offerings.length + '] ' + + offering.name + ' (ID: ' + offering.sys_id + ')'); + + // Add service commitments to this offering + var result = addServiceCommitmentsToOffering( + offering.sys_id, + CONFIG.SERVICE_COMMITMENTS_TO_ADD + ); + } + } + + return true; + + } catch (error) { + logError('Fatal error in main process: ' + error.message); + logError('Stack trace: ' + error.stack); + return false; + } + } + + /** + * Get all services from a portfolio + */ + function getServicesFromPortfolio(portfolioSysId) { + var services = []; + + try { + var serviceGR = new GlideRecord('cmdb_ci_service_business'); + serviceGR.addQuery('spm_service_portfolio', portfolioSysId); + + if (CONFIG.FILTER_OPERATIONAL_ONLY) { + serviceGR.addQuery('operational_status', 'operational'); + } + + serviceGR.query(); + + while (serviceGR.next()) { + services.push({ + sys_id: serviceGR.getUniqueValue(), + name: serviceGR.getDisplayValue('name'), + number: serviceGR.getDisplayValue('number') + }); + } + + } catch (error) { + logError('Error querying services: ' + error.message); + } + + return services; + } + + /** + * Get all offerings for a specific service + */ + function getOfferingsForService(serviceSysId) { + var offerings = []; + + try { + var offeringGR = new GlideRecord('service_offering'); + offeringGR.addQuery('parent', serviceSysId); + + if (CONFIG.FILTER_OPERATIONAL_ONLY) { + offeringGR.addQuery('operational_status', 'operational'); + } + + offeringGR.query(); + + while (offeringGR.next()) { + offerings.push({ + sys_id: offeringGR.getUniqueValue(), + name: offeringGR.getDisplayValue('name'), + number: offeringGR.getDisplayValue('number') + }); + } + + } catch (error) { + logError('Error querying offerings: ' + error.message); + } + + return offerings; + } + + /** + * Add service commitments to a specific offering + */ + function addServiceCommitmentsToOffering(offeringSysId, commitmentNames) { + var result = { + offering: offeringSysId, + processed: 0, + created: 0, + skipped: 0, + failed: 0 + }; + + try { + for (var i = 0; i < commitmentNames.length; i++) { + var commitmentName = commitmentNames[i].trim(); + + if (commitmentName === '') { + logWarn(' âš ī¸ Empty commitment name, skipping'); + result.skipped++; + continue; + } + + // Get or create service commitment + var commitmentSysId = getOrCreateServiceCommitment(commitmentName); + + if (!commitmentSysId) { + logError(' ❌ Failed to get/create commitment: ' + commitmentName); + result.failed++; + continue; + } + + result.processed++; + + // Check if relationship already exists + if (CONFIG.SKIP_EXISTING && offeringCommitmentExists(offeringSysId, commitmentSysId)) { + logInfo(' â­ī¸ Skipped - already linked: ' + commitmentName); + result.skipped++; + ExecutionContext.stats.relationsSkipped++; + continue; + } + + // Create relationship + if (ExecutionContext.dryRun) { + logInfo(' [DRY-RUN] Would create relationship: ' + commitmentName); + result.created++; + ExecutionContext.stats.relationsCreated++; + } else { + var relationshipId = createOfferingCommitmentRelationship( + offeringSysId, + commitmentSysId, + commitmentName, + i + ); + + if (relationshipId) { + logInfo(' ✅ Created relationship: ' + commitmentName); + result.created++; + ExecutionContext.stats.relationsCreated++; + } else { + logError(' ❌ Failed to create relationship: ' + commitmentName); + result.failed++; + ExecutionContext.stats.relationsFailed++; + } + } + } + + } catch (error) { + logError('Error adding commitments to offering: ' + error.message); + } + + return result; + } + + /** + * Get or create service commitment by name + */ + function getOrCreateServiceCommitment(commitmentName) { + try { + // Try to find existing commitment + var commitmentGR = new GlideRecord('service_commitment'); + commitmentGR.addQuery('name', commitmentName); + commitmentGR.query(); + + if (commitmentGR.next()) { + logInfo(' â„šī¸ Found existing commitment: ' + commitmentName); + ExecutionContext.stats.commitmentsFound++; + return commitmentGR.getUniqueValue(); + } + + // Create new commitment + if (ExecutionContext.dryRun) { + logInfo(' [DRY-RUN] Would create commitment: ' + commitmentName); + ExecutionContext.stats.commitmentsCreated++; + return 'DRY_RUN_ID_' + Date.now(); // Return pseudo-ID for dry-run + } else { + logInfo(' Creating new commitment: ' + commitmentName); + var newCommitmentGR = new GlideRecord('service_commitment'); + newCommitmentGR.initialize(); + newCommitmentGR.setValue('name', commitmentName); + newCommitmentGR.setValue('short_description', 'Auto-created: ' + commitmentName); + newCommitmentGR.setValue('operational_status', 'operational'); + + var newCommitmentId = newCommitmentGR.insert(); + + if (newCommitmentId) { + logInfo(' ✅ Created commitment: ' + commitmentName); + ExecutionContext.stats.commitmentsCreated++; + return newCommitmentId; + } else { + logError(' ❌ Failed to create commitment: ' + commitmentName); + return null; + } + } + + } catch (error) { + logError('Error getting/creating commitment: ' + error.message); + return null; + } + } + + /** + * Create offering-commitment relationship + */ + function createOfferingCommitmentRelationship(offeringSysId, commitmentSysId, commitmentName, order) { + try { + var relationshipGR = new GlideRecord('service_offering_commitment'); + relationshipGR.initialize(); + relationshipGR.setValue('service_offering', offeringSysId); + relationshipGR.setValue('service_commitment', commitmentSysId); + relationshipGR.setValue('order', (order + 1) * 100); + + var relationshipId = relationshipGR.insert(); + + if (relationshipId) { + return relationshipId; + } else { + return null; + } + + } catch (error) { + logError('Error creating relationship: ' + error.message); + return null; + } + } + + /** + * Check if offering-commitment relationship already exists + */ + function offeringCommitmentExists(offeringSysId, commitmentSysId) { + try { + var relationshipGR = new GlideRecord('service_offering_commitment'); + relationshipGR.addQuery('service_offering', offeringSysId); + relationshipGR.addQuery('service_commitment', commitmentSysId); + relationshipGR.query(); + + return relationshipGR.hasNext(); + + } catch (error) { + logError('Error checking relationship: ' + error.message); + return false; + } + } + + // ======================================================================== + // EXECUTION SUMMARY & REPORTING + // ======================================================================== + + function generateExecutionSummary() { + var executionTime = new Date() - ExecutionContext.startTime; + + logInfo(''); + logInfo('═══════════════════════════════════════════════════════════════'); + logInfo('📊 EXECUTION SUMMARY'); + logInfo('═══════════════════════════════════════════════════════════════'); + logInfo(''); + logInfo('âąī¸ Execution Time: ' + executionTime + ' ms'); + logInfo('🔄 Mode: ' + (ExecutionContext.dryRun ? '🟡 DRY-RUN' : 'đŸŸĸ LIVE')); + logInfo(''); + logInfo('📈 STATISTICS:'); + logInfo(' Services Found: ' + ExecutionContext.stats.servicesFound); + logInfo(' Services Processed: ' + ExecutionContext.stats.servicesProcessed); + logInfo(' Offerings Found: ' + ExecutionContext.stats.offeringsFound); + logInfo(' Offerings Processed: ' + ExecutionContext.stats.offeringsProcessed); + logInfo(' Commitments Found: ' + ExecutionContext.stats.commitmentsFound); + logInfo(' Commitments Created: ' + ExecutionContext.stats.commitmentsCreated); + logInfo(' Relationships Skipped: ' + ExecutionContext.stats.relationsSkipped); + logInfo(' Relationships Created: ' + ExecutionContext.stats.relationsCreated); + logInfo(' Relationships Failed: ' + ExecutionContext.stats.relationsFailed); + logInfo(' Errors Encountered: ' + ExecutionContext.stats.errorsEncountered); + logInfo(''); + logInfo('═══════════════════════════════════════════════════════════════'); + + if (ExecutionContext.dryRun) { + logInfo('✅ DRY-RUN COMPLETE - No changes were made to the database'); + logInfo('📌 Review results above and set DRY_RUN: false to execute'); + } else { + if (ExecutionContext.stats.errorsEncountered === 0) { + logInfo('✅ EXECUTION COMPLETE - All changes have been saved'); + } else { + logWarn('âš ī¸ EXECUTION COMPLETE - Some errors occurred'); + } + } + logInfo(''); + } + + // ======================================================================== + // MAIN EXECUTION + // ======================================================================== + + try { + var success = executeProcess(); + generateExecutionSummary(); + + if (!success) { + gs.error('⛔ Execution failed - see log for details'); + } + + } catch (error) { + gs.error('❌ Critical error: ' + error.message); + gs.error(error.stack); + } + +})(); From f145d62ec8c9bb64056af38d5f2fcaafcf9b8d03 Mon Sep 17 00:00:00 2001 From: AAladeen <114360717+AAladeen@users.noreply.github.com> Date: Fri, 24 Oct 2025 10:35:02 +0200 Subject: [PATCH 2/2] Create README.md --- .../Business Rules/README.md | 62 +++++++++++++++++++ 1 file changed, 62 insertions(+) create mode 100644 Server-Side Components/Business Rules/README.md diff --git a/Server-Side Components/Business Rules/README.md b/Server-Side Components/Business Rules/README.md new file mode 100644 index 0000000000..e89504ee8e --- /dev/null +++ b/Server-Side Components/Business Rules/README.md @@ -0,0 +1,62 @@ +# ServiceNow Portfolio Commitments Loader + +Bulk link service commitments to portfolio offerings with dry-run validation. + +## Overview + +This background script automates linking service commitments across multiple offerings in a ServiceNow portfolio. Instead of manually adding commitments one by one, you can process entire portfolios in seconds. + +The script checks if commitments exist before creating them, skips duplicates, and includes a dry-run mode so you can see exactly what will be updated before running it live. + +## What It Does + +- Loops through all services in a defined portfolio +- Gets all offerings for each service +- Adds service commitments from a configured array +- Checks if commitments already exist (avoids duplicates) +- Can simulate changes first (dry-run mode) to see impact +- Generates a summary of what was processed + +## Key Features + +- **Dry-run mode** - Preview changes without touching the database +- **Duplicate prevention** - Doesn't add the same commitment twice +- **Auto-create commitments** - Creates commitments if they don't exist +- **Detailed logging** - See exactly what the script did +- **Error handling** - Continues if something fails on one record +- **Easy to customize** - Can be modified to target specific services or use different commitments per service + +## Getting Started + +### Before You Run + +1. Note your portfolio's `sys_id` + - Go to Service Portfolio module + - Open your portfolio + - Copy the sys_id from the the list + +2. Know the names of your service commitments + - Go to Service Commitment table + - Copy the exact names you want to add (case matters) + + +### How It Works + +The script: + +Validates your configuration +Queries for all services in the portfolio +For each service, gets all offerings +For each offering: +Finds or creates the service commitment +Checks if it's already linked +Creates the relationship (or shows what it would do in dry-run) +Shows you a summary of what was processed +Configuration Options + +Setting Default What It Does +PORTFOLIO_SYS_ID Required The portfolio to process +SERVICE_COMMITMENTS_TO_ADD Required Commitments to add +DRY_RUN true If true, just show what would happen +FILTER_OPERATIONAL_ONLY false If true, only process operational items +SKIP_EXISTING true If true, don't add duplicate links