From 2cffec39bb7a8a75b85164af7fd09eb68660811e Mon Sep 17 00:00:00 2001 From: Gideon Goldberg <1764158+gidsg@users.noreply.github.com> Date: Tue, 18 Jan 2022 10:46:52 +0000 Subject: [PATCH] PP-9001: add webhook message model for serialised webhook message body (#44) * add webhook message model for serialised webhook message body Addresses TODO to serialise response in expected format add missing field 'resource_type' serialised from SQS message and added as top level field to message Serialises/Deserialises timestamp from SQS message as Instant * use naming strategy to set field names * get date and event type from WebhookMessageEntity * add fields to WebhookEntity and use to serialise webhook message body * restore comment (as resource still needs more transformation) rename record to make more specific to Webhook message body. * assorted changes based on PR comments --- .../webhooks/message/WebhookMessageBody.java | 35 ++++++++++ .../message/WebhookMessageSender.java | 2 +- .../message/WebhookMessageService.java | 8 ++- .../dao/entity/WebhookMessageEntity.java | 22 ++++++ .../gov/pay/webhooks/queue/EventMessage.java | 3 +- .../pay/webhooks/queue/EventMessageDto.java | 7 +- .../gov/pay/webhooks/queue/InternalEvent.java | 7 +- .../webhooks/util/InstantDeserializer.java | 17 +++++ ...MicrosecondPrecisionInstantSerializer.java | 27 ++++++++ .../0004_create_table_webhook_messages.sql | 4 +- .../message/WebhookMessageBodyTest.java | 68 +++++++++++++++++++ .../message/WebhookMessageSenderTest.java | 26 +++++-- .../gov/pay/webhooks/queue/EventQueueIT.java | 8 ++- .../webhooks/webhook/WebhookServiceTest.java | 2 +- 14 files changed, 217 insertions(+), 19 deletions(-) create mode 100644 src/main/java/uk/gov/pay/webhooks/message/WebhookMessageBody.java create mode 100644 src/main/java/uk/gov/pay/webhooks/util/InstantDeserializer.java create mode 100644 src/main/java/uk/gov/pay/webhooks/util/MicrosecondPrecisionInstantSerializer.java create mode 100644 src/test/java/uk/gov/pay/webhooks/message/WebhookMessageBodyTest.java diff --git a/src/main/java/uk/gov/pay/webhooks/message/WebhookMessageBody.java b/src/main/java/uk/gov/pay/webhooks/message/WebhookMessageBody.java new file mode 100644 index 00000000..7172501a --- /dev/null +++ b/src/main/java/uk/gov/pay/webhooks/message/WebhookMessageBody.java @@ -0,0 +1,35 @@ +package uk.gov.pay.webhooks.message; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.PropertyNamingStrategies; +import com.fasterxml.jackson.databind.annotation.JsonNaming; +import com.fasterxml.jackson.databind.annotation.JsonSerialize; +import uk.gov.pay.webhooks.eventtype.EventTypeName; +import uk.gov.pay.webhooks.message.dao.entity.WebhookMessageEntity; +import uk.gov.service.payments.commons.api.json.ApiResponseInstantSerializer; + +import java.time.Instant; + + +@JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class) +public record WebhookMessageBody(String id, + @JsonSerialize(using = ApiResponseInstantSerializer.class) Instant createdDate, + String resourceId, + Integer apiVersion, + String resourceType, + EventTypeName eventTypeName, + JsonNode resource) { + + public static final int API_VERSION = 1; + + public static WebhookMessageBody from(WebhookMessageEntity webhookMessage) { + return new WebhookMessageBody(webhookMessage.getExternalId(), + webhookMessage.getEventDate().toInstant(), + webhookMessage.getResourceExternalId(), + API_VERSION, + webhookMessage.getResourceType(), + webhookMessage.getEventType().getName(), + webhookMessage.getResource() + ); + } +} diff --git a/src/main/java/uk/gov/pay/webhooks/message/WebhookMessageSender.java b/src/main/java/uk/gov/pay/webhooks/message/WebhookMessageSender.java index ba7146eb..a1a843d4 100644 --- a/src/main/java/uk/gov/pay/webhooks/message/WebhookMessageSender.java +++ b/src/main/java/uk/gov/pay/webhooks/message/WebhookMessageSender.java @@ -32,7 +32,7 @@ public WebhookMessageSender(HttpClient httpClient, public HttpResponse sendWebhookMessage(WebhookMessageEntity webhookMessage) throws IOException, InterruptedException, InvalidKeyException { URI uri = URI.create(webhookMessage.getWebhookEntity().getCallbackUrl()); - String body = objectMapper.writeValueAsString(webhookMessage.getResource()); + String body = objectMapper.writeValueAsString(WebhookMessageBody.from(webhookMessage)); String signingKey = webhookMessage.getWebhookEntity().getSigningKey(); String signature = webhookMessageSignatureGenerator.generate(body, signingKey); diff --git a/src/main/java/uk/gov/pay/webhooks/message/WebhookMessageService.java b/src/main/java/uk/gov/pay/webhooks/message/WebhookMessageService.java index 99c213c9..92b84ff2 100644 --- a/src/main/java/uk/gov/pay/webhooks/message/WebhookMessageService.java +++ b/src/main/java/uk/gov/pay/webhooks/message/WebhookMessageService.java @@ -69,15 +69,17 @@ public void handleInternalEvent(InternalEvent event) throws IOException, Interru } private WebhookMessageEntity buildWebhookMessage(WebhookEntity webhook, InternalEvent event, LedgerTransaction ledgerTransaction) { - JsonNode resource = objectMapper.valueToTree(ledgerTransaction); // will probably need some more transformation + JsonNode resource = objectMapper.valueToTree(ledgerTransaction); var webhookMessageEntity = new WebhookMessageEntity(); webhookMessageEntity.setExternalId(idGenerator.newExternalId()); webhookMessageEntity.setCreatedDate(Date.from(instantSource.instant())); webhookMessageEntity.setWebhookEntity(webhook); - webhookMessageEntity.setEventDate(Date.from(event.eventDate().toInstant())); + webhookMessageEntity.setEventDate(Date.from(event.eventDate())); webhookMessageEntity.setEventType(eventTypeDao.findByName(EventMapper.getWebhookEventNameFor(event.eventType())).orElseThrow(IllegalArgumentException::new)); - webhookMessageEntity.setResource(resource); + webhookMessageEntity.setResource(objectMapper.valueToTree(resource)); // will probably need some more transformation + webhookMessageEntity.setResourceExternalId(event.resourceExternalId()); + webhookMessageEntity.setResourceType(event.resourceType()); return webhookMessageEntity; } diff --git a/src/main/java/uk/gov/pay/webhooks/message/dao/entity/WebhookMessageEntity.java b/src/main/java/uk/gov/pay/webhooks/message/dao/entity/WebhookMessageEntity.java index 06ca0678..a40e4bb8 100644 --- a/src/main/java/uk/gov/pay/webhooks/message/dao/entity/WebhookMessageEntity.java +++ b/src/main/java/uk/gov/pay/webhooks/message/dao/entity/WebhookMessageEntity.java @@ -71,6 +71,28 @@ public WebhookMessageEntity() {} @Column(name = "external_id") private String externalId; + public String getResourceExternalId() { + return resourceExternalId; + } + + public void setResourceExternalId(String resourceExternalId) { + this.resourceExternalId = resourceExternalId; + } + + @Column(name = "resource_external_id") + private String resourceExternalId; + + public String getResourceType() { + return resourceType; + } + + public void setResourceType(String resourceType) { + this.resourceType = resourceType; + } + + @Column(name = "resource_type") + private String resourceType; + @Column(name = "created_date") private Date createdDate; diff --git a/src/main/java/uk/gov/pay/webhooks/queue/EventMessage.java b/src/main/java/uk/gov/pay/webhooks/queue/EventMessage.java index b8643e7e..7de9e41b 100644 --- a/src/main/java/uk/gov/pay/webhooks/queue/EventMessage.java +++ b/src/main/java/uk/gov/pay/webhooks/queue/EventMessage.java @@ -16,7 +16,8 @@ public InternalEvent toInternalEvent() { eventMessageDto.resourceExternalId(), eventMessageDto.parentResourceExternalId(), eventMessageDto.eventData(), - eventMessageDto.eventDate() + eventMessageDto.eventDate(), + eventMessageDto.resourceType() ); } diff --git a/src/main/java/uk/gov/pay/webhooks/queue/EventMessageDto.java b/src/main/java/uk/gov/pay/webhooks/queue/EventMessageDto.java index 8f7d9b2b..ebcc42b4 100644 --- a/src/main/java/uk/gov/pay/webhooks/queue/EventMessageDto.java +++ b/src/main/java/uk/gov/pay/webhooks/queue/EventMessageDto.java @@ -6,17 +6,18 @@ import com.fasterxml.jackson.databind.PropertyNamingStrategies; import com.fasterxml.jackson.databind.annotation.JsonDeserialize; import com.fasterxml.jackson.databind.annotation.JsonNaming; -import uk.gov.service.payments.commons.api.json.MicrosecondPrecisionDateTimeDeserializer; +import uk.gov.pay.webhooks.util.InstantDeserializer; -import java.time.ZonedDateTime; +import java.time.Instant; @JsonIgnoreProperties(ignoreUnknown = true) @JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class) public record EventMessageDto(@JsonProperty("service_id") String serviceId, Boolean live, - @JsonProperty("event_date") @JsonDeserialize(using = MicrosecondPrecisionDateTimeDeserializer.class) ZonedDateTime eventDate, + @JsonProperty("event_date") @JsonDeserialize(using = InstantDeserializer.class) Instant eventDate, @JsonProperty("resource_external_id") String resourceExternalId, @JsonProperty("parent_resource_external_id") String parentResourceExternalId, @JsonProperty("event_type") String eventType, + @JsonProperty("resource_type") String resourceType, @JsonProperty("event_data") JsonNode eventData ) {} diff --git a/src/main/java/uk/gov/pay/webhooks/queue/InternalEvent.java b/src/main/java/uk/gov/pay/webhooks/queue/InternalEvent.java index de0f0448..fce5722f 100644 --- a/src/main/java/uk/gov/pay/webhooks/queue/InternalEvent.java +++ b/src/main/java/uk/gov/pay/webhooks/queue/InternalEvent.java @@ -2,9 +2,9 @@ import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.annotation.JsonSerialize; -import uk.gov.service.payments.commons.api.json.MicrosecondPrecisionDateTimeSerializer; +import uk.gov.pay.webhooks.util.MicrosecondPrecisionInstantSerializer; -import java.time.ZonedDateTime; +import java.time.Instant; public record InternalEvent( String eventType, @@ -13,5 +13,6 @@ public record InternalEvent( String resourceExternalId, String parentResourceExternalId, JsonNode eventData, - @JsonSerialize(using = MicrosecondPrecisionDateTimeSerializer.class) ZonedDateTime eventDate + @JsonSerialize(using = MicrosecondPrecisionInstantSerializer.class) Instant eventDate, + String resourceType ) {} diff --git a/src/main/java/uk/gov/pay/webhooks/util/InstantDeserializer.java b/src/main/java/uk/gov/pay/webhooks/util/InstantDeserializer.java new file mode 100644 index 00000000..a9657139 --- /dev/null +++ b/src/main/java/uk/gov/pay/webhooks/util/InstantDeserializer.java @@ -0,0 +1,17 @@ +package uk.gov.pay.webhooks.util; + +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.JsonDeserializer; + +import java.io.IOException; +import java.time.Instant; + +public class InstantDeserializer extends JsonDeserializer { + + @Override + public Instant deserialize(JsonParser p, DeserializationContext ctxt) throws IOException { + return Instant.parse(p.getText()); + } +} + diff --git a/src/main/java/uk/gov/pay/webhooks/util/MicrosecondPrecisionInstantSerializer.java b/src/main/java/uk/gov/pay/webhooks/util/MicrosecondPrecisionInstantSerializer.java new file mode 100644 index 00000000..d18d0b9a --- /dev/null +++ b/src/main/java/uk/gov/pay/webhooks/util/MicrosecondPrecisionInstantSerializer.java @@ -0,0 +1,27 @@ +package uk.gov.pay.webhooks.util; + +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.databind.JsonSerializer; +import com.fasterxml.jackson.databind.SerializerProvider; +import com.fasterxml.jackson.databind.ser.std.StdSerializer; + +import java.io.IOException; +import java.time.Instant; +import java.time.ZoneOffset; +import java.time.format.DateTimeFormatter; +import java.time.format.DateTimeFormatterBuilder; +import java.util.Locale; + +public class MicrosecondPrecisionInstantSerializer extends JsonSerializer { + + public static final DateTimeFormatter MICROSECOND_FORMATTER = + new DateTimeFormatterBuilder() + .appendInstant(6) + .toFormatter(Locale.ENGLISH); + + @Override + public void serialize(Instant value, JsonGenerator gen, SerializerProvider provider) throws IOException { + gen.writeString(MICROSECOND_FORMATTER.format(value)); + } +} + diff --git a/src/main/resources/migrations/0004_create_table_webhook_messages.sql b/src/main/resources/migrations/0004_create_table_webhook_messages.sql index 3dc447a9..e5b028cb 100644 --- a/src/main/resources/migrations/0004_create_table_webhook_messages.sql +++ b/src/main/resources/migrations/0004_create_table_webhook_messages.sql @@ -9,7 +9,9 @@ CREATE table webhook_messages ( webhook_id INT NOT NULL, event_date TIMESTAMP WITH TIME ZONE NOT NULL, event_type INT NOT NULL, - resource JSONB NOT NULL + resource JSONB NOT NULL, + resource_external_id VARCHAR(26), + resource_type VARCHAR ); ALTER TABLE webhook_messages ADD CONSTRAINT fk_webhook_message_webhook_id FOREIGN KEY (webhook_id) REFERENCES webhooks (id); diff --git a/src/test/java/uk/gov/pay/webhooks/message/WebhookMessageBodyTest.java b/src/test/java/uk/gov/pay/webhooks/message/WebhookMessageBodyTest.java new file mode 100644 index 00000000..c3487181 --- /dev/null +++ b/src/test/java/uk/gov/pay/webhooks/message/WebhookMessageBodyTest.java @@ -0,0 +1,68 @@ +package uk.gov.pay.webhooks.message; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import io.dropwizard.testing.junit5.DropwizardExtensionsSupport; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import uk.gov.pay.webhooks.eventtype.EventTypeName; +import uk.gov.pay.webhooks.eventtype.dao.EventTypeEntity; +import uk.gov.pay.webhooks.message.dao.entity.WebhookMessageEntity; + +import java.time.Instant; +import java.time.InstantSource; +import java.util.Date; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.equalTo; + +@ExtendWith(DropwizardExtensionsSupport.class) +class WebhookMessageBodyTest { + + private ObjectMapper objectMapper; + private InstantSource instantSource; + + @BeforeEach + public void setUp() { + instantSource = InstantSource.fixed(Instant.parse("2019-10-01T08:25:24.00Z")); + objectMapper = new ObjectMapper(); + } + + @Test + void serialisesWebhookMessageBody() throws JsonProcessingException { + String resource = """ + { + "json": "and", + "the": "argonauts" + } + """; + var webhookMessageEntity = new WebhookMessageEntity(); + webhookMessageEntity.setExternalId("externalId"); + webhookMessageEntity.setEventDate(Date.from(instantSource.instant())); + EventTypeEntity eventTypeEntity = new EventTypeEntity(EventTypeName.CARD_PAYMENT_CAPTURED); + webhookMessageEntity.setEventType(eventTypeEntity); + webhookMessageEntity.setResourceType("payment"); + webhookMessageEntity.setResourceExternalId("resource-external-id"); + webhookMessageEntity.setResource(objectMapper.readTree(resource)); + + var body = WebhookMessageBody.from(webhookMessageEntity);; + var expectedJson = """ + { + "id": "externalId", + "created_date": "2019-10-01T08:25:24.000Z", + "resource_id": "resource-external-id", + "api_version": 1, + "resource_type": "payment", + "event_type_name": "card_payment_captured", + "resource": { + "json": "and", + "the": "argonauts" + } + } + """; + + assertThat(objectMapper.readTree(expectedJson), equalTo(objectMapper.valueToTree(body))); + + } +} diff --git a/src/test/java/uk/gov/pay/webhooks/message/WebhookMessageSenderTest.java b/src/test/java/uk/gov/pay/webhooks/message/WebhookMessageSenderTest.java index 0c953f34..7d2b3b20 100644 --- a/src/test/java/uk/gov/pay/webhooks/message/WebhookMessageSenderTest.java +++ b/src/test/java/uk/gov/pay/webhooks/message/WebhookMessageSenderTest.java @@ -9,6 +9,8 @@ import org.mockito.ArgumentCaptor; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; +import uk.gov.pay.webhooks.eventtype.EventTypeName; +import uk.gov.pay.webhooks.eventtype.dao.EventTypeEntity; import uk.gov.pay.webhooks.message.dao.entity.WebhookMessageEntity; import uk.gov.pay.webhooks.webhook.dao.entity.WebhookEntity; @@ -18,6 +20,8 @@ import java.net.http.HttpRequest; import java.net.http.HttpResponse; import java.security.InvalidKeyException; +import java.sql.Date; +import java.time.Instant; import java.util.Optional; import static org.hamcrest.MatcherAssert.assertThat; @@ -33,10 +37,18 @@ class WebhookMessageSenderTest { private static final String PAYLOAD = """ - { + { + "id": "externalId", + "created_date": "2022-01-12T17:31:06.809Z", + "resource_id": "foo", + "api_version": 1, + "resource_type": null, + "event_type_name": "card_payment_captured", + "resource": { "json": "and", "the": "argonauts" } + } """; private static final URI CALLBACK_URL = URI.create("http://www.callbackurl.test/webhook-handler"); @@ -69,8 +81,14 @@ void setUp() throws JsonProcessingException, InvalidKeyException { webhookMessageEntity = new WebhookMessageEntity(); webhookMessageEntity.setWebhookEntity(webhookEntity); webhookMessageEntity.setResource(jsonPayload); + webhookMessageEntity.setEventDate(Date.from(Instant.parse("2019-10-01T08:25:24.00Z"))); + EventTypeEntity eventTypeEntity = new EventTypeEntity(EventTypeName.CARD_PAYMENT_CAPTURED); + webhookMessageEntity.setEventType(eventTypeEntity); + webhookMessageEntity.setResourceExternalId("foo"); + webhookMessageEntity.setExternalId("externalId"); + webhookMessageEntity.setResourceType("payment"); - given(mockWebhookMessageSignatureGenerator.generate(jsonPayload.toString(), SIGNING_KEY)).willReturn(SIGNATURE); + given(mockWebhookMessageSignatureGenerator.generate(objectMapper.writeValueAsString(WebhookMessageBody.from(webhookMessageEntity)), SIGNING_KEY)).willReturn(SIGNATURE); webhookMessageSender = new WebhookMessageSender(mockHttpClient, objectMapper, mockWebhookMessageSignatureGenerator); } @@ -104,8 +122,8 @@ void propagatesInterruptedException() throws IOException, InterruptedException { } @Test - void propagatesInvalidKeyException() throws InvalidKeyException { - given(mockWebhookMessageSignatureGenerator.generate(jsonPayload.toString(), SIGNING_KEY)).willThrow(InvalidKeyException.class); + void propagatesInvalidKeyException() throws InvalidKeyException, JsonProcessingException { + given(mockWebhookMessageSignatureGenerator.generate(objectMapper.writeValueAsString(WebhookMessageBody.from(webhookMessageEntity)), SIGNING_KEY)).willThrow(InvalidKeyException.class); assertThrows(InvalidKeyException.class, () -> webhookMessageSender.sendWebhookMessage(webhookMessageEntity)); } diff --git a/src/test/java/uk/gov/pay/webhooks/queue/EventQueueIT.java b/src/test/java/uk/gov/pay/webhooks/queue/EventQueueIT.java index ae112ede..add564cc 100644 --- a/src/test/java/uk/gov/pay/webhooks/queue/EventQueueIT.java +++ b/src/test/java/uk/gov/pay/webhooks/queue/EventQueueIT.java @@ -1,6 +1,7 @@ package uk.gov.pay.webhooks.queue; import com.amazonaws.services.sqs.AmazonSQS; +import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -16,6 +17,8 @@ import java.util.List; +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.MatcherAssert.assertThat; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; @@ -49,7 +52,7 @@ public void shouldReceiveMessageFromTheQueue() throws QueueException { } @Test - public void shouldConvertValidMessageFromQueueToEventMessage() throws QueueException { + public void shouldConvertValidMessageFromQueueToEventMessage() throws QueueException, JsonProcessingException { var sqsMessage = """ { "Type" : "Notification", @@ -58,7 +61,7 @@ public void shouldConvertValidMessageFromQueueToEventMessage() throws QueueExcep "TopicArn" : "card-payment-events-topic", "Timestamp" : "2021-12-16T18:52:27.068Z", "SignatureVersion" : "1", - "Signature" : "qYWtXrARHosfc5wgMSJLKofdn2q+QJIEs+XfpIBCFp94VHOujQBjVRGpQkr8OVF07lONWakkuKvdzcRIwRExtuCNDVyJinJGJgQEAFKpKmnJ9TfBsraTI5cmWAxnHv/wyYkK948QNe3DkdmascRU6ldKSIaZJ2k46cJfjtIkspMZ2tOuen39bd5pASXrGLyi9eq8HsZNY1IhD5OqDBtl+eLZ5DtJNdbroYF6+lg0f9K40fZIiPhFBtJPQoNZjCIwEb3VCG4o+OF7ga1gEOAj0se5BUXiMYWWT2zfusFBoxoecA/nout33sFS4NxGAsoKEkQlqxChSK1Lr/XTvZ77SA==", + "Signature" : "some-signature", "SigningCertURL" : "https://sns.eu-west-1.amazonaws.com/SimpleNotificationService-signing-cert-uuid.pem", "UnsubscribeURL" : "https://sns.eu-west-1.amazonaws.com/?Action=Unsubscribe&SubscriptionArn=arn:a-aws-arn" } @@ -80,5 +83,6 @@ public void shouldConvertValidMessageFromQueueToEventMessage() throws QueueExcep List result = eventQueue.retrieveEvents(); assertFalse(result.isEmpty()); + assertThat(result.get(0).eventMessageDto().resourceType(), is("payment")); } } diff --git a/src/test/java/uk/gov/pay/webhooks/webhook/WebhookServiceTest.java b/src/test/java/uk/gov/pay/webhooks/webhook/WebhookServiceTest.java index eab27a43..dd703137 100644 --- a/src/test/java/uk/gov/pay/webhooks/webhook/WebhookServiceTest.java +++ b/src/test/java/uk/gov/pay/webhooks/webhook/WebhookServiceTest.java @@ -107,7 +107,7 @@ public void shouldReturnSubscribedWebhooksForGivenEventTypeServiceIdLivenessTupl var webhookNotSubscribedToAnyEvents = new WebhookEntity(); when(webhookDao.list(live, serviceId)) .thenReturn(List.of(webhookSubscribedToCaptureEvent, webhookNotSubscribedToAnyEvents)); - var event = new InternalEvent("CAPTURE_CONFIRMED", serviceId, live, "resource_id", null, null, ZonedDateTime.now()); + var event = new InternalEvent("CAPTURE_CONFIRMED", serviceId, live, "resource_id", null, null, instantSource.instant(), "PAYMENT"); var subscribedWebhooks = webhookService.getWebhooksSubscribedToEvent(event);