mirror of
https://github.com/andwati/telecom-platform.git
synced 2026-10-25 14:12:16 +00:00
feat: Add Kotlin SDK with coroutine-based HTTP client and comprehensive Telecom Platform API integration
- Add TelecomSDK class with create factory method and TelecomConfig with 30s timeout, 3 retries - Add Ktor HttpClient with ContentNegotiation, HttpTimeout, Retry plugins and Bearer token authentication - Add subscriber management: getSubscriber, listSubscribers with pagination/status filter, createSubscriber, updateSubscriber, suspendSubscriber, activateSubscriber, terminateSubscriber - Add usage
This commit is contained in:
@@ -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)
|
||||
}
|
||||
Reference in New Issue
Block a user