Skip to content

BSS Agent Enhancements - Technical Specification

Purpose: Define technical requirements for enhancing BSS Agent v2 to support the unified attendant workflow.

Status: 📝 Specification
Target Implementation: Phase 1 (2 weeks)


Overview

This specification outlines enhancements to the existing BSS Agent v2 to support:

  1. Electricity quota tracking - Track net electricity delivered during swaps
  2. Unified service completion - Single action to complete swap transaction
  3. Enhanced equipment tracking - Include SOC% data in battery identification
  4. Payment event publishing - Emit payment events for Odoo integration

Enhancement 1: Electricity Quota Tracking

Objective

Enable tracking of electricity (kWh) delivered during battery swaps, separate from swap-count quota.

Current State

The serviceStates[] array supports multiple service quotas:

serviceStates: [
  {
    service_id: "svc-battery-fleet-kenya-premium",
    used: 6,           // Swap-Count usage
    quota: 10,
    current_asset: "BAT-12345"
  },
  {
    service_id: "svc-swap-network-kenya",
    used: 0,
    quota: 100000000,  // Infinity quota
    current_asset: null
  }
]

Proposed Enhancement

Add a third service for electricity tracking:

serviceStates: [
  {
    service_id: "svc-battery-fleet-kenya-premium",
    used: 6,
    quota: 10,
    current_asset: "BAT-12345"
  },
  {
    service_id: "svc-electricity-fuel-kenya",  // NEW
    used: 344.5,        // kWh consumed (decimal allowed)
    quota: 400,         // kWh limit
    current_asset: null // No asset association
  },
  {
    service_id: "svc-swap-network-kenya",
    used: 0,
    quota: 100000000,
    current_asset: null
  }
]

Implementation Requirements

1.1 Template Configuration

File: models/battery-swap/setup-data/bss-template-*.json

Add electricity service configuration:

{
  "service_configurations": [
    {
      "service_id": "svc-battery-fleet-kenya-premium",
      "quota": 10,
      "tracks_asset": true
    },
    {
      "service_id": "svc-electricity-fuel-kenya",
      "quota": 400,
      "tracks_asset": false,
      "unit": "kWh",
      "decimal_precision": 1
    },
    {
      "service_id": "svc-swap-network-kenya",
      "quota": 100000000,
      "tracks_asset": false
    }
  ]
}

1.2 Agent Function: calculateElectricityUsage()

Location: docs/models/bss/bss-agent-v2.ts (hypothetical - actual location TBD)

Purpose: Calculate net electricity delivered during swap.

Input:

interface ElectricityUsageInput {
  incoming_battery_id: string;
  incoming_kwh: number;          // kWh read directly from battery BMS
  outgoing_battery_id: string;
  outgoing_kwh: number;          // kWh read directly from battery BMS
}

Logic:

function calculateElectricityUsage(input: ElectricityUsageInput): number {
  // Direct kWh values from battery scanner (no conversion needed)
  const net_kwh = input.outgoing_kwh - input.incoming_kwh;

  // Round to 1 decimal place
  return Math.round(net_kwh * 10) / 10;
}

// Example:
// Input: { incoming_kwh: 4.8, outgoing_kwh: 30.4 }
// Output: 25.6 kWh

Output: number (kWh delivered, rounded to 1 decimal)

Note: Battery scanner reads kWh directly from BMS - no SOC% conversion required.

1.3 Integration with equipmentCheckout()

Enhancement: Modify existing equipmentCheckout() function to: 1. Accept incoming SOC% and outgoing SOC% as parameters 2. Call calculateElectricityUsage() to compute net kWh 3. Update svc-electricity-fuel-* quota in serviceStates[]

Updated Function Signature:

interface EquipmentCheckoutInput {
  action: "EQUIPMENT_CHECKOUT";
  replacement_equipment_id: string;

  // NEW FIELDS
  incoming_equipment_id?: string;      // Optional for first visit
  incoming_kwh?: number;               // kWh read from battery scanner (optional for first visit)
  outgoing_kwh: number;                // kWh read from battery scanner (required)
}

Example Request:

{
  "action": "EQUIPMENT_CHECKOUT",
  "replacement_equipment_id": "BAT-67890",
  "incoming_equipment_id": "BAT-12345",
  "incoming_kwh": 4.8,
  "outgoing_kwh": 30.4
}

Quota Check Logic (performed during checkout):

// 1. Calculate net electricity
const net_kwh = outgoing_kwh - (incoming_kwh || 0);

// 2. Get current electricity quota
const electricityService = serviceStates.find(s => s.service_id === 'svc-electricity-fuel-*');
const remaining_quota = electricityService.quota - electricityService.used;

// 3. Check if sufficient
if (remaining_quota < net_kwh) {
  // Quota exhausted - require top-up
  return { signals: ['QUOTA_EXHAUSTED'], metadata: { deficit_kwh: net_kwh - remaining_quota } };
} else {
  // Quota sufficient - proceed
  return { signals: ['QUOTA_AVAILABLE'], metadata: { net_kwh } };
}

Example Response (Quota Sufficient):

{
  "signals": ["QUOTA_AVAILABLE", "EQUIPMENT_CHECKOUT_SUCCESS"],
  "metadata": {
    "outgoing_battery_id": "BAT-67890",
    "incoming_battery_id": "BAT-12345",
    "electricity_calculation": {
      "incoming_kwh": 4.8,
      "outgoing_kwh": 30.4,
      "net_delivered_kwh": 25.6
    },
    "quota_check": {
      "remaining_before": 55.5,
      "net_required": 25.6,
      "remaining_after": 29.9,
      "status": "sufficient"
    },
    "quota_updates": [
      {
        "service_id": "svc-battery-fleet-kenya-premium",
        "used_before": 6,
        "used_after": 7,
        "increment": 1
      },
      {
        "service_id": "svc-electricity-fuel-kenya",
        "used_before": 344.5,
        "used_after": 370.1,
        "increment": 25.6
      }
    ]
  },
  "fsmInputs": [
    {
      "cycle": "service_cycle",
      "input": "BATTERY_ISSUED"
    }
  ]
}

Example Response (Quota Exhausted):

{
  "signals": ["QUOTA_EXHAUSTED"],
  "metadata": {
    "outgoing_battery_id": "BAT-67890",
    "incoming_battery_id": "BAT-12345",
    "electricity_calculation": {
      "incoming_kwh": 4.8,
      "outgoing_kwh": 30.4,
      "net_delivered_kwh": 25.6
    },
    "quota_check": {
      "remaining_before": 10.0,
      "net_required": 25.6,
      "deficit_kwh": 15.6,
      "status": "exhausted"
    },
    "topup_required": {
      "amount_kwh": 15.6,
      "estimated_cost_usd": 12.48  // Example: $0.80/kWh
    }
  },
  "fsmInputs": [
    {
      "cycle": "service_cycle",
      "input": "QUOTA_EXHAUSTED"
    }
  ]
}


Enhancement 2: Unified Service Completion

Objective

Provide a single SERVICE_COMPLETE action that: 1. Publishes Service Event signal 2. Publishes Payment Event signal (if payment occurred) 3. Executes FSM transitions 4. Updates ServicePlan quotas 5. Generates transaction receipt

Current State

Service completion requires multiple sequential calls: 1. equipmentCheckin() - Check in returned battery 2. equipmentCheckout() - Check out new battery 3. processPayment() - Process payment (if needed) 4. reportAttendantActivity() - Report completion

Proposed Enhancement

Create a new completeService() action that orchestrates all completion steps.

2.1 Agent Function: completeService()

Purpose: Single action to complete entire swap transaction.

Input:

interface CompleteServiceInput {
  action: "COMPLETE_SERVICE";

  // Battery swap details
  incoming_battery_id?: string;      // null for first visit
  incoming_kwh?: number;             // kWh read from scanner (null for first visit)
  outgoing_battery_id: string;
  outgoing_kwh: number;              // kWh read from scanner

  // Payment details (if payment occurred)
  payment_occurred: boolean;
  payment_amount?: number;
  payment_receipt_id?: string;
  payment_method?: string;

  // Attendant details
  attendant_id: string;
  attendant_station: string;

  // Transaction metadata
  transaction_timestamp: string;
  correlation_id: string;
}

Logic Flow:

async function completeService(ctx: ExecutionContext, input: CompleteServiceInput): Promise<AgentResult> {
  // Step 1: Validate all prerequisites
  const validations = await validateServiceCompletion(ctx, input);
  if (!validations.passed) {
    return { signals: ['SERVICE_COMPLETION_FAILED'], metadata: validations };
  }

  // Step 2: Calculate quota updates
  const quotaUpdates = calculateQuotaUpdates(input);

  // Step 3: Create Service Event (for history persistence)
  const serviceEvent = createServiceEvent(ctx, input, quotaUpdates);

  // Step 4: Create Payment Event (if payment occurred, for history persistence)
  const paymentEvent = input.payment_occurred 
    ? createPaymentEvent(ctx, input) 
    : null;

  // Step 5: Execute FSM transitions
  const fsmInputs = [
    { cycle: 'service_cycle', input: 'BATTERY_ISSUED' }
  ];

  if (input.payment_occurred) {
    fsmInputs.push({ cycle: 'payment_cycle', input: 'PAYMENT_RECEIVED' });
  }

  // Step 6: Update ServicePlan
  await updateServicePlan(ctx, {
    quotaUpdates,
    currentAsset: input.outgoing_battery_id
  });

  // Step 7: Persist events to ABS database (source of truth for history)
  await persistServiceEvent(serviceEvent);
  if (paymentEvent) {
    await persistPaymentEvent(paymentEvent);
  }

  // Step 8: Publish events to MQTT (for Odoo integration if needed)
  await publishEvent('SERVICE_COMPLETED', serviceEvent);
  if (paymentEvent) {
    await publishEvent('PAYMENT_RECEIVED', paymentEvent);
  }

  // Step 9: Generate receipt (combines service + payment events)
  const receipt = generateReceipt(ctx, input, quotaUpdates);

  return {
    signals: ['SERVICE_COMPLETED_SUCCESS'],
    metadata: {
      transaction_id: generateTransactionId(),
      quota_updates: quotaUpdates,
      service_event: serviceEvent,
      payment_event: paymentEvent,
      receipt,
      fsm_transitions: fsmInputs
    },
    fsmInputs
  };
}
}

Example Request:

{
  "action": "COMPLETE_SERVICE",
  "incoming_battery_id": "BAT-12345",
  "incoming_kwh": 4.8,
  "outgoing_battery_id": "BAT-67890",
  "outgoing_kwh": 30.4,
  "payment_occurred": true,
  "payment_amount": 15.00,
  "payment_receipt_id": "PAY-78910",
  "payment_method": "MOBILE_MONEY",
  "attendant_id": "ATT-001",
  "attendant_station": "STATION_XYZ",
  "transaction_timestamp": "2025-01-15T10:30:00Z",
  "correlation_id": "TXN-12345"
}

Example Response:

{
  "signals": ["SERVICE_COMPLETED_SUCCESS"],
  "metadata": {
    "transaction_id": "TXN-12345",
    "quota_updates": [
      {
        "service_id": "svc-battery-fleet-kenya-premium",
        "used_before": 6,
        "used_after": 7
      },
      {
        "service_id": "svc-electricity-fuel-kenya",
        "used_before": 344.5,
        "used_after": 370.1
      }
    ],
    "receipt": {
      "transaction_id": "TXN-12345",
      "timestamp": "2025-01-15T10:30:00Z",
      "customer_id": "CUST-001",
      "batteries_swapped": {
        "returned": "BAT-12345",
        "issued": "BAT-67890"
      },
      "electricity_delivered_kwh": 25.6,
      "payment": {
        "amount": 15.00,
        "receipt_id": "PAY-78910",
        "method": "MOBILE_MONEY"
      },
      "quotas_remaining": {
        "swap_count": "3 of 10",
        "electricity_fuel": "29.9 kWh of 400 kWh"
      }
    },
    "fsm_transitions": [
      { "cycle": "service_cycle", "input": "BATTERY_ISSUED" },
      { "cycle": "payment_cycle", "input": "PAYMENT_RECEIVED" }
    ]
  },
  "fsmInputs": [
    { "cycle": "service_cycle", "input": "BATTERY_ISSUED" },
    { "cycle": "payment_cycle", "input": "PAYMENT_RECEIVED" }
  ]
}


Enhancement 3: Enhanced Equipment Tracking

Objective

Include kWh data in battery identification responses.

Current State

identifyReturnedEquipment() and identifyOutgoingEquipment() return basic battery details but not energy data.

Proposed Enhancement

Integrate with battery scanner/ARM microservice to retrieve kWh data directly from BMS.

3.1 Battery Scanner Integration

Method: Direct kWh reading from battery BMS via scanner device.

Scanner Interface (hypothetical):

interface BatteryTelemetry {
  asset_id: string;
  kwh_remaining: number;      // Direct kWh reading from BMS
  voltage: number;             // Optional
  temperature_celsius: number; // Optional
  timestamp: string;
}

ARM API Endpoint (for logging/verification):

GET /api/v1/assets/{asset_id}/telemetry/latest

Response:

{
  "asset_id": "BAT-12345",
  "telemetry": {
    "kwh_remaining": 4.8,
    "voltage": 51.2,
    "temperature_celsius": 25,
    "timestamp": "2025-01-15T10:15:00Z"
  }
}

Note: Scanner reads kWh directly from battery - no conversion or estimation needed.

3.2 Agent Function Updates

Function: identifyReturnedEquipment()

Enhancement: After verifying battery ownership, read kWh from scanner:

async function identifyReturnedEquipment(ctx, input) {
  // Existing logic: Verify ownership
  const ownership = await verifyBatteryOwnership(ctx, input.equipment_id);

  // NEW: Read kWh from battery scanner
  const telemetry = await readBatteryTelemetry(input.equipment_id);

  return {
    signals: ['EQUIPMENT_BELONGS_TO_USER'],
    metadata: {
      equipment_id: input.equipment_id,
      ownership_verified: true,
      kwh_remaining: telemetry.kwh_remaining,  // NEW - Direct kWh reading
      read_timestamp: telemetry.timestamp,      // NEW
      battery_voltage: telemetry.voltage,       // NEW (optional)
      battery_temperature: telemetry.temperature_celsius  // NEW (optional)
    }
  };
}

Updated Response:

{
  "signals": ["EQUIPMENT_BELONGS_TO_USER"],
  "metadata": {
    "equipment_id": "BAT-12345",
    "ownership_verified": true,
    "kwh_remaining": 4.8,
    "read_timestamp": "2025-01-15T10:15:00Z",
    "battery_voltage": 51.2,
    "battery_temperature": 25
  }
}


Enhancement 4: Payment Event Publishing

Objective

Publish standardized payment events when top-up payments are received.

Current State

Payment confirmation is handled via Odoo callback but not explicitly published as an event.

Proposed Enhancement

Create a dedicated PAYMENT_RECEIVED event that is published to MQTT when payment is confirmed.

4.1 Event Structure

Event Type: PAYMENT_RECEIVED

Payload:

{
  "event_type": "PAYMENT_RECEIVED",
  "event_id": "EVT-PAY-78910",
  "timestamp": "2025-01-15T10:25:00Z",
  "plan_id": "bss-plan-weekly-freedom-nairobi-v2-plan1",
  "customer_id": "CUST-001",
  "payment_data": {
    "amount": 15.00,
    "currency": "USD",
    "payment_method": "MOBILE_MONEY",
    "odoo_receipt_id": "PAY-78910",
    "service_description": "Battery Swap + Electricity Top-up",
    "merchant_station": "STATION_XYZ",
    "payment_timestamp": "2025-01-15T10:24:30Z"
  },
  "service_context": {
    "transaction_id": "TXN-12345",
    "attendant_id": "ATT-001",
    "correlation_id": "att-payment-001"
  },
  "fsm_inputs": [
    {
      "cycle": "payment_cycle",
      "input": "PAYMENT_RECEIVED"
    }
  ]
}

MQTT Topic: event/abs/payment/plan/{plan_id}/payment_received

4.2 Integration Points

Trigger 1: Odoo payment confirmation callback

Odoo → ABS Platform → BSS Agent → Publish PAYMENT_RECEIVED event

Trigger 2: completeService() action (if payment occurred)

Attendant App → BSS Agent → completeService() → Publish PAYMENT_RECEIVED event


Testing Requirements

Unit Tests

  • [ ] calculateElectricityUsage() - Various kWh combinations (including first visit with incoming_kwh=0)
  • [ ] completeService() - All workflow paths (with/without payment)
  • [ ] identifyReturnedEquipment() - kWh data retrieval from scanner
  • [ ] Quota update logic - Electricity quota increments and exhaustion detection

Integration Tests

  • [ ] End-to-end workflow: Customer scan → Battery swap → Service complete
  • [ ] Payment flow: Quota exhaustion → Payment → Service resume
  • [ ] First visit workflow: Skip battery return → Issue battery
  • [ ] Error scenarios: Wrong battery, damaged battery, payment failure

Performance Tests

  • [ ] MQTT message latency (< 500ms)
  • [ ] Agent calculation time (< 1s)
  • [ ] Database update time (< 2s)
  • [ ] Receipt generation time (< 500ms)

Migration Plan

Phase 1: Backend Changes (Week 1)

  • [ ] Add electricity service to templates
  • [ ] Implement calculateElectricityUsage()
  • [ ] Enhance equipmentCheckout() with SOC tracking
  • [ ] Add ARM telemetry integration

Phase 2: Agent Enhancements (Week 1)

  • [ ] Implement completeService() action
  • [ ] Add payment event publishing
  • [ ] Update FSM transition logic
  • [ ] Implement receipt generation

Phase 3: Testing (Week 2)

  • [ ] Unit tests for new functions
  • [ ] Integration tests for workflows
  • [ ] Performance testing
  • [ ] UAT with attendants

Phase 4: Deployment (Week 2)

  • [ ] Deploy to staging environment
  • [ ] Smoke testing
  • [ ] Deploy to production
  • [ ] Monitor for issues

API Documentation Updates

New MQTT Topics

Topic Pattern Description QoS Retained
call/uxi/attendant/plan/{plan_id}/complete_service Complete swap transaction 0 No
rtrn/abs/attendant/plan/{plan_id}/complete_service_response Immediate acknowledgment 0 No
event/abs/payment/plan/{plan_id}/payment_received Payment event broadcast 0 No
event/abs/service/plan/{plan_id}/service_completed Service event broadcast 0 No

Updated Agent Actions

Action Status Changes
IDENTIFY_CUSTOMER Unchanged No changes
IDENTIFY_RETURNED_EQUIPMENT Enhanced + kWh data from scanner
IDENTIFY_OUTGOING_EQUIPMENT Enhanced + kWh data from scanner
EQUIPMENT_CHECKOUT Enhanced + kWh tracking, quota check, electricity quota update
COMPLETE_SERVICE NEW Orchestrates service completion

Configuration Changes

Template Configuration

File: models/battery-swap/setup-data/bss-template-weekly-freedom-nairobi-v2.json

Add:

{
  "service_configurations": [
    {
      "service_id": "svc-electricity-fuel-kenya",
      "quota": 400,
      "tracks_asset": false,
      "unit": "kWh",
      "decimal_precision": 1
    }
  ]
}

Agent Parameters

File: models/battery-swap/setup-data/bss-agent-params-v2.json (hypothetical)

Add:

{
  "electricity_tracking": {
    "enabled": true,
    "default_battery_capacity_kwh": 32,
    "decimal_precision": 1,
    "soc_telemetry_timeout_seconds": 10
  },
  "service_completion": {
    "require_payment_confirmation": true,
    "receipt_generation_enabled": true,
    "event_publishing_enabled": true
  }
}


Rollback Plan

If issues arise post-deployment:

  1. Disable Electricity Tracking:
  2. Set electricity_tracking.enabled = false in agent params
  3. System reverts to swap-count quota only

  4. Disable Unified Completion:

  5. Remove COMPLETE_SERVICE action from available actions
  6. Revert to sequential completion steps

  7. Revert Template Changes:

  8. Remove electricity service from templates
  9. Service plans continue with existing quota structure

  10. Database Rollback:

  11. Restore serviceStates[] to pre-deployment state
  12. Electricity usage data is lost (acceptable for pilot phase)

Appendix: Code Examples

Example: Electricity Quota Update

// Before
serviceStates: [
  { service_id: "svc-battery-fleet", used: 6, quota: 10 }
]

// After swap (25.6 kWh delivered)
serviceStates: [
  { service_id: "svc-battery-fleet", used: 7, quota: 10 },
  { service_id: "svc-electricity-fuel", used: 25.6, quota: 400 }
]

Example: First Visit (No Incoming Battery)

// Request
{
  "action": "EQUIPMENT_CHECKOUT",
  "replacement_equipment_id": "BAT-67890",
  "incoming_equipment_id": null,       // No incoming battery
  "incoming_soc_percent": null,
  "outgoing_soc_percent": 95
}

// Electricity calculation
incoming_kwh = 0 (no incoming battery)
outgoing_kwh = 30.4 (95% of 32 kWh)
net_delivered_kwh = 30.4 (full battery delivered)

Conclusion

These enhancements enable the unified attendant workflow while maintaining backward compatibility with existing systems. All changes are additive and can be rolled back if needed.

Next Steps: 1. Review and approve this specification 2. Begin Phase 1 implementation 3. Proceed with UXI mock-up development in parallel