From 63506fdc24ad16868813d75921f62b99fccc394d Mon Sep 17 00:00:00 2001 From: nutcas3 Date: Fri, 17 Apr 2026 17:45:34 +0300 Subject: [PATCH] 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 --- .../src/main/kotlin/com/telecom/TelecomSDK.kt | 345 ++++++++++++++++++ .../src/main/kotlin/com/telecom/Types.kt | 279 ++++++++++++++ 2 files changed, 624 insertions(+) create mode 100644 sdk/kotlin/src/main/kotlin/com/telecom/TelecomSDK.kt create mode 100644 sdk/kotlin/src/main/kotlin/com/telecom/Types.kt diff --git a/sdk/kotlin/src/main/kotlin/com/telecom/TelecomSDK.kt b/sdk/kotlin/src/main/kotlin/com/telecom/TelecomSDK.kt new file mode 100644 index 0000000..fe1aa88 --- /dev/null +++ b/sdk/kotlin/src/main/kotlin/com/telecom/TelecomSDK.kt @@ -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() + 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 { + 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 = 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(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 = 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 +) diff --git a/sdk/kotlin/src/main/kotlin/com/telecom/Types.kt b/sdk/kotlin/src/main/kotlin/com/telecom/Types.kt new file mode 100644 index 0000000..eb6dd4c --- /dev/null +++ b/sdk/kotlin/src/main/kotlin/com/telecom/Types.kt @@ -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, + 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? = null +) + +@Serializable +data class UsageEventList( + val events: List, + 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? = 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? = null +) + +@Serializable +data class PaymentTransactionList( + val transactions: List, + 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? = 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, + val uptime: Double +) + +@Serializable +data class WebSocketMessage( + val type: String, + val data: Map, + val timestamp: LocalDateTime +) + +@Serializable +data class GraphQLRequest( + val query: String, + val variables: Map = emptyMap() +) + +@Serializable +data class GraphQLResponse( + val data: Map? = null, + val errors: List? = null +) + +@Serializable +data class GraphQLError( + val message: String, + val locations: List? = null, + val path: List? = null, + val extensions: Map? = null +) + +@Serializable +data class GraphQLErrorLocation( + val line: Int, + val column: Int +) + +object AnySerializer : KSerializer { + 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) +}