Friday, 5 June 2026

Mastering fflib Unit of Work in Apex: Insert, Update, and Delete with Clean Transaction Management

 When Salesforce code grows, DML statements can quickly become scattered across triggers, services, and helper classes. The fflib Unit of Work pattern helps by collecting pending database changes and committing them together at the end of a business transaction. This makes code easier to maintain, encourages bulk-safe design, and helps keep related changes inside one managed transaction. The fflib docs also note that Unit of Work is generally created in the Service layer, and the transaction is finalized with commitWork() at the end. [fflib.dev], [deepwiki.com], [fflib.dev]

In fflib, the Unit of Work tracks several categories of record changes before commit: new records for insert, dirty records for update, and deleted records for delete. The framework provides methods such as registerNew, registerDirty, registerDeleted, and commitWork() to manage these changes in a structured way. Documentation and source summaries also highlight that the implementation is designed to bulkify DML, maintain transaction integrity, and apply operations in a consistent sequence. [deepwiki.com], [deepwiki.com], [github.com]

A particularly useful method is registerDirty(...). In the Unit of Work pattern, a dirty record means an existing record that has already been loaded or identified, then modified in memory, and now needs to be updated in the database when commitWork() is called. That gives you a clean way to separate business logic from database execution. [deepwiki.com], [deepwiki.com]


Why use Unit of Work in Apex?

Apex developers often try to keep DML outside loops and avoid mixing persistent operations with business logic. Unit of Work supports this goal by centralizing inserts, updates, and deletes into one place. According to the framework documentation and source summaries, this pattern helps with bulkified DML, atomic transaction handling, and even relationship management for related objects when needed. [deepwiki.com], [deepwiki.com], [github.com]

Another useful detail from the fflib docs is that the order of SObject types passed into the Unit of Work can matter, especially when object dependencies exist. For a simple Account example, that is not a major issue, but it becomes important in multi-object transactions such as Account + Contact or Opportunity + OpportunityLineItem. [fflib.dev]


A simple but proper Apex example

Below is a clean example using a single service class to demonstrate:

  • Insert with registerNew
  • Update with registerDirty
  • Delete with registerDeleted

This example keeps the pattern simple and readable while still reflecting how Unit of Work is intended to be used in a service-oriented design. The class uses Account records only, which makes the learning path straightforward. The code itself below is original and written for this post. The method choices are based on the documented Unit of Work API. [fflib.dev], [deepwiki.com], [deepwiki.com]

Apex Service Class


public with sharing class AccountUnitOfWorkService {

/**
* Inserts new Accounts using Unit of Work.
*/
public static void createAccounts(List<String> accountNames) {
if (accountNames == null || accountNames.isEmpty()) {
return;
}

fflib_ISObjectUnitOfWork uow =
new fflib_SObjectUnitOfWork(
new List<SObjectType>{ Account.SObjectType }
);

for (String accountName : accountNames) {
if (String.isBlank(accountName)) {
continue;
}

Account acc = new Account(
Name = accountName.trim()
);

uow.registerNew(acc);
}

uow.commitWork();
}

/**
* Updates existing Account names using registerDirty.
* Map key = Account Id
* Map value = New Account Name
*/
public static void updateAccountNames(Map<Id, String> accountNamesById) {
if (accountNamesById == null || accountNamesById.isEmpty()) {
return;
}

List<Account> accountsToUpdate = [
SELECT Id, Name
FROM Account
WHERE Id IN :accountNamesById.keySet()
];

fflib_ISObjectUnitOfWork uow =
new fflib_SObjectUnitOfWork(
new List<SObjectType>{ Account.SObjectType }
);

for (Account acc : accountsToUpdate) {
String newName = accountNamesById.get(acc.Id);

if (String.isBlank(newName)) {
continue;
}

String trimmedName = newName.trim();
if (acc.Name != trimmedName) {
acc.Name = trimmedName;
uow.registerDirty(acc);
}
}

uow.commitWork();
}

/**
* Deletes Accounts using Unit of Work.
*/
public static void deleteAccounts(Set<Id> accountIds) {
if (accountIds == null || accountIds.isEmpty()) {
return;
}

List<Account> accountsToDelete = [
SELECT Id
FROM Account
WHERE Id IN :accountIds
];

fflib_ISObjectUnitOfWork uow =
new fflib_SObjectUnitOfWork(
new List<SObjectType>{ Account.SObjectType }
);

for (Account acc : accountsToDelete) {
uow.registerDeleted(acc);
}

uow.commitWork();
}
}

How this class maps to the Unit of Work pattern

The createAccounts method builds new Account records in memory and registers each one with registerNew(...). These records are not inserted immediately; instead, they are staged inside the Unit of Work until commitWork() runs. That aligns with the documented behavior of Unit of Work tracking new records and then inserting them in a single managed commit. [deepwiki.com], [deepwiki.com]

The updateAccountNames method demonstrates the most important concept for many developers: dirty records. First, it queries existing Accounts, then updates the Name field in memory, and finally uses registerDirty(acc) to tell the Unit of Work that the record must be updated when the transaction is committed. This follows the documented purpose of registerDirty(...) for existing modified records. [deepwiki.com], [deepwiki.com]

The deleteAccounts method selects existing records and adds them to the Unit of Work using registerDeleted(...). The actual delete does not happen until commitWork() is called, which matches how the framework tracks deleted records before commit. [deepwiki.com], [deepwiki.com]

Example usage

Here is a simple example of how the service methods could be called from anonymous Apex or another orchestration layer:

// Insert
AccountUnitOfWorkService.createAccounts(
new List<String>{ 'Acme UK', 'Cloud Nova Ltd', 'Northwind Services' }
);


// Update
Map<Id, String> nameUpdates = new Map<Id, String>();
nameUpdates.put('001XXXXXXXXXXXXAAA', 'Acme UK - Updated');
nameUpdates.put('001YYYYYYYYYYYYAAA', 'Cloud Nova Ltd - Updated');
AccountUnitOfWorkService.updateAccountNames(nameUpdates);


// Delete
Set<Id> accountsToDelete = new Set<Id>{
'001XXXXXXXXXXXXAAA',
'001YYYYYYYYYYYYAAA'
};
AccountUnitOfWorkService.deleteAccounts(accountsToDelete);

This style keeps calling code clean and leaves DML orchestration in one dedicated service class, which is consistent with the service-layer guidance in the fflib docs. [fflib.dev], [fflib.dev]

Optional test class

A blog post is much stronger when it includes a working test. Here is a compact example:

@IsTest
private class AccountUnitOfWorkServiceTest {


@IsTest
static void testInsertUpdateDeleteUsingUnitOfWork() {
// Insert
Test.startTest();
AccountUnitOfWorkService.createAccounts(
new List<String>{ 'Original Account' }
);
Test.stopTest();


Account inserted = [
SELECT Id, Name
FROM Account
WHERE Name = 'Original Account'
LIMIT 1
];
System.assertNotEquals(null, inserted.Id);


// Update using registerDirty
Test.startTest();
AccountUnitOfWorkService.updateAccountNames(
new Map<Id, String>{
inserted.Id => 'Updated Account'
}
);
Test.stopTest();


Account updated = [
SELECT Id, Name
FROM Account
WHERE Id = :inserted.Id
];
System.assertEquals('Updated Account', updated.Name);


// Delete
Test.startTest();
AccountUnitOfWorkService.deleteAccounts(
new Set<Id>{ inserted.Id }
);
Test.stopTest();


Integer remaining = [
SELECT COUNT()
FROM Account
WHERE Id = :inserted.Id
];
System.assertEquals(0, remaining);
}
}

This test validates the three major flows in one place: insert, update via registerDirty, and delete. Because Unit of Work collects changes and applies them on commit, it becomes much easier to reason about the transaction boundary in testing as well. [deepwiki.com], [deepwiki.com]


Best practices to mention in your blog

If you publish this article, here are a few practical recommendations worth calling out:

  1. Create the Unit of Work in the service layer and commit once at the end of the business transaction whenever possible. That is consistent with the fflib usage guidance. [fflib.dev], [fflib.dev]

  2. Use registerDirty(...) only for existing records that need an update. For brand-new records, use registerNew(...); for deletes, use registerDeleted(...). [deepwiki.com], [deepwiki.com]

  3. Think in bulk, even for simple examples. One of the key benefits of the pattern is reducing scattered DML and keeping operations bulk-friendly. [deepwiki.com], [github.com]

  4. Be mindful of SObject ordering when multiple object types participate in the same Unit of Work, because the docs note that order can matter for dependency handling. [fflib.dev]


Conclusion

The fflib Unit of Work pattern gives Apex developers a cleaner way to manage database operations. Instead of inserting, updating, and deleting records directly throughout your code, you can track all pending changes and commit them together in one controlled place. For updates specifically, registerDirty(...) is the key method to remember: it represents an existing record that has been modified and should be saved during commitWork(). This approach improves readability, supports bulkification, and makes transaction boundaries much easier to understand. [fflib.dev], [deepwiki.com], [deepwiki.com]


Reference

Primary reference used for this article: fflib Unit of Work documentationhttps://fflib.dev/docs/unit-of-work [fflib.dev]

Additional supporting references:


Tuesday, 2 September 2025

Salesforce test class references for commerce cloud

Salesforce test class references for commerce cloud


TaxCartCalculatorSampleTest:


 https://github.com/forcedotcom/commerce-extensibility/blob/main/commerce/domain/tax/cart/calculator/classes/TaxCartCalculatorSampleTest.cls#L124




// This tax calculator extension class makes a call to an external service to retrieve tax
// information for a cart item and its adjustments and saves it to a cart data transfer object
// (DTO). For a tax calculator extension to be processed by the checkout flow, you must implement the
// CartExtension.TaxCartCalculator class.
//
// You need to have a good reason to use this extention point. For example, if you need to use cart custom fields in your calculation.
// Always check that commercestoretax.TaxService extention point isn't enough for you before extending the TaxCartCalculator.
// Extending commercestoretax.TaxService is required if you deal with subscription products and the TaxCartCalculator must call the commercestoretax.TaxService
// if overriden.
//
// Disclaimer: the code listed here is a sample that hasn't been tested for production use. Always test your code before releasing to production.
public with sharing class TaxCartCalculatorSample extends CartExtension.TaxCartCalculator {

// Disclaimer: the code listed here is a sample that hasn't been tested for production use. Always test your code before releasing to production.
public virtual override void calculate(CartExtension.CartCalculateCalculatorRequest request) {
try {
CartExtension.Cart cart = request.getCart();

CartExtension.CartDeliveryGroupList cartDeliveryGroups = cart.getCartDeliveryGroups();
Integer cartItemIdSeq = 0;

// Cart might have multiple delivery groups, you should handle that
CartExtension.CartDeliveryGroup cartDeliveryGroup = cartDeliveryGroups.get(0);

// Map cart ID to cart item with type Product.
CartExtension.CartItemList cartItemCollection = cart.getCartItems();

// The cartItemCollection contains both products and shipping cart items.
Map<String, CartExtension.CartItem> cartItemById = new Map<String, CartExtension.CartItem>();

Iterator<CartExtension.CartItem> cartItemCollectionIterator = cartItemCollection.iterator();

while (cartItemCollectionIterator.hasNext()) {
CartExtension.CartItem cartItem = cartItemCollectionIterator.next();

String cartItemId = (cartItem.getId() == null) ? String.valueOf(++cartItemIdSeq) : cartItem.getId();
cartItemById.put(cartItemId, cartItem);
}

// Get the tax rates and tax amounts from an external service for all given products
Map<String, TaxData> dataFromExternalService = getTaxesFromStaticResponse(
cartItemById,
CartDeliveryGroup.getDeliverToAddress().getState(),
CartDeliveryGroup.getDeliverToAddress().getCountry(),
cart.getTaxType());

for (String cartItemId : dataFromExternalService.keySet()) {
TaxData taxDetailsToCartId = dataFromExternalService.get(cartItemId);
CartExtension.CartItem cartItem = cartItemById.get(cartItemId);
addTaxesToCartItem(cartItem, taxDetailsToCartId);
}

} catch (Exception e) {
// For testing purposes, this example treats exceptions as user errors, which means they are
// displayed to the buyer user. In production, you probably want exceptions to be admin-type
// errors. In that case, throw the exception here and make sure that a notification system is
// in place to let the admin know that the error occurred. See the README section about error
// handling for details about how to create that notification.
throw new CalloutException('There was a problem with the request.');
}
return;
}

private void addTaxesToCartItem(CartExtension.CartItem cartItem, TaxData taxData) {
if (cartItem.getCartTaxes().size() > 0) {
// this sample always has at most one, your integration might have several
cartItem.getCartTaxes().remove(cartItem.getCartTaxes().get(0));
}

if (cartItem.getCartTaxes() == null || cartItem.getCartTaxes().isEmpty()) {
cartItem.setNetUnitPrice(taxData.getNetUnitPrice());
cartItem.setGrossUnitPrice(taxData.getGrossUnitPrice());
CartExtension.CartTaxList cartTaxCollection = cartItem.getCartTaxes();
CartExtension.CartTax cartTax = new CartExtension.CartTax(
CartExtension.TaxTypeEnum.ESTIMATED,
taxData.getAmount(),
taxData.getTaxName());
cartTax.setTaxRate(String.valueOf(taxData.getRate()));
cartTaxCollection.add(cartTax);
}
}

private Map<String, TaxData> getTaxesFromStaticResponse(Map<String, CartExtension.CartItem> cartItemsMap, String state, String country, CartExtension.TaxLocaleTypeEnum taxType) {
Double taxRate = 0.15;
Map<String, TaxData> taxDetailsFromExternalService = new Map<String, TaxData>();
for (String cartItemIdOrDeliveryGroupId : cartItemsMap.keySet()) {
CartExtension.CartItem cartItem = cartItemsMap.get(cartItemIdOrDeliveryGroupId);
String cartItemId = (cartItem.getId()==null) ? cartItemIdOrDeliveryGroupId : cartItem.getId();

Double amount = cartItem.getTotalPriceAfterAllAdjustments()==null ? cartItem.getTotalListPrice() : cartItem.getTotalPriceAfterAllAdjustments();
Double quantity = cartItem.getQuantity();

Double netUnitPrice = 0.00;
Double grossUnitPrice = 0.00;

// always remember to round correctly for the currency
Double cartItemTax = amount * taxRate;

if(taxType == CartExtension.TaxLocaleTypeEnum.GROSS) {
grossUnitPrice = amount / quantity;
netUnitPrice = (amount - cartItemTax) / quantity;
} else {
grossUnitPrice = (amount + cartItemTax) / quantity;
netUnitPrice = amount / quantity;
}

taxDetailsFromExternalService.put(cartItemId, new TaxData(
(Decimal) taxRate,
(Decimal) cartItemTax,
'GST',
(Decimal) grossUnitPrice,
(Decimal) netUnitPrice));
}

return taxDetailsFromExternalService;
}

// Structure to store the tax data retrieved from external service. This class simplifies our
// ability to access the data when storing it in Salesforce's CartTaxDto.
class TaxData {
private Decimal rate;
private Decimal amount;
private String taxName;
private Decimal grossUnitPrice;
private Decimal netUnitPrice;

public TaxData(
Decimal rateObj,
Decimal amountObj,
String taxNameObj,
Decimal grossUnitPriceObj,
Decimal netUnitPriceObj
) {
rate = rateObj;
amount = amountObj;
taxName = taxNameObj;
grossUnitPrice = grossUnitPriceObj;
netUnitPrice = netUnitPriceObj;
}

public Decimal getRate() {
return rate;
}

public Decimal getAmount() {
return amount;
}

public String getTaxName() {
return taxName;
}

public Decimal getGrossUnitPrice() {
return grossUnitPrice;
}

public Decimal getNetUnitPrice() {
return netUnitPrice;
}
}
}





/**
* @description A Sample unit test for TaxCartCalculatorSample.
*/
@IsTest
public inherited sharing class TaxCartCalculatorSampleTest {

private static final String CART_NAME = 'My Cart';
private static final String ACCOUNT_NAME = 'My Account';
private static final String WEBSTORE_NAME = 'My WebStore';
private static final String DELIVERYGROUP_NAME = 'My Delivery Group';
private static final String CART_ITEM1_NAME = 'My Cart Item 1';
private static final String CART_ITEM2_NAME = 'My Cart Item 2';
private static final String CART_ITEM3_NAME = 'My Cart Item 3';
private static final String SKU1_NAME = 'My SKU 1';
private static final String SKU2_NAME = 'My SKU 2';
private static final String SKU3_NAME = 'My SKU 3';
private static final Decimal ESTIMATED_PRICE = 350.00;
private static final Decimal ACTUAL_PRICE_SKU1 = 100.00;
private static final Decimal ACTUAL_PRICE_SKU2 = 200.00;
private static final Decimal ACTUAL_PRICE_SKU3 = 300.00;

@IsTest
static void testCalculate_withEmptyDeliveryAddress() {
// Arrange
CartExtension.Cart cart = arrangeAndLoadCartWithSpecifiedStatusAndThreeItems(CartExtension.CartStatusEnum.ACTIVE);
CartExtension.CartCalculateCalculatorRequest request = new CartExtension.CartCalculateCalculatorRequest(cart, CartExtension.OptionalBuyerActionDetails.empty());
TaxCartCalculatorSample calculator = new TaxCartCalculatorSample();

// Act
Test.startTest();
calculator.calculate(request);
Test.stopTest();

// Assert
cart = request.getCart();
CartExtension.CartItemList cartItemCollection = cart.getCartItems();
Assert.areEqual(0, cartItemCollection.get(0).getCartTaxes().size());
}

@IsTest
static void testCalculate_withEmptyCartItems() {
// Arrange
CartExtension.Cart cart = arrangeAndLoadCartWithNoCartItems(CartExtension.CartStatusEnum.ACTIVE);
CartExtension.CartDeliveryGroup deliveryGroup = cart.getCartDeliveryGroups().get(0);
deliveryGroup.setDeliverToStreet('newStreet');
deliveryGroup.setDeliverToCity('newCity');
deliveryGroup.setDeliverToState('Washington');
deliveryGroup.setDeliverToCountry('US');
deliveryGroup.setDeliverToPostalCode('987654');
deliveryGroup.setDeliverToLatitude(48.1);
deliveryGroup.setDeliverToLongitude(33.2);
deliveryGroup.setDeliverToGeocodeAccuracy(null);
CartExtension.CartCalculateCalculatorRequest request = new CartExtension.CartCalculateCalculatorRequest(cart, CartExtension.OptionalBuyerActionDetails.empty());
TaxCartCalculatorSample calculator = new TaxCartCalculatorSample();

// Act
Test.startTest();
calculator.calculate(request);
Test.stopTest();

// Assert
cart = request.getCart();
CartExtension.CartItemList cartItemCollection = cart.getCartItems();
Assert.areEqual(0, cartItemCollection.size());
}

@IsTest
static void testCalculate_withZeroPrice() {
// Arrange
CartExtension.Cart cart = arrangeAndLoadCartWithSpecifiedStatus(CartExtension.CartStatusEnum.ACTIVE);
CartExtension.CartDeliveryGroup deliveryGroup = cart.getCartDeliveryGroups().get(0);
deliveryGroup.setDeliverToStreet('newStreet');
deliveryGroup.setDeliverToCity('newCity');
deliveryGroup.setDeliverToState('Washington');
deliveryGroup.setDeliverToCountry('US');
deliveryGroup.setDeliverToPostalCode('987654');
deliveryGroup.setDeliverToLatitude(48.1);
deliveryGroup.setDeliverToLongitude(33.2);
deliveryGroup.setDeliverToGeocodeAccuracy(null);
CartExtension.CartCalculateCalculatorRequest request = new CartExtension.CartCalculateCalculatorRequest(cart, CartExtension.OptionalBuyerActionDetails.empty());
TaxCartCalculatorSample calculator = new TaxCartCalculatorSample();

CartExtension.CartValidationOutputList cartValidationOutputCollection = cart.getCartValidationOutputs();
CartExtension.CartValidationOutput cvo = new CartExtension.CartValidationOutput(
CartExtension.CartValidationOutputTypeEnum.TAXES, CartExtension.CartValidationOutputLevelEnum.ERROR);
cartValidationOutputCollection.add(cvo);

// Act
Test.startTest();
calculator.calculate(request);
Test.stopTest();

// Assert
cart = request.getCart();
CartExtension.CartItemList cartItemCollection = cart.getCartItems();
Assert.areEqual(0, cart.getCartValidationOutputs().size());
Iterator<CartExtension.CartItem> cartItemCollectionIterator = cartItemCollection.iterator();
while (cartItemCollectionIterator.hasNext()) {
CartExtension.CartItem cartItem = cartItemCollectionIterator.next();
Assert.areEqual(0.00, cartItem.getNetUnitPrice());
Assert.areEqual(0.00, cartItem.getGrossUnitPrice());
}
}

@IsTest
static void testCalculate_withDeliveryAddress() {
// Arrange
CartExtension.Cart cart = arrangeAndLoadCartWithSpecifiedStatus(CartExtension.CartStatusEnum.ACTIVE);
CartExtension.CartDeliveryGroup deliveryGroup = cart.getCartDeliveryGroups().get(0);
deliveryGroup.setDeliverToStreet('newStreet');
deliveryGroup.setDeliverToCity('newCity');
deliveryGroup.setDeliverToState('Washington');
deliveryGroup.setDeliverToCountry('US');
deliveryGroup.setDeliverToPostalCode('987654');
deliveryGroup.setDeliverToLatitude(48.1);
deliveryGroup.setDeliverToLongitude(33.2);
deliveryGroup.setDeliverToGeocodeAccuracy(null);
cart.getCartItems().get(0).setTotalPrice(100.00);
CartExtension.CartCalculateCalculatorRequest request = new CartExtension.CartCalculateCalculatorRequest(cart, CartExtension.OptionalBuyerActionDetails.empty());
TaxCartCalculatorSample calculator = new TaxCartCalculatorSample();

// Act
Test.startTest();
calculator.calculate(request);
Test.stopTest();

// Assert
cart = request.getCart();
CartExtension.CartItemList cartItemCollection = cart.getCartItems();
Iterator<CartExtension.CartItem> cartItemCollectionIterator = cartItemCollection.iterator();
while (cartItemCollectionIterator.hasNext()) {
CartExtension.CartItem cartItem = cartItemCollectionIterator.next();
Assert.areEqual(100.00, cartItem.getNetUnitPrice());
Assert.areEqual(108.00, cartItem.getGrossUnitPrice());
}
}

@IsTest
static void testCalculate_withShippingChargeItem() {
// Arrange
CartExtension.Cart cart = arrangeAndLoadCartWithShippingChargeItem(CartExtension.CartStatusEnum.ACTIVE);
CartExtension.CartDeliveryGroup deliveryGroup = cart.getCartDeliveryGroups().get(0);
deliveryGroup.setDeliverToStreet('newStreet');
deliveryGroup.setDeliverToCity('newCity');
deliveryGroup.setDeliverToState('Washington');
deliveryGroup.setDeliverToCountry('US');
deliveryGroup.setDeliverToPostalCode('987654');
deliveryGroup.setDeliverToLatitude(48.1);
deliveryGroup.setDeliverToLongitude(33.2);
deliveryGroup.setDeliverToGeocodeAccuracy(null);
cart.getCartItems().get(0).setTotalPrice(100.00);
CartExtension.CartCalculateCalculatorRequest request = new CartExtension.CartCalculateCalculatorRequest(cart, CartExtension.OptionalBuyerActionDetails.empty());
TaxCartCalculatorSample calculator = new TaxCartCalculatorSample();

// Act
Test.startTest();
calculator.calculate(request);
Test.stopTest();

// Assert
cart = request.getCart();
CartExtension.CartItemList cartItemCollection = cart.getCartItems();
Assert.areEqual(2, cartItemCollection.size());
}

@IsTest
static void testCalculate_withNetPrice() {
// Arrange
CartExtension.Cart cart = arrangeAndLoadCartWithSpecifiedStatus(CartExtension.CartStatusEnum.ACTIVE);
CartExtension.CartDeliveryGroup deliveryGroup = cart.getCartDeliveryGroups().get(0);
deliveryGroup.setDeliverToStreet('newStreet');
deliveryGroup.setDeliverToCity('newCity');
deliveryGroup.setDeliverToState('Washington');
deliveryGroup.setDeliverToCountry('US');
deliveryGroup.setDeliverToPostalCode('987654');
deliveryGroup.setDeliverToLatitude(48.1);
deliveryGroup.setDeliverToLongitude(33.2);
deliveryGroup.setDeliverToGeocodeAccuracy(null);
cart.getCartItems().get(0).setTotalPrice(100.00);
cart.getCartItems().get(0).setNetUnitPrice(200.00);
CartExtension.CartCalculateCalculatorRequest request = new CartExtension.CartCalculateCalculatorRequest(cart, CartExtension.OptionalBuyerActionDetails.empty());
TaxCartCalculatorSample calculator = new TaxCartCalculatorSample();

// Act
Test.startTest();
calculator.calculate(request);
Test.stopTest();

// Assert
cart = request.getCart();
CartExtension.CartItemList cartItemCollection = cart.getCartItems();
Iterator<CartExtension.CartItem> cartItemCollectionIterator = cartItemCollection.iterator();
while (cartItemCollectionIterator.hasNext()) {
CartExtension.CartItem cartItem = cartItemCollectionIterator.next();
Assert.areEqual(100.00, cartItem.getNetUnitPrice());
Assert.areEqual(108.00, cartItem.getGrossUnitPrice());
}
}

@IsTest
static void testCalculate_withPriceAdjustments() {
// Arrange
CartExtension.Cart cart = arrangeAndLoadCartWithAdjustments(CartExtension.CartStatusEnum.ACTIVE);
CartExtension.CartDeliveryGroup deliveryGroup = cart.getCartDeliveryGroups().get(0);
deliveryGroup.setDeliverToStreet('newStreet');
deliveryGroup.setDeliverToCity('newCity');
deliveryGroup.setDeliverToState('Washington');
deliveryGroup.setDeliverToCountry('US');
deliveryGroup.setDeliverToPostalCode('987654');
deliveryGroup.setDeliverToLatitude(48.1);
deliveryGroup.setDeliverToLongitude(33.2);
deliveryGroup.setDeliverToGeocodeAccuracy(null);

CartExtension.CartItemPriceAdjustment newItemPriceAdjustment = new CartExtension.CartItemPriceAdjustment
(CartExtension.CartAdjustmentTargetTypeEnum.ITEM, 1,
CartExtension.PriceAdjustmentSourceEnum.PROMOTION,
CartExtension.AdjustmentTypeEnum.ADJUSTMENT_AMOUNT, -2, '0c8RO0000005qNPYAY');
newItemPriceAdjustment.setPriority(2);
newItemPriceAdjustment.setAdjustmentValue(3);
CartExtension.CartItemPriceAdjustmentList cartItemPriceAdjustments = cart.getCartItems().get(0).getCartItemPriceAdjustments();
cartItemPriceAdjustments.add(newItemPriceAdjustment);

cart.getCartItems().get(0).setTotalPrice(100.00);
cart.getCartItems().get(0).setNetUnitPrice(200.00);
CartExtension.CartCalculateCalculatorRequest request = new CartExtension.CartCalculateCalculatorRequest(cart, CartExtension.OptionalBuyerActionDetails.empty());
TaxCartCalculatorSample calculator = new TaxCartCalculatorSample();

// Act
Test.startTest();
calculator.calculate(request);
Test.stopTest();

// Assert
cart = request.getCart();
CartExtension.CartItemList cartItemCollection = cart.getCartItems();
Iterator<CartExtension.CartItem> cartItemCollectionIterator = cartItemCollection.iterator();
while (cartItemCollectionIterator.hasNext()) {
CartExtension.CartItem cartItem = cartItemCollectionIterator.next();
Assert.areEqual(33.666666666666664, cartItem.getNetUnitPrice());
Assert.areEqual(36.36, cartItem.getGrossUnitPrice());
}
}

/**
* @description Create and return a WebCart with the specified status and 3 items.
*
* @param cartStatus The status of the cart.
*
* @return <<CartExtension.Cart>>
*/
private static ID arrangeCartWithSpecifiedStatus(CartExtension.CartStatusEnum cartStatus) {
Account account = new Account(Name = ACCOUNT_NAME);
insert account;

WebStore webStore = new WebStore(Name = WEBSTORE_NAME, OptionsCartCalculateEnabled = true);
insert webStore;

WebCart webCart = new WebCart(
Name = CART_NAME,
WebStoreId = webStore.Id,
AccountId = account.Id,
Status = cartStatus.name());
insert webCart;
return webCart.Id;
}

private static List<ID> arrangeThreeCartItems(ID cartId) {
CartDeliveryGroup deliveryGroup = new CartDeliveryGroup(Name = DELIVERYGROUP_NAME, CartId = cartId);
insert deliveryGroup;

CartItem cartItem1 = new CartItem(
Name = CART_ITEM1_NAME,
CartId = cartId,
CartDeliveryGroupId = deliveryGroup.Id,
Quantity = 3,
SKU = SKU1_NAME,
Type = CartExtension.SalesItemTypeEnum.PRODUCT.name());
insert cartItem1;

CartItem cartItem2 = new CartItem(
Name = CART_ITEM2_NAME,
CartId = cartId,
CartDeliveryGroupId = deliveryGroup.Id,
Quantity = 3,
SKU = SKU2_NAME,
Type = CartExtension.SalesItemTypeEnum.PRODUCT.name());
insert cartItem2;

CartItem cartItem3 = new CartItem(
Name = CART_ITEM3_NAME,
CartId = cartId,
CartDeliveryGroupId = deliveryGroup.Id,
Quantity = 3,
SKU = SKU3_NAME,
Type = CartExtension.SalesItemTypeEnum.PRODUCT.name());
insert cartItem3;
return new List<ID>{cartItem1.Id, cartItem2.Id, cartItem3.Id};
}

private static CartExtension.Cart arrangeAndLoadCartWithSpecifiedStatusAndThreeItems(CartExtension.CartStatusEnum cartStatus) {
Id cartId = arrangeCartWithSpecifiedStatus(cartStatus);
arrangeThreeCartItems(cartId);
return CartExtension.CartTestUtil.getCart(cartId);
}

private static List<ID> arrangeOneCartItemsWithShippingChargeType(ID cartId) {
CartDeliveryGroup deliveryGroup = new CartDeliveryGroup(Name = DELIVERYGROUP_NAME, CartId = cartId);
insert deliveryGroup;

CartItem cartItem1 = new CartItem(
Name = CART_ITEM1_NAME,
CartId = cartId,
CartDeliveryGroupId = deliveryGroup.Id,
Quantity = 3,
SKU = SKU1_NAME,
Type = CartExtension.SalesItemTypeEnum.CHARGE.name());
insert cartItem1;

CartItem cartItem2 = new CartItem(
Name = CART_ITEM2_NAME,
CartId = cartId,
CartDeliveryGroupId = deliveryGroup.Id,
Quantity = 3,
SKU = SKU2_NAME,
Type = CartExtension.SalesItemTypeEnum.PRODUCT.name());
insert cartItem2;

return new List<ID>{cartItem1.Id, cartItem2.Id};
}

private static CartExtension.Cart arrangeAndLoadCartWithShippingChargeItem(CartExtension.CartStatusEnum cartStatus) {
Id cartId = arrangeCartWithSpecifiedStatus(cartStatus);
arrangeOneCartItemsWithShippingChargeType(cartId);
return CartExtension.CartTestUtil.getCart(cartId);
}

private static List<ID> arrangeOneCartItemWithPriceAdjustments(ID cartId) {
CartDeliveryGroup deliveryGroup = new CartDeliveryGroup(Name = DELIVERYGROUP_NAME, CartId = cartId);
insert deliveryGroup;

CartItem cartItem = new CartItem(
Name = CART_ITEM1_NAME,
CartId = cartId,
CartDeliveryGroupId = deliveryGroup.Id,
Quantity = 3,
SKU = SKU1_NAME,
Type = CartExtension.SalesItemTypeEnum.PRODUCT.name());
insert cartItem;
return new List<ID>{cartItem.Id};
}

private static CartExtension.Cart arrangeAndLoadCartWithAdjustments(CartExtension.CartStatusEnum cartStatus) {
Id cartId = arrangeCartWithSpecifiedStatus(cartStatus);
arrangeOneCartItemWithPriceAdjustments(cartId);
return CartExtension.CartTestUtil.getCart(cartId);
}

private static List<ID> arrangeDeliveryGroup(ID cartId) {
CartDeliveryGroup deliveryGroup = new CartDeliveryGroup(Name = DELIVERYGROUP_NAME, CartId = cartId);
insert deliveryGroup;
return new List<ID>{};
}

private static CartExtension.Cart arrangeAndLoadCartWithNoCartItems(CartExtension.CartStatusEnum cartStatus) {
Id cartId = arrangeCartWithSpecifiedStatus(cartStatus);
arrangeDeliveryGroup(cartId);
return CartExtension.CartTestUtil.getCart(cartId);
}

private static CartExtension.Cart arrangeAndLoadCartWithSpecifiedStatus(CartExtension.CartStatusEnum cartStatus) {
Id cartId = arrangeCartWithSpecifiedStatus(cartStatus);
arrangeCartItemsWithDeliveryAddress(cartId);
return CartExtension.CartTestUtil.getCart(cartId);
}

private static List<ID> arrangeCartItemsWithDeliveryAddress(ID cartId) {
CartDeliveryGroup deliveryGroup = new CartDeliveryGroup(Name = DELIVERYGROUP_NAME, CartId = cartId);
insert deliveryGroup;

CartItem cartItem1 = new CartItem(
Name = CART_ITEM1_NAME,
CartId = cartId,
CartDeliveryGroupId = deliveryGroup.Id,
Quantity = 1,
SKU = SKU1_NAME,
Type = CartExtension.SalesItemTypeEnum.PRODUCT.name());
insert cartItem1;

return new List<ID>{cartItem1.Id};
}

}