TABLE OF CONTENTS
- 1. Introduction
- 2. Extension Framework Overview
- 3. Promotion Extensibility Overview
- 4. Prerequisites
- 5. Getting Started: Building Your First Custom Promotion
- 6. Promotion SDK Overview
- 7. Understanding the Promotion Context
- 8. Logging and Observability
- 9. Deployment and Extension Management
- 10. Registering Promotion Types and Configuring Promotions
- 11. Promotion Execution Flow and Runtime Processing
- 12. SDK Helper Methods and Discount APIs
- 13. Best Practices and Troubleshooting
1. Introduction
1.1 Purpose
The Promotion Extensibility Framework enables developers to build custom promotion logic outside the core Znode application. Instead of modifying the product source code, custom promotions are implemented as independent extension libraries that are dynamically loaded and executed by the Znode Extension Framework.
This approach allows organizations to implement complex, business-specific promotion scenarios while remaining compatible with future Znode upgrades. Typical use cases include:
- Custom order discounts
- Customer-specific promotions
- Industry-specific pricing rules
- Loyalty-based discounts
- Brand-specific offers
- Membership pricing
- Complex coupon validation
- External system integrations
1.2 Intended Audience
This guide is intended for:
- Solution Architects
- Backend Developers
- Technical Consultants
- Znode Partners
- Extension Developers
- System Integrators
Readers should have working experience with:
- C#
- .NET 8
- Dependency Injection
- Object-Oriented Programming
- Znode Administration
1.3 Scope
This guide explains how to:
- Understand the Extension Framework and how it relates to Promotion Extensibility
- Create, build, and deploy custom promotion extensions
- Deploy extensions using the Znode CLI
- Register promotion types and configure promotions in Admin
- Use the Promotions SDK to access runtime context and helper APIs
- Execute, debug, and monitor custom promotion logic
1.4 Benefits of Promotion Extensibility
Using the Promotion Extensibility Framework provides several advantages:
- No modifications to Znode core source code
- Upgrade-safe customizations
- Independent deployment of promotion logic
- Clear separation between product and customer-specific code
- Dynamic extension loading with no application restart
- Centralized logging and observability
- Support for multiple custom promotion types running side by side
- Simplified maintenance and versioning
2. Extension Framework Overview
2.1 What Is the Extension Framework?
The Znode Extension Framework is a runtime extensibility platform that enables developers to introduce new business capabilities without changing the core application. Instead of embedding custom code within the Znode source, functionality is packaged into extension assemblies (DLLs). These assemblies are deployed independently and loaded dynamically by the Extension Loader during application startup or synchronization.
This architecture enables organizations to customize business behavior while keeping the product maintainable and upgrade-friendly.
2.2 Why an Extension Framework?
Traditional customization approaches require direct modifications to the product source code, making upgrades costly and difficult. The Extension Framework addresses these challenges by providing:
- Loose coupling between the core platform and custom implementations
- Independent development and deployment of custom functionality
- Version isolation for extension assemblies
- Dynamic discovery of extensions
- Standardized contracts through the SDK
- Built-in observability and diagnostics
2.3 End-to-End Lifecycle
The following diagram illustrates the complete lifecycle of a promotion extension, from initial development through cart execution and logging.
Developer
|
v
Create Promotion Project
|
v
Reference Promotions SDK
|
v
Implement Promotion Logic
|
v
Build Extension DLL
|
v
Deploy using Znode CLI
|
v
Extension Storage
|
v
Extension Loader
|
v
Sync Extensions
|
v
Register Promotion Type
|
v
Create Promotion in Admin
|
v
Customer Cart
|
v
Promotion Engine
|
v
Execute Custom Promotion
|
v
Return Discount Result
|
v
Execution LogsThis lifecycle is explored step by step in Chapter 5 (development), Chapter 9 (deployment), Chapter 10 (registration and configuration), and Chapter 11 (runtime execution).
2.4 High-Level Components
Promotion SDK
Provides the contracts, interfaces, base classes, helper methods, and attributes required to develop custom promotions. Covered in detail in Chapter 6.
Extension Loader
Responsible for discovering, loading, and managing deployed promotion extensions. During synchronization, it reloads the latest extension assemblies without requiring changes to the core application.
Extension Storage
Stores the compiled extension assemblies that have been deployed using the Znode CLI. The Extension Loader retrieves these assemblies during synchronization.
Promotion Engine
Evaluates active promotions during cart calculation. It invokes the appropriate custom promotion classes and applies the discounts returned by the extension. See Chapter 11 for the full runtime execution pipeline.
Extension Observability
Provides visibility into extension execution by exposing deployment status, synchronization controls, and execution logs. This enables developers to monitor and troubleshoot custom promotions effectively. See Chapter 8.
2.5 Benefits of the Extension Framework
- Upgrade-safe customizations
- Dynamic extension loading
- Independent deployment
- Standardized SDK contracts
- Centralized logging
- Improved maintainability and better separation of concerns
- Enhanced debugging and observability
- Reduced product maintenance effort
3. Promotion Extensibility Overview
3.1 Overview
Promotion Extensibility enables developers to create custom promotion types that execute alongside Znode's built-in promotion engine without modifying the core product. Custom promotions are implemented as extension libraries using the Promotions SDK, deployed independently, and dynamically loaded through the Znode Extension Framework.
Unlike traditional customizations that require changes to the product source code, Promotion Extensibility provides a clean separation between the Znode platform and customer-specific business logic. This ensures that custom implementations remain upgrade-safe while allowing organizations to implement complex promotional requirements.
The Promotion Engine treats custom promotions the same way it treats built-in promotion types. Once a custom promotion is registered in Admin, business users can configure and manage it through the standard Promotion Management screens.
3.2 Supported Promotion Types
The framework supports extending all major promotion scenarios, including:
| Promotion Type | Description |
|---|---|
| Cart Promotions | Discounts applied to the overall cart value |
| Product Promotions | Discounts on selected products |
| Category Promotions | Discounts for products within specific categories |
| Brand Promotions | Brand-based promotional offers |
| Shipping Promotions | Shipping fee discounts |
| Coupon Promotions | Coupon-based promotions requiring customer input |
| Membership Promotions | Customer segment |
4. Prerequisites
Before developing custom promotions, ensure the following prerequisites are met.
4.1 Development Environment
| Component | Recommended Version |
|---|---|
| Visual Studio | 2022 Enterprise or Professional |
| .NET SDK | .NET 8 |
| Znode CLI | Latest version |
| Git | Latest version |
| SQL Server | SQL Server 2022 or later |
4.2 Required Knowledge
Developers should have working knowledge of:
- C#
- .NET 8
- Dependency Injection
- Object-Oriented Programming
- Reflection and attributes
- Znode Promotion Management
- Basic commerce concepts
4.3 Required Access
Ensure you have access to:
- Znode Admin
- Custom API solution
- Promotions SDK package
- Znode CLI
- Extension deployment permissions
- Extension Observability page
4.4 Required SDK
Every custom promotion project must reference the Promotions SDK:
Znode10.Libraries.Promotions.SDK
This SDK provides:
- CartPromotionTypeBase
- PromotionExtensionAttribute
- IPromotionContext
- IPromotionCart
- IPromotionBag
- IPromotionCalculationHelper
- PromotionCalculateResult
- PromotionDiscountDetail
- IExtensionLogger
Important: Without this SDK, custom promotion development is not possible.
4.5 Recommended Folder Structure
Custom API Solution │ ├── Engine.Custom.Api │ ├── Znode.Libraries.Promotions.Custom │ ├── FlatTenDollarPromotion.cs │ ├── BrandPromotion.cs │ ├── LoyaltyPromotion.cs │ ├── Shared │ └── Tests
Organizing promotion extensions into a dedicated class library keeps business logic isolated, simplifies maintenance, and allows independent deployment.
5. Getting Started: Building Your First Custom Promotion
This chapter walks through the complete, end-to-end process of creating a custom promotion extension — from project creation through to a working, deployable discount. It is the single authoritative walkthrough for this workflow; later chapters build on the concepts introduced here rather than repeating them.
Development Workflow
Create Extension Project
|
v
Reference Promotions SDK
|
v
Create Promotion Class
|
v
Implement Business Logic
|
v
Build Project
|
v
Deploy Extension
|
v
Sync Extension
|
v
Register Promotion Type
|
v
Create Promotion
|
v
Execute & ValidateStep 1 — Create a New Promotion Project
A custom promotion should always be implemented in a separate Class Library rather than modifying any Znode core project. Keeping promotion logic isolated improves maintainability and ensures compatibility with future product upgrades.
Recommended naming conventions for the project:
- Znode.Libraries.Promotions.Custom
- Znode.Libraries.Promotions.Extensions
- CompanyName.CustomPromotions
- Contoso.PromotionExtensions
Recommendation: Avoid placing custom promotion logic directly inside the API project.
Step 2 — Reference the Promotions SDK
Add the following SDK package to the project:
Znode10.Libraries.Promotions.SDKThe SDK contains all the contracts required by the Promotion Engine, including base promotion classes, extension attributes, context objects, discount models, helper methods, dependency injection support, and logging interfaces.
Important: Without this package, the Promotion Engine cannot recognize or execute custom promotions.
Step 3 — Create the Promotion Class
Create a new class inside your project. Each class represents a single promotion type. Example names:
- FlatTenDollarPromotion.cs
- BrandDiscountPromotion.cs
- CustomerTierPromotion.cs
Step 4 — Decorate the Class with PromotionExtension
Every promotion class must be decorated with the PromotionExtension attribute. The Extension Framework scans all deployed assemblies during synchronization, and only classes decorated with this attribute are discovered and registered as promotion extensions.
using Znode.Libraries.Promotions.SDK;
using Znode.Libraries.Promotions.SDK.Attributes;
namespace Znode.Libraries.Promotions.Custom;
[PromotionExtension(typeof(CartPromotionTypeBase))]
public class FlatTenDollarPromotion : CartPromotionTypeBase
{
}
Important: Without this attribute, the Extension Loader will ignore the class during discovery.
Step 5 — Inherit from CartPromotionTypeBase
Every promotion must inherit from the base class supplied by the SDK. The base class provides access to the promotion context, cart information, helper methods, promotion configuration, dependency injection, and discount creation helpers.
Important: Developers should never implement promotions from scratch without inheriting from the base class.
Step 6 — Configure Promotion Metadata
Inside the constructor, configure the promotion metadata:
public FlatTenDollarPromotion()
{
Name = "Flat $10 Discount";
Description = "Applies a flat $10 discount.";
ClassName = "FlatTenDollarPromotion";
}
| Property | Purpose |
|---|---|
| Name | Display name of the promotion |
| Description | Description shown to administrators |
| ClassName | Unique identifier used during execution |
Important: The ClassName value must exactly match the Class Name configured in Admin → Dev Center → Promotion Settings. This comparison is case-sensitive.
Step 7 — Override the Calculate() Method
Every promotion must override the Calculate() method. This is the entry point that the Promotion Engine invokes whenever the promotion is evaluated, and it is the only location where business logic should be implemented.
public override PromotionCalculateResult Calculate(
int? couponIndex,
IReadOnlyList<IPromotionInfo> allPromotions)
{
var result = new PromotionCalculateResult();
// Business logic
return result;
}
When this method is invoked, the SDK has already initialized the following objects — there is no need to load them manually:
Promotion Context
|
v
Current Cart
|
v
Promotion Configuration
|
v
Applied Coupons
|
v
Customer Information
|
v
Catalog Information
|
v
Promotion Helper
|
v
Dependency Injection
|
v
Logging ServicesStep 8 — Validate Cart and Business Conditions
Before applying a discount, validate whether the promotion is applicable, and return an empty result if it is not:
if (GetCartQuantity() <= 0)
{
return new PromotionCalculateResult();
}
Common validations performed at this stage include:
| Validation | Purpose |
|---|---|
| Cart is empty | Skip execution |
| Minimum order amount | Ensure order value qualifies |
| Minimum quantity | Validate purchased quantity |
| Product / brand / category eligibility | Restrict to configured items |
| Coupon validation | Validate coupon availability |
| Customer profile | Restrict to customer segments |
| Portal validation | Restrict to specific stores |
| Promotion priority | Prevent conflicting promotions |
Step 9 — Retrieve Promotion Configuration and Cart Data
The configuration entered by the administrator is available through the Promotion Bag, and the shopping cart is available through the Cart object:
var bag = Context.PromotionBag;
var cart = Context.Cart;
var promotionName = bag.PromotionName;
decimal subtotal = cart.SubTotal;
decimal quantity = cart.GetTotalItemCount();
Both objects are described fully in Chapter 7. When a promotion needs to evaluate individual products, iterate the cart items:
foreach (var item in Context.Cart.CartItems)
{
var sku = item.Product.SKU;
var quantity = item.Quantity;
var price = item.ExtendedPrice;
}
Typical use cases for iterating cart items include Buy-X-Get-Y offers, category or brand discounts, product bundles, and tiered pricing.
Step 10 — Apply the Discount
Use the SDK's discount helper methods rather than constructing discount objects manually.
Order-Level Discount
result.OrderLevelDiscount =
CreatePromoDiscount(
10,
DiscountLevelCode.OrderLevel);
result.IsApplied = true;
Line-Item Discount
result.LineItemDiscounts.Add(
itemIndex,
CreatePromoDiscount(
discountAmount,
DiscountLevelCode.LineItemLevel));
Shipping Discount
result.ShippingLevelDiscount =
CreatePromoDiscount(
20,
DiscountLevelCode.ShippingLevel);
result.IsApplied = true;
Important: The Promotion Engine only applies a discount when IsApplied is explicitly set to true.
Step 11 — Return the Result
Always return a valid PromotionCalculateResult. Never return null.
return result;
Step 12 — Build and Verify
Compile the project to generate the extension DLL:
dotnet build
bin
└── Release
└── net8.0
Znode.Libraries.Promotions.Custom.dllOnly the compiled DLL is deployed to the Extension Framework (see Chapter 9 for deployment steps).
Complete Working Example
The following example combines every step above into a single, minimal but complete promotion implementation.
This example demonstrates a custom promotion named FlatTenDollarPromotion created using the Promotion SDK.
When the promotion runs, it checks whether the cart has items and then applies a flat $10 order-level discount.
using Znode.Libraries.Promotions.SDK;
using Znode.Libraries.Promotions.SDK.Attributes;
namespace YourCompany.CustomPromotions;
[PromotionExtension(typeof(CartPromotionTypeBase))]
public class FlatTenDollarPromotion : CartPromotionTypeBase
{
public FlatTenDollarPromotion()
{
Name = "Flat $10 Off";
Description = "Applies a flat $10 discount.";
ClassName = "FlatTenDollarPromotion";
}
public override PromotionCalculateResult Calculate(
int? couponIndex,
IReadOnlyList<IPromotionInfo> allPromotions)
{
var result = new PromotionCalculateResult();
if (GetCartQuantity() <= 0)
return result;
result.OrderLevelDiscount = CreatePromoDiscount(
10,
DiscountLevelCode.OrderLevel);
result.IsApplied = true;
return result;
}
}
Error Handling
A custom promotion should never terminate the Promotion Engine because of an unhandled exception. Wrap business logic in structured exception handling and log the failure:
try
{
// Promotion logic
}
catch (Exception ex)
{
var logger = ServiceProvider.GetService<IExtensionLogger>();
await logger.LogErrorAsync(
"Promotion",
ClassName,
nameof(Calculate),
ex);
return new PromotionCalculateResult();
}
Returning an empty result ensures that the Promotion Engine continues evaluating other promotions even if one extension fails.
Validation Checklist
| Promotions SDK referenced | ✓ |
|---|---|
| Separate class library created | ✓ |
| Validation | Status |
| Promotion inherits CartPromotionTypeBase | ✓ |
| PromotionExtension attribute applied | ✓ |
| Calculate() method implemented | ✓ |
| ClassName configured and matches Admin | ✓ |
| Project builds successfully | ✓ |
Common Mistakes
| Issue | Impact | Resolution |
|---|---|---|
| Missing PromotionExtension attribute | Extension is never discovered | Add the attribute to the class |
| Does not inherit CartPromotionTypeBase | Promotion cannot execute | Inherit from the SDK base class |
| ClassName mismatch | Promotion is never invoked | Ensure Admin configuration matches exactly (case-sensitive) |
| Returning null | Runtime exception | Always return a new PromotionCalculateResult |
| Forgetting IsApplied = true | Discount ignored | Set IsApplied only when a discount is successfully applied |
| Building the wrong project | Incorrect DLL deployed | Build only the extension project |
6. Promotion SDK Overview
The Promotions SDK is the foundation of the Promotion Extensibility Framework. It provides a standardized programming model for developing custom promotions without requiring changes to the Znode core application, exposing the contracts, base classes, helper methods, and context objects required to implement promotion logic. Every custom promotion should use these SDK components rather than interacting directly with the internal promotion engine.
6.1 SDK Architecture
The following diagram illustrates how the Promotion SDK interacts with the Extension Framework and Promotion Engine.
Promotion Engine
(executes Calculate())
|
v
CartPromotionTypeBase
|
v
Context | Helper APIs | Logging | DI Services | Base Methods
|
v
Cart | Promotions | Coupons | Products | Customer | PortalThe SDK abstracts the internal implementation of the Promotion Engine and provides developers with only the APIs required to implement business logic.
6.2 SDK Components
| Component | Purpose |
|---|---|
| CartPromotionTypeBase | Base class for all cart promotions |
| PromotionExtension attribute | Registers the class as an extension |
| Promotion Context | Provides runtime data |
| Promotion Bag | Provides promotion configuration |
| Cart / Product / Coupon Objects | Runtime shopping cart, product, and coupon information |
| Helper APIs | Common promotion calculations and lookups |
| Logger | Extension logging |
| Discount Models | Apply discounts to the cart |
6.3 CartPromotionTypeBase
Every custom promotion must inherit from CartPromotionTypeBase. Without inheriting from this base class, the promotion cannot participate in the execution pipeline.
public class FlatDiscountPromotion : CartPromotionTypeBase
{
}
The base class exposes the following inherited properties:
| Property | Description |
|---|---|
| Context | Complete promotion execution context (see Chapter 7) |
| ServiceProvider | Dependency Injection container |
| Name | Promotion display name |
| Description | Promotion description |
| ClassName | Unique promotion identifier |
6.4 Dependency Injection
The SDK supports Dependency Injection through the built-in ServiceProvider. Developers may resolve any registered service from the DI container, such as logging, configuration, external APIs, repository services, or cache providers.
var logger = ServiceProvider.GetService();
6.5 Base Class Helper Methods
The base class exposes several helper methods that eliminate repetitive code and ensure consistent discount creation:
| Method | Purpose |
|---|---|
| GetCartSubTotal() | Returns the cart subtotal before discounts |
| GetCartQuantity() | Returns the total quantity across the cart |
| CreatePromoDiscount() | Creates an order/line/shipping-level promotion discount |
| CreateCouponDiscount() | Creates a coupon-based discount |
The full set of helper APIs — including product, category, brand, and catalog lookups exposed through IPromotionCalculationHelper — is covered in Chapter 12.
6.6 PromotionCalculateResult
Every promotion returns a PromotionCalculateResult, which communicates the outcome of the promotion execution back to the Promotion Engine:
var result = new PromotionCalculateResult();
It may contain:
- Order-level discount
- Shipping-level discount
- Line-item discounts
- Coupon result
- The IsApplied flag
Important: The engine applies discounts only when IsApplied is set to true.
6.7 SDK Best Practices
- Always inherit from CartPromotionTypeBase.
- Access runtime data through the Context object rather than external services.
- Use SDK helper methods instead of custom implementations.
- Resolve services through ServiceProvider rather than creating instances manually.
- Use CreatePromoDiscount() and CreateCouponDiscount() to construct discount objects.
- Keep business logic focused on promotion rules; avoid embedding unrelated application logic.
- Return a valid PromotionCalculateResult in every execution path.
7. Understanding the Promotion Context
IPromotionContext is the central object provided by the Promotions SDK. It contains all the runtime information required to evaluate and apply a promotion during cart calculation. Every custom promotion receives an initialized Context object through CartPromotionTypeBase, and it should be used as the single entry point for accessing cart information, promotion configuration, helper methods, and other runtime data.
7.1 Context Architecture
IPromotionContext
|
v
PromotionCart | PromotionBag | Helper APIs | ServiceProvider
|
v
Cart Items, Coupons, Customer, Products /
Promotion Info, Coupons, Settings, Attributes /
Product, Category, Brand, Catalog Lookup| Property | Type | Description |
|---|---|---|
| Cart | IPromotionCart | Current shopping cart |
| PromotionBag | IPromotionBag | Promotion configuration from Admin |
| Helper | IPromotionCalculationHelper | Helper methods for promotion validation |
| ServiceProvider | IServiceProvider | Dependency Injection container |
public override PromotionCalculateResult Calculate(
int? couponIndex,
IReadOnlyList allPromotions)
{
var cart = Context.Cart;
var promotion = Context.PromotionBag;
var helper = Context.Helper;
return new PromotionCalculateResult();
}
7.2 IPromotionCart
Represents the customer's shopping cart, providing access to cart totals, customer information, products, coupons, and order details.
| OrderId | int? | Existing order ID (if applicable) |
|---|---|---|
| UserId | int? | Logged-in customer ID |
| ProfileId | int? | Customer profile |
| PortalId | int? | Current store identifier |
| SubTotal | decimal | Cart subtotal before discounts |
| Property | Type | Description |
| Property | Type | Description |
|---|---|---|
| IsAnyPromotionApplied | bool | Whether another promotion has already been applied |
| CartItems | IReadOnlyList<IPromotionCartItem> | Products in the cart |
| Coupons | IReadOnlyList<ICouponInfo> | Coupons entered by the customer |
Useful Methods
decimal quantity =
Context.Cart.GetTotalItemCount();
decimal maxDiscount =
Context.Cart.GetMaximumApplicableDiscountOnLineItem(
discount,
quantity,
product);
// ensures a discount never exceeds product price
int sequence =
Context.Cart.GetDiscountAppliedSequence(discountCode);
7.3 IPromotionCartItem
Each product in the shopping cart is represented by an IPromotionCartItem.
| Property | Type | Description |
|---|---|---|
| Product | IPromotionProduct | Product details |
| Quantity | decimal | Ordered quantity |
| PromotionalPrice | decimal | Current promotional price |
| ExtendedPrice | decimal | Total line amount |
| ExtendedPriceDiscount | decimal | Discount applied to the line |
| ParentProductId | int | Parent product for grouped/configurable products |
| Custom1–Custom5 | string | Custom fields available for business use |
foreach (var item in Context.Cart.CartItems)
{
var sku = item.Product.SKU;
var quantity = item.Quantity;
var lineTotal = item.ExtendedPrice;
}
7.4 IPromotionProduct
| PromotionalPrice | Promotional selling price |
|---|---|
| FinalPrice | Final configured price |
| BrandCode | Brand identifier |
| SKU | Product SKU |
| ProductID | Internal product identifier |
| Property | Description |
| Property | Description |
|---|---|
| DiscountAmount | Total discount applied |
| OrderDiscountAmount | Order-level allocated discount |
| SelectedQuantity | Quantity selected |
| GroupProducts / ConfigurableProducts / AddonProducts | Child, configurable, and add-on products |
foreach (var item in Context.Cart.CartItems)
{
if (item.Product.BrandCode == "APPLE")
{
// Brand-specific logic
}
}
7.5 IPromotionBag
Contains all configuration entered by administrators when creating the promotion. Unlike the cart, this object represents promotion configuration, not runtime data.
| Property | Description |
|---|---|
| PromotionId | Promotion record ID |
| PromotionName | Display name |
| PromotionMessage | Customer message |
| PromotionTypeId | Promotion type |
| PromoCode | Promotion or coupon code |
| PortalId / ProfileId | Portal / customer profile restriction |
| IsUnique | Unique coupon flag |
| HasCoupons | Indicates a coupon-based promotion |
| Coupons | Coupons configured in Admin |
var promotionName =
Context.PromotionBag.PromotionName;
var promoCode =
Context.PromotionBag.PromoCode;
7.6 IPromotionInfo
The allPromotions parameter passed to Calculate() contains every active promotion currently being evaluated.
| Discount | Discount value |
|---|---|
| Name | Promotion name |
| PromoCode | Coupon code |
| PromotionId | Promotion identifier |
| Property | Description |
| Property | Description |
|---|---|
| OrderMinimum / QuantityMinimum | Minimum order amount / quantity |
| IsCouponRequired | Coupon requirement |
| DisplayOrder | Promotion priority |
| IsUnique | Unique coupon |
| ClassName / ClassType | Promotion class and type |
| Attributes | User-defined fields |
Filter this list to the promotions relevant to the current class — this should typically be the first operation performed inside Calculate():
var promotions = Context.GetPromotionsByType(
"Cart",
ClassName,
allPromotions,
"PromotionId");
7.7 ICouponInfo
Coupons are available from two locations: customer-entered coupons on the cart, and coupons configured for the promotion.
Context.Cart.Coupons
Context.PromotionBag.Coupons
| Property | Description |
|---|---|
| Code | Coupon code |
| InitialQuantity / AvailableQuantity | Issued / remaining quantity |
| IsActive | Coupon status |
| CouponValid | Coupon validation result |
| CouponApplied | Applied flag |
| CouponMessage | Customer message |
| IsExistInOrder | Already used in an order |
foreach (var coupon in Context.PromotionBag.Coupons)
{
if (coupon.AvailableQuantity > 0)
{
// Apply coupon
}
}
7.8 IPromotionCalculationHelper
Provides reusable APIs for common promotion operations such as product, category, brand, and catalog lookups. The complete list of methods is detailed in Chapter 12.
var helper = Context.Helper;
var productIds =
Context.Helper.GetPromotionProductIds(
promotionId);
7.9 Best Practices
- Always access runtime data through the Context object instead of external services.
- Use Context.Helper methods for product, category, brand, and catalog lookups instead of writing custom queries.
- Treat IPromotionCart as read-only; return changes through PromotionCalculateResult.
- Avoid modifying cart or coupon objects directly.
- Keep business logic focused on evaluating promotion eligibility and calculating discounts.
8. Logging and Observability
Since promotion execution occurs within the Promotion Engine rather than a traditional web request, developers need visibility into the execution flow, validation decisions, applied discounts, and runtime exceptions. The framework provides this through the IExtensionLogger interface and the Extension Observability page in Znode Admin.
Without proper logging, it can be difficult to determine whether a promotion was discovered, whether Calculate() was invoked, why a promotion was skipped or a discount was not applied, whether an exception occurred, or which promotion was applied to a given cart.
8.1 Logging Architecture
Promotion Engine
|
v
Custom Promotion Extension
|
v
IExtensionLogger
|
v
Extension Logging Service
|
v
Extension Observability Page
|
v
Execution Logs & Diagnostics8.2 IExtensionLogger
Unlike writing directly to a file or console, this logger integrates with the Extension Framework and surfaces logs in the Extension Observability page. Resolve it using Dependency Injection:
var logger = ServiceProvider?.GetService();
Best Practice: Resolve the logger inside Calculate() rather than the constructor — the service provider may not be available during object construction.
8.3 Writing Logs
Informational Logs
Useful for tracing execution flow, successful application, and validation outcomes:
await logger.LogInformationAsync(
"Znode.Libraries.Promotions.Custom",
ClassName,
nameof(Calculate),
"Promotion execution started.");
Typical informational events: execution started, validation completed, promotion skipped (with the reason), discount applied, execution completed.
Warnings
Indicate unexpected but non-fatal conditions such as an expired coupon, incomplete promotion configuration, a missing attribute, an empty product list, or a zero discount value:
await logger.LogWarningAsync(
"Znode.Libraries.Promotions.Custom",
ClassName,
nameof(Calculate),
"Promotion configuration does not contain eligible products.");
Errors
Unexpected exceptions should always be logged:
try
{
// Promotion logic
}
catch (Exception ex)
{
await logger.LogErrorAsync(
"Znode.Libraries.Promotions.Custom",
ClassName,
nameof(Calculate),
ex);
return new PromotionCalculateResult();
}
8.4 Recommended Logging Strategy
| Stage | Log Level |
|---|---|
| Promotion execution started | Information |
| Validation completed | Information |
| Promotion skipped | Information |
| Discount applied | Information |
| Missing configuration | Warning |
| Invalid coupon | Warning |
| Unexpected exception | Error |
| Promotion execution completed | Information |
Example log sequence for a successful execution:
INFO Promotion execution started. INFO Minimum order validation passed. INFO Eligible products found. INFO Applied $10 order discount. INFO Promotion execution completed successfully.
8.5 Extension Observability
The Extension Observability page provides centralized visibility into all deployed extensions, allowing developers and administrators to view deployed extensions, reload updated DLLs, monitor execution, and diagnose runtime failures.
Navigate to:
Admin → Extension Observability
The page provides two key capabilities:
- Extension Synchronization — synchronizes the latest deployed extension assemblies with the running application.
- Execution Logs — displays runtime logs generated by custom extensions.
Important: Always click Sync after deploying a new or updated extension. Otherwise, the Promotion Engine will continue executing the previously loaded version.
8.6 Common Troubleshooting Scenarios
| Issue | Possible Cause | Resolution |
|---|---|---|
| Promotion never executes | Missing PromotionExtension attribute | Add the attribute and redeploy |
| Old code still executes | Extension not synchronized | Click Sync in Extension Observability |
| No logs appear | Logger not resolved or logging not implemented | Verify IExtensionLogger usage |
| Promotion skipped | Validation conditions not met | Review informational logs |
| Runtime exception | Error in custom logic | Check Error logs and exception details |
8.7 Logging Best Practices
- Log meaningful business events rather than every line of code.
- Include the promotion class name in every log entry.
- Use consistent and descriptive log messages.
- Avoid logging sensitive customer information.
- Use Warning for recoverable issues and Error for unexpected exceptions.
- Keep log messages concise and actionable.
- Enable Information, Warning, and Error logging in production; avoid excessive debug logging.
- Review execution logs after every extension deployment.
9. Deployment and Extension Management
Once a custom promotion has been developed and tested, it must be deployed to the Znode Extension Framework before the Promotion Engine can use it. Unlike traditional deployments that require redeploying the whole application, extensions are deployed independently and activated through the Extension Observability page.
9.1 Deployment Overview
Build Extension Project
|
v
Generate Extension DLL
|
v
Deploy using Znode CLI
|
v
Extension Storage
|
v
Extension Loader
|
v
Click Sync
|
v
Reload Latest DLL
|
v
Promotion Ready for ExecutionOnly the extension assembly is deployed — the core Znode application remains unchanged.
9.2 Deployment Prerequisites
| Requirement | Description |
|---|---|
| Extension project builds successfully | No compilation errors |
| Znode CLI installed | Latest supported version |
| CLI authenticated | Connected using a valid token |
| Extension deployment permissions | Required to upload extensions |
| Admin access | Required for synchronization |
| Extension Observability access | Required to reload extensions |
9.3 Step 1 — Build the Extension
dotnet build
bin
└── Release
└── net8.0
Znode.Libraries.Promotions.Custom.dll
Only successfully compiled assemblies should be deployed.
9.4 Step 2 — Connect to the Znode CLI
Authenticate the CLI with the target environment before deploying:
znode connect <token>
znode connect eyJhbGciOiJIUzI1NiIs...
9.5 Step 3 — Deploy a Single Extension Project
znode deploy extension "<path-to-project.csproj>"
znode deploy extension "D:\Extensions\Znode.Libraries.Promotions.Custom\Znode.Libraries.Promotions.Custom.csproj"
During deployment, the CLI builds the project, packages the extension, uploads the compiled assembly, and registers the new extension version.
9.6 Step 4 — Deploy Multiple Extension Projects
To deploy several extension projects at once, point the CLI at the containing folder:
znode deploy extension "<path-to-folder>"
znode deploy extension "D:\Extensions"
The CLI automatically searches for supported extension projects, builds each one, and uploads all generated assemblies. This is the recommended approach when deploying multiple promotion extensions together.
9.7 Step 5 — Synchronize the Extension
After deployment, navigate to Admin → Extension Observability and click Sync. Synchronization detects newly deployed assemblies, reloads updated versions, refreshes the Extension Loader, and makes the latest extension available to the Promotion Engine.
Important: Deployment alone does not activate the extension. Synchronization is required before the Promotion Engine can execute the latest version.
9.8 Verifying Deployment
- The extension appears in the Extension Observability page.
- No loading errors are reported.
- Execution logs are available.
- The promotion type can be registered in Admin.
- The Promotion Engine executes the new extension.
9.9 Updating an Existing Extension
- Modify the source code.
- Build the project.
- Deploy the updated extension.
- Synchronize the application.
- Validate the updated behavior.
No changes are required to the core Znode application.
9.10 Extension Version Management
The Extension Framework supports independent versioning of custom extensions.
- Version each release using semantic versioning (e.g., 1.0.0, 1.1.0, 2.0.0).
- Maintain release notes for each deployment.
- Avoid breaking changes in minor releases.
- Test new versions in a non-production environment before deployment.
| Version | Description |
|---|---|
| 1.0.0 | Initial implementation |
| 1.1.0 | Added category validation |
| 1.2.0 | Improved logging |
| 2.0.0 | Introduced coupon enhancements |
9.11 Rollback Strategy
- Redeploy the previous stable version.
- Synchronize the application.
- Verify that the earlier version is active.
- Investigate the issue before redeploying the updated version.
Maintaining previous builds in source control or an artifact repository simplifies rollback.
9.12 Common Deployment Issues
| Issue | Possible Cause | Resolution |
|---|---|---|
| CLI connection fails | Invalid or expired token | Reconnect using a valid token |
| Build fails | Compilation errors | Resolve errors before deployment |
| Extension not visible | Deployment unsuccessful | Verify CLI output and upload status |
| Old logic still executes | Synchronization not performed | Click Sync in Extension Observability |
| Promotion not found | Promotion type not registered | Register the class in Promotion Settings |
| Extension load error | Missing dependencies or incompatible assembly | Verify project references and rebuild |
9.13 Deployment Checklist
| Validation | Status |
|---|---|
| Project builds successfully | ✓ |
| CLI connected to target environment | ✓ |
| Extension deployed successfully | ✓ |
| Synchronization completed | ✓ |
| Extension loaded without errors | ✓ |
| Promotion type registered | ✓ |
| Promotion created in Admin | ✓ |
| Execution logs verified | ✓ |
| Functional testing completed | ✓ |
9.14 Deployment Best Practices
- Build in Release configuration for production deployments.
- Test all promotion logic before deployment.
- Deploy only the intended extension project.
- Synchronize immediately after deployment.
- Review execution logs to confirm successful loading.
- Version extensions consistently and keep deployment artifacts in a secure repository.
- Document deployment history and release notes.
10. Registering Promotion Types and Configuring Promotions
After deploying and synchronizing a custom promotion extension, the next step is to register the promotion type within Znode Admin. Registration allows the Promotion Engine to associate a configured promotion with the corresponding extension class. Once registered, business users can create and manage promotions through the standard Promotion Management interface without further code changes.
10.1 Registration Workflow
Deploy Extension DLL
|
v
Synchronize Extension
|
v
Register Promotion Type
|
v
Configure Promotion Attributes
|
v
Create Promotion
|
v
Activate Promotion
|
v
Customer Adds Products
|
v
Promotion Engine Executes
|
v
Discount Applied10.2 Prerequisites
| Requirement | Status |
|---|---|
| Extension successfully deployed | ✓ |
| Extension synchronized | ✓ |
| Promotion class discovered | ✓ |
| Admin access available | ✓ |
| Custom promotion DLL loaded | ✓ |
10.3 Step 1 — Open Promotion Settings
Navigate to:
Admin Portal → Dev Center → Promotion Settings → Promotion Types.
This page displays all available promotion types, including both built-in and custom extensions.
10.4 Step 2 — Create a New Promotion Type
Click Add Promotion Type and provide the required information:
| Field | Value |
|---|---|
| Name | Flat Order Discount |
| Description | Applies a flat discount on qualifying orders |
| Class Name | FlatTenDollarPromotion |
| Status | Active |
Class Name
The Class Name is the most critical configuration field. It must exactly match the value assigned in the promotion class constructor:
public FlatTenDollarPromotion()
{
ClassName = "FlatTenDollarPromotion";
}
Important: The class name comparison is case-sensitive. Any mismatch prevents the Promotion Engine from locating the extension.
10.5 Step 3 — Configure User-Defined Fields (Optional)
Some promotions require configuration values that are not part of the standard promotion model — such as minimum purchase amount, eligible customer tier, brand code, category ID, maximum discount, reward points, or an external campaign identifier. These are captured using User-Defined Fields (UDFs):
| Field Name | Data Type | Example Value |
|---|---|---|
| MinimumOrder | Decimal | 500 |
| DiscountPercent | Decimal | 10 |
| BrandCode | Text | NIKE |
| MaxDiscount | Decimal | 200 |
| CustomerTier | Text | Gold |
These values become available through the PromotionBag during execution:
var minimumOrder =
Context.PromotionBag.Attributes["MinimumOrder"];
var discount =
Context.PromotionBag.Attributes["DiscountPercent"];
Using configurable attributes allows business users to change promotion behavior without requiring code changes or redeployment.
10.6 Step 4 — Create a Promotion
Navigate to:
Marketing → Promotions → Create Promotion
Then select the custom promotion type registered in the previous step.
| Configuration | Description |
|---|---|
| Promotion Name | Display name |
| Promotion Type | Custom promotion type |
| Start Date / End Date | Activation and expiration dates |
| Priority | Execution priority |
| Active | Enable/disable promotion |
| Stores | Applicable portals |
| Customer Profiles | Target customer groups |
| User-Defined Fields | Custom configuration |
10.7 Coupon Configuration
If the promotion requires a coupon, enable coupon support during promotion creation:
| Property | Description |
|---|---|
| Coupon Code | Customer-entered code |
| Expiration Date | Coupon validity |
| Available Quantity | Number of uses |
| Maximum Usage | Usage limit |
| Unique Coupon | One-time use |
Coupon Code: WELCOME10
During execution:
foreach (var coupon in Context.Cart.Coupons)
{
if (coupon.Code == "WELCOME10")
{
// Apply promotion
}
}
10.9 Verifying Registration
- The promotion type appears in the Promotion Settings page.
- The custom class name matches the implementation.
- The promotion is active and appears in the Promotions list.
- The extension executes during cart calculation.
- Execution logs are visible in Extension Observability.
10.10 Common Configuration Issues
| Issue | Cause | Resolution |
|---|---|---|
| Promotion type not visible | Extension not synchronized | Synchronize the extension |
| Promotion never executes | Incorrect Class Name | Ensure Admin configuration matches the class exactly |
| Promotion unavailable | Promotion inactive | Activate the promotion |
| Coupon ignored | Coupon not configured | Verify coupon settings and validity |
| User-defined fields empty | Attribute names mismatch | Ensure attribute keys match between Admin and code |
| Promotion not applied | Business rules not satisfied | Review validation logic and execution logs |
10.11 Example Configuration
| Setting | Value |
|---|---|
| Promotion Name | Summer Sale |
| Promotion Type | Flat Order Discount |
| Class Name | FlatTenDollarPromotion |
| Discount | 10 USD |
| Minimum Order | 100 USD |
| Start Date | 01-Jun-2026 |
| End Date | 30-Jun-2026 |
| Coupon Required | No |
| Active | Yes |
10.12 Best Practices
- Use descriptive names for promotion types and promotions.
- Keep the ClassName consistent across code and Admin.
- Prefer configurable user-defined fields over hardcoded values.
- Validate promotion settings before activating them.
- Test each promotion in a non-production environment.
- Document the purpose and configuration of each custom promotion for administrators.
11. Promotion Execution Flow and Runtime Processing
This chapter explains the complete runtime lifecycle of a promotion — from the moment a customer performs an action in the storefront until the promotion result is applied to the shopping cart. Understanding this pipeline helps developers optimize promotion logic, troubleshoot issues effectively, and avoid common implementation mistakes.
11.1 When Is the Promotion Engine Invoked?
The engine evaluates all eligible promotions — built-in and custom, using the same execution pipeline — whenever one of the following customer actions occurs:
| Customer Action | Promotion Evaluation |
|---|---|
| Add product to cart | ✓ |
| Remove product | ✓ |
| Update quantity | ✓ |
| Apply / remove coupon | ✓ |
| View shopping cart | ✓ |
| Checkout | ✓ |
| Shipping method change | ✓ |
| Cart recalculation | ✓ |
Developers do not need to manually invoke the Promotion Engine.
11.2 Runtime Execution Pipeline
Step 1 — Load Active Promotions
The engine retrieves promotions that satisfy active status, current date/time, portal, customer profile, and store assignment. Inactive or expired promotions are ignored.
Step 2 — Validate Promotion Eligibility
The engine validates promotion dates, portal and customer restrictions, coupon requirements, promotion status, and execution priority. Only eligible promotions continue to the next stage.
Step 3 — Discover Extension Class
The Extension Framework locates the registered promotion class using the configured Class Name and creates an instance. If the class cannot be found, the promotion is skipped and an error is logged.
Promotion Type
|
v
FlatTenDollarPromotion (Class Name)
|
v
Extension Loader
|
v
FlatTenDollarPromotion.dll
|
v
Create InstanceStep 4 — Create Promotion Context
Before invoking the promotion, the SDK initializes a fully populated runtime context — cart, cart items, products, coupons, promotion configuration, helper APIs, service provider, and logger.
Step 5 — Execute Calculate()
The engine invokes the overridden Calculate() method. This is the only location where promotion business logic should be implemented.
Step 6 — Business Rule Evaluation
Inside Calculate(), the promotion evaluates conditions such as minimum order amount, product/brand/category eligibility, customer profile, coupon validity, and quantity. If conditions are not satisfied, an empty result is returned:
if (Context.Cart.SubTotal < 500)
{
return new PromotionCalculateResult();
}
Step 7 — Discount Calculation
Once eligibility is confirmed, the applicable discount is calculated using the SDK's discount helpers:
var result = new PromotionCalculateResult();
result.OrderLevelDiscount =
CreatePromoDiscount(
100,
DiscountLevelCode.OrderLevel);
result.IsApplied = true;
Step 8 — Return Result
The completed PromotionCalculateResult is returned to the Promotion Engine, which validates and applies the discount:
return result;
11.3 Execution Sequence
Customer
|
v
Shopping Cart
|
v
Promotion Engine
|
v
Extension Loader
|
v
Custom Promotion
|
v
Calculate()
|
v
Promotion Result
|
v
Promotion Engine
|
v
Updated Cart
|
v
Customer11.6 Execution Flow Example
Promotion: "Flat $100 Off on Orders Above $1000"
| Product | Price |
|---|---|
| Laptop | $900 |
| Mouse | $150 |
| Keyboard | $100 |
Subtotal: $1,150
- Customer opens the cart.
- Promotion Engine starts evaluation.
- Active promotions are loaded.
- FlatOrderPromotion is identified.
- Context is created.
- Calculate() executes.
- Minimum order validation passes.
- $100 discount is generated.
- Promotion result is returned.
- Cart total becomes $1,050.
- Execution is logged.
11.7 Performance Considerations
Promotion execution occurs frequently during the customer journey, so inefficient logic can negatively impact cart and checkout performance.
- Exit early when validation fails.
- Avoid unnecessary loops over cart items.
- Use SDK helper methods for lookups.
- Cache repeated calculations within Calculate().
- Avoid database or external API calls whenever possible.
- Resolve services once per execution.
11.8 Runtime Error Handling
A failure in one custom promotion should not prevent other promotions from executing. Always wrap complex logic in exception handling and return an empty result on failure, so the Promotion Engine continues evaluating other promotions:
try
{
// Promotion logic
}
catch (Exception ex)
{
await logger.LogErrorAsync(
"Promotion",
ClassName,
nameof(Calculate),
ex);
return new PromotionCalculateResult();
}
12. SDK Helper Methods and Discount APIs
The Promotions SDK provides a rich set of helper methods that simplify common promotion development tasks. Instead of implementing complex validation or discount logic manually, developers should leverage these built-in APIs to ensure consistency, improve performance, and reduce maintenance effort. This chapter is the complete reference for the helper method categories introduced in Chapters 6 and 7.
12.1 Helper Method Categories
| Category | Purpose |
|---|---|
| Cart Helpers | Retrieve cart totals, quantities, and validation data |
| Product Helpers | Access eligible products |
| Category Helpers | Retrieve promotion categories |
| Brand Helpers | Validate product brands |
| Catalog Helpers | Access catalog mappings |
| Coupon Helpers | Validate coupon information |
| Discount Helpers | Create order, line-item, and shipping discounts |
12.2 Cart Helper Methods
GetCartSubTotal()
Returns the subtotal of the shopping cart before discounts. Typical use cases: minimum order amount validation, tiered pricing, free shipping eligibility.
if (GetCartSubTotal() >= 500)
{
// Apply promotion
}
GetCartQuantity()
Returns the total quantity of all items in the cart.
if (GetCartQuantity() >= 5)
{
// Apply bulk purchase discount
}
Accessing Cart Items
foreach (var item in Context.Cart.CartItems)
{
// Process each item
}
12.3 Product, Category, Brand, and Catalog Helpers
GetPromotionProductIds()
var productIds =
Context.Helper.GetPromotionProductIds(promotionId);
if (productIds.Contains(item.Product.ProductID))
{
// Eligible product
}
GetPromotionCategoryIds() / GetCategoryIdsByProduct()
var categoryIds =
Context.Helper.GetPromotionCategoryIds(promotionId);
var categories =
Context.Helper.GetCategoryIdsByProduct(item.Product.ProductID);
if (categories.Any(c => categoryIds.Contains(c)))
{
// Product belongs to an eligible category
}
GetPromotionBrandCodes()
var brands =
Context.Helper.GetPromotionBrandCodes(promotionId);
if (brands.Contains(item.Product.BrandCode))
{
// Brand qualifies
}
GetPromotionCatalogIds()
Returns all catalogs configured for the promotion — useful when promotions are restricted to specific catalogs.
var catalogs =
Context.Helper.GetPromotionCatalogIds(promotionId);
12.4 Coupon Helper Methods
Customer-entered coupons are available through the cart:
var coupons = Context.Cart.Coupons;
foreach (var coupon in coupons)
{
if (coupon.Code == "WELCOME10")
{
// Coupon is present
}
}
12.5 Discount Helper Methods
Always use these SDK methods instead of creating discount objects manually.
CreatePromoDiscount()
result.OrderLevelDiscount =
CreatePromoDiscount(
100,
DiscountLevelCode.OrderLevel);
result.IsApplied = true;
CreateCouponDiscount()
result.OrderLevelDiscount =
CreateCouponDiscount(
coupon.Code,
100,
DiscountLevelCode.OrderLevel);
Both methods automatically populate required metadata such as promotion name, promotion type, discount code, and promotion message.
12.6 Discount Levels
| Discount Level | Description |
|---|---|
| OrderLevel | Discount applied to the entire order |
| LineItemLevel | Discount applied to individual products |
| ShippingLevel | Discount applied to shipping charges |
// Order-level
result.OrderLevelDiscount =
CreatePromoDiscount(
50,
DiscountLevelCode.OrderLevel);
// Line-item
result.LineItemDiscounts.Add(
lineItemIndex,
CreatePromoDiscount(
20,
DiscountLevelCode.LineItemLevel));
// Shipping
result.ShippingLevelDiscount =
CreatePromoDiscount(
shippingCharge,
DiscountLevelCode.ShippingLevel);
12.7 Maximum Discount Validation
Always ensure discounts do not exceed the value of the eligible product or order:
decimal maxDiscount =
Context.Cart.GetMaximumApplicableDiscountOnLineItem(
requestedDiscount,
item.Quantity,
item.Product);
Using this helper prevents negative prices and over-discounting.
12.8 Retrieving Applicable Promotions
The Calculate() method receives all active promotions. Filter the list to the promotions applicable to the current extension to reduce unnecessary processing:
var promotions =
Context.GetPromotionsByType(
"Cart",
ClassName,
allPromotions,
"PromotionId");
12.9 Helper Method Quick Reference
| Retrieve promotion products | GetPromotionProductIds() |
|---|---|
| Validate minimum quantity | GetCartQuantity() |
| Validate minimum order | GetCartSubTotal() |
| Requirement | Recommended Helper |
| Requirement | Recommended Helper |
|---|---|
| Retrieve categories | GetPromotionCategoryIds() |
| Retrieve brands | GetPromotionBrandCodes() |
| Retrieve catalogs | GetPromotionCatalogIds() |
| Create order discount | CreatePromoDiscount() |
| Create coupon discount | CreateCouponDiscount() |
| Validate maximum discount | GetMaximumApplicableDiscountOnLineItem() |
12.10 Combined Example
The following example combines several helper methods in a single promotion:
public override PromotionCalculateResult Calculate(
int? couponIndex,
IReadOnlyList allPromotions)
{
var result = new PromotionCalculateResult();
if (GetCartSubTotal() < 1000) return result;
if (GetCartQuantity() < 2) return result;
result.OrderLevelDiscount = CreatePromoDiscount(
100,
DiscountLevelCode.OrderLevel);
result.IsApplied = true;
return result;
}
13. Best Practices and Troubleshooting
This chapter consolidates every recommendation introduced throughout this guide into a single reference, along with the complete troubleshooting reference for diagnosing issues in custom promotion extensions.
13.1 Development Best Practices
Keep promotions focused
Implement a single business rule per promotion class. Avoid combining multiple unrelated conditions in one class.
Use SDK APIs
Always use the helper methods provided by the SDK (GetCartSubTotal(), GetCartQuantity(), CreatePromoDiscount(), CreateCouponDiscount(), and the IPromotionCalculationHelper lookups) instead of implementing custom lookup or discount logic.
Avoid hardcoded values
Store configurable values — discount percentages, minimum order amounts, product IDs — in Promotion Attributes (User-Defined Fields) rather than in code.
| Approach | Example |
|---|---|
| Recommended | Discount = Promotion Attribute |
| Avoid | Discount = 10 (hardcoded) |
Add structured logging
Log promotion started, validation passed/failed, discount applied, and exceptions — but avoid excessive logging inside loops.
Handle exceptions
Always wrap business logic in exception handling and return an empty result on failure so that other promotions continue to be evaluated.
Optimize performance
- Exit early when validation fails.
- Avoid unnecessary iterations over cart items.
- Minimize external service or database calls.
- Reuse SDK helper methods whenever possible.
- Keep promotion logic stateless and thread-safe.
13.2 Deployment Best Practices
- Build in Release configuration.
- Test extensions before deployment.
- Synchronize extensions immediately after deployment.
- Version extension assemblies using semantic versioning.
- Validate execution logs after every deployment.
- Maintain deployment history and release notes.
13.3 Configuration Best Practices
- Use descriptive names for promotion types and promotions.
- Keep the ClassName consistent — and case-matched — across code and Admin.
- Prefer configurable user-defined fields over hardcoded values.
- Validate and test promotion settings in a non-production environment before activating them.
13.4 Common Issues Reference
| Issue | Possible Cause | Resolution |
|---|---|---|
| Promotion not executing | Extension not synchronized | Synchronize the extension |
| Promotion type not found | Incorrect Class Name | Verify Admin configuration |
| Discount not applied | IsApplied not set | Set result.IsApplied = true |
| No execution logs | Logging not implemented | Configure IExtensionLogger |
| Coupon ignored | Invalid or expired coupon | Verify coupon configuration |
| Old logic executing | Previous DLL still loaded | Deploy the latest DLL and Sync |
| Extension not loaded | Build or deployment failure | Verify CLI deployment and dependencies |
13.5 Debugging Checklist
- Class name matches the implementation.
- Promotion is active.
- Coupon (if required) is valid.
- Business conditions are satisfied.
- Calculate() method executes.
- PromotionCalculateResult is returned.
- IsApplied is set to true.
- Execution logs have been reviewed.
13.6 Performance Recommendations
For production environments:
- Keep promotion logic lightweight; avoid long-running operations.
- Use efficient collection lookups and the SDK's helper methods.
- Log only meaningful business events.
- Test promotions with large carts and multiple active promotions.
By following these best practices, developers can build reliable, high-performance promotion extensions that are easy to maintain and troubleshoot. Consistent use of the Promotions SDK, proper logging, and thorough testing ensures custom promotions integrate seamlessly with the Znode Extension Framework.