Service Pricing Customer-Specific Rate Overrides
Admin-facing feature that lets org admins set per-customer rates for each service preset. Builds on top of the existing OrgConfigReplica rate system by adding a third tier: customer → org → default.
Supports three types of rate overrides:
- Per-minute rates (
customer_rate) for per-minute billing customers - Per-second rates (
customer_rate_per_sec) for per-second billing customers - Per-interval rates (
customer_interval_pricing) for interval-based billing customers (e.g., 14s, 15s, 30s)
Architecture Overview
Interval Pricing Overview
What is Interval Pricing?
Interval pricing allows superadmins to define custom billing intervals with specific costs. Instead of only per-minute or per-second billing, customers can be billed per-interval (e.g., per 14 seconds, per 15 seconds, per 30 seconds).
How SuperAdmin Creates Interval Pricing
SuperAdmin configures interval_pricing on presets in SystemConfig:
{
"preset_id": "oktivo_balanced",
"name": "Balanced",
"cost_per_min": 2.0,
"cost_per_sec": 0.04,
"interval_pricing": [
{ "interval_seconds": 14, "cost_per_interval": 0.5, "label": "14 Second Billing" },
{ "interval_seconds": 15, "cost_per_interval": 0.6, "label": "Standard" },
{ "interval_seconds": 30, "cost_per_interval": 1.0, "label": "Premium" }
]
}SuperAdmin UI: System Configuration Panel → Edit Preset → Interval Pricing section
How Org Admins Set Margin
Org admins can set their margin via org_rate_per_sec. The system scales this rate by the interval:
- Formula:
org_rate = org_rate_per_sec × interval_seconds - Example:
org_rate_per_sec = 0.05, interval = 14s →org_rate = 0.7 credits per 14s
Org Admin UI: Config Management → Pricing & Visibility tab
How Org Admins Override Customer Interval Rates
Org admins can set direct interval rates for specific customers via customer_interval_pricing.
Org Admin UI: Config Management → Customer Overrides tab → Edit customer → Interval Rates inputs
How It Works
Preset Visibility What presets appear in Service Pricing
getVisiblePresetIds(orgUID) determines which presets the org can see. It computes the union of:
- Organization.allowedPresets presets assigned by the superadmin in the Organization Manager
- OrgConfigReplica.preset_overrides where
visible !== falsepresets the admin has configured
Then subtracts any preset explicitly marked visible: false in the replica. This means a superadmin-assigned preset shows up even if the admin hasn't configured its rate yet.
Rate Resolution Hierarchy (3-tier + Interval)
When a call starts, captureRateSnapshot resolves the billing rate.
For Interval-Based Billing (customer.billingUnit matches an interval_pricing tier):
Priority 1: customer_interval_pricing[interval].cost_per_interval (direct override)
↓ if not found
Priority 2: customer_rate_per_sec × interval_seconds (calculated)
↓ if not found
Priority 3: org_rate_per_sec × interval_seconds (calculated)
↓ if not found
Priority 4: interval_pricing[interval].cost_per_interval (superadmin base)For Per-Minute Billing (billingUnit = 60):
Priority 1: customer_rate (ServicePricing)
Priority 2: org_rate (OrgConfigReplica)
Priority 3: cost_per_min (SystemConfig)For Per-Second Billing (billingUnit = 1):
Priority 1: customer_rate_per_sec (ServicePricing)
Priority 2: org_rate_per_sec (OrgConfigReplica)
Priority 3: cost_per_sec (SystemConfig)Floor Constraint
All rates must be >= the org floor:
customer_rate >= org_ratecustomer_rate_per_sec >= org_rate_per_seccustomer_interval_pricing[X].cost_per_interval >= org_rate_per_sec × X(or superadmin's base)
Cascade Logic
| Trigger | Action | Logged |
|---|---|---|
| Org rate increases | All customer_rates below new floor → raised to floor | Each adjustment |
| Org rate_per_sec increases | All customer_rate_per_sec below new floor → raised | Each adjustment |
| Preset visibility → false | All ServicePricing docs for org+preset deleted | Count removed |
File Reference
| File | Role | Key Functions |
|---|---|---|
models/ServicePricing.ts | Mongoose schema | IServicePricing, ICustomerIntervalPricing, compound index |
repositories/servicePricing.repository.ts | DB operations | findByOrg, findOne, upsert, findBelowFloorInterval, bulkRaiseToFloorInterval |
services/servicePricing.service.ts | Business logic + validation | getOrgCustomerPricing, upsertCustomerRate, removeCustomerRate, cascade methods |
controllers/servicePricing.controller.ts | HTTP handlers | getServicePricing, upsertCustomerServicePricing, deleteCustomerServicePricing |
routes/admin.routes.ts | Route registration | 3 routes under /admin/ |
utils/rateResolver.ts | Call-start rate capture | captureRateSnapshot, captureIntervalPricingSnapshot |
services/orgConfigReplica.service.ts | Cascade hooks | updatePresetRate, togglePresetVisibility |
Frontend
| File | Role |
|---|---|
app/src/types/credit.ts | PresetPricingGroup, CustomerPricingOverride, IntervalPricingTier interfaces |
app/src/services/admin.service.ts | getServicePricing, upsertCustomerServicePricing (with interval pricing support) |
app/src/components/features/CustomerPricingOverrides.tsx | Customer pricing overrides with interval editing UI |
app/src/pages/admin/ConfigManagementPage.tsx | Parent page with tabs: System Config, Pricing & Visibility, Customer Overrides |
app/src/hooks/useAgentConfig.ts | Preset type includes effective_rate, customer_rate, rate_source |
app/src/components/features/VoicePlanSelector.tsx | Displays per-preset rate + "(custom rate)" label for customer overrides |
controllers/config.controller.ts | getAgentConfig includes customer-specific rate in preset data |
Note: The standalone
/admin/service-pricingroute now redirects to/admin/config. Customer pricing overrides are accessed via the "Customer Overrides" tab in Config Management.
Data Model
Schema:
interface ICustomerIntervalPricing {
interval_seconds: number; // 1-3600
cost_per_interval: number; // > 0
}
interface IServicePricing {
orgUID: string;
customerUID: string;
preset_id: string;
customer_rate: number; // Per-minute rate
customer_rate_per_sec?: number; // Per-second rate (optional)
customer_interval_pricing?: ICustomerIntervalPricing[]; // Per-interval rates (optional)
}Indexes:
{ orgUID: 1, customerUID: 1, preset_id: 1 }unique compound (one rate per customer per preset per org){ orgUID: 1, preset_id: 1 }query index for cascade operations
Validation: customer_rate has min: 0.01 (positive values only).
API Endpoints
List Pricing
- Method:
GET - Path:
/api/admin/service-pricing - Auth:
verifyJwt→requireRole("admin")→requireOrg - Response:
200with{ success: true, pricing: PresetPricingGroup[] }
Response includes interval pricing:
{
"pricing": [{
"preset_id": "oktivo_balanced",
"interval_pricing": [{ "interval_seconds": 14, "cost_per_interval": 0.5 }],
"overrides": [{
"customerUID": "usr_123",
"customer_interval_pricing": [{ "interval_seconds": 14, "cost_per_interval": 0.6 }]
}]
}]
}Upsert Customer Rate
- Method:
PUT - Path:
/api/admin/customers/:customerUID/service-pricing/:presetId - Auth: Same as above
- Body:json
{ "customer_rate": 2.5, "customer_rate_per_sec": 0.05, "customer_interval_pricing": [ { "interval_seconds": 14, "cost_per_interval": 0.6 } ] } - Response:
200with{ success: true, message, pricing } - Errors:
400(invisible preset, below floor, invalid rate, interval not defined),403(wrong org)
Delete Customer Rate
- Method:
DELETE - Path:
/api/admin/customers/:customerUID/service-pricing/:presetId - Auth: Same as above
- Response:
200with{ success: true, message } - Errors:
403(wrong org),404(override not found)
UI Components
Customer Pricing Overrides Tab
Location: Config Management → Customer Overrides tab
Component: app/src/components/features/CustomerPricingOverrides.tsx
Features
Preset Cards Each visible preset shows:
- Org rate (per-minute)
- Org rate (per-second) if configured
- Interval pricing badges superadmin-defined intervals with base costs
Customer Overrides Table Columns:
- Customer Name, Email
- Rate (credits/min) inline editable
- Rate (credits/sec) inline editable (if per-second pricing enabled)
- Interval Rates badges showing effective rate per interval
Interval Rates Column Shows badges color-coded by source:
- Blue (default): Direct customer override
- Grey (secondary): Calculated from per-second rate
- Outline: Using org or base rate
Inline Editing Click edit icon to:
- Edit per-minute and per-second rates
- Edit individual interval rates via input fields
- Leave interval input empty to use calculated rate
Add Override Dialog Set all rates including interval overrides
How to Test
Via UI
- Login as org admin and go to
/admin/config - Click the "Customer Overrides" tab
- Verify visible presets are listed with their org rates and interval pricing badges
- Click "Add Override" on a preset with interval pricing:
- Select customer with interval billing unit (e.g., 14s)
- Set per-minute rate
- In "Interval Pricing Override" section, set a rate for 14s
- Submit
- Verify the override appears with interval rate badge showing "direct override" (blue)
- Click edit icon → change interval rate → save → verify badge updates
- Clear interval rate input → save → verify badge shows "calculated" or "base"
Via API
# Get all overrides (includes interval pricing)
curl -H "Authorization: Bearer <token>" \
http://localhost:3001/api/admin/service-pricing
# Add/update with interval pricing
curl -X PUT -H "Authorization: Bearer <token>" \
-H "Content-Type: application/json" \
-d '{
"customer_rate": 3.0,
"customer_rate_per_sec": 0.05,
"customer_interval_pricing": [
{ "interval_seconds": 14, "cost_per_interval": 0.6 }
]
}' \
http://localhost:3001/api/admin/customers/cust_123/service-pricing/oktivo_balanced
# Remove override
curl -X DELETE -H "Authorization: Bearer <token>" \
http://localhost:3001/api/admin/customers/cust_123/service-pricing/oktivo_balancedProperty Tests
# Run all service pricing property tests
npx vitest --run --testPathPattern="service-pricing|rate-resolution-hierarchy|invisible-preset"Troubleshooting
| Symptom | Cause | Fix |
|---|---|---|
| "Preset X is not available" on upsert | Preset has visible=false in OrgConfigReplica | Enable the preset in Config Management first |
| "Interval Xs is not defined in superadmin config" | Trying to set rate for interval not in preset | SuperAdmin must add interval to preset first |
| "Customer rate for Xs interval must be >= Y" | Rate below org floor | Increase rate to at least the floor value |
| No "Interval Rates" column | Preset has no interval_pricing defined | SuperAdmin must add interval_pricing to preset |
| 403 Forbidden on upsert/delete | Customer doesn't belong to admin's org | Verify customerUID matches a user with orgUID matching admin's org |
| Customer rate shows org_rate after org rate increase | Cascade raised the customer_rate to new floor | Expected behavior set a higher rate if needed |
| Missing preset in Service Pricing page | Preset not in Organization.allowedPresets AND not in replica | Superadmin must assign the preset to the org via Organization Manager |
| Empty pricing page (no presets shown) | Org has no allowedPresets and no replica overrides | Superadmin must assign presets to the org |
Rate resolver returns org_override instead of customer_override | Preset is invisible OR customerUID not passed to captureRateSnapshot | Check that the calling code passes customerUID param |
| Agent editor not showing rate | Backend not returning effective_rate | Check config.controller.ts includes customer pricing lookup for role=customer |
| E11000 duplicate key error on upsert | Legacy MongoDB index using old field names (serviceType instead of preset_id) | Run the index fix migration (see below) |
| 403 Forbidden when editing existing row | Invalid record with non-customer customerUID in DB | Run the index fix migration to clean up invalid records |
| Interval badges show "Base rate" | No customer or org per-second rate set | Set customer_rate_per_sec or org_rate_per_sec for calculated rates |
| Can't edit interval rates | Edit mode not active | Click the edit (pencil) icon first |
Migration: Fix Legacy Indexes
If you encounter E11000 duplicate key error mentioning serviceType or organizationId, the MongoDB collection has legacy indexes from an older schema version.
Symptoms
E11000 duplicate key error collection: Oktivo-Dev.ServicePricing
index: orgUID_1_serviceType_1_customerUID_1
dup key: { orgUID: "...", serviceType: null, customerUID: "..." }Cause
The collection previously used different field names:
serviceType→ nowpreset_idorganizationId→ noworgUIDcustomerId→ nowcustomerUID
Old indexes still exist in the database and conflict with new records.
Fix via API
# Dry run first (see what would change)
curl -X POST http://localhost:3000/internal/scripts/fix-service-pricing-indexes \
-H "Content-Type: application/json" \
-H "x-api-secret: YOUR_SUPERADMIN_API_SECRET" \
-d '{"dryRun": true}'
# Execute the fix
curl -X POST http://localhost:3000/internal/scripts/fix-service-pricing-indexes \
-H "Content-Type: application/json" \
-H "x-api-secret: YOUR_SUPERADMIN_API_SECRET" \
-d '{}'What the Script Does
- Drops legacy indexes containing
serviceType,organizationId, orcustomerId - Deletes invalid records where
customerUIDdoesn't belong to a customer-role user - Creates correct index on
{ orgUID, customerUID, preset_id }(unique)
Manual Fix via MongoDB Shell
use Oktivo-Dev
// Drop legacy indexes
db.ServicePricing.dropIndex("orgUID_1_serviceType_1_customerUID_1")
db.ServicePricing.dropIndex("organizationId_1_serviceType_1_customerId_1")
// Clean up invalid records (non-customer users)
db.ServicePricing.deleteMany({ customerUID: "usr_INVALID_UID" })
// Verify correct index exists
db.ServicePricing.getIndexes()Data Integrity
The getOrgCustomerPricing service filters out invalid records where:
- The
customerUIDdoesn't exist in the User collection - The user exists but doesn't have
role: "customer"
These records are logged with event service_pricing_invalid_records for debugging. To clean them up permanently, run the index fix migration above.