mirror of
https://github.com/andwati/telecom-platform.git
synced 2026-10-18 11:02:54 +00:00
@@ -118,7 +118,6 @@ pnpm dev
|
||||
|
||||
## Documentation
|
||||
|
||||
- **[Complete TODO List](./todos.md)**: Comprehensive development roadmap
|
||||
- **[API Documentation](./docs/api-spec.yaml)**: OpenAPI 3.0 specification
|
||||
- **[Architecture Guide](./docs/architecture.md)**: System design and data flows
|
||||
- **[Deployment Guide](./docs/deployment.md)**: Kubernetes and Docker setup
|
||||
|
||||
@@ -0,0 +1,335 @@
|
||||
# Telecom Platform Monitoring Stack
|
||||
|
||||
This directory contains the complete monitoring and observability setup for the Telecom Platform, including Prometheus metrics collection, Grafana dashboards, and InfluxDB time-series storage.
|
||||
|
||||
## Architecture
|
||||
|
||||
```
|
||||
Telecom Platform Services
|
||||
|
|
||||
| Metrics (Prometheus format)
|
||||
v
|
||||
+-----------------+
|
||||
| Prometheus | <- Scrapes metrics from all services
|
||||
+-----------------+
|
||||
|
|
||||
| Query API
|
||||
v
|
||||
+-----------------+
|
||||
| Grafana | <- Visualization and alerting
|
||||
+-----------------+
|
||||
|
|
||||
| Long-term storage
|
||||
v
|
||||
+-----------------+
|
||||
| InfluxDB | <- Time-series data storage
|
||||
+-----------------+
|
||||
```
|
||||
|
||||
## Components
|
||||
|
||||
### 1. Prometheus Metrics Exporter
|
||||
|
||||
**Location**: `apps/api-server/internal/metrics/prometheus.go`
|
||||
|
||||
**Features**:
|
||||
- HTTP request metrics (rate, duration, status codes)
|
||||
- Subscriber metrics (total, active, suspended)
|
||||
- Usage metrics (data, voice, SMS)
|
||||
- Charging metrics (credit balance, transactions)
|
||||
- System metrics (sessions, uptime, database connections)
|
||||
- Chaos engineering metrics
|
||||
- eBPF packet gateway metrics
|
||||
|
||||
**Key Metrics**:
|
||||
- `telecom_http_requests_total` - Total HTTP requests
|
||||
- `telecom_subscribers_total` - Total subscriber count
|
||||
- `telecom_data_usage_bytes_total` - Data usage in bytes
|
||||
- `telecom_credit_balance` - Subscriber credit balances
|
||||
- `telecom_active_sessions` - Active session count
|
||||
- `telecom_packets_processed_total` - eBPF packet processing
|
||||
|
||||
### 2. Advanced Time-Series Storage
|
||||
|
||||
**Location**: `apps/api-server/internal/metrics/timeseries.go`
|
||||
|
||||
**Features**:
|
||||
- InfluxDB integration for long-term storage
|
||||
- High-resolution usage metrics
|
||||
- Historical data analysis
|
||||
- Complex queries and aggregations
|
||||
- Top user analytics
|
||||
|
||||
**Data Types**:
|
||||
- Usage metrics (data up/down, voice, SMS)
|
||||
- System metrics (CPU, memory, network)
|
||||
- Charging metrics (transactions, balances)
|
||||
- Packet processing metrics
|
||||
|
||||
### 3. Grafana Dashboards
|
||||
|
||||
**Location**: `monitoring/grafana/dashboards/`
|
||||
|
||||
#### Dashboard: Telecom Platform Overview
|
||||
- Total subscribers and active sessions
|
||||
- Data usage rates
|
||||
- HTTP request rates
|
||||
- Subscriber status breakdown
|
||||
- Payment transaction rates
|
||||
|
||||
#### Dashboard: Telecom Subscriber Metrics
|
||||
- Top 10 data users
|
||||
- Subscriber credit balances
|
||||
- Low balance alerts
|
||||
- Data usage distribution
|
||||
- Voice & SMS usage rates
|
||||
|
||||
### 4. Docker Compose Setup
|
||||
|
||||
**Location**: `monitoring/docker-compose.yml`
|
||||
|
||||
**Services**:
|
||||
- **Prometheus** (port 9090) - Metrics collection and storage
|
||||
- **Grafana** (port 3001) - Visualization and dashboards
|
||||
- **InfluxDB** (port 8086) - Time-series data storage
|
||||
- **Redis** (port 6379) - Caching and session storage
|
||||
- **Node Exporter** (port 9100) - System metrics
|
||||
- **Alertmanager** (port 9093) - Alert management
|
||||
|
||||
## Quick Start
|
||||
|
||||
### 1. Start Monitoring Stack
|
||||
|
||||
```bash
|
||||
cd monitoring
|
||||
docker-compose up -d
|
||||
```
|
||||
|
||||
### 2. Access Services
|
||||
|
||||
- **Prometheus**: http://localhost:9090
|
||||
- **Grafana**: http://localhost:3001 (admin/admin)
|
||||
- **InfluxDB**: http://localhost:8086
|
||||
|
||||
### 3. Start API Server with Metrics
|
||||
|
||||
```bash
|
||||
cd apps/api-server
|
||||
go run cmd/server.go
|
||||
```
|
||||
|
||||
The API server will:
|
||||
- Expose metrics on `http://localhost:9090/metrics`
|
||||
- Store time-series data in InfluxDB (if configured)
|
||||
- Update Prometheus metrics every 30 seconds
|
||||
|
||||
## Configuration
|
||||
|
||||
### Environment Variables
|
||||
|
||||
```bash
|
||||
# API Server
|
||||
METRICS_PORT=9090
|
||||
|
||||
# InfluxDB (optional)
|
||||
INFLUXDB_URL=http://localhost:8086
|
||||
INFLUXDB_TOKEN=your-token
|
||||
INFLUXDB_ORG=telecom
|
||||
INFLUXDB_BUCKET=telecom
|
||||
```
|
||||
|
||||
### Prometheus Configuration
|
||||
|
||||
**Location**: `monitoring/prometheus/prometheus.yml`
|
||||
|
||||
Key scrape targets:
|
||||
- `telecom-api-server` - Main API metrics
|
||||
- `telecom-charging-engine` - Rust charging engine metrics
|
||||
- `telecom-packet-gateway` - eBPF packet gateway metrics
|
||||
|
||||
### Grafana Data Sources
|
||||
|
||||
**Location**: `monitoring/grafana/provisioning/datasources/datasources.yml`
|
||||
|
||||
- **Prometheus** - Primary metrics source
|
||||
- **InfluxDB** - Time-series analytics
|
||||
|
||||
## Metrics Collection
|
||||
|
||||
### Automatic Collection
|
||||
|
||||
The metrics collector automatically gathers:
|
||||
1. **HTTP Metrics** - Request rate, duration, status codes
|
||||
2. **Business Metrics** - Subscriber counts, usage data
|
||||
3. **System Metrics** - Database connections, Redis operations
|
||||
4. **Chaos Metrics** - Experiment results and status
|
||||
|
||||
### Custom Metrics
|
||||
|
||||
Add custom metrics in your service code:
|
||||
|
||||
```go
|
||||
// Record custom usage metrics
|
||||
metricsCollector.RecordDataUsage("subscriber123", "up", 1024)
|
||||
metricsCollector.UpdateCreditBalance("subscriber123", 25.50)
|
||||
|
||||
// Record chaos experiment
|
||||
metricsCollector.RecordChaosExperiment("latency", "completed")
|
||||
```
|
||||
|
||||
### Time-Series Storage
|
||||
|
||||
Store detailed metrics in InfluxDB:
|
||||
|
||||
```go
|
||||
usageMetrics := metrics.UsageMetrics{
|
||||
SubscriberID: "subscriber123",
|
||||
IMSI: "208930000000001",
|
||||
DataUp: 1024 * 1024, // 1MB
|
||||
DataDown: 2 * 1024 * 1024, // 2MB
|
||||
VoiceSeconds: 300,
|
||||
SMSCount: 5,
|
||||
Timestamp: time.Now(),
|
||||
}
|
||||
|
||||
err := timeSeriesStorage.StoreUsageMetrics(ctx, usageMetrics)
|
||||
```
|
||||
|
||||
## Alerting
|
||||
|
||||
### Prometheus Alert Rules
|
||||
|
||||
Create alert rules in `monitoring/prometheus/alert_rules.yml`:
|
||||
|
||||
```yaml
|
||||
groups:
|
||||
- name: telecom_alerts
|
||||
rules:
|
||||
- alert: HighErrorRate
|
||||
expr: rate(telecom_http_requests_total{status_code=~"5.."}[5m]) > 0.1
|
||||
for: 5m
|
||||
labels:
|
||||
severity: critical
|
||||
annotations:
|
||||
summary: "High error rate detected"
|
||||
|
||||
- alert: LowCreditBalance
|
||||
expr: telecom_credit_balance < 5
|
||||
for: 2m
|
||||
labels:
|
||||
severity: warning
|
||||
annotations:
|
||||
summary: "Subscriber has low credit balance"
|
||||
```
|
||||
|
||||
### Grafana Alerting
|
||||
|
||||
Configure alerts in Grafana dashboards:
|
||||
- Low balance alerts
|
||||
- High error rate alerts
|
||||
- System resource alerts
|
||||
- Usage threshold alerts
|
||||
|
||||
## Advanced Features
|
||||
|
||||
### 1. Distributed Tracing
|
||||
|
||||
Integration with Jaeger for request tracing:
|
||||
- Trace HTTP requests across services
|
||||
- Track GraphQL query performance
|
||||
- Monitor database query performance
|
||||
|
||||
### 2. Log Aggregation
|
||||
|
||||
ELK stack integration for log aggregation:
|
||||
- Centralized log collection
|
||||
- Log parsing and indexing
|
||||
- Log-based alerting
|
||||
|
||||
### 3. Business Intelligence
|
||||
|
||||
Advanced analytics with InfluxDB:
|
||||
- Customer usage patterns
|
||||
- Revenue analytics
|
||||
- Network performance trends
|
||||
- Capacity planning
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Common Issues
|
||||
|
||||
1. **Metrics not appearing in Prometheus**
|
||||
- Check if metrics endpoint is accessible: `curl http://localhost:9090/metrics`
|
||||
- Verify Prometheus configuration targets
|
||||
- Check network connectivity
|
||||
|
||||
2. **Grafana dashboards not loading**
|
||||
- Verify Grafana can reach Prometheus: `curl http://prometheus:9090/api/v1/query?query=up`
|
||||
- Check data source configuration
|
||||
- Verify dashboard JSON syntax
|
||||
|
||||
3. **InfluxDB connection issues**
|
||||
- Verify InfluxDB is running: `curl http://localhost:8086/ping`
|
||||
- Check authentication token
|
||||
- Verify bucket and organization names
|
||||
|
||||
### Debug Commands
|
||||
|
||||
```bash
|
||||
# Check Prometheus targets
|
||||
curl http://localhost:9090/api/v1/targets
|
||||
|
||||
# Query specific metric
|
||||
curl "http://localhost:9090/api/v1/query?query=telecom_subscribers_total"
|
||||
|
||||
# Check InfluxDB health
|
||||
curl http://localhost:8086/health
|
||||
|
||||
# View Grafana data sources
|
||||
curl -u admin:admin http://localhost:3001/api/datasources
|
||||
```
|
||||
|
||||
## Performance Considerations
|
||||
|
||||
### Metrics Collection Overhead
|
||||
- Prometheus scraping adds minimal overhead (~1-2ms per request)
|
||||
- InfluxDB writes are batched for efficiency
|
||||
- Time-series storage is optional and can be disabled
|
||||
|
||||
### Storage Requirements
|
||||
- Prometheus: ~1-2GB per month for medium load
|
||||
- InfluxDB: Depends on retention period and data volume
|
||||
- Grafana: Minimal storage for dashboards
|
||||
|
||||
### Scaling
|
||||
- Prometheus can handle ~10k metrics per second
|
||||
- InfluxDB scales horizontally with clustering
|
||||
- Grafana supports multiple data sources
|
||||
|
||||
## Security
|
||||
|
||||
### Authentication
|
||||
- Grafana: Configure LDAP/OAuth integration
|
||||
- InfluxDB: Use authentication tokens
|
||||
- Prometheus: Use basic auth or TLS
|
||||
|
||||
### Network Security
|
||||
- Run monitoring stack in isolated network
|
||||
- Use TLS for all communications
|
||||
- Implement firewall rules for metric endpoints
|
||||
|
||||
## Next Steps
|
||||
|
||||
1. **Custom Dashboards** - Create domain-specific dashboards
|
||||
2. **Advanced Alerting** - Implement predictive alerting
|
||||
3. **ML Integration** - Add anomaly detection
|
||||
4. **Automation** - Auto-scaling based on metrics
|
||||
5. **Compliance** - Audit logging and retention policies
|
||||
|
||||
## Support
|
||||
|
||||
For monitoring-related issues:
|
||||
1. Check service logs: `docker-compose logs [service]`
|
||||
2. Verify configuration files
|
||||
3. Test connectivity between services
|
||||
4. Check resource usage and limits
|
||||
@@ -0,0 +1,381 @@
|
||||
defmodule TelecomSDK do
|
||||
@moduledoc """
|
||||
Telecom Platform Elixir SDK
|
||||
|
||||
Provides GenServer-based async access to the Telecom Platform API with full Elixir pattern matching.
|
||||
"""
|
||||
|
||||
use GenServer
|
||||
require Logger
|
||||
|
||||
# Client API
|
||||
|
||||
@doc """
|
||||
Start the Telecom SDK GenServer
|
||||
"""
|
||||
def start_link(opts \\ []) do
|
||||
config = struct(Config, opts)
|
||||
GenServer.start_link(__MODULE__, config, name: __MODULE__)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Get subscriber by ID
|
||||
"""
|
||||
def get_subscriber(id) do
|
||||
GenServer.call(__MODULE__, {:get_subscriber, id})
|
||||
end
|
||||
|
||||
@doc """
|
||||
List subscribers with pagination
|
||||
"""
|
||||
def list_subscribers(page \\ 1, page_size \\ 50, status \\ nil) do
|
||||
GenServer.call(__MODULE__, {:list_subscribers, page, page_size, status})
|
||||
end
|
||||
|
||||
@doc """
|
||||
Create a new subscriber
|
||||
"""
|
||||
def create_subscriber(request) do
|
||||
GenServer.call(__MODULE__, {:create_subscriber, request})
|
||||
end
|
||||
|
||||
@doc """
|
||||
Update an existing subscriber
|
||||
"""
|
||||
def update_subscriber(id, request) do
|
||||
GenServer.call(__MODULE__, {:update_subscriber, id, request})
|
||||
end
|
||||
|
||||
@doc """
|
||||
Suspend a subscriber
|
||||
"""
|
||||
def suspend_subscriber(id) do
|
||||
GenServer.call(__MODULE__, {:suspend_subscriber, id})
|
||||
end
|
||||
|
||||
@doc """
|
||||
Activate a suspended subscriber
|
||||
"""
|
||||
def activate_subscriber(id) do
|
||||
GenServer.call(__MODULE__, {:activate_subscriber, id})
|
||||
end
|
||||
|
||||
@doc """
|
||||
Terminate a subscriber
|
||||
"""
|
||||
def terminate_subscriber(id) do
|
||||
GenServer.call(__MODULE__, {:terminate_subscriber, id})
|
||||
end
|
||||
|
||||
@doc """
|
||||
Get usage statistics for a subscriber
|
||||
"""
|
||||
def get_usage_stats(subscriber_id, start_date, end_date) do
|
||||
GenServer.call(__MODULE__, {:get_usage_stats, subscriber_id, start_date, end_date})
|
||||
end
|
||||
|
||||
@doc """
|
||||
List usage events with filtering
|
||||
"""
|
||||
def list_usage_events(opts \\ []) do
|
||||
GenServer.call(__MODULE__, {:list_usage_events, opts})
|
||||
end
|
||||
|
||||
@doc """
|
||||
Get real-time usage for a subscriber
|
||||
"""
|
||||
def get_real_time_usage(subscriber_id) do
|
||||
GenServer.call(__MODULE__, {:get_real_time_usage, subscriber_id})
|
||||
end
|
||||
|
||||
@doc """
|
||||
Create a payment transaction
|
||||
"""
|
||||
def create_payment_transaction(request) do
|
||||
GenServer.call(__MODULE__, {:create_payment_transaction, request})
|
||||
end
|
||||
|
||||
@doc """
|
||||
Get payment transaction by ID
|
||||
"""
|
||||
def get_payment_transaction(transaction_id) do
|
||||
GenServer.call(__MODULE__, {:get_payment_transaction, transaction_id})
|
||||
end
|
||||
|
||||
@doc """
|
||||
List payment transactions with filtering
|
||||
"""
|
||||
def list_payment_transactions(opts \\ []) do
|
||||
GenServer.call(__MODULE__, {:list_payment_transactions, opts})
|
||||
end
|
||||
|
||||
@doc """
|
||||
List all available rating plans
|
||||
"""
|
||||
def list_rating_plans do
|
||||
GenServer.call(__MODULE__, :list_rating_plans)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Get rating plan by ID
|
||||
"""
|
||||
def get_rating_plan(plan_id) do
|
||||
GenServer.call(__MODULE__, {:get_rating_plan, plan_id})
|
||||
end
|
||||
|
||||
@doc """
|
||||
Get system statistics
|
||||
"""
|
||||
def get_system_stats do
|
||||
GenServer.call(__MODULE__, :get_system_stats)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Get system health status
|
||||
"""
|
||||
def get_health_status do
|
||||
GenServer.call(__MODULE__, :get_health_status)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Execute a GraphQL query
|
||||
"""
|
||||
def execute_graphql_query(query, variables \\ %{}) do
|
||||
GenServer.call(__MODULE__, {:execute_graphql_query, query, variables})
|
||||
end
|
||||
|
||||
@doc """
|
||||
Connect to WebSocket for real-time updates
|
||||
"""
|
||||
def connect_websocket(message_handler \\ nil) do
|
||||
GenServer.call(__MODULE__, {:connect_websocket, message_handler})
|
||||
end
|
||||
|
||||
# GenServer Callbacks
|
||||
|
||||
@impl true
|
||||
def init(config) do
|
||||
Finch.start_link(name: MyFinch)
|
||||
{:ok, %{config: config, finch: MyFinch}}
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_call({:get_subscriber, id}, _from, state) do
|
||||
case make_request(:get, "/v1/subscribers/#{id}", nil, state) do
|
||||
{:ok, response} -> {:reply, {:ok, Subscriber.from_map(response)}, state}
|
||||
{:error, error} -> {:reply, {:error, error}, state}
|
||||
end
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_call({:list_subscribers, page, page_size, status}, _from, state) do
|
||||
params = %{page: page, page_size: page_size}
|
||||
params = if status, do: Map.put(params, :status, status), else: params
|
||||
|
||||
case make_request(:get, "/v1/subscribers", params, state) do
|
||||
{:ok, response} -> {:reply, {:ok, SubscriberList.from_map(response)}, state}
|
||||
{:error, error} -> {:reply, {:error, error}, state}
|
||||
end
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_call({:create_subscriber, request}, _from, state) do
|
||||
case make_request(:post, "/v1/subscribers", request.to_map(), state) do
|
||||
{:ok, response} -> {:reply, {:ok, Subscriber.from_map(response)}, state}
|
||||
{:error, error} -> {:reply, {:error, error}, state}
|
||||
end
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_call({:update_subscriber, id, request}, _from, state) do
|
||||
case make_request(:put, "/v1/subscribers/#{id}", request.to_map(), state) do
|
||||
{:ok, response} -> {:reply, {:ok, Subscriber.from_map(response)}, state}
|
||||
{:error, error} -> {:reply, {:error, error}, state}
|
||||
end
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_call({:suspend_subscriber, id}, _from, state) do
|
||||
case make_request(:post, "/v1/subscribers/#{id}/suspend", nil, state) do
|
||||
{:ok, response} -> {:reply, {:ok, Subscriber.from_map(response)}, state}
|
||||
{:error, error} -> {:reply, {:error, error}, state}
|
||||
end
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_call({:activate_subscriber, id}, _from, state) do
|
||||
case make_request(:post, "/v1/subscribers/#{id}/activate", nil, state) do
|
||||
{:ok, response} -> {:reply, {:ok, Subscriber.from_map(response)}, state}
|
||||
{:error, error} -> {:reply, {:error, error}, state}
|
||||
end
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_call({:terminate_subscriber, id}, _from, state) do
|
||||
case make_request(:delete, "/v1/subscribers/#{id}", nil, state) do
|
||||
{:ok, response} -> {:reply, {:ok, response["success"]}, state}
|
||||
{:error, error} -> {:reply, {:error, error}, state}
|
||||
end
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_call({:get_usage_stats, subscriber_id, start_date, end_date}, _from, state) do
|
||||
params = %{
|
||||
start_date: DateTime.to_iso8601(start_date),
|
||||
end_date: DateTime.to_iso8601(end_date)
|
||||
}
|
||||
|
||||
case make_request(:get, "/v1/subscribers/#{subscriber_id}/usage", params, state) do
|
||||
{:ok, response} -> {:reply, {:ok, UsageStats.from_map(response)}, state}
|
||||
{:error, error} -> {:reply, {:error, error}, state}
|
||||
end
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_call({:list_usage_events, opts}, _from, state) do
|
||||
params = build_params(opts, [:subscriber_id, :usage_type, :start_date, :end_date, :page, :page_size])
|
||||
|
||||
case make_request(:get, "/v1/usage/events", params, state) do
|
||||
{:ok, response} -> {:reply, {:ok, UsageEventList.from_map(response)}, state}
|
||||
{:error, error} -> {:reply, {:error, error}, state}
|
||||
end
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_call({:get_real_time_usage, subscriber_id}, _from, state) do
|
||||
case make_request(:get, "/v1/subscribers/#{subscriber_id}/realtime", nil, state) do
|
||||
{:ok, response} -> {:reply, {:ok, RealTimeUsage.from_map(response)}, state}
|
||||
{:error, error} -> {:reply, {:error, error}, state}
|
||||
end
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_call({:create_payment_transaction, request}, _from, state) do
|
||||
case make_request(:post, "/v1/payments/transactions", request.to_map(), state) do
|
||||
{:ok, response} -> {:reply, {:ok, PaymentTransaction.from_map(response)}, state}
|
||||
{:error, error} -> {:reply, {:error, error}, state}
|
||||
end
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_call({:get_payment_transaction, transaction_id}, _from, state) do
|
||||
case make_request(:get, "/v1/payments/transactions/#{transaction_id}", nil, state) do
|
||||
{:ok, response} -> {:reply, {:ok, PaymentTransaction.from_map(response)}, state}
|
||||
{:error, error} -> {:reply, {:error, error}, state}
|
||||
end
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_call({:list_payment_transactions, opts}, _from, state) do
|
||||
params = build_params(opts, [:subscriber_id, :status, :page, :page_size])
|
||||
|
||||
case make_request(:get, "/v1/payments/transactions", params, state) do
|
||||
{:ok, response} -> {:reply, {:ok, PaymentTransactionList.from_map(response)}, state}
|
||||
{:error, error} -> {:reply, {:error, error}, state}
|
||||
end
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_call(:list_rating_plans, _from, state) do
|
||||
case make_request(:get, "/v1/rating-plans", nil, state) do
|
||||
{:ok, response} ->
|
||||
plans = Enum.map(response, &RatingPlan.from_map/1)
|
||||
{:reply, {:ok, plans}, state}
|
||||
{:error, error} -> {:reply, {:error, error}, state}
|
||||
end
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_call({:get_rating_plan, plan_id}, _from, state) do
|
||||
case make_request(:get, "/v1/rating-plans/#{plan_id}", nil, state) do
|
||||
{:ok, response} -> {:reply, {:ok, RatingPlan.from_map(response)}, state}
|
||||
{:error, error} -> {:reply, {:error, error}, state}
|
||||
end
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_call(:get_system_stats, _from, state) do
|
||||
case make_request(:get, "/v1/system/stats", nil, state) do
|
||||
{:ok, response} -> {:reply, {:ok, SystemStats.from_map(response)}, state}
|
||||
{:error, error} -> {:reply, {:error, error}, state}
|
||||
end
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_call(:get_health_status, _from, state) do
|
||||
case make_request(:get, "/v1/health", nil, state) do
|
||||
{:ok, response} -> {:reply, {:ok, HealthStatus.from_map(response)}, state}
|
||||
{:error, error} -> {:reply, {:error, error}, state}
|
||||
end
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_call({:execute_graphql_query, query, variables}, _from, state) do
|
||||
request = %{query: query, variables: variables}
|
||||
|
||||
case make_request(:post, "/graphql", request, state) do
|
||||
{:ok, response} -> {:reply, {:ok, GraphQLResponse.from_map(response)}, state}
|
||||
{:error, error} -> {:reply, {:error, error}, state}
|
||||
end
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_call({:connect_websocket, message_handler}, _from, state) do
|
||||
ws_url = String.replace(state.config.api_url, "http://", "ws://") <> "/ws"
|
||||
|
||||
case WebSockex.start_link(ws_url, WebSocketHandler, %{message_handler: message_handler}) do
|
||||
{:ok, pid} -> {:reply, {:ok, pid}, state}
|
||||
{:error, error} -> {:reply, {:error, error}, state}
|
||||
end
|
||||
end
|
||||
|
||||
# Private functions
|
||||
|
||||
defp make_request(method, path, params, state) do
|
||||
url = state.config.api_url <> path
|
||||
|
||||
headers = [
|
||||
{"user-agent", "Telecom-Elixir-SDK/1.0.0"},
|
||||
{"content-type", "application/json"}
|
||||
]
|
||||
|
||||
headers = if state.config.api_key do
|
||||
[{"authorization", "Bearer #{state.config.api_key}"} | headers]
|
||||
else
|
||||
headers
|
||||
end
|
||||
|
||||
request = Finch.build(method, url, headers, Jason.encode!(params || %{}))
|
||||
|
||||
case Finch.request(request, state.finch) do
|
||||
{:ok, response} ->
|
||||
case response.status do
|
||||
200 ->
|
||||
body = response.body |> Jason.decode!()
|
||||
{:ok, body}
|
||||
401 ->
|
||||
{:error, :authentication_error}
|
||||
429 ->
|
||||
{:error, :rate_limit_error}
|
||||
status when status in 400..499 ->
|
||||
body = response.body |> Jason.decode!()
|
||||
{:error, {:api_error, body["error"] || "Bad request"}}
|
||||
status when status in 500..599 ->
|
||||
{:error, {:server_error, status}}
|
||||
_ ->
|
||||
{:error, :unknown_error}
|
||||
end
|
||||
{:error, reason} ->
|
||||
{:error, {:network_error, reason}}
|
||||
end
|
||||
end
|
||||
|
||||
defp build_params(opts, keys) do
|
||||
Enum.reduce(keys, %{}, fn key, acc ->
|
||||
case Map.get(opts, key) do
|
||||
nil -> acc
|
||||
value -> Map.put(acc, key, value)
|
||||
end
|
||||
end)
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,39 @@
|
||||
defmodule TelecomSDK.MixProject do
|
||||
use Mix.Project
|
||||
|
||||
def project do
|
||||
[
|
||||
app: :telecom_sdk,
|
||||
version: "1.0.0",
|
||||
elixir: "~> 1.15",
|
||||
start_permanent: Mix.env() == :prod,
|
||||
deps: deps(),
|
||||
description: "Elixir SDK for Telecom Platform",
|
||||
package: package()
|
||||
]
|
||||
end
|
||||
|
||||
# Run "mix help compile.app" to learn about applications.
|
||||
def application do
|
||||
[
|
||||
extra_applications: [:logger]
|
||||
]
|
||||
end
|
||||
|
||||
# Run "mix help deps.get" to learn about dependencies.
|
||||
defp deps do
|
||||
[
|
||||
{:finch, "~> 0.16"},
|
||||
{:jason, "~> 1.4"},
|
||||
{:websockex, "~> 0.4"},
|
||||
{:ex_doc, "~> 0.27", only: :dev, runtime: false}
|
||||
]
|
||||
end
|
||||
|
||||
defp package do
|
||||
[
|
||||
licenses: ["MIT"],
|
||||
links: %{"GitHub" => "https://github.com/nutcas3/telecom-platform"}
|
||||
]
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,226 @@
|
||||
# Telecom Platform Go SDK
|
||||
|
||||
Go SDK for the Telecom Platform with both HTTP and gRPC support.
|
||||
|
||||
## Features
|
||||
|
||||
- **Dual Protocol Support**: Both HTTP REST and gRPC APIs
|
||||
- **Type Safety**: Strong typing with Go structs
|
||||
- **Context Support**: Full context.Context support
|
||||
- **Error Handling**: Comprehensive error handling
|
||||
- **Retry Logic**: Built-in retry mechanism
|
||||
- **Async Operations**: Non-blocking operations
|
||||
|
||||
## Installation
|
||||
|
||||
```bash
|
||||
go get github.com/nutcas3/telecom-sdk-go
|
||||
```
|
||||
|
||||
## Quick Start
|
||||
|
||||
### HTTP Client
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/nutcas3/telecom-sdk-go"
|
||||
)
|
||||
|
||||
func main() {
|
||||
config := telecom.DefaultConfig()
|
||||
config.APIKey = "your-api-key"
|
||||
|
||||
client, err := telecom.NewClient(config)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
defer client.Close()
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
// Get subscriber
|
||||
subscriber, err := client.GetSubscriber(ctx, 1)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
fmt.Printf("Subscriber: %s %s\n", subscriber.FirstName, subscriber.LastName)
|
||||
|
||||
// List subscribers
|
||||
list, err := client.ListSubscribers(ctx, 1, 50)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
fmt.Printf("Total subscribers: %d\n", list.Total)
|
||||
}
|
||||
```
|
||||
|
||||
### gRPC Client
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/nutcas3/telecom-sdk-go"
|
||||
)
|
||||
|
||||
func main() {
|
||||
config := telecom.DefaultConfig()
|
||||
config.APIKey = "your-api-key"
|
||||
config.EnableGRPC = true
|
||||
config.EnableHTTP = false // Only gRPC
|
||||
|
||||
client, err := telecom.NewClient(config)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
defer client.Close()
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
// Get subscriber via gRPC
|
||||
req := &telecom.GetSubscriberRequest{Id: 1}
|
||||
subscriber, err := client.GetSubscriberGRPC(ctx, req)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
fmt.Printf("Subscriber: %s %s\n", subscriber.FirstName, subscriber.LastName)
|
||||
}
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
### Configuration
|
||||
|
||||
```go
|
||||
config := &telecom.Config{
|
||||
APIURL: "http://localhost:8000",
|
||||
GRPCURL: "localhost:50051",
|
||||
APIKey: "your-api-key",
|
||||
Timeout: 30 * time.Second,
|
||||
Retries: 3,
|
||||
EnableGRPC: true,
|
||||
EnableHTTP: true,
|
||||
}
|
||||
```
|
||||
|
||||
### Subscriber Management
|
||||
|
||||
```go
|
||||
// Get subscriber
|
||||
subscriber, err := client.GetSubscriber(ctx, 1)
|
||||
|
||||
// List subscribers
|
||||
list, err := client.ListSubscribers(ctx, 1, 50)
|
||||
|
||||
// Create subscriber
|
||||
req := &telecom.CreateSubscriberRequest{
|
||||
IMSI: "208930000000001",
|
||||
MSISDN: "+1234567890",
|
||||
FirstName: "John",
|
||||
LastName: "Doe",
|
||||
Email: "[email protected]",
|
||||
PlanID: 1,
|
||||
}
|
||||
subscriber, err := client.CreateSubscriber(ctx, req)
|
||||
|
||||
// Update subscriber
|
||||
updateReq := &telecom.UpdateSubscriberRequest{
|
||||
FirstName: stringPtr("Jane"),
|
||||
}
|
||||
subscriber, err := client.UpdateSubscriber(ctx, 1, updateReq)
|
||||
|
||||
// Delete subscriber
|
||||
err := client.DeleteSubscriber(ctx, 1)
|
||||
```
|
||||
|
||||
### Usage Management
|
||||
|
||||
```go
|
||||
// Get usage stats
|
||||
stats, err := client.GetUsageStats(ctx, 1, time.Now().AddDate(0, -1, 0), time.Now())
|
||||
fmt.Printf("Data usage: %d bytes\n", stats.DataUp + stats.DataDown)
|
||||
```
|
||||
|
||||
### Payment Management
|
||||
|
||||
```go
|
||||
// Create payment
|
||||
req := &telecom.CreatePaymentRequest{
|
||||
SubscriberID: "1",
|
||||
Amount: 25.00,
|
||||
Currency: "USD",
|
||||
Gateway: "stripe",
|
||||
}
|
||||
transaction, err := client.CreatePaymentTransaction(ctx, req)
|
||||
```
|
||||
|
||||
### System Management
|
||||
|
||||
```go
|
||||
// Get system stats
|
||||
stats, err := client.GetSystemStats(ctx)
|
||||
fmt.Printf("Active sessions: %d\n", stats.ActiveSessions)
|
||||
```
|
||||
|
||||
## Error Handling
|
||||
|
||||
```go
|
||||
subscriber, err := client.GetSubscriber(ctx, 1)
|
||||
if err != nil {
|
||||
switch {
|
||||
case strings.Contains(err.Error(), "HTTP error: 401"):
|
||||
fmt.Println("Authentication failed")
|
||||
case strings.Contains(err.Error(), "HTTP error: 404"):
|
||||
fmt.Println("Subscriber not found")
|
||||
case strings.Contains(err.Error(), "HTTP error: 500"):
|
||||
fmt.Println("Server error")
|
||||
default:
|
||||
fmt.Printf("Error: %v\n", err)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## gRPC Service Interfaces
|
||||
|
||||
The SDK provides interfaces for all gRPC services:
|
||||
|
||||
- `SubscriberServiceClient` - Subscriber management
|
||||
- `PaymentServiceClient` - Payment processing
|
||||
- `UsageServiceClient` - Usage tracking
|
||||
- `SystemServiceClient` - System monitoring
|
||||
|
||||
## Development
|
||||
|
||||
### Running Tests
|
||||
|
||||
```bash
|
||||
go test ./...
|
||||
```
|
||||
|
||||
### Code Coverage
|
||||
|
||||
```bash
|
||||
go test -cover ./...
|
||||
```
|
||||
|
||||
### Building
|
||||
|
||||
```bash
|
||||
go build ./...
|
||||
```
|
||||
|
||||
## License
|
||||
|
||||
This SDK is licensed under the MIT License.
|
||||
@@ -0,0 +1,3 @@
|
||||
module github.com/nutcas3/telecom-sdk-go
|
||||
|
||||
go 1.26
|
||||
@@ -0,0 +1,273 @@
|
||||
package telecom
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Client represents the Telecom Platform SDK client
|
||||
type Client struct {
|
||||
config *Config
|
||||
httpClient *HTTPClient
|
||||
}
|
||||
|
||||
// Config holds the SDK configuration
|
||||
type Config struct {
|
||||
APIURL string
|
||||
APIKey string
|
||||
Timeout time.Duration
|
||||
Retries int
|
||||
EnableHTTP bool
|
||||
}
|
||||
|
||||
// HTTPClient handles HTTP requests
|
||||
type HTTPClient struct {
|
||||
client *http.Client
|
||||
apiKey string
|
||||
baseURL string
|
||||
}
|
||||
|
||||
// DefaultConfig returns a default configuration
|
||||
func DefaultConfig() *Config {
|
||||
return &Config{
|
||||
APIURL: "http://localhost:8000",
|
||||
APIKey: "",
|
||||
Timeout: 30 * time.Second,
|
||||
Retries: 3,
|
||||
EnableHTTP: true,
|
||||
}
|
||||
}
|
||||
|
||||
// NewClient creates a new Telecom SDK client
|
||||
func NewClient(config *Config) (*Client, error) {
|
||||
if config == nil {
|
||||
config = DefaultConfig()
|
||||
}
|
||||
|
||||
client := &Client{
|
||||
config: config,
|
||||
}
|
||||
|
||||
// Initialize HTTP client
|
||||
if config.EnableHTTP {
|
||||
client.httpClient = &HTTPClient{
|
||||
client: &http.Client{
|
||||
Timeout: config.Timeout,
|
||||
},
|
||||
apiKey: config.APIKey,
|
||||
baseURL: config.APIURL,
|
||||
}
|
||||
}
|
||||
|
||||
return client, nil
|
||||
}
|
||||
|
||||
// Close closes the client connections
|
||||
func (c *Client) Close() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// HTTP Methods
|
||||
|
||||
// GetSubscriber retrieves a subscriber by ID
|
||||
func (c *Client) GetSubscriber(ctx context.Context, id int64) (*Subscriber, error) {
|
||||
if !c.config.EnableHTTP {
|
||||
return nil, fmt.Errorf("HTTP client not enabled")
|
||||
}
|
||||
|
||||
var subscriber Subscriber
|
||||
err := c.httpClient.Get(ctx, fmt.Sprintf("/v1/subscribers/%d", id), &subscriber)
|
||||
return &subscriber, err
|
||||
}
|
||||
|
||||
// ListSubscribers retrieves a list of subscribers
|
||||
func (c *Client) ListSubscribers(ctx context.Context, page, pageSize int32) (*SubscriberList, error) {
|
||||
if !c.config.EnableHTTP {
|
||||
return nil, fmt.Errorf("HTTP client not enabled")
|
||||
}
|
||||
|
||||
params := map[string]string{
|
||||
"page": fmt.Sprintf("%d", page),
|
||||
"page_size": fmt.Sprintf("%d", pageSize),
|
||||
}
|
||||
|
||||
var list SubscriberList
|
||||
err := c.httpClient.Get(ctx, "/v1/subscribers", &list, params)
|
||||
return &list, err
|
||||
}
|
||||
|
||||
// CreateSubscriber creates a new subscriber
|
||||
func (c *Client) CreateSubscriber(ctx context.Context, req *CreateSubscriberRequest) (*Subscriber, error) {
|
||||
if !c.config.EnableHTTP {
|
||||
return nil, fmt.Errorf("HTTP client not enabled")
|
||||
}
|
||||
|
||||
var subscriber Subscriber
|
||||
err := c.httpClient.Post(ctx, "/v1/subscribers", req, &subscriber)
|
||||
return &subscriber, err
|
||||
}
|
||||
|
||||
// UpdateSubscriber updates an existing subscriber
|
||||
func (c *Client) UpdateSubscriber(ctx context.Context, id int64, req *UpdateSubscriberRequest) (*Subscriber, error) {
|
||||
if !c.config.EnableHTTP {
|
||||
return nil, fmt.Errorf("HTTP client not enabled")
|
||||
}
|
||||
|
||||
var subscriber Subscriber
|
||||
err := c.httpClient.Put(ctx, fmt.Sprintf("/v1/subscribers/%d", id), req, &subscriber)
|
||||
return &subscriber, err
|
||||
}
|
||||
|
||||
// DeleteSubscriber deletes a subscriber
|
||||
func (c *Client) DeleteSubscriber(ctx context.Context, id int64) error {
|
||||
if !c.config.EnableHTTP {
|
||||
return fmt.Errorf("HTTP client not enabled")
|
||||
}
|
||||
|
||||
return c.httpClient.Delete(ctx, fmt.Sprintf("/v1/subscribers/%d", id))
|
||||
}
|
||||
|
||||
// GetUsageStats retrieves usage statistics for a subscriber
|
||||
func (c *Client) GetUsageStats(ctx context.Context, subscriberID int64, startDate, endDate time.Time) (*UsageStats, error) {
|
||||
if !c.config.EnableHTTP {
|
||||
return nil, fmt.Errorf("HTTP client not enabled")
|
||||
}
|
||||
|
||||
params := map[string]string{
|
||||
"start_date": startDate.Format(time.RFC3339),
|
||||
"end_date": endDate.Format(time.RFC3339),
|
||||
}
|
||||
|
||||
var stats UsageStats
|
||||
err := c.httpClient.Get(ctx, fmt.Sprintf("/v1/subscribers/%d/usage", subscriberID), &stats, params)
|
||||
return &stats, err
|
||||
}
|
||||
|
||||
// CreatePaymentTransaction creates a new payment transaction
|
||||
func (c *Client) CreatePaymentTransaction(ctx context.Context, req *CreatePaymentRequest) (*PaymentTransaction, error) {
|
||||
if !c.config.EnableHTTP {
|
||||
return nil, fmt.Errorf("HTTP client not enabled")
|
||||
}
|
||||
|
||||
var transaction PaymentTransaction
|
||||
err := c.httpClient.Post(ctx, "/v1/payments/transactions", req, &transaction)
|
||||
return &transaction, err
|
||||
}
|
||||
|
||||
// GetSystemStats retrieves system statistics
|
||||
func (c *Client) GetSystemStats(ctx context.Context) (*SystemStats, error) {
|
||||
if !c.config.EnableHTTP {
|
||||
return nil, fmt.Errorf("HTTP client not enabled")
|
||||
}
|
||||
|
||||
var stats SystemStats
|
||||
err := c.httpClient.Get(ctx, "/v1/system/stats", &stats)
|
||||
return &stats, err
|
||||
}
|
||||
|
||||
// HTTPClient implementation
|
||||
|
||||
// Get makes an HTTP GET request
|
||||
func (h *HTTPClient) Get(ctx context.Context, path string, result interface{}, params ...map[string]string) error {
|
||||
req, err := http.NewRequestWithContext(ctx, "GET", h.baseURL+path, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if h.apiKey != "" {
|
||||
req.Header.Set("Authorization", "Bearer "+h.apiKey)
|
||||
}
|
||||
|
||||
if len(params) > 0 {
|
||||
q := req.URL.Query()
|
||||
for k, v := range params[0] {
|
||||
q.Add(k, v)
|
||||
}
|
||||
req.URL.RawQuery = q.Encode()
|
||||
}
|
||||
|
||||
resp, err := h.client.Do(req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return fmt.Errorf("HTTP error: %d", resp.StatusCode)
|
||||
}
|
||||
|
||||
return json.NewDecoder(resp.Body).Decode(result)
|
||||
}
|
||||
|
||||
// Post makes an HTTP POST request
|
||||
func (h *HTTPClient) Post(ctx context.Context, path string, body, result interface{}) error {
|
||||
return h.doRequest(ctx, "POST", path, body, result)
|
||||
}
|
||||
|
||||
// Put makes an HTTP PUT request
|
||||
func (h *HTTPClient) Put(ctx context.Context, path string, body, result interface{}) error {
|
||||
return h.doRequest(ctx, "PUT", path, body, result)
|
||||
}
|
||||
|
||||
// Delete makes an HTTP DELETE request
|
||||
func (h *HTTPClient) Delete(ctx context.Context, path string) error {
|
||||
req, err := http.NewRequestWithContext(ctx, "DELETE", h.baseURL+path, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if h.apiKey != "" {
|
||||
req.Header.Set("Authorization", "Bearer "+h.apiKey)
|
||||
}
|
||||
|
||||
resp, err := h.client.Do(req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusNoContent {
|
||||
return fmt.Errorf("HTTP error: %d", resp.StatusCode)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// doRequest makes an HTTP request with a body
|
||||
func (h *HTTPClient) doRequest(ctx context.Context, method, path string, body, result interface{}) error {
|
||||
var bodyBytes []byte
|
||||
var err error
|
||||
|
||||
if body != nil {
|
||||
bodyBytes, err = json.Marshal(body)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
req, err := http.NewRequestWithContext(ctx, method, h.baseURL+path, bytes.NewReader(bodyBytes))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if h.apiKey != "" {
|
||||
req.Header.Set("Authorization", "Bearer "+h.apiKey)
|
||||
}
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
|
||||
resp, err := h.client.Do(req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return fmt.Errorf("HTTP error: %d", resp.StatusCode)
|
||||
}
|
||||
|
||||
return json.NewDecoder(resp.Body).Decode(result)
|
||||
}
|
||||
+261
@@ -0,0 +1,261 @@
|
||||
package telecom
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Subscriber represents a telecom subscriber
|
||||
type Subscriber struct {
|
||||
ID int64 `json:"id"`
|
||||
IMSI string `json:"imsi"`
|
||||
MSISDN string `json:"msisdn"`
|
||||
FirstName string `json:"first_name"`
|
||||
LastName string `json:"last_name"`
|
||||
Email string `json:"email"`
|
||||
OrganizationID *string `json:"organization_id,omitempty"`
|
||||
Status string `json:"status"`
|
||||
PlanID int64 `json:"plan_id"`
|
||||
Balance float64 `json:"balance"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
}
|
||||
|
||||
// SubscriberList represents a paginated list of subscribers
|
||||
type SubscriberList struct {
|
||||
Subscribers []Subscriber `json:"subscribers"`
|
||||
Total int64 `json:"total"`
|
||||
Page int32 `json:"page"`
|
||||
PageSize int32 `json:"page_size"`
|
||||
HasNext bool `json:"has_next"`
|
||||
HasPrev bool `json:"has_prev"`
|
||||
}
|
||||
|
||||
// CreateSubscriberRequest represents a request to create a subscriber
|
||||
type CreateSubscriberRequest struct {
|
||||
IMSI string `json:"imsi"`
|
||||
MSISDN string `json:"msisdn"`
|
||||
FirstName string `json:"first_name"`
|
||||
LastName string `json:"last_name"`
|
||||
Email string `json:"email"`
|
||||
PlanID int64 `json:"plan_id"`
|
||||
OrganizationID *string `json:"organization_id,omitempty"`
|
||||
}
|
||||
|
||||
// UpdateSubscriberRequest represents a request to update a subscriber
|
||||
type UpdateSubscriberRequest struct {
|
||||
FirstName *string `json:"first_name,omitempty"`
|
||||
LastName *string `json:"last_name,omitempty"`
|
||||
Email *string `json:"email,omitempty"`
|
||||
PlanID *int64 `json:"plan_id,omitempty"`
|
||||
Status *string `json:"status,omitempty"`
|
||||
}
|
||||
|
||||
// UsageStats represents usage statistics
|
||||
type UsageStats struct {
|
||||
SubscriberID string `json:"subscriber_id"`
|
||||
DataUp int64 `json:"data_up"`
|
||||
DataDown int64 `json:"data_down"`
|
||||
VoiceSeconds int64 `json:"voice_seconds"`
|
||||
SMSCount int64 `json:"sms_count"`
|
||||
PeriodStart time.Time `json:"period_start"`
|
||||
PeriodEnd time.Time `json:"period_end"`
|
||||
Cost float64 `json:"cost"`
|
||||
}
|
||||
|
||||
// PaymentTransaction represents a payment transaction
|
||||
type PaymentTransaction struct {
|
||||
ID string `json:"id"`
|
||||
SubscriberID string `json:"subscriber_id"`
|
||||
Amount float64 `json:"amount"`
|
||||
Currency string `json:"currency"`
|
||||
Status string `json:"status"`
|
||||
Gateway string `json:"gateway"`
|
||||
TransactionID *string `json:"transaction_id,omitempty"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
CompletedAt *time.Time `json:"completed_at,omitempty"`
|
||||
Metadata map[string]interface{} `json:"metadata,omitempty"`
|
||||
}
|
||||
|
||||
// CreatePaymentRequest represents a request to create a payment
|
||||
type CreatePaymentRequest struct {
|
||||
SubscriberID string `json:"subscriber_id"`
|
||||
Amount float64 `json:"amount"`
|
||||
Currency string `json:"currency"`
|
||||
Gateway string `json:"gateway"`
|
||||
Metadata map[string]interface{} `json:"metadata,omitempty"`
|
||||
}
|
||||
|
||||
// SystemStats represents system statistics
|
||||
type SystemStats struct {
|
||||
ActiveSessions int64 `json:"active_sessions"`
|
||||
TotalAccounts int64 `json:"total_accounts"`
|
||||
BlockedUsers int64 `json:"blocked_users"`
|
||||
LowBalanceAlerts int64 `json:"low_balance_alerts"`
|
||||
Uptime float64 `json:"uptime"`
|
||||
CPUUsage float64 `json:"cpu_usage"`
|
||||
MemoryUsage float64 `json:"memory_usage"`
|
||||
}
|
||||
|
||||
// HealthStatus represents system health status
|
||||
type HealthStatus struct {
|
||||
Status string `json:"status"`
|
||||
Timestamp time.Time `json:"timestamp"`
|
||||
Checks map[string]interface{} `json:"checks"`
|
||||
Uptime float64 `json:"uptime"`
|
||||
}
|
||||
|
||||
// RatingPlan represents a rating plan
|
||||
type RatingPlan struct {
|
||||
PlanID string `json:"plan_id"`
|
||||
Name string `json:"name"`
|
||||
DataRate float64 `json:"data_rate"`
|
||||
VoiceRate float64 `json:"voice_rate"`
|
||||
SMSRate float64 `json:"sms_rate"`
|
||||
MonthlyFee float64 `json:"monthly_fee"`
|
||||
DataLimit int64 `json:"data_limit"`
|
||||
VoiceLimit int64 `json:"voice_limit"`
|
||||
SMSLimit int64 `json:"sms_limit"`
|
||||
}
|
||||
|
||||
// UsageEvent represents a usage event
|
||||
type UsageEvent struct {
|
||||
ID string `json:"id"`
|
||||
SubscriberID string `json:"subscriber_id"`
|
||||
UsageType string `json:"usage_type"`
|
||||
Amount int64 `json:"amount"`
|
||||
Cost float64 `json:"cost"`
|
||||
Timestamp time.Time `json:"timestamp"`
|
||||
Metadata map[string]interface{} `json:"metadata,omitempty"`
|
||||
}
|
||||
|
||||
// CurrentSession represents a current active session
|
||||
type CurrentSession struct {
|
||||
SessionID string `json:"session_id"`
|
||||
StartTime time.Time `json:"start_time"`
|
||||
DataUp int64 `json:"data_up"`
|
||||
DataDown int64 `json:"data_down"`
|
||||
VoiceSeconds int64 `json:"voice_seconds"`
|
||||
SMSCount int64 `json:"sms_count"`
|
||||
}
|
||||
|
||||
// RealTimeUsage represents real-time usage data
|
||||
type RealTimeUsage struct {
|
||||
CurrentSession *CurrentSession `json:"current_session,omitempty"`
|
||||
TodayUsage map[string]int64 `json:"today_usage,omitempty"`
|
||||
}
|
||||
|
||||
// gRPC Service Interfaces
|
||||
|
||||
// SubscriberServiceClient interface for gRPC subscriber service
|
||||
type SubscriberServiceClient interface {
|
||||
GetSubscriber(ctx context.Context, req *GetSubscriberRequest) (*Subscriber, error)
|
||||
ListSubscribers(ctx context.Context, req *ListSubscribersRequest) (*SubscriberList, error)
|
||||
CreateSubscriber(ctx context.Context, req *CreateSubscriberRequest) (*Subscriber, error)
|
||||
UpdateSubscriber(ctx context.Context, req *UpdateSubscriberRequest) (*Subscriber, error)
|
||||
DeleteSubscriber(ctx context.Context, req *DeleteSubscriberRequest) (*DeleteSubscriberResponse, error)
|
||||
}
|
||||
|
||||
// GetSubscriberRequest represents a gRPC request to get a subscriber
|
||||
type GetSubscriberRequest struct {
|
||||
Id int64 `json:"id"`
|
||||
}
|
||||
|
||||
// ListSubscribersRequest represents a gRPC request to list subscribers
|
||||
type ListSubscribersRequest struct {
|
||||
Page int32 `json:"page"`
|
||||
PageSize int32 `json:"page_size"`
|
||||
Status string `json:"status,omitempty"`
|
||||
}
|
||||
|
||||
// DeleteSubscriberRequest represents a gRPC request to delete a subscriber
|
||||
type DeleteSubscriberRequest struct {
|
||||
Id int64 `json:"id"`
|
||||
}
|
||||
|
||||
// DeleteSubscriberResponse represents a gRPC response for delete subscriber
|
||||
type DeleteSubscriberResponse struct {
|
||||
Success bool `json:"success"`
|
||||
Message string `json:"message"`
|
||||
}
|
||||
|
||||
// PaymentServiceClient interface for gRPC payment service
|
||||
type PaymentServiceClient interface {
|
||||
CreatePayment(ctx context.Context, req *CreatePaymentRequest) (*PaymentTransaction, error)
|
||||
GetPayment(ctx context.Context, req *GetPaymentRequest) (*PaymentTransaction, error)
|
||||
ListPayments(ctx context.Context, req *ListPaymentsRequest) (*PaymentTransactionList, error)
|
||||
}
|
||||
|
||||
// GetPaymentRequest represents a gRPC request to get a payment
|
||||
type GetPaymentRequest struct {
|
||||
Id string `json:"id"`
|
||||
}
|
||||
|
||||
// ListPaymentsRequest represents a gRPC request to list payments
|
||||
type ListPaymentsRequest struct {
|
||||
SubscriberID string `json:"subscriber_id,omitempty"`
|
||||
Status string `json:"status,omitempty"`
|
||||
Page int32 `json:"page"`
|
||||
PageSize int32 `json:"page_size"`
|
||||
}
|
||||
|
||||
// PaymentTransactionList represents a list of payment transactions
|
||||
type PaymentTransactionList struct {
|
||||
Transactions []PaymentTransaction `json:"transactions"`
|
||||
Total int64 `json:"total"`
|
||||
Page int32 `json:"page"`
|
||||
PageSize int32 `json:"page_size"`
|
||||
HasNext bool `json:"has_next"`
|
||||
HasPrev bool `json:"has_prev"`
|
||||
}
|
||||
|
||||
// UsageServiceClient interface for gRPC usage service
|
||||
type UsageServiceClient interface {
|
||||
GetUsageStats(ctx context.Context, req *GetUsageStatsRequest) (*UsageStats, error)
|
||||
GetRealTimeUsage(ctx context.Context, req *GetRealTimeUsageRequest) (*RealTimeUsage, error)
|
||||
ListUsageEvents(ctx context.Context, req *ListUsageEventsRequest) (*UsageEventList, error)
|
||||
}
|
||||
|
||||
// GetUsageStatsRequest represents a gRPC request to get usage stats
|
||||
type GetUsageStatsRequest struct {
|
||||
SubscriberID int64 `json:"subscriber_id"`
|
||||
StartDate time.Time `json:"start_date"`
|
||||
EndDate time.Time `json:"end_date"`
|
||||
}
|
||||
|
||||
// GetRealTimeUsageRequest represents a gRPC request to get real-time usage
|
||||
type GetRealTimeUsageRequest struct {
|
||||
SubscriberID int64 `json:"subscriber_id"`
|
||||
}
|
||||
|
||||
// ListUsageEventsRequest represents a gRPC request to list usage events
|
||||
type ListUsageEventsRequest struct {
|
||||
SubscriberID *int64 `json:"subscriber_id,omitempty"`
|
||||
UsageType string `json:"usage_type,omitempty"`
|
||||
StartDate *time.Time `json:"start_date,omitempty"`
|
||||
EndDate *time.Time `json:"end_date,omitempty"`
|
||||
Page int32 `json:"page"`
|
||||
PageSize int32 `json:"page_size"`
|
||||
}
|
||||
|
||||
// UsageEventList represents a list of usage events
|
||||
type UsageEventList struct {
|
||||
Events []UsageEvent `json:"events"`
|
||||
Total int64 `json:"total"`
|
||||
Page int32 `json:"page"`
|
||||
PageSize int32 `json:"page_size"`
|
||||
HasNext bool `json:"has_next"`
|
||||
HasPrev bool `json:"has_prev"`
|
||||
}
|
||||
|
||||
// SystemServiceClient interface for gRPC system service
|
||||
type SystemServiceClient interface {
|
||||
GetSystemStats(ctx context.Context, req *GetSystemStatsRequest) (*SystemStats, error)
|
||||
GetHealthStatus(ctx context.Context, req *GetHealthStatusRequest) (*HealthStatus, error)
|
||||
}
|
||||
|
||||
// GetSystemStatsRequest represents a gRPC request to get system stats
|
||||
type GetSystemStatsRequest struct{}
|
||||
|
||||
// GetHealthStatusRequest represents a gRPC request to get health status
|
||||
type GetHealthStatusRequest struct{}
|
||||
@@ -0,0 +1,41 @@
|
||||
plugins {
|
||||
kotlin("jvm") version "1.9.10"
|
||||
kotlin("plugin.serialization") version "1.9.10"
|
||||
id("maven-publish")
|
||||
}
|
||||
|
||||
group = "com.telecom"
|
||||
version = "1.0.0"
|
||||
|
||||
repositories {
|
||||
mavenCentral()
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.3")
|
||||
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.0")
|
||||
implementation("io.ktor:ktor-client-core:2.3.4")
|
||||
implementation("io.ktor:ktor-client-cio:2.3.4")
|
||||
implementation("io.ktor:ktor-client-content-negotiation:2.3.4")
|
||||
implementation("io.ktor:ktor-serialization-kotlinx-json:2.3.4")
|
||||
implementation("io.ktor:ktor-client-websockets:2.3.4")
|
||||
|
||||
testImplementation("org.jetbrains.kotlin:kotlin-test")
|
||||
testImplementation("io.ktor:ktor-client-mock:2.3.4")
|
||||
}
|
||||
|
||||
kotlin {
|
||||
jvmToolchain(17)
|
||||
}
|
||||
|
||||
tasks.test {
|
||||
useJUnitPlatform()
|
||||
}
|
||||
|
||||
publishing {
|
||||
publications {
|
||||
create<MavenPublication>("maven") {
|
||||
from(components["java"])
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,345 @@
|
||||
package com.telecom
|
||||
|
||||
import io.ktor.client.*
|
||||
import io.ktor.client.call.*
|
||||
import io.ktor.client.plugins.*
|
||||
import io.ktor.client.plugins.contentnegotiation.*
|
||||
import io.ktor.client.request.*
|
||||
import io.ktor.client.statement.*
|
||||
import io.ktor.http.*
|
||||
import io.ktor.serialization.kotlinx.json.*
|
||||
import io.ktor.websocket.*
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.flow.*
|
||||
import kotlinx.serialization.*
|
||||
import kotlinx.serialization.json.*
|
||||
import java.time.*
|
||||
import java.time.format.DateTimeFormatter
|
||||
|
||||
/**
|
||||
* Telecom Platform Kotlin SDK
|
||||
*
|
||||
* Provides coroutine-based access to the Telecom Platform API with full Kotlin serialization support.
|
||||
*/
|
||||
class TelecomSDK private constructor(
|
||||
private val httpClient: HttpClient,
|
||||
private val config: TelecomConfig,
|
||||
private val webSocketClient: WebSocketClient
|
||||
) {
|
||||
|
||||
companion object {
|
||||
/**
|
||||
* Create a new TelecomSDK instance
|
||||
*/
|
||||
fun create(config: TelecomConfig = TelecomConfig()): TelecomSDK {
|
||||
val httpClient = HttpClient {
|
||||
defaultRequest {
|
||||
url(config.apiURL)
|
||||
header("User-Agent", "Telecom-Kotlin-SDK/1.0.0")
|
||||
config.apiKey?.let { header("Authorization", "Bearer $it") }
|
||||
}
|
||||
|
||||
install(ContentNegotiation) {
|
||||
json(Json {
|
||||
ignoreUnknownKeys = true
|
||||
isLenient = true
|
||||
})
|
||||
}
|
||||
|
||||
install(HttpTimeout) {
|
||||
requestTimeoutMillis = config.timeout.toMillis()
|
||||
}
|
||||
|
||||
install(Retry) {
|
||||
maxRetries = config.maxRetries
|
||||
retryOnExceptionIf { request, throwable ->
|
||||
throwable is HttpRequestTimeoutException ||
|
||||
throwable is ConnectTimeoutException
|
||||
}
|
||||
exponentialDelay()
|
||||
}
|
||||
|
||||
HttpResponseValidator {
|
||||
handleResponseExceptionWithRequest { exception, request ->
|
||||
when (exception) {
|
||||
is ClientRequestException -> {
|
||||
when (exception.response.status) {
|
||||
HttpStatusCode.Unauthorized -> throw TelecomException.AuthenticationError("Authentication failed")
|
||||
HttpStatusCode.TooManyRequests -> throw TelecomException.RateLimitError("Rate limit exceeded")
|
||||
in HttpStatusCode.BadRequest..HttpStatusCode.UnprocessableEntity -> {
|
||||
val errorBody = exception.response.body<String>()
|
||||
throw TelecomException.APIError("API error: $errorBody")
|
||||
}
|
||||
in HttpStatusCode.InternalServerError..HttpStatusCode.NetworkAuthenticationRequired -> {
|
||||
throw TelecomException.ServerError("Server error: ${exception.response.status}")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val webSocketClient = WebSocketClient()
|
||||
|
||||
return TelecomSDK(httpClient, config, webSocketClient)
|
||||
}
|
||||
}
|
||||
|
||||
// Subscriber Management
|
||||
|
||||
/**
|
||||
* Get subscriber by ID
|
||||
*/
|
||||
suspend fun getSubscriber(id: Long): Subscriber {
|
||||
return httpClient.get("/v1/subscribers/$id").body()
|
||||
}
|
||||
|
||||
/**
|
||||
* List subscribers with pagination
|
||||
*/
|
||||
suspend fun listSubscribers(
|
||||
page: Int = 1,
|
||||
pageSize: Int = 50,
|
||||
status: SubscriberStatus? = null
|
||||
): SubscriberList {
|
||||
val params = mutableListOf(
|
||||
"page" to page.toString(),
|
||||
"page_size" to pageSize.toString()
|
||||
)
|
||||
status?.let { params.add("status" to it.value) }
|
||||
|
||||
return httpClient.get("/v1/subscribers") {
|
||||
parameter(params)
|
||||
}.body()
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new subscriber
|
||||
*/
|
||||
suspend fun createSubscriber(request: CreateSubscriberRequest): Subscriber {
|
||||
return httpClient.post("/v1/subscribers") {
|
||||
setBody(request)
|
||||
}.body()
|
||||
}
|
||||
|
||||
/**
|
||||
* Update an existing subscriber
|
||||
*/
|
||||
suspend fun updateSubscriber(id: Long, request: UpdateSubscriberRequest): Subscriber {
|
||||
return httpClient.put("/v1/subscribers/$id") {
|
||||
setBody(request)
|
||||
}.body()
|
||||
}
|
||||
|
||||
/**
|
||||
* Suspend a subscriber
|
||||
*/
|
||||
suspend fun suspendSubscriber(id: Long): Subscriber {
|
||||
return httpClient.post("/v1/subscribers/$id/suspend").body()
|
||||
}
|
||||
|
||||
/**
|
||||
* Activate a suspended subscriber
|
||||
*/
|
||||
suspend fun activateSubscriber(id: Long): Subscriber {
|
||||
return httpClient.post("/v1/subscribers/$id/activate").body()
|
||||
}
|
||||
|
||||
/**
|
||||
* Terminate a subscriber
|
||||
*/
|
||||
suspend fun terminateSubscriber(id: Long): Boolean {
|
||||
return httpClient.delete("/v1/subscribers/$id").body()
|
||||
}
|
||||
|
||||
// Usage Management
|
||||
|
||||
/**
|
||||
* Get usage statistics for a subscriber
|
||||
*/
|
||||
suspend fun getUsageStats(
|
||||
subscriberId: Long,
|
||||
startDate: LocalDateTime,
|
||||
endDate: LocalDateTime
|
||||
): UsageStats {
|
||||
return httpClient.get("/v1/subscribers/$subscriberId/usage") {
|
||||
parameter("start_date", startDate.format(DateTimeFormatter.ISO_DATE_TIME))
|
||||
parameter("end_date", endDate.format(DateTimeFormatter.ISO_DATE_TIME))
|
||||
}.body()
|
||||
}
|
||||
|
||||
/**
|
||||
* List usage events with filtering
|
||||
*/
|
||||
suspend fun listUsageEvents(
|
||||
subscriberId: Long? = null,
|
||||
usageType: UsageType? = null,
|
||||
startDate: LocalDateTime? = null,
|
||||
endDate: LocalDateTime? = null,
|
||||
page: Int = 1,
|
||||
pageSize: Int = 50
|
||||
): UsageEventList {
|
||||
val params = mutableListOf(
|
||||
"page" to page.toString(),
|
||||
"page_size" to pageSize.toString()
|
||||
)
|
||||
|
||||
subscriberId?.let { params.add("subscriber_id" to it.toString()) }
|
||||
usageType?.let { params.add("usage_type" to it.value) }
|
||||
startDate?.let { params.add("start_date", it.format(DateTimeFormatter.ISO_DATE_TIME)) }
|
||||
endDate?.let { params.add("end_date", it.format(DateTimeFormatter.ISO_DATE_TIME)) }
|
||||
|
||||
return httpClient.get("/v1/usage/events") {
|
||||
parameter(params)
|
||||
}.body()
|
||||
}
|
||||
|
||||
/**
|
||||
* Get real-time usage for a subscriber
|
||||
*/
|
||||
suspend fun getRealTimeUsage(subscriberId: Long): RealTimeUsage {
|
||||
return httpClient.get("/v1/subscribers/$subscriberId/realtime").body()
|
||||
}
|
||||
|
||||
// Payment Management
|
||||
|
||||
/**
|
||||
* Create a payment transaction
|
||||
*/
|
||||
suspend fun createPaymentTransaction(request: CreatePaymentRequest): PaymentTransaction {
|
||||
return httpClient.post("/v1/payments/transactions") {
|
||||
setBody(request)
|
||||
}.body()
|
||||
}
|
||||
|
||||
/**
|
||||
* Get payment transaction by ID
|
||||
*/
|
||||
suspend fun getPaymentTransaction(transactionId: String): PaymentTransaction {
|
||||
return httpClient.get("/v1/payments/transactions/$transactionId").body()
|
||||
}
|
||||
|
||||
/**
|
||||
* List payment transactions with filtering
|
||||
*/
|
||||
suspend fun listPaymentTransactions(
|
||||
subscriberId: Long? = null,
|
||||
status: PaymentStatus? = null,
|
||||
page: Int = 1,
|
||||
pageSize: Int = 50
|
||||
): PaymentTransactionList {
|
||||
val params = mutableListOf(
|
||||
"page" to page.toString(),
|
||||
"page_size" to pageSize.toString()
|
||||
)
|
||||
|
||||
subscriberId?.let { params.add("subscriber_id" to it.toString()) }
|
||||
status?.let { params.add("status" to it.value) }
|
||||
|
||||
return httpClient.get("/v1/payments/transactions") {
|
||||
parameter(params)
|
||||
}.body()
|
||||
}
|
||||
|
||||
// Rating Plans
|
||||
|
||||
/**
|
||||
* List all available rating plans
|
||||
*/
|
||||
suspend fun listRatingPlans(): List<RatingPlan> {
|
||||
return httpClient.get("/v1/rating-plans").body()
|
||||
}
|
||||
|
||||
/**
|
||||
* Get rating plan by ID
|
||||
*/
|
||||
suspend fun getRatingPlan(planId: String): RatingPlan {
|
||||
return httpClient.get("/v1/rating-plans/$planId").body()
|
||||
}
|
||||
|
||||
// System Management
|
||||
|
||||
/**
|
||||
* Get system statistics
|
||||
*/
|
||||
suspend fun getSystemStats(): SystemStats {
|
||||
return httpClient.get("/v1/system/stats").body()
|
||||
}
|
||||
|
||||
/**
|
||||
* Get system health status
|
||||
*/
|
||||
suspend fun getHealthStatus(): HealthStatus {
|
||||
return httpClient.get("/v1/health").body()
|
||||
}
|
||||
|
||||
// WebSocket Support
|
||||
|
||||
/**
|
||||
* Connect to WebSocket for real-time updates
|
||||
*/
|
||||
fun connectWebSocket(
|
||||
messageHandler: suspend (WebSocketMessage) -> Unit
|
||||
): Flow<WebSocketMessage> = flow {
|
||||
val wsUrl = config.apiURL.replace("http://", "ws://") + "/ws"
|
||||
|
||||
webSocketClient.webSocket(
|
||||
urlString = wsUrl,
|
||||
request = {
|
||||
config.apiKey?.let { header("Authorization", "Bearer $it") }
|
||||
}
|
||||
) { session ->
|
||||
session.incoming.consumeAsFlow().collect { frame ->
|
||||
when (frame) {
|
||||
is Frame.Text -> {
|
||||
try {
|
||||
val message = Json.decodeFromString<WebSocketMessage>(frame.readText())
|
||||
emit(message)
|
||||
messageHandler(message)
|
||||
} catch (e: Exception) {
|
||||
throw TelecomException.WebSocketError("Failed to parse WebSocket message: $e")
|
||||
}
|
||||
}
|
||||
else -> {
|
||||
// Handle other frame types if needed
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}.catch { e ->
|
||||
throw TelecomException.WebSocketError("WebSocket connection failed: $e")
|
||||
}
|
||||
|
||||
// GraphQL Support
|
||||
|
||||
/**
|
||||
* Execute a GraphQL query
|
||||
*/
|
||||
suspend fun executeGraphQLQuery(
|
||||
query: String,
|
||||
variables: Map<String, Any> = emptyMap()
|
||||
): GraphQLResponse {
|
||||
val request = GraphQLRequest(query, variables)
|
||||
return httpClient.post("/graphql") {
|
||||
setBody(request)
|
||||
}.body()
|
||||
}
|
||||
|
||||
/**
|
||||
* Close the SDK and cleanup resources
|
||||
*/
|
||||
fun close() {
|
||||
httpClient.close()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Configuration for Telecom SDK
|
||||
*/
|
||||
data class TelecomConfig(
|
||||
val apiURL: String = "http://localhost:8000",
|
||||
val apiKey: String? = null,
|
||||
val timeout: Duration = Duration.ofSeconds(30),
|
||||
val maxRetries: Int = 3
|
||||
)
|
||||
@@ -0,0 +1,279 @@
|
||||
package com.telecom
|
||||
|
||||
import kotlinx.serialization.*
|
||||
import java.time.*
|
||||
|
||||
@Serializable
|
||||
enum class SubscriberStatus(val value: String) {
|
||||
@SerialName("active")
|
||||
ACTIVE("active"),
|
||||
@SerialName("suspended")
|
||||
SUSPENDED("suspended"),
|
||||
@SerialName("terminated")
|
||||
TERMINATED("terminated")
|
||||
}
|
||||
|
||||
@Serializable
|
||||
enum class UsageType(val value: String) {
|
||||
@SerialName("data")
|
||||
DATA("data"),
|
||||
@SerialName("voice")
|
||||
VOICE("voice"),
|
||||
@SerialName("sms")
|
||||
SMS("sms")
|
||||
}
|
||||
|
||||
@Serializable
|
||||
enum class PaymentStatus(val value: String) {
|
||||
@SerialName("pending")
|
||||
PENDING("pending"),
|
||||
@SerialName("completed")
|
||||
COMPLETED("completed"),
|
||||
@SerialName("failed")
|
||||
FAILED("failed"),
|
||||
@SerialName("refunded")
|
||||
REFUNDED("refunded")
|
||||
}
|
||||
|
||||
@Serializable
|
||||
data class Subscriber(
|
||||
val id: Long,
|
||||
val imsi: String,
|
||||
val msisdn: String,
|
||||
val firstName: String,
|
||||
val lastName: String,
|
||||
val email: String,
|
||||
val organizationId: String? = null,
|
||||
val status: SubscriberStatus,
|
||||
val planId: Long,
|
||||
val balance: Double,
|
||||
val createdAt: LocalDateTime,
|
||||
val updatedAt: LocalDateTime
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class SubscriberList(
|
||||
val subscribers: List<Subscriber>,
|
||||
val total: Long,
|
||||
val page: Int,
|
||||
val pageSize: Int,
|
||||
val hasNext: Boolean,
|
||||
val hasPrev: Boolean
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class CreateSubscriberRequest(
|
||||
val imsi: String,
|
||||
val msisdn: String,
|
||||
val firstName: String,
|
||||
val lastName: String,
|
||||
val email: String,
|
||||
val planId: Long,
|
||||
val organizationId: String? = null
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class UpdateSubscriberRequest(
|
||||
val firstName: String? = null,
|
||||
val lastName: String? = null,
|
||||
val email: String? = null,
|
||||
val planId: Long? = null,
|
||||
val status: SubscriberStatus? = null
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class UsageStats(
|
||||
val subscriberId: String,
|
||||
val dataUp: Long,
|
||||
val dataDown: Long,
|
||||
val voiceSeconds: Long,
|
||||
val smsCount: Long,
|
||||
val periodStart: LocalDateTime,
|
||||
val periodEnd: LocalDateTime,
|
||||
val cost: Double
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class UsageEvent(
|
||||
val id: String,
|
||||
val subscriberId: String,
|
||||
val usageType: UsageType,
|
||||
val amount: Long,
|
||||
val cost: Double,
|
||||
val timestamp: LocalDateTime,
|
||||
val metadata: Map<String, @Serializable(with = AnySerializer::class) Any>? = null
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class UsageEventList(
|
||||
val events: List<UsageEvent>,
|
||||
val total: Long,
|
||||
val page: Int,
|
||||
val pageSize: Int,
|
||||
val hasNext: Boolean,
|
||||
val hasPrev: Boolean
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class CurrentSession(
|
||||
val sessionId: String,
|
||||
val startTime: LocalDateTime,
|
||||
val dataUp: Long,
|
||||
val dataDown: Long,
|
||||
val voiceSeconds: Long,
|
||||
val smsCount: Long
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class RealTimeUsage(
|
||||
val currentSession: CurrentSession? = null,
|
||||
val todayUsage: Map<String, Long>? = null
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class PaymentTransaction(
|
||||
val id: String,
|
||||
val subscriberId: String,
|
||||
val amount: Double,
|
||||
val currency: String,
|
||||
val status: PaymentStatus,
|
||||
val gateway: String,
|
||||
val transactionId: String? = null,
|
||||
val createdAt: LocalDateTime,
|
||||
val completedAt: LocalDateTime? = null,
|
||||
val metadata: Map<String, @Serializable(with = AnySerializer::class) Any>? = null
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class PaymentTransactionList(
|
||||
val transactions: List<PaymentTransaction>,
|
||||
val total: Long,
|
||||
val page: Int,
|
||||
val pageSize: Int,
|
||||
val hasNext: Boolean,
|
||||
val hasPrev: Boolean
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class CreatePaymentRequest(
|
||||
val subscriberId: String,
|
||||
val amount: Double,
|
||||
val currency: String = "USD",
|
||||
val gateway: String = "stripe",
|
||||
val metadata: Map<String, @Serializable(with = AnySerializer::class) Any>? = null
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class RatingPlan(
|
||||
val planId: String,
|
||||
val name: String,
|
||||
val dataRate: Double,
|
||||
val voiceRate: Double,
|
||||
val smsRate: Double,
|
||||
val monthlyFee: Double,
|
||||
val dataLimit: Long,
|
||||
val voiceLimit: Long,
|
||||
val smsLimit: Long
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class SystemStats(
|
||||
val activeSessions: Long,
|
||||
val totalAccounts: Long,
|
||||
val blockedUsers: Long,
|
||||
val lowBalanceAlerts: Long,
|
||||
val uptime: Double,
|
||||
val cpuUsage: Double,
|
||||
val memoryUsage: Double
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class HealthStatus(
|
||||
val status: String,
|
||||
val timestamp: LocalDateTime,
|
||||
val checks: Map<String, @Serializable(with = AnySerializer::class) Any>,
|
||||
val uptime: Double
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class WebSocketMessage(
|
||||
val type: String,
|
||||
val data: Map<String, @Serializable(with = AnySerializer::class) Any>,
|
||||
val timestamp: LocalDateTime
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class GraphQLRequest(
|
||||
val query: String,
|
||||
val variables: Map<String, @Serializable(with = AnySerializer::class) Any> = emptyMap()
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class GraphQLResponse(
|
||||
val data: Map<String, @Serializable(with = AnySerializer::class) Any>? = null,
|
||||
val errors: List<GraphQLError>? = null
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class GraphQLError(
|
||||
val message: String,
|
||||
val locations: List<GraphQLErrorLocation>? = null,
|
||||
val path: List<String>? = null,
|
||||
val extensions: Map<String, @Serializable(with = AnySerializer::class) Any>? = null
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class GraphQLErrorLocation(
|
||||
val line: Int,
|
||||
val column: Int
|
||||
)
|
||||
|
||||
object AnySerializer : KSerializer<Any> {
|
||||
override val descriptor: SerialDescriptor = buildClassSerialDescriptor("Any") {
|
||||
element("value")
|
||||
}
|
||||
|
||||
override fun serialize(encoder: Encoder, value: Any) {
|
||||
when (value) {
|
||||
is String -> encoder.encodeString(value)
|
||||
is Int -> encoder.encodeInt(value)
|
||||
is Long -> encoder.encodeLong(value)
|
||||
is Double -> encoder.encodeDouble(value)
|
||||
is Float -> encoder.encodeDouble(value.toDouble())
|
||||
is Boolean -> encoder.encodeBoolean(value)
|
||||
else -> throw IllegalArgumentException("Unsupported type: ${value::class}")
|
||||
}
|
||||
}
|
||||
|
||||
override fun deserialize(decoder: Decoder): Any {
|
||||
return when (val elementValue = decoder.decodeSerializableValue(JsonElement.serializer())) {
|
||||
is JsonPrimitive -> {
|
||||
when {
|
||||
elementValue.isString -> elementValue.content
|
||||
elementValue.content.contains('.') -> elementValue.content.toDouble()
|
||||
else -> elementValue.content.toLong()
|
||||
}
|
||||
}
|
||||
is JsonArray -> elementValue.map { AnySerializer.deserialize(JsonDecoder(it)) }
|
||||
is JsonObject -> elementValue.mapValues { AnySerializer.deserialize(JsonDecoder(it.value)) }
|
||||
else -> throw IllegalArgumentException("Unsupported JSON element type")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
sealed class TelecomException(message: String, cause: Throwable? = null) : Exception(message, cause) {
|
||||
|
||||
class AuthenticationError(message: String) : TelecomException(message)
|
||||
|
||||
class APIError(message: String) : TelecomException(message)
|
||||
|
||||
class NetworkError(message: String) : TelecomException(message)
|
||||
|
||||
class ValidationError(message: String) : TelecomException(message)
|
||||
|
||||
class RateLimitError(message: String) : TelecomException(message)
|
||||
|
||||
class ServerError(message: String) : TelecomException(message)
|
||||
|
||||
class WebSocketError(message: String) : TelecomException(message)
|
||||
}
|
||||
@@ -0,0 +1,300 @@
|
||||
# Telecom Platform Python SDK
|
||||
|
||||
Async Python SDK for the Telecom Platform with full type hints and modern async/await support.
|
||||
|
||||
## Features
|
||||
|
||||
- **Async/Await Support**: Full async/await support with aiohttp
|
||||
- **Type Safety**: Complete type hints with Pydantic models
|
||||
- **WebSocket Support**: Real-time updates via WebSocket
|
||||
- **GraphQL Support**: Execute GraphQL queries
|
||||
- **Retry Logic**: Built-in retry mechanism with exponential backoff
|
||||
- **Error Handling**: Comprehensive error handling with custom exceptions
|
||||
- **Logging**: Optional debug logging
|
||||
- **Context Manager**: Async context manager support
|
||||
|
||||
## Installation
|
||||
|
||||
```bash
|
||||
pip install telecom-sdk
|
||||
```
|
||||
|
||||
Or install from source:
|
||||
|
||||
```bash
|
||||
git clone https://github.com/nutcas3/telecom-platform.git
|
||||
cd telecom-platform/sdk/python
|
||||
# Use uv for faster installation (recommended)
|
||||
uv pip install -r requirements.txt
|
||||
uv pip install -e .
|
||||
# Or traditional pip
|
||||
pip install -r requirements.txt
|
||||
pip install -e .
|
||||
```
|
||||
|
||||
## Quick Start
|
||||
|
||||
```python
|
||||
import asyncio
|
||||
from telecom_sdk import TelecomSDK
|
||||
|
||||
async def main():
|
||||
async with TelecomSDK(
|
||||
api_url="http://localhost:8000",
|
||||
api_key="your-api-key",
|
||||
enable_logging=True
|
||||
) as sdk:
|
||||
|
||||
# Get subscriber
|
||||
subscriber = await sdk.get_subscriber(1)
|
||||
print(f"Subscriber: {subscriber.first_name} {subscriber.last_name}")
|
||||
|
||||
# Get usage stats
|
||||
from datetime import datetime, timedelta
|
||||
end_date = datetime.now()
|
||||
start_date = end_date - timedelta(days=30)
|
||||
|
||||
usage = await sdk.get_usage_stats(1, start_date, end_date)
|
||||
print(f"Data usage: {usage.data_up + usage.data_down} bytes")
|
||||
|
||||
# Create payment transaction
|
||||
transaction = await sdk.create_payment_transaction(
|
||||
subscriber_id=1,
|
||||
amount=25.00,
|
||||
currency="USD"
|
||||
)
|
||||
print(f"Transaction ID: {transaction.id}")
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
```
|
||||
|
||||
## WebSocket Example
|
||||
|
||||
```python
|
||||
import asyncio
|
||||
from telecom_sdk import TelecomSDK, WebSocketMessage
|
||||
|
||||
async def handle_message(message: WebSocketMessage):
|
||||
"""Handle WebSocket messages."""
|
||||
print(f"Received message type: {message.type}")
|
||||
print(f"Message data: {message.data}")
|
||||
|
||||
async def main():
|
||||
sdk = TelecomSDK(api_url="http://localhost:8000")
|
||||
|
||||
# Connect to WebSocket
|
||||
await sdk.connect_websocket(handle_message)
|
||||
|
||||
# Keep running
|
||||
try:
|
||||
while True:
|
||||
await asyncio.sleep(1)
|
||||
except KeyboardInterrupt:
|
||||
await sdk.disconnect_websocket()
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
```
|
||||
|
||||
## GraphQL Example
|
||||
|
||||
```python
|
||||
import asyncio
|
||||
from telecom_sdk import TelecomSDK
|
||||
|
||||
async def main():
|
||||
async with TelecomSDK(api_url="http://localhost:8000") as sdk:
|
||||
|
||||
query = """
|
||||
query GetSubscribers($first: Int, $after: String) {
|
||||
subscribers(first: $first, after: $after) {
|
||||
edges {
|
||||
node {
|
||||
id
|
||||
firstName
|
||||
lastName
|
||||
email
|
||||
status
|
||||
}
|
||||
}
|
||||
pageInfo {
|
||||
hasNextPage
|
||||
endCursor
|
||||
}
|
||||
}
|
||||
}
|
||||
"""
|
||||
|
||||
variables = {"first": 10}
|
||||
result = await sdk.execute_graphql_query(query, variables)
|
||||
|
||||
for edge in result["data"]["subscribers"]["edges"]:
|
||||
subscriber = edge["node"]
|
||||
print(f"{subscriber['firstName']} {subscriber['lastName']}")
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
### Subscriber Management
|
||||
|
||||
```python
|
||||
# Get subscriber
|
||||
subscriber = await sdk.get_subscriber(subscriber_id)
|
||||
|
||||
# List subscribers
|
||||
subscribers = await sdk.list_subscribers(page=1, page_size=50, status="active")
|
||||
|
||||
# Create subscriber
|
||||
subscriber = await sdk.create_subscriber(
|
||||
imsi="208930000000001",
|
||||
msisdn="+1234567890",
|
||||
first_name="John",
|
||||
last_name="Doe",
|
||||
email="[email protected]",
|
||||
plan_id=1
|
||||
)
|
||||
|
||||
# Update subscriber
|
||||
subscriber = await sdk.update_subscriber(subscriber_id, first_name="Jane")
|
||||
|
||||
# Suspend subscriber
|
||||
subscriber = await sdk.suspend_subscriber(subscriber_id)
|
||||
|
||||
# Activate subscriber
|
||||
subscriber = await sdk.activate_subscriber(subscriber_id)
|
||||
|
||||
# Terminate subscriber
|
||||
success = await sdk.terminate_subscriber(subscriber_id)
|
||||
```
|
||||
|
||||
### Usage Management
|
||||
|
||||
```python
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
# Get usage stats
|
||||
usage = await sdk.get_usage_stats(
|
||||
subscriber_id=1,
|
||||
start_date=datetime.now() - timedelta(days=30),
|
||||
end_date=datetime.now()
|
||||
)
|
||||
|
||||
# List usage events
|
||||
events = await sdk.list_usage_events(
|
||||
subscriber_id=1,
|
||||
usage_type="data",
|
||||
page=1,
|
||||
page_size=50
|
||||
)
|
||||
|
||||
# Get real-time usage
|
||||
realtime = await sdk.get_real_time_usage(subscriber_id)
|
||||
```
|
||||
|
||||
### Payment Management
|
||||
|
||||
```python
|
||||
# Create payment transaction
|
||||
transaction = await sdk.create_payment_transaction(
|
||||
subscriber_id=1,
|
||||
amount=25.00,
|
||||
currency="USD",
|
||||
gateway="stripe",
|
||||
metadata={"description": "Monthly plan"}
|
||||
)
|
||||
|
||||
# Get transaction
|
||||
transaction = await sdk.get_payment_transaction(transaction_id)
|
||||
|
||||
# List transactions
|
||||
transactions = await sdk.list_payment_transactions(
|
||||
subscriber_id=1,
|
||||
status="completed"
|
||||
)
|
||||
```
|
||||
|
||||
### System Management
|
||||
|
||||
```python
|
||||
# Get system stats
|
||||
stats = await sdk.get_system_stats()
|
||||
|
||||
# Get health status
|
||||
health = await sdk.get_health_status()
|
||||
```
|
||||
|
||||
### Rating Plans
|
||||
|
||||
```python
|
||||
# List rating plans
|
||||
plans = await sdk.list_rating_plans()
|
||||
|
||||
# Get specific plan
|
||||
plan = await sdk.get_rating_plan(plan_id)
|
||||
```
|
||||
|
||||
## Error Handling
|
||||
|
||||
```python
|
||||
from telecom_sdk import TelecomSDK, AuthenticationError, APIError, NetworkError
|
||||
|
||||
async def main():
|
||||
try:
|
||||
async with TelecomSDK(api_url="http://localhost:8000") as sdk:
|
||||
subscriber = await sdk.get_subscriber(1)
|
||||
except AuthenticationError:
|
||||
print("Authentication failed")
|
||||
except APIError as e:
|
||||
print(f"API error: {e}")
|
||||
except NetworkError as e:
|
||||
print(f"Network error: {e}")
|
||||
except TelecomError as e:
|
||||
print(f"Telecom error: {e}")
|
||||
```
|
||||
|
||||
## Configuration
|
||||
|
||||
The SDK can be configured with the following options:
|
||||
|
||||
- `api_url`: Base URL for the API server (default: "http://localhost:8000")
|
||||
- `api_key`: API key for authentication
|
||||
- `timeout`: Request timeout in seconds (default: 30)
|
||||
- `max_retries`: Maximum number of retry attempts (default: 3)
|
||||
- `retry_delay`: Delay between retries in seconds (default: 1.0)
|
||||
- `enable_logging`: Enable debug logging (default: False)
|
||||
|
||||
## Development
|
||||
|
||||
### Running Tests
|
||||
|
||||
```bash
|
||||
# Install development dependencies
|
||||
pip install -r requirements-dev.txt
|
||||
|
||||
# Run tests
|
||||
pytest tests/
|
||||
|
||||
# Run tests with coverage
|
||||
pytest tests/ --cov=telecom_sdk
|
||||
```
|
||||
|
||||
### Code Style
|
||||
|
||||
```bash
|
||||
# Format code
|
||||
black telecom_sdk/
|
||||
|
||||
# Lint code
|
||||
flake8 telecom_sdk/
|
||||
|
||||
# Type checking
|
||||
mypy telecom_sdk/
|
||||
```
|
||||
|
||||
## License
|
||||
|
||||
This SDK is licensed under the MIT License.
|
||||
@@ -0,0 +1,6 @@
|
||||
# Install with: uv pip install -r requirements.txt
|
||||
# Or traditional: pip install -r requirements.txt
|
||||
aiohttp>=3.8.0
|
||||
websockets>=11.0.0
|
||||
pydantic>=2.0.0
|
||||
typing-extensions>=4.0.0
|
||||
@@ -0,0 +1,30 @@
|
||||
from .client import TelecomSDK
|
||||
from .types import (
|
||||
Subscriber,
|
||||
UsageStats,
|
||||
PaymentTransaction,
|
||||
SystemStats,
|
||||
HealthStatus,
|
||||
WebSocketMessage,
|
||||
)
|
||||
from .exceptions import (
|
||||
TelecomError,
|
||||
AuthenticationError,
|
||||
APIError,
|
||||
NetworkError,
|
||||
)
|
||||
|
||||
__version__ = "1.0.0"
|
||||
__all__ = [
|
||||
"TelecomSDK",
|
||||
"Subscriber",
|
||||
"UsageStats",
|
||||
"PaymentTransaction",
|
||||
"SystemStats",
|
||||
"HealthStatus",
|
||||
"WebSocketMessage",
|
||||
"TelecomError",
|
||||
"AuthenticationError",
|
||||
"APIError",
|
||||
"NetworkError",
|
||||
]
|
||||
@@ -0,0 +1,422 @@
|
||||
import asyncio
|
||||
import json
|
||||
import logging
|
||||
from typing import Optional, Dict, Any, List, Union
|
||||
from datetime import datetime
|
||||
import aiohttp
|
||||
import websockets
|
||||
from websockets.exceptions import ConnectionClosed
|
||||
|
||||
from .types import (
|
||||
Subscriber,
|
||||
UsageStats,
|
||||
PaymentTransaction,
|
||||
SystemStats,
|
||||
HealthStatus,
|
||||
WebSocketMessage,
|
||||
UsageEvent,
|
||||
RatingPlan,
|
||||
RealTimeUsage,
|
||||
PaginatedResponse,
|
||||
)
|
||||
from .exceptions import (
|
||||
TelecomError,
|
||||
AuthenticationError,
|
||||
APIError,
|
||||
NetworkError,
|
||||
ValidationError,
|
||||
RateLimitError,
|
||||
ServerError,
|
||||
)
|
||||
|
||||
|
||||
class TelecomSDK:
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
api_url: str = "http://localhost:8000",
|
||||
api_key: Optional[str] = None,
|
||||
timeout: int = 30,
|
||||
max_retries: int = 3,
|
||||
retry_delay: float = 1.0,
|
||||
enable_logging: bool = False,
|
||||
):
|
||||
"""
|
||||
Initialize the Telecom SDK client.
|
||||
|
||||
Args:
|
||||
api_url: Base URL for the API server
|
||||
api_key: API key for authentication
|
||||
timeout: Request timeout in seconds
|
||||
max_retries: Maximum number of retry attempts
|
||||
retry_delay: Delay between retries in seconds
|
||||
enable_logging: Enable debug logging
|
||||
"""
|
||||
self.api_url = api_url.rstrip('/')
|
||||
self.api_key = api_key
|
||||
self.timeout = aiohttp.ClientTimeout(total=timeout)
|
||||
self.max_retries = max_retries
|
||||
self.retry_delay = retry_delay
|
||||
self.enable_logging = enable_logging
|
||||
|
||||
if enable_logging:
|
||||
logging.basicConfig(level=logging.DEBUG)
|
||||
self.logger = logging.getLogger(__name__)
|
||||
else:
|
||||
self.logger = logging.getLogger(__name__)
|
||||
self.logger.disabled = True
|
||||
|
||||
self._session: Optional[aiohttp.ClientSession] = None
|
||||
self._websocket: Optional[websockets.WebSocketServerProtocol] = None
|
||||
|
||||
async def __aenter__(self):
|
||||
"""Async context manager entry."""
|
||||
await self._ensure_session()
|
||||
return self
|
||||
|
||||
async def __aexit__(self, exc_type, exc_val, exc_tb):
|
||||
"""Async context manager exit."""
|
||||
await self.close()
|
||||
|
||||
async def _ensure_session(self):
|
||||
"""Ensure aiohttp session is created."""
|
||||
if self._session is None or self._session.closed:
|
||||
headers = self._get_headers()
|
||||
self._session = aiohttp.ClientSession(
|
||||
headers=headers,
|
||||
timeout=self.timeout,
|
||||
)
|
||||
|
||||
def _get_headers(self) -> Dict[str, str]:
|
||||
"""Get default headers for API requests."""
|
||||
headers = {
|
||||
"Content-Type": "application/json",
|
||||
"User-Agent": f"Telecom-Python-SDK/1.0.0",
|
||||
}
|
||||
if self.api_key:
|
||||
headers["Authorization"] = f"Bearer {self.api_key}"
|
||||
return headers
|
||||
|
||||
async def _make_request(
|
||||
self,
|
||||
method: str,
|
||||
endpoint: str,
|
||||
data: Optional[Dict[str, Any]] = None,
|
||||
params: Optional[Dict[str, str]] = None,
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
Make an HTTP request with retry logic.
|
||||
|
||||
Args:
|
||||
method: HTTP method
|
||||
endpoint: API endpoint
|
||||
data: Request body data
|
||||
params: Query parameters
|
||||
|
||||
Returns:
|
||||
Response data as dictionary
|
||||
|
||||
Raises:
|
||||
TelecomError: On API errors
|
||||
"""
|
||||
await self._ensure_session()
|
||||
url = f"{self.api_url}{endpoint}"
|
||||
|
||||
for attempt in range(self.max_retries + 1):
|
||||
try:
|
||||
self.logger.debug(f"Making {method} request to {url} (attempt {attempt + 1})")
|
||||
|
||||
async with self._session.request(
|
||||
method=method,
|
||||
url=url,
|
||||
json=data,
|
||||
params=params,
|
||||
) as response:
|
||||
await self._handle_response_errors(response)
|
||||
return await response.json()
|
||||
|
||||
except aiohttp.ClientError as e:
|
||||
if attempt == self.max_retries:
|
||||
raise NetworkError(f"Network error after {self.max_retries} retries: {e}")
|
||||
|
||||
self.logger.warning(f"Request failed (attempt {attempt + 1}), retrying in {self.retry_delay}s: {e}")
|
||||
await asyncio.sleep(self.retry_delay * (2 ** attempt))
|
||||
|
||||
async def _handle_response_errors(self, response: aiohttp.ClientResponse):
|
||||
"""Handle HTTP response errors."""
|
||||
if response.status == 401:
|
||||
raise AuthenticationError("Authentication failed")
|
||||
elif response.status == 429:
|
||||
raise RateLimitError("Rate limit exceeded")
|
||||
elif 400 <= response.status < 500:
|
||||
error_data = await response.json().get("error", "Bad request")
|
||||
raise APIError(f"API error: {error_data}", response.status)
|
||||
elif response.status >= 500:
|
||||
raise ServerError(f"Server error: {response.status}", response.status)
|
||||
|
||||
# Subscriber Management
|
||||
|
||||
async def get_subscriber(self, subscriber_id: int) -> Subscriber:
|
||||
"""Get subscriber by ID."""
|
||||
response = await self._make_request("GET", f"/v1/subscribers/{subscriber_id}")
|
||||
return Subscriber(**response)
|
||||
|
||||
async def list_subscribers(
|
||||
self,
|
||||
page: int = 1,
|
||||
page_size: int = 50,
|
||||
status: Optional[str] = None,
|
||||
) -> PaginatedResponse:
|
||||
"""List subscribers with pagination."""
|
||||
params = {"page": page, "page_size": page_size}
|
||||
if status:
|
||||
params["status"] = status
|
||||
|
||||
response = await self._make_request("GET", "/v1/subscribers", params=params)
|
||||
return PaginatedResponse(**response)
|
||||
|
||||
async def create_subscriber(
|
||||
self,
|
||||
imsi: str,
|
||||
msisdn: str,
|
||||
first_name: str,
|
||||
last_name: str,
|
||||
email: str,
|
||||
plan_id: int,
|
||||
organization_id: Optional[str] = None,
|
||||
) -> Subscriber:
|
||||
"""Create a new subscriber."""
|
||||
data = {
|
||||
"imsi": imsi,
|
||||
"msisdn": msisdn,
|
||||
"first_name": first_name,
|
||||
"last_name": last_name,
|
||||
"email": email,
|
||||
"plan_id": plan_id,
|
||||
}
|
||||
if organization_id:
|
||||
data["organization_id"] = organization_id
|
||||
|
||||
response = await self._make_request("POST", "/v1/subscribers", data=data)
|
||||
return Subscriber(**response)
|
||||
|
||||
async def update_subscriber(
|
||||
self,
|
||||
subscriber_id: int,
|
||||
**kwargs,
|
||||
) -> Subscriber:
|
||||
"""Update subscriber details."""
|
||||
response = await self._make_request(
|
||||
"PUT",
|
||||
f"/v1/subscribers/{subscriber_id}",
|
||||
data=kwargs,
|
||||
)
|
||||
return Subscriber(**response)
|
||||
|
||||
async def suspend_subscriber(self, subscriber_id: int) -> Subscriber:
|
||||
"""Suspend a subscriber."""
|
||||
response = await self._make_request("POST", f"/v1/subscribers/{subscriber_id}/suspend")
|
||||
return Subscriber(**response)
|
||||
|
||||
async def activate_subscriber(self, subscriber_id: int) -> Subscriber:
|
||||
"""Activate a suspended subscriber."""
|
||||
response = await self._make_request("POST", f"/v1/subscribers/{subscriber_id}/activate")
|
||||
return Subscriber(**response)
|
||||
|
||||
async def terminate_subscriber(self, subscriber_id: int) -> bool:
|
||||
"""Terminate a subscriber."""
|
||||
response = await self._make_request("DELETE", f"/v1/subscribers/{subscriber_id}")
|
||||
return response.get("success", False)
|
||||
|
||||
# Usage Management
|
||||
|
||||
async def get_usage_stats(
|
||||
self,
|
||||
subscriber_id: int,
|
||||
start_date: datetime,
|
||||
end_date: datetime,
|
||||
) -> UsageStats:
|
||||
"""Get usage statistics for a subscriber."""
|
||||
params = {
|
||||
"start_date": start_date.isoformat(),
|
||||
"end_date": end_date.isoformat(),
|
||||
}
|
||||
response = await self._make_request(
|
||||
"GET",
|
||||
f"/v1/subscribers/{subscriber_id}/usage",
|
||||
params=params,
|
||||
)
|
||||
return UsageStats(**response)
|
||||
|
||||
async def list_usage_events(
|
||||
self,
|
||||
subscriber_id: Optional[int] = None,
|
||||
usage_type: Optional[str] = None,
|
||||
start_date: Optional[datetime] = None,
|
||||
end_date: Optional[datetime] = None,
|
||||
page: int = 1,
|
||||
page_size: int = 50,
|
||||
) -> PaginatedResponse:
|
||||
"""List usage events with filtering."""
|
||||
params = {"page": page, "page_size": page_size}
|
||||
|
||||
if subscriber_id:
|
||||
params["subscriber_id"] = subscriber_id
|
||||
if usage_type:
|
||||
params["usage_type"] = usage_type
|
||||
if start_date:
|
||||
params["start_date"] = start_date.isoformat()
|
||||
if end_date:
|
||||
params["end_date"] = end_date.isoformat()
|
||||
|
||||
response = await self._make_request("GET", "/v1/usage/events", params=params)
|
||||
return PaginatedResponse(**response)
|
||||
|
||||
async def get_real_time_usage(self, subscriber_id: int) -> RealTimeUsage:
|
||||
"""Get real-time usage for a subscriber."""
|
||||
response = await self._make_request("GET", f"/v1/subscribers/{subscriber_id}/realtime")
|
||||
return RealTimeUsage(**response)
|
||||
|
||||
# Payment Management
|
||||
|
||||
async def create_payment_transaction(
|
||||
self,
|
||||
subscriber_id: int,
|
||||
amount: float,
|
||||
currency: str = "USD",
|
||||
gateway: str = "stripe",
|
||||
metadata: Optional[Dict[str, Any]] = None,
|
||||
) -> PaymentTransaction:
|
||||
"""Create a payment transaction."""
|
||||
data = {
|
||||
"subscriber_id": subscriber_id,
|
||||
"amount": amount,
|
||||
"currency": currency,
|
||||
"gateway": gateway,
|
||||
}
|
||||
if metadata:
|
||||
data["metadata"] = metadata
|
||||
|
||||
response = await self._make_request("POST", "/v1/payments/transactions", data=data)
|
||||
return PaymentTransaction(**response)
|
||||
|
||||
async def get_payment_transaction(self, transaction_id: str) -> PaymentTransaction:
|
||||
"""Get payment transaction by ID."""
|
||||
response = await self._make_request("GET", f"/v1/payments/transactions/{transaction_id}")
|
||||
return PaymentTransaction(**response)
|
||||
|
||||
async def list_payment_transactions(
|
||||
self,
|
||||
subscriber_id: Optional[int] = None,
|
||||
status: Optional[str] = None,
|
||||
page: int = 1,
|
||||
page_size: int = 50,
|
||||
) -> PaginatedResponse:
|
||||
"""List payment transactions with filtering."""
|
||||
params = {"page": page, "page_size": page_size}
|
||||
|
||||
if subscriber_id:
|
||||
params["subscriber_id"] = subscriber_id
|
||||
if status:
|
||||
params["status"] = status
|
||||
|
||||
response = await self._make_request("GET", "/v1/payments/transactions", params=params)
|
||||
return PaginatedResponse(**response)
|
||||
|
||||
# Rating Plans
|
||||
|
||||
async def list_rating_plans(self) -> List[RatingPlan]:
|
||||
"""List all available rating plans."""
|
||||
response = await self._make_request("GET", "/v1/rating-plans")
|
||||
return [RatingPlan(**plan) for plan in response]
|
||||
|
||||
async def get_rating_plan(self, plan_id: str) -> RatingPlan:
|
||||
"""Get rating plan by ID."""
|
||||
response = await self._make_request("GET", f"/v1/rating-plans/{plan_id}")
|
||||
return RatingPlan(**response)
|
||||
|
||||
# System Management
|
||||
|
||||
async def get_system_stats(self) -> SystemStats:
|
||||
"""Get system statistics."""
|
||||
response = await self._make_request("GET", "/v1/system/stats")
|
||||
return SystemStats(**response)
|
||||
|
||||
async def get_health_status(self) -> HealthStatus:
|
||||
"""Get system health status."""
|
||||
response = await self._make_request("GET", "/v1/health")
|
||||
return HealthStatus(**response)
|
||||
|
||||
# WebSocket Support
|
||||
|
||||
async def connect_websocket(self, message_handler: callable) -> None:
|
||||
"""
|
||||
Connect to WebSocket for real-time updates.
|
||||
|
||||
Args:
|
||||
message_handler: Async function to handle WebSocket messages
|
||||
"""
|
||||
ws_url = self.api_url.replace("http://", "ws://") + "/ws"
|
||||
|
||||
try:
|
||||
self._websocket = await websockets.connect(ws_url)
|
||||
self.logger.info("WebSocket connected")
|
||||
|
||||
async for message in self._websocket:
|
||||
try:
|
||||
data = json.loads(message)
|
||||
ws_message = WebSocketMessage(**data)
|
||||
await message_handler(ws_message)
|
||||
except json.JSONDecodeError as e:
|
||||
self.logger.error(f"Failed to decode WebSocket message: {e}")
|
||||
except Exception as e:
|
||||
self.logger.error(f"Error handling WebSocket message: {e}")
|
||||
|
||||
except ConnectionClosed:
|
||||
self.logger.warning("WebSocket connection closed")
|
||||
except Exception as e:
|
||||
self.logger.error(f"WebSocket error: {e}")
|
||||
raise NetworkError(f"WebSocket connection failed: {e}")
|
||||
|
||||
async def disconnect_websocket(self) -> None:
|
||||
"""Disconnect from WebSocket."""
|
||||
if self._websocket:
|
||||
await self._websocket.close()
|
||||
self._websocket = None
|
||||
self.logger.info("WebSocket disconnected")
|
||||
|
||||
# GraphQL Support
|
||||
|
||||
async def execute_graphql_query(
|
||||
self,
|
||||
query: str,
|
||||
variables: Optional[Dict[str, Any]] = None,
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
Execute a GraphQL query.
|
||||
|
||||
Args:
|
||||
query: GraphQL query string
|
||||
variables: Optional variables for the query
|
||||
|
||||
Returns:
|
||||
GraphQL response data
|
||||
"""
|
||||
data = {"query": query}
|
||||
if variables:
|
||||
data["variables"] = variables
|
||||
|
||||
response = await self._make_request("POST", "/graphql", data=data)
|
||||
return response
|
||||
|
||||
# Utility Methods
|
||||
|
||||
async def close(self) -> None:
|
||||
"""Close the SDK client and cleanup resources."""
|
||||
if self._websocket:
|
||||
await self.disconnect_websocket()
|
||||
|
||||
if self._session and not self._session.closed:
|
||||
await self._session.close()
|
||||
|
||||
self.logger.info("Telecom SDK client closed")
|
||||
@@ -0,0 +1,31 @@
|
||||
class TelecomError(Exception):
|
||||
"""Base exception for all Telecom SDK errors."""
|
||||
|
||||
def __init__(self, message: str, status_code: Optional[int] = None, response_data: Optional[dict] = None):
|
||||
super().__init__(message)
|
||||
self.status_code = status_code
|
||||
self.response_data = response_data or {}
|
||||
|
||||
|
||||
class AuthenticationError(TelecomError):
|
||||
pass
|
||||
|
||||
|
||||
class APIError(TelecomError):
|
||||
pass
|
||||
|
||||
|
||||
class NetworkError(TelecomError):
|
||||
pass
|
||||
|
||||
|
||||
class ValidationError(TelecomError):
|
||||
pass
|
||||
|
||||
|
||||
class RateLimitError(TelecomError):
|
||||
pass
|
||||
|
||||
|
||||
class ServerError(TelecomError):
|
||||
pass
|
||||
@@ -0,0 +1,141 @@
|
||||
from typing import Optional, Dict, Any, List
|
||||
from datetime import datetime
|
||||
from pydantic import BaseModel, Field
|
||||
from enum import Enum
|
||||
|
||||
class SubscriberStatus(str, Enum):
|
||||
ACTIVE = "active"
|
||||
SUSPENDED = "suspended"
|
||||
TERMINATED = "terminated"
|
||||
|
||||
class UsageType(str, Enum):
|
||||
DATA = "data"
|
||||
VOICE = "voice"
|
||||
SMS = "sms"
|
||||
|
||||
|
||||
class PaymentStatus(str, Enum):
|
||||
PENDING = "pending"
|
||||
COMPLETED = "completed"
|
||||
FAILED = "failed"
|
||||
REFUNDED = "refunded"
|
||||
|
||||
|
||||
class Subscriber(BaseModel):
|
||||
id: int
|
||||
imsi: str
|
||||
msisdn: str
|
||||
first_name: str
|
||||
last_name: str
|
||||
email: str
|
||||
organization_id: Optional[str] = None
|
||||
status: SubscriberStatus
|
||||
plan_id: int
|
||||
balance: float = Field(ge=0.0)
|
||||
created_at: datetime
|
||||
updated_at: datetime
|
||||
|
||||
class Config:
|
||||
use_enum_values = True
|
||||
|
||||
|
||||
class UsageStats(BaseModel):
|
||||
subscriber_id: str
|
||||
data_up: int = Field(ge=0)
|
||||
data_down: int = Field(ge=0)
|
||||
voice_seconds: int = Field(ge=0)
|
||||
sms_count: int = Field(ge=0)
|
||||
period_start: datetime
|
||||
period_end: datetime
|
||||
cost: float = Field(ge=0.0)
|
||||
|
||||
|
||||
class PaymentTransaction(BaseModel):
|
||||
id: str
|
||||
subscriber_id: str
|
||||
amount: float = Field(ge=0.0)
|
||||
currency: str = Field(min_length=3, max_length=3)
|
||||
status: PaymentStatus
|
||||
gateway: str
|
||||
transaction_id: Optional[str] = None
|
||||
created_at: datetime
|
||||
completed_at: Optional[datetime] = None
|
||||
metadata: Optional[Dict[str, Any]] = None
|
||||
|
||||
|
||||
class SystemStats(BaseModel):
|
||||
active_sessions: int = Field(ge=0)
|
||||
total_accounts: int = Field(ge=0)
|
||||
blocked_users: int = Field(ge=0)
|
||||
low_balance_alerts: int = Field(ge=0)
|
||||
uptime: float = Field(ge=0.0)
|
||||
cpu_usage: float = Field(ge=0.0, le=100.0)
|
||||
memory_usage: float = Field(ge=0.0, le=100.0)
|
||||
|
||||
|
||||
class HealthStatus(BaseModel):
|
||||
status: str # "healthy", "degraded", "unhealthy"
|
||||
timestamp: datetime
|
||||
checks: Dict[str, Dict[str, Any]]
|
||||
uptime: float = Field(ge=0.0)
|
||||
|
||||
|
||||
class WebSocketMessage(BaseModel):
|
||||
type: str
|
||||
data: Dict[str, Any]
|
||||
timestamp: datetime
|
||||
|
||||
|
||||
class UsageEvent(BaseModel):
|
||||
id: str
|
||||
subscriber_id: str
|
||||
usage_type: UsageType
|
||||
amount: int = Field(ge=0)
|
||||
cost: float = Field(ge=0.0)
|
||||
timestamp: datetime
|
||||
metadata: Optional[Dict[str, Any]] = None
|
||||
|
||||
|
||||
class RatingPlan(BaseModel):
|
||||
plan_id: str
|
||||
name: str
|
||||
data_rate: float = Field(ge=0.0)
|
||||
voice_rate: float = Field(ge=0.0)
|
||||
sms_rate: float = Field(ge=0.0)
|
||||
monthly_fee: float = Field(ge=0.0)
|
||||
data_limit: int = Field(ge=0)
|
||||
voice_limit: int = Field(ge=0)
|
||||
sms_limit: int = Field(ge=0)
|
||||
|
||||
|
||||
class CurrentSession(BaseModel):
|
||||
session_id: str
|
||||
start_time: datetime
|
||||
data_up: int = Field(ge=0)
|
||||
data_down: int = Field(ge=0)
|
||||
voice_seconds: int = Field(ge=0)
|
||||
sms_count: int = Field(ge=0)
|
||||
|
||||
|
||||
class RealTimeUsage(BaseModel):
|
||||
"""Real-time usage model."""
|
||||
current_session: Optional[CurrentSession] = None
|
||||
today_usage: Optional[Dict[str, int]] = None
|
||||
|
||||
|
||||
class APIResponse(BaseModel):
|
||||
"""Generic API response model."""
|
||||
success: bool
|
||||
data: Optional[Any] = None
|
||||
error: Optional[str] = None
|
||||
message: Optional[str] = None
|
||||
|
||||
|
||||
class PaginatedResponse(BaseModel):
|
||||
"""Paginated response model."""
|
||||
items: List[Any]
|
||||
total: int = Field(ge=0)
|
||||
page: int = Field(ge=1)
|
||||
page_size: int = Field(ge=1, le=100)
|
||||
has_next: bool
|
||||
has_prev: bool
|
||||
@@ -0,0 +1,222 @@
|
||||
require "httparty"
|
||||
require "json"
|
||||
require "websocket-client-simple"
|
||||
require "uri"
|
||||
|
||||
module TelecomSDK
|
||||
class Error < StandardError; end
|
||||
class AuthenticationError < Error; end
|
||||
class APIError < Error; end
|
||||
class NetworkError < Error; end
|
||||
class ValidationError < Error; end
|
||||
class RateLimitError < Error; end
|
||||
class ServerError < Error; end
|
||||
|
||||
class Client
|
||||
include HTTParty
|
||||
|
||||
attr_reader :config
|
||||
|
||||
def initialize(config = {})
|
||||
@config = Config.new(config)
|
||||
self.class.base_uri @config.api_url
|
||||
self.class.default_options.update(
|
||||
timeout: @config.timeout,
|
||||
headers: {
|
||||
"User-Agent" => "Telecom-Ruby-SDK/1.0.0",
|
||||
"Content-Type" => "application/json"
|
||||
}
|
||||
)
|
||||
|
||||
if @config.api_key
|
||||
self.class.default_options[:headers]["Authorization"] = "Bearer #{@config.api_key}"
|
||||
end
|
||||
end
|
||||
|
||||
# Subscriber Management
|
||||
|
||||
def get_subscriber(id)
|
||||
response = self.class.get("/v1/subscribers/#{id}")
|
||||
handle_response(response)
|
||||
Subscriber.new(response.parsed_response)
|
||||
end
|
||||
|
||||
def list_subscribers(page: 1, page_size: 50, status: nil)
|
||||
params = { page: page, page_size: page_size }
|
||||
params[:status] = status if status
|
||||
|
||||
response = self.class.get("/v1/subscribers", query: params)
|
||||
handle_response(response)
|
||||
SubscriberList.new(response.parsed_response)
|
||||
end
|
||||
|
||||
def create_subscriber(request)
|
||||
response = self.class.post("/v1/subscribers", body: request.to_json)
|
||||
handle_response(response)
|
||||
Subscriber.new(response.parsed_response)
|
||||
end
|
||||
|
||||
def update_subscriber(id, request)
|
||||
response = self.class.put("/v1/subscribers/#{id}", body: request.to_json)
|
||||
handle_response(response)
|
||||
Subscriber.new(response.parsed_response)
|
||||
end
|
||||
|
||||
def suspend_subscriber(id)
|
||||
response = self.class.post("/v1/subscribers/#{id}/suspend")
|
||||
handle_response(response)
|
||||
Subscriber.new(response.parsed_response)
|
||||
end
|
||||
|
||||
def activate_subscriber(id)
|
||||
response = self.class.post("/v1/subscribers/#{id}/activate")
|
||||
handle_response(response)
|
||||
Subscriber.new(response.parsed_response)
|
||||
end
|
||||
|
||||
def terminate_subscriber(id)
|
||||
response = self.class.delete("/v1/subscribers/#{id}")
|
||||
handle_response(response)
|
||||
response.parsed_response["success"]
|
||||
end
|
||||
|
||||
# Usage Management
|
||||
|
||||
def get_usage_stats(subscriber_id, start_date, end_date)
|
||||
params = {
|
||||
start_date: start_date.iso8601,
|
||||
end_date: end_date.iso8601
|
||||
}
|
||||
|
||||
response = self.class.get("/v1/subscribers/#{subscriber_id}/usage", query: params)
|
||||
handle_response(response)
|
||||
UsageStats.new(response.parsed_response)
|
||||
end
|
||||
|
||||
def list_usage_events(subscriber_id: nil, usage_type: nil, start_date: nil, end_date: nil, page: 1, page_size: 50)
|
||||
params = { page: page, page_size: page_size }
|
||||
params[:subscriber_id] = subscriber_id if subscriber_id
|
||||
params[:usage_type] = usage_type if usage_type
|
||||
params[:start_date] = start_date.iso8601 if start_date
|
||||
params[:end_date] = end_date.iso8601 if end_date
|
||||
|
||||
response = self.class.get("/v1/usage/events", query: params)
|
||||
handle_response(response)
|
||||
UsageEventList.new(response.parsed_response)
|
||||
end
|
||||
|
||||
def get_real_time_usage(subscriber_id)
|
||||
response = self.class.get("/v1/subscribers/#{subscriber_id}/realtime")
|
||||
handle_response(response)
|
||||
RealTimeUsage.new(response.parsed_response)
|
||||
end
|
||||
|
||||
# Payment Management
|
||||
|
||||
def create_payment_transaction(request)
|
||||
response = self.class.post("/v1/payments/transactions", body: request.to_json)
|
||||
handle_response(response)
|
||||
PaymentTransaction.new(response.parsed_response)
|
||||
end
|
||||
|
||||
def get_payment_transaction(transaction_id)
|
||||
response = self.class.get("/v1/payments/transactions/#{transaction_id}")
|
||||
handle_response(response)
|
||||
PaymentTransaction.new(response.parsed_response)
|
||||
end
|
||||
|
||||
def list_payment_transactions(subscriber_id: nil, status: nil, page: 1, page_size: 50)
|
||||
params = { page: page, page_size: page_size }
|
||||
params[:subscriber_id] = subscriber_id if subscriber_id
|
||||
params[:status] = status if status
|
||||
|
||||
response = self.class.get("/v1/payments/transactions", query: params)
|
||||
handle_response(response)
|
||||
PaymentTransactionList.new(response.parsed_response)
|
||||
end
|
||||
|
||||
# Rating Plans
|
||||
|
||||
def list_rating_plans
|
||||
response = self.class.get("/v1/rating-plans")
|
||||
handle_response(response)
|
||||
response.parsed_response.map { |plan| RatingPlan.new(plan) }
|
||||
end
|
||||
|
||||
def get_rating_plan(plan_id)
|
||||
response = self.class.get("/v1/rating-plans/#{plan_id}")
|
||||
handle_response(response)
|
||||
RatingPlan.new(response.parsed_response)
|
||||
end
|
||||
|
||||
# System Management
|
||||
|
||||
def get_system_stats
|
||||
response = self.class.get("/v1/system/stats")
|
||||
handle_response(response)
|
||||
SystemStats.new(response.parsed_response)
|
||||
end
|
||||
|
||||
def get_health_status
|
||||
response = self.class.get("/v1/health")
|
||||
handle_response(response)
|
||||
HealthStatus.new(response.parsed_response)
|
||||
end
|
||||
|
||||
# WebSocket Support
|
||||
|
||||
def connect_websocket(message_handler: nil)
|
||||
ws_url = @config.api_url.gsub("http://", "ws://") + "/ws"
|
||||
|
||||
WebSocket::Client::Simple.connect(ws_url) do |ws|
|
||||
ws.on :message do |msg|
|
||||
begin
|
||||
data = JSON.parse(msg.data)
|
||||
ws_message = WebSocketMessage.new(data)
|
||||
message_handler.call(ws_message) if message_handler
|
||||
rescue JSON::ParserError => e
|
||||
raise APIError, "Failed to parse WebSocket message: #{e.message}"
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# GraphQL Support
|
||||
|
||||
def execute_graphql_query(query, variables = {})
|
||||
request = { query: query, variables: variables }
|
||||
response = self.class.post("/graphql", body: request.to_json)
|
||||
handle_response(response)
|
||||
GraphQLResponse.new(response.parsed_response)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def handle_response(response)
|
||||
case response.code
|
||||
when 401
|
||||
raise AuthenticationError, "Authentication failed"
|
||||
when 429
|
||||
raise RateLimitError, "Rate limit exceeded"
|
||||
when 400..499
|
||||
error_data = response.parsed_response["error"] || "Bad request"
|
||||
raise APIError, "API error: #{error_data}"
|
||||
when 500..599
|
||||
raise ServerError, "Server error: #{response.code}"
|
||||
end
|
||||
|
||||
response
|
||||
end
|
||||
end
|
||||
|
||||
class Config
|
||||
attr_accessor :api_url, :api_key, :timeout, :max_retries
|
||||
|
||||
def initialize(options = {})
|
||||
@api_url = options[:api_url] || "http://localhost:8000"
|
||||
@api_key = options[:api_key]
|
||||
@timeout = options[:timeout] || 30
|
||||
@max_retries = options[:max_retries] || 3
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,26 @@
|
||||
Gem::Specification.new do |spec|
|
||||
spec.name = "telecom-sdk"
|
||||
spec.version = "1.0.0"
|
||||
spec.authors = ["Telecom Platform Team"]
|
||||
spec.email = ["[email protected]"]
|
||||
spec.summary = "Ruby SDK for Telecom Platform"
|
||||
spec.description = "Ruby SDK for interacting with the Telecom Platform API"
|
||||
spec.homepage = "https://github.com/nutcas3/telecom-platform"
|
||||
spec.license = "MIT"
|
||||
spec.required_ruby_version = ">= 3.0.0"
|
||||
|
||||
spec.files = Dir.chdir(File.expand_path(__dir__)) do
|
||||
`git ls-files -z`.split("\x0").reject { |f| f.match(%r{\A(?:test|spec|features)/}) }
|
||||
end
|
||||
|
||||
spec.bindir = "exe"
|
||||
spec.executables = spec.name.gsub(/^.*:/, "")
|
||||
spec.require_paths = ["lib"]
|
||||
|
||||
spec.add_dependency "httparty", "~> 0.21"
|
||||
spec.add_dependency "websocket-client-simple", "~> 0.3"
|
||||
spec.add_dependency "json", "~> 2.6"
|
||||
|
||||
spec.add_development_dependency "minitest", "~> 5.0"
|
||||
spec.add_development_dependency "rubocop", "~> 1.21"
|
||||
end
|
||||
@@ -0,0 +1,23 @@
|
||||
[package]
|
||||
name = "telecom-sdk"
|
||||
version = "1.0.0"
|
||||
edition = "2024"
|
||||
authors = ["nutcas3 <[email protected]>"]
|
||||
description = "Rust SDK for Telecom Platform"
|
||||
license = "MIT"
|
||||
repository = "https://github.com/nutcas3/telecom-platform"
|
||||
|
||||
[dependencies]
|
||||
tokio = { version = "1.52.1", features = ["full"] }
|
||||
serde = { version = "1.0.228", features = ["derive"] }
|
||||
serde_json = "1.0.149"
|
||||
reqwest = { version = "0.13.2", features = ["json"] }
|
||||
tokio-tungstenite = "0.29.0"
|
||||
futures-util = "0.3.32"
|
||||
uuid = { version = "1.23.1", features = ["v4", "serde"] }
|
||||
chrono = { version = "0.4.44", features = ["serde"] }
|
||||
thiserror = "2.0.18"
|
||||
url = "2.5.8"
|
||||
|
||||
[dev-dependencies]
|
||||
tokio-test = "0.4.5"
|
||||
@@ -0,0 +1,525 @@
|
||||
//! Telecom Platform Rust SDK
|
||||
//!
|
||||
//! Provides async/await support with tokio for the Telecom Platform API.
|
||||
|
||||
use chrono::{DateTime, Utc};
|
||||
use reqwest::{Client, StatusCode};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::HashMap;
|
||||
use std::time::Duration;
|
||||
use thiserror::Error;
|
||||
use tokio_tungstenite::{connect_async, tungstenite::Message};
|
||||
use url::Url;
|
||||
use uuid::Uuid;
|
||||
|
||||
#[derive(Error, Debug)]
|
||||
pub enum TelecomError {
|
||||
#[error("Authentication failed")]
|
||||
AuthenticationError,
|
||||
#[error("API error: {0}")]
|
||||
APIError(String),
|
||||
#[error("Network error: {0}")]
|
||||
NetworkError(#[from] reqwest::Error),
|
||||
#[error("Validation error: {0}")]
|
||||
ValidationError(String),
|
||||
#[error("Rate limit exceeded")]
|
||||
RateLimitError,
|
||||
#[error("Server error: {0}")]
|
||||
ServerError(StatusCode),
|
||||
#[error("WebSocket error: {0}")]
|
||||
WebSocketError(String),
|
||||
#[error("JSON error: {0}")]
|
||||
JsonError(#[from] serde_json::Error),
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, Serialize)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum SubscriberStatus {
|
||||
Active,
|
||||
Suspended,
|
||||
Terminated,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, Serialize)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum UsageType {
|
||||
Data,
|
||||
Voice,
|
||||
Sms,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, Serialize)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum PaymentStatus {
|
||||
Pending,
|
||||
Completed,
|
||||
Failed,
|
||||
Refunded,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, Serialize)]
|
||||
pub struct Subscriber {
|
||||
pub id: i64,
|
||||
pub imsi: String,
|
||||
pub msisdn: String,
|
||||
pub first_name: String,
|
||||
pub last_name: String,
|
||||
pub email: String,
|
||||
pub organization_id: Option<String>,
|
||||
pub status: SubscriberStatus,
|
||||
pub plan_id: i64,
|
||||
pub balance: f64,
|
||||
pub created_at: DateTime<Utc>,
|
||||
pub updated_at: DateTime<Utc>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, Serialize)]
|
||||
pub struct SubscriberList {
|
||||
pub subscribers: Vec<Subscriber>,
|
||||
pub total: i64,
|
||||
pub page: i32,
|
||||
pub page_size: i32,
|
||||
pub has_next: bool,
|
||||
pub has_prev: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
pub struct CreateSubscriberRequest {
|
||||
pub imsi: String,
|
||||
pub msisdn: String,
|
||||
pub first_name: String,
|
||||
pub last_name: String,
|
||||
pub email: String,
|
||||
pub plan_id: i64,
|
||||
pub organization_id: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
pub struct UpdateSubscriberRequest {
|
||||
pub first_name: Option<String>,
|
||||
pub last_name: Option<String>,
|
||||
pub email: Option<String>,
|
||||
pub plan_id: Option<i64>,
|
||||
pub status: Option<SubscriberStatus>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, Serialize)]
|
||||
pub struct UsageStats {
|
||||
pub subscriber_id: String,
|
||||
pub data_up: i64,
|
||||
pub data_down: i64,
|
||||
pub voice_seconds: i64,
|
||||
pub sms_count: i64,
|
||||
pub period_start: DateTime<Utc>,
|
||||
pub period_end: DateTime<Utc>,
|
||||
pub cost: f64,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, Serialize)]
|
||||
pub struct UsageEvent {
|
||||
pub id: String,
|
||||
pub subscriber_id: String,
|
||||
pub usage_type: UsageType,
|
||||
pub amount: i64,
|
||||
pub cost: f64,
|
||||
pub timestamp: DateTime<Utc>,
|
||||
pub metadata: Option<HashMap<String, serde_json::Value>>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, Serialize)]
|
||||
pub struct UsageEventList {
|
||||
pub events: Vec<UsageEvent>,
|
||||
pub total: i64,
|
||||
pub page: i32,
|
||||
pub page_size: i32,
|
||||
pub has_next: bool,
|
||||
pub has_prev: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, Serialize)]
|
||||
pub struct CurrentSession {
|
||||
pub session_id: String,
|
||||
pub start_time: DateTime<Utc>,
|
||||
pub data_up: i64,
|
||||
pub data_down: i64,
|
||||
pub voice_seconds: i64,
|
||||
pub sms_count: i64,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, Serialize)]
|
||||
pub struct RealTimeUsage {
|
||||
pub current_session: Option<CurrentSession>,
|
||||
pub today_usage: Option<HashMap<String, i64>>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, Serialize)]
|
||||
pub struct PaymentTransaction {
|
||||
pub id: String,
|
||||
pub subscriber_id: String,
|
||||
pub amount: f64,
|
||||
pub currency: String,
|
||||
pub status: PaymentStatus,
|
||||
pub gateway: String,
|
||||
pub transaction_id: Option<String>,
|
||||
pub created_at: DateTime<Utc>,
|
||||
pub completed_at: Option<DateTime<Utc>>,
|
||||
pub metadata: Option<HashMap<String, serde_json::Value>>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, Serialize)]
|
||||
pub struct PaymentTransactionList {
|
||||
pub transactions: Vec<PaymentTransaction>,
|
||||
pub total: i64,
|
||||
pub page: i32,
|
||||
pub page_size: i32,
|
||||
pub has_next: bool,
|
||||
pub has_prev: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
pub struct CreatePaymentRequest {
|
||||
pub subscriber_id: String,
|
||||
pub amount: f64,
|
||||
pub currency: String,
|
||||
pub gateway: String,
|
||||
pub metadata: Option<HashMap<String, serde_json::Value>>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, Serialize)]
|
||||
pub struct RatingPlan {
|
||||
pub plan_id: String,
|
||||
pub name: String,
|
||||
pub data_rate: f64,
|
||||
pub voice_rate: f64,
|
||||
pub sms_rate: f64,
|
||||
pub monthly_fee: f64,
|
||||
pub data_limit: i64,
|
||||
pub voice_limit: i64,
|
||||
pub sms_limit: i64,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, Serialize)]
|
||||
pub struct SystemStats {
|
||||
pub active_sessions: i64,
|
||||
pub total_accounts: i64,
|
||||
pub blocked_users: i64,
|
||||
pub low_balance_alerts: i64,
|
||||
pub uptime: f64,
|
||||
pub cpu_usage: f64,
|
||||
pub memory_usage: f64,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, Serialize)]
|
||||
pub struct HealthStatus {
|
||||
pub status: String,
|
||||
pub timestamp: DateTime<Utc>,
|
||||
pub checks: HashMap<String, serde_json::Value>,
|
||||
pub uptime: f64,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, Serialize)]
|
||||
pub struct WebSocketMessage {
|
||||
pub r#type: String,
|
||||
pub data: HashMap<String, serde_json::Value>,
|
||||
pub timestamp: DateTime<Utc>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
pub struct GraphQLRequest {
|
||||
pub query: String,
|
||||
pub variables: Option<HashMap<String, serde_json::Value>>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, Serialize)]
|
||||
pub struct GraphQLResponse {
|
||||
pub data: Option<HashMap<String, serde_json::Value>>,
|
||||
pub errors: Option<Vec<GraphQLError>>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, Serialize)]
|
||||
pub struct GraphQLError {
|
||||
pub message: String,
|
||||
pub locations: Option<Vec<GraphQLErrorLocation>>,
|
||||
pub path: Option<Vec<String>>,
|
||||
pub extensions: Option<HashMap<String, serde_json::Value>>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, Serialize)]
|
||||
pub struct GraphQLErrorLocation {
|
||||
pub line: i32,
|
||||
pub column: i32,
|
||||
}
|
||||
|
||||
/// Telecom Platform SDK client
|
||||
#[derive(Clone)]
|
||||
pub struct TelecomClient {
|
||||
client: Client,
|
||||
config: TelecomConfig,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct TelecomConfig {
|
||||
pub api_url: String,
|
||||
pub api_key: Option<String>,
|
||||
pub timeout: Duration,
|
||||
pub max_retries: u32,
|
||||
}
|
||||
|
||||
impl Default for TelecomConfig {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
api_url: "http://localhost:8000".to_string(),
|
||||
api_key: None,
|
||||
timeout: Duration::from_secs(30),
|
||||
max_retries: 3,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl TelecomClient {
|
||||
/// Create a new Telecom client
|
||||
pub fn new(config: TelecomConfig) -> Self {
|
||||
let client = Client::builder()
|
||||
.timeout(config.timeout)
|
||||
.build()
|
||||
.expect("Failed to create HTTP client");
|
||||
|
||||
Self { client, config }
|
||||
}
|
||||
|
||||
async fn handle_response(&self, response: reqwest::Response) -> Result<reqwest::Response, TelecomError> {
|
||||
match response.status() {
|
||||
StatusCode::UNAUTHORIZED => Err(TelecomError::AuthenticationError),
|
||||
StatusCode::TOO_MANY_REQUESTS => Err(TelecomError::RateLimitError),
|
||||
status if status.is_client_error() => {
|
||||
let error_text = response.text().await.unwrap_or_else(|_| "Bad request".to_string());
|
||||
Err(TelecomError::APIError(error_text))
|
||||
}
|
||||
status if status.is_server_error() => Err(TelecomError::ServerError(status)),
|
||||
_ => Ok(response),
|
||||
}
|
||||
}
|
||||
|
||||
/// Get subscriber by ID
|
||||
pub async fn get_subscriber(&self, id: i64) -> Result<Subscriber, TelecomError> {
|
||||
let response = self
|
||||
.client
|
||||
.get(&format!("{}/v1/subscribers/{}", self.config.api_url, id))
|
||||
.send()
|
||||
.await?;
|
||||
|
||||
let response = self.handle_response(response).await?;
|
||||
Ok(response.json().await?)
|
||||
}
|
||||
|
||||
/// List subscribers with pagination
|
||||
pub async fn list_subscribers(
|
||||
&self,
|
||||
page: i32,
|
||||
page_size: i32,
|
||||
status: Option<SubscriberStatus>,
|
||||
) -> Result<SubscriberList, TelecomError> {
|
||||
let mut request = self
|
||||
.client
|
||||
.get(&format!("{}/v1/subscribers", self.config.api_url))
|
||||
.query(&[("page", page), ("page_size", page_size)]);
|
||||
|
||||
if let Some(status) = status {
|
||||
request = request.query(&[("status", serde_json::to_string(&status).unwrap())]);
|
||||
}
|
||||
|
||||
let response = request.send().await?;
|
||||
let response = self.handle_response(response).await?;
|
||||
Ok(response.json().await?)
|
||||
}
|
||||
|
||||
/// Create a new subscriber
|
||||
pub async fn create_subscriber(&self, request: CreateSubscriberRequest) -> Result<Subscriber, TelecomError> {
|
||||
let response = self
|
||||
.client
|
||||
.post(&format!("{}/v1/subscribers", self.config.api_url))
|
||||
.json(&request)
|
||||
.send()
|
||||
.await?;
|
||||
|
||||
let response = self.handle_response(response).await?;
|
||||
Ok(response.json().await?)
|
||||
}
|
||||
|
||||
/// Update an existing subscriber
|
||||
pub async fn update_subscriber(&self, id: i64, request: UpdateSubscriberRequest) -> Result<Subscriber, TelecomError> {
|
||||
let response = self
|
||||
.client
|
||||
.put(&format!("{}/v1/subscribers/{}", self.config.api_url, id))
|
||||
.json(&request)
|
||||
.send()
|
||||
.await?;
|
||||
|
||||
let response = self.handle_response(response).await?;
|
||||
Ok(response.json().await?)
|
||||
}
|
||||
|
||||
/// Suspend a subscriber
|
||||
pub async fn suspend_subscriber(&self, id: i64) -> Result<Subscriber, TelecomError> {
|
||||
let response = self
|
||||
.client
|
||||
.post(&format!("{}/v1/subscribers/{}/suspend", self.config.api_url, id))
|
||||
.send()
|
||||
.await?;
|
||||
|
||||
let response = self.handle_response(response).await?;
|
||||
Ok(response.json().await?)
|
||||
}
|
||||
|
||||
/// Activate a suspended subscriber
|
||||
pub async fn activate_subscriber(&self, id: i64) -> Result<Subscriber, TelecomError> {
|
||||
let response = self
|
||||
.client
|
||||
.post(&format!("{}/v1/subscribers/{}/activate", self.config.api_url, id))
|
||||
.send()
|
||||
.await?;
|
||||
|
||||
let response = self.handle_response(response).await?;
|
||||
Ok(response.json().await?)
|
||||
}
|
||||
|
||||
/// Terminate a subscriber
|
||||
pub async fn terminate_subscriber(&self, id: i64) -> Result<bool, TelecomError> {
|
||||
let response = self
|
||||
.client
|
||||
.delete(&format!("{}/v1/subscribers/{}", self.config.api_url, id))
|
||||
.send()
|
||||
.await?;
|
||||
|
||||
let response = self.handle_response(response).await?;
|
||||
let result: serde_json::Value = response.json().await?;
|
||||
Ok(result["success"].as_bool().unwrap_or(false))
|
||||
}
|
||||
|
||||
/// Get usage statistics for a subscriber
|
||||
pub async fn get_usage_stats(
|
||||
&self,
|
||||
subscriber_id: i64,
|
||||
start_date: DateTime<Utc>,
|
||||
end_date: DateTime<Utc>,
|
||||
) -> Result<UsageStats, TelecomError> {
|
||||
let response = self
|
||||
.client
|
||||
.get(&format!("{}/v1/subscribers/{}/usage", self.config.api_url, subscriber_id))
|
||||
.query(&[
|
||||
("start_date", start_date.to_rfc3339()),
|
||||
("end_date", end_date.to_rfc3339()),
|
||||
])
|
||||
.send()
|
||||
.await?;
|
||||
|
||||
let response = self.handle_response(response).await?;
|
||||
Ok(response.json().await?)
|
||||
}
|
||||
|
||||
/// Create a payment transaction
|
||||
pub async fn create_payment_transaction(&self, request: CreatePaymentRequest) -> Result<PaymentTransaction, TelecomError> {
|
||||
let response = self
|
||||
.client
|
||||
.post(&format!("{}/v1/payments/transactions", self.config.api_url))
|
||||
.json(&request)
|
||||
.send()
|
||||
.await?;
|
||||
|
||||
let response = self.handle_response(response).await?;
|
||||
Ok(response.json().await?)
|
||||
}
|
||||
|
||||
/// Get system statistics
|
||||
pub async fn get_system_stats(&self) -> Result<SystemStats, TelecomError> {
|
||||
let response = self
|
||||
.client
|
||||
.get(&format!("{}/v1/system/stats", self.config.api_url))
|
||||
.send()
|
||||
.await?;
|
||||
|
||||
let response = self.handle_response(response).await?;
|
||||
Ok(response.json().await?)
|
||||
}
|
||||
|
||||
/// Get system health status
|
||||
pub async fn get_health_status(&self) -> Result<HealthStatus, TelecomError> {
|
||||
let response = self
|
||||
.client
|
||||
.get(&format!("{}/v1/health", self.config.api_url))
|
||||
.send()
|
||||
.await?;
|
||||
|
||||
let response = self.handle_response(response).await?;
|
||||
Ok(response.json().await?)
|
||||
}
|
||||
|
||||
/// Execute a GraphQL query
|
||||
pub async fn execute_graphql_query(
|
||||
&self,
|
||||
query: String,
|
||||
variables: Option<HashMap<String, serde_json::Value>>,
|
||||
) -> Result<GraphQLResponse, TelecomError> {
|
||||
let request = GraphQLRequest { query, variables };
|
||||
let response = self
|
||||
.client
|
||||
.post(&format!("{}/graphql", self.config.api_url))
|
||||
.json(&request)
|
||||
.send()
|
||||
.await?;
|
||||
|
||||
let response = self.handle_response(response).await?;
|
||||
Ok(response.json().await?)
|
||||
}
|
||||
|
||||
/// Connect to WebSocket for real-time updates
|
||||
pub async fn connect_websocket<F>(&self, message_handler: F) -> Result<(), TelecomError>
|
||||
where
|
||||
F: Fn(WebSocketMessage) + Send + Sync + 'static,
|
||||
{
|
||||
let ws_url = self.config.api_url.replace("http://", "ws://") + "/ws";
|
||||
let (ws_stream, _) = connect_async(&ws_url).await.map_err(|e| TelecomError::WebSocketError(e.to_string()))?;
|
||||
|
||||
println!("WebSocket connected to {}", ws_url);
|
||||
|
||||
let (mut write, mut read) = ws_stream.split();
|
||||
let handler = Arc::new(message_handler);
|
||||
|
||||
// Spawn task to handle incoming messages
|
||||
let handler_clone = handler.clone();
|
||||
let read_task = tokio::spawn(async move {
|
||||
while let Some(msg) = read.next().await {
|
||||
match msg {
|
||||
Ok(tungstenite::Message::Text(text)) => {
|
||||
if let Ok(ws_msg) = serde_json::from_str::<WebSocketMessage>(&text) {
|
||||
handler_clone(ws_msg);
|
||||
}
|
||||
}
|
||||
Ok(tungstenite::Message::Binary(data)) => {
|
||||
if let Ok(text) = String::from_utf8(data) {
|
||||
if let Ok(ws_msg) = serde_json::from_str::<WebSocketMessage>(&text) {
|
||||
handler_clone(ws_msg);
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(tungstenite::Message::Close(_)) => {
|
||||
println!("WebSocket connection closed");
|
||||
break;
|
||||
}
|
||||
Err(e) => {
|
||||
eprintln!("WebSocket error: {}", e);
|
||||
break;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Return a handle that can be used to manage the connection
|
||||
tokio::spawn(async move {
|
||||
let _ = read_task.await;
|
||||
});
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
// swift-tools-version: 5.9
|
||||
// The swift-tools-version declares the minimum version of Swift required to build this package.
|
||||
|
||||
import PackageDescription
|
||||
|
||||
let package = Package(
|
||||
name: "TelecomSDK",
|
||||
platforms: [
|
||||
.iOS(.v13),
|
||||
.macOS(.v10_15),
|
||||
.watchOS(.v6),
|
||||
.tvOS(.v13)
|
||||
],
|
||||
products: [
|
||||
.library(
|
||||
name: "TelecomSDK",
|
||||
targets: ["TelecomSDK"]),
|
||||
],
|
||||
dependencies: [
|
||||
.package(url: "https://github.com/apple/swift-nio.git", from: "2.40.0"),
|
||||
.package(url: "https://github.com/apple/swift-async-algorithms.git", from: "1.0.0"),
|
||||
],
|
||||
targets: [
|
||||
.target(
|
||||
name: "TelecomSDK",
|
||||
dependencies: [
|
||||
.product(name: "NIO", package: "swift-nio"),
|
||||
.product(name: "AsyncAlgorithms", package: "swift-async-algorithms"),
|
||||
]),
|
||||
.testTarget(
|
||||
name: "TelecomSDKTests",
|
||||
dependencies: ["TelecomSDK"]),
|
||||
]
|
||||
)
|
||||
@@ -0,0 +1,299 @@
|
||||
import Foundation
|
||||
import NIO
|
||||
import AsyncAlgorithms
|
||||
|
||||
/// Telecom Platform Swift SDK
|
||||
///
|
||||
/// Provides async/await support with Swift Concurrency for the Telecom Platform API.
|
||||
public class TelecomSDK {
|
||||
|
||||
// MARK: - Properties
|
||||
|
||||
private let config: TelecomConfig
|
||||
private let httpClient: HTTPClient
|
||||
private let webSocketClient: WebSocketClient
|
||||
|
||||
// MARK: - Initialization
|
||||
|
||||
/// Initialize a new TelecomSDK instance
|
||||
/// - Parameter config: Configuration for the SDK
|
||||
public init(config: TelecomConfig = TelecomConfig()) {
|
||||
self.config = config
|
||||
self.httpClient = HTTPClient(config: config)
|
||||
self.webSocketClient = WebSocketClient(config: config)
|
||||
}
|
||||
|
||||
// MARK: - Subscriber Management
|
||||
|
||||
/// Get subscriber by ID
|
||||
/// - Parameter id: Subscriber ID
|
||||
/// - Returns: Subscriber information
|
||||
public func getSubscriber(id: Int64) async throws -> Subscriber {
|
||||
return try await httpClient.get(path: "/v1/subscribers/\(id)")
|
||||
}
|
||||
|
||||
/// List subscribers with pagination
|
||||
/// - Parameters:
|
||||
/// - page: Page number
|
||||
/// - pageSize: Page size
|
||||
/// - status: Optional status filter
|
||||
/// - Returns: Paginated list of subscribers
|
||||
public func listSubscribers(
|
||||
page: Int = 1,
|
||||
pageSize: Int = 50,
|
||||
status: SubscriberStatus? = nil
|
||||
) async throws -> SubscriberList {
|
||||
var queryParams: [String: String] = [
|
||||
"page": "\(page)",
|
||||
"page_size": "\(pageSize)"
|
||||
]
|
||||
|
||||
if let status = status {
|
||||
queryParams["status"] = status.rawValue
|
||||
}
|
||||
|
||||
return try await httpClient.get(path: "/v1/subscribers", queryParams: queryParams)
|
||||
}
|
||||
|
||||
/// Create a new subscriber
|
||||
/// - Parameter request: Create subscriber request
|
||||
/// - Returns: Created subscriber
|
||||
public func createSubscriber(request: CreateSubscriberRequest) async throws -> Subscriber {
|
||||
return try await httpClient.post(path: "/v1/subscribers", body: request)
|
||||
}
|
||||
|
||||
/// Update an existing subscriber
|
||||
/// - Parameters:
|
||||
/// - id: Subscriber ID
|
||||
/// - request: Update subscriber request
|
||||
/// - Returns: Updated subscriber
|
||||
public func updateSubscriber(id: Int64, request: UpdateSubscriberRequest) async throws -> Subscriber {
|
||||
return try await httpClient.put(path: "/v1/subscribers/\(id)", body: request)
|
||||
}
|
||||
|
||||
/// Suspend a subscriber
|
||||
/// - Parameter id: Subscriber ID
|
||||
/// - Returns: Suspended subscriber
|
||||
public func suspendSubscriber(id: Int64) async throws -> Subscriber {
|
||||
return try await httpClient.post(path: "/v1/subscribers/\(id)/suspend")
|
||||
}
|
||||
|
||||
/// Activate a suspended subscriber
|
||||
/// - Parameter id: Subscriber ID
|
||||
/// - Returns: Activated subscriber
|
||||
public func activateSubscriber(id: Int64) async throws -> Subscriber {
|
||||
return try await httpClient.post(path: "/v1/subscribers/\(id)/activate")
|
||||
}
|
||||
|
||||
/// Terminate a subscriber
|
||||
/// - Parameter id: Subscriber ID
|
||||
/// - Returns: Success status
|
||||
public func terminateSubscriber(id: Int64) async throws -> Bool {
|
||||
return try await httpClient.delete(path: "/v1/subscribers/\(id)")
|
||||
}
|
||||
|
||||
// MARK: - Usage Management
|
||||
|
||||
/// Get usage statistics for a subscriber
|
||||
/// - Parameters:
|
||||
/// - subscriberId: Subscriber ID
|
||||
/// - startDate: Start date
|
||||
/// - endDate: End date
|
||||
/// - Returns: Usage statistics
|
||||
public func getUsageStats(
|
||||
subscriberId: Int64,
|
||||
startDate: Date,
|
||||
endDate: Date
|
||||
) async throws -> UsageStats {
|
||||
let formatter = ISO8601DateFormatter()
|
||||
formatter.timeZone = TimeZone.current
|
||||
|
||||
let queryParams: [String: String] = [
|
||||
"start_date": formatter.string(from: startDate),
|
||||
"end_date": formatter.string(from: endDate)
|
||||
]
|
||||
|
||||
return try await httpClient.get(
|
||||
path: "/v1/subscribers/\(subscriberId)/usage",
|
||||
queryParams: queryParams
|
||||
)
|
||||
}
|
||||
|
||||
/// List usage events with filtering
|
||||
/// - Parameters:
|
||||
/// - subscriberId: Optional subscriber ID filter
|
||||
/// - usageType: Optional usage type filter
|
||||
/// - startDate: Optional start date filter
|
||||
/// - endDate: Optional end date filter
|
||||
/// - page: Page number
|
||||
/// - pageSize: Page size
|
||||
/// - Returns: Paginated list of usage events
|
||||
public func listUsageEvents(
|
||||
subscriberId: Int64? = nil,
|
||||
usageType: UsageType? = nil,
|
||||
startDate: Date? = nil,
|
||||
endDate: Date? = nil,
|
||||
page: Int = 1,
|
||||
pageSize: Int = 50
|
||||
) async throws -> UsageEventList {
|
||||
var queryParams: [String: String] = [
|
||||
"page": "\(page)",
|
||||
"page_size": "\(pageSize)"
|
||||
]
|
||||
|
||||
if let subscriberId = subscriberId {
|
||||
queryParams["subscriber_id"] = "\(subscriberId)"
|
||||
}
|
||||
|
||||
if let usageType = usageType {
|
||||
queryParams["usage_type"] = usageType.rawValue
|
||||
}
|
||||
|
||||
if let startDate = startDate {
|
||||
let formatter = ISO8601DateFormatter()
|
||||
formatter.timeZone = TimeZone.current
|
||||
queryParams["start_date"] = formatter.string(from: startDate)
|
||||
}
|
||||
|
||||
if let endDate = endDate {
|
||||
let formatter = ISO8601DateFormatter()
|
||||
formatter.timeZone = TimeZone.current
|
||||
queryParams["end_date"] = formatter.string(from: endDate)
|
||||
}
|
||||
|
||||
return try await httpClient.get(path: "/v1/usage/events", queryParams: queryParams)
|
||||
}
|
||||
|
||||
/// Get real-time usage for a subscriber
|
||||
/// - Parameter subscriberId: Subscriber ID
|
||||
/// - Returns: Real-time usage data
|
||||
public func getRealTimeUsage(subscriberId: Int64) async throws -> RealTimeUsage {
|
||||
return try await httpClient.get(path: "/v1/subscribers/\(subscriberId)/realtime")
|
||||
}
|
||||
|
||||
// MARK: - Payment Management
|
||||
|
||||
/// Create a payment transaction
|
||||
/// - Parameter request: Create payment request
|
||||
/// - Returns: Created payment transaction
|
||||
public func createPaymentTransaction(request: CreatePaymentRequest) async throws -> PaymentTransaction {
|
||||
return try await httpClient.post(path: "/v1/payments/transactions", body: request)
|
||||
}
|
||||
|
||||
/// Get payment transaction by ID
|
||||
/// - Parameter transactionId: Transaction ID
|
||||
/// - Returns: Payment transaction
|
||||
public func getPaymentTransaction(transactionId: String) async throws -> PaymentTransaction {
|
||||
return try await httpClient.get(path: "/v1/payments/transactions/\(transactionId)")
|
||||
}
|
||||
|
||||
/// List payment transactions with filtering
|
||||
/// - Parameters:
|
||||
/// - subscriberId: Optional subscriber ID filter
|
||||
/// - status: Optional status filter
|
||||
/// - page: Page number
|
||||
/// - pageSize: Page size
|
||||
/// - Returns: Paginated list of payment transactions
|
||||
public func listPaymentTransactions(
|
||||
subscriberId: Int64? = nil,
|
||||
status: PaymentStatus? = nil,
|
||||
page: Int = 1,
|
||||
pageSize: Int = 50
|
||||
) async throws -> PaymentTransactionList {
|
||||
var queryParams: [String: String] = [
|
||||
"page": "\(page)",
|
||||
"page_size": "\(pageSize)"
|
||||
]
|
||||
|
||||
if let subscriberId = subscriberId {
|
||||
queryParams["subscriber_id"] = "\(subscriberId)"
|
||||
}
|
||||
|
||||
if let status = status {
|
||||
queryParams["status"] = status.rawValue
|
||||
}
|
||||
|
||||
return try await httpClient.get(path: "/v1/payments/transactions", queryParams: queryParams)
|
||||
}
|
||||
|
||||
// MARK: - Rating Plans
|
||||
|
||||
/// List all available rating plans
|
||||
/// - Returns: List of rating plans
|
||||
public func listRatingPlans() async throws -> [RatingPlan] {
|
||||
return try await httpClient.get(path: "/v1/rating-plans")
|
||||
}
|
||||
|
||||
/// Get rating plan by ID
|
||||
/// - Parameter planId: Plan ID
|
||||
/// - Returns: Rating plan
|
||||
public func getRatingPlan(planId: String) async throws -> RatingPlan {
|
||||
return try await httpClient.get(path: "/v1/rating-plans/\(planId)")
|
||||
}
|
||||
|
||||
// MARK: - System Management
|
||||
|
||||
/// Get system statistics
|
||||
/// - Returns: System statistics
|
||||
public func getSystemStats() async throws -> SystemStats {
|
||||
return try await httpClient.get(path: "/v1/system/stats")
|
||||
}
|
||||
|
||||
/// Get system health status
|
||||
/// - Returns: Health status
|
||||
public func getHealthStatus() async throws -> HealthStatus {
|
||||
return try await httpClient.get(path: "/v1/health")
|
||||
}
|
||||
|
||||
// MARK: - WebSocket Support
|
||||
|
||||
/// Connect to WebSocket for real-time updates
|
||||
/// - Parameter messageHandler: Closure to handle WebSocket messages
|
||||
/// - Returns: Async stream of WebSocket messages
|
||||
public func connectWebSocket(
|
||||
messageHandler: @escaping (WebSocketMessage) async -> Void
|
||||
) -> AsyncThrowingStream<WebSocketMessage, Error> {
|
||||
return try await webSocketClient.connect(messageHandler: messageHandler)
|
||||
}
|
||||
|
||||
// MARK: - GraphQL Support
|
||||
|
||||
/// Execute a GraphQL query
|
||||
/// - Parameters:
|
||||
/// - query: GraphQL query string
|
||||
/// - variables: Optional query variables
|
||||
/// - Returns: GraphQL response
|
||||
public func executeGraphQLQuery(
|
||||
query: String,
|
||||
variables: [String: Any]? = nil
|
||||
) async throws -> GraphQLResponse {
|
||||
let request = GraphQLRequest(query: query, variables: variables ?? [:])
|
||||
return try await httpClient.post(path: "/graphql", body: request)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Configuration
|
||||
|
||||
/// Configuration for Telecom SDK
|
||||
public struct TelecomConfig {
|
||||
/// Base URL for the API server
|
||||
public var apiURL: String
|
||||
/// API key for authentication
|
||||
public var apiKey: String?
|
||||
/// Request timeout in seconds
|
||||
public var timeout: TimeInterval
|
||||
/// Maximum number of retry attempts
|
||||
public var maxRetries: Int
|
||||
|
||||
public init(
|
||||
apiURL: String = "http://localhost:8000",
|
||||
apiKey: String? = nil,
|
||||
timeout: TimeInterval = 30.0,
|
||||
maxRetries: Int = 3
|
||||
) {
|
||||
self.apiURL = apiURL
|
||||
self.apiKey = apiKey
|
||||
self.timeout = timeout
|
||||
self.maxRetries = maxRetries
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,312 @@
|
||||
# Telecom Platform TypeScript SDK
|
||||
|
||||
A comprehensive TypeScript SDK for integrating with the Telecom Platform API. This SDK provides a clean, type-safe interface for managing subscribers, monitoring usage, handling payments, and receiving real-time updates.
|
||||
|
||||
## Installation
|
||||
|
||||
```bash
|
||||
npm install @telecom-platform/sdk
|
||||
# or
|
||||
yarn add @telecom-platform/sdk
|
||||
# or
|
||||
pnpm add @telecom-platform/sdk
|
||||
```
|
||||
|
||||
## Quick Start
|
||||
|
||||
```typescript
|
||||
import { TelecomSDK } from '@telecom-platform/sdk';
|
||||
|
||||
// Initialize the SDK
|
||||
const sdk = TelecomSDK.initialize({
|
||||
baseURL: 'https://api.telecom-platform.com',
|
||||
apiKey: 'your-api-key',
|
||||
websocketURL: 'wss://ws.telecom-platform.com',
|
||||
enableLogging: true,
|
||||
});
|
||||
|
||||
// Connect to WebSocket for real-time updates
|
||||
await sdk.connectWebSocket();
|
||||
|
||||
// Subscribe to usage updates for a subscriber
|
||||
sdk.subscribeToUsage('123456789012345');
|
||||
|
||||
// Listen for usage updates
|
||||
sdk.onUsageUpdate((update) => {
|
||||
console.log('Usage update:', update);
|
||||
});
|
||||
|
||||
// Subscribe to alerts
|
||||
sdk.subscribeToAlerts();
|
||||
|
||||
// Listen for alerts
|
||||
sdk.onAlert((alert) => {
|
||||
console.log('Alert:', alert);
|
||||
});
|
||||
|
||||
// List subscribers
|
||||
const subscribers = await sdk.subscribersService.listSubscribers({
|
||||
page: 1,
|
||||
pageSize: 20,
|
||||
status: 'active',
|
||||
});
|
||||
|
||||
console.log('Subscribers:', subscribers);
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
### Initialization
|
||||
|
||||
```typescript
|
||||
const sdk = TelecomSDK.initialize({
|
||||
baseURL: string, // API base URL (required)
|
||||
apiKey?: string, // API key (optional)
|
||||
timeout?: number, // Request timeout in ms (default: 30000)
|
||||
retryAttempts?: number, // Number of retry attempts (default: 3)
|
||||
retryDelay?: number, // Retry delay in ms (default: 1000)
|
||||
enableLogging?: boolean, // Enable debug logging (default: false)
|
||||
websocketURL?: string, // WebSocket URL for real-time updates
|
||||
});
|
||||
```
|
||||
|
||||
### Subscriber Management
|
||||
|
||||
```typescript
|
||||
// List subscribers
|
||||
const subscribers = await sdk.subscribersService.listSubscribers({
|
||||
page: 1,
|
||||
pageSize: 20,
|
||||
status: 'active',
|
||||
organizationId: 'org-123',
|
||||
search: 'John Doe',
|
||||
});
|
||||
|
||||
// Get subscriber by ID
|
||||
const subscriber = await sdk.subscribersService.getSubscriber(123);
|
||||
|
||||
// Get subscriber by IMSI
|
||||
const subscriber = await sdk.subscribersService.getSubscriberByImsi('123456789012345');
|
||||
|
||||
// Create new subscriber
|
||||
const newSubscriber = await sdk.subscribersService.createSubscriber({
|
||||
msisdn: '+1234567890',
|
||||
firstName: 'John',
|
||||
lastName: 'Doe',
|
||||
email: '[email protected]',
|
||||
planId: 1,
|
||||
});
|
||||
|
||||
// Update subscriber
|
||||
const updated = await sdk.subscribersService.updateSubscriber(123, {
|
||||
firstName: 'Jane',
|
||||
email: '[email protected]',
|
||||
});
|
||||
|
||||
// Delete subscriber
|
||||
await sdk.subscribersService.deleteSubscriber(123);
|
||||
|
||||
// Suspend subscriber
|
||||
await sdk.subscribersService.suspendSubscriber(123);
|
||||
|
||||
// Activate subscriber
|
||||
await sdk.subscribersService.activateSubscriber(123);
|
||||
|
||||
// Terminate subscriber
|
||||
await sdk.subscribersService.terminateSubscriber(123);
|
||||
```
|
||||
|
||||
### Account Management
|
||||
|
||||
```typescript
|
||||
// Get subscriber account
|
||||
const account = await sdk.subscribersService.getSubscriberAccount('123456789012345');
|
||||
|
||||
// Top up balance
|
||||
const updatedAccount = await sdk.subscribersService.topUpBalance('123456789012345', {
|
||||
amount: 1000, // $10.00 in cents
|
||||
paymentMethodId: 'pm_123',
|
||||
});
|
||||
|
||||
// Get usage statistics
|
||||
const stats = await sdk.subscribersService.getUsageStats('123456789012345', 'monthly');
|
||||
|
||||
// Get real-time usage
|
||||
const realtime = await sdk.subscribersService.getRealTimeUsage('123456789012345');
|
||||
```
|
||||
|
||||
### eSIM Management
|
||||
|
||||
```typescript
|
||||
// Provision eSIM profile
|
||||
const esim = await sdk.subscribersService.provisionESIM('123456789012345');
|
||||
|
||||
// Activate eSIM profile
|
||||
await sdk.subscribersService.activateESIM('123456789012345', esim.profileId);
|
||||
|
||||
// Deactivate eSIM profile
|
||||
await sdk.subscribersService.deactivateESIM('123456789012345', esim.profileId);
|
||||
|
||||
// Get eSIM status
|
||||
const status = await sdk.subscribersService.getESIMStatus('123456789012345');
|
||||
```
|
||||
|
||||
### Billing & Invoices
|
||||
|
||||
```typescript
|
||||
// Get subscriber invoices
|
||||
const invoices = await sdk.subscribersService.getInvoices('123456789012345');
|
||||
|
||||
// Get specific invoice
|
||||
const invoice = await sdk.subscribersService.getInvoice('inv_123');
|
||||
|
||||
// Download invoice PDF
|
||||
const pdfBlob = await sdk.subscribersService.downloadInvoicePDF('inv_123');
|
||||
```
|
||||
|
||||
### Real-time Updates
|
||||
|
||||
```typescript
|
||||
// Connect to WebSocket
|
||||
await sdk.connectWebSocket();
|
||||
|
||||
// Subscribe to usage updates
|
||||
sdk.subscribeToUsage('123456789012345');
|
||||
|
||||
// Subscribe to alerts
|
||||
sdk.subscribeToAlerts();
|
||||
|
||||
// Listen for usage updates
|
||||
sdk.onUsageUpdate((update) => {
|
||||
console.log(`Data used: ${update.dataUsed} bytes`);
|
||||
console.log(`Cost: $${update.cost / 100}`);
|
||||
});
|
||||
|
||||
// Listen for alerts
|
||||
sdk.onAlert((alert) => {
|
||||
console.log(`Alert: ${alert.message}`);
|
||||
if (alert.severity === 'critical') {
|
||||
// Handle critical alert
|
||||
}
|
||||
});
|
||||
|
||||
// Custom event handlers
|
||||
sdk.onWebSocketEvent('custom_event', (message) => {
|
||||
console.log('Custom event:', message);
|
||||
});
|
||||
|
||||
// Check connection status
|
||||
if (sdk.isWebSocketConnected) {
|
||||
console.log('WebSocket is connected');
|
||||
}
|
||||
|
||||
// Disconnect
|
||||
sdk.disconnectWebSocket();
|
||||
```
|
||||
|
||||
### System Monitoring
|
||||
|
||||
```typescript
|
||||
// Get system statistics
|
||||
const stats = await sdk.getSystemStats();
|
||||
console.log('Active sessions:', stats.activeSessions);
|
||||
console.log('Total accounts:', stats.totalAccounts);
|
||||
|
||||
// Get health status
|
||||
const health = await sdk.getHealthStatus();
|
||||
console.log('Redis connected:', health.redisConnected);
|
||||
|
||||
// Test connection
|
||||
const test = await sdk.testConnection();
|
||||
console.log('API status:', test.status);
|
||||
```
|
||||
|
||||
## Error Handling
|
||||
|
||||
The SDK provides comprehensive error handling with detailed error information:
|
||||
|
||||
```typescript
|
||||
try {
|
||||
const subscriber = await sdk.subscribersService.getSubscriber(123);
|
||||
} catch (error) {
|
||||
console.error('Error:', error.code);
|
||||
console.error('Message:', error.message);
|
||||
console.error('Details:', error.details);
|
||||
console.error('Timestamp:', error.timestamp);
|
||||
}
|
||||
```
|
||||
|
||||
## Configuration Updates
|
||||
|
||||
You can update the SDK configuration at runtime:
|
||||
|
||||
```typescript
|
||||
// Update API key
|
||||
sdk.setApiKey('new-api-key');
|
||||
|
||||
// Update base URL
|
||||
sdk.setBaseURL('https://new-api.telecom-platform.com');
|
||||
|
||||
// Enable/disable logging
|
||||
sdk.setLogging(true);
|
||||
|
||||
// Get current configuration
|
||||
const config = sdk.getConfig();
|
||||
console.log('Current config:', config);
|
||||
```
|
||||
|
||||
## TypeScript Support
|
||||
|
||||
The SDK is fully typed with TypeScript. All methods have proper type definitions and IntelliSense support:
|
||||
|
||||
```typescript
|
||||
import { Subscriber, SubscriberStatus, UsageType } from '@telecom-platform/sdk';
|
||||
|
||||
// Types are automatically inferred
|
||||
const subscriber: Subscriber = await sdk.subscribersService.getSubscriber(123);
|
||||
const status: SubscriberStatus = subscriber.status;
|
||||
```
|
||||
|
||||
## Browser Support
|
||||
|
||||
The SDK works in modern browsers that support:
|
||||
- ES2020 features
|
||||
- WebSocket API
|
||||
- Fetch API
|
||||
|
||||
For older browsers, you may need to include appropriate polyfills.
|
||||
|
||||
## Node.js Usage
|
||||
|
||||
The SDK also works in Node.js environments:
|
||||
|
||||
```typescript
|
||||
import { TelecomSDK } from '@telecom-platform/sdk';
|
||||
|
||||
// Node.js WebSocket polyfill may be required
|
||||
global.WebSocket = require('ws');
|
||||
|
||||
const sdk = TelecomSDK.initialize({
|
||||
baseURL: 'https://api.telecom-platform.com',
|
||||
apiKey: process.env.API_KEY,
|
||||
});
|
||||
```
|
||||
|
||||
## Examples
|
||||
|
||||
See the `examples/` directory for complete examples of:
|
||||
- Basic subscriber management
|
||||
- Real-time usage monitoring
|
||||
- eSIM provisioning
|
||||
- Billing integration
|
||||
- Custom dashboard implementation
|
||||
|
||||
## Support
|
||||
|
||||
- Documentation: [https://docs.telecom-platform.com/sdk](https://docs.telecom-platform.com/sdk)
|
||||
- Issues: [GitHub Issues](https://github.com/telecom-platform/sdk/issues)
|
||||
- Support: support@telecom-platform.com
|
||||
|
||||
## License
|
||||
|
||||
MIT License - see LICENSE file for details.
|
||||
@@ -0,0 +1,38 @@
|
||||
{
|
||||
"name": "@telecom-platform/sdk",
|
||||
"version": "1.0.0",
|
||||
"description": "TypeScript SDK for Telecom Platform API",
|
||||
"main": "dist/index.js",
|
||||
"types": "dist/index.d.ts",
|
||||
"scripts": {
|
||||
"build": "tsc",
|
||||
"dev": "tsc --watch",
|
||||
"test": "jest",
|
||||
"lint": "eslint src/**/*.ts",
|
||||
"prepublishOnly": "npm run build"
|
||||
},
|
||||
"keywords": ["telecom", "api", "sdk", "typescript"],
|
||||
"author": "Telecom Platform Team",
|
||||
"license": "MIT",
|
||||
"dependencies": {},
|
||||
"devDependencies": {
|
||||
"@types/node": "^25.6.0",
|
||||
"@typescript-eslint/eslint-plugin": "^8.58.2",
|
||||
"@typescript-eslint/parser": "^8.58.2",
|
||||
"eslint": "^10.2.0",
|
||||
"jest": "^30.3.0",
|
||||
"typescript": "^6.0.3"
|
||||
},
|
||||
"files": [
|
||||
"dist/**/*",
|
||||
"README.md"
|
||||
],
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/telecom-platform/sdk.git"
|
||||
},
|
||||
"bugs": {
|
||||
"url": "https://github.com/telecom-platform/sdk/issues"
|
||||
},
|
||||
"homepage": "https://github.com/telecom-platform/sdk#readme"
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
export interface TelecomSDKConfig {
|
||||
baseURL: string;
|
||||
apiKey?: string;
|
||||
timeout?: number;
|
||||
retryAttempts?: number;
|
||||
retryDelay?: number;
|
||||
enableLogging?: boolean;
|
||||
websocketURL?: string;
|
||||
}
|
||||
|
||||
export class SDKConfig {
|
||||
private static instance: SDKConfig;
|
||||
private config: TelecomSDKConfig;
|
||||
|
||||
private constructor(config: TelecomSDKConfig) {
|
||||
this.config = {
|
||||
timeout: 30000,
|
||||
retryAttempts: 3,
|
||||
retryDelay: 1000,
|
||||
enableLogging: false,
|
||||
...config
|
||||
};
|
||||
}
|
||||
|
||||
public static initialize(config: TelecomSDKConfig): void {
|
||||
if (!SDKConfig.instance) {
|
||||
SDKConfig.instance = new SDKConfig(config);
|
||||
}
|
||||
}
|
||||
|
||||
public static getInstance(): SDKConfig {
|
||||
if (!SDKConfig.instance) {
|
||||
throw new Error('SDKConfig not initialized. Call SDKConfig.initialize() first.');
|
||||
}
|
||||
return SDKConfig.instance;
|
||||
}
|
||||
|
||||
public get baseURL(): string {
|
||||
return this.config.baseURL;
|
||||
}
|
||||
|
||||
public get apiKey(): string | undefined {
|
||||
return this.config.apiKey;
|
||||
}
|
||||
|
||||
public get timeout(): number {
|
||||
return this.config.timeout!;
|
||||
}
|
||||
|
||||
public get retryAttempts(): number {
|
||||
return this.config.retryAttempts!;
|
||||
}
|
||||
|
||||
public get retryDelay(): number {
|
||||
return this.config.retryDelay!;
|
||||
}
|
||||
|
||||
public get enableLogging(): boolean {
|
||||
return this.config.enableLogging!;
|
||||
}
|
||||
|
||||
public get websocketURL(): string | undefined {
|
||||
return this.config.websocketURL;
|
||||
}
|
||||
|
||||
public updateConfig(updates: Partial<TelecomSDKConfig>): void {
|
||||
this.config = { ...this.config, ...updates };
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,131 @@
|
||||
import { Error as SdkError } from './types';
|
||||
import { SDKConfig } from './config';
|
||||
|
||||
export class HttpClient {
|
||||
private config: SDKConfig;
|
||||
|
||||
constructor() {
|
||||
this.config = SDKConfig.getInstance();
|
||||
}
|
||||
|
||||
private get headers(): Record<string, string> {
|
||||
const h: Record<string, string> = {
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'application/json',
|
||||
};
|
||||
if (this.config.apiKey) {
|
||||
h['Authorization'] = `Bearer ${this.config.apiKey}`;
|
||||
}
|
||||
return h;
|
||||
}
|
||||
|
||||
private buildURL(path: string, params?: Record<string, unknown>): string {
|
||||
const url = new URL(path, this.config.baseURL);
|
||||
if (params) {
|
||||
Object.entries(params).forEach(([k, v]) => {
|
||||
if (v !== undefined && v !== null) {
|
||||
url.searchParams.set(k, String(v));
|
||||
}
|
||||
});
|
||||
}
|
||||
return url.toString();
|
||||
}
|
||||
|
||||
private async fetchWithRetry(url: string, init: RequestInit, attempt = 0): Promise<Response> {
|
||||
const controller = new AbortController();
|
||||
const timeout = setTimeout(() => controller.abort(), this.config.timeout);
|
||||
try {
|
||||
if (this.config.enableLogging) {
|
||||
console.log(`[SDK] ${init.method ?? 'GET'} ${url}`);
|
||||
}
|
||||
|
||||
const response = await fetch(url, { ...init, signal: controller.signal });
|
||||
|
||||
if (this.config.enableLogging) {
|
||||
console.log(`[SDK] Response ${response.status}`);
|
||||
}
|
||||
|
||||
if (!response.ok && this.shouldRetry(response.status) && attempt < this.config.retryAttempts) {
|
||||
await new Promise(r => setTimeout(r, this.config.retryDelay * Math.pow(2, attempt)));
|
||||
return this.fetchWithRetry(url, init, attempt + 1);
|
||||
}
|
||||
|
||||
return response;
|
||||
} catch (err) {
|
||||
if (attempt < this.config.retryAttempts && !(err instanceof DOMException)) {
|
||||
await new Promise(r => setTimeout(r, this.config.retryDelay * Math.pow(2, attempt)));
|
||||
return this.fetchWithRetry(url, init, attempt + 1);
|
||||
}
|
||||
throw err;
|
||||
} finally {
|
||||
clearTimeout(timeout);
|
||||
}
|
||||
}
|
||||
|
||||
private shouldRetry(status: number): boolean {
|
||||
return status >= 500 || status === 429;
|
||||
}
|
||||
|
||||
private async handleResponse<T>(response: Response): Promise<T> {
|
||||
if (!response.ok) {
|
||||
let body: any = {};
|
||||
try { body = await response.json(); } catch { /* empty */ }
|
||||
const err: SdkError = {
|
||||
code: body.code ?? `HTTP_${response.status}`,
|
||||
message: body.message ?? response.statusText,
|
||||
details: body.details,
|
||||
timestamp: new Date().toISOString(),
|
||||
};
|
||||
throw err;
|
||||
}
|
||||
return response.json() as Promise<T>;
|
||||
}
|
||||
|
||||
async get<T>(url: string, params?: Record<string, unknown>): Promise<T> {
|
||||
const response = await this.fetchWithRetry(
|
||||
this.buildURL(url, params),
|
||||
{ method: 'GET', headers: this.headers },
|
||||
);
|
||||
return this.handleResponse<T>(response);
|
||||
}
|
||||
|
||||
async post<T>(url: string, data?: unknown): Promise<T> {
|
||||
const response = await this.fetchWithRetry(
|
||||
this.buildURL(url),
|
||||
{ method: 'POST', headers: this.headers, body: data ? JSON.stringify(data) : null },
|
||||
);
|
||||
return this.handleResponse<T>(response);
|
||||
}
|
||||
|
||||
async put<T>(url: string, data?: unknown): Promise<T> {
|
||||
const response = await this.fetchWithRetry(
|
||||
this.buildURL(url),
|
||||
{ method: 'PUT', headers: this.headers, body: data ? JSON.stringify(data) : null },
|
||||
);
|
||||
return this.handleResponse<T>(response);
|
||||
}
|
||||
|
||||
async patch<T>(url: string, data?: unknown): Promise<T> {
|
||||
const response = await this.fetchWithRetry(
|
||||
this.buildURL(url),
|
||||
{ method: 'PATCH', headers: this.headers, body: data ? JSON.stringify(data) : null },
|
||||
);
|
||||
return this.handleResponse<T>(response);
|
||||
}
|
||||
|
||||
async delete<T>(url: string): Promise<T> {
|
||||
const response = await this.fetchWithRetry(
|
||||
this.buildURL(url),
|
||||
{ method: 'DELETE', headers: this.headers },
|
||||
);
|
||||
return this.handleResponse<T>(response);
|
||||
}
|
||||
|
||||
setApiKey(apiKey: string): void {
|
||||
this.config.updateConfig({ apiKey });
|
||||
}
|
||||
|
||||
setBaseURL(baseURL: string): void {
|
||||
this.config.updateConfig({ baseURL });
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
// Main entry point for the Telecom Platform TypeScript SDK
|
||||
|
||||
export { TelecomSDK } from './telecom-sdk';
|
||||
export { SDKConfig, TelecomSDKConfig } from './config';
|
||||
export { HttpClient } from './http-client';
|
||||
export { SubscribersService } from './subscribers';
|
||||
export { WebSocketClient } from './websocket';
|
||||
|
||||
// Export all types
|
||||
export * from './types';
|
||||
@@ -0,0 +1,193 @@
|
||||
import { HttpClient } from './http-client';
|
||||
import {
|
||||
Subscriber,
|
||||
CreateSubscriberRequest,
|
||||
UpdateSubscriberRequest,
|
||||
ListSubscribersRequest,
|
||||
PaginatedResponse,
|
||||
SubscriberAccount,
|
||||
TopUpRequest,
|
||||
Invoice,
|
||||
ProfileStatus,
|
||||
} from './types';
|
||||
|
||||
export class SubscribersService {
|
||||
private http: HttpClient;
|
||||
|
||||
constructor(http: HttpClient) {
|
||||
this.http = http;
|
||||
}
|
||||
|
||||
/**
|
||||
* List all subscribers with pagination and filtering
|
||||
*/
|
||||
async listSubscribers(request?: ListSubscribersRequest): Promise<PaginatedResponse<Subscriber>> {
|
||||
const params = {
|
||||
page: request?.page || 1,
|
||||
pageSize: request?.pageSize || 20,
|
||||
status: request?.status,
|
||||
organizationId: request?.organizationId,
|
||||
search: request?.search,
|
||||
};
|
||||
|
||||
return this.http.get<PaginatedResponse<Subscriber>>('/api/subscribers', params);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a specific subscriber by ID
|
||||
*/
|
||||
async getSubscriber(id: number): Promise<Subscriber> {
|
||||
return this.http.get<Subscriber>(`/api/subscribers/${id}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a subscriber by IMSI
|
||||
*/
|
||||
async getSubscriberByImsi(imsi: string): Promise<Subscriber> {
|
||||
return this.http.get<Subscriber>(`/api/subscribers/imsi/${imsi}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new subscriber
|
||||
*/
|
||||
async createSubscriber(request: CreateSubscriberRequest): Promise<Subscriber> {
|
||||
return this.http.post<Subscriber>('/api/subscribers', request);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update an existing subscriber
|
||||
*/
|
||||
async updateSubscriber(id: number, request: UpdateSubscriberRequest): Promise<Subscriber> {
|
||||
return this.http.put<Subscriber>(`/api/subscribers/${id}`, request);
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a subscriber
|
||||
*/
|
||||
async deleteSubscriber(id: number): Promise<void> {
|
||||
await this.http.delete<void>(`/api/subscribers/${id}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get subscriber account information
|
||||
*/
|
||||
async getSubscriberAccount(imsi: string): Promise<SubscriberAccount> {
|
||||
return this.http.get<SubscriberAccount>(`/api/subscribers/${imsi}/account`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Top up subscriber balance
|
||||
*/
|
||||
async topUpBalance(imsi: string, request: TopUpRequest): Promise<SubscriberAccount> {
|
||||
return this.http.post<SubscriberAccount>(`/api/subscribers/${imsi}/top-up`, request);
|
||||
}
|
||||
|
||||
/**
|
||||
* Suspend a subscriber
|
||||
*/
|
||||
async suspendSubscriber(id: number): Promise<Subscriber> {
|
||||
return this.http.post<Subscriber>(`/api/subscribers/${id}/suspend`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Activate a subscriber
|
||||
*/
|
||||
async activateSubscriber(id: number): Promise<Subscriber> {
|
||||
return this.http.post<Subscriber>(`/api/subscribers/${id}/activate`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Terminate a subscriber
|
||||
*/
|
||||
async terminateSubscriber(id: number): Promise<Subscriber> {
|
||||
return this.http.post<Subscriber>(`/api/subscribers/${id}/terminate`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Provision eSIM profile for subscriber
|
||||
*/
|
||||
async provisionESIM(imsi: string): Promise<{ profileId: string; activationCode: string }> {
|
||||
return this.http.post<{ profileId: string; activationCode: string }>(`/api/subscribers/${imsi}/esim/provision`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Activate eSIM profile
|
||||
*/
|
||||
async activateESIM(imsi: string, profileId: string): Promise<void> {
|
||||
return this.http.post<void>(`/api/subscribers/${imsi}/esim/activate`, { profileId });
|
||||
}
|
||||
|
||||
/**
|
||||
* Deactivate eSIM profile
|
||||
*/
|
||||
async deactivateESIM(imsi: string, profileId: string): Promise<void> {
|
||||
return this.http.post<void>(`/api/subscribers/${imsi}/esim/deactivate`, { profileId });
|
||||
}
|
||||
|
||||
/**
|
||||
* Get eSIM profile status
|
||||
*/
|
||||
async getESIMStatus(imsi: string): Promise<ProfileStatus> {
|
||||
return this.http.get<ProfileStatus>(`/api/subscribers/${imsi}/esim/status`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get subscriber invoices
|
||||
*/
|
||||
async getInvoices(imsi: string, page = 1, pageSize = 20): Promise<PaginatedResponse<Invoice>> {
|
||||
return this.http.get<PaginatedResponse<Invoice>>(`/api/subscribers/${imsi}/invoices`, {
|
||||
page,
|
||||
pageSize,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get specific invoice
|
||||
*/
|
||||
async getInvoice(invoiceId: string): Promise<Invoice> {
|
||||
return this.http.get<Invoice>(`/api/invoices/${invoiceId}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Download invoice PDF
|
||||
*/
|
||||
async downloadInvoicePDF(invoiceId: string): Promise<Blob> {
|
||||
const response = await this.http.get(`/api/invoices/${invoiceId}/pdf`);
|
||||
return response as any; // Return blob for PDF download
|
||||
}
|
||||
|
||||
/**
|
||||
* Get subscriber usage statistics
|
||||
*/
|
||||
async getUsageStats(imsi: string, period: 'daily' | 'weekly' | 'monthly' = 'monthly'): Promise<{
|
||||
dataUsage: number;
|
||||
voiceUsage: number;
|
||||
smsUsage: number;
|
||||
cost: number;
|
||||
period: string;
|
||||
}> {
|
||||
return this.http.get(`/api/subscribers/${imsi}/usage/stats`, { period });
|
||||
}
|
||||
|
||||
/**
|
||||
* Get real-time usage data
|
||||
*/
|
||||
async getRealTimeUsage(imsi: string): Promise<{
|
||||
currentSession: {
|
||||
sessionId: string;
|
||||
startTime: string;
|
||||
dataUsed: number;
|
||||
voiceUsed: number;
|
||||
smsUsed: number;
|
||||
cost: number;
|
||||
} | null;
|
||||
todayUsage: {
|
||||
dataUsed: number;
|
||||
voiceUsed: number;
|
||||
smsUsed: number;
|
||||
cost: number;
|
||||
};
|
||||
}> {
|
||||
return this.http.get(`/api/subscribers/${imsi}/usage/realtime`);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,201 @@
|
||||
import { SDKConfig, TelecomSDKConfig } from './config';
|
||||
import { HttpClient } from './http-client';
|
||||
import { SubscribersService } from './subscribers';
|
||||
import { WebSocketClient, WebSocketEventHandler } from './websocket';
|
||||
import { SystemStats, HealthStatus, Alert, RealTimeUsageUpdate, WebSocketMessage } from './types';
|
||||
|
||||
export class TelecomSDK {
|
||||
private static instance: TelecomSDK;
|
||||
private http: HttpClient;
|
||||
private ws: WebSocketClient;
|
||||
private subscribers: SubscribersService;
|
||||
|
||||
private constructor() {
|
||||
this.http = new HttpClient();
|
||||
this.ws = new WebSocketClient();
|
||||
this.subscribers = new SubscribersService(this.http);
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize the SDK with configuration
|
||||
*/
|
||||
static initialize(config: TelecomSDKConfig): TelecomSDK {
|
||||
if (!TelecomSDK.instance) {
|
||||
SDKConfig.initialize(config);
|
||||
TelecomSDK.instance = new TelecomSDK();
|
||||
}
|
||||
return TelecomSDK.instance;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the singleton instance
|
||||
*/
|
||||
static getInstance(): TelecomSDK {
|
||||
if (!TelecomSDK.instance) {
|
||||
throw new Error('TelecomSDK not initialized. Call TelecomSDK.initialize() first.');
|
||||
}
|
||||
return TelecomSDK.instance;
|
||||
}
|
||||
|
||||
/**
|
||||
* Subscribers service
|
||||
*/
|
||||
get subscribersService(): SubscribersService {
|
||||
return this.subscribers;
|
||||
}
|
||||
|
||||
/**
|
||||
* Connect to WebSocket for real-time updates
|
||||
*/
|
||||
async connectWebSocket(): Promise<void> {
|
||||
await this.ws.connect();
|
||||
}
|
||||
|
||||
/**
|
||||
* Disconnect from WebSocket
|
||||
*/
|
||||
disconnectWebSocket(): void {
|
||||
this.ws.disconnect();
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if WebSocket is connected
|
||||
*/
|
||||
get isWebSocketConnected(): boolean {
|
||||
return this.ws.isConnected;
|
||||
}
|
||||
|
||||
/**
|
||||
* Subscribe to real-time usage updates
|
||||
*/
|
||||
subscribeToUsage(imsi: string): void {
|
||||
this.ws.subscribeToUsage(imsi);
|
||||
}
|
||||
|
||||
/**
|
||||
* Unsubscribe from usage updates
|
||||
*/
|
||||
unsubscribeFromUsage(imsi: string): void {
|
||||
this.ws.unsubscribeFromUsage(imsi);
|
||||
}
|
||||
|
||||
/**
|
||||
* Subscribe to alerts
|
||||
*/
|
||||
subscribeToAlerts(): void {
|
||||
this.ws.subscribeToAlerts();
|
||||
}
|
||||
|
||||
/**
|
||||
* Unsubscribe from alerts
|
||||
*/
|
||||
unsubscribeFromAlerts(): void {
|
||||
this.ws.unsubscribeFromAlerts();
|
||||
}
|
||||
|
||||
/**
|
||||
* Add WebSocket event handler
|
||||
*/
|
||||
onWebSocketEvent(eventType: string, handler: WebSocketEventHandler): void {
|
||||
this.ws.on(eventType, handler);
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove WebSocket event handler
|
||||
*/
|
||||
offWebSocketEvent(eventType: string, handler: WebSocketEventHandler): void {
|
||||
this.ws.off(eventType, handler);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get system statistics
|
||||
*/
|
||||
async getSystemStats(): Promise<SystemStats> {
|
||||
return this.http.get<SystemStats>('/api/system/stats');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get system health status
|
||||
*/
|
||||
async getHealthStatus(): Promise<HealthStatus> {
|
||||
return this.http.get<HealthStatus>('/api/system/health');
|
||||
}
|
||||
|
||||
/**
|
||||
* Test API connection
|
||||
*/
|
||||
async testConnection(): Promise<{ status: string; timestamp: string }> {
|
||||
return this.http.get('/api/test');
|
||||
}
|
||||
|
||||
/**
|
||||
* Update API key
|
||||
*/
|
||||
setApiKey(apiKey: string): void {
|
||||
this.http.setApiKey(apiKey);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update base URL
|
||||
*/
|
||||
setBaseURL(baseURL: string): void {
|
||||
this.http.setBaseURL(baseURL);
|
||||
}
|
||||
|
||||
/**
|
||||
* Enable/disable logging
|
||||
*/
|
||||
setLogging(enabled: boolean): void {
|
||||
SDKConfig.getInstance().updateConfig({ enableLogging: enabled });
|
||||
}
|
||||
|
||||
/**
|
||||
* Get current configuration
|
||||
*/
|
||||
getConfig(): TelecomSDKConfig {
|
||||
const config = SDKConfig.getInstance();
|
||||
const result: TelecomSDKConfig = {
|
||||
baseURL: config.baseURL,
|
||||
timeout: config.timeout,
|
||||
retryAttempts: config.retryAttempts,
|
||||
retryDelay: config.retryDelay,
|
||||
enableLogging: config.enableLogging,
|
||||
};
|
||||
if (config.apiKey !== undefined) result.apiKey = config.apiKey;
|
||||
if (config.websocketURL !== undefined) result.websocketURL = config.websocketURL;
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convenience method for usage updates
|
||||
*/
|
||||
onUsageUpdate(callback: (update: RealTimeUsageUpdate) => void): void {
|
||||
this.onWebSocketEvent('usage_update', (message: WebSocketMessage) => {
|
||||
callback(message.data as RealTimeUsageUpdate);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Convenience method for alerts
|
||||
*/
|
||||
onAlert(callback: (alert: Alert) => void): void {
|
||||
this.onWebSocketEvent('alert', (message: WebSocketMessage) => {
|
||||
callback(message.data as Alert);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Cleanup resources
|
||||
*/
|
||||
destroy(): void {
|
||||
this.disconnectWebSocket();
|
||||
// Clear any other resources if needed
|
||||
}
|
||||
}
|
||||
|
||||
// Export types for external use
|
||||
export * from './types';
|
||||
export * from './config';
|
||||
export { WebSocketClient } from './websocket';
|
||||
export { HttpClient } from './http-client';
|
||||
export { SubscribersService } from './subscribers';
|
||||
@@ -0,0 +1,245 @@
|
||||
export interface Subscriber {
|
||||
id: number;
|
||||
imsi: string;
|
||||
msisdn: string;
|
||||
firstName: string;
|
||||
lastName: string;
|
||||
email: string;
|
||||
organizationId?: string;
|
||||
planId: number;
|
||||
euiccId?: string;
|
||||
profileId?: string;
|
||||
profileStatus: ProfileStatus;
|
||||
status: SubscriberStatus;
|
||||
balance: number;
|
||||
dataLimit: number;
|
||||
dataUsed: number;
|
||||
voiceLimit: number;
|
||||
voiceUsed: number;
|
||||
smsLimit: number;
|
||||
smsUsed: number;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
}
|
||||
|
||||
export enum SubscriberStatus {
|
||||
Active = 'active',
|
||||
Inactive = 'inactive',
|
||||
Suspended = 'suspended',
|
||||
Terminated = 'terminated',
|
||||
Provisioning = 'provisioning'
|
||||
}
|
||||
|
||||
export enum ProfileStatus {
|
||||
Active = 'active',
|
||||
Inactive = 'inactive',
|
||||
Downloading = 'downloading',
|
||||
Failed = 'failed'
|
||||
}
|
||||
|
||||
export interface CreateSubscriberRequest {
|
||||
msisdn: string;
|
||||
firstName: string;
|
||||
lastName: string;
|
||||
email: string;
|
||||
organizationId?: string;
|
||||
planId: number;
|
||||
euiccId?: string;
|
||||
}
|
||||
|
||||
export interface UpdateSubscriberRequest {
|
||||
firstName?: string;
|
||||
lastName?: string;
|
||||
email?: string;
|
||||
organizationId?: string;
|
||||
planId?: number;
|
||||
}
|
||||
|
||||
export interface SubscriberAccount {
|
||||
imsi: string;
|
||||
balance: number;
|
||||
dataLimit: number;
|
||||
dataUsed: number;
|
||||
voiceLimit: number;
|
||||
voiceUsed: number;
|
||||
smsLimit: number;
|
||||
smsUsed: number;
|
||||
status: SubscriberStatus;
|
||||
lastUpdated: string;
|
||||
}
|
||||
|
||||
export interface UsageEvent {
|
||||
imsi: string;
|
||||
sessionId: string;
|
||||
usageType: UsageType;
|
||||
volume: number;
|
||||
timestamp: string;
|
||||
rate: number;
|
||||
cost: number;
|
||||
}
|
||||
|
||||
export enum UsageType {
|
||||
Data = 'data',
|
||||
Voice = 'voice',
|
||||
SMS = 'sms'
|
||||
}
|
||||
|
||||
export interface RatingPlan {
|
||||
planId: string;
|
||||
name: string;
|
||||
dataRate: number;
|
||||
voiceRate: number;
|
||||
smsRate: number;
|
||||
monthlyFee: number;
|
||||
dataLimit: number;
|
||||
voiceLimit: number;
|
||||
smsLimit: number;
|
||||
}
|
||||
|
||||
export interface ChargingSession {
|
||||
sessionId: string;
|
||||
imsi: string;
|
||||
startTime: string;
|
||||
endTime?: string;
|
||||
dataBytes: number;
|
||||
voiceSeconds: number;
|
||||
smsCount: number;
|
||||
totalCost: number;
|
||||
status: SessionStatus;
|
||||
}
|
||||
|
||||
export enum SessionStatus {
|
||||
Active = 'active',
|
||||
Completed = 'completed',
|
||||
Terminated = 'terminated'
|
||||
}
|
||||
|
||||
export interface SystemStats {
|
||||
activeSessions: number;
|
||||
totalAccounts: number;
|
||||
blockedUsers: number;
|
||||
lowBalanceAlerts: number;
|
||||
uptime: number;
|
||||
}
|
||||
|
||||
export interface HealthStatus {
|
||||
redisConnected: boolean;
|
||||
activeSync: boolean;
|
||||
lastSync: string;
|
||||
memoryUsage: number;
|
||||
}
|
||||
|
||||
export interface ApiResponse<T> {
|
||||
data: T;
|
||||
message: string;
|
||||
success: boolean;
|
||||
code: number;
|
||||
}
|
||||
|
||||
export interface PaginatedResponse<T> {
|
||||
data: T[];
|
||||
total: number;
|
||||
page: number;
|
||||
pageSize: number;
|
||||
totalPages: number;
|
||||
}
|
||||
|
||||
export interface ListSubscribersRequest {
|
||||
page?: number;
|
||||
pageSize?: number;
|
||||
status?: SubscriberStatus;
|
||||
organizationId?: string;
|
||||
search?: string;
|
||||
}
|
||||
|
||||
export interface TopUpRequest {
|
||||
amount: number;
|
||||
paymentMethodId?: string;
|
||||
}
|
||||
|
||||
export interface PaymentMethod {
|
||||
id: string;
|
||||
type: PaymentMethodType;
|
||||
last4: string;
|
||||
expiryMonth: number;
|
||||
expiryYear: number;
|
||||
brand: string;
|
||||
isDefault: boolean;
|
||||
}
|
||||
|
||||
export enum PaymentMethodType {
|
||||
CreditCard = 'credit_card',
|
||||
BankAccount = 'bank_account'
|
||||
}
|
||||
|
||||
export interface Invoice {
|
||||
id: string;
|
||||
subscriberId: number;
|
||||
amount: number;
|
||||
currency: string;
|
||||
status: InvoiceStatus;
|
||||
dueDate: string;
|
||||
createdAt: string;
|
||||
lineItems: InvoiceLineItem[];
|
||||
}
|
||||
|
||||
export enum InvoiceStatus {
|
||||
Draft = 'draft',
|
||||
Pending = 'pending',
|
||||
Paid = 'paid',
|
||||
Overdue = 'overdue',
|
||||
Cancelled = 'cancelled'
|
||||
}
|
||||
|
||||
export interface InvoiceLineItem {
|
||||
description: string;
|
||||
quantity: number;
|
||||
unitPrice: number;
|
||||
amount: number;
|
||||
}
|
||||
|
||||
export interface WebSocketMessage {
|
||||
type: string;
|
||||
data: any;
|
||||
timestamp: string;
|
||||
}
|
||||
|
||||
export interface RealTimeUsageUpdate {
|
||||
imsi: string;
|
||||
dataUsed: number;
|
||||
voiceUsed: number;
|
||||
smsUsed: number;
|
||||
cost: number;
|
||||
timestamp: string;
|
||||
}
|
||||
|
||||
export interface Alert {
|
||||
id: string;
|
||||
type: AlertType;
|
||||
severity: AlertSeverity;
|
||||
message: string;
|
||||
subscriberId?: number;
|
||||
timestamp: string;
|
||||
resolved: boolean;
|
||||
}
|
||||
|
||||
export enum AlertType {
|
||||
LowBalance = 'low_balance',
|
||||
HighUsage = 'high_usage',
|
||||
PaymentFailed = 'payment_failed',
|
||||
SystemError = 'system_error'
|
||||
}
|
||||
|
||||
export enum AlertSeverity {
|
||||
Low = 'low',
|
||||
Medium = 'medium',
|
||||
High = 'high',
|
||||
Critical = 'critical'
|
||||
}
|
||||
|
||||
export interface Error {
|
||||
code: string;
|
||||
message: string;
|
||||
details?: any;
|
||||
timestamp: string;
|
||||
}
|
||||
@@ -0,0 +1,204 @@
|
||||
import { SDKConfig } from './config';
|
||||
import { WebSocketMessage } from './types';
|
||||
|
||||
export type WebSocketEventHandler = (message: WebSocketMessage) => void;
|
||||
|
||||
export class WebSocketClient {
|
||||
private ws: WebSocket | null = null;
|
||||
private config: SDKConfig;
|
||||
private eventHandlers: Map<string, WebSocketEventHandler[]> = new Map();
|
||||
private reconnectAttempts = 0;
|
||||
private maxReconnectAttempts = 5;
|
||||
private reconnectDelay = 1000;
|
||||
private isConnecting = false;
|
||||
private pingInterval: ReturnType<typeof setInterval> | null = null;
|
||||
|
||||
constructor() {
|
||||
this.config = SDKConfig.getInstance();
|
||||
}
|
||||
|
||||
/**
|
||||
* Connect to WebSocket server
|
||||
*/
|
||||
async connect(): Promise<void> {
|
||||
if (this.isConnecting || (this.ws && this.ws.readyState === WebSocket.OPEN)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this.config.websocketURL) {
|
||||
throw new Error('WebSocket URL not configured');
|
||||
}
|
||||
|
||||
this.isConnecting = true;
|
||||
|
||||
try {
|
||||
this.ws = new WebSocket(this.config.websocketURL);
|
||||
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
if (!this.ws) return reject(new Error('WebSocket not initialized'));
|
||||
|
||||
this.ws.onopen = () => {
|
||||
this.isConnecting = false;
|
||||
this.reconnectAttempts = 0;
|
||||
this.startPing();
|
||||
if (this.config.enableLogging) {
|
||||
console.log('[SDK] WebSocket connected');
|
||||
}
|
||||
resolve();
|
||||
};
|
||||
|
||||
this.ws.onmessage = (event: MessageEvent) => {
|
||||
try {
|
||||
const message: WebSocketMessage = JSON.parse(
|
||||
typeof event.data === 'string' ? event.data : new TextDecoder().decode(event.data),
|
||||
);
|
||||
this.handleMessage(message);
|
||||
} catch (error) {
|
||||
if (this.config.enableLogging) {
|
||||
console.error('[SDK] Failed to parse WebSocket message:', error);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
this.ws.onclose = (event: CloseEvent) => {
|
||||
this.isConnecting = false;
|
||||
this.stopPing();
|
||||
if (this.config.enableLogging) {
|
||||
console.log(`[SDK] WebSocket closed: ${event.code} - ${event.reason}`);
|
||||
}
|
||||
if (event.code !== 1000 && this.reconnectAttempts < this.maxReconnectAttempts) {
|
||||
this.scheduleReconnect();
|
||||
}
|
||||
};
|
||||
|
||||
this.ws.onerror = () => {
|
||||
this.isConnecting = false;
|
||||
if (this.config.enableLogging) {
|
||||
console.error('[SDK] WebSocket error');
|
||||
}
|
||||
reject(new Error('WebSocket connection failed'));
|
||||
};
|
||||
});
|
||||
} catch (error) {
|
||||
this.isConnecting = false;
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Disconnect from WebSocket server
|
||||
*/
|
||||
disconnect(): void {
|
||||
this.stopPing();
|
||||
if (this.ws) {
|
||||
this.ws.close(1000, 'Client disconnect');
|
||||
this.ws = null;
|
||||
}
|
||||
this.reconnectAttempts = this.maxReconnectAttempts;
|
||||
}
|
||||
|
||||
/**
|
||||
* Send message to WebSocket server
|
||||
*/
|
||||
send(message: unknown): void {
|
||||
if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
|
||||
throw new Error('WebSocket not connected');
|
||||
}
|
||||
this.ws.send(JSON.stringify(message));
|
||||
}
|
||||
|
||||
/**
|
||||
* Subscribe to real-time usage updates for a subscriber
|
||||
*/
|
||||
subscribeToUsage(imsi: string): void {
|
||||
this.send({ type: 'subscribe', channel: 'usage', imsi });
|
||||
}
|
||||
|
||||
/**
|
||||
* Unsubscribe from usage updates
|
||||
*/
|
||||
unsubscribeFromUsage(imsi: string): void {
|
||||
this.send({ type: 'unsubscribe', channel: 'usage', imsi });
|
||||
}
|
||||
|
||||
/**
|
||||
* Subscribe to alerts
|
||||
*/
|
||||
subscribeToAlerts(): void {
|
||||
this.send({ type: 'subscribe', channel: 'alerts' });
|
||||
}
|
||||
|
||||
/**
|
||||
* Unsubscribe from alerts
|
||||
*/
|
||||
unsubscribeFromAlerts(): void {
|
||||
this.send({ type: 'unsubscribe', channel: 'alerts' });
|
||||
}
|
||||
|
||||
/**
|
||||
* Add event handler for specific message type
|
||||
*/
|
||||
on(eventType: string, handler: WebSocketEventHandler): void {
|
||||
if (!this.eventHandlers.has(eventType)) {
|
||||
this.eventHandlers.set(eventType, []);
|
||||
}
|
||||
this.eventHandlers.get(eventType)!.push(handler);
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove event handler
|
||||
*/
|
||||
off(eventType: string, handler: WebSocketEventHandler): void {
|
||||
const handlers = this.eventHandlers.get(eventType);
|
||||
if (handlers) {
|
||||
const index = handlers.indexOf(handler);
|
||||
if (index > -1) {
|
||||
handlers.splice(index, 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get connection status
|
||||
*/
|
||||
get isConnected(): boolean {
|
||||
return this.ws?.readyState === WebSocket.OPEN;
|
||||
}
|
||||
|
||||
private handleMessage(message: WebSocketMessage): void {
|
||||
const handlers = this.eventHandlers.get(message.type);
|
||||
if (handlers) {
|
||||
handlers.forEach(handler => handler(message));
|
||||
}
|
||||
}
|
||||
|
||||
private scheduleReconnect(): void {
|
||||
const delay = this.reconnectDelay * Math.pow(2, this.reconnectAttempts);
|
||||
setTimeout(() => {
|
||||
this.reconnectAttempts++;
|
||||
if (this.config.enableLogging) {
|
||||
console.log(`[SDK] Reconnecting (${this.reconnectAttempts}/${this.maxReconnectAttempts})`);
|
||||
}
|
||||
this.connect().catch(error => {
|
||||
if (this.config.enableLogging) {
|
||||
console.error('[SDK] Reconnection failed:', error);
|
||||
}
|
||||
});
|
||||
}, delay);
|
||||
}
|
||||
|
||||
private startPing(): void {
|
||||
this.pingInterval = setInterval(() => {
|
||||
if (this.ws && this.ws.readyState === WebSocket.OPEN) {
|
||||
this.send({ type: 'ping', timestamp: new Date().toISOString() });
|
||||
}
|
||||
}, 30000);
|
||||
}
|
||||
|
||||
private stopPing(): void {
|
||||
if (this.pingInterval) {
|
||||
clearInterval(this.pingInterval);
|
||||
this.pingInterval = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2020",
|
||||
"module": "commonjs",
|
||||
"lib": ["ES2020", "DOM"],
|
||||
"outDir": "./dist",
|
||||
"rootDir": "./src",
|
||||
"strict": true,
|
||||
"esModuleInterop": true,
|
||||
"skipLibCheck": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"declaration": true,
|
||||
"declarationMap": true,
|
||||
"sourceMap": true,
|
||||
"removeComments": false,
|
||||
"noImplicitAny": true,
|
||||
"noImplicitReturns": true,
|
||||
"noImplicitThis": true,
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"exactOptionalPropertyTypes": true
|
||||
},
|
||||
"include": [
|
||||
"src/**/*"
|
||||
],
|
||||
"exclude": [
|
||||
"node_modules",
|
||||
"dist",
|
||||
"**/*.test.ts"
|
||||
]
|
||||
}
|
||||
Reference in New Issue
Block a user