Skip to main content

Create New Subscription, Amend Subscription, and Renew Subscription Quotes

Zuora

Create New Subscription, Amend Subscription, and Renew Subscription Quotes

The Create Quote page appears after user selects the type of quote they want to create and provides the related information for linking the new subscription, amendment, or renewal in Z-Billing.

This sample code applies to Zuora Quotes 5.92 or earlier versions.

In the later versions of Zuora Quotes, you can use the CreateQuote component to create the custom page.

Mandatory fields and field validations

The mandatory field values required are different for each type of quote. Some fields should be read-only for certain types of quotes. The existing Zuora Quotes logic is given in the sample page and controller, but you can add the enforcement of this logic in any stage of the Quote-to-Subscription flow. Without these required validations, a quote cannot be sent to Z-Billing to create a subscription.

Some examples of validations are:

  • For an Amendment type quote, the Start Date is mandatory and the Renewal Term should not be changed.
  • For a Renewal type quote, the Start Date is read-only and populated from the Z-Billing Subscription Term End Date. In addition, Renewal Term is a mandatory input field.

Sample code

  1. The NewQuote page contains the fields to be entered for proper quote creation. There are some mandatory fields per Zuora Quotes logic shown in the sample page. However, you can decide to not provide them on this page and can provide them at any stage before sending the quote to Zuora Billing for the actual creation of the Subscription.
  2. You can add any custom field and provide any specific business logic on this page before creating the actual quote.
  3. The Save button creates the new quote. You must perform the following actions to keep the Zuora Quotes logic intact:
    1. Set the Record Type and Subscription Type for all types of Quotes.
    2. For quotes of type Amendment, populate the original subscription information to quote as retrieved from Z-Billing.
    3. For quotes of type Renewal, call the global method renewQuote (Quote__c quote).
  4. The NewQuote Visualforce page and the NewQuoteController controller create a quote record. The record will be categorized on two fields: Record Type and Subscription Type. The page will receive all of the parameters passed from PrepareNewQuote and will then create the corresponding quote record.
<apex:page id="NewQuote" 
  showHeader="true" 
  sideBar="true" 
  standardController="zqu__NewQuoteController__c" 
  ​​​action="{!startFlow}" />
public with sharing class NewQuoteController extends zqu.PropertyComponentController.ParentController {
 ApexPages.StandardController controller;
 public zqu__Quote__c quote {
   get;
   set;
 }
 public zqu.PropertyComponentOptions theOptions {
   get;
   set;
 }
 public Opportunity opp {
   get;
   set;
 }
 public zqu.NotificationOptions notificationOptions {
   get {
     if (notificationOptions == null) notificationOptions = new zqu.NotificationOptions();
     return notificationOptions;
   }
   set;
 }
 public String quoteType {
   get;
   set;
 }
 public Zuora.zObject subscription {
   get;
   set;
 }
 private static final Map < String, Schema.RecordTypeInfo > recordTypeInfoMap = zqu__Quote__c.sObjectType.getDescribe().getRecordTypeInfosByName();
 private static final String CANCEL_QUOTE_WIZARD_WARNING = 'Are you sure you want to cancel? This action will delete the quote you are currently creating.';
 // Constructor
 public NewQuoteController(ApexPages.StandardController stdController) {
   System.debug('NewQuoteController(stdCtrl) executed!');
   this.controller = stdController;
   this.quote = (zqu__Quote__c)this.controller.getRecord();
   // Get opportunity from id in URL parameters
   final String oppId = ApexPages.currentPage().getParameters().get('oppid');
   if (String.isBlank(oppId)) {
     throw new zqu.ZQException('Need to specify the oppid in the url.');
   }
   setOpportunity(oppId);
   // Initialize property component options
   theOptions = new zqu.PropertyComponentOptions();
   theOptions.objectName = 'zqu__Quote__c';
   theOptions.objectId = this.quote.Id != null ? this.quote.Id : null;
   theOptions.viewType = zqu.ViewConfigurationManager.VIEW_CONFIGURATION_VIEW_TYPE_CREATE;
   theOptions.propertyPageTitle = 'Create Quote Sample';
   theOptions.isEditMode = true;
   // Initialize notification options
   notificationOptions.isPopup = false;
   theOptions.notificationOptions = notificationOptions;
   // Set to detail mode
   String mode = ApexPages.currentPage().getParameters().get('mode');
   if (this.quote.Id != null && mode == 'detail')
     theOptions.isEditMode = false;
   theOptions.renderButtonBar = theOptions.isEditMode;
   theOptions.renderBackButton = true;
   theOptions.parentController = this;
   theOptions.instanceName = 'sampleProperty';
   // Get the quote type from the URL paramters
   this.quoteType = ApexPages.currentPage().getParameters().get('quoteType');
   if (String.isBlank(this.quoteType)) {
     throw new zqu.ZQException('Need to specify the quotetype in the url.');
   }
   // Set the record type Id based on the quote type
   final String recordTypeName = this.quotetype == 'Subscription' ? 'Default' : this.quoteType;
   theOptions.recordTypeId = recordTypeInfoMap.get(recordTypeName).getRecordTypeId();
   theOptions.populateValuePlugin = 'NewQuoteController.PopulateDefaultFieldValuePlugin';
   theOptions.relatedObjectPlugin = 'NewQuoteController.PopulateRelatedObjectFieldPlugin';
   theOptions.goBackPlugin = 'NewQuoteController.GoBackPlugin';
   theOptions.updatePlugin = 'NewQuoteController.UpdateRecordPlugin';
   theOptions.cancelPlugin = 'NewQuoteController.CancelRecordPlugin';
   // Set read only fields
   theOptions.readonlyFields.add('zqu__Opportunity__c');
   theOptions.readonlyFields.add('zqu__SubscriptionVersion__c');
   if(this.quoteType == 'Renewal') theOptions.readonlyFields.add('zqu__Subscription_Term_Type__c');
   // For existing billing accounts, currency should be read only
   if(String.isNotBlank(ApexPages.currentPage().getParameters().get('billingAccountId'))) theOptions.readonlyFields.add('zqu__Currency__c');
   if (this.opp != null) {
     // Set up options for Bill To Contact lookup field
     zqu.LookupComponentOptions optionsForBillTo = new zqu.LookupComponentOptions();
     optionsForBillTo.objectName = 'Contact';
     optionsForBillTo.Id = 'BillToContact';
     optionsForBillTo.contextParameters = new Map < String, String > {
       'objectId' => this.opp.Id
     };
     optionsForBillTo.isEditMode = theOptions.isEditMode;
     optionsForBillTo.isRequired = true;
     optionsForBillTo.lookupComponentControllerName = 'zqu.ContactLookupComponentController';
     optionsForBillTo.recordTypeId = Contact.SObjectType.getDescribe().getRecordTypeInfosByName().get('Master').getRecordTypeId();
     optionsForBillTo.popupWindowTitle = 'Bill to Contact Lookup';
     optionsForBillTo.fieldLabel = 'Bill to Contact';
     // Set up options for Sold To Contact lookup field
     zqu.LookupComponentOptions optionsForSoldTo = new zqu.LookupComponentOptions();
     optionsForSoldTo.objectName = 'Contact';
     optionsForSoldTo.Id = 'SoldToContact';
     optionsForSoldTo.contextParameters = new Map < String, String > {
       'objectId' => this.opp.Id
     };
     optionsForSoldTo.isEditMode = theOptions.isEditMode;
     optionsForSoldTo.isRequired = true;
     optionsForSoldTo.lookupComponentControllerName = 'zqu.ContactLookupComponentController';
     optionsForSoldTo.recordTypeId = Contact.SObjectType.getDescribe().getRecordTypeInfosByName().get('Master').getRecordTypeId();
     optionsForSoldTo.popupWindowTitle = 'Sold to Contact Lookup';
     optionsForSoldTo.fieldLabel = 'Sold To Contact';
     theOptions.lookupFields = new Map < String, zqu.LookupComponentOptions > {
       'zqu__BillToContact__c' => optionsForBillTo,
         'zqu__SoldToContact__c' => optionsForSoldTo
     };
   }
 }
 // Plugin to set field default values
public class PopulateDefaultFieldValuePlugin implements IPopulateValuePlugin {
 // ZApi for querying existing subscription and billing account from Zuora
 private Zuora.zApi api {
   get {
     if (api == null) api = new Zuora.zApi();
     return api;
   }
   set;
 }
 public void populateDefaultFieldValue(SObject record, zqu.PropertyComponentController.ParentController pcc) {
   // Get NewQuoteController instance
   NewQuoteController parentController = (NewQuoteController) pcc;
   Opportunity opportunity = parentController.opp;
   // Set default field values when create new quote
   if (parentController.quote.Id == null && opportunity != null) {
     // Set default opportunity
     record.put('zqu__Opportunity__c', opportunity.Id);
   }
   record.put('Name', 'Sample Quote Name');
   if ('Amendment' == parentController.quoteType || 'Renewal' == parentController.quoteType) {
     final String billingAccountId = ApexPages.currentPage().getParameters().get('billingaccountid');
     if (String.isNotBlank(billingAccountId)) {
       record.put('zqu__ZuoraAccountId__c', billingAccountId);
     }
     else {
       throw new zqu.ZQException('Need to specify the billingaccountid for ' + parentController.quoteType + ' in the url.');
     }
     final String existSubscriptionID = ApexPages.currentPage().getParameters().get('subscriptionid');
     
     if (String.isNotBlank(existSubscriptionId)) {
       record.put('zqu__existSubscriptionID__c', existSubscriptionId);
     }
     else {
       throw new zqu.ZQException('Need to specify the existsubscriptionid for ' + parentController.quoteType + ' in the url.');
     }
     try {
       api.zlogin();
       Zuora.zObject acczobj = this.getBillingAccount(billingAccountID);
       if (acczobj != null) {
         record.put('zqu__Currency__c', (String) acczobj.getValue('Currency'));
       }
       final Zuora.zObject subzobj = this.getSubscription(existSubscriptionId);
       if (null != subzobj) {
         parentController.subscription = subzobj;
         // For amendments
         if ('Amend' == parentController.quoteType) {
           record.put('zqu__StartDate__c', ((Datetime) subzobj.getValue('TermStartDate')).date());
         }
         // For renewals
         else {
           record.put('zqu__StartDate__c', ((Datetime) subzobj.getValue('TermEndDate')).date());
         }
         // Populate terms and conditions from original subscription
         record.put('zqu__AutoRenew__c', (Boolean) subzobj.getValue('AutoRenew'));
         record.put('zqu__InitialTerm__c', (Integer) subzobj.getValue('InitialTerm'));
         record.put('zqu__RenewalTerm__c', (Integer) subzobj.getValue('RenewalTerm'));
         record.put('zqu__SubscriptionTermStartDate__c', ((Datetime) subzobj.getValue('TermStartDate')).date());
         record.put('zqu__SubscriptionTermEndDate__c', ((Datetime) subzobj.getValue('TermEndDate')).date());
         record.put('zqu__Subscription_Term_Type__c', (String) subzobj.getValue('TermType'));
         record.put('zqu__Hidden_Subscription_Name__c', (String) subzobj.getValue('Name'));
         record.put('zqu__SubscriptionVersion__c', (Integer) subzobj.getValue('Version'));
       }
     }
     catch (Exception e) {
       throw new zqu.ZQException(e.getMessage());
     }
   }
   // Set record type and subscription type based on quote type URL parameter
   if (parentController.quoteType == 'Subscription') {
     record.put('zqu__SubscriptionType__c', 'New Subscription');
     record.put('RecordTypeId', NewQuoteController.recordTypeInfoMap.get('Default').getRecordTypeId());
   }
   else if (parentController.quoteType == 'Amendment') {
     record.put('zqu__SubscriptionType__c', 'Amend Subscription');
     record.put('RecordTypeId', NewQuoteController.recordTypeInfoMap.get('Amendment').getRecordTypeId());
   }
   else if (parentController.quoteType == 'Renewal') {
     record.put('zqu__SubscriptionType__c', 'Renew Subscription');
     record.put('RecordTypeId', NewQuoteController.recordTypeInfoMap.get('Renewal').getRecordTypeId());
   }

   // For edit / detail mode, make sure the quote is get from record of property component
   if (parentController.quote.Id != null) {
     parentController.quote = (zqu__Quote__c) record;
   }
 }
 private Zuora.zObject getSubscription(String subscriptionId) {
   final Zuora.zObject subzobj;
   final String zoqlsubscription = 'Select Id, Name, Version, AccountId, OriginalId, ContractEffectiveDate, AutoRenew, InitialTerm, TermStartDate, TermEndDate, TermType, RenewalTerm from Subscription where Id = \'' + subscriptionId + '\'';
   final List < Zuora.zObject > subzobjs = api.zquery(zoqlsubscription);
   if (subzobjs.size() == 1) subzobj = subzobjs[0];
   return subzobj;
 }
 private Zuora.zObject getBillingAccount(String billingaccountId) {
   final Zuora.zObject acczobj;
   final String zoqlacc = 'SELECT BillToId,SoldToId,Currency from Account where Id=\'' + billingaccountId + '\'';
   final List < Zuora.zObject > acczobjs = api.zquery(zoqlacc);
   if (acczobjs.size() == 1) acczobj = acczobjs[0];
   return acczobj;
 }
}
 // Plugin to populate related objects
 public class PopulateRelatedObjectFieldPlugin implements IRelatedObjectPlugin {
   public Map < String, SObject > getRelatedObject(zqu.PropertyComponentController.ParentController pcc) {
     // Get NewQuoteController instance
     NewQuoteController parentController = (NewQuoteController) pcc;
     Map < String, SObject > relatedObjectMap = new Map < String, SObject > ();
     // Set value for related object field : Opportunity__r.AccountId
     relatedObjectMap.put('Opportunity__r', parentController.opp);
     return relatedObjectMap;
   }
 }
 //Plugin for Back button
 public virtual class GoBackPlugin implements IGoBackPlugin {
   public virtual PageReference goBack(SObject record, zqu.PropertyComponentController.ParentController pcc) {
     //Cast parent controller
     NewQuoteController parentController = (NewQuoteController) pcc;
     PageReference pageRef;
     
     //If on step number 1, cancel the quote wizard
     if (ApexPages.currentPage().getParameters().get('stepNumber') == '1') {
       pageRef = zqu.QuoteWizardManager.cancel(parentController.getReturnUrl());
     }
     //Else, revert the quote wizard
     else {
       if(parentController.opp != null){
         pageRef = zqu.QuoteWizardManager.navigateBack(new Map < String, String > {
           'oppId' => parentController.opp.Id
         });
       }
       else {
         Map<String, String> urlParams = new Map<String, String>();
         if(ApexPages.currentPage().getParameters().get('crmAccountId') != null){
           urlParams.put('crmAccountId', ApexPages.currentPage().getParameters().get('crmAccountId'));
         }
         if(ApexPages.currentPage().getParameters().get('retUrl') != null){
           urlParams.put('retUrl', ApexPages.currentPage().getParameters().get('retUrl'));
         }
         pageRef = zqu.QuoteWizardManager.navigateBack(urlParams);
       }
     }
     return pageRef;
   }
 }
 // Plugin for Save button
 public virtual class UpdateRecordPlugin implements IUpdatePlugin {
   public virtual PageReference doUpdate(SObject record, zqu.PropertyComponentController.ParentController pcc) {
     // Get ZQQuoteEditController instance
     NewQuoteController parentController = (NewQuoteController) pcc;
     // Set changes to be processed for T&C amendments
     parentController.prepareTermsAndConditionChanges(record);
     // Changed for upsert
     try{
       upsert record;
     } catch (Exception e){
       ApexPages.addMessages(e);
       return null;
     }
     // Map of URL parameters for next page
     Map < String, String > urlParams = new Map < String, String > {
       'Id' => record.Id
     };
     // Use quoteWizardManager to navigate to the next page of the quoting flow
     PageReference pageRef = zqu.QuoteWizardManager.navigateNext(urlParams);
     return pageRef;
   }
 }
 // Plugin for Cancel button
 public virtual class CancelRecordPlugin implements ICancelPlugin {
   public virtual PageReference doCancel(SObject record, zqu.PropertyComponentController.ParentController pcc) {
     // Get NewQuoteController instance
     NewQuoteController parentController = (NewQuoteController) pcc;
     // Put the embedded notification component into popup mode
     parentController.enablePopup();
     // Display warning message on popup notification
     ApexPages.addMessage(new ApexPages.Message(ApexPages.Severity.WARNING, CANCEL_QUOTE_WIZARD_WARNING));
     return null;
   }
 }
 // Private helper methods
 // Enable popup mode for notification component
 private void enablePopup() {
   notificationOptions.isPopup = true;
   notificationOptions.continueAction = 'hidePopupNotification(); displayStatusModal(); doCancel()';
   notificationOptions.backAction = 'hidePopupNotification(); closeStatusModal(); goBack();';
 }
 // Disable popup mode for notification component
 private void disablePopup() {
   notificationOptions.isPopup = false;
   notificationOptions.continueAction = '';
   notificationOptions.backAction = '';
 }
 private String getReturnUrl() {
   String retUrl = null;
   if (this.opp != null) {
     retUrl = '/' + this.opp.Id;
   }
   else if (ApexPages.currentPage() != null && ApexPages.currentPage().getParameters().get('retUrl') != null) {
     retUrl = ApexPages.currentPage().getParameters().get('retUrl');
   }
   else {
     retUrl = Page.zqu__QuoteList.getUrl();
   }
   return retUrl;
 }
 public void prepareTermsAndConditionChanges(SObject record) {
   // Cast the record into a quote object
   zqu__Quote__c quote = (zqu__Quote__c)record;
   // Do not process T&C amendments if the quote is not linked to a subscription
   if(String.isBlank(quote.zqu__ExistSubscriptionID__c)) return;
   // Get the terms and condition changes
   Map < String, Object > termsAndConditionChanges = getTermAndConditionChanges(this.subscription, quote);
   // Pass the changes to subscriptionTermsMap to be used when the quote record is upserted
   zqu.zQuoteUtil.setSubscriptionTermChanges(new Map<String, Map<String, Object>>{quote.zqu__ExistSubscriptionID__c => termsAndConditionChanges});
 }
 private static Map < String, Object > getTermAndConditionChanges(Zuora.zObject originalSub, zqu__Quote__c quote) {
   // Initialize changes to empty map
   Map < String, Object > changes = new Map < String, Object > ();
   // First, add the differences for AutoRenew and RenewalTerm (they are valid for both amendments and renewals)
   if ((Boolean)originalSub.getValue('AutoRenew') != quote.zqu__AutoRenew__c) changes.put('zqu__AutoRenew__c', String.valueOf(quote.zqu__AutoRenew__c));
   if ((Integer)originalSub.getValue('RenewalTerm') != quote.zqu__RenewalTerm__c && quote.zqu__RenewalTerm__c != 0) changes.put('zqu__RenewalTerm__c', quote.zqu__RenewalTerm__c);
   
   // If the update version of the quote has a type of Renewal, do not generate differences for the remaining fields (they are not valid for renewal quotes)
   if (quote.zqu__SubscriptionType__c == 'Renew Amendment') return changes;
   // Compare every terms/conditions field of the quote to the original subscription information, adding any discrepancies found
   if ((String)originalSub.getValue('TermType') != quote.zqu__Subscription_Term_Type__c) changes.put('zqu__Subscription_Term_Type__c', quote.zqu__Subscription_Term_Type__c);
   if (((Datetime)originalSub.getValue('TermStartDate')).dateGMT() != quote.zqu__SubscriptionTermStartDate__c) changes.put('zqu__TermStartDate__c', quote.zqu__SubscriptionTermStartDate__c);
   if ((Integer)originalSub.getValue('InitialTerm') != quote.zqu__InitialTerm__c && quote.zqu__InitialTerm__c != 0) changes.put('zqu__InitialTerm__c', quote.zqu__InitialTerm__c);
   return changes;
 }
 // Public methods called from javascript in page
 // When user clicks cancel on popup notification component
 public PageReference cancelQuoteWizard() {
   return zqu.QuoteWizardManager.cancel(getReturnUrl());
 }
 // When user clicks goBack on popup notification, disable popup and rerender page without taking any action
 public PageReference goBack() {
   disablePopup();
   return null;
 }
 public void setOpportunity(Id oppId) {
   String opp_query;
   if (UserInfo.isMultiCurrencyOrganization()) {
     opp_query = 'SELECT Id, Name,CurrencyISOCode, Account.Id, Account.Name FROM Opportunity WHERE Id = \'' + oppId + '\'';
   }
   else {
     opp_query = 'SELECT Id, Name, Account.Id, Account.Name FROM Opportunity WHERE Id = \'' + oppId + '\'';
   }
   this.opp = Database.query(opp_query);
 }
}

Considerations

  • The Zuora Quotes validations are enforced for each type of Quote in the New Quote page. If you do not include these validations, sending the information to Z-Billing will fail and the Quote-to-Subscription process will not be complete.
  • After the creating a quote, you can redirect the user to any page, such as a custom Product Selector wizard or a Quote Detail page. The sample page in the code will take the user to the Quote Detail page.
  • The sample controller code is not reading the Zuora Quote Configuration parameters, which is different with Zuora Quotes logic. Zuora Quotes reads the configuration settings populated by User to automatically populate and validate the quote information during creation. This is left to you to use our global method, getZuoraConfigInformation(), and use those parameters to automatically populate and validate the Quote information. See Global Classes for more information.