Multi-Instance Product Matching
Critical Scenario
When a subscription has multiple instances of the same Product (e.g., 3 SIM cards), how does an incoming EDR get routed to the correct instance?
Example:
- Subscription has 3 SIM cards (same Product)
- Each SIM has different attributes (SIM_ID: SIM_001, SIM_002, SIM_003)
- EDR arrives with parameter1="SIM_002"
- System must route to correct ServiceInstance
EDR Parameters
EDR (Event Detail Record) contains matching parameters:
| Parameter Group | Fields | Type |
|---|---|---|
| String params | parameter1 through parameter9 | String (max 255 chars) |
| Date params | dateParam1 through dateParam5 | Date |
| Numeric params | decimalParam1 through decimalParam5 | BigDecimal |
The incoming usage event (EDR) carries these parameters, which the matching engine reads to route the event to the right charge.
UsageChargeTemplate Filter Parameters
Each UsageChargeTemplate has filter parameters for EDR matching:
| Field | Purpose |
|---|---|
filterParam1 | Match against EDR.parameter1 |
filterParam2 | Match against EDR.parameter2 |
filterParam3 | Match against EDR.parameter3 |
filterParam4 | Match against EDR.parameter4 |
filterExpression | EL expression for complex logic |
priority | Matching order (lower = matched first) |
Matching Algorithm
Step 1: Database Query
The system queries active usage charge instances:
SELECT uci FROM UsageChargeInstance uci
WHERE uci.subscription.id = :subscriptionId
AND uci.status = 'ACTIVE'
AND (uci.chargeTemplate.filterParam1 IS NULL
OR uci.chargeTemplate.filterParam1 = :edrParam1)
AND (uci.chargeTemplate.filterParam2 IS NULL
OR uci.chargeTemplate.filterParam2 = :edrParam2)
AND (uci.chargeTemplate.filterParam3 IS NULL
OR uci.chargeTemplate.filterParam3 = :edrParam3)
AND (uci.chargeTemplate.filterParam4 IS NULL
OR uci.chargeTemplate.filterParam4 = :edrParam4)
ORDER BY uci.chargeTemplate.priority ASC
Step 2: EL Expression Filter
For matched charges, evaluate filterExpression:
private boolean isChargeMatch(UsageChargeInstance ci, EDR edr) {
String expression = ci.getChargeTemplate().getFilterExpression();
if (StringUtils.isBlank(expression)) {
return true; // No filter = match
}
Map<Object, Object> context = new HashMap<>();
context.put("ci", ci); // ChargeInstance (access to serviceInstance, attributes)
context.put("edr", edr); // Event Detail Record
return ValueExpressionWrapper.evaluateToBoolean(expression, context);
}
Key Insight: The filterExpression has access to:
ci- ChargeInstance (and via it,ci.serviceInstance.attributeInstances)edr- The incoming EDR with all parameters
Solution Patterns
Option A: Separate UsageChargeTemplates
Create one charge template per instance with static filterParam:
Product: SIM Card
├── UsageChargeTemplate: SIM_USAGE_001 (filterParam1="SIM_001")
├── UsageChargeTemplate: SIM_USAGE_002 (filterParam1="SIM_002")
└── UsageChargeTemplate: SIM_USAGE_003 (filterParam1="SIM_003")
Flow:
EDR arrives with parameter1="SIM_002"
↓
Database query matches SIM_USAGE_002 (filterParam1="SIM_002")
↓
Route to correct UsageChargeInstance
Pros: Simple, clear Cons: Requires creating charge template per instance (not scalable)
Option B: Single Template with EL Expression (Recommended)
One charge template with dynamic filterExpression:
UsageChargeTemplate: SIM_USAGE
filterParam1: null (no static filter)
filterParam2: null
filterParam3: null
filterParam4: null
filterExpression: #{ci.serviceInstance.getCfValue('sim_number') == edr.parameter1}
ServiceInstance Attributes:
ServiceInstance 1: CF sim_number="SIM_001"
ServiceInstance 2: CF sim_number="SIM_002"
ServiceInstance 3: CF sim_number="SIM_003"
Flow:
EDR arrives with parameter1="SIM_002"
↓
Database returns ALL UsageChargeInstances (no filterParam set)
↓
For each, evaluate filterExpression:
- ci.serviceInstance.getCfValue('sim_number') == "SIM_002"
- ServiceInstance 1: "SIM_001" != "SIM_002" → false
- ServiceInstance 2: "SIM_002" == "SIM_002" → TRUE (match!)
- ServiceInstance 3: "SIM_003" != "SIM_002" → false
↓
Route to ServiceInstance 2's UsageChargeInstance
Pros: Single charge template, dynamic routing, scalable Cons: All instances evaluated (performance for many instances)
Option C: Hybrid Approach
Combine filterParam for coarse filtering and filterExpression for fine matching:
UsageChargeTemplate: SIM_USAGE
filterParam1: "SIM_" (prefix match)
filterExpression: #{ci.serviceInstance.getCfValue('sim_number') == edr.parameter1}
Billing Charges Separately
invoicingCalendar Override
Each ChargeInstance can have its own billing timing:
// ChargeInstance.java
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "invoicing_calendar_id")
protected Calendar invoicingCalendar;
Configuration Example
Subscription (default BillingCycle: Monthly-1st)
├── ServiceInstance 1 (SIM_001)
│ └── UsageChargeInstance
│ └── invoicingCalendar: Monthly-15th ← OVERRIDE
├── ServiceInstance 2 (SIM_002)
│ └── UsageChargeInstance
│ └── invoicingCalendar: null (uses subscription default)
└── ServiceInstance 3 (SIM_003)
└── UsageChargeInstance
└── invoicingCalendar: Weekly ← DIFFERENT CYCLE
Result: SIM_001 billed on 15th, SIM_002 on 1st, SIM_003 weekly - all on same subscription!
Billing Cycle Override Hierarchy
BillingAccount.billingCycle (default)
↓ can be overridden by
Subscription.billingCycle (subscription-level override)
↓ can be overridden by
ChargeInstance.invoicingCalendar (charge-level override)