1

I created a schedule class which creates an Opportunity for every Service Contract which has this quarter as end date, and inserts the Contract Line Items as Opportunity Line Items.

I have the following apex class:

global class serviceContract_Scheduled implements Schedulable
    {



        global void execute(SchedulableContext sc)
        {
          List<OpportunityLineItem> lineItems = [SELECT Id, Description, OpportunityId, Quantity, TotalPrice, PriceBookEntryId FROM OpportunityLineItem];

          for(ServiceContract serv : [SELECT s.ID, s.Legal_Entity__c, s.Name, s.AccountId, s.EndDate FROM ServiceContract s WHERE EndDate = This_quarter AND Terminated__c = False])
            {
              Opportunity opp = new Opportunity();
              opp.Name = serv.Name;
              opp.AccountId = serv.AccountId;
              opp.Order_Type__c = 'AMS';
              opp.StageName = 'Value Proposition';
              opp.CloseDate = serv.EndDate;
              opp.Related_Service_Contract_del__c  = serv.Id; 
              opp.Legal_Entity__c = serv.Legal_Entity__c;
              opp.System_Type__c = 'Maintenance Renewal';
              opp.Type = 'Existing Business';
              opp.LeadSource = 'Maintenance';
              insert opp;

                  for (ContractLineItem cli : [SELECT c.ID, c.Description, c.Quantity, c.TotalPrice, c.Total_AMS_Value_del__c, c.Pricebookentry.Name, c.PriceBookEntryId 
                                    From ContractLineItem c])
                    {              
                      OpportunityLineItem oppItem = new OpportunityLineItem();
                      oppItem.PriceBookEntryId = cli.PriceBookEntryId;
                      oppItem.Description = cli.Description;
                      oppItem.OpportunityId = opp.Id;
                      oppItem.Quantity = cli.Quantity;
                      oppItem.TotalPrice = cli.Total_AMS_Value_del__c;
                      lineItems.add(oppItem);
                      upsert lineItems;
                    }     

              }     
        } 
    }

With the following @istest class:

@isTest
        private class serviceContract_ScheduledTest{


           static testmethod void testschedule(){
            Test.StartTest();
         serviceContract_Scheduled sh1 = new serviceContract_Scheduled();      
         String sch = '0  00 1 3 * ?';
           system.schedule('Test', sch, sh1);




//Set PriceBook Id
Id pricebookId = Test.getStandardPricebookId();

//Create servicecontract
ServiceContract sc = new ServiceContract(
    Name = 'Test',
    StartDate = system.today() - 10,
    EndDate = system.today(),
    pricebook2Id = pricebookId
    );
insert sc;


//Create opportunity
Opportunity oppt = new Opportunity(
    Name = 'Test',
    CloseDate = system.today(),
    StageName = 'Closed Won',
    Order_Type__c = 'AMS'
    );
 insert oppt;


//Create your product
Product2 prod = new Product2(
     Name = 'Product X',
     ProductCode = 'Pro-X',
     isActive = true

);
insert prod;

//Create your pricebook entry
PricebookEntry pbEntry = new PricebookEntry(
     Pricebook2Id = pricebookId,
     Product2Id = prod.Id,
     UnitPrice = 100.00,
     IsActive = true
);
insert pbEntry;

//create your contract line item.
ContractLineItem coli = new ContractLineItem(
     ServiceContractId = sc.Id,
     StartDate = sc.StartDate,
     EndDate = sc.EndDate,
     Quantity = 5,
     PricebookEntryId = pbEntry.Id,
     UnitPrice = pbEntry.UnitPrice
);

insert coli;

//Create a list of servicecontracts and add sc
list<ServiceContract> listServiceContracts = new list<ServiceContract>(); 
listServiceContracts.add(sc);
upsert listServiceContracts;


list<ContractLineItem> listCLI = new list<ContractLineItem>();
list<Opportunity> oppList = new list<Opportunity>();
list<OpportunityLineItem> oliList = new list<OpportunityLineItem>();


         for(ServiceContract serv : listServiceContracts) {
             Opportunity opp = new Opportunity();
             opp.Name = 'Test';
             opp.Order_Type__c = 'AMS';
             opp.CloseDate = system.today();
             opp.StageName = 'Closed Won';
             oppList.add(opp);
             insert oppList;

             for (ContractLineItem cli : listCLI)
                    {              
                      OpportunityLineItem oppItem = new OpportunityLineItem();
                      oppItem.PriceBookEntryId = cli.PriceBookEntryId;
                      oppItem.Description = cli.Description;
                      oppItem.OpportunityId = opp.Id;
                      oppItem.Quantity = cli.Quantity;
                      oppItem.TotalPrice = cli.TotalPrice;
                      oliList.add(oppItem);
                      upsert oliList;
                    }     

              }


  Test.stopTest();


    }
  }

But i am getting the following error message:

Error Message System.DmlException: Insert failed. First exception on row 0; first error: CANNOT_INSERT_UPDATE_ACTIVATE_ENTITY, updateContractLineItem: execution of AfterInsert

caused by: System.NullPointerException: Argument cannot be null.

Trigger.updateContractLineItem: line 53, column 1: [] Stack Trace Class.serviceContract_ScheduledTest.testschedule: line 65, column 1

Apparently this has to do with an existing Trigger from 2010, but I am not quite sure what this Trigger is meant to do and why it's interfering with the test class. As you can read from the description it states: Fill the AMS fields on the ServiceContract on base of the contractlineitems. I am not sure why a trigger is needed for this. Removing the Trigger works, but unfortunately that's not an option...

Trigger:

/*
Name:       updateContractLineItem 
Function:   Fill the AMS fields on the ServiceContract on base of the contractlineitems
Date:       25-5-2010
*/
trigger updateContractLineItem on ContractLineItem (after update, after insert) {

    // loop through all contractlineitems and get servicecontract
    Id[] scIds = new Id[]{};
    for (ContractLineItem cli : Trigger.new) {
        scIds.add(cli.ServiceContractId);
    }

    // select all contractlineitems 
    ContractLineItem[] cliList = [Select ServiceContract.Id, ServiceContract.Total_AMS_Base__c, 
                                    ServiceContract.Total_AMS__c, Quantity, AMS_Price__c, Sales_Price_Indexed__c, 
                                    PricebookEntry.UnitPrice, UnitPrice, ServiceContract.Account.Software_Discount__c
                                    From ContractLineItem 
                                    Where ServiceContract.Id In :scIds
                                    Order By ServiceContract.Id Asc];
    ServiceContract[] scList = new ServiceContract[]{};

    // loop through servicecontract and update AMS values
    Id scId;
    Double Total_AMS_Base;
    Double Total_AMS;
    Integer counter = 1;
    Integer total = cliList.size();
    ServiceContract sc;
    for (ContractLineItem cli : cliList) {
        if (scId == null || cli.ServiceContract.Id != scId) {
            // add values to servicecontract and clear values
            if (scId != null) {
                sc.Total_AMS_Base__c = Total_AMS_Base;
                sc.Total_AMS__c = Total_AMS;
                scList.add(sc);
            }
            sc = cli.ServiceContract;
            Total_AMS_Base = 0;
            Total_AMS = 0;
        }
        // is record inserted, then worklow isn't fired yet
        if (cli.Sales_Price_Indexed__c == null) {
            if (cli.ServiceContract.Account.Software_Discount__c > 0.00) {
                Total_AMS_Base += (cli.Quantity * cli.PricebookEntry.UnitPrice);
            } else {
                Total_AMS_Base += (cli.Quantity * cli.UnitPrice);
            }
        } else {
            Total_AMS_Base += (cli.Quantity * cli.Sales_Price_Indexed__c);
        }
        Total_AMS += (cli.Quantity * cli.AMS_Price__c);
        if (counter == total) {
            // latest entrie, add values
            sc.Total_AMS_Base__c = Total_AMS_Base;
            sc.Total_AMS__c = Total_AMS;
            scList.add(sc);
        }
        scId = cli.ServiceContract.Id;
        counter++;
    }

    try {
        update scList;
    } catch (Exception e) {

    }

}

Any idea how I could edit the test class to not interfere, or to edit the trigger to make sure the functionality still works but doesn't interfere?

PS If you have any Best Practice advice on my code (I have a feeling I am being very verbose) I'd love to hear it as well!

3

Your code will quickly violate governor limits. You have DML statements inside for loops, plus queries inside for loops. You'll need to restructure your code so that you don't hit these limits, which will look something like this:

Map<Id, Opportunity> opps = new Map<Id, Opportunity>();
for(ServiceContract record: [select ... from servicecontract ...]) {
    opps.put(record.Id, new Opportunity(...));
}
insert opps.values();
OpportunityLineItem[] lines = new OpportunityLineItem[0];
for(ContractLineItem record: [select servicecontractid ... from contractlineitem where servicecontractid = :opps.keyset()]) {
    lines.add(new OpportunityLineItem(OpportunityId=opps.get(record.ServiceContractId).Id ...));
}
insert lines;

As for your trigger, it looks like the error might be here:

Total_AMS += (cli.Quantity * cli.AMS_Price__c);

This is because either Total_AMS or AMS_Price__c is null (possibly both). Your trigger has a number of other concerns related to your unit test as well. Your unit test isn't technically "wrong" in pointing out this error, as it is doing what it is intended to do: point out that there are logic bugs in your code.

If possible, consider writing this trigger as a Flow instead, or sit down and spend the time to understand why you're getting the exception. You can't just "fix" the unit test, because you have flaws that could cause problems in production if you deploy this code. Just because it coincidentally "happens" to have worked all these years, it's fragile and only takes one piece of data to crash the entire transaction.

Your unit test itself should not be creating an opportunity or opportunity line items directly. It should instead insert the service contract and its line items, then run the scheduled class, and finally validate that the opportunity and its line items were created by querying the records.

2
  • Appreciate the answer, still have a long way to go then.. I'll go ahead and hit the documentation to see how I can better restructure the code. Regarding the trigger, I am not quite sure what the intention was, since it was written before my time. I'll try to understand the reasoning behind the trigger and see if I can put it in a flow, thanks! – Johan de Wild Feb 9 '17 at 10:57
  • Updated my code reflecting the changes that you suggested, and it works beautifully. Now all I need to figure out is how to test it and how to put the intention of the Trigger in a flow. Thank you so much! – Johan de Wild Feb 9 '17 at 12:06

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.