TransWikia.com

trigger to validate price ranges

Salesforce Asked by Luís Aguiar on December 10, 2021

We have the need to insert price ranges (between Quantidade Quantidade_Cx_Minima__c and_Cx_Maxima__c) dynamically (in the object Escalao_de_Promocao__c), for specific products in specific stores, so that they handle promotions in stores. The quantities of a product relates to the discount on unit price. The type of discount accepted are configured in a related product custom object.

The manager inserts ranges integers like 0-9, unit price or discount percentage (there is a validating rule demanding one of the fields filled) = 20€ or -1%;

so 10 to 19 = 18€ or -3%; etc.

table of registered Escalao_de_Promocao__c

These ranges can be sequential, or if there is a gap, or a range is deleted, it can be filled later. Like:
with these ranges: 10-19, 30-39, 40-49 these inserts are valid: 0-9, 20-25, 50-59

and these should be invalid: 0-20, 20-25, 20-60

the validation is now made with a trigger a few validation rules:
the trigger:

public with sharing class tl_Escalao_de_Promocao {
private th_Escalao_de_Promocao handler;
public tl_Escalao_de_Promocao(th_Escalao_de_Promocao handler) {    
    this.handler = handler;        
}

public void ValidateEscaloesPromocoes(List<Escalao_de_Promocao__c> l_EscalaoPromocao) {        
    List<String> sIDPromocao = new List<String>();
    List<String> sIDProduto = new List<String>();
    
    for(Escalao_de_Promocao__c ep: l_EscalaoPromocao){
        sIDPromocao.add(ep.Promocao__c);
        sIDProduto.add(ep.Produto_da_Promocao__c);
    }
    if(sIDPromocao.size()>0 || sIDProduto.size()>0){
        
        Map<Decimal, Decimal> Map_Number = new Map<Decimal, Decimal>();
        List<Escalao_de_Promocao__c> escaloes = [
            SELECT Id, Quantidade_Cx_Minima__c, Quantidade_Cx_Maxima__c 
            FROM Escalao_de_Promocao__c 
            WHERE Promocao__c IN: sIDPromocao and Produto_da_Promocao__c IN: sIDProduto ];

        for(Escalao_de_Promocao__c e: escaloes){

            for(Decimal i= e.Quantidade_Cx_Minima__c; i <= e.Quantidade_Cx_Maxima__c; i++ ){
                Map_Number.put(i, i);
            }
        }

        system.debug('Map_Number: ' + Map_Number);
        for(Decimal valores: Map_Number.keySet()){
            for(Escalao_de_Promocao__c esc : l_EscalaoPromocao ){
                if(Map_Number.keySet().contains(esc.Quantidade_Cx_Minima__c) || 
                Map_Number.keySet().contains(esc.Quantidade_Cx_Maxima__c)){
                    if(!Test.isRunningTest()) esc.addError('ERRO'); 
                }
            }
        }
    }       
}

The process here is:

  • mapping all integer values between minumim and maximum values, for recorded Escalao_de_Promocao__c objects
  • if the value is already there, cant be saved. so for ranges 0-4, 10-14, 30-39, befores the insert, we have

    Map_Number is: {0=false, 1=false, 2=false, 3=false, 4=true, 6=true, 10=false, 11=false, 12=false, 13=false, …}

so far, gives an interface error with inserting:

  • repeated range
  • overlapping range

with validation rules we validate maximum bellow minimum.

But we cant validate if a range starts and ends in empty slots, while overlaping a filled range. So inserting this should also be invalid: 8-29. And here is the remaining issue.

Does anyone have a better solution? Thanks

edit: my implementation

public void ValidateEscaloesPromocoes(List<Escalao_de_Promocao__c> l_EscalaoPromocao) {        
    List<String> sIDPromocao = new List<String>();
    List<String> sIDProduto = new List<String>();
    Set<Decimal> allocatedRanges = new Set<Decimal>();
    
    for(Escalao_de_Promocao__c ep: l_EscalaoPromocao){
        sIDPromocao.add(ep.Promocao__c);
        sIDProduto.add(ep.Produto_da_Promocao__c);
    }
    if(sIDPromocao.size()>0 || sIDProduto.size()>0){
        
        Set<Decimal> currentRange = new Set<Decimal>();

        List<Escalao_de_Promocao__c> escaloes = [SELECT Id, Quantidade_Cx_Minima__c, Quantidade_Cx_Maxima__c FROM Escalao_de_Promocao__c WHERE Promocao__c IN: sIDPromocao and Produto_da_Promocao__c IN: sIDProduto ];

        System.debug('escaloes: ' + escaloes);

        for(Escalao_de_Promocao__c rec : escaloes){
            for(Decimal i = rec.Quantidade_Cx_Minima__c; i <= rec.Quantidade_Cx_Maxima__c; i++){  
                currentRange.add(i);
            }
        }
        System.debug('allocatedRanges: ' + allocatedRanges);

        if(!currentRange.clone().removeAll(allocatedRanges)){
            allocatedRanges.addAll(currentRange);
            System.debug('currentRange s overlap: ' + currentRange);
        } else {
            System.debug('allocatedRanges overlap: ' + allocatedRanges);
            if(allocatedRanges.containsAll(currentRange)){
                System.debug('currentRange contains: ' + currentRange);
            } else {
                System.debug('currentRange not contains: ' + currentRange);
            }
        } 

        System.debug('allocatedRanges: ' + allocatedRanges); 
    }       
}

One Answer

I think that what you have is fairly close.

While there are some datasets that have nice properties that can help us design an algorithm (IP Address allocation maps very nicely to a binary tree, for example), I don't think that this problem really gives us much to work with.

About the only suggestion that I can come up with is I don't think using a Map is appropriate here. Instead, I'd use a simple Set<Decimal>.

The idea is to turn each range into a Set<Decimal> (i.e. Minimum = 9, Maximum = 16 becomes Set<Decimal>{9, 10, 11, 12, 13, 14, 15, 16}), and then use a combination of removeAll(), retainAll(), and containsAll() to check for overlap.

// I'm just assuming that there's no issue with combining the existing and new records
// That assumption means that existing records are re-subjected to the overlap check
// We need to generate ranges for them (the existing records) anyway, and I don't think
//   that the Set class methods consume too much cpu time
Set<Decimal> allocatedRanges = new Set<Decimal>();
for(Record__c rec :allRecords){
    Set<Decimal> currentRange = new Set<Decimal>();
    for(Decimal i = rec.min; i <= rec.max; i++){    
        currentRange.add(i);
    }

    // removeAll returns a boolean
    // false = original set was not changed
    // true = original set did undergo a change
    // The extra clone() call in there helps ensure that currentRange is not modified
    //   (so that we can use it to help determine what type of overlap we have)
    if(!currentRange.clone().removeAll(allocatedRanges)){
        // In here, we know that there is no overlap between currentRange
        //   and allocatedRanges
        // Add current to allocated so we can check the next record
        allocatedRanges.addAll(currentRange);
    }else{
        // Inside this else block, we know that there was _some_ overlap
        // Determining if that's due to partial overlap, a repeated range,
        //   or a sub-range requires extra work
        if(allocatedRanges.containsAll(current)){
            // In here, we know that we either repeated a range, or are a
            //   sub-range
            // Figuring out which it is, though, may not be possible without
            //   additional data
        }else{
            // In here, we know that there is some non-complete overlap
            // i.e. not all of the range has already been allocated
        }
    }
}

A pseudo-code description of the above (which may be more readable) is

declare a primary set for things that have already been allocated
for each Escalao_de_Promocao__c record{
    declare a second set to hold the range for _this_ particular record
 
    iterate from record min to record max (to build our range){
        add this number to our secondary set
    }

    does our secondary set contain any numbers from the primary set?{
        no, it does not, there is no overlap
        add our secondary set into the primary set
        continue on to checking the next record
    }{
       yes, it does, so there is overlap
       error out
    }
}

It's important that you keep checking for overlap and adding to the allocated set for each individual record.

There may be a way to do this without needing to expand a range into a Set, but unless you're running into the CPU or Heap governor limits I don't think it's worth trying to come up with something more clever than this.

Answered by Derek F on December 10, 2021

Add your own answers!

Ask a Question

Get help from others!

© 2024 TransWikia.com. All rights reserved. Sites we Love: PCI Database, UKBizDB, Menu Kuliner, Sharing RPP