Custom Transaction Compilers
Building custom transaction compilation strategies that control obligation calculations.
Last updated: April 6, 2026
Custom Transaction Compilers
Transaction compilers (formally “transaction detail filter strategies”) control which parts of a transaction are included in commission calculations. When a sale occurs, the transaction contains multiple detail lines. These include product line items, taxes, shipping charges, discounts, and fees. Transaction compilers determine which of these detail types flow into the reward calculation for each program or distribution. For a user-level overview of how these filters are configured through the admin UI, see Transaction Filtering.
The Problem They Solve
Consider a sale with these transaction details:
| Detail Type | Amount |
|---|---|
| Product A (line item) | $50.00 |
| Product B (line item) | $30.00 |
| Shipping | $8.00 |
| Tax | $6.40 |
| Discount | -$10.00 |
Should the collaborator earn commission on the $80.00 product total? On the $84.40 after-tax total? On the $74.40 after discount? Transaction compilers answer this question by filtering which TransactionDetail records are included in the reward calculation.
What interface do transaction compilers implement?
namespace Siren\Incentives\Core\Interfaces;
use Siren\Conversions\Core\Models\Conversion;
use Siren\Transactions\Core\Models\TransactionDetail;
interface TransactionDetailFilterStrategy
{
/**
* Returns true if the provided transaction detail should be included
* for the given program and conversion.
*/
public function shouldIncludeTransactionDetailForProgram(
TransactionDetail $detail,
int $programId,
Conversion $conversion
): bool;
/**
* Returns true if the provided transaction detail should be included
* for the given distribution.
*/
public function shouldIncludeTransactionDetailForDistributor(
TransactionDetail $detail,
int $distributionId
): bool;
/**
* The unique identifier for this filter strategy.
*/
public static function getId(): string;
/**
* Human-readable name for the admin UI.
*/
public function getName(): string;
/**
* Brief description of what this filter includes.
*/
public function getDescription(): string;
}
Each filter strategy answers two questions via separate methods. One is for program-based rewards (instant commissions) and the other is for distribution-based rewards (scheduled payouts). The program method receives additional context via the Conversion object.
The Compiler Models
Transaction compilers are linked to programs and distributions through junction models:
ProgramTransactionCompiler
namespace Siren\Programs\Core\Models;
class ProgramTransactionCompiler implements DataModel, HasSingleIntIdentity
{
public function getProgramId(): int;
public function getTransactionCompiler(): string; // The filter strategy ID
}
DistributionTransactionCompiler
namespace Siren\Distributions\Core\Models;
class DistributionTransactionCompiler implements DataModel, HasSingleIntIdentity
{
public function getDistributionId(): int;
public function getTransactionCompiler(): string; // The filter strategy ID
}
A program or distribution can have multiple compilers bound to it. Each compiler references a filter strategy by its string ID (e.g., 'includeLineItems', 'includeTaxes').
How does the compilation process work?
The TransactionCompilerService orchestrates the filtering. It initializes the registry on first access, then applies compilers to transaction details:
namespace Siren\Distributions\Core\Services;
class TransactionCompilerService
{
/**
* Filters transaction details for a program.
*
* @param TransactionDetail[] $inputTransactionDetails
* @param ProgramTransactionCompiler[]|DistributionTransactionCompiler[] $compilers
* @param int $programId
* @param Conversion $conversion
* @return TransactionDetail[]
*/
public function compileTransactionDetailsForProgram(
array $inputTransactionDetails,
array $compilers,
int $programId,
Conversion $conversion
): array;
/**
* Filters transaction details for a distribution.
*/
public function compileTransactionDetailsForDistribution(
array $inputTransactionDetails,
array $compilers,
int $distributionId
): array;
}
The compilation process iterates through each bound compiler model, resolves the corresponding filter strategy from the registry, and tests each transaction detail against it. Details that pass any compiler’s filter are included; details consumed by one compiler are removed from the pool so they are not double-counted.
What filters ship out of the box?
Siren ships with five built-in filters, one for each transaction detail type. includeLineItems passes product line items through to the calculation and is the most complex of the five. It can further filter by product categories, SKUs, line item type, and whether the product must be “owned” by the collaborator. includeTaxes passes tax charges, includeShipping passes shipping charges, includeDiscounts passes discount line items, and includeFees passes fee line items. Each filter is a simple type check against the transaction detail’s getType() value.
Registration Pattern
Filter strategies are registered by listening for TransactionFilterRegistryInitiated:
namespace Siren\Incentives\Core\Handlers;
use PHPNomad\Events\Interfaces\CanHandle;
use PHPNomad\Events\Interfaces\Event;
use Siren\Incentives\Core\Events\TransactionFilterRegistryInitiated;
class RegisterCoreTransactionDetailFilters implements CanHandle
{
public function handle(Event $event): void
{
$event->addStrategy(IncludeDiscounts::class);
$event->addStrategy(IncludeFees::class);
$event->addStrategy(IncludeLineItems::class);
$event->addStrategy(IncludeShipping::class);
$event->addStrategy(IncludeTaxes::class);
}
}
The event provides addStrategy() and deleteStrategy():
public function addStrategy(string $strategyClass): void
{
$this->registry->set(
$strategyClass::getId(),
fn() => $this->provider->get($strategyClass)
);
}
Creating a Custom Transaction Compiler
Example: Include Only Digital Products
This filter includes transaction details only for digital/downloadable products, identified by a 'digital' attribute:
namespace MyPlugin\TransactionFilters;
use Siren\Conversions\Core\Models\Conversion;
use Siren\Incentives\Core\Interfaces\TransactionDetailFilterStrategy;
use Siren\Transactions\Core\Datastores\TransactionDetailAttribute\Interfaces\TransactionDetailAttributeDatastore;
use Siren\Transactions\Core\Models\TransactionDetail;
class IncludeDigitalProducts implements TransactionDetailFilterStrategy
{
public function __construct(
protected TransactionDetailAttributeDatastore $attributes
) {}
public function shouldIncludeTransactionDetailForProgram(
TransactionDetail $detail,
int $programId,
Conversion $conversion
): bool {
return $this->isDigital($detail);
}
public function shouldIncludeTransactionDetailForDistributor(
TransactionDetail $detail,
int $distributionId
): bool {
return $this->isDigital($detail);
}
private function isDigital(TransactionDetail $detail): bool
{
if ($detail->getType() !== 'line_item') {
return false;
}
try {
$isDigital = $this->attributes->getAttributeValue(
$detail->getId(),
'digital',
'false'
);
return $isDigital === 'true';
} catch (\Exception $e) {
return false;
}
}
public static function getId(): string
{
return 'includeDigitalProducts';
}
public function getName(): string
{
return 'Digital Products Only';
}
public function getDescription(): string
{
return 'Include only digital/downloadable product line items in commission calculations.';
}
}
Register It
namespace MyPlugin\Listeners;
use PHPNomad\Events\Interfaces\CanHandle;
use PHPNomad\Events\Interfaces\Event;
use Siren\Incentives\Core\Events\TransactionFilterRegistryInitiated;
use MyPlugin\TransactionFilters\IncludeDigitalProducts;
class RegisterDigitalProductFilter implements CanHandle
{
public function handle(Event $event): void
{
if ($event instanceof TransactionFilterRegistryInitiated) {
$event->addStrategy(IncludeDigitalProducts::class);
}
}
}
Wire the listener in your Initializer:
public function getListeners(): array
{
return [
TransactionFilterRegistryInitiated::class => RegisterDigitalProductFilter::class,
];
}
How Compilers Are Applied
When a conversion is processed:
- The system loads the
ProgramTransactionCompilerrecords bound to the program - The
TransactionCompilerServiceiterates through each compiler - For each compiler, it resolves the
TransactionDetailFilterStrategyfrom the registry - Each transaction detail is tested against the filter’s
shouldIncludeTransactionDetailForProgram()method - Details that pass are added to the accumulator and removed from the input pool
- The accumulated details are used to calculate the reward amount
This means a program configured with includeLineItems and includeTaxes will base commissions on product prices plus tax, but exclude shipping and discounts. The order of compiler evaluation does not affect the result. Each detail can only be included once.