diff --git a/CHANGELOG.md b/CHANGELOG.md
index f862754..a088761 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -4,6 +4,11 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](http://keepachangelog.com/)
and this project adheres to [Semantic Versioning](http://semver.org/).
+## [0.7.0] - 2024-08-??
+
+### Added
+- Numbers API
+
## [0.6.0] - 2024-07-30
### Added
diff --git a/README.md b/README.md
index a3f884e..088b948 100644
--- a/README.md
+++ b/README.md
@@ -23,6 +23,7 @@ You'll need to have [created a Vonage account](https://dashboard.nexmo.com/sign-
- [Voice](https://developer.vonage.com/en/voice/voice-api/overview)
- [SIM Swap](https://developer.vonage.com/en/sim-swap/overview)
- [Number Verification](https://developer.vonage.com/en/number-verification/overview)
+- [Number Management](https://developer.vonage.com/en/numbers/overview)
- [Number Insight](https://developer.vonage.com/en/number-insight/overview)
- [SMS](https://developer.vonage.com/en/messaging/sms/overview)
- [Conversion](https://developer.vonage.com/en/messaging/conversion-api/overview)
diff --git a/pom.xml b/pom.xml
index 14ebdf1..859036c 100644
--- a/pom.xml
+++ b/pom.xml
@@ -5,7 +5,7 @@
com.vonage
server-sdk-kotlin
- 0.6.0
+ 0.7.0
Vonage Kotlin Server SDK
Kotlin client for Vonage APIs
@@ -59,7 +59,7 @@
com.vonage
server-sdk
- 8.9.4
+ 8.10.0
org.jetbrains.kotlin
diff --git a/src/main/kotlin/com/vonage/client/kt/Numbers.kt b/src/main/kotlin/com/vonage/client/kt/Numbers.kt
new file mode 100644
index 0000000..b73d59b
--- /dev/null
+++ b/src/main/kotlin/com/vonage/client/kt/Numbers.kt
@@ -0,0 +1,41 @@
+/*
+ * Copyright 2024 Vonage
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.vonage.client.kt
+
+import com.vonage.client.numbers.*
+
+class Numbers(private val numbersClient: NumbersClient) {
+
+ fun number(countryCode: String, msisdn: String) = ExistingNumber(countryCode, msisdn)
+
+ inner class ExistingNumber internal constructor(val countryCode: String, val msisdn: String) {
+
+ fun buy(targetApiKey: String? = null) =
+ numbersClient.buyNumber(countryCode, msisdn, targetApiKey)
+
+ fun cancel(targetApiKey: String? = null) =
+ numbersClient.cancelNumber(countryCode, msisdn, targetApiKey)
+
+ fun update(properties: UpdateNumberRequest.Builder.() -> Unit) =
+ numbersClient.updateNumber(UpdateNumberRequest.builder(msisdn, countryCode).apply(properties).build())
+ }
+
+ fun listOwned(filter: ListNumbersFilter.Builder.() -> Unit = {}) =
+ numbersClient.listNumbers(ListNumbersFilter.builder().apply(filter).build())
+
+ fun searchAvailable(filter: SearchNumbersFilter.Builder.() -> Unit) =
+ numbersClient.searchNumbers(SearchNumbersFilter.builder().apply(filter).build())
+}
diff --git a/src/main/kotlin/com/vonage/client/kt/Voice.kt b/src/main/kotlin/com/vonage/client/kt/Voice.kt
index fb5068e..42b4939 100644
--- a/src/main/kotlin/com/vonage/client/kt/Voice.kt
+++ b/src/main/kotlin/com/vonage/client/kt/Voice.kt
@@ -25,9 +25,7 @@ class Voice(private val voiceClient: VoiceClient) {
fun call(callId: String): ExistingCall = ExistingCall(callId)
- fun call(callId: UUID): ExistingCall = call(callId.toString())
-
- inner class ExistingCall(val callId: String) {
+ inner class ExistingCall internal constructor(val callId: String) {
fun info(): CallInfo = voiceClient.getCallDetails(callId)
diff --git a/src/main/kotlin/com/vonage/client/kt/Vonage.kt b/src/main/kotlin/com/vonage/client/kt/Vonage.kt
index 9688632..5fa5a7b 100644
--- a/src/main/kotlin/com/vonage/client/kt/Vonage.kt
+++ b/src/main/kotlin/com/vonage/client/kt/Vonage.kt
@@ -28,8 +28,9 @@ class Vonage(init: VonageClient.Builder.() -> Unit) {
val redact = Redact(vonageClient.redactClient)
val verifyLegacy = VerifyLegacy(vonageClient.verifyClient)
val numberInsight = NumberInsight(vonageClient.insightClient)
- val simSwap = SimSwap(vonageClient.simSwapClient)
+ val numbers = Numbers(vonageClient.numbersClient)
val numberVerification = NumberVerification(vonageClient.numberVerificationClient)
+ val simSwap = SimSwap(vonageClient.simSwapClient)
}
fun VonageClient.Builder.authFromEnv(): VonageClient.Builder {
diff --git a/src/test/kotlin/com/vonage/client/kt/AbstractTest.kt b/src/test/kotlin/com/vonage/client/kt/AbstractTest.kt
index 7f6c02e..e6bd8f3 100644
--- a/src/test/kotlin/com/vonage/client/kt/AbstractTest.kt
+++ b/src/test/kotlin/com/vonage/client/kt/AbstractTest.kt
@@ -52,8 +52,13 @@ abstract class AbstractTest {
protected val testUuid: UUID = UUID.fromString(testUuidStr)
protected val toNumber = "447712345689"
protected val altNumber = "447700900001"
+ protected val brand = "Nexmo KT"
protected val text = "Hello, World!"
+ protected val sipUri = "sip:rebekka@sip.example.com"
+ protected val clientRef = "my-personal-reference"
protected val textHexEncoded = "48656c6c6f2c20576f726c6421"
+ protected val entityId = "1101407360000017170"
+ protected val contentId = "1107158078772563946"
protected val smsMessageId = "0C000000217B7F02"
protected val callIdStr = "63f61863-4a51-4f6b-86e1-46edebcf9356"
protected val networkCode = "65512"
diff --git a/src/test/kotlin/com/vonage/client/kt/MessagesTest.kt b/src/test/kotlin/com/vonage/client/kt/MessagesTest.kt
index b46f40a..14ae0cd 100644
--- a/src/test/kotlin/com/vonage/client/kt/MessagesTest.kt
+++ b/src/test/kotlin/com/vonage/client/kt/MessagesTest.kt
@@ -96,7 +96,6 @@ class MessagesTest : AbstractTest() {
@Test
fun `send SMS text all parameters`() {
- val clientRef = "My reference"
val webhookUrl = "https://example.com/status"
val ttl = 9000
val contentId = "1107457532145798767"
diff --git a/src/test/kotlin/com/vonage/client/kt/NumberVerificationTest.kt b/src/test/kotlin/com/vonage/client/kt/NumberVerificationTest.kt
index d2e439e..b549219 100644
--- a/src/test/kotlin/com/vonage/client/kt/NumberVerificationTest.kt
+++ b/src/test/kotlin/com/vonage/client/kt/NumberVerificationTest.kt
@@ -62,8 +62,7 @@ class NumberVerificationTest : AbstractTest() {
URLEncoder.encode(redirectUrl, "UTF-8")
}&response_type=code"
- val expectedUrlWithoutState = URI.create("$expectedUrlStr&state=null")
- assertEquals(expectedUrlWithoutState, nvClient.createVerificationUrl(toNumber, redirectUrl))
+ assertEquals(URI.create(expectedUrlStr), nvClient.createVerificationUrl(toNumber, redirectUrl))
val expectedUrlWithState = URI.create("$expectedUrlStr&state=$state")
assertEquals(expectedUrlWithState, nvClient.createVerificationUrl(toNumber, redirectUrl, state))
diff --git a/src/test/kotlin/com/vonage/client/kt/NumbersTest.kt b/src/test/kotlin/com/vonage/client/kt/NumbersTest.kt
new file mode 100644
index 0000000..41b4a59
--- /dev/null
+++ b/src/test/kotlin/com/vonage/client/kt/NumbersTest.kt
@@ -0,0 +1,274 @@
+/*
+ * Copyright 2024 Vonage
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.vonage.client.kt
+
+import com.vonage.client.numbers.*
+import com.vonage.client.numbers.UpdateNumberRequest.CallbackType
+import java.util.*
+import kotlin.test.*
+
+class NumbersTest : AbstractTest() {
+ private val numbersClient = vonage.numbers
+ private val country = "GB"
+ private val targetApiKey = "1a2345b7"
+ private val moHttpUrl = "$callbackUrl/inbound-sms"
+ private val moSmppSysType = "inbound"
+ private val voiceStatusCallback = "$callbackUrl/status"
+ private val featureNames = Feature.entries.map(Feature::name)
+ private val pattern = "1337*"
+ private val count = 1247
+ private val size = 25
+ private val index = 6
+ private val existingNumber = numbersClient.number(country, toNumber)
+ private val baseRequestParams = mapOf(
+ "country" to existingNumber.countryCode,
+ "msisdn" to existingNumber.msisdn
+ )
+ private val targetApiKeyMap = mapOf("target_api_key" to targetApiKey)
+ private val successResponseMap = mapOf(
+ "error-code" to "200",
+ "error-code-label" to "success"
+ )
+
+ private fun mockAction(endpoint: String, additionalParams: Map = mapOf()) {
+ mockPostQueryParams(
+ expectedUrl = "/number/$endpoint",
+ expectedRequestParams = baseRequestParams + additionalParams,
+ authType = AuthType.API_KEY_SECRET_HEADER,
+ expectedResponseParams = successResponseMap
+ )
+ }
+
+ private fun assertOwnedNumbers(params: Map, invocation: Numbers.() -> ListNumbersResponse) {
+ val type = Type.MOBILE_LVN
+ val voiceCallbackType = CallbackType.SIP
+ val messagesCallbackValue = "aaaaaaaa-bbbb-cccc-dddd-0123456789ab"
+
+ mockGet(
+ expectedUrl = "/account/numbers",
+ expectedQueryParams = params,
+ authType = AuthType.API_KEY_SECRET_HEADER,
+ expectedResponseParams = mapOf(
+ "count" to count,
+ "numbers" to listOf(
+ mapOf(),
+ baseRequestParams + mapOf(
+ "moHttpUrl" to moHttpUrl,
+ "type" to type.name.lowercase().replace('_', '-'),
+ "features" to featureNames,
+ "messagesCallbackType" to "app",
+ "messagesCallbackValue" to messagesCallbackValue,
+ "voiceCallbackType" to voiceCallbackType.name.lowercase(),
+ "voiceCallbackValue" to sipUri,
+ "app_id" to applicationId
+ )
+ )
+ )
+ )
+
+ val response = invocation.invoke(numbersClient)
+ assertNotNull(response)
+ assertEquals(count, response.count)
+ val numbers = response.numbers
+ assertNotNull(numbers)
+ assertEquals(2, numbers.size)
+
+ val empty = numbers[0]
+ assertNotNull(empty)
+ assertNull(empty.msisdn)
+ assertNull(empty.country)
+ assertNull(empty.voiceCallbackType)
+ assertNull(empty.voiceCallbackValue)
+ assertNull(empty.messagesCallbackValue)
+ assertNull(empty.moHttpUrl)
+ assertNull(empty.type)
+ assertNull(empty.features)
+
+ val main = numbers[1]
+ assertNotNull(main)
+ assertEquals(country, main.country)
+ assertEquals(toNumber, main.msisdn)
+ assertEquals(moHttpUrl, main.moHttpUrl)
+ assertEquals(type, Type.fromString(main.type))
+ assertEquals(featureNames, main.features.toList())
+ assertEquals(UUID.fromString(messagesCallbackValue), main.messagesCallbackValue)
+ assertEquals(voiceCallbackType, CallbackType.fromString(main.voiceCallbackType))
+ assertEquals(sipUri, main.voiceCallbackValue)
+ }
+
+ private fun assertAvailableNumbers(params: Map, invocation: Numbers.() -> SearchNumbersResponse) {
+ val landline = "44800123456"
+ mockGet(
+ expectedUrl = "/number/search",
+ expectedQueryParams = params,
+ authType = AuthType.API_KEY_SECRET_HEADER,
+ expectedResponseParams = mapOf(
+ "count" to count,
+ "numbers" to listOf(
+ mapOf("cost" to "1.29"),
+ mapOf(
+ "country" to country,
+ "msisdn" to landline,
+ "type" to "landline-toll-free",
+ "cost" to "3.80",
+ "features" to listOf("VOICE")
+ ),
+ baseRequestParams + mapOf(
+ "features" to listOf("SMS", "MMS"),
+ "type" to "mobile-lvn"
+ )
+ )
+ )
+ )
+
+ val response = invocation.invoke(numbersClient)
+ assertNotNull(response)
+ assertEquals(count, response.count)
+ val numbers = response.numbers
+ assertNotNull(numbers)
+ assertEquals(3, numbers.size)
+
+ val costOnly = numbers[0]
+ assertNotNull(costOnly)
+ assertEquals(1.29, costOnly.cost.toDouble())
+ assertNull(costOnly.type)
+ assertNull(costOnly.country)
+ assertNull(costOnly.msisdn)
+ assertNull(costOnly.features)
+
+ val main = numbers[1]
+ assertNotNull(main)
+ assertEquals(Type.LANDLINE_TOLL_FREE, Type.fromString(main.type))
+ assertEquals(3.80, main.cost.toDouble())
+ assertEquals(landline, main.msisdn)
+ assertEquals(country, main.country)
+ val mainFeatures = main.features
+ assertNotNull(mainFeatures)
+ assertEquals(1, mainFeatures.size)
+ assertEquals(Feature.VOICE, Feature.fromString(mainFeatures[0]))
+
+ val mobile = numbers[2]
+ assertEquals(country, mobile.country)
+ assertEquals(toNumber, mobile.msisdn)
+ assertEquals(Type.MOBILE_LVN, Type.fromString(mobile.type))
+ val mobileFeatures = mobile.features
+ assertNotNull(mobileFeatures)
+ assertEquals(2, mobileFeatures.size)
+ assertEquals(Feature.SMS, Feature.fromString(mobileFeatures[0]))
+ assertEquals(Feature.MMS, Feature.fromString(mobileFeatures[1]))
+ assertNull(mobile.cost)
+ }
+
+ @Test
+ fun `buy number`() {
+ mockAction("buy")
+ existingNumber.buy()
+ }
+
+ @Test
+ fun `buy number with target api key`() {
+ mockAction("buy", targetApiKeyMap)
+ existingNumber.buy(targetApiKey)
+ }
+
+ @Test
+ fun `cancel number`() {
+ mockAction("cancel")
+ existingNumber.cancel()
+ }
+
+ @Test
+ fun `cancel number with target api key`() {
+ mockAction("cancel", targetApiKeyMap)
+ existingNumber.cancel(targetApiKey)
+ }
+
+ @Test
+ fun `update no parameters`() {
+ mockAction("update")
+ existingNumber.update {}
+ }
+
+ @Test
+ fun `update all parameters`() {
+ mockAction("update", mapOf(
+ "app_id" to applicationId,
+ "moHttpUrl" to moHttpUrl,
+ "moSmppSysType" to moSmppSysType,
+ "voiceStatusCallback" to voiceStatusCallback,
+ "voiceCallbackType" to "tel",
+ "voiceCallbackValue" to altNumber
+ ))
+ existingNumber.update {
+ applicationId(applicationId)
+ moHttpUrl(moHttpUrl); moSmppSysType(moSmppSysType)
+ voiceStatusCallback(voiceStatusCallback)
+ voiceCallback(CallbackType.TEL, altNumber)
+ }
+ }
+
+ @Test
+ fun `list owned numbers no parameters`() {
+ assertOwnedNumbers(mapOf()) { listOwned() }
+ }
+
+ @Test
+ fun `list owned numbers all parameters`() {
+ val hasApplication = true
+ assertOwnedNumbers(mapOf(
+ "country" to country,
+ "application_id" to applicationId,
+ "has_application" to hasApplication,
+ "pattern" to pattern,
+ "search_pattern" to 2,
+ "size" to size,
+ "index" to index
+ )) {
+ listOwned {
+ country(country)
+ applicationId(applicationId)
+ hasApplication(hasApplication)
+ pattern(SearchPattern.ENDS_WITH, pattern)
+ size(size); index(index)
+ }
+ }
+ }
+
+ @Test
+ fun `search available numbers no parameters`() {
+ assertAvailableNumbers(mapOf()) {
+ searchAvailable { }
+ }
+ }
+
+ @Test
+ fun `search available numbers all parameters`() {
+ assertAvailableNumbers(mapOf(
+ "country" to country,
+ "pattern" to pattern,
+ "search_pattern" to 0,
+ "features" to featureNames.joinToString(","),
+ "size" to size,
+ "index" to index
+ )) {
+ searchAvailable {
+ country(country); size(size); index(index)
+ pattern(SearchPattern.STARTS_WITH, pattern)
+ features(Feature.SMS, Feature.MMS, Feature.VOICE)
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/test/kotlin/com/vonage/client/kt/SmsTest.kt b/src/test/kotlin/com/vonage/client/kt/SmsTest.kt
index b7c80cd..7d98294 100644
--- a/src/test/kotlin/com/vonage/client/kt/SmsTest.kt
+++ b/src/test/kotlin/com/vonage/client/kt/SmsTest.kt
@@ -24,14 +24,11 @@ import kotlin.test.*
class SmsTest : AbstractTest() {
private val smsClient = vonage.sms
private val sendUrl = "/sms/json"
- private val from = "Nexmo"
- private val clientRef = "my-personal-reference"
+ private val from = brand
private val accountRef = "customer1234"
private val ttl = 900000
private val statusReport = true
- private val callback = "https://example.com/sms-dlr"
- private val entityId = "1101456324675322134"
- private val contentId = "1107457532145798767"
+ private val callback = "$exampleUrlBase/sms-dlr"
private val udhBinary = byteArrayOf(0x05, 0x00, 0x03, 0x7A, 0x02, 0x01)
@OptIn(ExperimentalStdlibApi::class)
private val udhHex = udhBinary.toHexString(HexFormat.UpperCase)
diff --git a/src/test/kotlin/com/vonage/client/kt/VerifyTest.kt b/src/test/kotlin/com/vonage/client/kt/VerifyTest.kt
index 0586cec..0d429f1 100644
--- a/src/test/kotlin/com/vonage/client/kt/VerifyTest.kt
+++ b/src/test/kotlin/com/vonage/client/kt/VerifyTest.kt
@@ -29,8 +29,6 @@ class VerifyTest : AbstractTest() {
private val requestIdStr = "c11236f4-00bf-4b89-84ba-88b25df97315"
private val requestId = UUID.fromString(requestIdStr)
private val requestIdUrl = "$baseUrl/$requestIdStr"
- private val brand = "Nexmo KT"
- private val clientRef = "my-personal-reference"
private val timeout = 60
private val fraudCheck = false
private val sandbox = true
@@ -38,8 +36,6 @@ class VerifyTest : AbstractTest() {
private val code = "1228864"
private val locale = "ja-jp"
private val whatsappNumber = "447700400080"
- private val entityId = "1101407360000017170"
- private val contentId = "1107158078772563946"
private val appHash = "ABC123def45"
private val toEmail = "alice@example.com"
private val fromEmail = "bob@example.org"
diff --git a/src/test/kotlin/com/vonage/client/kt/VoiceTest.kt b/src/test/kotlin/com/vonage/client/kt/VoiceTest.kt
index 80ba092..54bc226 100644
--- a/src/test/kotlin/com/vonage/client/kt/VoiceTest.kt
+++ b/src/test/kotlin/com/vonage/client/kt/VoiceTest.kt
@@ -27,7 +27,7 @@ class VoiceTest : AbstractTest() {
private val voiceClient = vonage.voice
private val callsBaseUrl = "/v1/calls"
private val callUrl = "$callsBaseUrl/$callIdStr"
- private val callObj = voiceClient.call(UUID.fromString(callIdStr))
+ private val callObj = voiceClient.call(callIdStr)
private val conversationId = "CON-f972836a-550f-45fa-956c-12a2ab5b7d22"
private val price = "23.40"
private val duration = 60
@@ -45,7 +45,6 @@ class VoiceTest : AbstractTest() {
private val onAnswerUrl = "https://example.com/ncco.json"
private val websocketUri = "wss://example.com/socket"
private val ringbackTone = "http://example.com/ringbackTone.wav"
- private val sipUri = "sip:rebekka@sip.example.com"
private val wsContentType = "audio/l16;rate=8000"
private val userToUserHeader = "56a390f3d2b7310023a"
private val conversationName = "selective-audio Demo"