The purpose of this guide is to provide technical instructions on the installation and setup of an Apttus E-Commerce storefront. The intended audience are developers who will be creating and maintaining new storefronts on a Salesforce Apttus installation. If you haven't already, please contact Apttus to get the E-Commerce package installed in your org prior to installation.
The Apttus E-Commerce SDK is intended to work with the unified data model of the underlying Apttus Quote-to-cash platform. This means, there are no additional data setup steps in order to create and maintain your catalog within Apttus outside of what you may already do if you use Apttus CPQ. That said, this guide assumes you have already created data for the following within Apttus:
The Apttus E-Commerce platform leverages a Salesforce Community to provide authentication and hosting features for guest users. After the e-commerce package is deployed, the next step is to create a Salesforce Community. At minimum, you just need the community URL. However, if you intend to support guest users, you will need to enable that within the community settings. After deployment, the angular library will provide a Visualforce page that you can set as the default page for all page settings within the community (i.e home, login, forgot password, change password etc). Being that its a single page application, it is designed to handle all incoming requests.
Apart from the underlying catalog, the E-Commerce package comes with a store object and tab to map a storefront to a catalog. If you are using an Apttus MDO org, there may already be a 'store' object installed. This object is deprecated in favor of the 'Storefront' object that comes with the E-Commerce package.
After your catalog has been setup within Apttus, the next step is to create a 'Storefront' record. The storefront object is very basic and contains only a couple fields to map a storefront to a price list and logo for the guest user. The price list should look up to the price list you want the guest user to access and the logo should be an id or a url of the logo attachment for the store. The storefront record also has a 'banner' related list that can be used to setup banners for the jumbotron component in the reference template. Remember the name of the storefront you created. This will be used in a later step to associate with a storefront codebase.
The E-Commerce package comes with a basic permission set for providing the necessary access to users. The permission set is named 'Apttus Ecommerce' and should be assigned to users access the e-commerce storefront. If you would like to make any changes to the permissions, you may clone the permission set and make any changes necessary.
In order to install the apttus ecommerce library, you must first be granted access. Please reach out to Apttus support for obtaining access to this library.
To install this library, run:
npm install @apttus/ecommerce --save
During installation, you will be asked for a number of things to connect the application to your salesforce org (will be skipped if you already installed ng-salesforce).
and then from your Angular AppModule
:
import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';
import { AppComponent } from './app.component';
// Import your library
import { SalesforceModule } from 'ng-salesforce';
import { CommerceModule } from '@apttus/ecommerce';
// salesforce.config.ts will be created during installation and saved at src/app/salesforce.config.ts
import { Configuration } from './salesforce.config';
@NgModule({
declarations: [
AppComponent
],
imports: [
BrowserModule,
// Specify your ng-salesforce as an import
SalesforceModule.forRoot(Configuration),
// Specify the Ecommerce Module as in import
// The only parameter is the string name of the APTSMD_Store__c record you want to use.
CommerceModule.forRoot('My Store')
],
providers: [],
bootstrap: [AppComponent]
})
export class AppModule { }
Once your library is imported, you can use its components, directives and pipes in your Angular application:
Proper state management is crucial to a successful Angular PWA. To that end, accessing data within Salesforce requires you to define your model within a Typescript class. Behind the scenes, ng-salesforce is managing how your components access and interact with that data to avoid uncessary operations and maintain synchronicity.
Here's an example that defines the account and contact relationship for a component.
import { SObjectModel, SObject, ChildRecord } from 'ng-salesforce';
// We use the decorator SObjectModel to tell ng-salesforce this class maps to the 'Account' object
// In order to access standard sObject fields, our Account class must extend 'SObject'
// The class name can be anything, but the name property in the SObjectModel decorator must map to an object API name
@SObjectModel({name : 'Account'})
export class Account extends SObject{
// Define the fields you would like in your object. The name of the field must map to a field API name in salesforce
public Name: string = null;
public IsCustomerPortal: boolean = null;
public My_Custom_Field__c: number = null;
//Related Lists
public Opportunities: ChildRecord = new ChildRecord(new Opportunity())
}
@SObjectModel({name : 'Opportunity'})
export class Oppoortunity extends SObject{
public Name: string = null;
public AccountId: string = null;
}
@SobjectModel({name : 'Contact'})
export class Contact extends SObject{
public Name: string = null;
//Lookup
public Account: Account = new Account();
}
After defining your model, you can access the data by creating a service for the models. The SObjectService class contains many of the standard DML and query operations to access the data, however you may add any convenience methods you want to your service. The core service methods are usually very simple.
import { SObjectService, SObjectType } from 'ng-salesforce';
import { Injectable } from '@angular/core';
import { Account } from './account.model.ts' // This is a reference to the account model created in the previous section
@Injectable({
providedIn : 'root'
})
export class AccountService extends SObjectService{
//Add service methods here
type = Account;
}
import { Component, OnOnit } from '@angular/core';
import { AccountService } from './account.service.ts';
import { Account } from './account.model.ts';
@Component({
selector : 'app-account',
template : `
`,
styles : [``]
})
export class AccountComponent implements OnInit{
constructor(private accountService: AccountService){}
// Always perform service methods in the ngOnInit method
ngOnInit(){
this.accountService.where(`Id <> NULL LIMIT 1`).subscribe(res => {
/*
res === [
{
Name : 'Account A',
IsCustomerPortal : false,
My_Custom_Field__c : 1,
Id : 'xxx',
...
Opportunities : {
totalSize : 1,
records : [
Name : 'Opportunity A',
AccountId : 'xxx',
Id : 'xxx',
...
]
}
}
]
*/
});
this.accountService.describe('My_Custom_Field__c', false).subscribe(res => {
// Describe information for My_Custom_Field__c
});
this.accountService.search(`FIND 'map*' IN ALL FIELDS RETURNING Account (Id, Name)`).subscribe(res => {
// SOSL Search Results
// Note : search does not follow the model pattern and will return results specified in the query
});
this.accountService.get(['00Fxxxxxxx', '00Fxxxxxxx']).subscribe(res => {
// Returns an array of account records
})
this.accountService.aggregate(`ID <> NULL`).subscribe(res => {
// Returns aggregates for the specified clause. (i.e. total records as well as max/min values for all
// fields specified in the model)
})
this.accountService.create([new Account()]).subscribe(res => {
// Returns list of id's created
})
this.accountService.update([new Account()]).subscribe(res => {
// Returns list of id's updated
})
this.accountService.upsert([new Account()]).subscribe(res => {
// Returns list of objects upserted
})
this.accountService.delete([new Account()]).subscribe(res => {
// Returns list of boolean values for accounts that were successfully deleted
})
/**
* Low level method to build a structured query
* @Param where clause
* @Param limit (optional)
* @Param offset (optional)
* @Param sort by (optional)
* @Param sort direction (optional)
* @Param ignore cache (optional default false)
* @Param ignore constraints (optional default false)
*/
this.accountService.queryBuilder('ID <> NULL', 5, 2, 'Name', 'ASC', false, false).subscribe(res => {
// Returns query results
})
}
}
To lint all *.ts
files:
$ npm run lint
To deploy your code to your salesforce org
$ npm run deploy
If you've upgraded to @angular/cli 6, and you're seeing the following error
WARNING in C:/Workspace/ngs-workspace/node_modules/xml2js/node_modules/sax/lib/sax.js
Module not found: Error: Can't resolve 'stream' in 'C:\Workspace\ngs-workspace\node_modules\xml2js\node_modules\sax\lib'
ERROR in C:/Workspace/ngs-workspace/node_modules/csv-parse/lib/index.js
Module not found: Error: Can't resolve 'stream' in 'C:\Workspace\ngs-workspace\node_modules\csv-parse\lib'
ERROR in C:/Workspace/ngs-workspace/node_modules/csv-stringify/lib/index.js
Module not found: Error: Can't resolve 'stream' in 'C:\Workspace\ngs-workspace\node_modules\csv-stringify\lib'
ERROR in C:/Workspace/ngs-workspace/node_modules/xml2js/lib/parser.js
Module not found: Error: Can't resolve 'timers' in 'C:\Workspace\ngs-workspace\node_modules\xml2js\lib'
i 「wdm」: Failed to compile.
Add the following to your tsconfig.app.json file under 'compilerOptions':
"paths" : {
"jsforce" : ["./node_modules/jsforce/build/jsforce.min.js"]
...
}
In order to work on a visualforce page, your app needs to be setup to use the hash routing location strategy instead of the default
In your app-routing.module.ts file, set the useHash flag in the RouterModule.forRoot(...) call
@NgModule({
imports: [RouterModule.forRoot(appRoutes, { useHash: true })],
exports: [RouterModule]
})
export class AppRoutingModule { }
Even if you enabled CORS during setup, Salesforce only supports it for certain APIs. This won't be an issue when you deploy to production. However, during development you will see this error for calls to the SOAP API. To bypass this, you must setup a proxy in @angular/cli.
First, create a file called 'proxy.conf.json' in your root directory. This json file will look like the following:
{
"/services/Soap/*": {
"target": "https://my.community.url.force.com",
"secure": false,
"logLevel": "debug",
"changeOrigin": true
}
}
Make sure you set the target to be the community url you are using.
Secondly, point your angular.json configuration to this newly created proxy configuration.
{
...
"projects" : {
...
"myProject" : {
...
"architect" : {
...
"serve" : {
...
"options" : {
...
"proxyConfig": "./proxy.conf.json"
}
}
}
}
}
}
If you've upgraded to @angular/cli 6, and you're seeing the following error
Uncaught ReferenceError: global is not defined
at Object.../../node_modules/lodash.support/index.js (index.js:30)
at __webpack_require__ (bootstrap:81)
at Object.../../node_modules/lodash._basecreatecallback/index.js (index.js:12)
at __webpack_require__ (bootstrap:81)
at Object.../../node_modules/lodash.foreach/index.js (index.js:9)
at __webpack_require__ (bootstrap:81)
at Object.../../node_modules/convert-units/lib/index.js (index.js:3)
at __webpack_require__ (bootstrap:81)
at Object.../../dist/@apttus/ecommerce/fesm5/apttus-ecommerce.js (main.js:98)
at __webpack_require__ (bootstrap:81)
Add the following to your polyfills.ts file:
(window as any).global = window;