Merge pull request #11 from nutcas3/sdks

Sdks
This commit is contained in:
Nutcase
2026-04-18 17:42:32 +03:00
committed by GitHub
33 changed files with 5676 additions and 1 deletions
-1
View File
@@ -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
+335
View File
@@ -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
+381
View File
@@ -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
+39
View File
@@ -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
+226
View File
@@ -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.
+3
View File
@@ -0,0 +1,3 @@
module github.com/nutcas3/telecom-sdk-go
go 1.26
+273
View File
@@ -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
View File
@@ -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{}
+41
View File
@@ -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)
}
+300
View File
@@ -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.
+6
View File
@@ -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
+30
View File
@@ -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",
]
+422
View File
@@ -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")
+31
View File
@@ -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
+141
View File
@@ -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
+222
View File
@@ -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
+26
View File
@@ -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
+23
View File
@@ -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"
+525
View File
@@ -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(())
}
}
+34
View File
@@ -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
}
}
+312
View File
@@ -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.
+38
View File
@@ -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"
}
+69
View File
@@ -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 };
}
}
+131
View File
@@ -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 });
}
}
+10
View File
@@ -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';
+193
View File
@@ -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`);
}
}
+201
View File
@@ -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';
+245
View File
@@ -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;
}
+204
View File
@@ -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;
}
}
}
+31
View File
@@ -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"
]
}