Convergent Rating¶
Rating is convergent in the strict sense: a single Rater class implements both prepaid and postpaid pricing, both paths read from the same REF_Price_plan table, and both apply the same per-month free-units allowance. A subscriber is flagged either prepaid or postpaid — the same plan id rates correctly for either.
Reference Data¶
Every operator price plan exposes four service-specific fields per service slot. The platform fixes Utils::SERVICES = 4, with the following slot allocation declared in include/utils/utility.hpp:
| Slot | Utils constant |
Typical service |
|---|---|---|
| 1 | SVC_VOICE |
Voice (per-second) |
| 2 | SVC_SMS |
SMS (per-message) |
| 3 | SVC_DATA |
Data (per-KB or per-MB) |
| 4 | SVC_MMS |
MMS (per-message) |
Each plan in proto/types.proto carries:
message REF_Price_plan {
int32 id = 1;
int32 operator_id = 2;
string name = 3;
float svc1_price = 4;
float svc2_price = 5;
float svc3_price = 6;
float svc4_price = 7;
int32 svc1_free_units = 8;
int32 svc2_free_units = 9;
int32 svc3_free_units = 10;
int32 svc4_free_units = 11;
float monthly_fee = 12;
uint64 expiration_date = 13;
}
Free-unit semantics: a value of -1 means unlimited — every requested unit is covered for free. Any non-negative value is a hard monthly cap; consumed units are tracked per (subscriber_id, year, month, service) in the database and surfaced through Subscriber::getUsedFreeUnits().
Rater Surface¶
Rater (in include/rater.hpp) provides three rating overloads, each operating on the matching protobuf type:
class Rater {
public:
// Postpaid
int rate(billing::CDR &);
int pre_rate(billing::CDR &);
int post_rate(billing::CDR &);
// Prepaid final charge (TERMINATION_REQUEST or EVENT_REQUEST)
int rate(billing::Charge &);
int pre_rate(billing::Charge &);
int post_rate(billing::Charge &);
// Prepaid intermediate balance reserve (INITIAL/UPDATE_REQUEST)
int rate(billing::Balance_reserve &);
};
The three overloads share the same algorithm but differ in which protobuf field they write back. The flow per rating call is:
- Resolve the price plan and service through
REFDATA::instance()->getPP(plan_id)andREFDATA::instance()->getSV(svc). A null result fails the call with-1. - Pick the correct
svcN_priceslot via a switch oncdr.svc(). Unknown service ids fail. - If
app.free_units_functionalityis1, derive the calendar month and year from the event timestamp (CDR usescdr.tstamp(); the balance-reserve path falls back tostd::time(nullptr)), look up the subscriber's used free units for that month, and compute how many of the current event's units are covered. The covered count is written tofree_unitson the record and the subscriber's running total is incremented. - Compute
rated_price = unit_cost * (units - covered_free_units). TheChargeandCDRpaths additionally subtract any explicit discount (discountfield) clamped at zero to producefinal_price. - Stamp the record's
status_idtoEVENT_STATUS_RATED(2). PostpaidCDRs additionally storediscount = unit_cost * free_unitsso the originally-rated cost and the applied discount are both retained on the row for invoicing.
pre_rate and post_rate are extension hooks. pre_rate is currently used in CDRLoader to resolve the subscriber's plan id from MSISDN before the actual rate() call (postpaid case). post_rate is a no-op placeholder kept for volume-based discount logic.
Prepaid Path¶
In PrepaidServer::svc() the same Rater instance is reused for the lifetime of a DIAMETER session. Mid-session updates rate against Balance_reserve; the closing CCR (TERMINATION_REQUEST or EVENT_REQUEST) rates against Charge. Because BalanceReserve::fromCCR() and Charge::fromCCR() produce protobuf objects whose plan id is overwritten with the current subscriber's plan_id after the database lookup, the rater is fed the live plan even if the network includes a stale identifier.
The Charge path also has a set_free_units step that decrements the subscriber's monthly allowance immediately — this is the behaviour that matches a real OCS where free-unit consumption must be observable in the next reservation request.
Postpaid Path¶
In CDRLoader, every record entering the system runs through pre_rate → rate → post_rate in sequence. The rated row, with final_price, discount, free_units, and status_id=EVENT_STATUS_RATED, is then inserted by DB_layer::insert(billing::CDR). At bill-cycle time the billing handler does not re-rate — it aggregates already-rated CDRs by service and month, sums their final_price into the four svcN_cost fields of billing::Bill, and inserts the bill. CDRs included in the bill are advanced to EVENT_STATUS_BILLED (4) by DB_layer::updateEventStatusToBilled(billing_id).
If the loader is configured without rating (the rater pointer is null in some test scenarios), CDRs are persisted as-is and rating happens later via the developer-only mass-rating role described below.
Mass Prepaid Rating (Test Mode)¶
prepaid_mass_rating is a developer mode that re-rates queued prepaid charges in bulk. It is only available when modules.prepaid_mass_rating = 1. The activity dispatch in BillingHandler::processActivity routes Utils::PREPAID_RATING activities to DB_layer::ratePrepaidCharges, which iterates pending charge rows and re-runs the rater. This path exists to load-test the rater against synthetic prepaid traffic and is explicitly documented as not for production use.
Currency¶
The platform's primary currency is the Euro. Every monetary value persisted by the rating engine — CDR.final_price, Charge.final_price, Balance_reserve.rated_price, and the running balance.balance — is in EUR, and the in-memory price plans (REF_Price_plan.svcN_price, monthly_fee) are likewise EUR-denominated. The default Utils::formatCurrency(float) helper formats these internal amounts as €%.2f for log lines and dev tooling.
Display currency is a separate, per-subscriber concern. The ref_currency reference table holds an ISO 4217 code, a symbol, and a rate_from_eur multiplier for each supported currency; id=1 is the EUR anchor at rate 1.0. A subscriber's currency_id selects the row used to convert and format on-screen amounts at render time (PDF invoices, CRM dashboard, usage page). The conversion is one-way and lossless — the database never sees a non-EUR figure — and adding a new currency is a single INSERT INTO ref_currency with no application-code change.
Free-Unit Storage¶
Used free units are kept in a per-subscriber, per-month, per-service table accessed through Subscriber::getUsedFreeUnits(month, year) and Subscriber::useFreeUnits(month, year, service, units). The first time a subscriber consumes free units in a new month, Subscriber::initUsedFreeUnits(month, year) lazily seeds the row. Both prepaid and postpaid paths read and write through the same accessor — there is no duplicate accounting between online and offline domains.