Per-Product Gating
Some publishers license per organisational unit — Adobe traditionally licenses by department, some Oracle products by location. The default engine treats department as a soft preference (affinity), not a hard rule (requirement). This page explains the gating pattern for making a dimension into a requirement for specific products only.
The Problem
The default rule:
Affinity Consumption.DepartmentID = License.DepartmentID, 3000
is global. It applies to every product. A naive change to:
Requirement Consumption.DepartmentID = License.DepartmentID
would also apply to every product, breaking allocations for every license that does not have a department set. That is too blunt.
You need: "for products marked as licensed-by-department, enforce the requirement; for everything else, behave normally."
The Pattern
The pattern uses calculated fields with a sentinel value. When the product is flagged, both sides resolve to the real DepartmentID and the requirement enforces. When the product is not flagged, both sides collapse to -1 and the requirement is trivially satisfied (because -1 = -1 always).
Step 1: Add a Flag to the Catalog
Add a field on Software Catalog entries called LicenseByDepartment (integer, defaults to 0). Set it to 1 for products that must be department-bound.
This is a custom spec field — see the Configuration Guide: Custom Spec Fields for how to add it. Add it to the form so administrators can tick it on individual catalog entries.
Step 2: Surface the Flag in the Data Sources
Both feeder queries need to surface the flag, aliased exactly as LicenseByDepartment on both sides:
Calculate Software License Entitlements— join from license to its product (viaAsset.SoftwareCatalogEntry) and selectLicenseByDepartmentCalculate Software License Consumptions— join fromst.ProductIDto the product and selectLicenseByDepartment
Use an outer join or ISNULL(..., 0) so the flag is always present on every row. If a license or consumption is missing the flag entirely, the engine throws "Missing required field in affinity rules."
Step 3: Add the Gate Rules
In the Configure dialog, add three rules:
Set Consumption.DeptGate = IIF(Consumption.LicenseByDepartment = 1, Consumption.DepartmentID, -1)
Set License.DeptGate = IIF(License.LicenseByDepartment = 1, License.DepartmentID, -1)
Requirement Consumption.DeptGate = License.DeptGate
The first two are calculated fields; the third is a requirement.
How It Works
For a flagged product:
Consumption.DeptGate= the consumption'sDepartmentIDLicense.DeptGate= the license'sDepartmentID- Requirement: must be equal → license excluded if departments differ
For an unflagged product:
Consumption.DeptGate=-1License.DeptGate=-1- Requirement:
-1 = -1→ always satisfied → no-op
The default Affinity Consumption.DepartmentID = License.DepartmentID, 3000 keeps running for unflagged products as before — the gate is in addition to the affinity, not a replacement.
Generalising to Other Dimensions
The same pattern applies to location, cost centre, or any other dimension. The naming convention should be consistent: LicenseByDepartment / DeptGate, LicenseByLocation / LocGate, LicenseByCostCentre / CCGate.
For a more elaborate version that supports both Require and Prefer modes selectable per product, see the "configurable section-by" pattern in BusClassNET04/sam-affinity-rules.md (developer-facing skills file). It uses two integer columns on the catalog entry — SectionBy (which dimension) and SectionMode (require vs prefer) — and 12 rule lines that cover the full matrix.
Important Constraints
| Constraint | Detail |
|---|---|
| Use integer flags | Strings (e.g., IIF(Manufacturer = "Adobe", ...)) misbehave silently because the expression evaluator does not quote string literals reliably. Use IIF(LicenseByDepartment = 1, ...). |
| Sentinel choice matters | Use -1 (or another value that cannot legitimately occur in the column), not 0 — some records genuinely have DepartmentID = 0. |
| Column must exist on every row | The data source queries must always return the flag. Use outer joins or ISNULL — a missing column throws. |
Comparator is = only |
The gate pattern works with equality. Hierarchy gates using within are technically possible but the within comparator is wired only for the three real hierarchy fields (DepartmentID, LocationID, CostCentreID) and does not work on synthetic gate fields like DeptGate. See Hierarchy Fields. |
Hierarchy Limitation
Because the gate field is synthetic, you cannot use within on it. A license in "IT Division" will not cover consumptions in "IT Support Dept" under the gate pattern — only exact department matches work.
For most per-product gating policies, this is what you want anyway (the whole point is to prevent cross-tree bleed). If you genuinely need hierarchy-aware gating, the workaround is to expand the source data: emit one row per ancestor at query time so equality matching can pick up the parent. This blows up cardinality but needs no engine change.
Testing the Gate
After saving the rules:
- Pick a flagged product (e.g., set
LicenseByDepartment = 1on Adobe Creative Cloud). - Confirm at least one license and one consumption exist with mismatched departments.
- Recalculate from the current period.
- Open the Licensing Position for that product. The mismatched consumptions should now be in deficit (no allocation), even if licenses with capacity exist in other departments.
- Pick an unflagged product and confirm allocation behaves normally.
If the flagged product allocates anyway, the most likely cause is the data source not surfacing LicenseByDepartment correctly. Check both feeder queries return the column.
Why Not Just Add a Per-Product Requirement Syntax
The engine processes one product at a time, but the rules block is global to the calculation — not parameterized per product. There is no Requirement ... WHERE Product = 'Adobe' form. The gating pattern is the supported way to express per-product policies in a global rule block.
This is a deliberate design choice: it keeps the rule evaluator simple and predictable, while pushing the per-product variability into the data (the flag column) where it belongs.
Related Reading
- Common Recipes — including a worked Adobe-by-department example
- Hierarchy Fields and the Within Comparator
- Concepts: Affinity vs Requirements
- Configuration Guide: Custom Spec Fields — how to add the catalog flag