Creating a Custom Shipment Constructor
This guide is for Salesforce administrators and developers who want to implement custom shipment construction logic in OrderCentral.
Overview
OrderCentral Commerce uses a plugin architecture for shipment construction. The system dynamically selects a shipment constructor class based on the plugin definition in Salesforce custom metadata. This allows you to override or extend the default shipment construction logic without modifying core code.
Steps to Create and Register a Custom Shipment Constructor
1. Implement Your Custom Constructor Class
Create an Apex class that implements the
IConstructShipmentsinterface or extends the DefaultShipmentsConstructor.Example class skeleton:
public with sharing class MyCustomShipmentsConstructor implements IConstructShipments {
public List<ShipmentData> construct(ConstructShipmentsContext context) {
// Your custom logic here
}
}
2. Register Your Class as the Plugin
Go to Setup > Custom Metadata Types in Salesforce.
Find or create a record for
Plugin.IConstructShipments.Set the
Default_Class_Name__cfield to the name of your custom class (e.g.,MyCustomShipmentsConstructor).Set the
Type__cfield toConstructShipments.
Alternatively, update the metadata file directly if you manage metadata in source control:
<values>
<field>Default_Class_Name__c</field>
<value xsi:type="xsd:string">MyCustomShipmentsConstructor</value>
</values>
<values>
<field>Type__c</field>
<value xsi:type="xsd:string">ConstructShipments</value>
</values>
3. How the System Uses Your Constructor
When the checkout is entered the constructShipments() method on the shipments constructor is called.
Your custom logic is executed to construct shipments for the shopping cart.
How Shipment Constructors Are Selected (Admin UI Steps)
As a Salesforce administrator, you can control which shipment constructor logic is used by updating a custom metadata record in the Salesforce Setup UI:
Go to Setup in Salesforce.
In the Quick Find box, type Custom Metadata Types and select it.
Find and click on Manage Records next to Plugin Registration.
Click on New (or edit to change an existing one).
In the Class Name field, enter the name of the Apex class you want to use for shipment construction (for example,
MyCustomShipmentsConstructor).Make sure to select
Construct Shipmentsin the Plugin field.Save your changes.
The system will now use the specified Apex class for constructing shipments whenever the process runs. No code deployment is needed for this change—just update the metadata record in Setup.
Best Practices
Always implement the required interface (
IConstructShipments).Place your class in the correct domain folder for maintainability.
Test your constructor thoroughly with different cart scenarios.
Use dependency injection and avoid direct DML/SOQL in your constructor logic—leverage selectors and unit of work patterns as per project standards.
Troubleshooting
If your custom logic is not being called, verify the
Default_Class_Name__cvalue in the plugin metadata.Ensure your class is deployed and visible in the org.
Check for errors in the
ShippingServicelogic or plugin instantiation.
By following these steps, you can safely extend or override shipment construction logic in OrderCentral Commerce without modifying core code, ensuring maintainability and upgradability.
Example Shipments Constructor
global class ExampleShipmentsConstructor extends welisacommerce.DefaultShipmentsConstructor {
//override the default constructor to create new shipments
//based on the shopping cart and its items
global override List<welisacommerce.ShipmentData> doConstruct(welisacommerce.ConstructShipmentsContext context) {
//Create two example shipments
welisacommerce.ShipmentData shipment1 = new welisacommerce.ShipmentData();
shipment1.record = new welisacommerce__Shipment__c(
Name = 'Example shipment 1');
shipment1.items = new List<welisacommerce.ShoppingCartData.Item> { context.shoppingCart.items[0] };
welisacommerce.ShipmentData shipment2 = new welisacommerce.ShipmentData();
shipment2.record = new welisacommerce__Shipment__c(
Name = 'Example shipment 2');
shipment2.items = new List<welisacommerce.ShoppingCartData.Item> { context.shoppingCart.items[1] };
return new List<welisacommerce.ShipmentData> { shipment1, shipment2 };
}
global override void populateDefaultValues(welisacommerce.ShipmentData shipment) {
//optional override to implement custom logic for populating default shipment values
shipment.record.welisacommerce__Delivery_Method__c = 'Shipping';
shipment.record.welisacommerce__Shipping_City__c = 'Default City';
shipment.record.welisacommerce__Shipping_Country__c = 'US';
shipment.record.welisacommerce__Shipping_Postal_Code__c = '12345';
shipment.record.welisacommerce__Shipping_State__c = 'US-AL';
shipment.record.welisacommerce__Shipping_Street__c = 'Default Street';
}
}
Example Shipments Constructor Test
@IsTest
private class ExampleShipmentsConstructorTest {
@IsTest
private static void itShouldSetShipmentNameDuringConstruction() {
//GIVEN
welisacommerce.ConstructShipmentsContext context = generateContext();
//WHEN
Test.startTest();
List<welisacommerce.ShipmentData> constructedShipments = new ExampleShipmentsConstructor().doConstruct(context);
Test.stopTest();
//THEN
Assert.isNotNull(constructedShipments[0].record.Name);
Assert.isNotNull(constructedShipments[1].record.Name);
}
@IsTest
private static void itShouldAssignAShoppingCartItemPerShipment() {
//GIVEN
welisacommerce.ConstructShipmentsContext context = generateContext();
//WHEN
Test.startTest();
List<welisacommerce.ShipmentData> constructedShipments = new ExampleShipmentsConstructor().doConstruct(context);
Test.stopTest();
//THEN
Assert.areEqual(constructedShipments[0].items[0], context.shoppingCart.items[0]);
Assert.areEqual(constructedShipments[1].items[0], context.shoppingCart.items[1]);
}
@IsTest
private static void itShouldPopulateDefaultValues() {
//GIVEN
welisacommerce.ShipmentData shipmentData = generateShipmentData();
//WHEN
Test.startTest();
new ExampleShipmentsConstructor().populateDefaultValues(shipmentData);
Test.stopTest();
//THEN
Assert.isNotNull(shipmentData.record.welisacommerce__Delivery_Method__c);
Assert.isNotNull(shipmentData.record.welisacommerce__Shipping_City__c);
Assert.isNotNull(shipmentData.record.welisacommerce__Shipping_Country__c);
Assert.isNotNull(shipmentData.record.welisacommerce__Shipping_Postal_Code__c);
Assert.isNotNull(shipmentData.record.welisacommerce__Shipping_State__c);
Assert.isNotNull(shipmentData.record.welisacommerce__Shipping_Street__c);
}
private static welisacommerce.ShipmentData generateShipmentData() {
welisacommerce.ShipmentData shipmentData = new welisacommerce.ShipmentData();
shipmentData.record = new welisacommerce__Shipment__c(
Name = 'Example Shipment 1'
);
return shipmentData;
}
private static welisacommerce.ConstructShipmentsContext generateContext() {
welisacommerce.ConstructShipmentsContext context = new welisacommerce.ConstructShipmentsContext();
context.shoppingCart = new welisacommerce.ShoppingCartData();
context.shoppingCart.record = new welisacommerce__Shopping_Cart__c(
Id = generateId(welisacommerce__Shopping_Cart__c.SObjectType)
);
welisacommerce.ShoppingCartData.Item item1 = new welisacommerce.ShoppingCartData.Item();
item1.record = new welisacommerce__Shopping_Cart_Item__c(
Id = generateId(welisacommerce__Shopping_Cart_Item__c.SObjectType)
);
welisacommerce.ShoppingCartData.Item item2 = new welisacommerce.ShoppingCartData.Item();
item2.record = new welisacommerce__Shopping_Cart_Item__c(
Id = generateId(welisacommerce__Shopping_Cart_Item__c.SObjectType)
);
context.shoppingCart.items = new List<welisacommerce.ShoppingCartData.Item> { item1, item2 };
return context;
}
private static Integer fakeIdCount = 0;
/**
* Generate a fake Salesforce Id for the given SObjectType
*/
private static Id generateId(Schema.SObjectType sobjectType)
{
String keyPrefix = sobjectType.getDescribe().getKeyPrefix();
fakeIdCount++;
String fakeIdPrefix = ('000000000000').substring(0, 12 - String.valueOf(fakeIdCount).length());
return Id.valueOf(keyPrefix + fakeIdPrefix + fakeIdCount);
}
}