Implement Custom Pricing Rules
Why custom pricing rules matter
Section titled “Why custom pricing rules matter”Standard price lists cover most scenarios, but real-world pricing often requires logic that goes beyond static price entries. You may need to calculate prices based on external feeds, apply contract-specific rates, adjust prices by time of day, or combine multiple data sources into a final price. Commerce exposes its pricing through an extensible pipeline that you can customize without replacing the entire pricing system.
This guide covers implementing a custom price service, building dynamic pricing logic, and plugging into the price resolver pipeline.
What you will do
Section titled “What you will do”- Understand the price resolution pipeline
- Implement a custom
IPriceServicefor dynamic pricing - Build a price resolver filter for conditional adjustments
- Register and test custom pricing components
Understand the price resolution pipeline
Section titled “Understand the price resolution pipeline”When Commerce resolves a price, it follows a pipeline:
- IPriceService returns candidate prices from price lists
- IPriceOptimizer selects the best price based on customer context
- Promotion engine applies any applicable discounts
You can extend any step. The most common extension points are implementing a custom IPriceService for external price sources and adding filters to modify resolved prices.
Implement a custom IPriceService
Section titled “Implement a custom IPriceService”Replace or decorate the default price service to pull prices from an external system (ERP, PIM, or pricing engine).
using Mediachase.Commerce;
using Mediachase.Commerce.Pricing;
public class ExternalPriceService : IPriceService
{
private readonly IPriceService _defaultService;
private readonly IExternalPriceApi _externalApi;
public ExternalPriceService(
IPriceService defaultService,
IExternalPriceApi externalApi)
{
_defaultService = defaultService;
_externalApi = externalApi;
}
public IPriceValue GetDefaultPrice(
MarketId marketId, DateTime validOn,
CatalogKey catalogKey, Currency currency)
{
// Try external source first
var externalPrice = _externalApi
.GetPrice(catalogKey.CatalogEntryCode,
marketId.Value, currency.CurrencyCode);
if (externalPrice.HasValue)
{
return new PriceValue
{
CatalogKey = catalogKey,
MarketId = marketId,
UnitPrice = new Money(
externalPrice.Value, currency),
CustomerPricing =
CustomerPricing.AllCustomers,
ValidFrom = DateTime.UtcNow,
ValidUntil = null,
MinQuantity = 0
};
}
// Fall back to built-in prices
return _defaultService.GetDefaultPrice(
marketId, validOn, catalogKey, currency);
}
public IEnumerable<IPriceValue> GetPrices(
MarketId marketId, DateTime validOn,
CatalogKey catalogKey,
PriceFilter filter)
{
return _defaultService.GetPrices(
marketId, validOn, catalogKey, filter);
}
public IEnumerable<IPriceValue> GetCatalogEntryPrices(
IEnumerable<CatalogKey> catalogKeys)
{
return _defaultService
.GetCatalogEntryPrices(catalogKeys);
}
} Build a price resolver filter
Section titled “Build a price resolver filter”For conditional adjustments (loyalty discounts, time-based pricing, contract rates), implement a filter that modifies the resolved price.
using Mediachase.Commerce;
using Mediachase.Commerce.Pricing;
public class LoyaltyPriceFilter
{
private readonly ICustomerGroupService
_groupService;
public LoyaltyPriceFilter(
ICustomerGroupService groupService)
{
_groupService = groupService;
}
public IPriceValue AdjustPrice(
IPriceValue originalPrice,
Guid customerId)
{
var loyaltyTier = _groupService
.GetLoyaltyTier(customerId);
decimal discount = loyaltyTier switch
{
"Gold" => 0.10m,
"Platinum" => 0.15m,
"Diamond" => 0.20m,
_ => 0m
};
if (discount == 0m)
return originalPrice;
var adjustedAmount = originalPrice
.UnitPrice.Amount * (1 - discount);
return new PriceValue
{
CatalogKey = originalPrice.CatalogKey,
MarketId = originalPrice.MarketId,
UnitPrice = new Money(adjustedAmount,
originalPrice.UnitPrice.Currency),
CustomerPricing =
originalPrice.CustomerPricing,
ValidFrom = originalPrice.ValidFrom,
ValidUntil = originalPrice.ValidUntil,
MinQuantity = originalPrice.MinQuantity
};
}
} Register custom pricing components
Section titled “Register custom pricing components”Register your custom services using dependency injection so Commerce uses them instead of (or alongside) the defaults.
using Microsoft.Extensions.DependencyInjection;
public static class PricingServiceRegistration
{
public static IServiceCollection
AddCustomPricing(
this IServiceCollection services)
{
// Decorate the default IPriceService
services.Decorate<IPriceService,
ExternalPriceService>();
// Register the loyalty filter
services.AddSingleton<LoyaltyPriceFilter>();
// Register the external API client
services.AddHttpClient<IExternalPriceApi,
ExternalPriceApi>(client =>
{
client.BaseAddress = new Uri(
"https://pricing.example.com/api/");
client.Timeout = TimeSpan.FromSeconds(5);
});
return services;
}
} Key design decisions:
| Approach | When to use |
|---|---|
| Decorate IPriceService | External price source that replaces or augments stored prices |
| Price filter | Conditional adjustments based on customer context |
| Custom IPriceOptimizer | Change how Commerce selects among multiple candidate prices |
| Promotion engine extension | Discounts that should appear as promotions in the cart |
Common issues
Section titled “Common issues”| Issue | Cause | Fix |
|---|---|---|
| Custom prices not appearing | Service not registered or decorator order incorrect | Verify DI registration and ensure your decorator wraps the default service |
| External API timeout slowing page loads | No caching on external price calls | Add a short-lived cache (30-60 seconds) around external price lookups |
| Price shows original amount, not adjusted | Filter not called in the resolution pipeline | Ensure your filter is invoked after the default price resolution |
| Negative prices after discount | Discount percentage exceeds 100% | Add a floor check (Math.Max(0, adjustedAmount)) in your filter |