diff --git a/CHANGELOG.md b/CHANGELOG.md index 9ee0ffc..aec7db4 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.5.0] - 2024-07-25 + +### Added +- Number Insight v1 API + ## [0.4.0] - 2024-07-23 ### Added diff --git a/README.md b/README.md index 7e38ec9..3f32d03 100644 --- a/README.md +++ b/README.md @@ -21,6 +21,7 @@ You'll need to have [created a Vonage account](https://dashboard.nexmo.com/sign- - [Messages](https://developer.vonage.com/en/messages/overview) - [Verify](https://developer.vonage.com/en/verify/overview) - [Voice](https://developer.vonage.com/en/voice/voice-api/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) - [Redact](https://developer.vonage.com/en/redact/overview) diff --git a/pom.xml b/pom.xml index d149214..c9fbb15 100644 --- a/pom.xml +++ b/pom.xml @@ -5,7 +5,7 @@ com.vonage server-sdk-kotlin - 0.4.0 + 0.5.0 Vonage Kotlin Server SDK Kotlin client for Vonage APIs @@ -59,7 +59,7 @@ com.vonage server-sdk - 8.9.3 + 8.9.4 org.jetbrains.kotlin diff --git a/src/main/kotlin/com/vonage/client/kt/NumberInsight.kt b/src/main/kotlin/com/vonage/client/kt/NumberInsight.kt new file mode 100644 index 0000000..a12f7b7 --- /dev/null +++ b/src/main/kotlin/com/vonage/client/kt/NumberInsight.kt @@ -0,0 +1,27 @@ +package com.vonage.client.kt + +import com.vonage.client.insight.* + +class NumberInsight(private val niClient: InsightClient) { + + fun basic(number: String, countryCode: String? = null): BasicInsightResponse = + niClient.getBasicNumberInsight(number, countryCode) + + fun standard(number: String, countryCode: String? = null, cnam: Boolean? = null): StandardInsightResponse = + niClient.getStandardNumberInsight(StandardInsightRequest.builder() + .number(number).country(countryCode).cnam(cnam).build() + ) + + fun advanced(number: String, countryCode: String? = null, cnam: Boolean = false, + realTimeData: Boolean = false): AdvancedInsightResponse = + niClient.getAdvancedNumberInsight(AdvancedInsightRequest.builder().async(false) + .number(number).country(countryCode).cnam(cnam).realTimeData(realTimeData).build() + ) + + fun advancedAsync(number: String, callbackUrl: String, countryCode: String? = null, cnam: Boolean = false) { + niClient.getAdvancedNumberInsight( + AdvancedInsightRequest.builder().async(true) + .number(number).country(countryCode).cnam(cnam).callback(callbackUrl).build() + ) + } +} diff --git a/src/main/kotlin/com/vonage/client/kt/Redact.kt b/src/main/kotlin/com/vonage/client/kt/Redact.kt index d66d347..d7e5a4f 100644 --- a/src/main/kotlin/com/vonage/client/kt/Redact.kt +++ b/src/main/kotlin/com/vonage/client/kt/Redact.kt @@ -4,23 +4,18 @@ import com.vonage.client.redact.* class Redact(private val redactClient: RedactClient) { - fun redactSms(messageId: String, direction: RedactRequest.Type = RedactRequest.Type.OUTBOUND) { + fun redactSms(messageId: String, direction: RedactRequest.Type = RedactRequest.Type.OUTBOUND) = redactClient.redactTransaction(messageId, RedactRequest.Product.SMS, direction) - } - fun redactMessage(messageId: String, direction: RedactRequest.Type = RedactRequest.Type.OUTBOUND) { + fun redactMessage(messageId: String, direction: RedactRequest.Type = RedactRequest.Type.OUTBOUND) = redactClient.redactTransaction(messageId, RedactRequest.Product.MESSAGES, direction) - } - fun redactCall(callId: String, direction: RedactRequest.Type = RedactRequest.Type.OUTBOUND) { + fun redactCall(callId: String, direction: RedactRequest.Type = RedactRequest.Type.OUTBOUND) = redactClient.redactTransaction(callId, RedactRequest.Product.VOICE, direction) - } - fun redactInsight(requestId: String) { + fun redactInsight(requestId: String) = redactClient.redactTransaction(requestId, RedactRequest.Product.NUMBER_INSIGHTS) - } - fun redactVerification(requestId: String) { + fun redactVerification(requestId: String) = redactClient.redactTransaction(requestId, RedactRequest.Product.VERIFY) - } } diff --git a/src/main/kotlin/com/vonage/client/kt/Vonage.kt b/src/main/kotlin/com/vonage/client/kt/Vonage.kt index 1187f63..8074596 100644 --- a/src/main/kotlin/com/vonage/client/kt/Vonage.kt +++ b/src/main/kotlin/com/vonage/client/kt/Vonage.kt @@ -12,6 +12,7 @@ class Vonage(init: VonageClient.Builder.() -> Unit) { val conversion = Conversion(vonageClient.conversionClient) val redact = Redact(vonageClient.redactClient) val verifyLegacy = VerifyLegacy(vonageClient.verifyClient) + val numberInsight = NumberInsight(vonageClient.insightClient) } 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 92b49ce..6ad88bb 100644 --- a/src/test/kotlin/com/vonage/client/kt/AbstractTest.kt +++ b/src/test/kotlin/com/vonage/client/kt/AbstractTest.kt @@ -50,6 +50,8 @@ abstract class AbstractTest { protected val timestamp2Str = "2020-01-29T14:08:30.201Z" protected val timestamp2: Instant = Instant.parse(timestamp2Str) protected val currency = "EUR" + protected val exampleUrlBase = "https://example.com" + protected val callbackUrl = "$exampleUrlBase/callback" private val port = 8081 private val wiremock: WireMockServer = WireMockServer( diff --git a/src/test/kotlin/com/vonage/client/kt/NumberInsightTest.kt b/src/test/kotlin/com/vonage/client/kt/NumberInsightTest.kt new file mode 100644 index 0000000..a82210c --- /dev/null +++ b/src/test/kotlin/com/vonage/client/kt/NumberInsightTest.kt @@ -0,0 +1,262 @@ +package com.vonage.client.kt + +import com.vonage.client.insight.* +import com.vonage.client.insight.CarrierDetails.NetworkType +import com.vonage.client.insight.RoamingDetails.RoamingStatus +import java.math.BigDecimal +import kotlin.test.* + +class NumberInsightTest : AbstractTest() { + private val niClient = vonage.numberInsight + private val cnam = true + private val realTimeData = true + private val statusMessage = "Success" + private val nationalNumber = "07712 345689" + private val countryCode = "GB" + private val countryCodeIso3 = "GBR" + private val countryName = "United Kingdom" + private val countryPrefix = "44" + private val requestPrice = "0.035900000" + private val refundPrice = "0.01500000" + private val remainingBalance = "1.23456789" + private val reachable = Reachability.REACHABLE + private val ported = PortedStatus.ASSUMED_PORTED + private val callerType = CallerType.CONSUMER + private val firstName = "Max" + private val lastName = "Mustermann" + private val callerName = "$firstName $lastName" + private val originalNetworkCode = "12345" + private val originalName = "Acme Inc" + private val originalCountry = "CA" + private val originalNetworkType = NetworkType.PAGER + private val currentNetworkCode = networkCode + private val currentName = "Nexmo" + private val currentCountry = countryCode + private val currentNetworkType = NetworkType.LANDLINE_PREMIUM + private val roamingStatus = RoamingStatus.ROAMING + private val roamingCountryCode = "DE" + private val roamingNetworkCode = "26201" + private val roamingNetworkName = "Telekom Deutschland GmbH" + private val lookupOutcomeMessage = "Partial success - some fields populated" + private val validNumber = Validity.INFERRED_NOT_VALID + private val active = true + private val handsetStatus = "On" + + + private enum class InsightType { + BASIC, STANDARD, ADVANCED, ADVANCED_ASYNC + } + + private fun mockInsight(type: InsightType, optionalParams: Boolean = false) { + val expectedRequestParams = mutableMapOf("number" to toNumber) + if (optionalParams) { + expectedRequestParams["country"] = countryCode + if (type != InsightType.BASIC) { + expectedRequestParams["cnam"] = cnam + } + if (type == InsightType.ADVANCED) { + expectedRequestParams["real_time_data"] = realTimeData + } + } + + val expectedResponseParams = mutableMapOf( + "status" to 0, + "request_id" to testUuidStr, + "status_message" to statusMessage + ) + if (type != InsightType.ADVANCED_ASYNC) { + expectedResponseParams.putAll( + mapOf( + "international_format_number" to toNumber, + "national_format_number" to nationalNumber, + "country_code" to countryCode, + "country_code_iso3" to countryCodeIso3, + "country_name" to countryName, + "country_prefix" to countryPrefix + ) + ) + } + + if (type != InsightType.BASIC) { + expectedResponseParams.putAll(mapOf( + "request_price" to requestPrice, + "remaining_balance" to remainingBalance + )) + + if (type == InsightType.ADVANCED_ASYNC) { + expectedResponseParams.putAll(mapOf( + "number" to toNumber, + "error_text" to statusMessage + )) + } + else { + val callerIdentity = mapOf( + "caller_name" to callerName, + "last_name" to lastName, + "first_name" to firstName, + "caller_type" to callerType.name.lowercase() + ) + + expectedResponseParams.putAll( + mapOf( + "refund_price" to refundPrice, + "current_carrier" to mapOf( + "network_code" to currentNetworkCode, + "name" to currentName, + "country" to currentCountry, + "network_type" to currentNetworkType + ), + "ported" to ported.name.lowercase(), + "original_carrier" to mapOf( + "network_code" to originalNetworkCode, + "name" to originalName, + "country" to originalCountry, + "network_type" to originalNetworkType + ), + "caller_identity" to callerIdentity + ) + ) + + if (type == InsightType.STANDARD) { + expectedResponseParams.putAll(callerIdentity) + } + } + } + + if (type == InsightType.ADVANCED) { + expectedResponseParams.putAll(mapOf( + "roaming" to mapOf( + "status" to roamingStatus.name.lowercase(), + "roaming_country_code" to roamingCountryCode, + "roaming_network_code" to roamingNetworkCode, + "roaming_network_name" to roamingNetworkName + ), + "reachable" to reachable, + "lookup_outcome" to 1, + "lookup_outcome_message" to lookupOutcomeMessage, + "valid_number" to validNumber.name.lowercase(), + "real_time_data" to mapOf( + "active_status" to active, + "handset_status" to handsetStatus + ) + )) + } + + mockPostQueryParams( + expectedUrl = "/ni/${type.name.lowercase().replace('_', '/')}/json", + expectedRequestParams = expectedRequestParams, + expectedResponseParams = expectedResponseParams + ) + } + + private fun assertBasicResponse(response: BasicInsightResponse) { + assertNotNull(response) + assertEquals(InsightStatus.SUCCESS, response.status) + assertEquals(statusMessage, response.statusMessage) + assertEquals(testUuidStr, response.requestId) + assertEquals(toNumber, response.internationalFormatNumber) + assertEquals(nationalNumber, response.nationalFormatNumber) + assertEquals(countryCode, response.countryCode) + assertEquals(countryCodeIso3, response.countryCodeIso3) + assertEquals(countryName, response.countryName) + assertEquals(countryPrefix, response.countryPrefix) + } + + private fun assertStandardResponse(response: StandardInsightResponse) { + assertBasicResponse(response) + assertEquals(BigDecimal(requestPrice), response.requestPrice) + assertEquals(BigDecimal(refundPrice), response.refundPrice) + assertEquals(BigDecimal(remainingBalance), response.remainingBalance) + assertEquals(ported, response.ported) + if (response::class == StandardInsightResponse::class) { + assertEquals(firstName, response.firstName) + assertEquals(lastName, response.lastName) + assertEquals(callerName, response.callerName) + assertEquals(callerType, response.callerType) + } + val callerIdentity = response.callerIdentity + assertNotNull(callerIdentity) + assertEquals(firstName, callerIdentity.firstName) + assertEquals(lastName, callerIdentity.lastName) + assertEquals(callerName, callerIdentity.name) + assertEquals(callerType, callerIdentity.type) + val currentCarrier = response.currentCarrier + assertNotNull(currentCarrier) + assertEquals(currentName, currentCarrier.name) + assertEquals(currentCountry, currentCarrier.country) + assertEquals(currentNetworkType, currentCarrier.networkType) + assertEquals(currentNetworkCode, currentCarrier.networkCode) + val originalCarrier = response.originalCarrier + assertNotNull(originalCarrier) + assertEquals(originalName, originalCarrier.name) + assertEquals(originalCountry, originalCarrier.country) + assertEquals(originalNetworkType, originalCarrier.networkType) + assertEquals(originalNetworkCode, originalCarrier.networkCode) + } + + private fun assertAdvancedResponse(response: AdvancedInsightResponse) { + assertStandardResponse(response) + assertEquals(reachable, response.reachability) + assertEquals(LookupOutcome.PARTIAL_SUCCESS, response.lookupOutcome) + assertEquals(lookupOutcomeMessage, response.lookupOutcomeMessage) + assertEquals(validNumber, response.validNumber) + val rtd = response.realTimeData + assertNotNull(rtd) + assertEquals(active, rtd.activeStatus) + assertEquals(handsetStatus, rtd.handsetStatus) + val roaming = response.roaming + assertNotNull(roaming) + assertEquals(roamingStatus, roaming.status) + assertEquals(roamingCountryCode, roaming.roamingCountryCode) + assertEquals(roamingNetworkCode, roaming.roamingNetworkCode) + assertEquals(roamingNetworkName, roaming.roamingNetworkName) + } + + @Test + fun `basic insight required params`() { + mockInsight(InsightType.BASIC, false) + assertBasicResponse(niClient.basic(toNumber)) + } + + @Test + fun `basic insight all params`() { + mockInsight(InsightType.BASIC, true) + assertBasicResponse(niClient.basic(toNumber, countryCode)) + } + + @Test + fun `standard insight required params`() { + mockInsight(InsightType.STANDARD, false) + assertStandardResponse(niClient.standard(toNumber)) + } + + @Test + fun `standard insight all params`() { + mockInsight(InsightType.STANDARD, true) + assertStandardResponse(niClient.standard(toNumber, countryCode, cnam)) + } + + @Test + fun `advanced insight required params`() { + mockInsight(InsightType.ADVANCED, false) + assertAdvancedResponse(niClient.advanced(toNumber)) + } + + @Test + fun `advanced insight all params`() { + mockInsight(InsightType.ADVANCED, true) + assertAdvancedResponse(niClient.advanced(toNumber, countryCode, cnam, realTimeData)) + } + + @Test + fun `advanced async insight required params`() { + mockInsight(InsightType.ADVANCED_ASYNC, false) + niClient.advancedAsync(toNumber, callbackUrl) + } + + @Test + fun `advanced async insight all params`() { + mockInsight(InsightType.ADVANCED_ASYNC, true) + niClient.advancedAsync(toNumber, callbackUrl, countryCode, cnam) + } +} \ No newline at end of file