Skip to content

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:

json
{
  "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:

  1. Organization.allowedPresets presets assigned by the superadmin in the Organization Manager
  2. OrgConfigReplica.preset_overrides where visible !== false presets 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_rate
  • customer_rate_per_sec >= org_rate_per_sec
  • customer_interval_pricing[X].cost_per_interval >= org_rate_per_sec × X (or superadmin's base)

Cascade Logic ​

TriggerActionLogged
Org rate increasesAll customer_rates below new floor → raised to floorEach adjustment
Org rate_per_sec increasesAll customer_rate_per_sec below new floor → raisedEach adjustment
Preset visibility → falseAll ServicePricing docs for org+preset deletedCount removed

File Reference ​

FileRoleKey Functions
models/ServicePricing.tsMongoose schemaIServicePricing, ICustomerIntervalPricing, compound index
repositories/servicePricing.repository.tsDB operationsfindByOrg, findOne, upsert, findBelowFloorInterval, bulkRaiseToFloorInterval
services/servicePricing.service.tsBusiness logic + validationgetOrgCustomerPricing, upsertCustomerRate, removeCustomerRate, cascade methods
controllers/servicePricing.controller.tsHTTP handlersgetServicePricing, upsertCustomerServicePricing, deleteCustomerServicePricing
routes/admin.routes.tsRoute registration3 routes under /admin/
utils/rateResolver.tsCall-start rate capturecaptureRateSnapshot, captureIntervalPricingSnapshot
services/orgConfigReplica.service.tsCascade hooksupdatePresetRate, togglePresetVisibility

Frontend ​

FileRole
app/src/types/credit.tsPresetPricingGroup, CustomerPricingOverride, IntervalPricingTier interfaces
app/src/services/admin.service.tsgetServicePricing, upsertCustomerServicePricing (with interval pricing support)
app/src/components/features/CustomerPricingOverrides.tsxCustomer pricing overrides with interval editing UI
app/src/pages/admin/ConfigManagementPage.tsxParent page with tabs: System Config, Pricing & Visibility, Customer Overrides
app/src/hooks/useAgentConfig.tsPreset type includes effective_rate, customer_rate, rate_source
app/src/components/features/VoicePlanSelector.tsxDisplays per-preset rate + "(custom rate)" label for customer overrides
controllers/config.controller.tsgetAgentConfig includes customer-specific rate in preset data

Note: The standalone /admin/service-pricing route now redirects to /admin/config. Customer pricing overrides are accessed via the "Customer Overrides" tab in Config Management.


Data Model ​

Schema:

typescript
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: 200 with { success: true, pricing: PresetPricingGroup[] }

Response includes interval pricing:

json
{
  "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: 200 with { 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: 200 with { 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 ​

  1. 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
  2. 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
  3. 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
  4. 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
  5. Add Override Dialog Set all rates including interval overrides


How to Test ​

Via UI ​

  1. Login as org admin and go to /admin/config
  2. Click the "Customer Overrides" tab
  3. Verify visible presets are listed with their org rates and interval pricing badges
  4. 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
  5. Verify the override appears with interval rate badge showing "direct override" (blue)
  6. Click edit icon → change interval rate → save → verify badge updates
  7. Clear interval rate input → save → verify badge shows "calculated" or "base"

Via API ​

bash
# 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_balanced

Property Tests ​

bash
# Run all service pricing property tests
npx vitest --run --testPathPattern="service-pricing|rate-resolution-hierarchy|invisible-preset"

Troubleshooting ​

SymptomCauseFix
"Preset X is not available" on upsertPreset has visible=false in OrgConfigReplicaEnable the preset in Config Management first
"Interval Xs is not defined in superadmin config"Trying to set rate for interval not in presetSuperAdmin must add interval to preset first
"Customer rate for Xs interval must be >= Y"Rate below org floorIncrease rate to at least the floor value
No "Interval Rates" columnPreset has no interval_pricing definedSuperAdmin must add interval_pricing to preset
403 Forbidden on upsert/deleteCustomer doesn't belong to admin's orgVerify customerUID matches a user with orgUID matching admin's org
Customer rate shows org_rate after org rate increaseCascade raised the customer_rate to new floorExpected behavior set a higher rate if needed
Missing preset in Service Pricing pagePreset not in Organization.allowedPresets AND not in replicaSuperadmin must assign the preset to the org via Organization Manager
Empty pricing page (no presets shown)Org has no allowedPresets and no replica overridesSuperadmin must assign presets to the org
Rate resolver returns org_override instead of customer_overridePreset is invisible OR customerUID not passed to captureRateSnapshotCheck that the calling code passes customerUID param
Agent editor not showing rateBackend not returning effective_rateCheck config.controller.ts includes customer pricing lookup for role=customer
E11000 duplicate key error on upsertLegacy MongoDB index using old field names (serviceType instead of preset_id)Run the index fix migration (see below)
403 Forbidden when editing existing rowInvalid record with non-customer customerUID in DBRun the index fix migration to clean up invalid records
Interval badges show "Base rate"No customer or org per-second rate setSet customer_rate_per_sec or org_rate_per_sec for calculated rates
Can't edit interval ratesEdit mode not activeClick 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 → now preset_id
  • organizationId → now orgUID
  • customerId → now customerUID

Old indexes still exist in the database and conflict with new records.

Fix via API ​

bash
# 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 ​

  1. Drops legacy indexes containing serviceType, organizationId, or customerId
  2. Deletes invalid records where customerUID doesn't belong to a customer-role user
  3. Creates correct index on { orgUID, customerUID, preset_id } (unique)

Manual Fix via MongoDB Shell ​

javascript
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 customerUID doesn'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.

Last updated: