Background:
My company sells software. In my org, we sell subscriptions to our software and addons, or subscriptions for software support. We have 3 main types of subscriptions (Support, Software, Communication).
Objects:
Zuora__Subscription__c : Tracks if subscription is active or cancelled.
Zuora__SubscriptionProductCharge__c : Tracks the type of Subscription (from 3 options above)
Zuora__Subscription__c is a child of Account, and Zuora__SubscriptionProductCharge__c (SPC) is a child of Zuora__Subscription__c.
Situation:
I have an apex class that is responsible for sending email alerts to our accounting department if we find more than one ACTIVE subscription of the same TYPE. The types, mentioned above, are stored in a text field on the SubscriptionProductCharge object, and the Active status is on the Subscription object. This code works very well 90% of the time, but recently I have been haunted by null reference exceptions that I can't pinpoint.
Problem:
I've tracked it down to a problem where my accToSPCMap Map sometimes doesn't contain the key, but from my understanding of the code, there shouldn't ever be a situation where something can end up in the map without the key (Account Id). Can someone pleeeease look through my code and tell me why or how records with no associated Account Id are making it into my map? I'm stumped.
Help me Stack Exchange, you're my only hope.
Code:
public with sharing class SendAlertForRelatedProductPlans
{
// Make the constructor private since this is a utility class that should not be instantiated
private SendAlertForRelatedProductPlans()
{
}
public static void checkNewSubscriptionProductCharges(List<Zuora__SubscriptionProductCharge__c> newSubscriptionProductCharges)
{
// The below code will query for the accounts related to the SPCs that are being inserted
Set<Id> accountIdSet = new Set<Id>();
List<Messaging.SingleEmailMessage> messages = new List<Messaging.SingleEmailMessage>();
for(Zuora__SubscriptionProductCharge__c spc : newSubscriptionProductCharges)
{
if(spc.Zuora__Account__c != null && spc.Zuora__Subscription__c != null) // Make sure the SPCs are related to an account and subscription
{
accountIdSet.add(spc.Zuora__Account__c);
}
}
if(accountIdSet.size() == 0)
{
System.debug('The SPCs being inserted were not associated with accounts, quitting early.');
return; // If none of the SPCs are related to accounts, quit early
}
List<Account> accounts = [SELECT Id, Name, Customer_Number__c FROM Account WHERE Id IN :accountIdSet];
// For each account, we'll need to query for *all* the related active subscriptions.
Map<Id, Zuora__Subscription__c> subscriptions = new Map<Id, Zuora__Subscription__c>([SELECT Id, Name, Zuora__Account__c
FROM Zuora__Subscription__c
WHERE Zuora__Account__c IN: accountIdSet
AND Zuora__Status__c = 'Active'
AND Name != null]);
// If there are no active subscriptions associated with the accounts specified, we should not continue.
if(subscriptions.size() == 0)
{
System.debug('There were no active subscriptions associated with the SPCs being inserted. Quitting early.');
return;
}
Set<Id> subscriptionIdSet = subscriptions.keySet();
// We also need to query for all SPCs associated with the above active subscriptions.
// Retrieve any SPCs that are:
// (a) Associated with the set of subscriptions ids
// (b) The subscriptions must be active
// (c) Associated with the set of account ids within this trigger context
// (d) The SPC isn't a Discount Record
// (e) The SPC has a Rate Plan Classification
Map<Id,Zuora__SubscriptionProductCharge__c> spcsAssociatedWithActiveSubscription = new Map<Id,Zuora__SubscriptionProductCharge__c>(
[SELECT Id, RatePlanClassification__c, Zuora__Account__c, Zuora__Subscription__c
FROM Zuora__SubscriptionProductCharge__c
WHERE Name != 'Discount'
AND RatePlanClassification__c != null
AND Zuora__Subscription__c IN : subscriptionIdSet
AND Zuora__Subscription__r.Zuora__Status__c = 'Active'
AND Zuora__Account__c IN :accountIdSet]);
Map<Id, List<Zuora__SubscriptionProductCharge__c>> accToSPCMap = new Map<Id, List<Zuora__SubscriptionProductCharge__c>> ();
for(Zuora__SubscriptionProductCharge__c zs: spcsAssociatedWithActiveSubscription.values()){
List<Zuora__SubscriptionProductCharge__c> sub = accToSPCMap.get(zs.Zuora__Account__c);
if(sub == null){
sub = new List<Zuora__SubscriptionProductCharge__c>();
sub.add(zs);
accToSPCMap.put(zs.Zuora__Account__c, sub);
}else{
sub.add(zs);
}
}
// The below two maps will capture whether a subscription has:
// (a) at least one 'Support' related SPC
// (b) at least one 'Software' related SPC
Integer count = 0;
System.debug('KLE accounts : '+accounts.size());
for(Account account : accounts){
Set<ID> subscriptionIdHasSupportSPCs = new Set<ID>();
Set<ID> subscriptionIdHasSoftwareSPCs = new Set<ID>();
Set<ID> subscriptionIdHasCommSPCs = new Set<ID>();
if(accToSPCMap.size() > 0){
System.debug('MTM acctToSPCMap containsKey:' +accToSPCMap.containsKey(account.id));
if(accToSPCMap.get(account.id).size() > 0){ // THROWS ERROR ON THIS LINE - because of get(account.id) since account.id isn't in the map?
System.debug('MTM acctToSPC MapSize>0 containsKey:' +accToSPCMap.containsKey(account.id));
System.debug('KLE spc per account : '+accToSPCMap.get(account.id).size() + ' for acc: '+account.id); // THROWS ERROR ON THIS LINE - because of get(account.id) since account.id isn't in the map?
for(Zuora__SubscriptionProductCharge__c spc : accToSPCMap.get(account.id)) // THROWS ERROR ON THIS LINE - because of get(account.id) since account.id isn't in the map?
{
if(spc.RatePlanClassification__c == 'Support')
{
subscriptionIdHasSupportSPCs.add(spc.Zuora__Subscription__c);
}
else if(spc.RatePlanClassification__c == 'Software')
{
subscriptionIdHasSoftwareSPCs.add(spc.Zuora__Subscription__c);
}
else if(spc.RatePlanClassification__c == 'Communication')
{
subscriptionIdHasCommSPCs.add(spc.Zuora__Subscription__c);
}
count++;
} // End of loop through SPCs
}
}
Error Being Generated:
Apex script unhandled trigger exception by user/organization:
SubscriptionProductChargeTrigger: execution of AfterInsert
caused by: System.NullPointerException: Attempt to de-reference a null object
Class.SendAlertForRelatedProductPlans.checkNewSubscriptionProductCharges: line 107, column 1
Trigger.SubscriptionProductChargeTrigger: line 35, column 1