From 45cd658e2098da32579f2bf0c3271e440a068428 Mon Sep 17 00:00:00 2001
From: Heber <Anthony.Heber@gmail.com>
Date: Sun, 1 Sep 2024 17:16:03 -0600
Subject: [PATCH] Enable rollup bypass via Custom Setting, Dev API, or
 per-rollup Cust Perm

---
 dlrs/main/classes/BypassHandler.cls           | 26 +++---
 dlrs/main/classes/BypassHandlerTest.cls       | 83 +++++++++++++------
 dlrs/main/classes/RollupEditorController.cls  |  4 +
 dlrs/main/classes/RollupService.cls           | 27 +++++-
 dlrs/main/classes/RollupServiceTest.cls       | 54 ++++++------
 dlrs/main/classes/RollupSummariesSelector.cls |  1 +
 dlrs/main/classes/RollupSummary.cls           | 15 ++++
 dlrs/main/classes/RollupSummaryTest.cls       | 23 +++++
 .../classes/RollupSummaryTest.cls-meta.xml    |  5 ++
 dlrs/main/classes/Utilities.cls               | 18 ++++
 dlrs/main/classes/UtilitiesTest.cls           | 21 +++++
 dlrs/main/classes/UtilitiesTest.cls-meta.xml  |  5 ++
 ...okup Rollup Summary Layout.layout-meta.xml |  4 +
 dlrs/main/lwc/rollupEditor/rollupEditor.html  | 23 ++++-
 dlrs/main/lwc/rollupEditor/rollupEditor.js    |  3 +-
 .../DisableDLRSGlobally__c.field-meta.xml     | 11 +++
 .../BypassPermissionApiName__c.field-meta.xml | 14 ++++
 17 files changed, 268 insertions(+), 69 deletions(-)
 create mode 100644 dlrs/main/classes/RollupSummaryTest.cls
 create mode 100644 dlrs/main/classes/RollupSummaryTest.cls-meta.xml
 create mode 100644 dlrs/main/classes/UtilitiesTest.cls
 create mode 100644 dlrs/main/classes/UtilitiesTest.cls-meta.xml
 create mode 100644 dlrs/main/objects/DeclarativeLookupRollupSummaries__c/fields/DisableDLRSGlobally__c.field-meta.xml
 create mode 100644 dlrs/main/objects/LookupRollupSummary2__mdt/fields/BypassPermissionApiName__c.field-meta.xml

diff --git a/dlrs/main/classes/BypassHandler.cls b/dlrs/main/classes/BypassHandler.cls
index 5accb586..efd0c72e 100644
--- a/dlrs/main/classes/BypassHandler.cls
+++ b/dlrs/main/classes/BypassHandler.cls
@@ -31,30 +31,31 @@
  * The bypass and removebypass method return the result of the default Set object operations.
  **/
 public without sharing class BypassHandler {
-  private static Set<String> bypassedRollups;
+  private static Set<String> bypassedRollups = new Set<String>();
+  private static Boolean bypassAll = false;
 
   /**
-   * Initialize the set if necessary for adding rollups to the bypass list.
+   * Checks if the rollup is bypassed or not. Returns true if it is. False otherwise.
+   * Could be bypassed by custom setting, bypass all, or specific named bypass
    */
-  private static void init() {
-    if (bypassedRollups == null) {
-      bypassedRollups = new Set<String>();
-    }
+  public static Boolean isBypassed(String handlerName) {
+    return DeclarativeLookupRollupSummaries__c.getInstance()
+        .DisableDLRSGlobally__c == true ||
+      bypassAll ||
+      bypassedRollups.contains(handlerName);
   }
 
   /**
-   * Checks if the rollup is bypassed or not. Returns true if it is. False otherwise.
+   * Sets a global bypass value, if true all rollups will be disabled for execution
    */
-  public static Boolean isBypassed(String handlerName) {
-    return bypassedRollups != null && bypassedRollups.contains(handlerName);
+  public static void setBypassAll(Boolean val) {
+    bypassAll = val;
   }
 
   /**
    * Adds a rollup to the bypassed rollups list.
    */
   public static Boolean bypass(String handlerName) {
-    init();
-
     if (handlerName != null) {
       System.debug(
         LoggingLevel.INFO,
@@ -75,7 +76,7 @@ public without sharing class BypassHandler {
    * Clears the bypass for a single rollup.
    */
   public static Boolean clearBypass(String handlerName) {
-    if (bypassedRollups != null && handlerName != null) {
+    if (handlerName != null) {
       System.debug(
         LoggingLevel.INFO,
         'DLRS trigger handler is no longer bypassed: ' + handlerName
@@ -95,6 +96,7 @@ public without sharing class BypassHandler {
    * Clears all bypasses, if any.
    */
   public static void clearAllBypasses() {
+    bypassAll = false;
     if (bypassedRollups != null) {
       bypassedRollups.clear();
     }
diff --git a/dlrs/main/classes/BypassHandlerTest.cls b/dlrs/main/classes/BypassHandlerTest.cls
index 52d69340..866e5614 100644
--- a/dlrs/main/classes/BypassHandlerTest.cls
+++ b/dlrs/main/classes/BypassHandlerTest.cls
@@ -28,54 +28,85 @@ private class BypassHandlerTest {
   @IsTest
   static void testApi() {
     String rollupUniqueName = 'SampleRollup';
-    Boolean bypassResult;
-
-    Test.startTest();
-    System.assertEquals(
-      false,
+    Assert.isFalse(
       BypassHandler.isBypassed(rollupUniqueName),
       'The rollup should not be bypassed yet.'
     );
-    bypassResult = BypassHandler.bypass(rollupUniqueName);
-    System.assert(
-      bypassResult,
+
+    Assert.isTrue(
+      BypassHandler.bypass(rollupUniqueName),
       'Should have modified the bypassed rollups set.'
     );
-    System.assertEquals(
-      true,
+    Assert.isTrue(
       BypassHandler.isBypassed(rollupUniqueName),
       'The rollup should be bypassed.'
     );
-    bypassResult = BypassHandler.clearBypass(rollupUniqueName);
-    System.assert(
-      bypassResult,
+
+    Assert.isTrue(
+      BypassHandler.clearBypass(rollupUniqueName),
       'Should have modified the bypassed rollups set.'
     );
-    System.assertEquals(
-      false,
+    Assert.isFalse(
       BypassHandler.isBypassed(rollupUniqueName),
       'The rollup should not be bypassed anymore.'
     );
     BypassHandler.bypass(rollupUniqueName);
     BypassHandler.clearAllBypasses();
-    System.assertEquals(
-      false,
+    Assert.isFalse(
       BypassHandler.isBypassed(rollupUniqueName),
       'The rollup should not be bypassed anymore.'
     );
 
-    bypassResult = BypassHandler.bypass(null);
-    System.assertEquals(
-      false,
-      bypassResult,
+    Assert.isFalse(
+      BypassHandler.bypass(null),
       'Should return "false" for a null rollup name.'
     );
-    bypassResult = BypassHandler.clearBypass(null);
-    System.assertEquals(
-      false,
-      bypassResult,
+
+    Assert.isFalse(
+      BypassHandler.clearBypass(null),
       'Should return "false" for a null rollup name.'
     );
-    Test.stopTest();
+
+    BypassHandler.setBypassAll(true);
+    Assert.isTrue(
+      BypassHandler.isBypassed(rollupUniqueName),
+      'Should return "true" for all rollup names.'
+    );
+    Assert.isTrue(
+      BypassHandler.isBypassed('new name'),
+      'Should return "true" for all rollup names.'
+    );
+    BypassHandler.setBypassAll(false);
+
+    Assert.isFalse(
+      BypassHandler.isBypassed(rollupUniqueName),
+      'Should return "false" for all rollup names.'
+    );
+    Assert.isFalse(
+      BypassHandler.isBypassed('new name'),
+      'Should return "false" for all rollup names.'
+    );
+    BypassHandler.setBypassAll(true);
+    Assert.isTrue(
+      BypassHandler.isBypassed('new name'),
+      'Should return "true" for all rollup names.'
+    );
+    BypassHandler.clearAllBypasses();
+    Assert.isFalse(
+      BypassHandler.isBypassed('new name'),
+      'Should return "false" for all rollup names.'
+    );
+  }
+
+  @IsTest
+  static void testCustomSettingDisable() {
+    String rollupUniqueName = 'Rollup1';
+    Assert.isFalse(BypassHandler.isBypassed(rollupUniqueName));
+
+    DeclarativeLookupRollupSummaries__c settings = DeclarativeLookupRollupSummaries__c.getInstance();
+    settings.DisableDLRSGlobally__c = true;
+    insert settings;
+
+    Assert.isTrue(BypassHandler.isBypassed(rollupUniqueName));
   }
 }
diff --git a/dlrs/main/classes/RollupEditorController.cls b/dlrs/main/classes/RollupEditorController.cls
index cbef278b..e35018e1 100644
--- a/dlrs/main/classes/RollupEditorController.cls
+++ b/dlrs/main/classes/RollupEditorController.cls
@@ -271,6 +271,8 @@ public with sharing class RollupEditorController {
     @AuraEnabled
     public String aggregateResultField;
     @AuraEnabled
+    public String bypassPermissionApiName;
+    @AuraEnabled
     public String calculationMode;
     @AuraEnabled
     public String calculationSharingMode;
@@ -309,6 +311,7 @@ public with sharing class RollupEditorController {
       this.aggregateAllRows = record.AggregateAllRows__c;
       this.aggregateOperation = record.AggregateOperation__c;
       this.aggregateResultField = record.AggregateResultField__c;
+      this.bypassPermissionApiName = record.BypassPermissionApiName__c;
       this.calculationMode = record.CalculationMode__c;
       this.calculationSharingMode = record.CalculationSharingMode__c;
       this.childObject = record.ChildObject__c;
@@ -335,6 +338,7 @@ public with sharing class RollupEditorController {
       record.AggregateAllRows__c = this.aggregateAllRows;
       record.AggregateOperation__c = this.aggregateOperation;
       record.AggregateResultField__c = this.aggregateResultField;
+      record.BypassPermissionApiName__c = this.bypassPermissionApiName;
       record.CalculationMode__c = this.calculationMode;
       record.CalculationSharingMode__c = this.calculationSharingMode;
       record.ChildObject__c = this.childObject;
diff --git a/dlrs/main/classes/RollupService.cls b/dlrs/main/classes/RollupService.cls
index e2fe5b74..cbd10777 100644
--- a/dlrs/main/classes/RollupService.cls
+++ b/dlrs/main/classes/RollupService.cls
@@ -381,6 +381,13 @@ global with sharing class RollupService {
     return BypassHandler.bypass(rollupName);
   }
 
+  /**
+   * Allow the bypass of all rollups for this transaction, can be cleared with "clearAllBypasses" method
+   */
+  global static void bypassAll() {
+    BypassHandler.setBypassAll(true);
+  }
+
   /**
    * Clears the bypass of a rollup, given its unique name.
    */
@@ -964,7 +971,10 @@ global with sharing class RollupService {
       // this avoids having to re-parse RelationshipCriteria & OrderBy fields during field change detection
       Map<Id, Set<String>> fieldsInvolvedInLookup = new Map<Id, Set<String>>();
       for (RollupSummary lookup : lookups) {
-        if (BypassHandler.isBypassed(lookup.UniqueName)) {
+        if (
+          Utilities.userHasCustomPermission(lookup.BypassCustPermApiName) ||
+          BypassHandler.isBypassed(lookup.UniqueName)
+        ) {
           continue;
         }
 
@@ -1099,7 +1109,10 @@ global with sharing class RollupService {
       // Build a revised list of lookups to process that includes only where fields used in the rollup have changed
       List<RollupSummary> lookupsToProcess = new List<RollupSummary>();
       for (RollupSummary lookup : lookups) {
-        if (BypassHandler.isBypassed(lookup.UniqueName)) {
+        if (
+          Utilities.userHasCustomPermission(lookup.BypassCustPermApiName) ||
+          BypassHandler.isBypassed(lookup.UniqueName)
+        ) {
           continue;
         }
 
@@ -1134,7 +1147,10 @@ global with sharing class RollupService {
       : existingRecords;
     for (SObject childRecord : recordsToProcess.values()) {
       for (RollupSummary lookup : lookups) {
-        if (BypassHandler.isBypassed(lookup.UniqueName)) {
+        if (
+          Utilities.userHasCustomPermission(lookup.BypassCustPermApiName) ||
+          BypassHandler.isBypassed(lookup.UniqueName)
+        ) {
           continue;
         }
 
@@ -1179,7 +1195,10 @@ global with sharing class RollupService {
     List<RollupSummary> runnowLookups = new List<RollupSummary>();
     List<LookupRollupSummaryScheduleItems__c> scheduledItems = new List<LookupRollupSummaryScheduleItems__c>();
     for (RollupSummary lookup : lookups) {
-      if (BypassHandler.isBypassed(lookup.UniqueName)) {
+      if (
+        Utilities.userHasCustomPermission(lookup.BypassCustPermApiName) ||
+        BypassHandler.isBypassed(lookup.UniqueName)
+      ) {
         continue;
       }
 
diff --git a/dlrs/main/classes/RollupServiceTest.cls b/dlrs/main/classes/RollupServiceTest.cls
index 23f8ad48..080fd0c7 100644
--- a/dlrs/main/classes/RollupServiceTest.cls
+++ b/dlrs/main/classes/RollupServiceTest.cls
@@ -2623,54 +2623,58 @@ private with sharing class RollupServiceTest {
   @IsTest
   static void testBypassApi() {
     String rollupUniqueName = 'SampleRollup';
-    Boolean bypassResult;
 
-    Test.startTest();
-    System.assertEquals(
-      false,
+    Assert.isFalse(
       RollupService.isBypassed(rollupUniqueName),
       'The rollup should not be bypassed yet.'
     );
-    bypassResult = RollupService.bypass(rollupUniqueName);
-    System.assert(
-      bypassResult,
+
+    Assert.isTrue(
+      RollupService.bypass(rollupUniqueName),
       'Should have modified the bypassed rollups set.'
     );
-    System.assertEquals(
-      true,
+    Assert.isTrue(
       RollupService.isBypassed(rollupUniqueName),
       'The rollup should be bypassed.'
     );
-    bypassResult = RollupService.clearBypass(rollupUniqueName);
-    System.assert(
-      bypassResult,
+
+    Assert.isTrue(
+      RollupService.clearBypass(rollupUniqueName),
       'Should have modified the bypassed rollups set.'
     );
-    System.assertEquals(
-      false,
+    Assert.isFalse(
       RollupService.isBypassed(rollupUniqueName),
       'The rollup should not be bypassed anymore.'
     );
     RollupService.bypass(rollupUniqueName);
     RollupService.clearAllBypasses();
-    System.assertEquals(
-      false,
+    Assert.isFalse(
       RollupService.isBypassed(rollupUniqueName),
       'The rollup should not be bypassed anymore.'
     );
 
-    bypassResult = RollupService.bypass(null);
-    System.assertEquals(
-      false,
-      bypassResult,
+    Assert.isFalse(
+      RollupService.bypass(null),
       'Should return "false" for a null rollup name.'
     );
-    bypassResult = RollupService.clearBypass(null);
-    System.assertEquals(
-      false,
-      bypassResult,
+    Assert.isFalse(
+      RollupService.clearBypass(null),
       'Should return "false" for a null rollup name.'
     );
-    Test.stopTest();
+
+    RollupService.bypassAll();
+    Assert.isTrue(
+      RollupService.isBypassed(rollupUniqueName),
+      'Should return "true" for all rollup names.'
+    );
+    Assert.isTrue(
+      RollupService.isBypassed('new name'),
+      'Should return "true" for all rollup names.'
+    );
+    RollupService.clearAllBypasses();
+    Assert.isFalse(
+      RollupService.isBypassed(rollupUniqueName),
+      'Should return "false" for all rollup names.'
+    );
   }
 }
diff --git a/dlrs/main/classes/RollupSummariesSelector.cls b/dlrs/main/classes/RollupSummariesSelector.cls
index ad44b875..d31f44e2 100644
--- a/dlrs/main/classes/RollupSummariesSelector.cls
+++ b/dlrs/main/classes/RollupSummariesSelector.cls
@@ -279,6 +279,7 @@ public class RollupSummariesSelector {
         LookupRollupSummary2__mdt.Active__c,
         LookupRollupSummary2__mdt.AggregateOperation__c,
         LookupRollupSummary2__mdt.AggregateResultField__c,
+        LookupRollupSummary2__mdt.BypassPermissionApiName__c,
         LookupRollupSummary2__mdt.CalculationMode__c,
         LookupRollupSummary2__mdt.ChildObject__c,
         LookupRollupSummary2__mdt.ConcatenateDelimiter__c,
diff --git a/dlrs/main/classes/RollupSummary.cls b/dlrs/main/classes/RollupSummary.cls
index 4aaf8fed..9bbc4141 100644
--- a/dlrs/main/classes/RollupSummary.cls
+++ b/dlrs/main/classes/RollupSummary.cls
@@ -100,6 +100,21 @@ public class RollupSummary {
     }
   }
 
+  public String BypassCustPermApiName {
+    get {
+      if (Record instanceof LookupRollupSummary2__mdt) {
+        return (String) Record.get('BypassPermissionApiName__c');
+      } else {
+        return null;
+      }
+    }
+    set {
+      if (Record instanceof LookupRollupSummary2__mdt) {
+        Record.put('BypassPermissionApiName__c', value);
+      }
+    }
+  }
+
   public String CalculationMode {
     get {
       return (String) Record.get('CalculationMode__c');
diff --git a/dlrs/main/classes/RollupSummaryTest.cls b/dlrs/main/classes/RollupSummaryTest.cls
new file mode 100644
index 00000000..b6cbd011
--- /dev/null
+++ b/dlrs/main/classes/RollupSummaryTest.cls
@@ -0,0 +1,23 @@
+@IsTest
+public class RollupSummaryTest {
+  @IsTest
+  static void testBypassCustPermApiName() {
+    LookupRollupSummary2__mdt rollup = new LookupRollupSummary2__mdt();
+    rollup.BypassPermissionApiName__c = null;
+    RollupSummary rs = new RollupSummary(rollup);
+    Assert.areEqual(null, rs.BypassCustPermApiName);
+    rollup.BypassPermissionApiName__c = 'Rollup1';
+    rs = new RollupSummary(rollup);
+    Assert.areEqual('Rollup1', rs.BypassCustPermApiName);
+
+    rs.BypassCustPermApiName = 'Rollup2';
+    Assert.areEqual('Rollup2', rs.BypassCustPermApiName);
+
+    LookupRollupSummary__c rollupCO = new LookupRollupSummary__c();
+    rs = new RollupSummary(rollupCO);
+    Assert.areEqual(null, rs.BypassCustPermApiName);
+    rs.BypassCustPermApiName = 'Rollup1';
+    // we're not building support in the Custom Object rollup versions, setting the value is ignored
+    Assert.areEqual(null, rs.BypassCustPermApiName);
+  }
+}
diff --git a/dlrs/main/classes/RollupSummaryTest.cls-meta.xml b/dlrs/main/classes/RollupSummaryTest.cls-meta.xml
new file mode 100644
index 00000000..7d5f9e8a
--- /dev/null
+++ b/dlrs/main/classes/RollupSummaryTest.cls-meta.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<ApexClass xmlns="http://soap.sforce.com/2006/04/metadata">
+    <apiVersion>61.0</apiVersion>
+    <status>Active</status>
+</ApexClass>
\ No newline at end of file
diff --git a/dlrs/main/classes/Utilities.cls b/dlrs/main/classes/Utilities.cls
index 9f342e50..16a23cf3 100644
--- a/dlrs/main/classes/Utilities.cls
+++ b/dlrs/main/classes/Utilities.cls
@@ -85,6 +85,24 @@ public class Utilities {
     return orderByFields;
   }
 
+  /**
+   * permissionNames is null or comma-separated list of Custom Permissions
+   * returns `true` if user has any of those custom permissions
+   */
+  public static Boolean userHasCustomPermission(String permissionNames) {
+    if (String.isBlank(permissionNames)) {
+      return false;
+    }
+
+    for (String permName : permissionNames.split(',')) {
+      if (FeatureManagement.checkPermission(permName.trim())) {
+        return true;
+      }
+    }
+
+    return false;
+  }
+
   // Regular expression for Order By Clause
   // Case-Insensitive pattern
   // Group 1 - Field Name (required)
diff --git a/dlrs/main/classes/UtilitiesTest.cls b/dlrs/main/classes/UtilitiesTest.cls
new file mode 100644
index 00000000..9f47be43
--- /dev/null
+++ b/dlrs/main/classes/UtilitiesTest.cls
@@ -0,0 +1,21 @@
+@IsTest
+public class UtilitiesTest {
+  @IsTest
+  static void testUserHasCustomPermission() {
+    Assert.areEqual(false, Utilities.userHasCustomPermission(null));
+    Assert.areEqual(false, Utilities.userHasCustomPermission('madeup_name'));
+    Assert.areEqual(
+      false,
+      Utilities.userHasCustomPermission('madeup_name,name2 , name3,name4')
+    );
+    // TODO: add custom perm and perm set assigned to working user for tests but not add to package
+    // Assert.areEqual(
+    //   true,
+    //   Utilities.userHasCustomPermission('DLRSLimitedDisable')
+    // );
+    // Assert.areEqual(
+    //   true,
+    //   Utilities.userHasCustomPermission('rollup1, DLRSLimitedDisable ,rollup2')
+    // );
+  }
+}
diff --git a/dlrs/main/classes/UtilitiesTest.cls-meta.xml b/dlrs/main/classes/UtilitiesTest.cls-meta.xml
new file mode 100644
index 00000000..7d5f9e8a
--- /dev/null
+++ b/dlrs/main/classes/UtilitiesTest.cls-meta.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<ApexClass xmlns="http://soap.sforce.com/2006/04/metadata">
+    <apiVersion>61.0</apiVersion>
+    <status>Active</status>
+</ApexClass>
\ No newline at end of file
diff --git a/dlrs/main/layouts/LookupRollupSummary2__mdt-Lookup Rollup Summary Layout.layout-meta.xml b/dlrs/main/layouts/LookupRollupSummary2__mdt-Lookup Rollup Summary Layout.layout-meta.xml
index 421986aa..bc10cea0 100644
--- a/dlrs/main/layouts/LookupRollupSummary2__mdt-Lookup Rollup Summary Layout.layout-meta.xml	
+++ b/dlrs/main/layouts/LookupRollupSummary2__mdt-Lookup Rollup Summary Layout.layout-meta.xml	
@@ -169,6 +169,10 @@
                 <behavior>Edit</behavior>
                 <field>TestCodeSeeAllData__c</field>
             </layoutItems>
+            <layoutItems>
+                <behavior>Edit</behavior>
+                <field>BypassPermissionApiName__c</field>
+            </layoutItems>
         </layoutColumns>
         <style>OneColumn</style>
     </layoutSections>
diff --git a/dlrs/main/lwc/rollupEditor/rollupEditor.html b/dlrs/main/lwc/rollupEditor/rollupEditor.html
index ba05977c..8548eb1a 100644
--- a/dlrs/main/lwc/rollupEditor/rollupEditor.html
+++ b/dlrs/main/lwc/rollupEditor/rollupEditor.html
@@ -465,7 +465,11 @@ <h3 class="slds-section__title slds-theme--shade">
             errors={errors.testCodeParent}
           ></c-rollup-editor-error>
         </lightning-layout-item>
-        <lightning-layout-item class="slds-form-element_stacked">
+        <lightning-layout-item
+          size="12"
+          large-device-size="6"
+          class="slds-form-element_stacked"
+        >
           <lightning-input
             type="checkbox"
             name="rollup_testCodeSeeAllData"
@@ -479,6 +483,23 @@ <h3 class="slds-section__title slds-theme--shade">
             errors={errors.testCodeSeeAllData}
           ></c-rollup-editor-error>
         </lightning-layout-item>
+        <lightning-layout-item
+          size="12"
+          large-device-size="6"
+          class="slds-form-element_stacked"
+        >
+          <lightning-input
+            max-length="255"
+            name="rollup_bypassPermissionApiName"
+            data-name="rollup_bypassPermissionApiName"
+            label="Custom Permission name to bypass this rollup"
+            field-level-help="API Name of a Custom Permission that when held by the running user will prevent this rollup from processing"
+            value={rollup.bypassPermissionApiName}
+          ></lightning-input>
+          <c-rollup-editor-error
+            errors={errors.bypassPermissionApiName}
+          ></c-rollup-editor-error>
+        </lightning-layout-item>
       </lightning-layout>
     </details>
   </lightning-modal-body>
diff --git a/dlrs/main/lwc/rollupEditor/rollupEditor.js b/dlrs/main/lwc/rollupEditor/rollupEditor.js
index 9909beaf..fe471590 100644
--- a/dlrs/main/lwc/rollupEditor/rollupEditor.js
+++ b/dlrs/main/lwc/rollupEditor/rollupEditor.js
@@ -439,7 +439,8 @@ export default class RollupEditor extends LightningModal {
       "concatenateDelimiter",
       "testCode",
       "testCodeParent",
-      "testCodeSeeAllData"
+      "testCodeSeeAllData",
+      "bypassPermissionApiName"
     ];
 
     let isValid = true;
diff --git a/dlrs/main/objects/DeclarativeLookupRollupSummaries__c/fields/DisableDLRSGlobally__c.field-meta.xml b/dlrs/main/objects/DeclarativeLookupRollupSummaries__c/fields/DisableDLRSGlobally__c.field-meta.xml
new file mode 100644
index 00000000..cdb19433
--- /dev/null
+++ b/dlrs/main/objects/DeclarativeLookupRollupSummaries__c/fields/DisableDLRSGlobally__c.field-meta.xml
@@ -0,0 +1,11 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<CustomField xmlns="http://soap.sforce.com/2006/04/metadata">
+    <fullName>DisableDLRSGlobally__c</fullName>
+    <defaultValue>false</defaultValue>
+    <description>Turns off all DLRS calculations</description>
+    <externalId>false</externalId>
+    <inlineHelpText>Disable DLRS calculations, useful for bulk loading or other large-scale actions</inlineHelpText>
+    <label>Disable DLRS Globally</label>
+    <trackTrending>false</trackTrending>
+    <type>Checkbox</type>
+</CustomField>
diff --git a/dlrs/main/objects/LookupRollupSummary2__mdt/fields/BypassPermissionApiName__c.field-meta.xml b/dlrs/main/objects/LookupRollupSummary2__mdt/fields/BypassPermissionApiName__c.field-meta.xml
new file mode 100644
index 00000000..be4def99
--- /dev/null
+++ b/dlrs/main/objects/LookupRollupSummary2__mdt/fields/BypassPermissionApiName__c.field-meta.xml
@@ -0,0 +1,14 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<CustomField xmlns="http://soap.sforce.com/2006/04/metadata">
+    <fullName>BypassPermissionApiName__c</fullName>
+    <deprecated>false</deprecated>
+    <externalId>false</externalId>
+    <fieldManageability>DeveloperControlled</fieldManageability>
+    <inlineHelpText
+  >API name of a Custom Permission, if the running user has that permission then this rollup is skipped</inlineHelpText>
+    <label>Bypass Permission API Name</label>
+    <length>255</length>
+    <required>false</required>
+    <type>Text</type>
+    <unique>false</unique>
+</CustomField>