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 |