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:
nutcas3
2026-04-17 17:45:34 +03:00
parent 232d80bdce
commit 63506fdc24
2 changed files with 624 additions and 0 deletions
@@ -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)
}