From 2c20793dc42325fbcd4361c35b90a10690f812cd Mon Sep 17 00:00:00 2001 From: Jean-Pierre Portier Date: Tue, 12 Dec 2023 16:13:35 +0100 Subject: [PATCH] feat (DEVEXP-211): Support Numbers webhooks --- .../numbers/CallbackConfigurationService.java | 2 +- .../sdk/domains/numbers/NumbersService.java | 8 ++ .../sdk/domains/numbers/WebHooksService.java | 28 +++++ .../numbers/adapters/NumbersService.java | 10 ++ .../numbers/adapters/WebHooksService.java | 28 +++++ ...erUpdateRequestParametersDtoConverter.java | 4 +- .../CallbackPayloadDtoConverter.java | 41 +++++++ .../ActiveNumberUpdateRequestParameters.java | 22 +++- .../models/webhooks/EventNotification.java | 108 ++++++++++++++++++ .../numbers/models/webhooks/EventStatus.java | 38 ++++++ .../numbers/models/webhooks/EventType.java | 66 +++++++++++ .../numbers/models/webhooks/ResourceType.java | 36 ++++++ .../numbers/models/webhooks/package-info.java | 6 + .../sdk/domains/numbers/package-info.java | 5 + .../numbers/adapters/WebhooksServiceTest.java | 52 +++++++++ .../CallbackPayloadDtoConverterTest.java | 70 ++++++++++++ .../models/dto/v1/CallbackPayloadDtoTest.java | 32 ++++++ sample-app/README.md | 16 +-- ...lication.java => WebHooksApplication.java} | 6 +- .../webhooks/numbers/NumbersController.java | 41 +++++++ .../webhooks/numbers/NumbersService.java | 21 ++++ .../src/main/resources/application.properties | 2 + .../numbers/v1/CallbackPayloadDto.json | 10 ++ 23 files changed, 633 insertions(+), 19 deletions(-) create mode 100644 client/src/main/com/sinch/sdk/domains/numbers/WebHooksService.java create mode 100644 client/src/main/com/sinch/sdk/domains/numbers/adapters/WebHooksService.java create mode 100644 client/src/main/com/sinch/sdk/domains/numbers/adapters/converters/CallbackPayloadDtoConverter.java create mode 100644 client/src/main/com/sinch/sdk/domains/numbers/models/webhooks/EventNotification.java create mode 100644 client/src/main/com/sinch/sdk/domains/numbers/models/webhooks/EventStatus.java create mode 100644 client/src/main/com/sinch/sdk/domains/numbers/models/webhooks/EventType.java create mode 100644 client/src/main/com/sinch/sdk/domains/numbers/models/webhooks/ResourceType.java create mode 100644 client/src/main/com/sinch/sdk/domains/numbers/models/webhooks/package-info.java create mode 100644 client/src/test/java/com/sinch/sdk/domains/numbers/adapters/WebhooksServiceTest.java create mode 100644 client/src/test/java/com/sinch/sdk/domains/numbers/adapters/converters/CallbackPayloadDtoConverterTest.java create mode 100644 openapi-contracts/src/test/java/com/sinch/sdk/domains/numbers/models/dto/v1/CallbackPayloadDtoTest.java rename sample-app/src/main/java/com/sinch/sample/webhooks/{VerificationApplication.java => WebHooksApplication.java} (74%) create mode 100644 sample-app/src/main/java/com/sinch/sample/webhooks/numbers/NumbersController.java create mode 100644 sample-app/src/main/java/com/sinch/sample/webhooks/numbers/NumbersService.java create mode 100644 sample-app/src/main/resources/application.properties create mode 100644 test-resources/src/test/resources/domains/numbers/v1/CallbackPayloadDto.json diff --git a/client/src/main/com/sinch/sdk/domains/numbers/CallbackConfigurationService.java b/client/src/main/com/sinch/sdk/domains/numbers/CallbackConfigurationService.java index 78aeab9d..df2796d5 100644 --- a/client/src/main/com/sinch/sdk/domains/numbers/CallbackConfigurationService.java +++ b/client/src/main/com/sinch/sdk/domains/numbers/CallbackConfigurationService.java @@ -8,7 +8,7 @@ * Callback Configuration Service * * @see https://developers.sinch.com/docs/numbers/api-reference/callbacks-numbers/tag/Callback-Configuration/ + * href="https://developers.sinch.com/docs/numbers/api-reference/numbers/tag/Callbacks/">https://developers.sinch.com/docs/numbers/api-reference/numbers/tag/Callbacks/ */ public interface CallbackConfigurationService { diff --git a/client/src/main/com/sinch/sdk/domains/numbers/NumbersService.java b/client/src/main/com/sinch/sdk/domains/numbers/NumbersService.java index b66dc84f..7789ccaa 100644 --- a/client/src/main/com/sinch/sdk/domains/numbers/NumbersService.java +++ b/client/src/main/com/sinch/sdk/domains/numbers/NumbersService.java @@ -40,4 +40,12 @@ public interface NumbersService { * @since 1.0 */ CallbackConfigurationService callback(); + + /** + * Webhooks helpers instance + * + * @return instance service related to webhooks helpers + * @since 1.0 + */ + WebHooksService webhooks(); } diff --git a/client/src/main/com/sinch/sdk/domains/numbers/WebHooksService.java b/client/src/main/com/sinch/sdk/domains/numbers/WebHooksService.java new file mode 100644 index 00000000..7c31e05f --- /dev/null +++ b/client/src/main/com/sinch/sdk/domains/numbers/WebHooksService.java @@ -0,0 +1,28 @@ +package com.sinch.sdk.domains.numbers; + +import com.sinch.sdk.core.exceptions.ApiMappingException; +import com.sinch.sdk.domains.numbers.models.webhooks.EventNotification; + +/** + * Webhooks service + * + *

Callback events are used to get notified about Numbers usage according to your configured + * callback URL + * + *

see https://developers.sinch.com/docs/numbers/api-reference/numbers/tag/Callbacks/#tag/Callbacks/operation/ImportedNumberService_EventsCallback + * + * @since 1.0 + */ +public interface WebHooksService { + + /** + * This function can be called to deserialize received payload onto callback. Function return Java + * class instance from un-serialized payload + * + * @param jsonPayload Received payload to be un-serialized + * @return The decoded event notification instance class + * @since 1.0 + */ + EventNotification unserializeEventNotification(String jsonPayload) throws ApiMappingException; +} diff --git a/client/src/main/com/sinch/sdk/domains/numbers/adapters/NumbersService.java b/client/src/main/com/sinch/sdk/domains/numbers/adapters/NumbersService.java index a5b83af2..44216ac2 100644 --- a/client/src/main/com/sinch/sdk/domains/numbers/adapters/NumbersService.java +++ b/client/src/main/com/sinch/sdk/domains/numbers/adapters/NumbersService.java @@ -18,6 +18,8 @@ public class NumbersService implements com.sinch.sdk.domains.numbers.NumbersServ private ActiveNumberService active; private AvailableRegionService regions; private CallbackConfigurationService callback; + private WebHooksService webhooks; + private final Map authManagers; public NumbersService(Configuration configuration, HttpClient httpClient) { @@ -57,4 +59,12 @@ public CallbackConfigurationService callback() { } return this.callback; } + + public WebHooksService webhooks() { + + if (null == this.webhooks) { + this.webhooks = new WebHooksService(); + } + return this.webhooks; + } } diff --git a/client/src/main/com/sinch/sdk/domains/numbers/adapters/WebHooksService.java b/client/src/main/com/sinch/sdk/domains/numbers/adapters/WebHooksService.java new file mode 100644 index 00000000..fcbfa31b --- /dev/null +++ b/client/src/main/com/sinch/sdk/domains/numbers/adapters/WebHooksService.java @@ -0,0 +1,28 @@ +package com.sinch.sdk.domains.numbers.adapters; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.sinch.sdk.core.exceptions.ApiMappingException; +import com.sinch.sdk.core.utils.databind.Mapper; +import com.sinch.sdk.domains.numbers.adapters.converters.CallbackPayloadDtoConverter; +import com.sinch.sdk.domains.numbers.models.dto.v1.CallbackPayloadDto; +import com.sinch.sdk.domains.numbers.models.webhooks.EventNotification; + +public class WebHooksService implements com.sinch.sdk.domains.numbers.WebHooksService { + + @Override + public EventNotification unserializeEventNotification(String jsonPayload) + throws ApiMappingException { + + try { + CallbackPayloadDto dto = + Mapper.getInstance().readValue(jsonPayload, CallbackPayloadDto.class); + if (null != dto) { + return CallbackPayloadDtoConverter.convert(dto); + } + throw new ApiMappingException(jsonPayload, null); + + } catch (JsonProcessingException e) { + throw new ApiMappingException(jsonPayload, e); + } + } +} diff --git a/client/src/main/com/sinch/sdk/domains/numbers/adapters/converters/ActiveNumberUpdateRequestParametersDtoConverter.java b/client/src/main/com/sinch/sdk/domains/numbers/adapters/converters/ActiveNumberUpdateRequestParametersDtoConverter.java index 8cca6abb..82e78725 100644 --- a/client/src/main/com/sinch/sdk/domains/numbers/adapters/converters/ActiveNumberUpdateRequestParametersDtoConverter.java +++ b/client/src/main/com/sinch/sdk/domains/numbers/adapters/converters/ActiveNumberUpdateRequestParametersDtoConverter.java @@ -15,9 +15,7 @@ public static ActiveNumberRequestDto convert(ActiveNumberUpdateRequestParameters .getVoiceConfiguration() .ifPresent( value -> dto.setVoiceConfiguration(VoiceConfigurationDtoConverter.convert(value))); - // TODO: OAS file do not yet contains callback field - // parameters.getCallback() - // .ifPresent(value -> dto.setCallback.convert(value))); + parameters.getCallback().ifPresent(dto::setCallbackUrl); return dto; } } diff --git a/client/src/main/com/sinch/sdk/domains/numbers/adapters/converters/CallbackPayloadDtoConverter.java b/client/src/main/com/sinch/sdk/domains/numbers/adapters/converters/CallbackPayloadDtoConverter.java new file mode 100644 index 00000000..388db4d5 --- /dev/null +++ b/client/src/main/com/sinch/sdk/domains/numbers/adapters/converters/CallbackPayloadDtoConverter.java @@ -0,0 +1,41 @@ +package com.sinch.sdk.domains.numbers.adapters.converters; + +import com.sinch.sdk.domains.numbers.models.SmsErrorCode; +import com.sinch.sdk.domains.numbers.models.dto.v1.CallbackPayloadDto; +import com.sinch.sdk.domains.numbers.models.webhooks.EventNotification; +import com.sinch.sdk.domains.numbers.models.webhooks.EventStatus; +import com.sinch.sdk.domains.numbers.models.webhooks.EventType; +import com.sinch.sdk.domains.numbers.models.webhooks.ResourceType; +import java.time.Instant; +import java.time.LocalDateTime; +import java.time.ZoneOffset; +import java.time.format.DateTimeParseException; + +public class CallbackPayloadDtoConverter { + + public static EventNotification convert(CallbackPayloadDto dto) { + + String eventId = dto.getEventId(); + // FIXME: Currently Numbers API return a string without timezone + // Workaround: + // 1. try to parse as ISO8601 (with timezone) + // 2. If failure: fallback to a local date time in UTC time zoe + Instant timestamp = null; + if (null != dto.getTimestamp()) { + try { + timestamp = Instant.parse(dto.getTimestamp()); + } catch (DateTimeParseException e) { + timestamp = LocalDateTime.parse(dto.getTimestamp()).toInstant(ZoneOffset.UTC); + } + } + String projectId = dto.getProjectId(); + String resourceId = dto.getResourceId(); + ResourceType resourceType = ResourceType.from(dto.getResourceType()); + EventType eventType = EventType.from(dto.getEventType()); + EventStatus status = EventStatus.from(dto.getStatus()); + SmsErrorCode failureCode = SmsErrorCode.from(dto.getFailureCode()); + + return new EventNotification( + eventId, timestamp, projectId, resourceId, resourceType, eventType, status, failureCode); + } +} diff --git a/client/src/main/com/sinch/sdk/domains/numbers/models/requests/ActiveNumberUpdateRequestParameters.java b/client/src/main/com/sinch/sdk/domains/numbers/models/requests/ActiveNumberUpdateRequestParameters.java index 8cc04bdb..956dc75c 100644 --- a/client/src/main/com/sinch/sdk/domains/numbers/models/requests/ActiveNumberUpdateRequestParameters.java +++ b/client/src/main/com/sinch/sdk/domains/numbers/models/requests/ActiveNumberUpdateRequestParameters.java @@ -16,11 +16,10 @@ public class ActiveNumberUpdateRequestParameters { private final String callback; /** - * @param displayName User supplied name for the phone number - * @param smsConfiguration The current SMS configuration for this number - * @param voiceConfiguration The current voice configuration for this number - * @param callback The callback URL to be called for a rented number's provisioning / - * deprovisioning operations + * @param displayName see {@link #getDisplayName() getDisplayName getter} + * @param smsConfiguration see {@link #getSmsConfiguration() getSmsConfiguration getter} + * @param voiceConfiguration see {@link #getVoiceConfiguration() getVoiceConfiguration getter} + * @param callback see {@link #getCallback() getCallback getter} */ public ActiveNumberUpdateRequestParameters( String displayName, @@ -33,18 +32,31 @@ public ActiveNumberUpdateRequestParameters( this.callback = callback; } + /** + * @return User supplied name for the phone number + */ public Optional getDisplayName() { return Optional.ofNullable(displayName); } + /** + * @return The current SMS configuration for this number + */ public Optional getSmsConfiguration() { return Optional.ofNullable(smsConfiguration); } + /** + * @return The current voice configuration for this number + */ public Optional getVoiceConfiguration() { return Optional.ofNullable(voiceConfiguration); } + /** + * @return The callback URL to be called for a rented number's provisioning / deprovisioning + * operations ({@link com.sinch.sdk.domains.numbers.WebHooksService see WebHooksService}) + */ public Optional getCallback() { return Optional.ofNullable(callback); } diff --git a/client/src/main/com/sinch/sdk/domains/numbers/models/webhooks/EventNotification.java b/client/src/main/com/sinch/sdk/domains/numbers/models/webhooks/EventNotification.java new file mode 100644 index 00000000..3b973281 --- /dev/null +++ b/client/src/main/com/sinch/sdk/domains/numbers/models/webhooks/EventNotification.java @@ -0,0 +1,108 @@ +package com.sinch.sdk.domains.numbers.models.webhooks; + +import com.sinch.sdk.domains.numbers.models.SmsErrorCode; +import java.time.Instant; + +/** A notification of an event sent to your configured callback URL. */ +public class EventNotification { + + private final String eventId; + private final Instant timestamp; + private final String projectId; + private final String resourceId; + private final ResourceType resourceType; + private final EventType eventType; + private final EventStatus status; + private final SmsErrorCode failureCode; + + /** + * @param eventId The ID of the event + * @param timestamp The date and time when the callback was created and added to the callbacks + * queue + * @param projectId The ID of the project to which the event belongs + * @param resourceId The unique identifier of the resource, depending on the resource type. For + * example, a phone number, a hosting order ID, or a brand ID. + * @param resourceType The type of the resource + * @param eventType The type of the event + * @param status The status of the event + * @param failureCode If the status is FAILED, a failure code will be provided. For numbers + * provisioning to SMS platform, there won't be any extra failureCode, as the result is + * binary. For campaign provisioning-related failures, refer to the list for the possible + * values. + */ + public EventNotification( + String eventId, + Instant timestamp, + String projectId, + String resourceId, + ResourceType resourceType, + EventType eventType, + EventStatus status, + SmsErrorCode failureCode) { + this.eventId = eventId; + this.timestamp = timestamp; + this.projectId = projectId; + this.resourceId = resourceId; + this.resourceType = resourceType; + this.eventType = eventType; + this.status = status; + this.failureCode = failureCode; + } + + public String getEventId() { + return eventId; + } + + public Instant getTimestamp() { + return timestamp; + } + + public String getProjectId() { + return projectId; + } + + public String getResourceId() { + return resourceId; + } + + public ResourceType getResourceType() { + return resourceType; + } + + public EventType getEventType() { + return eventType; + } + + public EventStatus getStatus() { + return status; + } + + public SmsErrorCode getFailureCode() { + return failureCode; + } + + @Override + public String toString() { + return "EventNotification{" + + "eventId='" + + eventId + + '\'' + + ", timestamp=" + + timestamp + + ", projectId='" + + projectId + + '\'' + + ", resourceId='" + + resourceId + + '\'' + + ", resourceType=" + + resourceType + + ", eventType=" + + eventType + + ", status=" + + status + + ", failureCode=" + + failureCode + + '}'; + } +} diff --git a/client/src/main/com/sinch/sdk/domains/numbers/models/webhooks/EventStatus.java b/client/src/main/com/sinch/sdk/domains/numbers/models/webhooks/EventStatus.java new file mode 100644 index 00000000..e0131e5b --- /dev/null +++ b/client/src/main/com/sinch/sdk/domains/numbers/models/webhooks/EventStatus.java @@ -0,0 +1,38 @@ +package com.sinch.sdk.domains.numbers.models.webhooks; + +import com.sinch.sdk.core.utils.EnumDynamic; +import com.sinch.sdk.core.utils.EnumSupportDynamic; +import java.util.Arrays; +import java.util.stream.Stream; + +public final class EventStatus extends EnumDynamic { + + /** + * @see https://developers.sinch.com/docs/numbers/api-reference/numbers/tag/Callbacks/#tag/Callbacks/operation/ImportedNumberService_EventsCallback!path=status&t=request/ + * @since 1.0 + */ + public static final EventStatus SUCCEEDED = new EventStatus("SUCCEEDED"); + + public static final EventStatus FAILED = new EventStatus("FAILED"); + + private static final EnumSupportDynamic ENUM_SUPPORT = + new EnumSupportDynamic<>( + EventStatus.class, EventStatus::new, Arrays.asList(SUCCEEDED, FAILED)); + + EventStatus(String value) { + super(value); + } + + public static Stream values() { + return ENUM_SUPPORT.values(); + } + + public static EventStatus from(String value) { + return ENUM_SUPPORT.from(value); + } + + public static String valueOf(EventStatus e) { + return ENUM_SUPPORT.valueOf(e); + } +} diff --git a/client/src/main/com/sinch/sdk/domains/numbers/models/webhooks/EventType.java b/client/src/main/com/sinch/sdk/domains/numbers/models/webhooks/EventType.java new file mode 100644 index 00000000..d73fb9d0 --- /dev/null +++ b/client/src/main/com/sinch/sdk/domains/numbers/models/webhooks/EventType.java @@ -0,0 +1,66 @@ +package com.sinch.sdk.domains.numbers.models.webhooks; + +import com.sinch.sdk.core.utils.EnumDynamic; +import com.sinch.sdk.core.utils.EnumSupportDynamic; +import java.util.Arrays; +import java.util.stream.Stream; + +/** + * @see https://developers.sinch.com/docs/numbers/api-reference/numbers/tag/Callbacks/#tag/Callbacks/operation/ImportedNumberService_EventsCallback!path=eventType&t=request + * @since 1.0 + */ +public final class EventType extends EnumDynamic { + + /** An event that occurs when a number is linked to a Service Plan ID. */ + public static final EventType PROVISIONING_TO_SMS_PLATFORM = + new EventType("PROVISIONING_TO_SMS_PLATFORM"); + + /** An event that occurs when a number is unlinked from a Service Plan ID */ + public static final EventType DEPROVISIONING_FROM_SMS_PLATFORM = + new EventType("DEPROVISIONING_FROM_SMS_PLATFORM"); + + /** An event that occurs when a number is linked to a Campaign */ + public static final EventType PROVISIONING_TO_CAMPAIGN = + new EventType("PROVISIONING_TO_CAMPAIGN"); + + /** An event that occurs when a number is unlinked from a Campaign */ + public static final EventType DEPROVISIONING_FROM_CAMPAIGN = + new EventType("DEPROVISIONING_FROM_CAMPAIGN"); + + /** An event that occurs when a number is enabled for Voice operations. */ + public static final EventType PROVISIONING_TO_VOICE_PLATFORM = + new EventType("PROVISIONING_TO_VOICE_PLATFORM"); + + /** An event that occurs when a number is disabled for Voice operations */ + public static final EventType DEPROVISIONING_TO_VOICE_PLATFORM = + new EventType("DEPROVISIONING_TO_VOICE_PLATFORM"); + + private static final EnumSupportDynamic ENUM_SUPPORT = + new EnumSupportDynamic<>( + EventType.class, + EventType::new, + Arrays.asList( + PROVISIONING_TO_SMS_PLATFORM, + DEPROVISIONING_FROM_SMS_PLATFORM, + PROVISIONING_TO_CAMPAIGN, + DEPROVISIONING_FROM_CAMPAIGN, + PROVISIONING_TO_VOICE_PLATFORM, + DEPROVISIONING_TO_VOICE_PLATFORM)); + + EventType(String value) { + super(value); + } + + public static Stream values() { + return ENUM_SUPPORT.values(); + } + + public static EventType from(String value) { + return ENUM_SUPPORT.from(value); + } + + public static String valueOf(EventType e) { + return ENUM_SUPPORT.valueOf(e); + } +} diff --git a/client/src/main/com/sinch/sdk/domains/numbers/models/webhooks/ResourceType.java b/client/src/main/com/sinch/sdk/domains/numbers/models/webhooks/ResourceType.java new file mode 100644 index 00000000..5b74448f --- /dev/null +++ b/client/src/main/com/sinch/sdk/domains/numbers/models/webhooks/ResourceType.java @@ -0,0 +1,36 @@ +package com.sinch.sdk.domains.numbers.models.webhooks; + +import com.sinch.sdk.core.utils.EnumDynamic; +import com.sinch.sdk.core.utils.EnumSupportDynamic; +import java.util.Arrays; +import java.util.stream.Stream; + +/** + * @see https://developers.sinch.com/docs/numbers/api-reference/numbers/tag/Callbacks/#tag/Callbacks/operation/ImportedNumberService_EventsCallback!path=resourceType&t=request + * @since 1.0 + */ +public final class ResourceType extends EnumDynamic { + + /** Numbers which are already active and updated with new campaign IDs or service plan IDs. */ + public static final ResourceType ACTIVE_NUMBER = new ResourceType("ACTIVE_NUMBER"); + + private static final EnumSupportDynamic ENUM_SUPPORT = + new EnumSupportDynamic<>(ResourceType.class, ResourceType::new, Arrays.asList(ACTIVE_NUMBER)); + + ResourceType(String value) { + super(value); + } + + public static Stream values() { + return ENUM_SUPPORT.values(); + } + + public static ResourceType from(String value) { + return ENUM_SUPPORT.from(value); + } + + public static String valueOf(ResourceType e) { + return ENUM_SUPPORT.valueOf(e); + } +} diff --git a/client/src/main/com/sinch/sdk/domains/numbers/models/webhooks/package-info.java b/client/src/main/com/sinch/sdk/domains/numbers/models/webhooks/package-info.java new file mode 100644 index 00000000..cb0c2db7 --- /dev/null +++ b/client/src/main/com/sinch/sdk/domains/numbers/models/webhooks/package-info.java @@ -0,0 +1,6 @@ +/** + * Numbers API webhooks (callback) related models + * + * @since 1.0 + */ +package com.sinch.sdk.domains.numbers.models.webhooks; diff --git a/client/src/main/com/sinch/sdk/domains/numbers/package-info.java b/client/src/main/com/sinch/sdk/domains/numbers/package-info.java index 9944337d..f1c82a0d 100644 --- a/client/src/main/com/sinch/sdk/domains/numbers/package-info.java +++ b/client/src/main/com/sinch/sdk/domains/numbers/package-info.java @@ -1,6 +1,11 @@ /** * Numbers API interface * + *

The Numbers API enables you to search for, view, and activate numbers. It's considered a + * precursor to other APIs in the Sinch product family. The numbers API can be used in tandem with + * any of our APIs that perform messaging or calling. Once you have activated your numbers, you can + * then use the various other APIs, such as SMS or Voice, to assign and use those numbers. + * * @since 1.0 */ package com.sinch.sdk.domains.numbers; diff --git a/client/src/test/java/com/sinch/sdk/domains/numbers/adapters/WebhooksServiceTest.java b/client/src/test/java/com/sinch/sdk/domains/numbers/adapters/WebhooksServiceTest.java new file mode 100644 index 00000000..d69bca54 --- /dev/null +++ b/client/src/test/java/com/sinch/sdk/domains/numbers/adapters/WebhooksServiceTest.java @@ -0,0 +1,52 @@ +package com.sinch.sdk.domains.numbers.adapters; + +import com.adelean.inject.resources.junit.jupiter.GivenTextResource; +import com.adelean.inject.resources.junit.jupiter.TestWithResources; +import com.sinch.sdk.BaseTest; +import com.sinch.sdk.SinchClient; +import com.sinch.sdk.core.exceptions.ApiException; +import com.sinch.sdk.domains.numbers.WebHooksService; +import com.sinch.sdk.domains.numbers.adapters.converters.CallbackPayloadDtoConverter; +import com.sinch.sdk.domains.numbers.models.dto.v1.CallbackPayloadDtoTest; +import com.sinch.sdk.domains.numbers.models.webhooks.EventNotification; +import com.sinch.sdk.models.Configuration; +import java.io.IOException; +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +@TestWithResources +public class WebhooksServiceTest extends BaseTest { + + @GivenTextResource("/domains/numbers/v1/CallbackPayloadDto.json") + String incomingCallbackPayloadDtoJsonString; + + WebHooksService webHooksService; + + @Test + void incomingEventNotification() throws ApiException { + + EventNotification response = + webHooksService.unserializeEventNotification(incomingCallbackPayloadDtoJsonString); + + Assertions.assertThat(response).isInstanceOf(EventNotification.class); + Assertions.assertThat(response) + .usingRecursiveComparison() + .isEqualTo( + CallbackPayloadDtoConverter.convert( + CallbackPayloadDtoTest.expectedCallbackPayloadDtoDto)); + } + + @BeforeEach + public void setUp() throws IOException { + + Configuration configuration = + Configuration.builder() + .setProjectId("unused") + .setKeyId("unused") + .setKeySecret("unused") + .build(); + + webHooksService = new SinchClient(configuration).numbers().webhooks(); + } +} diff --git a/client/src/test/java/com/sinch/sdk/domains/numbers/adapters/converters/CallbackPayloadDtoConverterTest.java b/client/src/test/java/com/sinch/sdk/domains/numbers/adapters/converters/CallbackPayloadDtoConverterTest.java new file mode 100644 index 00000000..8f4f6579 --- /dev/null +++ b/client/src/test/java/com/sinch/sdk/domains/numbers/adapters/converters/CallbackPayloadDtoConverterTest.java @@ -0,0 +1,70 @@ +package com.sinch.sdk.domains.numbers.adapters.converters; + +import com.sinch.sdk.domains.numbers.models.SmsErrorCode; +import com.sinch.sdk.domains.numbers.models.dto.v1.CallbackPayloadDto; +import com.sinch.sdk.domains.numbers.models.dto.v1.CallbackPayloadDtoTest; +import com.sinch.sdk.domains.numbers.models.webhooks.EventNotification; +import com.sinch.sdk.domains.numbers.models.webhooks.EventStatus; +import com.sinch.sdk.domains.numbers.models.webhooks.EventType; +import com.sinch.sdk.domains.numbers.models.webhooks.ResourceType; +import java.time.Instant; +import java.time.LocalDateTime; +import java.time.ZoneOffset; +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.Test; + +class CallbackPayloadDtoConverterTest { + + @Test + void convertCallbackPayloadDtoWithLocalDate() { + + EventNotification client = + new EventNotification( + "abcd1234efghijklmnop567890", + LocalDateTime.parse("2023-06-06T07:45:27.78789").toInstant(ZoneOffset.UTC), + "abcd12ef-ab12-ab12-bc34-abcdef123456", + "+12345612345", + ResourceType.ACTIVE_NUMBER, + EventType.PROVISIONING_TO_CAMPAIGN, + EventStatus.FAILED, + SmsErrorCode.CAMPAIGN_NOT_AVAILABLE); + + Assertions.assertThat( + CallbackPayloadDtoConverter.convert( + CallbackPayloadDtoTest.expectedCallbackPayloadDtoDto)) + .usingRecursiveComparison() + .isEqualTo(client); + } + + @Test + void convertCallbackPayloadDtoWithTimeZone() { + + // patch timestamp with a timezone compliant value + CallbackPayloadDto dto = + new CallbackPayloadDto() + .eventId(CallbackPayloadDtoTest.expectedCallbackPayloadDtoDto.getEventId()) + // adding a time zone + .timestamp(CallbackPayloadDtoTest.expectedCallbackPayloadDtoDto.getTimestamp() + "Z") + .projectId(CallbackPayloadDtoTest.expectedCallbackPayloadDtoDto.getProjectId()) + .resourceId(CallbackPayloadDtoTest.expectedCallbackPayloadDtoDto.getResourceId()) + .resourceType(CallbackPayloadDtoTest.expectedCallbackPayloadDtoDto.getResourceType()) + .eventType(CallbackPayloadDtoTest.expectedCallbackPayloadDtoDto.getEventType()) + .status(CallbackPayloadDtoTest.expectedCallbackPayloadDtoDto.getStatus()) + .failureCode(CallbackPayloadDtoTest.expectedCallbackPayloadDtoDto.getFailureCode()); + + EventNotification client = + new EventNotification( + "abcd1234efghijklmnop567890", + Instant.parse("2023-06-06T07:45:27.78789Z"), + "abcd12ef-ab12-ab12-bc34-abcdef123456", + "+12345612345", + ResourceType.ACTIVE_NUMBER, + EventType.PROVISIONING_TO_CAMPAIGN, + EventStatus.FAILED, + SmsErrorCode.CAMPAIGN_NOT_AVAILABLE); + + Assertions.assertThat(CallbackPayloadDtoConverter.convert(dto)) + .usingRecursiveComparison() + .isEqualTo(client); + } +} diff --git a/openapi-contracts/src/test/java/com/sinch/sdk/domains/numbers/models/dto/v1/CallbackPayloadDtoTest.java b/openapi-contracts/src/test/java/com/sinch/sdk/domains/numbers/models/dto/v1/CallbackPayloadDtoTest.java new file mode 100644 index 00000000..72ef298f --- /dev/null +++ b/openapi-contracts/src/test/java/com/sinch/sdk/domains/numbers/models/dto/v1/CallbackPayloadDtoTest.java @@ -0,0 +1,32 @@ +package com.sinch.sdk.domains.numbers.models.dto.v1; + +import com.adelean.inject.resources.junit.jupiter.GivenJsonResource; +import com.adelean.inject.resources.junit.jupiter.TestWithResources; +import com.sinch.sdk.BaseTest; +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.Test; + +@TestWithResources +public class CallbackPayloadDtoTest extends BaseTest { + + @GivenJsonResource("/domains/numbers/v1/CallbackPayloadDto.json") + CallbackPayloadDto loadedCallbackPayloadDto; + + public static CallbackPayloadDto expectedCallbackPayloadDtoDto = + new CallbackPayloadDto() + .eventId("abcd1234efghijklmnop567890") + .timestamp("2023-06-06T07:45:27.78789") + .projectId("abcd12ef-ab12-ab12-bc34-abcdef123456") + .resourceId("+12345612345") + .resourceType("ACTIVE_NUMBER") + .eventType("PROVISIONING_TO_CAMPAIGN") + .status("FAILED") + .failureCode("CAMPAIGN_NOT_AVAILABLE"); + + @Test + void deserializeCallbackPayload() { + Assertions.assertThat(loadedCallbackPayloadDto) + .usingRecursiveComparison() + .isEqualTo(expectedCallbackPayloadDtoDto); + } +} diff --git a/sample-app/README.md b/sample-app/README.md index bc82093b..cb78fe20 100644 --- a/sample-app/README.md +++ b/sample-app/README.md @@ -116,8 +116,8 @@ A full application chaining calls to Numbers service to onboard onto Java SDK an | | - GetByReference | [com.sinch.sample.verification.status.GetByReference](src/main/java/com/sinch/sample/verification/status/GetByReference.java) | | ### Dedicated webhooks feature samples -#### How to run webhooks samples -Webhooks samples are based onto dedicated SpringBoot applications. +#### How to run webhooks sample application +Webhooks samples are based onto a dedicated SpringBoot applications. By using service like `ngrok` and running locally the SpringBoot application you'll be able to use the local springboot application to response to callbacks defined within your dashboard 1. Install `ngrok` and launch it (see [ngrok site](https://ngrok.com/docs)) 2. Run the application: `mvn -f pom-webhooks.xml clean package spring-boot:run` @@ -130,10 +130,12 @@ By using service like `ngrok` and running locally the SpringBoot application you #### Verification WebHooks Require to set following parameters (by environment or config file): - `APPLICATION_API_KEY` --` APPLICATION_API_SECRET` +- `APPLICATION_API_SECRET` -Check your dashboard to retrieve Application Credentials values +Check your dashboard to retrieve Application credentials values + +| API | Package | Notes | +|--------------|------------------------------------------------------------------------------------------------|-------| +| Numbers | [com.sinch.sample.webhooks.numbers](src/main/java/com/sinch/sample/webhooks/numbers) | | +| Verification | [com.sinch.sample.webhooks.verification](src/main/java/com/sinch/sample/webhooks/verification) | | -| API | Sample | Class | Notes | -|--------------|------------------------|---------------------------------------------------------------------------------------------------------------------------|-------| -| Verification | Springboot application | [com.sinch.sample.webhooks.VerificationApplication](src/main/java/com/sinch/sample/webhooks/VerificationApplication.java) | | diff --git a/sample-app/src/main/java/com/sinch/sample/webhooks/VerificationApplication.java b/sample-app/src/main/java/com/sinch/sample/webhooks/WebHooksApplication.java similarity index 74% rename from sample-app/src/main/java/com/sinch/sample/webhooks/VerificationApplication.java rename to sample-app/src/main/java/com/sinch/sample/webhooks/WebHooksApplication.java index 00511611..f958377f 100644 --- a/sample-app/src/main/java/com/sinch/sample/webhooks/VerificationApplication.java +++ b/sample-app/src/main/java/com/sinch/sample/webhooks/WebHooksApplication.java @@ -9,12 +9,12 @@ import org.springframework.context.annotation.Bean; @SpringBootApplication -public class VerificationApplication { - private static final Logger LOGGER = Logger.getLogger(VerificationApplication.class.getName()); +public class WebHooksApplication { + private static final Logger LOGGER = Logger.getLogger(WebHooksApplication.class.getName()); public static void main(String[] args) { - SpringApplication.run(VerificationApplication.class, args); + SpringApplication.run(WebHooksApplication.class, args); } @Bean diff --git a/sample-app/src/main/java/com/sinch/sample/webhooks/numbers/NumbersController.java b/sample-app/src/main/java/com/sinch/sample/webhooks/numbers/NumbersController.java new file mode 100644 index 00000000..c9739a7a --- /dev/null +++ b/sample-app/src/main/java/com/sinch/sample/webhooks/numbers/NumbersController.java @@ -0,0 +1,41 @@ +package com.sinch.sample.webhooks.numbers; + +import com.sinch.sdk.SinchClient; +import java.util.Map; +import java.util.logging.Logger; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.MediaType; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestHeader; +import org.springframework.web.bind.annotation.RestController; + +@RestController +public class NumbersController { + + private final SinchClient sinchClient; + private final NumbersService service; + private static final Logger LOGGER = Logger.getLogger(NumbersController.class.getName()); + + @Autowired + public NumbersController(SinchClient sinchClient, NumbersService service) { + this.sinchClient = sinchClient; + this.service = service; + } + + @PostMapping( + value = "/NumbersEvent", + consumes = MediaType.APPLICATION_JSON_VALUE, + produces = MediaType.APPLICATION_JSON_VALUE) + public void NumbersEvent(@RequestHeader Map headers, @RequestBody String body) { + + LOGGER.finest("Received body:" + body); + LOGGER.finest("Received headers: " + headers); + + // decode the request payload + var event = sinchClient.numbers().webhooks().unserializeEventNotification(body); + + // let business layer process the request + service.numbersEvent(event); + } +} diff --git a/sample-app/src/main/java/com/sinch/sample/webhooks/numbers/NumbersService.java b/sample-app/src/main/java/com/sinch/sample/webhooks/numbers/NumbersService.java new file mode 100644 index 00000000..c85c91cd --- /dev/null +++ b/sample-app/src/main/java/com/sinch/sample/webhooks/numbers/NumbersService.java @@ -0,0 +1,21 @@ +package com.sinch.sample.webhooks.numbers; + +import com.sinch.sdk.domains.numbers.models.webhooks.EventNotification; +import java.util.logging.Logger; +import org.springframework.stereotype.Component; + +@Component +public class NumbersService { + + private static final Logger LOGGER = Logger.getLogger(NumbersService.class.getName()); + + /* @Autowired + public NumbersService(SinchClient sinchClient) { + + }*/ + + public void numbersEvent(EventNotification event) { + + LOGGER.info("Handle event :" + event); + } +} diff --git a/sample-app/src/main/resources/application.properties b/sample-app/src/main/resources/application.properties new file mode 100644 index 00000000..b82d8750 --- /dev/null +++ b/sample-app/src/main/resources/application.properties @@ -0,0 +1,2 @@ +# springboot sample related config file +logging.level.com = TRACE diff --git a/test-resources/src/test/resources/domains/numbers/v1/CallbackPayloadDto.json b/test-resources/src/test/resources/domains/numbers/v1/CallbackPayloadDto.json new file mode 100644 index 00000000..55ce3356 --- /dev/null +++ b/test-resources/src/test/resources/domains/numbers/v1/CallbackPayloadDto.json @@ -0,0 +1,10 @@ +{ + "eventId": "abcd1234efghijklmnop567890", + "timestamp": "2023-06-06T07:45:27.78789", + "projectId": "abcd12ef-ab12-ab12-bc34-abcdef123456", + "resourceId": "+12345612345", + "resourceType": "ACTIVE_NUMBER", + "eventType": "PROVISIONING_TO_CAMPAIGN", + "status": "FAILED", + "failureCode": "CAMPAIGN_NOT_AVAILABLE" +} \ No newline at end of file