Skip to main content

Calendar Matrix — Rating, Billing, Invoicing + Standard Calendar Definitions

The 3 Calendar Concepts (Renamed for Clarity)

Per Charge Type

RECURRING Charge

V1 API FieldProposed NameEntity Field (current)PurposeMandatoryNote
chargeApplicationCalendarCharge Application CalendarRecurringChargeTemplate.calendarHOW OFTEN the charge fires. Defines period boundaries (startDate/endDate on WO).YesExisting field, renamed from billingCalendar
ratingPeriodCalendarN/A for recurringNoNot applicable — recurring charges don't accumulate EDRs
invoicingCalendarInvoicing CalendarChargeInstance.invoicingCalendar (runtime)WHEN the WO becomes eligible for a bill run. Sets WO.invoicingDate.Yes (new!)Currently optional, inherited from ServiceInstance. Flag: "Same as chargeApplicationCalendar" = default. Uncheck to set different calendar.

Sidebar: RecurringChargeTemplate.calendar is the existing JPA field. The V1 API currently exposes it as billingCalendar. Proposal: rename API field to chargeApplicationCalendar.

USAGE Charge

V1 API FieldProposed NameEntity Field (current)PurposeMandatoryNote
chargeApplicationCalendarN/A for usageNoUsage charges fire per EDR, not per calendar period
ratingPeriodCalendarRating Period CalendarCounterTemplate.calendar (linked via product)WHEN usage counters reset. Defines accumulation window for tiered/volume/package pricing.Required if TIERED/VOLUME/PACKAGENot on ChargeTemplate directly — lives on CounterTemplate, linked via ProductChargeTemplateMapping → CounterTemplate
invoicingCalendarInvoicing CalendarChargeInstance.invoicingCalendar (runtime)WHEN the WO becomes eligible for a bill run.Yes (new!)Flag: "Same as ratingPeriodCalendar" = default. Uncheck to set different calendar. If no ratingPeriodCalendar, must be set explicitly.

Sidebar: CounterTemplate.calendar is the actual entity field for rating period. It's NOT on UsageChargeTemplate directly — it's on the counter that's linked to the charge via the product. The V1 API currently accepts ratingCalendar.calendar in the charge payload, but internally it maps to creating/updating a CounterTemplate.

ONE-SHOT Charge

V1 API FieldProposed NameEntity FieldPurposeMandatoryNote
chargeApplicationCalendarN/AFires once on event
ratingPeriodCalendarN/ANo accumulation
invoicingCalendarInvoicing CalendarChargeInstance.invoicingCalendarOptional gate. If set, delays billing eligibility.NoUsually null → immediately eligible. Use case: "bill penalty only at next billing cycle"

Summary Matrix

                    chargeApplication    ratingPeriod       invoicing
Calendar Calendar Calendar
───────────────── ──────────────── ─────────────────
RECURRING ✅ MANDATORY ❌ N/A ✅ MANDATORY (new)
When charge fires No accumulation Flag: = chargeApp
Entity: ChargeTemplate.calendar Entity: ChargeInstance

USAGE ❌ N/A ✅ IF tiered/vol ✅ MANDATORY (new)
Fires per EDR Counter reset Flag: = ratingPeriod
Entity: CounterTemplate.calendar

ONE-SHOT ❌ N/A ❌ N/A ⚪ OPTIONAL
Fires once No accumulation Gate for deferred billing

Per Charge Type Matrix

RECURRING Charges

CalendarUsed?What it doesExample
billingCalendarYES (mandatory)Defines charge period. Calendar boundaries → WO.startDate/endDate. Determines when charge fires.MONTHLY → charge fires Jan 1, Feb 1, Mar 1...
ratingCalendarNONot applicable — recurring charges don't accumulate EDRs
invoicingCalendarOptionalIf set: WO.invoicingDate = calendar.nextDate(operationDate). If null: invoicingDate = null → immediately eligible for billingMONTHLY → WO created Jan 1 gets invoicingDate = Feb 1, meaning it won't appear on January invoice but will on February's

Impact on rating:

RecurringRatingJob runs with rateUntilDate = Feb 1

ChargeInstance:
billingCalendar = MONTHLY (1st of month)
chargeDate = Jan 1
nextChargeDate = Feb 1 (from calendar.nextCalendarDate)

Since nextChargeDate <= rateUntilDate:
→ Create WO:
operationDate = Jan 1 (applyInAdvance=true)
startDate = Jan 1 (calendar.previousCalendarDate)
endDate = Feb 1 (calendar.nextCalendarDate)
invoicingDate = null (no invoicingCalendar)
amount = 150 MAD

Impact on billing:

Bill run with lastTransactionDate = Feb 1:
SELECT RT WHERE status='OPEN'
AND usageDate >= Jan 1 AND usageDate < Feb 1
AND (invoicingDate IS NULL OR invoicingDate < Feb 1)
→ RT with usageDate=Jan 1, invoicingDate=null → SELECTED ✓

USAGE Charges

CalendarUsed?What it doesExample
billingCalendarOptionalIf set on charge: defines the invoicing period for usage. Currently mapped to invoicingCalendar on ChargeInstance at runtime.MONTHLY → usage accumulated during January is invoiced in February
ratingCalendarYES (key for tiered/volume)Defines counter reset window. When period boundary is crossed, accumulated quantity resets to 0.WEEKLY → data tier counters reset every Monday. "5 GB free per week"
invoicingCalendarOptionalSame as recurring: sets WO.invoicingDate as billing gateSee above

Impact on rating:

UsageRatingJob processes EDR:
EDR: eventDate=Jan 15, quantity=3000 MB, parameter1=DATA

1. Find matching UsageChargeInstance (filterParam1=DATA)

2. Check CounterPeriod (ratingCalendar = WEEKLY):
Current period: Jan 13 → Jan 20
Accumulated so far: 4000 MB

3. Accumulate: 4000 + 3000 = 7000 MB

4. Apply tiered pricing:
Tier 1: 0-5120 MB → free (1120 MB remaining in tier)
Tier 2: 5120+ MB → 0.10 MAD/MB (1880 MB in this tier)
amount = 1880 * 0.10 = 188 MAD

5. Create WO:
operationDate = Jan 15 (EDR.eventDate)
startDate = NULL (usage has no period)
endDate = NULL
invoicingDate = null (no invoicingCalendar)
amount = 188 MAD

6. Jan 20 (WEEKLY boundary): counter RESETS to 0
Next EDR starts fresh in Tier 1

Impact on billing:

Bill run with lastTransactionDate = Feb 1:
SELECT RT WHERE usageDate >= Jan 1 AND usageDate < Feb 1
→ All January usage RTs selected

ONE-SHOT Charges

CalendarUsed?What it doesExample
billingCalendarNONot applicable — one-shot fires once
ratingCalendarNONot applicable — no accumulation
invoicingCalendarOptionalSame gate mechanismUsually null → immediately eligible

Your 3-Charge Scenario

Subscription starts: Jan 5

CHARGE 1: Recurring monthly at anniversary
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
billingCalendar = CalendarPeriod(MONTH, initDate=Jan 5) ← JOIN/anniversary
ratingCalendar = n/a
invoicingCalendar = null (immediately eligible)

Timeline:
Jan 5 Feb 5 Mar 5
├────────────┤────────────┤
│ Period 1 │ Period 2 │
│ WO: 150MAD │ WO: 150MAD│
│ inv: null │ inv: null │

Any bill run after Jan 5 can pick up Period 1

CHARGE 2: Usage formula, rated per EDR, billed daily
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
billingCalendar = n/a (no charge period for usage)
ratingCalendar = null (formula pricing, no accumulation needed)
invoicingCalendar = CalendarFixed(DAILY) ← "eligible next day"

Timeline:
Jan 15 EDR → WO rated immediately (formula) → invoicingDate = Jan 16
Jan 15 EDR → WO rated immediately → invoicingDate = Jan 16
Jan 16 EDR → WO rated immediately → invoicingDate = Jan 17

Daily bill run on Jan 16:
Selects all RTs where invoicingDate < Jan 16
→ Jan 15 WOs (invoicingDate=Jan 16 is NOT < Jan 16) ← EXCLUDED!

Hmm, this depends on the bill run's invoiceUpToDate calculation.
Alternative: invoicingCalendar = null, bill run daily with
lastTransactionDate = yesterday.

CHARGE 3: Usage rated weekly (tiered), billed end of month
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
billingCalendar = n/a
ratingCalendar = CalendarFixed(WEEKLY) ← counters reset weekly
invoicingCalendar = CalendarFixed(MONTHLY, 1st of month) ← eligible at month end

Timeline:
Week 1 (Jan 1-7): 6GB → 1GB overage = 100 MAD
WO.invoicingDate = Feb 1
── counter resets ──
Week 2 (Jan 8-14): 3GB → no overage = 0 MAD
── counter resets ──
Week 3 (Jan 15-21): 7GB → 2GB overage = 200 MAD
WO.invoicingDate = Feb 1
── counter resets ──
Week 4 (Jan 22-28): 4GB → no overage = 0 MAD

Monthly bill run on Feb 1 (lastTransactionDate = Feb 1):
Selects all RTs where invoicingDate < Feb 1
→ Jan WOs with invoicingDate = Feb 1... NOT < Feb 1 → EXCLUDED again!

This shows the invoicingDate gate is <= or < sensitive.
Need to check: is it < or <=?

Current Gap: invoicingCalendar Not in V1 API

Today's state:

  • billingCalendar → V1 API ✅ (for recurring)
  • ratingCalendar → V1 API ✅ (for usage, accumulator config)
  • invoicingCalendarNOT in V1 API

To support the 3-charge scenario, we need to add:

  • invoicingCalendar field to ChargeV1Dto
  • Map it in ChargeV1Mapper (read from ChargeInstance.invoicingCalendar)
  • Persist it: either on ChargeTemplate (catalog level, preferred) or keep on ServiceInstance
  • Expose in UI ChargeModal

Key Insight: billingCalendar vs invoicingCalendar for Usage

For USAGE charges, the current V1 API field billingCalendar is ambiguous:

  • In RecurringChargeTemplate, calendar = charge period (when it fires)
  • For USAGE, there's no "charge period" — EDRs fire when they arrive
  • So what does billingCalendar mean for usage? → It maps to invoicingCalendar at runtime

Proposal for V1 API clarity:

V1 API fieldRECURRING meaningUSAGE meaning
billingCalendarCharge period (when it fires)invoicingCalendar (when eligible for billing)
ratingCalendar.calendarn/aCounter reset calendar
invoicingCalendar (NEW)When WO eligible for billingWhen WO eligible for billing

For recurring charges, if invoicingCalendar is null, the WO is immediately eligible. The billingCalendar only defines the charge period, NOT the invoicing gate.


Standard Calendar Definitions (OOTB)

Calendar Types in the Engine

TypeJava ClassBehaviorInit date used?
YEARLYCalendarYearlyFixed monthly boundaries (1st of each month)NO — boundaries are absolute
PERIODCalendarPeriodRepeating period (N days/months/hours) from init dateYES — period starts from init date (anniversary)
JOINCalendarJoinCombines two calendars (UNION, INTERSECT, APPEND)YES — propagated to children
BANKINGCalendarBankingBanking holidays calendarNO — fixed dates
FIXEDCalendarFixedExplicit date pairs (ad-hoc periods)NO — explicit boundaries

Period Units (CalendarPeriodUnitEnum)

UnitJava constantExample
MONTHCalendar.MONTH1 month, 3 months (quarterly), 6 months
DAY_OF_MONTHCalendar.DAY_OF_MONTH1 day, 7 days (weekly), 14 days
HOUR_OF_DAYCalendar.HOUR_OF_DAY1 hour, 24 hours
MINUTECalendar.MINUTE15 minutes, 60 minutes
SECONDCalendar.SECONDFor real-time billing

Proposed Standard Calendar Codes (Naming Convention)

Convention: CAL_{FREQUENCY}_{BOUNDARY}

CodeTypePeriodBoundaryUse Case
CAL_MONTHLYYEARLY1 month1st of each monthStandard billing cycle, counter reset
CAL_MONTHLY_ANNIVPERIOD(MONTH,1)1 monthAnniversary of init dateAnniversary billing (sub on 15th → 15th each month)
CAL_QUARTERLYYEARLY3 monthsJan/Apr/Jul/Oct 1stQuarterly billing
CAL_QUARTERLY_ANNIVPERIOD(MONTH,3)3 monthsEvery 3 months from initAnniversary quarterly
CAL_SEMI_ANNUALYEARLY6 monthsJan/Jul 1stSemi-annual billing
CAL_ANNUALYEARLY12 monthsJan 1stAnnual billing
CAL_ANNUAL_ANNIVPERIOD(MONTH,12)12 monthsAnniversary of initAnnual anniversary
CAL_WEEKLYPERIOD(DAY,7)7 daysEvery 7 days from initWeekly counter reset
CAL_WEEKLY_MONYEARLY7 daysEvery MondayFixed weekly (Mon-Sun)
CAL_DAILYPERIOD(DAY,1)1 dayEvery day from initDaily billing/reset
CAL_DAILY_MIDNIGHTYEARLY1 dayEvery midnightFixed daily (00:00-23:59)
CAL_HOURLYPERIOD(HOUR,1)1 hourEvery hourReal-time usage rating

Key Difference: YEARLY vs PERIOD for "Monthly"

CAL_MONTHLY (YEARLY type):
Always 1st of month → 1st of next month
Sub on Jan 15 → period is still Jan 1 → Feb 1
No relationship to subscription date
✅ Use for: standard billing cycles, invoicing gates

CAL_MONTHLY_ANNIV (PERIOD type, unit=MONTH, length=1):
Anchored to init date (subscription date)
Sub on Jan 15 → period is Jan 15 → Feb 15
⚠️ Edge case: Jan 31 → Feb 28 → Mar 28 (drifts!)
✅ Use for: anniversary billing, commitment periods

Which Calendar for Each Purpose?

PurposeRecommended CalendarWhy
Recurring billing (standard)CAL_MONTHLYPredictable boundaries, no drift
Recurring billing (anniversary)CAL_MONTHLY_ANNIVTied to customer's start date
Usage counter reset (monthly)CAL_MONTHLYReset on 1st, predictable for customers
Usage counter reset (weekly)CAL_WEEKLY_MONReset every Monday, clear boundaries
Invoicing gate (end of month)CAL_MONTHLYWO eligible on 1st of next month
Invoicing gate (daily)CAL_DAILY_MIDNIGHTWO eligible next day
Commitment period (12 months)CAL_ANNUAL_ANNIVExactly 12 months from subscription

Your 3-Charge Scenario with New Naming + Standard Calendars

Subscription start: Jan 5

CHARGE 1: Recurring monthly at anniversary
chargeApplicationCalendar = CAL_MONTHLY_PERIODIC ← fires Jan 5, Feb 5, Mar 5
ratingPeriodCalendar = n/a ← not applicable for recurring
invoicingCalendar = ☑ Same as chargeApplicationCalendar
= CAL_MONTHLY_PERIODIC ← eligible at period end
startDateStrategy = PRODUCT_ACTIVATION_DATE
→ WO created Jan 5, invoicingDate = Feb 5
→ Bill run after Feb 5 picks it up

CHARGE 2: Usage formula, rated per EDR, billed daily
chargeApplicationCalendar = n/a ← fires per EDR
ratingPeriodCalendar = null ← no accumulation for formula
invoicingCalendar = CAL_DAILY ← eligible next midnight
startDateStrategy = n/a (no periodic cal)
→ EDR Jan 15 14:00 → WO rated → invoicingDate = Jan 16 00:00
→ Daily bill run on Jan 16 picks it up

CHARGE 3: Usage tiered, counters reset weekly, billed monthly
chargeApplicationCalendar = n/a ← fires per EDR
ratingPeriodCalendar = CAL_1ST_WEEK_MON ← counters reset every Monday
invoicingCalendar = ☑ Same as ratingPeriodCalendar? NO → override:
= CAL_1ST_MONTH ← eligible 1st of next month
startDateStrategy = CALENDAR_START (fixed Monday boundary, no init needed)
→ Week 1: 6GB → 1GB overage = 100 MAD, invoicingDate = Feb 1
→ Week 2 counter resets, 3GB → free
→ Week 3: 7GB → 2GB overage = 200 MAD, invoicingDate = Feb 1
→ Monthly bill run on Feb 1 picks up all January WOs

Why ratingPeriodCalendar is NOT on ChargeTemplate (and why API should abstract this)

Current entity model:

UsageChargeTemplate ──(no FK to CounterTemplate)──╮

ProductChargeTemplateMapping ──────────────────────┤
├── chargeTemplate (FK to ChargeTemplate) │
├── counterTemplate (FK to CounterTemplate) ◄────╯ ← HERE
└── accumulatorCounterTemplates (M2M)

CounterTemplate holds: calendar, accumulatorType, keyEl, valueEl, filterEl, ceiling

Why this matters for API design:

  • From the API user's perspective, they configure accumulation ON the charge
  • Internally, a CounterTemplate is auto-created with code ACC_{type}_{trackedValue}
  • The link goes through ProductChargeTemplateMapping.counterTemplate (not on charge)
  • The V1 API already abstracts this: ratingCalendar.accumulator.* ← user doesn't see CounterTemplate

Impact on schema: No entity changes needed for the API. The V1 API should continue to accept ratingPeriodCalendar + accumulator on the charge payload, and internally create/link the CounterTemplate. This is the current behavior (with TODO for incomplete field mapping).

Impact on UI: The ChargeModal already has the accumulator fields. They just need to be properly wired to save via V1 API.

Why CounterTemplate is on ProductChargeTemplateMapping and NOT on ChargeTemplate?

Historical reason: In the old ServiceTemplate model, a charge could be shared across multiple services with DIFFERENT counters. The counter was per "usage in a service", not per "charge template". This is a legacy design decision.

Problem this creates: If you define a charge WITHOUT a parent product (standalone charge), you can't attach a counter/ratingPeriodCalendar.

Proposed fix: Add ratingPeriodCalendar (calendar code) and accumulator fields directly on UsageChargeTemplate entity. This makes the charge self-contained. The ProductChargeTemplateMapping.counterTemplate becomes a runtime override (optional, for product-specific counter configuration).

Entity change:

// UsageChargeTemplate.java — NEW fields
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "rating_calendar_id")
private Calendar ratingPeriodCalendar;

@Column(name = "accumulator_type")
@Enumerated(EnumType.STRING)
private AccumulatorCounterTypeEnum accumulatorType;

@Column(name = "accumulator_tracked_value")
private String accumulatorTrackedValue; // QUANTITY, AMOUNT, AMOUNT_WITH_TAX

@Column(name = "accumulator_ceiling")
private BigDecimal accumulatorCeiling;

@Column(name = "accumulator_key_el")
private String accumulatorKeyEl;

@Column(name = "accumulator_value_el")
private String accumulatorValueEl;

@Column(name = "accumulator_filter_el")
private String accumulatorFilterEl;

At runtime, if ProductChargeTemplateMapping.counterTemplate is set, it overrides these defaults.


V1 API Payloads (Target State)

RECURRING Charge — Create/Update

POST /api/v1/recurringCharges/CHG_NFLX_STD
{
"code": "CHG_NFLX_STD",
"description": "Netflix Standard - Mensuel",
"invoiceSubCategory": "ISUB_STREAM_BASE",
"taxClass": "TAX_MA_20",

"chargeApplicationCalendar": "CAL_MONTHLY_PERIODIC",
"startDateStrategy": "PRODUCT_ACTIVATION_DATE",

"invoicingCalendar": "CAL_MONTHLY_PERIODIC",
"invoicingCalendarSameAsChargeApplication": true,

"applyInAdvance": true,
"subscriptionProrata": true,
"terminationProrata": true,

"articleSimilarToCharge": true,
"amountEditable": true
}

USAGE Charge — Create/Update

POST /api/v1/usageCharges/CHG_INWI_DATA
{
"code": "CHG_INWI_DATA",
"description": "Data usage - overage after 5GB",
"invoiceSubCategory": "ISUB_TEL_DATA",
"taxClass": "TAX_MA_20",
"inputUnitOfMeasure": "MB",
"ratingUnitOfMeasure": "MB",

"ratingPeriodCalendar": "CAL_1ST_MONTH",
"startDateStrategy": "CALENDAR_START",
"accumulator": {
"type": "SINGLE_VALUE",
"trackedValue": "QUANTITY",
"initialValue": null,
"dimensions": null
},

"invoicingCalendar": "CAL_1ST_MONTH",
"invoicingCalendarSameAsRatingPeriod": true,

"filterParam1": "DATA",
"priority": 1,
"triggerNextCharge": false,

"articleSimilarToCharge": true
}

USAGE Charge — Multi-Value Accumulator Example

POST /api/v1/usageCharges/CHG_API_TOKENS
{
"code": "CHG_API_TOKENS",
"description": "API Token usage per model",

"ratingPeriodCalendar": "CAL_1ST_MONTH",
"accumulator": {
"type": "MULTI_VALUE",
"trackedValue": "QUANTITY",
"initialValue": 1000000,
"dimensions": {
"key_fx": "#{edr.parameter1}",
"value_fx": "#{edr.quantity}",
"filter_fx": "#{edr.parameter2 == 'TOKEN'}"
}
},

"invoicingCalendar": "CAL_1ST_MONTH",
"invoicingCalendarSameAsRatingPeriod": true
}

ONE-SHOT Charge — Create/Update

POST /api/v1/oneShotCharges/CHG_ORG_SETUP
{
"code": "CHG_ORG_SETUP",
"description": "Frais de mise en service",
"invoiceSubCategory": "ISUB_SETUP",
"taxClass": "TAX_MA_20",
"oneShotType": "SUBSCRIPTION",
"immediateInvoicing": true,

"invoicingCalendar": null,

"articleSimilarToCharge": true,
"amountEditable": true
}

UI Fields per Charge Type (Target State)

General Tab (ALL charge types)

FieldAPI NameTypeMandatorySource
Codecodetext (readonly edit)Yes
NamedescriptiontextYes
Invoice Sub-CategoryinvoiceSubCategoryautocompleteYes/api/v1/invoiceSubCategories
Tax ClasstaxClassautocompleteYes/api/v1/taxClasses
Unit of MeasureinputUnitOfMeasureautocompleteNo/api/v1/unitOfMeasures
Auto-create ArticlearticleSimilarToChargecheckboxNoDefault: true

Recurring Settings Tab (RECURRING only)

FieldAPI NameTypeMandatorySource
Charge Application CalendarchargeApplicationCalendarautocompleteYes/api/v1/calendars
Start Date StrategystartDateStrategyselectNoPRODUCT_ACTIVATION_DATE (default), SUBSCRIPTION_START, CALENDAR_START
Invoicing CalendarinvoicingCalendarautocompleteYes/api/v1/calendars
☑ Same as Charge ApplicationinvoicingCalendarSameAsChargeApplicationcheckboxDefault: true. Hides calendar field when checked.
Apply In AdvanceapplyInAdvancetoggleNoDefault: true
Prorate on SubscriptionsubscriptionProratatoggleNoDefault: true
Prorate on TerminationterminationProratatoggleNoDefault: true

Usage Settings Tab (USAGE only)

Section: Rating Period

FieldAPI NameTypeMandatorySource
Rating Period CalendarratingPeriodCalendarautocompleteIf tiered/volume/api/v1/calendars
Start Date StrategystartDateStrategyselectNoCALENDAR_START (default), PRODUCT_ACTIVATION_DATE, SUBSCRIPTION_START, FIRST_EVENT
Accumulator Typeaccumulator.typeselectIf rating cal setSINGLE_VALUE, MULTI_VALUE
Tracked Valueaccumulator.trackedValueselectIf accum type setQUANTITY (default), AMOUNT, AMOUNT_WITH_TAX
Ceilingaccumulator.ceilingnumberNoMax accumulated value

Section: Accumulator Dimensions (if MULTI_VALUE)

FieldAPI NameTypeMandatory
Key Expression (EL)accumulator.dimensions.key_fxformulaIf MULTI_VALUE
Value Expression (EL)accumulator.dimensions.value_fxformulaIf MULTI_VALUE
Filter Expression (EL)accumulator.dimensions.filter_fxformulaNo

Section: Invoicing

FieldAPI NameTypeMandatorySource
Invoicing CalendarinvoicingCalendarautocompleteYes/api/v1/calendars
☑ Same as Rating PeriodinvoicingCalendarSameAsRatingPeriodcheckboxDefault: true. Hides calendar field when checked.

Section: EDR Routing

FieldAPI NameTypeMandatory
Route Parameter 1filterParam1textNo
Route Parameter 2-4filterParam2-4textNo
PriorityprioritynumberNo
Cascade to NexttriggerNextChargetoggleNo

One-Time Settings Tab (ONE-SHOT only)

FieldAPI NameTypeMandatory
Sub-TypeoneShotTypeselectYes (SUBSCRIPTION, TERMINATION, OTHER)
Immediate InvoicingimmediateInvoicingtoggleNo (default: true)
Invoicing CalendarinvoicingCalendarautocompleteNo (optional gate)

Advanced Tab (ALL charge types)

FieldAPI NameTypeMandatory
Allow Price OverrideamountEditabletoggleNo (default: true)
Rounding ModeroundingModeselectNo
Application Filter (EL)filterExpressionformulaNo

Deep Dive: Counters, Accumulators, and Multi-Value

CounterInstance → linked to ServiceInstance (or subscription/account level)

CounterTemplate (catalog)
→ CounterInstance (runtime, per ServiceInstance or per Subscription)
→ CounterPeriod (per calendar period, holds accumulated values)

CounterInstance is NOT linked to ChargeInstance directly. It's linked to one of:

  • ServiceInstance (most common for usage charges)
  • Subscription
  • BillingAccount / CustomerAccount / Customer (shared counters)

SINGLE_VALUE vs MULTI_VALUE Accumulators

SINGLE_VALUE: One cumulative number per period

counterType: USAGE → tracks walletOperation.quantity
counterType: USAGE_AMOUNT → tracks walletOperation.amountWithoutTax

Example: "5 GB included per month"
Period Jan: value starts at 0
EDR 1 (+1000 MB): value = 1000
EDR 2 (+2000 MB): value = 3000
EDR 3 (+3000 MB): value = 6000 → exceeded 5120 MB → overage billed

MULTI_VALUE: Cumulative number per dimension KEY per period

keyEl:   "#{op.parameter1}"      → extracts dimension key from WalletOperation
valueEl: "#{op.quantity}" → extracts value to accumulate
filterEl: "#{op.quantity > 0}" → optional filter

Example: "Track usage per API model"
Period Jan:
WO(param1="gpt-4", qty=1000): → accumulatedValues["gpt-4"] = 1000
WO(param1="gpt-3.5", qty=5000): → accumulatedValues["gpt-3.5"] = 5000
WO(param1="gpt-4", qty=2000): → accumulatedValues["gpt-4"] = 3000

Result: { "gpt-4": 3000, "gpt-3.5": 5000 }

EL Context Variables

In keyEl/valueEl/filterEl, the WalletOperation is available as op:

#{op.quantity}            → BigDecimal (rated quantity)
#{op.amountWithoutTax} → BigDecimal (rated amount excl. tax)
#{op.amountWithTax} → BigDecimal (rated amount incl. tax)
#{op.parameter1} → String (EDR param1)
#{op.parameter2} → String (EDR param2)
#{op.parameter3} → String (EDR param3)
#{op.parameterExtra} → String
#{op.startDate} → Date
#{op.endDate} → Date
#{op.getCountryCode()} → String (if available)

What ceiling does

ceiling is the initial counter level (NOT a cap):

  • For "5 GB included": ceiling = 5120 (MB)
  • Counter starts at ceiling value and DECREASES as usage is consumed
  • When counter reaches 0, included quota is exhausted → overage starts
  • ceilingExpressionEl allows dynamic ceiling (e.g., based on subscription attribute)

dimensions should be an array?

Current model: Single key/value pair (one keyEl + one valueEl). Limitation: Can't track multiple independent dimensions (e.g., country AND model simultaneously). Each dimension generates a different key string in the accumulatedValues map.

For composite dimensions, use concatenated key:

keyEl: "#{op.parameter1}_#{op.parameter2}"  → "US_gpt-4", "FR_gpt-3.5"

If true multi-dimensional tracking is needed (array of dimensions), that would require entity model changes.

trackedValue should be trackedValue_fx?

Current model uses counterType enum (not EL):

  • USAGE → tracks walletOperation.quantity
  • USAGE_AMOUNT → tracks walletOperation.amountWithoutTax/Tax

But valueEl IS an EL expression that can track anything. So for the V1 API:

  • trackedValue = QUANTITY maps to counterType=USAGE (uses quantity)
  • trackedValue = AMOUNT maps to counterType=USAGE_AMOUNT (uses amount)
  • trackedValue = CUSTOM → use accumulator.valueEl (any EL expression)

UnitOfMeasure — on ALL charge types

inputUnitOfMeasure and ratingUnitOfMeasure are on the base ChargeTemplate class (all types inherit):

  • RECURRING: can use UoM (e.g., "per seat", "per license")
  • USAGE: commonly used (MB→GB, seconds→minutes, tokens→K tokens)
  • ONE-SHOT: rarely used but available

Sample Data Walkthrough: Complete EDR → Invoice

Setup

Subscription: SUB_KARIM
Product: PROD_INWI_IDAR
Started: 2026-01-05

Charge 1: CHG_INWI_BASE (RECURRING)
chargeApplicationCalendar: CAL_MONTHLY_PERIODIC (init=Jan 5)
invoicingCalendar: same (CAL_MONTHLY_PERIODIC)
price: 49.00 MAD (FLAT)
applyInAdvance: true

Charge 2: CHG_INWI_DATA (USAGE, TIERED)
ratingPeriodCalendar: CAL_1ST_MONTH (counter resets 1st of month)
invoicingCalendar: CAL_1ST_MONTH
accumulator: SINGLE_VALUE, trackedValue=QUANTITY, ceiling=5120 (MB)
tiers: [0-5120: 0.00, 5120+: 0.10 MAD/MB]
filterParam1: DATA
startDateStrategy: CALENDAR_START

Charge 3: CHG_INWI_VOICE (USAGE, TIERED)
ratingPeriodCalendar: CAL_1ST_MONTH
invoicingCalendar: CAL_1ST_MONTH
accumulator: SINGLE_VALUE, trackedValue=QUANTITY, ceiling=300 (min)
tiers: [0-300: 0.00, 300+: 0.50 MAD/min]
filterParam1: VOICE

Step 1: RecurringRatingJob runs (rateUntilDate=Feb 5)

CHG_INWI_BASE:
chargeApplicationCalendar = CAL_MONTHLY_PERIODIC, init=Jan 5
period: Jan 5 → Feb 5
applyInAdvance=true → operationDate = Jan 5

→ WO #1:
operationDate = 2026-01-05
startDate = 2026-01-05
endDate = 2026-02-05
amount = 49.00 MAD
invoicingDate = 2026-02-05 (from invoicingCalendar.nextDate(Jan 5))
status = OPEN

Step 2: EDRs arrive during January

EDR #1: Jan 10, param1=DATA, quantity=1000 MB
EDR #2: Jan 15, param1=DATA, quantity=2500 MB
EDR #3: Jan 20, param1=DATA, quantity=2000 MB ← crosses 5120 threshold
EDR #4: Jan 22, param1=VOICE, quantity=200 min
EDR #5: Jan 25, param1=DATA, quantity=500 MB
EDR #6: Jan 28, param1=VOICE, quantity=150 min ← crosses 300 threshold

Step 3: UsageRatingJob processes EDRs

EDR #1 (DATA, 1000 MB):
Counter: 0 + 1000 = 1000 (< 5120 ceiling)
Tier 1: 1000 × 0.00 = 0 MAD
→ WO #2: operationDate=Jan 10, amount=0.00, invoicingDate=Feb 1

EDR #2 (DATA, 2500 MB):
Counter: 1000 + 2500 = 3500 (< 5120)
Tier 1: 2500 × 0.00 = 0 MAD
→ WO #3: operationDate=Jan 15, amount=0.00, invoicingDate=Feb 1

EDR #3 (DATA, 2000 MB):
Counter: 3500 + 2000 = 5500 (EXCEEDS 5120!)
Tier 1: 1620 × 0.00 = 0 MAD (remaining in tier)
Tier 2: 380 × 0.10 = 38.00 MAD (overage)
→ WO #4: operationDate=Jan 20, amount=38.00, invoicingDate=Feb 1

EDR #4 (VOICE, 200 min):
Voice counter: 0 + 200 = 200 (< 300)
Tier 1: 200 × 0.00 = 0 MAD
→ WO #5: operationDate=Jan 22, amount=0.00, invoicingDate=Feb 1

EDR #5 (DATA, 500 MB):
Data counter: 5500 + 500 = 6000 (all in Tier 2)
Tier 2: 500 × 0.10 = 50.00 MAD
→ WO #6: operationDate=Jan 25, amount=50.00, invoicingDate=Feb 1

EDR #6 (VOICE, 150 min):
Voice counter: 200 + 150 = 350 (EXCEEDS 300!)
Tier 1: 100 × 0.00 = 0 MAD (remaining)
Tier 2: 50 × 0.50 = 25.00 MAD (overage)
→ WO #7: operationDate=Jan 28, amount=25.00, invoicingDate=Feb 1

Step 4: RT_JOB runs (converts WO → RT)

WO #1 (recurring) → RT #1: usageDate=Jan 5,  amount=49.00, invoicingDate=Feb 5
WO #2 (data free) → RT #2: usageDate=Jan 10, amount=0.00, invoicingDate=Feb 1
WO #3 (data free) → RT #3: usageDate=Jan 15, amount=0.00, invoicingDate=Feb 1
WO #4 (data over) → RT #4: usageDate=Jan 20, amount=38.00, invoicingDate=Feb 1
WO #5 (voice free)→ RT #5: usageDate=Jan 22, amount=0.00, invoicingDate=Feb 1
WO #6 (data over) → RT #6: usageDate=Jan 25, amount=50.00, invoicingDate=Feb 1
WO #7 (voice over)→ RT #7: usageDate=Jan 28, amount=25.00, invoicingDate=Feb 1

Step 5: Bill Run on Feb 1 (BillingCycle = CAL_1ST_MONTH)

lastTransactionDate = Feb 1
invoiceUpToDate = Feb 1

SELECT WHERE status=OPEN
AND usageDate >= Jan 1 AND usageDate < Feb 1
AND (invoicingDate IS NULL OR invoicingDate < Feb 1)

RT #1: usageDate=Jan 5 ✓, invoicingDate=Feb 5 → Feb 5 < Feb 1? NO → ❌ EXCLUDED!
RT #2: usageDate=Jan 10 ✓, invoicingDate=Feb 1 → Feb 1 < Feb 1? NO → ❌ EXCLUDED!

Hmm — ALL excluded because invoicingDate = Feb 1 is NOT < Feb 1!

THIS IS THE < vs <= ISSUE. Need to verify: is the query < or <=?
If <=: RT #2-7 included, RT #1 still excluded (Feb 5 > Feb 1)
If <: nothing included — bill run produces empty invoice!

Resolution: The invoicingDate gate uses <= or the bill run uses invoiceUpToDate = lastTransactionDate + 1 day

This is a configuration detail that determines exact behavior. The key takeaway is that the invoicingCalendar must be carefully aligned with the billingCycle to avoid "just missed" scenarios.

Step 6: Bill run on Feb 5 (catches everything)

lastTransactionDate = Feb 5

RT #1: usageDate=Jan 5 ✓, invoicingDate=Feb 5 → Feb 5 <= Feb 5 → ✓ INCLUDED
RT #2-7: usageDate in Jan ✓, invoicingDate=Feb 1 → Feb 1 <= Feb 5 → ✓ INCLUDED

Invoice for SUB_KARIM:
InvoiceLine 1 (ISUB_TEL_BASE): 49.00 MAD (recurring)
validity: Jan 5 → Feb 5
InvoiceLine 2 (ISUB_TEL_DATA): 88.00 MAD (38.00 + 50.00, grouped)
validity: Jan 20 → Jan 25
InvoiceLine 3 (ISUB_TEL_VOICE): 25.00 MAD
validity: Jan 28 → Jan 28

Subtotal: 162.00 MAD
Tax (20%): 32.40 MAD
Total: 194.40 MAD

startDateStrategy — Applicable to BOTH Recurring and Usage

You're right — startDateStrategy should be available for ANY charge that uses a PERIOD-type calendar:

Charge TypeWhen startDateStrategy is needed
RECURRINGIf chargeApplicationCalendar is PERIOD (anniversary)
USAGEIf ratingPeriodCalendar is PERIOD (anniversary counter reset)
ONE-SHOTNever (no calendar)

If calendar is FIXED (CAL_1ST_MONTH, CAL_1ST_WEEK_MON, etc.), startDateStrategy is NOT needed — boundaries are absolute.

startDateStrategy Values

ValueInit Date SourceUse Case
PRODUCT_ACTIVATION_DATEserviceInstance.activationDateDefault — most precise anchor
SUBSCRIPTION_STARTsubscription.subscriptionDateLegacy default (current code)
FIRST_EVENTFirst EDR eventDateCounter starts on first usage
CALENDAR_STARTNot applicable (fixed boundary)Auto-set for CAL_1ST_* calendars
CHARGE_DATEchargeInstance.chargeDateExplicit override

Calendar Init Date: startDateStrategy

Current behavior

Calendar period start is initialized with subscriptionDate (hardcoded).

Proposed: configurable via startDateStrategy

StrategyInit DateUse Case
SUBSCRIPTION_STARTsubscription.subscriptionDateCurrent default
PRODUCT_ACTIVATION_DATEserviceInstance.activationDateProposed new default — more precise for product-based billing
FIRST_EVENTFirst EDR eventDateCounter starts on first usage
CALENDAR_STARTNo init needed (fixed boundary)For CAL_1ST_MONTH type calendars
CHARGE_DATEchargeInstance.chargeDateExplicit override

Multi-Value Tracking Clarified

It's NOT "multiple values" — it's "one value tracked per dimension key"

The accumulatedValues map is Map<String, BigDecimal> — one BigDecimal per key string.

SINGLE_VALUE: tracks one total number (e.g., total MB used = 6000) MULTI_VALUE: tracks one number PER key (e.g., US=175 MB, FR=150 MB)

You can't track BOTH quantity AND amount simultaneously in one counter. Each counter tracks ONE metric. If you need both → use TWO counters linked to the same charge.

Default tracking presets

PresetcounterTypekeyElvalueElDescription
QUANTITYUSAGEnull#{op.quantity}Track total quantity
AMOUNTUSAGE_AMOUNTnull#{op.amountWithoutTax}Track total amount excl. tax
AMOUNT_WITH_TAXUSAGE_AMOUNTnull#{op.amountWithTax}Track total amount incl. tax
QUANTITY_BY_KEYUSAGE#{op.parameter1}#{op.quantity}Quantity per dimension
AMOUNT_BY_KEYUSAGE_AMOUNT#{op.parameter1}#{op.amountWithoutTax}Amount per dimension

initialValue (renamed from ceiling)

  • initialValue is the starting counter level (NOT a cap)
  • If not provided: counter starts at 0 and accumulates upward (pure accumulator)
  • If provided (e.g., 5120 for "5 GB included"):
    • Counter starts at 5120
    • Each EDR DECREASES the counter
    • When counter reaches 0 → included quota exhausted → overage begins
    • Negative values = overage amount
  • initialValueEl: EL expression for dynamic initial value (e.g., based on subscription attribute)

TODO for Incomplete Field Mapping — MUST COMPLETE

CatalogImportService.java:1007 has incomplete mapping. Must be fixed in implementation:

  • Map accumulator.typeCounterTemplate.accumulatorType
  • Map accumulator.trackedValueCounterTemplate.counterType (USAGE/USAGE_AMOUNT)
  • Map accumulator.initialValueCounterTemplate.ceiling
  • Map accumulator.dimensions.key_fxCounterTemplate.keyEl
  • Map accumulator.dimensions.value_fxCounterTemplate.valueEl
  • Map accumulator.dimensions.filter_fxCounterTemplate.filterEl

Verification Plan

1. TDD — Backend Unit Tests

  • RecurringChargeV1ResourceTest: chargeApplicationCalendar, invoicingCalendar, startDateStrategy
  • UsageChargeV1ResourceTest: ratingPeriodCalendar, accumulator (single+multi), invoicingCalendar
  • ChargeV1MapperTest: all calendar fields returned in GET

2. API Validation — curl/Postman

  • Create charges with all new calendar fields
  • Verify GET returns them correctly
  • Verify counter creation from accumulator payload

3. UI Validation — Browser

  • Edit recurring charge → verify Charge Application Calendar + Invoicing Calendar populated
  • Edit usage charge → verify Rating Period Calendar + Accumulator fields + Invoicing Calendar
  • Verify "Same as" checkbox behavior

4. End-to-End — Provision + Bill Run

  • Run provision script → submit EDRs → run jobs → verify invoice

5. Documentation — Docusaurus page + .drawio diagram

Impact on jobs

  • RecurringRatingJobBean.createRecurringCharges() → calls RecurringRatingService.rateReccuringCharge() → calls CalendarService.initializeCalendar(cal, initDate, chargeInstance)
  • The initDate is currently chargeInstance.getSubscriptionDate() (line 708)
  • Change: read startDateStrategy from charge template → resolve init date accordingly
  • For PRODUCT_ACTIVATION_DATE: use chargeInstance.getServiceInstance().getActivationDate()

Standard Calendar Naming Convention

Pattern: CAL_{BOUNDARY}_{FREQUENCY}

Fixed boundary calendars (YEARLY type — ignores init date)

CodeTypeBoundariesDescription
CAL_1ST_MONTHYEARLY1st of each monthMonthly, fixed boundary
CAL_1ST_QUARTERYEARLYJan 1, Apr 1, Jul 1, Oct 1Quarterly, fixed boundary
CAL_1ST_SEMESTERYEARLYJan 1, Jul 1Semi-annual, fixed boundary
CAL_1ST_YEARYEARLYJan 1Annual, fixed boundary
CAL_1ST_WEEK_MONYEARLYEvery MondayWeekly, Monday start
CAL_DAILYYEARLYEvery midnightDaily, fixed boundary

Periodic calendars (PERIOD type — anchored to init date / anniversary)

CodeTypePeriodDescription
CAL_MONTHLY_PERIODICPERIOD(MONTH, 1)1 month from initMonthly anniversary
CAL_3MONTHLY_PERIODICPERIOD(MONTH, 3)3 months from initQuarterly anniversary
CAL_6MONTHLY_PERIODICPERIOD(MONTH, 6)6 months from initSemi-annual anniversary
CAL_YEARLY_PERIODICPERIOD(MONTH, 12)12 months from initAnnual anniversary
CAL_WEEKLY_PERIODICPERIOD(DAY, 7)7 days from initWeekly from init date
CAL_DAILY_PERIODICPERIOD(DAY, 1)1 day from initDaily from init date
CAL_BIWEEKLY_PERIODICPERIOD(DAY, 14)14 days from initBi-weekly from init

Which to use where?

PurposeRecommendedWhy
Recurring charge applicationCAL_1ST_MONTH or CAL_MONTHLY_PERIODICFixed = predictable, Periodic = anniversary
Usage counter reset (weekly)CAL_1ST_WEEK_MONFixed Monday boundaries
Usage counter reset (monthly)CAL_1ST_MONTHReset on 1st, clear for customers
Invoicing gate (monthly)CAL_1ST_MONTHAll WOs eligible on 1st
Invoicing gate (daily)CAL_DAILYWOs eligible next midnight
Commitment term (12 months)CAL_YEARLY_PERIODICExactly 12 months from start

Provision Script: Standard Calendar Creation

# ═══ Fixed boundary calendars (YEARLY type — ignores init date) ═══
v1_upsert "calendars" "CAL_1ST_MONTH" '{"code":"CAL_1ST_MONTH","description":"Monthly - 1st of each month","calendarType":"YEARLY","nbPeriods":12}'
v1_upsert "calendars" "CAL_1ST_QUARTER" '{"code":"CAL_1ST_QUARTER","description":"Quarterly - Jan/Apr/Jul/Oct 1st","calendarType":"YEARLY","nbPeriods":4}'
v1_upsert "calendars" "CAL_1ST_SEMESTER" '{"code":"CAL_1ST_SEMESTER","description":"Semi-annual - Jan/Jul 1st","calendarType":"YEARLY","nbPeriods":2}'
v1_upsert "calendars" "CAL_1ST_YEAR" '{"code":"CAL_1ST_YEAR","description":"Annual - Jan 1st","calendarType":"YEARLY","nbPeriods":1}'
v1_upsert "calendars" "CAL_1ST_WEEK_MON" '{"code":"CAL_1ST_WEEK_MON","description":"Weekly - every Monday","calendarType":"YEARLY","nbPeriods":52}'
v1_upsert "calendars" "CAL_DAILY" '{"code":"CAL_DAILY","description":"Daily - midnight to midnight","calendarType":"YEARLY","nbPeriods":365}'

# ═══ Periodic calendars (PERIOD type — anchored to init date / anniversary) ═══
v1_upsert "calendars" "CAL_MONTHLY_PERIODIC" '{"code":"CAL_MONTHLY_PERIODIC","description":"Monthly periodic (anniversary)","calendarType":"PERIOD","periodUnit":"MONTH","periodLength":1}'
v1_upsert "calendars" "CAL_3MONTHLY_PERIODIC" '{"code":"CAL_3MONTHLY_PERIODIC","description":"Quarterly periodic (anniversary)","calendarType":"PERIOD","periodUnit":"MONTH","periodLength":3}'
v1_upsert "calendars" "CAL_6MONTHLY_PERIODIC" '{"code":"CAL_6MONTHLY_PERIODIC","description":"Semi-annual periodic (anniversary)","calendarType":"PERIOD","periodUnit":"MONTH","periodLength":6}'
v1_upsert "calendars" "CAL_YEARLY_PERIODIC" '{"code":"CAL_YEARLY_PERIODIC","description":"Annual periodic (anniversary)","calendarType":"PERIOD","periodUnit":"MONTH","periodLength":12}'
v1_upsert "calendars" "CAL_WEEKLY_PERIODIC" '{"code":"CAL_WEEKLY_PERIODIC","description":"Weekly periodic (from init)","calendarType":"PERIOD","periodUnit":"DAY_OF_MONTH","periodLength":7}'
v1_upsert "calendars" "CAL_BIWEEKLY_PERIODIC" '{"code":"CAL_BIWEEKLY_PERIODIC","description":"Bi-weekly periodic (from init)","calendarType":"PERIOD","periodUnit":"DAY_OF_MONTH","periodLength":14}'
v1_upsert "calendars" "CAL_DAILY_PERIODIC" '{"code":"CAL_DAILY_PERIODIC","description":"Daily periodic (from init)","calendarType":"PERIOD","periodUnit":"DAY_OF_MONTH","periodLength":1}'