Skip to content

ABS Asset Flow — How Assets Enter and Move Through the System

Related: Customer Lifecycle · ABS Docs Flow

This document explains how physical assets — batteries, swap stations, fleets — exist in the ABS ecosystem, how they are restricted per customer plan, and what happens to them from first registration to loss/recovery/return.


The Core Split: ABS vs ARM

ABS does not own or store physical assets. There are two separate systems:

ABS (abs-platform)                      ARM (Asset Resource Manager)
──────────────────                      ────────────────────────────
Owns:                                   Owns:
  Customer subscriptions                  Physical battery inventory
  Service plans & quotas                  Swap station locations
  Fleet IDs (references only)             Concrete asset IDs (bat-007, bat-008)
  Who is allowed what                     IoT device connections
  FSM states (ACTIVE, SUSPENDED)          GPS tracking
  Billing & quota counters                Asset health & charge levels

ABS says:                               ARM responds:
"Give me a battery from                 Picks best available battery,
fleet-nairobi-standard-4ah              checks charge/health, unlocks
for customer plan-nairobi-001           station door, echoes back:
at station-nbi-003"                     "bat-008 issued"

ABS gives ARM fleet IDs (a category). ARM resolves those into real batteries. There is no direct coupling between BSS subscription logic and ARM asset logic. All coordination goes through MQTT.


What a "Fleet" Is

A fleet is a named group of interchangeable physical assets of the same type at one or more locations. It is the unit ABS works with — not individual batteries.

fleet-nairobi-standard-4ah     ← all standard 4Ah batteries in Nairobi
fleet-nairobi-premium-8ah      ← all premium 8Ah batteries in Nairobi
fleet-mombasa-standard-4ah     ← all standard 4Ah batteries in Mombasa

When ABS says "allocate from this fleet at this location", ARM looks inside that fleet, picks the best unit available (highest charge, best health), and assigns it. ABS never names a specific battery until ARM echoes back with one.


How Assets Are Registered

Assets are registered in ARM, not in ABS. ARM gives each asset:

asset_id:     "bat-007"
fleet_id:     "fleet-nairobi-standard-4ah"    ← which group it belongs to
location_id:  "station-nbi-003"               ← where it currently sits
status:       "available"                     ← available / assigned / damaged / lost
charge_level: 98%
health_score: 0.94

ABS learns about a specific asset only at the moment of allocation — ARM echoes the concrete ID back after resolving a fleet request. That ID is then stored in ServiceState.current_asset inside the customer's plan.


How Customer Plans Restrict Asset Access

Access restrictions are built in at two levels — both set at plan template creation, before a customer even signs up.


Level 1 — Service Bundle (Battery Type Restriction)

A subscription plan is built from a ServiceBundle which lists one or more Services. Each Service maps to exactly one fleet:

type Service {
  asset_type:      String    // e.g. "standard-4ah-battery", "premium-8ah-battery"
  asset_reference: String    // the fleet ID this service maps to
  access_control:  JSON      // additional access rules
}

A Basic plan bundle contains only fleet-nairobi-standard-4ah. A Premium plan bundle contains both fleet-nairobi-standard-4ah AND fleet-nairobi-premium-8ah.

When the customer requests a swap, the agent runs W1 (see below) which reads the plan's bundle and extracts the fleet IDs. If the customer tries to access a fleet not in their bundle, W1 rejects it immediately — the request never reaches ARM.

Example bundles:

Plan Fleets in Bundle Battery Types
Basic Nairobi fleet-nairobi-standard-4ah Standard 4Ah only
Premium Nairobi fleet-nairobi-standard-4ah, fleet-nairobi-premium-8ah Standard + Premium 8Ah
Roaming National All country fleets All battery types

Level 2 — allowed_locations (Station Restriction)

The plan template carries an allowed_locations list — a whitelist of location IDs. The agent checks this at W2 before anything physical happens:

// In the agent's calculation engine:
isLocationAllowed: (locationId: string) => {
  return agentParams.allowed_locations?.includes(locationId) || false;
}

If the customer shows up at a station not in their allowed_locations: - Agent returns access_denied - No fleet request is sent to ARM - Station door stays locked - Customer gets notified: "this station is not in your plan"

Example location restrictions:

Plan allowed_locations
Basic Nairobi ['station-nbi-001', 'station-nbi-002', 'station-nbi-003']
Premium Nairobi ['station-nbi-001', ..., 'station-mbs-001', 'station-mbs-002']
Roaming National ['*'] or full national list

The W1–W4 Asset Flow — Every Single Swap

Every battery swap goes through five workflow steps. Steps W1–W4 are about asset resolution:

Customer arrives at station-nbi-003, requests swap
         │
         ▼
W1 — getRequiredAssetIds
     Agent reads the plan's ServiceBundle
     Extracts: fleet_id = "fleet-nairobi-standard-4ah"
     Checks: is station-nbi-003 in allowed_locations? → YES
     Output: { fleet_id, location_id }
     ← ABS does NOT pick a specific battery. Just the fleet category.
         │
         ▼
W2 — emitServiceIntentSignal
     ABS publishes intent over MQTT:
       Topic:   emit/ABS/BSS/plan-nairobi-001/service_access
       Payload: { fleet_id: "fleet-nairobi-standard-4ah",
                  location_id: "station-nbi-003",
                  plan_id: "plan-nairobi-001" }
     ARM receives and confirms: "yes, batteries available at that station"
         │
         ▼
W3 — bindCustomerToLocation
     ABS creates a session binding:
       customer cust-mwangi ↔ station-nbi-003 ↔ plan-nairobi-001
     This is the "checked in" moment for this transaction
         │
         ▼
W4 — sendAssetAllocationSignal
     ABS sends the allocation command to ARM:
       Topic:   cmd/ABS/EPS/station-nbi-003/allocate
       Payload: { fleet_id: "fleet-nairobi-standard-4ah",
                  plan_id: "plan-nairobi-001" }

     ARM processes:
       → Scans all batteries in fleet-nairobi-standard-4ah at station-nbi-003
       → Picks best available: bat-008 (98% charge, health 0.97)
       → Physically unlocks compartment
       → Echoes back to ABS:
           Topic:   echo/ABS/EPS/station-nbi-003/allocate
           Payload: { asset_id: "bat-008", status: "issued" }

     ABS receives echo:
       → ServiceState.current_asset updated: "bat-007" → "bat-008"
       → quota.used: 4 → 5
         │
         ▼
W5 — updateServiceStatesAndBilling
     Quota counter updated in service_account
     If quota not exhausted: fires SERVICE_REQUESTED into FSM (stays WAIT_BATTERY_SWAP)
     If quota now exhausted: fires QUOTA_EXHAUSTED into both FSMs → SUSPENDED

The InventoryAgent — Pre-check Before Contacting ARM

The InventoryAgent runs inside ABS as a guard before W4 is even triggered. It tracks stock at a high level:

InventoryAgentState {
  current_stock:     12    // batteries currently tracked as available
  total_assignments: 47    // lifetime batteries issued
  total_returns:     35    // lifetime batteries returned
  total_restocks:    3     // times stock was replenished
}

Before sending an allocation command to ARM, it checks canAssignItem(). If current_stock === 0 it fires INVENTORY_EMPTY and denies the swap without contacting ARM at all.

Signals the InventoryAgent emits:

Signal Condition What Happens
INVENTORY_EMPTY current_stock === 0 Swap denied — customer told no batteries available
INVENTORY_LOW Stock ≤ low_threshold (e.g. 3) Alert sent to operator to restock
RESTOCK_NEEDED Same as LOW Triggers replenishment workflow
HIGH_DEMAND >100 assignments AND stock <30% Suggests rebalancing from other stations

IoT Integration — Three Scenarios

All IoT device events flow through ABS first, never directly to ARM. ABS acts as the hub; ARM and IoT devices are spokes.

S1 — Automated Station (Self-Service)

Customer taps phone/card at station
  → IoT sensor fires event to ABS
    → ABS validates:
        authorized? ✓
        quota available? ✓
        location allowed? ✓
        stock available? ✓
      → ARM allocates battery
        → Station door unlocks
          → Customer takes battery
            → BATTERY_ISSUED fires → service_state stays WAIT_BATTERY_SWAP

S2 — Manual Personnel (Field Operations)

Station attendant scans a battery barcode with their device
  → Reports condition: OK / damaged / needs replacement
    → ABS processes the personnel action
      → ARM updates asset status (e.g. marks bat-042 as damaged)
        → If customer was holding that battery:
            ABS triggers swap for replacement battery
            Customer notified of the swap

S3 — Asset Tracking and Recovery (Lost/Stolen)

GPS tracking signal goes silent for bat-008
OR customer reports phone stolen with battery inside
  │
  ▼
ABS fires: BATTERY_LOST
  service_state: WAIT_BATTERY_SWAP → (settlement state)
  ARM marks bat-008 as lost, initiates GPS recovery tracking
  │
  ├── If battery found later:
  │     ABS fires: BATTERY_FOUND
  │     Customer given options:
  │       A) New battery issued → back to WAIT_BATTERY_ISSUE
  │       B) Terminate plan → COMPLETE
  │
  └── If not found:
        Settlement workflow:
          Customer pays replacement fee
          ARM writes off bat-008 as lost asset
          service_state → COMPLETE
          payment_state → COMPLETE
          Plan at rest

ServiceState — What ABS Tracks Per Asset

For each service in a customer's plan, ABS tracks a ServiceState record:

ServiceState {
  service_id:    "battery-swap-daily"
  current_asset: "bat-008"      physical battery ID currently held by customer (ARM-assigned)
  used:          5              swaps consumed this billing cycle
  quota:         30             max swaps allowed this cycle
}

current_asset is the only place ABS stores a concrete battery ID. Everything else works with fleet IDs. When ARM echoes a different asset ID (e.g. after a swap), ABS simply updates this field.


ServiceConfiguration — What the Plan Template Controls

Each plan template sets the service rules at configuration time:

ServiceConfiguration {
  service_id:        "battery-swap-daily"
  initial_quota:     30           quota at plan start
  max_quota:         60           maximum quota ever granted
  rate_limit_per_day: 2           max swaps per calendar day
  auto_renewal:      true         auto-renew quota at cycle end
  overage_allowed:   false        can customer exceed quota?
  overage_rate:      null         cost per extra swap if allowed
}

rate_limit_per_day is a per-day cap independent of the monthly quota. Even if a customer has 25 swaps remaining in their monthly quota, they cannot do more than 2 in a single day. The agent enforces this.


Subscription Tiers — BASIC vs PREMIUM

From bss-parameters.json:

"subscription_tiers": ["BASIC", "PREMIUM"]

"tier_multipliers": {
  "BASIC":   { "quota_multiplier": 1.0, "fee_multiplier": 1.0 },
  "PREMIUM": { "quota_multiplier": 2.0, "fee_multiplier": 1.5 }
}

A PREMIUM customer gets 2× the quota (60 swaps vs 30) at 1.5× the fee. The same underlying fleet and FSM logic applies — only the quota number changes.


Complete Asset Lifecycle — Birth to Rest

REGISTRATION (in ARM)
  ARM registers bat-007
  fleet: fleet-nairobi-standard-4ah
  location: station-nbi-003
  status: available
         │
         ▼
ALLOCATION (W4 — first issue)
  ABS requests from fleet at location
  ARM selects bat-007 (best available)
  ARM status: available → assigned
  ABS ServiceState.current_asset: null → "bat-007"
         │
         ▼
IN SERVICE (daily swap loop)
  Each swap: bat-007 returned to station
             bat-008 issued to customer
  ARM status: bat-007 → available, bat-008 → assigned
  ABS ServiceState.current_asset: "bat-007" → "bat-008"
  ABS quota.used: increments each swap
         │
         ▼ (three possible endings)
         │
         ├── NORMAL RETURN (subscription ends)
         │   Customer returns last battery at station
         │   ARM status: bat-008 → available
         │   ABS ServiceState.current_asset: "bat-008" → null
         │   service_state: WAIT_BATTERY_RETURN → COMPLETE
         │   bat-008 back in pool, available for next customer
         │
         ├── GRACE PERIOD ENFORCEMENT
         │   Customer did not renew, grace period expired
         │   ARM notified: recover bat-008 from cust-mwangi
         │   Field personnel or customer returns battery
         │   Same as normal return path above
         │
         └── LOST / STOLEN
             ARM marks bat-008 as lost
             GPS recovery initiated
             If not recovered:
               ARM writes off bat-008 (removed from available inventory)
               ABS settlement complete
               plan_state: COMPLETE

Key Decisions Owned by ABS vs ARM

Decision Owner How
Is this customer allowed at this station? ABS allowed_locations check in agent
Is this customer allowed this battery type? ABS ServiceBundle fleet membership check
Does this customer have quota remaining? ABS quota.used < quota.total check
Is there stock available at all? ABS InventoryAgent.canAssignItem()
Which specific battery to assign? ARM Best available from fleet at location
Is a battery damaged/healthy? ARM IoT sensors + health scoring
Where is battery X right now? ARM GPS + station sensor data
Is a battery lost? Both ABS FSM + ARM tracking