Skip to main content

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 GroupFieldsType
String paramsparameter1 through parameter9String (max 255 chars)
Date paramsdateParam1 through dateParam5Date
Numeric paramsdecimalParam1 through decimalParam5BigDecimal

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:

FieldPurpose
filterParam1Match against EDR.parameter1
filterParam2Match against EDR.parameter2
filterParam3Match against EDR.parameter3
filterParam4Match against EDR.parameter4
filterExpressionEL expression for complex logic
priorityMatching 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)

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)