Ft/td 138/gh action (#1)

* TD-138: github actions, webhook-dispatcher sending

* Fix table name

* Added workflow, removed build_utils

* Added tests

* Removed event_id

* Added test

* Optimized imports, fix codacy warnings

* fix codacy warnings

* fix codestyle

* fix codestyle

* fix codacy

* Review fixes

* Fix readme

* Fix readme(2)

* Fix readme(3)

* Feedback fix

* Fix tests

* Fix tests(1)

* Fix tests(2)

* Feedback fixes

Co-authored-by: Inal Arsanukaev <inalarsanukaev@192.168.1.5>
Co-authored-by: Inal Arsanukaev <inalarsanukaev@192.168.1.4>
This commit is contained in:
Inal Arsanukaev 2022-03-15 11:27:43 +03:00 committed by GitHub
parent a7b866b090
commit aa94a879bb
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
133 changed files with 1364 additions and 3762 deletions

0
.gitmodules vendored
View File

16
Jenkinsfile vendored
View File

@ -1,16 +0,0 @@
#!groovy
build('hooker', 'java-maven') {
checkoutRepo()
loadBuildUtils()
def javaServicePipeline
runStage('load JavaService pipeline') {
javaServicePipeline = load("build_utils/jenkins_lib/pipeJavaService.groovy")
}
def serviceName = env.REPO_NAME
def mvnArgs = '-DjvmArgs="-Xmx256m"'
def useJava11 = true
javaServicePipeline(serviceName, useJava11, mvnArgs)
}

View File

@ -2,32 +2,16 @@
Сервис вебхуков
[![Build Status](http://ci.rbkmoney.com/buildStatus/icon?job=rbkmoney_private/hooker/master)](http://ci.rbkmoney.com/job/rbkmoney_private/job/hooker/job/master/)
Сервис предоставляет интерфейс для CAPI для создания, удаления вебхуков, установки/редактирования опций вебхука (таких как url, public/private keys - они генерируются сервисом, типы событий).
Сервис получает события инвойсинга и кастомеров из кафки и далее отправляет в webhook-dispatcher
1. Сервис предоставляет интерфейс для CAPI для создания, удаления вебхуков, установки/редактирования опций вебхука (
таких как url, public/private keys - они генерируются сервисом, типы событий).
2. Сервис поллит bustermaze на появление событий, по которым должны отправляться сообщения мерчантам
Интерфейс для capi доступен по пути /hook
Интерфейс для CAPI доступен по пути /hook
Для более подробного ознакомления со структурой объектов можно воспользоваться ссылкой на сам
объект [webhooker.thrift][1]
Подробная информация по протоколу отправки сообщений мерчантам описана в [swagger-спецификации][2]
#### Пример запроса к мерчанту
[1]: https://github.com/valitydev/damsel/blob/master/proto/webhooker.thrift
```
curl -v -X POST
-H "Content-Type: application/json; charset=utf-8"
-H "Content-Signature: alg=RS256; digest=QydtAwZ8jmmrX1wP87vAJIXVBiRZe3Zo5xRYnzMQIsDxxsVn26XGMBeD4op8_9LmXIAirYWd53gfX638ZR83D2pTSCXkNpzHyqiyzhPlH2asaC9qiiOOWaQ-kHk9xFlOcMa1Qtt0BaVst-tbGDsFhgjl6tvGuN4JHRq5khzl_iIJ_ZQniEuzGOYReWn8wCLmaspX0MPyACMTo8HDwyihOHB1_RQBliBFYyvw523xvSQ6WxWYjYhFsjglIg1wdKUMyaiScw0kmKm53OdxDAnjl4MPQmtryuANjbklN8_EatOrQAqGwRUp1ayR_3WMlayhpxaEHlG1sAHQaaO3ulI35g=="
-d '{"eventID":27,"occuredAt":"2017-05-16T13:49:34.935099Z","topic":"InvoicesTopic","eventType":"PaymentCaptured","invoice":{"id":"qXMiygTqb2","shopID":1,"createdAt":"2017-05-16T13:49:32.753723Z","status":"unpaid","reason":null,"dueDate":"2017-05-16T13:59:32Z","amount":100000,"currency":"RUB","metadata":{"type":"application/json","data":"eyJpbnZvaWNlX2R1bW15X2NvbnRleHQiOiJ0ZXN0X3ZhbHVlIn0="},"product":"test_product","description":"test_invoice_description"},"payment":{"id":"1","createdAt":"2017-05-16T13:49:33.182195Z","status":"captured","error":null,"amount":100000,"currency":"RUB","paymentToolToken":"5Gz2nhE1eleFGBAcGe9SrA","paymentSession":"2nTYVgk6h85O7vIVV9j4pA","contactInfo":{"email":"bla@bla.ru","phoneNumber":null},"ip":"10.100.2.1","fingerprint":"test fingerprint"}}
' https://{host}:{port}/{path}
```
Мерчант, используя публичный ключ и имея в распоряжении тело запроса, подпись, алгоритм подписи и хэширования, может
произвести проверку подписи
[1]: https://github.com/rbkmoney/damsel/blob/master/proto/webhooker.thrift
[2]: https://github.com/rbkmoney/swag-webhook-events/blob/master/spec/swagger.yaml
[2]: https://github.com/valitydev/swag-webhook-events

View File

@ -1,8 +0,0 @@
postgres:
image: postgres:9.6
environment:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
POSTGRES_DB: hook
ports:
- '5432:5432'

29
pom.xml
View File

@ -4,7 +4,7 @@
<modelVersion>4.0.0</modelVersion>
<artifactId>hooker</artifactId>
<version>2.0.60-SNAPSHOT</version>
<version>1.0.0-SNAPSHOT</version>
<packaging>jar</packaging>
<name>hooker</name>
@ -86,10 +86,6 @@
<groupId>dev.vality</groupId>
<artifactId>mamsel</artifactId>
</dependency>
<dependency>
<groupId>dev.vality.logback</groupId>
<artifactId>nop-rolling</artifactId>
</dependency>
<dependency>
<groupId>dev.vality</groupId>
<artifactId>shared-resources</artifactId>
@ -101,15 +97,6 @@
<dependency>
<groupId>dev.vality</groupId>
<artifactId>kafka-common-lib</artifactId>
<version>0.0.1</version>
</dependency>
<dependency>
<groupId>dev.vality</groupId>
<artifactId>custom-metrics-spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>dev.vality</groupId>
<artifactId>custom-actuator-endpoints</artifactId>
</dependency>
<dependency>
<groupId>dev.vality.woody</groupId>
@ -143,7 +130,11 @@
<artifactId>swag-webhook-events</artifactId>
<version>1.1-67063f3-client</version>
</dependency>
<dependency>
<groupId>dev.vality</groupId>
<artifactId>webhook-dispatcher-proto</artifactId>
<version>1.12-49f714e</version>
</dependency>
<!--Thirdparty libs-->
<dependency>
<groupId>org.slf4j</groupId>
@ -219,6 +210,12 @@
<artifactId>mockwebserver</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>dev.vality</groupId>
<artifactId>testcontainers-annotations</artifactId>
<version>1.4.0</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>postgresql</artifactId>
@ -286,7 +283,7 @@
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-remote-resources-plugin</artifactId>
<version>1.5</version>
<version>1.7.0</version>
<dependencies>
<dependency>
<groupId>org.apache.maven.shared</groupId>

View File

@ -6,47 +6,12 @@ import com.fasterxml.jackson.databind.SerializationFeature;
import com.fasterxml.jackson.datatype.jdk8.Jdk8Module;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import com.fasterxml.jackson.module.paramnames.ParameterNamesModule;
import dev.vality.hooker.dao.CustomerDao;
import dev.vality.hooker.dao.HookDao;
import dev.vality.hooker.dao.InvoicingMessageDao;
import dev.vality.hooker.dao.impl.CustomerQueueDao;
import dev.vality.hooker.dao.impl.CustomerTaskDao;
import dev.vality.hooker.dao.impl.InvoicingQueueDao;
import dev.vality.hooker.dao.impl.InvoicingTaskDao;
import dev.vality.hooker.model.CustomerMessage;
import dev.vality.hooker.model.CustomerQueue;
import dev.vality.hooker.model.InvoicingMessage;
import dev.vality.hooker.model.InvoicingQueue;
import dev.vality.hooker.retry.RetryPoliciesService;
import dev.vality.hooker.scheduler.MessageScheduler;
import dev.vality.hooker.scheduler.MessageSender;
import dev.vality.hooker.service.CustomerEventService;
import dev.vality.hooker.service.FaultDetectorService;
import dev.vality.hooker.service.InvoicingEventService;
import dev.vality.hooker.service.MessageProcessor;
import dev.vality.hooker.service.crypt.Signer;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler;
import org.springframework.transaction.support.TransactionTemplate;
@Configuration
public class AppConfig {
@Value("${message.scheduler.invoicing.threadPoolSize}")
private int invoicingThreadPoolSize;
@Value("${message.scheduler.customer.threadPoolSize}")
private int customerThreadPoolSize;
@Value("${message.scheduler.delay}")
private int delayMillis;
@Value("${merchant.callback.timeout}")
private int timeout;
@Bean
public ObjectMapper objectMapper() {
return new ObjectMapper()
@ -56,50 +21,4 @@ public class AppConfig {
.setSerializationInclusion(JsonInclude.Include.NON_EMPTY)
.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
}
@Bean
public MessageSender<InvoicingMessage, InvoicingQueue> invoicngMessageSender(
Signer signer, InvoicingEventService eventService,
ObjectMapper objectMapper, FaultDetectorService faultDetector) {
return new MessageSender<>(invoicingThreadPoolSize, timeout, signer, eventService, objectMapper, faultDetector);
}
@Bean
public MessageSender<CustomerMessage, CustomerQueue> customerMessageSender(
Signer signer, CustomerEventService eventService,
ObjectMapper objectMapper, FaultDetectorService faultDetector) {
return new MessageSender<>(customerThreadPoolSize, timeout, signer, eventService, objectMapper, faultDetector);
}
@Bean
public MessageProcessor<InvoicingMessage, InvoicingQueue> invoicingMessageProcessor(
HookDao hookDao, InvoicingTaskDao taskDao, InvoicingQueueDao queueDao, InvoicingMessageDao messageDao,
RetryPoliciesService retryPoliciesService, TransactionTemplate transactionTemplate,
FaultDetectorService faultDetector, MessageSender<InvoicingMessage, InvoicingQueue> invoicngMessageSender) {
return new MessageProcessor<>(hookDao, taskDao, queueDao, messageDao, retryPoliciesService, transactionTemplate,
faultDetector, invoicngMessageSender);
}
@Bean
public MessageProcessor<CustomerMessage, CustomerQueue> customerMessageProcessor(
HookDao hookDao, CustomerTaskDao taskDao, CustomerQueueDao queueDao, CustomerDao messageDao,
RetryPoliciesService retryPoliciesService, TransactionTemplate transactionTemplate,
FaultDetectorService faultDetector, MessageSender<CustomerMessage, CustomerQueue> customerMessageSender) {
return new MessageProcessor<>(hookDao, taskDao, queueDao, messageDao, retryPoliciesService, transactionTemplate,
faultDetector, customerMessageSender);
}
@Bean
public MessageScheduler<InvoicingMessage, InvoicingQueue> invoicingMessageScheduler(
MessageProcessor<InvoicingMessage, InvoicingQueue> invoicingMessageProcessor,
ThreadPoolTaskScheduler taskScheduler) {
return new MessageScheduler<>(invoicingThreadPoolSize, delayMillis, invoicingMessageProcessor, taskScheduler);
}
@Bean
public MessageScheduler<CustomerMessage, CustomerQueue> cuustomerMessageScheduler(
MessageProcessor<CustomerMessage, CustomerQueue> customerMessageProcessor,
ThreadPoolTaskScheduler taskScheduler) {
return new MessageScheduler<>(customerThreadPoolSize, delayMillis, customerMessageProcessor, taskScheduler);
}
}

View File

@ -1,21 +0,0 @@
package dev.vality.hooker.configuration;
import com.github.benmanes.caffeine.cache.Cache;
import com.github.benmanes.caffeine.cache.Caffeine;
import dev.vality.hooker.model.InvoicingMessage;
import dev.vality.hooker.model.InvoicingMessageKey;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class CacheConfig {
@Bean
public Cache<InvoicingMessageKey, InvoicingMessage> invoiceDataCache(
@Value("${cache.invoice.size}") int cacheSize) {
return Caffeine.newBuilder()
.maximumSize(cacheSize)
.build();
}
}

View File

@ -1,24 +0,0 @@
package dev.vality.hooker.configuration;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.TransactionDefinition;
import org.springframework.transaction.support.TransactionTemplate;
@Configuration
public class DaoConfiguration {
@Bean
@Primary
public TransactionTemplate transactionTemplate(PlatformTransactionManager transactionManager,
@Value("${db.jdbc.tr_timeout}") int transactionTimeout) {
TransactionTemplate transactionTemplate = new TransactionTemplate(transactionManager);
transactionTemplate.setIsolationLevel(TransactionDefinition.ISOLATION_READ_COMMITTED);
transactionTemplate.setTimeout(transactionTimeout);
return transactionTemplate;
}
}

View File

@ -1,24 +0,0 @@
package dev.vality.hooker.configuration;
import dev.vality.damsel.fault_detector.FaultDetectorSrv;
import dev.vality.woody.thrift.impl.http.THSpawnClientBuilder;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.Resource;
import java.io.IOException;
@Configuration
public class FaultDetectorConfig {
@Bean
public FaultDetectorSrv.Iface faultDetectorClient(@Value("${service.fault-detector.url}") Resource resource,
@Value("${service.fault-detector.networkTimeout}")
int networkTimeout) throws IOException {
return new THSpawnClientBuilder()
.withNetworkTimeout(networkTimeout)
.withAddress(resource.getURI()).build(FaultDetectorSrv.Iface.class);
}
}

View File

@ -4,26 +4,29 @@ import dev.vality.damsel.payment_processing.EventPayload;
import dev.vality.hooker.configuration.properties.KafkaSslProperties;
import dev.vality.hooker.serde.SinkEventDeserializer;
import dev.vality.kafka.common.exception.handler.SeekToCurrentWithSleepBatchErrorHandler;
import dev.vality.kafka.common.serialization.ThriftSerializer;
import dev.vality.machinegun.eventsink.MachineEvent;
import dev.vality.sink.common.parser.impl.MachineEventParser;
import dev.vality.sink.common.parser.impl.PaymentEventPayloadMachineEventParser;
import dev.vality.sink.common.serialization.BinaryDeserializer;
import dev.vality.sink.common.serialization.impl.PaymentEventPayloadDeserializer;
import dev.vality.webhook.dispatcher.WebhookMessage;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.kafka.clients.CommonClientConfigs;
import org.apache.kafka.clients.consumer.ConsumerConfig;
import org.apache.kafka.clients.producer.ProducerConfig;
import org.apache.kafka.common.config.SslConfigs;
import org.apache.kafka.common.security.auth.SecurityProtocol;
import org.apache.kafka.common.serialization.StringDeserializer;
import org.apache.kafka.common.serialization.StringSerializer;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.kafka.config.ConcurrentKafkaListenerContainerFactory;
import org.springframework.kafka.config.KafkaListenerContainerFactory;
import org.springframework.kafka.core.ConsumerFactory;
import org.springframework.kafka.core.DefaultKafkaConsumerFactory;
import org.springframework.kafka.core.*;
import org.springframework.kafka.listener.BatchErrorHandler;
import org.springframework.kafka.listener.ConcurrentMessageListenerContainer;
import org.springframework.kafka.listener.ContainerProperties;
@ -141,4 +144,17 @@ public class KafkaConfig {
return new PaymentEventPayloadMachineEventParser(paymentEventPayloadDeserializer);
}
@Bean
public KafkaTemplate<String, WebhookMessage> kafkaTemplate() {
return new KafkaTemplate<>(producerFactory());
}
private ProducerFactory<String, WebhookMessage> producerFactory() {
Map<String, Object> config = new HashMap<>();
config.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, bootstrapServers);
config.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, StringSerializer.class);
config.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, ThriftSerializer.class);
configureSsl(config);
return new DefaultKafkaProducerFactory<>(config);
}
}

View File

@ -0,0 +1,24 @@
package dev.vality.hooker.configuration;
import dev.vality.hooker.dao.rowmapper.WebhookModelRowMapper;
import dev.vality.hooker.model.CustomerMessage;
import dev.vality.hooker.model.InvoicingMessage;
import dev.vality.hooker.model.WebhookMessageModel;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.jdbc.core.RowMapper;
@Configuration
public class RowMapperConfig {
@Bean
public RowMapper<WebhookMessageModel<CustomerMessage>> customerWebhookRowMapper(
RowMapper<CustomerMessage> customerRowMapper) {
return new WebhookModelRowMapper<>(customerRowMapper);
}
@Bean
public RowMapper<WebhookMessageModel<InvoicingMessage>> invoicingWebhookRowMapper(
RowMapper<InvoicingMessage> invoicingRowMapper) {
return new WebhookModelRowMapper<>(invoicingRowMapper);
}
}

View File

@ -0,0 +1,31 @@
package dev.vality.hooker.configuration;
import dev.vality.hooker.converter.WebhookMessageBuilder;
import dev.vality.hooker.dao.MessageDao;
import dev.vality.hooker.model.CustomerMessage;
import dev.vality.hooker.model.InvoicingMessage;
import dev.vality.hooker.service.EventService;
import dev.vality.hooker.service.MessageService;
import dev.vality.hooker.service.WebhookKafkaProducerService;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class ServiceConfig {
@Bean
public MessageService<InvoicingMessage> invoicingService(MessageDao<InvoicingMessage> messageDao,
EventService<InvoicingMessage> eventService,
WebhookMessageBuilder webhookMessageBuilder,
WebhookKafkaProducerService webhookKafkaProducerService) {
return new MessageService<>(messageDao, eventService, webhookMessageBuilder, webhookKafkaProducerService);
}
@Bean
public MessageService<CustomerMessage> customerService(MessageDao<CustomerMessage> messageDao,
EventService<CustomerMessage> eventService,
WebhookMessageBuilder webhookMessageBuilder,
WebhookKafkaProducerService webhookKafkaProducerService) {
return new MessageService<>(messageDao, eventService, webhookMessageBuilder, webhookKafkaProducerService);
}
}

View File

@ -0,0 +1,41 @@
package dev.vality.hooker.converter;
import com.fasterxml.jackson.databind.ObjectMapper;
import dev.vality.geck.common.util.TypeUtil;
import dev.vality.hooker.model.WebhookMessageModel;
import dev.vality.hooker.service.AdditionalHeadersGenerator;
import dev.vality.hooker.service.crypt.Signer;
import dev.vality.swag_webhook_events.model.Event;
import dev.vality.webhook.dispatcher.WebhookMessage;
import lombok.RequiredArgsConstructor;
import lombok.SneakyThrows;
import org.apache.http.entity.ContentType;
import org.springframework.stereotype.Component;
import java.nio.charset.StandardCharsets;
import java.time.Instant;
@Component
@RequiredArgsConstructor
public class WebhookMessageBuilder {
private final AdditionalHeadersGenerator additionalHeadersGenerator;
private final ObjectMapper objectMapper;
private final Signer signer;
@SneakyThrows
public WebhookMessage build(WebhookMessageModel webhookMessageModel, Event event, String sourceId, Long parentId) {
final String messageJson = objectMapper.writeValueAsString(event);
final String signature = signer.sign(messageJson, webhookMessageModel.getPrivateKey());
return new WebhookMessage()
.setWebhookId(webhookMessageModel.getHookId())
.setSourceId(sourceId)
.setEventId(event.getEventID())
.setParentEventId(parentId)
.setCreatedAt(TypeUtil.temporalToString(Instant.now()))
.setUrl(webhookMessageModel.getUrl())
.setContentType(ContentType.APPLICATION_JSON.getMimeType())
.setAdditionalHeaders(additionalHeadersGenerator.generate(signature))
.setRequestBody(messageJson.getBytes(StandardCharsets.UTF_8));
}
}

View File

@ -1,64 +0,0 @@
package dev.vality.hooker.dao;
import dev.vality.hooker.exception.DaoException;
import dev.vality.hooker.model.Task;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.core.NestedRuntimeException;
import org.springframework.jdbc.core.RowMapper;
import org.springframework.jdbc.core.namedparam.MapSqlParameterSource;
import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
/**
* Created by jeckep on 17.04.17.
*/
@Slf4j
@RequiredArgsConstructor
public abstract class AbstractTaskDao implements TaskDao {
public static RowMapper<Task> taskRowMapper = (rs, i) ->
new Task(rs.getLong("message_id"), rs.getLong("queue_id"));
protected final NamedParameterJdbcTemplate jdbcTemplate;
protected abstract String getMessageTopic();
@Override
public void remove(long queueId, long messageId) throws DaoException {
final String sql =
"DELETE FROM hook.scheduled_task " +
" where queue_id=:queue_id " +
" and message_id=:message_id " +
" and message_type=CAST(:message_type as hook.message_topic)";
try {
jdbcTemplate.update(sql, new MapSqlParameterSource("queue_id", queueId)
.addValue("message_id", messageId)
.addValue("message_type", getMessageTopic()));
log.debug("Task with queueId {} messageId {} removed from hook.scheduled_task", queueId, messageId);
} catch (NestedRuntimeException e) {
log.warn("Fail to delete task by queue_id {} and message_id {}", queueId, messageId, e);
throw new DaoException(e);
}
}
@Override
public void removeAll(long queueId) throws DaoException {
final String sql = "DELETE FROM hook.scheduled_task " +
"where queue_id=:queue_id and message_type=CAST(:message_type as hook.message_topic)";
try {
jdbcTemplate.update(sql,
new MapSqlParameterSource("queue_id", queueId).addValue("message_type", getMessageTopic()));
} catch (NestedRuntimeException e) {
log.warn("Fail to delete tasks for hook:" + queueId, e);
throw new DaoException(e);
}
}
//should preserve order by message id
protected Map<Long, List<Task>> splitByQueue(List<Task> orderedByMessageIdTasks) {
return orderedByMessageIdTasks.stream().collect(Collectors.groupingBy(Task::getQueueId));
}
}

View File

@ -1,9 +0,0 @@
package dev.vality.hooker.dao;
import dev.vality.hooker.exception.DaoException;
import java.util.List;
public interface IdsGeneratorDao {
List<Long> get(int size) throws DaoException;
}

View File

@ -1,10 +1,14 @@
package dev.vality.hooker.dao;
import dev.vality.hooker.exception.DaoException;
import dev.vality.hooker.model.Message;
import dev.vality.hooker.model.WebhookMessageModel;
import java.util.Collection;
import java.util.List;
public interface MessageDao<M> {
List<M> getBy(Collection<Long> messageIds) throws DaoException;
public interface MessageDao<M extends Message> {
Long save(M message);
List<WebhookMessageModel<M>> getWebhookModels(Long messageId);
Long getParentId(Long hookId, String sourceId, Long messageId);
}

View File

@ -1,15 +0,0 @@
package dev.vality.hooker.dao;
import dev.vality.hooker.model.Queue;
import java.util.Collection;
import java.util.List;
/**
* Created by inalarsanukaev on 14.11.17.
*/
public interface QueueDao<Q extends Queue> {
List<Q> getWithPolicies(Collection<Long> ids);
void disable(long id);
}

View File

@ -1,11 +0,0 @@
package dev.vality.hooker.dao;
import dev.vality.hooker.exception.DaoException;
import dev.vality.hooker.retry.impl.simple.SimpleRetryPolicyRecord;
/**
* Created by jeckep on 17.04.17.
*/
public interface SimpleRetryPolicyDao {
void update(SimpleRetryPolicyRecord record) throws DaoException;
}

View File

@ -1,19 +0,0 @@
package dev.vality.hooker.dao;
import dev.vality.hooker.exception.DaoException;
import dev.vality.hooker.model.Task;
import java.util.List;
import java.util.Map;
/**
* Created by jeckep on 13.04.17.
*/
public interface TaskDao {
void remove(long queueId, long messageId);
void removeAll(long queueId) throws DaoException;
Map<Long, List<Task>> getScheduled() throws DaoException;
}

View File

@ -1,15 +1,8 @@
package dev.vality.hooker.dao;
import dev.vality.hooker.model.EventType;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.ToString;
import lombok.*;
/**
* Created by inalarsanukaev on 18.04.17.
*/
@Data
@ToString
@AllArgsConstructor

View File

@ -5,9 +5,6 @@ import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.Setter;
/**
* Created by inalarsanukaev on 18.04.17.
*/
@AllArgsConstructor
@Getter
@Setter

View File

@ -1,12 +1,15 @@
package dev.vality.hooker.dao.impl;
import dev.vality.hooker.dao.CustomerDao;
import dev.vality.hooker.dao.rowmapper.CustomerRowMapper;
import dev.vality.hooker.exception.DaoException;
import dev.vality.hooker.model.CustomerMessage;
import dev.vality.hooker.model.CustomerMessageEnum;
import dev.vality.hooker.model.EventType;
import dev.vality.hooker.model.WebhookMessageModel;
import dev.vality.swag_webhook_events.model.Event;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.core.NestedRuntimeException;
import org.springframework.dao.EmptyResultDataAccessException;
import org.springframework.jdbc.core.RowMapper;
@ -14,88 +17,60 @@ import org.springframework.jdbc.core.namedparam.MapSqlParameterSource;
import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate;
import org.springframework.jdbc.support.GeneratedKeyHolder;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
/**
* Created by inalarsanukaev on 13.10.17.
*/
@Slf4j
@Component
@RequiredArgsConstructor
public class CustomerDaoImpl implements CustomerDao {
public static final String ID = "id";
public static final String EVENT_ID = "event_id";
public static final String TYPE = "type";
public static final String OCCURED_AT = "occured_at";
public static final String SEQUENCE_ID = "sequence_id";
public static final String CHANGE_ID = "change_id";
public static final String PARTY_ID = "party_id";
public static final String EVENT_TYPE = "event_type";
public static final String CUSTOMER_ID = "customer_id";
public static final String CUSTOMER_SHOP_ID = "customer_shop_id";
public static final String BINDING_ID = "binding_id";
private static RowMapper<CustomerMessage> messageRowMapper = (rs, i) -> {
CustomerMessage message = new CustomerMessage();
message.setId(rs.getLong(ID));
message.setEventId(rs.getLong(EVENT_ID));
message.setPartyId(rs.getString(PARTY_ID));
message.setEventTime(rs.getString(OCCURED_AT));
message.setSequenceId(rs.getLong(SEQUENCE_ID));
message.setChangeId(rs.getInt(CHANGE_ID));
message.setType(CustomerMessageEnum.lookup(rs.getString(TYPE)));
message.setEventType(EventType.valueOf(rs.getString(EVENT_TYPE)));
message.setCustomerId(rs.getString(CUSTOMER_ID));
message.setShopId(rs.getString(CUSTOMER_SHOP_ID));
message.setBindingId(rs.getString(BINDING_ID));
return message;
};
private final RowMapper<CustomerMessage> customerMessageRowMapper;
private final RowMapper<WebhookMessageModel<CustomerMessage>> webhookMessageModelRowMapper;
private final NamedParameterJdbcTemplate jdbcTemplate;
@Value("${parent.not.exist.id:-1}")
private Long parentNotExistId;
@Override
public CustomerMessage getAny(String customerId, CustomerMessageEnum type) throws DaoException {
CustomerMessage result = null;
final String sql = "SELECT * FROM hook.customer_message " +
"WHERE customer_id =:customer_id AND type=CAST(:type as hook.customer_message_type) " +
"ORDER BY id DESC LIMIT 1";
MapSqlParameterSource params =
new MapSqlParameterSource(CUSTOMER_ID, customerId).addValue(TYPE, type.getValue());
MapSqlParameterSource params = new MapSqlParameterSource()
.addValue(CustomerRowMapper.CUSTOMER_ID, customerId)
.addValue(CustomerRowMapper.TYPE, type.getValue());
try {
result = jdbcTemplate.queryForObject(sql, params, messageRowMapper);
return jdbcTemplate.queryForObject(sql, params, customerMessageRowMapper);
} catch (EmptyResultDataAccessException e) {
log.warn("CustomerMessage with customerId {}, type {} not exist!", customerId, type);
return null;
} catch (NestedRuntimeException e) {
throw new DaoException("CustomerMessageDaoImpl.getAny error with customerId " + customerId, e);
}
return result;
}
@Transactional
public Long create(CustomerMessage message) throws DaoException {
@Override
public Long save(CustomerMessage message) throws DaoException {
final String sql = "INSERT INTO hook.customer_message " +
"(event_id, occured_at, sequence_id, change_id, type, " +
"(occured_at, sequence_id, change_id, type, " +
"party_id, event_type, customer_id, customer_shop_id, binding_id) " +
"VALUES " +
"(:event_id, :occured_at, :sequence_id, :change_id, CAST(:type as hook.customer_message_type), " +
"(:occured_at, :sequence_id, :change_id, CAST(:type as hook.customer_message_type), " +
":party_id, CAST(:event_type as hook.eventtype), :customer_id, :customer_shop_id, :binding_id) " +
"ON CONFLICT (customer_id, sequence_id, change_id) DO NOTHING " +
"RETURNING id";
MapSqlParameterSource params = new MapSqlParameterSource()
.addValue(EVENT_ID, message.getEventId())
.addValue(OCCURED_AT, message.getEventTime())
.addValue(SEQUENCE_ID, message.getSequenceId())
.addValue(CHANGE_ID, message.getChangeId())
.addValue(TYPE, message.getType().getValue())
.addValue(PARTY_ID, message.getPartyId())
.addValue(EVENT_TYPE, message.getEventType().name())
.addValue(CUSTOMER_ID, message.getCustomerId())
.addValue(CUSTOMER_SHOP_ID, message.getShopId())
.addValue(BINDING_ID, message.getBindingId());
.addValue(CustomerRowMapper.OCCURED_AT, message.getEventTime())
.addValue(CustomerRowMapper.SEQUENCE_ID, message.getSequenceId())
.addValue(CustomerRowMapper.CHANGE_ID, message.getChangeId())
.addValue(CustomerRowMapper.TYPE, message.getType().getValue())
.addValue(CustomerRowMapper.PARTY_ID, message.getPartyId())
.addValue(CustomerRowMapper.EVENT_TYPE, message.getEventType().name())
.addValue(CustomerRowMapper.CUSTOMER_ID, message.getSourceId())
.addValue(CustomerRowMapper.CUSTOMER_SHOP_ID, message.getShopId())
.addValue(CustomerRowMapper.BINDING_ID, message.getBindingId());
try {
GeneratedKeyHolder keyHolder = new GeneratedKeyHolder();
jdbcTemplate.update(sql, params, keyHolder);
@ -106,32 +81,46 @@ public class CustomerDaoImpl implements CustomerDao {
}
return null;
} catch (NestedRuntimeException e) {
throw new DaoException("Couldn't create customerMessage with customerId " + message.getCustomerId(), e);
}
}
public Long getMaxEventId() {
final String sql = "select max(event_id) from hook.customer_message ";
try {
return jdbcTemplate.queryForObject(sql, new MapSqlParameterSource(), Long.class);
} catch (EmptyResultDataAccessException e) {
return null;
throw new DaoException("Couldn't create customerMessage with customerId " + message.getSourceId(), e);
}
}
@Override
public List<CustomerMessage> getBy(Collection<Long> messageIds) throws DaoException {
if (messageIds.isEmpty()) {
return new ArrayList<>();
public List<WebhookMessageModel<CustomerMessage>> getWebhookModels(Long messageId) {
final String sql = "select m.*, w.id as hook_id, w.url, pk.priv_key" +
" from hook.customer_message m" +
" join hook.webhook w on m.party_id = w.party_id " +
" and w.enabled and w.topic=CAST(:message_type as hook.message_topic)" +
" join hook.webhook_to_events wte on wte.hook_id = w.id" +
" join hook.party_data pk on w.party_id=pk.party_id" +
" where m.id =:id " +
" and m.event_type = wte.event_type " +
" and (m.customer_shop_id = wte.invoice_shop_id or wte.invoice_shop_id is null) ";
MapSqlParameterSource mapSqlParameterSource = new MapSqlParameterSource()
.addValue("id", messageId)
.addValue("message_type", Event.TopicEnum.CUSTOMERSTOPIC.getValue());
return jdbcTemplate.query(sql, mapSqlParameterSource, webhookMessageModelRowMapper);
}
final String sql = "SELECT * FROM hook.customer_message WHERE id in (:ids)";
@Override
public Long getParentId(Long hookId, String customerId, Long messageId) {
final String sql = "select m.id" +
" from hook.customer_message m " +
" join hook.webhook w on w.id=:hook_id" +
" join hook.webhook_to_events wte on wte.hook_id = w.id" +
" where m.customer_id =:customer_id" +
" and m.id <:id " +
" and m.event_type = wte.event_type " +
" and (m.customer_shop_id = wte.invoice_shop_id or wte.invoice_shop_id is null) " +
" order by id desc limit 1 ";
MapSqlParameterSource mapSqlParameterSource = new MapSqlParameterSource()
.addValue("hook_id", hookId)
.addValue("customer_id", customerId)
.addValue("id", messageId);
try {
List<CustomerMessage> messagesFromDb =
jdbcTemplate.query(sql, new MapSqlParameterSource("ids", messageIds), messageRowMapper);
log.debug("messagesFromDb {}", messagesFromDb);
return messagesFromDb;
} catch (NestedRuntimeException e) {
throw new DaoException("CustomerMessageDaoImpl.getByIds error", e);
return jdbcTemplate.queryForObject(sql, mapSqlParameterSource, Long.class);
} catch (EmptyResultDataAccessException e) {
return parentNotExistId;
}
}
}

View File

@ -1,106 +0,0 @@
package dev.vality.hooker.dao.impl;
import dev.vality.hooker.dao.QueueDao;
import dev.vality.hooker.exception.DaoException;
import dev.vality.hooker.model.CustomerQueue;
import dev.vality.hooker.model.Hook;
import dev.vality.hooker.retry.RetryPolicyType;
import dev.vality.swag_webhook_events.model.Event;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.core.NestedRuntimeException;
import org.springframework.jdbc.core.RowMapper;
import org.springframework.jdbc.core.namedparam.MapSqlParameterSource;
import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate;
import org.springframework.stereotype.Component;
import java.util.Collection;
import java.util.List;
/**
* Created by inalarsanukaev on 14.11.17.
*/
@Slf4j
@Component
@RequiredArgsConstructor
public class CustomerQueueDao implements QueueDao<CustomerQueue> {
public static RowMapper<CustomerQueue> queueWithPolicyRowMapper = (rs, i) -> {
CustomerQueue queue = new CustomerQueue();
queue.setId(rs.getLong("id"));
queue.setCustomerId(rs.getString("customer_id"));
Hook hook = new Hook();
hook.setId(rs.getLong("hook_id"));
hook.setPartyId(rs.getString("party_id"));
hook.setTopic(rs.getString("message_type"));
hook.setUrl(rs.getString("url"));
hook.setPubKey(rs.getString("pub_key"));
hook.setPrivKey(rs.getString("priv_key"));
hook.setEnabled(rs.getBoolean("enabled"));
RetryPolicyType retryPolicyType = RetryPolicyType.valueOf(rs.getString("retry_policy"));
hook.setRetryPolicyType(retryPolicyType);
queue.setHook(hook);
queue.setRetryPolicyRecord(retryPolicyType.build(rs));
return queue;
};
private final NamedParameterJdbcTemplate jdbcTemplate;
public void createWithPolicy(long messageId) throws DaoException {
final String sql = "with queue as ( " +
" insert into hook.customer_queue(hook_id, customer_id)" +
" select w.id, m.customer_id" +
" from hook.customer_message m" +
" join hook.webhook w on m.party_id = w.party_id " +
" and w.enabled and w.topic=CAST(:message_type as hook.message_topic)" +
" where m.id = :id " +
" on conflict(hook_id, customer_id) do nothing returning *) " +
" insert into hook.simple_retry_policy(queue_id, message_type) " +
" select id, CAST(:message_type as hook.message_topic) from queue";
try {
int count = jdbcTemplate.update(sql, new MapSqlParameterSource("id", messageId)
.addValue("message_type", getMessagesTopic()));
log.info("Created {} queues for messageId {}", count, messageId);
} catch (NestedRuntimeException e) {
log.error("Fail to createWithPolicy queue {}", messageId, e);
throw new DaoException(e);
}
}
@Override
public List<CustomerQueue> getWithPolicies(Collection<Long> ids) throws DaoException {
final String sql =
" select q.id, q.hook_id, q.customer_id, wh.party_id, wh.url, k.pub_key, k.priv_key, wh.enabled, " +
" wh.retry_policy, srp.fail_count, srp.last_fail_time, " +
" srp.next_fire_time_ms, srp.message_type " +
" from hook.customer_queue q " +
" join hook.webhook wh on wh.id = q.hook_id " +
" and wh.enabled and wh.topic=CAST(:message_type as hook.message_topic)" +
" join hook.party_data k on k.party_id = wh.party_id " +
" left join hook.simple_retry_policy srp on q.id = srp.queue_id " +
" and srp.message_type=CAST(:message_type as hook.message_topic)" +
" where q.id in (:ids) and q.enabled";
final MapSqlParameterSource params = new MapSqlParameterSource("ids", ids)
.addValue("message_type", getMessagesTopic());
try {
return jdbcTemplate.query(sql, params, queueWithPolicyRowMapper);
} catch (NestedRuntimeException e) {
throw new DaoException(e);
}
}
@Override
public void disable(long id) throws DaoException {
final String sql = " UPDATE hook.customer_queue SET enabled = FALSE where id=:id;";
try {
jdbcTemplate.update(sql, new MapSqlParameterSource("id", id));
} catch (NestedRuntimeException e) {
log.error("Fail to disable queue: {}", id, e);
throw new DaoException(e);
}
}
public String getMessagesTopic() {
return Event.TopicEnum.CUSTOMERSTOPIC.getValue();
}
}

View File

@ -1,112 +0,0 @@
package dev.vality.hooker.dao.impl;
import dev.vality.hooker.dao.AbstractTaskDao;
import dev.vality.hooker.exception.DaoException;
import dev.vality.hooker.model.Task;
import dev.vality.swag_webhook_events.model.Event;
import lombok.extern.slf4j.Slf4j;
import org.springframework.core.NestedRuntimeException;
import org.springframework.jdbc.core.namedparam.MapSqlParameterSource;
import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate;
import org.springframework.stereotype.Component;
import java.util.List;
import java.util.Map;
@Slf4j
@Component
public class CustomerTaskDao extends AbstractTaskDao {
public CustomerTaskDao(NamedParameterJdbcTemplate jdbcTemplate) {
super(jdbcTemplate);
}
@Override
protected String getMessageTopic() {
return Event.TopicEnum.CUSTOMERSTOPIC.getValue();
}
public void create(long messageId) throws DaoException {
final String sql =
" insert into hook.scheduled_task(message_id, queue_id, message_type)" +
" select m.id, q.id, w.topic" +
" from hook.customer_message m" +
" join hook.webhook w on m.party_id = w.party_id " +
" and w.enabled and w.topic=CAST(:message_type as hook.message_topic)" +
" join hook.webhook_to_events wte on wte.hook_id = w.id" +
" join hook.customer_queue q on q.hook_id=w.id and q.enabled and q.customer_id=m.customer_id" +
" where m.id = :message_id " +
" and m.event_type = wte.event_type " +
" and (m.customer_shop_id = wte.invoice_shop_id or wte.invoice_shop_id is null) " +
" ON CONFLICT (message_id, queue_id, message_type) DO NOTHING";
try {
jdbcTemplate.update(sql, new MapSqlParameterSource("message_id", messageId)
.addValue("message_type", getMessageTopic()));
} catch (NestedRuntimeException e) {
log.error("Fail to create tasks for messages.", e);
throw new DaoException(e);
}
}
public int create(long hookId, String customerId) throws DaoException {
final String sql =
" insert into hook.scheduled_task(message_id, queue_id, message_type)" +
" select m.id, q.id, w.topic" +
" from hook.customer_message m" +
" join hook.webhook w on m.party_id = w.party_id " +
" and w.id = :hook_id " +
" and w.enabled " +
" and w.topic=CAST(:message_type as hook.message_topic)" +
" join hook.webhook_to_events wte on wte.hook_id = w.id" +
" join hook.customer_queue q on q.hook_id=w.id " +
" and q.enabled " +
" and q.customer_id=m.customer_id" +
" where m.customer_id = :customer_id " +
" and m.event_type = wte.event_type " +
" and (m.customer_shop_id = wte.invoice_shop_id or wte.invoice_shop_id is null) " +
" ON CONFLICT (message_id, queue_id, message_type) DO NOTHING";
try {
return jdbcTemplate.update(sql, new MapSqlParameterSource("hook_id", hookId)
.addValue("customer_id", customerId)
.addValue("message_type", getMessageTopic()));
} catch (NestedRuntimeException e) {
log.error("Fail to create tasks for messages.", e);
throw new DaoException(e);
}
}
@Override
public Map<Long, List<Task>> getScheduled() throws DaoException {
final String sql = " WITH scheduled AS (" +
"SELECT st.message_id, st.queue_id, cq.customer_id " +
"FROM hook.scheduled_task st " +
"JOIN hook.customer_queue cq ON st.queue_id=cq.id AND cq.enabled " +
"JOIN hook.simple_retry_policy srp ON st.queue_id=srp.queue_id " +
"AND st.message_type=srp.message_type " +
"JOIN hook.webhook w ON cq.hook_id = w.id AND w.enabled " +
"WHERE st.message_type = CAST(:message_type as hook.message_topic) " +
"AND COALESCE(srp.next_fire_time_ms, 0) < :curr_time " +
"ORDER BY w.availability ASC, st.message_id ASC " +
"LIMIT 1 " +
"FOR UPDATE OF cq SKIP LOCKED " +
"), locked_customer_queue AS (" +
" SELECT ciq.id FROM hook.customer_queue ciq " +
" WHERE ciq.customer_id IN (SELECT DISTINCT schd.customer_id FROM scheduled schd) " +
" FOR UPDATE OF ciq SKIP LOCKED " +
") SELECT message_id, queue_id FROM hook.scheduled_task s " +
" JOIN locked_customer_queue lq ON s.queue_id = lq.id " +
" ORDER BY s.message_id" +
" FOR UPDATE OF s SKIP LOCKED";
try {
List<Task> tasks = jdbcTemplate.query(sql,
new MapSqlParameterSource("message_type", getMessageTopic())
.addValue("curr_time", System.currentTimeMillis()),
taskRowMapper);
return splitByQueue(tasks);
} catch (NestedRuntimeException e) {
log.warn("Fail to get active tasks from scheduled_task", e);
throw new DaoException(e);
}
}
}

View File

@ -1,31 +0,0 @@
package dev.vality.hooker.dao.impl;
import dev.vality.hooker.dao.IdsGeneratorDao;
import dev.vality.hooker.exception.DaoException;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.core.NestedRuntimeException;
import org.springframework.jdbc.core.namedparam.MapSqlParameterSource;
import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate;
import org.springframework.stereotype.Component;
import java.util.List;
@Slf4j
@Component
@RequiredArgsConstructor
public class EventIdsGeneratorDaoImpl implements IdsGeneratorDao {
private final NamedParameterJdbcTemplate jdbcTemplate;
@Override
public List<Long> get(int size) throws DaoException {
try {
String sql = "select nextval('hook.event_id_seq') from generate_series(1, :size)";
MapSqlParameterSource parameterSource = new MapSqlParameterSource().addValue("size", size);
return jdbcTemplate.queryForList(sql, parameterSource, Long.class);
} catch (NestedRuntimeException e) {
throw new DaoException(e);
}
}
}

View File

@ -21,16 +21,9 @@ import org.springframework.jdbc.support.KeyHolder;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.*;
import java.util.stream.Collectors;
/**
* Created by inal on 28.11.2016.
*/
@Slf4j
@Component
@RequiredArgsConstructor

View File

@ -0,0 +1,137 @@
package dev.vality.hooker.dao.impl;
import dev.vality.hooker.dao.InvoicingMessageDao;
import dev.vality.hooker.dao.rowmapper.InvoicingRowMapper;
import dev.vality.hooker.exception.DaoException;
import dev.vality.hooker.exception.NotFoundException;
import dev.vality.hooker.model.InvoicingMessage;
import dev.vality.hooker.model.InvoicingMessageKey;
import dev.vality.hooker.model.WebhookMessageModel;
import dev.vality.swag_webhook_events.model.Event;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.core.NestedRuntimeException;
import org.springframework.dao.EmptyResultDataAccessException;
import org.springframework.jdbc.core.RowMapper;
import org.springframework.jdbc.core.namedparam.MapSqlParameterSource;
import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate;
import org.springframework.jdbc.support.GeneratedKeyHolder;
import org.springframework.stereotype.Component;
import java.util.List;
@Slf4j
@Component
@RequiredArgsConstructor
public class InvoicingDaoImpl implements InvoicingMessageDao {
private final RowMapper<InvoicingMessage> invoicingMessageRowMapper;
private final RowMapper<WebhookMessageModel<InvoicingMessage>> webhookMessageModelRowMapper;
private final NamedParameterJdbcTemplate jdbcTemplate;
@Value("${parent.not.exist.id:-1}")
private Long parentNotExistId;
@Override
public Long save(InvoicingMessage message) {
try {
final String sql = "INSERT INTO hook.message" +
"(event_time, sequence_id, change_id, type, party_id, event_type, " +
"invoice_id, shop_id, invoice_status, payment_id, payment_status, refund_id, refund_status) " +
"VALUES " +
"(:event_time, :sequence_id, :change_id, :type, :party_id, " +
"CAST(:event_type as hook.eventtype), :invoice_id, :shop_id, :invoice_status, :payment_id, " +
":payment_status, :refund_id, :refund_status) " +
"ON CONFLICT (invoice_id, sequence_id, change_id) DO NOTHING " +
"RETURNING id";
MapSqlParameterSource sqlParameterSources = new MapSqlParameterSource()
.addValue(InvoicingRowMapper.EVENT_TIME, message.getEventTime())
.addValue(InvoicingRowMapper.SEQUENCE_ID, message.getSequenceId())
.addValue(InvoicingRowMapper.CHANGE_ID, message.getChangeId())
.addValue(InvoicingRowMapper.TYPE, message.getType().getValue())
.addValue(InvoicingRowMapper.PARTY_ID, message.getPartyId())
.addValue(InvoicingRowMapper.EVENT_TYPE, message.getEventType().toString())
.addValue(InvoicingRowMapper.INVOICE_ID, message.getSourceId())
.addValue(InvoicingRowMapper.SHOP_ID, message.getShopId())
.addValue(InvoicingRowMapper.INVOICE_STATUS, message.getInvoiceStatus().getValue())
.addValue(InvoicingRowMapper.PAYMENT_ID, message.getPaymentId())
.addValue(InvoicingRowMapper.PAYMENT_STATUS,
message.getPaymentStatus() != null ? message.getPaymentStatus().getValue() : null)
.addValue(InvoicingRowMapper.REFUND_ID, message.getRefundId())
.addValue(InvoicingRowMapper.REFUND_STATUS,
message.getRefundStatus() != null ? message.getRefundStatus().getValue() : null);
GeneratedKeyHolder keyHolder = new GeneratedKeyHolder();
jdbcTemplate.update(sql, sqlParameterSources, keyHolder);
return keyHolder.getKey() == null ? null : keyHolder.getKey().longValue();
} catch (NestedRuntimeException e) {
throw new DaoException("Couldn't save batch messages: " + message.getSourceId(), e);
}
}
@Override
public InvoicingMessage getInvoicingMessage(InvoicingMessageKey key) throws NotFoundException, DaoException {
final String sql = "SELECT * FROM hook.message WHERE invoice_id =:invoice_id" +
" AND (payment_id IS NULL OR payment_id=:payment_id)" +
" AND (refund_id IS NULL OR refund_id=:refund_id)" +
" AND type =:type ORDER BY id DESC LIMIT 1";
var params = new MapSqlParameterSource(InvoicingRowMapper.INVOICE_ID, key.getInvoiceId())
.addValue(InvoicingRowMapper.PAYMENT_ID, key.getPaymentId())
.addValue(InvoicingRowMapper.REFUND_ID, key.getRefundId())
.addValue(InvoicingRowMapper.TYPE, key.getType().getValue());
try {
return jdbcTemplate.queryForObject(sql, params, invoicingMessageRowMapper);
} catch (EmptyResultDataAccessException e) {
throw new NotFoundException(String.format("InvoicingMessage not found %s!", key));
} catch (NestedRuntimeException e) {
throw new DaoException(String.format("InvoicingMessage error %s", key), e);
}
}
@Override
public List<WebhookMessageModel<InvoicingMessage>> getWebhookModels(Long messageId) {
final String sql = "select m.*, w.id as hook_id, w.url, pk.priv_key" +
" from hook.message m" +
" join hook.webhook w on m.party_id = w.party_id " +
" and w.enabled and w.topic=CAST(:message_type as hook.message_topic)" +
" join hook.webhook_to_events wte on wte.hook_id = w.id" +
" join hook.party_data pk on w.party_id=pk.party_id" +
" where m.id =:id " +
" and m.event_type = wte.event_type " +
" and (m.shop_id = wte.invoice_shop_id or wte.invoice_shop_id is null) " +
" and (m.invoice_status = wte.invoice_status or wte.invoice_status is null) " +
" and (m.payment_status = wte.invoice_payment_status or wte.invoice_payment_status is null)" +
" and (m.refund_status = wte.invoice_payment_refund_status " +
" or wte.invoice_payment_refund_status is null)";
MapSqlParameterSource mapSqlParameterSource = new MapSqlParameterSource().addValue("id", messageId)
.addValue("message_type", Event.TopicEnum.INVOICESTOPIC.getValue());
return jdbcTemplate.query(sql, mapSqlParameterSource, webhookMessageModelRowMapper);
}
@Override
public Long getParentId(Long hookId, String invoiceId, Long messageId) {
final String sql = "select m.id" +
" from hook.message m " +
" join hook.webhook w on w.id=:hook_id" +
" join hook.webhook_to_events wte on wte.hook_id = w.id" +
" where m.invoice_id =:invoice_id" +
" and m.id <:id " +
" and m.event_type = wte.event_type " +
" and (m.shop_id = wte.invoice_shop_id or wte.invoice_shop_id is null) " +
" and (m.invoice_status = wte.invoice_status or wte.invoice_status is null) " +
" and (m.payment_status = wte.invoice_payment_status or wte.invoice_payment_status is null)" +
" and (m.refund_status = wte.invoice_payment_refund_status " +
" or wte.invoice_payment_refund_status is null)" +
" order by id desc limit 1 ";
MapSqlParameterSource mapSqlParameterSource = new MapSqlParameterSource()
.addValue("hook_id", hookId)
.addValue("invoice_id", invoiceId)
.addValue("id", messageId);
try {
return jdbcTemplate.queryForObject(sql, mapSqlParameterSource, Long.class);
} catch (EmptyResultDataAccessException e) {
return parentNotExistId;
}
}
}

View File

@ -1,132 +0,0 @@
package dev.vality.hooker.dao.impl;
import com.github.benmanes.caffeine.cache.Cache;
import dev.vality.hooker.dao.InvoicingMessageDao;
import dev.vality.hooker.exception.DaoException;
import dev.vality.hooker.exception.NotFoundException;
import dev.vality.hooker.model.InvoicingMessage;
import dev.vality.hooker.model.InvoicingMessageKey;
import dev.vality.hooker.utils.KeyUtils;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.core.NestedRuntimeException;
import org.springframework.dao.EmptyResultDataAccessException;
import org.springframework.jdbc.core.RowMapper;
import org.springframework.jdbc.core.namedparam.MapSqlParameterSource;
import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate;
import org.springframework.stereotype.Component;
import java.util.Collection;
import java.util.List;
import java.util.stream.Collectors;
import java.util.stream.IntStream;
import static dev.vality.hooker.dao.impl.InvoicingMessageRowMapper.CHANGE_ID;
import static dev.vality.hooker.dao.impl.InvoicingMessageRowMapper.EVENT_TIME;
import static dev.vality.hooker.dao.impl.InvoicingMessageRowMapper.EVENT_TYPE;
import static dev.vality.hooker.dao.impl.InvoicingMessageRowMapper.ID;
import static dev.vality.hooker.dao.impl.InvoicingMessageRowMapper.INVOICE_ID;
import static dev.vality.hooker.dao.impl.InvoicingMessageRowMapper.INVOICE_STATUS;
import static dev.vality.hooker.dao.impl.InvoicingMessageRowMapper.NEW_EVENT_ID;
import static dev.vality.hooker.dao.impl.InvoicingMessageRowMapper.PARTY_ID;
import static dev.vality.hooker.dao.impl.InvoicingMessageRowMapper.PAYMENT_ID;
import static dev.vality.hooker.dao.impl.InvoicingMessageRowMapper.PAYMENT_STATUS;
import static dev.vality.hooker.dao.impl.InvoicingMessageRowMapper.REFUND_ID;
import static dev.vality.hooker.dao.impl.InvoicingMessageRowMapper.REFUND_STATUS;
import static dev.vality.hooker.dao.impl.InvoicingMessageRowMapper.SEQUENCE_ID;
import static dev.vality.hooker.dao.impl.InvoicingMessageRowMapper.SHOP_ID;
import static dev.vality.hooker.dao.impl.InvoicingMessageRowMapper.TYPE;
@Slf4j
@Component
@RequiredArgsConstructor
public class InvoicingMessageDaoImpl implements InvoicingMessageDao {
private static RowMapper<InvoicingMessage> messageRowMapper = new InvoicingMessageRowMapper();
private final NamedParameterJdbcTemplate jdbcTemplate;
private final Cache<InvoicingMessageKey, InvoicingMessage> invoicingCache;
public void saveBatch(List<InvoicingMessage> messages) throws DaoException {
int[] batchMessagesResult = saveBatchMessages(messages);
log.info("Batch messages saved info {}",
IntStream.range(0, messages.size())
.mapToObj(i -> "(" + i + " : " + batchMessagesResult[i] + " : " + messages.get(i).getId() +
" : " + messages.get(i).getInvoiceId() + ")")
.collect(Collectors.toList()));
}
private int[] saveBatchMessages(List<InvoicingMessage> messages) {
try {
messages.forEach(m -> invoicingCache.put(KeyUtils.key(m), m));
final String sql = "INSERT INTO hook.message" +
"(id, new_event_id, event_time, sequence_id, change_id, type, party_id, event_type, " +
"invoice_id, shop_id, invoice_status, payment_id, payment_status, refund_id, refund_status) " +
"VALUES " +
"(:id, :new_event_id, :event_time, :sequence_id, :change_id, :type, :party_id, " +
"CAST(:event_type as hook.eventtype), :invoice_id, :shop_id, :invoice_status, :payment_id, " +
":payment_status, :refund_id, :refund_status) " +
"ON CONFLICT (invoice_id, sequence_id, change_id) DO NOTHING ";
MapSqlParameterSource[] sqlParameterSources = messages.stream()
.map(message -> new MapSqlParameterSource()
.addValue(ID, message.getId())
.addValue(NEW_EVENT_ID, message.getEventId())
.addValue(EVENT_TIME, message.getEventTime())
.addValue(SEQUENCE_ID, message.getSequenceId())
.addValue(CHANGE_ID, message.getChangeId())
.addValue(TYPE, message.getType().getValue())
.addValue(PARTY_ID, message.getPartyId())
.addValue(EVENT_TYPE, message.getEventType().toString())
.addValue(INVOICE_ID, message.getInvoiceId())
.addValue(SHOP_ID, message.getShopId())
.addValue(INVOICE_STATUS, message.getInvoiceStatus().getValue())
.addValue(PAYMENT_ID, message.getPaymentId())
.addValue(PAYMENT_STATUS,
message.getPaymentStatus() != null ? message.getPaymentStatus().getValue() : null)
.addValue(REFUND_ID, message.getRefundId())
.addValue(REFUND_STATUS,
message.getRefundStatus() != null ? message.getRefundStatus().getValue() : null))
.toArray(MapSqlParameterSource[]::new);
return jdbcTemplate.batchUpdate(sql, sqlParameterSources);
} catch (NestedRuntimeException e) {
List<String> shortInfo = messages.stream()
.map(m -> "(" + m.getId() + " : " + m.getInvoiceId() + ")")
.collect(Collectors.toList());
throw new DaoException("Couldn't save batch messages: " + shortInfo, e);
}
}
@Override
public InvoicingMessage getInvoicingMessage(InvoicingMessageKey key) throws NotFoundException, DaoException {
InvoicingMessage result = invoicingCache.getIfPresent(key);
if (result != null) {
return result;
}
final String sql = "SELECT * FROM hook.message WHERE invoice_id =:invoice_id" +
" AND (payment_id IS NULL OR payment_id=:payment_id)" +
" AND (refund_id IS NULL OR refund_id=:refund_id)" +
" AND type =:type ORDER BY id DESC LIMIT 1";
MapSqlParameterSource params = new MapSqlParameterSource(INVOICE_ID, key.getInvoiceId())
.addValue(PAYMENT_ID, key.getPaymentId())
.addValue(REFUND_ID, key.getRefundId())
.addValue(TYPE, key.getType().getValue());
try {
return jdbcTemplate.queryForObject(sql, params, messageRowMapper);
} catch (EmptyResultDataAccessException e) {
throw new NotFoundException(String.format("InvoicingMessage not found %s!", key.toString()));
} catch (NestedRuntimeException e) {
throw new DaoException(String.format("InvoicingMessage error %s", key.toString()), e);
}
}
@Override
public List<InvoicingMessage> getBy(Collection<Long> messageIds) throws DaoException {
final String sql = "SELECT * FROM hook.message WHERE id in (:ids)";
try {
return jdbcTemplate.query(sql, new MapSqlParameterSource("ids", messageIds), messageRowMapper);
} catch (NestedRuntimeException e) {
throw new DaoException("Couldn't get invoice message by ids: " + messageIds, e);
}
}
}

View File

@ -1,105 +0,0 @@
package dev.vality.hooker.dao.impl;
import dev.vality.hooker.dao.QueueDao;
import dev.vality.hooker.exception.DaoException;
import dev.vality.hooker.model.Hook;
import dev.vality.hooker.model.InvoicingQueue;
import dev.vality.hooker.retry.RetryPolicyType;
import dev.vality.swag_webhook_events.model.Event;
import lombok.RequiredArgsConstructor;
import org.springframework.core.NestedRuntimeException;
import org.springframework.jdbc.core.RowMapper;
import org.springframework.jdbc.core.namedparam.MapSqlParameterSource;
import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate;
import org.springframework.stereotype.Component;
import java.util.Collection;
import java.util.List;
/**
* Created by inalarsanukaev on 14.11.17.
*/
@Component
@RequiredArgsConstructor
public class InvoicingQueueDao implements QueueDao<InvoicingQueue> {
public static RowMapper<InvoicingQueue> queueWithPolicyRowMapper = (rs, i) -> {
InvoicingQueue queue = new InvoicingQueue();
queue.setId(rs.getLong("id"));
queue.setInvoiceId(rs.getString("invoice_id"));
Hook hook = new Hook();
hook.setId(rs.getLong("hook_id"));
hook.setPartyId(rs.getString("party_id"));
hook.setTopic(rs.getString("message_type"));
hook.setUrl(rs.getString("url"));
hook.setPubKey(rs.getString("pub_key"));
hook.setPrivKey(rs.getString("priv_key"));
hook.setEnabled(rs.getBoolean("enabled"));
RetryPolicyType retryPolicyType = RetryPolicyType.valueOf(rs.getString("retry_policy"));
hook.setRetryPolicyType(retryPolicyType);
queue.setHook(hook);
queue.setRetryPolicyRecord(retryPolicyType.build(rs));
return queue;
};
private final NamedParameterJdbcTemplate jdbcTemplate;
public int[] saveBatchWithPolicies(List<Long> messageIds) throws DaoException {
final String sql = "with queue as ( " +
" insert into hook.invoicing_queue(hook_id, invoice_id)" +
" select w.id , m.invoice_id" +
" from hook.message m" +
" join hook.webhook w on m.party_id = w.party_id " +
" and w.enabled and w.topic=CAST(:message_type as hook.message_topic)" +
" where m.id = :id " +
" on conflict(hook_id, invoice_id) do nothing returning *) " +
" insert into hook.simple_retry_policy(queue_id, message_type) " +
" select id, CAST(:message_type as hook.message_topic) from queue";
MapSqlParameterSource[] sqlParameterSources = messageIds
.stream()
.map(id -> new MapSqlParameterSource()
.addValue("id", id)
.addValue("message_type", Event.TopicEnum.INVOICESTOPIC.getValue()))
.toArray(MapSqlParameterSource[]::new);
try {
return jdbcTemplate.batchUpdate(sql, sqlParameterSources);
} catch (NestedRuntimeException e) {
throw new DaoException("Couldn't save queue batch with messageIds=" + messageIds, e);
}
}
@Override
public List<InvoicingQueue> getWithPolicies(Collection<Long> ids) {
final String sql =
" select q.id, q.hook_id, q.invoice_id, wh.party_id, wh.url, k.pub_key, k.priv_key, wh.enabled, " +
" wh.retry_policy, srp.fail_count, srp.last_fail_time, " +
" srp.next_fire_time_ms, srp.message_type " +
" from hook.invoicing_queue q " +
" join hook.webhook wh on wh.id = q.hook_id " +
" and wh.enabled and wh.topic=CAST(:message_type as hook.message_topic)" +
" join hook.party_data k on k.party_id = wh.party_id " +
" left join hook.simple_retry_policy srp on q.id = srp.queue_id " +
" and srp.message_type=CAST(:message_type as hook.message_topic)" +
" where q.id in (:ids) and q.enabled";
final MapSqlParameterSource params = new MapSqlParameterSource("ids", ids)
.addValue("message_type", getMessagesTopic());
try {
return jdbcTemplate.query(sql, params, queueWithPolicyRowMapper);
} catch (NestedRuntimeException e) {
throw new DaoException("Couldn't get queue by queueIds=" + ids, e);
}
}
@Override
public void disable(long id) throws DaoException {
final String sql = " UPDATE hook.invoicing_queue SET enabled = FALSE where id=:id;";
try {
jdbcTemplate.update(sql, new MapSqlParameterSource("id", id));
} catch (NestedRuntimeException e) {
throw new DaoException("Couldn't disable queue with id=" + id, e);
}
}
public String getMessagesTopic() {
return Event.TopicEnum.INVOICESTOPIC.getValue();
}
}

View File

@ -1,130 +0,0 @@
package dev.vality.hooker.dao.impl;
import dev.vality.hooker.dao.AbstractTaskDao;
import dev.vality.hooker.exception.DaoException;
import dev.vality.hooker.model.Task;
import dev.vality.swag_webhook_events.model.Event;
import lombok.extern.slf4j.Slf4j;
import org.springframework.core.NestedRuntimeException;
import org.springframework.jdbc.core.namedparam.MapSqlParameterSource;
import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate;
import org.springframework.stereotype.Component;
import java.util.List;
import java.util.Map;
/**
* Created by jeckep on 17.04.17.
*/
@Slf4j
@Component
public class InvoicingTaskDao extends AbstractTaskDao {
public InvoicingTaskDao(NamedParameterJdbcTemplate jdbcTemplate) {
super(jdbcTemplate);
}
@Override
protected String getMessageTopic() {
return Event.TopicEnum.INVOICESTOPIC.getValue();
}
//TODO limit invoices from hook
public int save(List<Long> messageIds) throws DaoException {
if (messageIds == null || messageIds.isEmpty()) {
return 0;
}
final String sql =
" insert into hook.scheduled_task(message_id, queue_id, message_type)" +
" select m.id, q.id, w.topic" +
" from hook.message m" +
" join hook.webhook w on m.party_id = w.party_id " +
" and w.enabled and w.topic=CAST(:message_type as hook.message_topic)" +
" join hook.webhook_to_events wte on wte.hook_id = w.id" +
" join hook.invoicing_queue q on q.hook_id=w.id and q.enabled and q.invoice_id=m.invoice_id" +
" where m.id in (:message_ids) " +
" and m.event_type = wte.event_type " +
" and (m.shop_id = wte.invoice_shop_id or wte.invoice_shop_id is null) " +
" and (m.invoice_status = wte.invoice_status or wte.invoice_status is null) " +
" and (m.payment_status = wte.invoice_payment_status or wte.invoice_payment_status is null)" +
" and (m.refund_status = wte.invoice_payment_refund_status " +
" or wte.invoice_payment_refund_status is null)" +
" ON CONFLICT (message_id, queue_id, message_type) DO NOTHING";
final MapSqlParameterSource sqlParameterSources = new MapSqlParameterSource("message_ids", messageIds)
.addValue("message_type", Event.TopicEnum.INVOICESTOPIC.getValue());
try {
return jdbcTemplate.update(sql, sqlParameterSources);
} catch (NestedRuntimeException e) {
throw new DaoException("Failed to create tasks for messageIds=" + messageIds, e);
}
}
public int save(Long hookId, String invoiceId) throws DaoException {
final String sql =
" insert into hook.scheduled_task(message_id, queue_id, message_type)" +
" select m.id, q.id, w.topic" +
" from hook.message m" +
" join hook.webhook w on m.party_id = w.party_id " +
" and w.id = :hook_id " +
" and w.enabled " +
" and w.topic=CAST(:message_type as hook.message_topic)" +
" join hook.webhook_to_events wte on wte.hook_id = w.id" +
" join hook.invoicing_queue q on q.hook_id=w.id " +
" and q.enabled " +
" and q.invoice_id=m.invoice_id" +
" where m.invoice_id = :invoice_id " +
" and m.event_type = wte.event_type " +
" and (m.shop_id = wte.invoice_shop_id or wte.invoice_shop_id is null) " +
" and (m.invoice_status = wte.invoice_status or wte.invoice_status is null) " +
" and (m.payment_status = wte.invoice_payment_status or wte.invoice_payment_status is null)" +
" and (m.refund_status = wte.invoice_payment_refund_status " +
" or wte.invoice_payment_refund_status is null)" +
" ON CONFLICT (message_id, queue_id, message_type) DO NOTHING";
final MapSqlParameterSource sqlParameterSources = new MapSqlParameterSource("hook_id", hookId)
.addValue("invoice_id", invoiceId)
.addValue("message_type", Event.TopicEnum.INVOICESTOPIC.getValue());
try {
return jdbcTemplate.update(sql, sqlParameterSources);
} catch (NestedRuntimeException e) {
throw new DaoException("Failed to create tasks for hook_id=" + hookId, e);
}
}
@Override
public Map<Long, List<Task>> getScheduled() throws DaoException {
final String sql = " WITH scheduled AS (" +
"SELECT st.message_id, st.queue_id, iq.invoice_id " +
"FROM hook.scheduled_task st " +
"JOIN hook.invoicing_queue iq ON st.queue_id=iq.id AND iq.enabled " +
"JOIN hook.simple_retry_policy srp ON st.queue_id=srp.queue_id " +
"AND st.message_type=srp.message_type " +
"JOIN hook.webhook w ON iq.hook_id = w.id AND w.enabled " +
"WHERE st.message_type = CAST(:message_type as hook.message_topic) " +
"AND COALESCE(srp.next_fire_time_ms, 0) < :curr_time " +
"ORDER BY w.availability ASC, st.message_id ASC " +
"LIMIT 1 " +
"FOR UPDATE OF iq SKIP LOCKED " +
"), locked_invoicing_queue AS (" +
" SELECT liq.id FROM hook.invoicing_queue liq " +
" WHERE liq.invoice_id IN (SELECT DISTINCT schd.invoice_id FROM scheduled schd) " +
" FOR UPDATE OF liq SKIP LOCKED " +
") SELECT message_id, queue_id FROM hook.scheduled_task s " +
" JOIN locked_invoicing_queue lq ON s.queue_id = lq.id " +
" ORDER BY s.message_id " +
" FOR UPDATE OF s SKIP LOCKED";
try {
List<Task> tasks = jdbcTemplate.query(sql,
new MapSqlParameterSource("message_type", getMessageTopic())
.addValue("curr_time", System.currentTimeMillis()),
taskRowMapper);
return splitByQueue(tasks);
} catch (NestedRuntimeException e) {
log.warn("Fail to get active tasks from scheduled_task", e);
throw new DaoException(e);
}
}
}

View File

@ -1,31 +0,0 @@
package dev.vality.hooker.dao.impl;
import dev.vality.hooker.dao.IdsGeneratorDao;
import dev.vality.hooker.exception.DaoException;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.core.NestedRuntimeException;
import org.springframework.jdbc.core.namedparam.MapSqlParameterSource;
import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate;
import org.springframework.stereotype.Component;
import java.util.List;
@Slf4j
@Component
@RequiredArgsConstructor
public class MessageIdsGeneratorDaoImpl implements IdsGeneratorDao {
private final NamedParameterJdbcTemplate jdbcTemplate;
@Override
public List<Long> get(int size) throws DaoException {
try {
String sql = "select nextval('hook.seq') from generate_series(1, :size)";
MapSqlParameterSource parameterSource = new MapSqlParameterSource().addValue("size", size);
return jdbcTemplate.queryForList(sql, parameterSource, Long.class);
} catch (NestedRuntimeException e) {
throw new DaoException(e);
}
}
}

View File

@ -1,35 +0,0 @@
package dev.vality.hooker.dao.impl;
import dev.vality.hooker.dao.SimpleRetryPolicyDao;
import dev.vality.hooker.exception.DaoException;
import dev.vality.hooker.retry.impl.simple.SimpleRetryPolicyRecord;
import lombok.RequiredArgsConstructor;
import org.springframework.core.NestedRuntimeException;
import org.springframework.jdbc.core.namedparam.MapSqlParameterSource;
import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate;
import org.springframework.stereotype.Component;
@Component
@RequiredArgsConstructor
public class SimpleRetryPolicyDaoImpl implements SimpleRetryPolicyDao {
private final NamedParameterJdbcTemplate jdbcTemplate;
@Override
public void update(SimpleRetryPolicyRecord record) throws DaoException {
final String sql = "update hook.simple_retry_policy " +
" set last_fail_time = :last_fail_time, " +
" fail_count = :fail_count, " +
" next_fire_time_ms = :next_fire_time_ms" +
" where queue_id = :queue_id and message_type=CAST(:message_type as hook.message_topic)";
try {
jdbcTemplate.update(sql, new MapSqlParameterSource("queue_id", record.getQueueId())
.addValue("message_type", record.getMessageType())
.addValue("last_fail_time", record.getLastFailTime())
.addValue("fail_count", record.getFailCount())
.addValue("next_fire_time_ms", record.getNextFireTime()));
} catch (NestedRuntimeException e) {
throw new DaoException("Fail to update simple_retry_policy for record=" + record.getQueueId(), e);
}
}
}

View File

@ -0,0 +1,40 @@
package dev.vality.hooker.dao.rowmapper;
import dev.vality.hooker.model.CustomerMessage;
import dev.vality.hooker.model.CustomerMessageEnum;
import dev.vality.hooker.model.EventType;
import org.springframework.jdbc.core.RowMapper;
import org.springframework.stereotype.Component;
import java.sql.ResultSet;
import java.sql.SQLException;
@Component
public class CustomerRowMapper implements RowMapper<CustomerMessage> {
public static final String ID = "id";
public static final String TYPE = "type";
public static final String OCCURED_AT = "occured_at";
public static final String SEQUENCE_ID = "sequence_id";
public static final String CHANGE_ID = "change_id";
public static final String PARTY_ID = "party_id";
public static final String EVENT_TYPE = "event_type";
public static final String CUSTOMER_ID = "customer_id";
public static final String CUSTOMER_SHOP_ID = "customer_shop_id";
public static final String BINDING_ID = "binding_id";
@Override
public CustomerMessage mapRow(ResultSet rs, int rowNum) throws SQLException {
CustomerMessage message = new CustomerMessage();
message.setId(rs.getLong(ID));
message.setPartyId(rs.getString(PARTY_ID));
message.setEventTime(rs.getString(OCCURED_AT));
message.setSequenceId(rs.getLong(SEQUENCE_ID));
message.setChangeId(rs.getInt(CHANGE_ID));
message.setType(CustomerMessageEnum.lookup(rs.getString(TYPE)));
message.setEventType(EventType.valueOf(rs.getString(EVENT_TYPE)));
message.setSourceId(rs.getString(CUSTOMER_ID));
message.setShopId(rs.getString(CUSTOMER_SHOP_ID));
message.setBindingId(rs.getString(BINDING_ID));
return message;
}
}

View File

@ -1,20 +1,16 @@
package dev.vality.hooker.dao.impl;
package dev.vality.hooker.dao.rowmapper;
import dev.vality.hooker.model.EventType;
import dev.vality.hooker.model.InvoiceStatusEnum;
import dev.vality.hooker.model.InvoicingMessage;
import dev.vality.hooker.model.InvoicingMessageEnum;
import dev.vality.hooker.model.PaymentStatusEnum;
import dev.vality.hooker.model.RefundStatusEnum;
import dev.vality.hooker.model.*;
import org.springframework.jdbc.core.RowMapper;
import org.springframework.stereotype.Component;
import java.sql.ResultSet;
import java.sql.SQLException;
public class InvoicingMessageRowMapper implements RowMapper<InvoicingMessage> {
@Component
public class InvoicingRowMapper implements RowMapper<InvoicingMessage> {
public static final String ID = "id";
public static final String NEW_EVENT_ID = "new_event_id";
public static final String EVENT_TIME = "event_time";
public static final String SEQUENCE_ID = "sequence_id";
public static final String CHANGE_ID = "change_id";
@ -33,7 +29,6 @@ public class InvoicingMessageRowMapper implements RowMapper<InvoicingMessage> {
public InvoicingMessage mapRow(ResultSet rs, int i) throws SQLException {
InvoicingMessage message = new InvoicingMessage();
message.setId(rs.getLong(ID));
message.setEventId(rs.getLong(NEW_EVENT_ID));
message.setEventTime(rs.getString(EVENT_TIME));
message.setSequenceId(rs.getLong(SEQUENCE_ID));
message.setChangeId(rs.getInt(CHANGE_ID));
@ -41,7 +36,7 @@ public class InvoicingMessageRowMapper implements RowMapper<InvoicingMessage> {
message.setPartyId(rs.getString(PARTY_ID));
message.setShopId(rs.getString(SHOP_ID));
message.setEventType(EventType.valueOf(rs.getString(EVENT_TYPE)));
message.setInvoiceId(rs.getString(INVOICE_ID));
message.setSourceId(rs.getString(INVOICE_ID));
message.setInvoiceStatus(InvoiceStatusEnum.lookup(rs.getString(INVOICE_STATUS)));
message.setPaymentId(rs.getString(PAYMENT_ID));
message.setPaymentStatus(rs.getString(PAYMENT_STATUS) != null

View File

@ -0,0 +1,29 @@
package dev.vality.hooker.dao.rowmapper;
import dev.vality.hooker.model.Message;
import dev.vality.hooker.model.WebhookMessageModel;
import lombok.RequiredArgsConstructor;
import org.springframework.jdbc.core.RowMapper;
import java.sql.ResultSet;
import java.sql.SQLException;
@RequiredArgsConstructor
public class WebhookModelRowMapper<T extends Message> implements RowMapper<WebhookMessageModel<T>> {
public static final String HOOK_ID = "hook_id";
public static final String URL = "url";
public static final String PRIV_KEY = "priv_key";
private final RowMapper<T> messageRowMapper;
@Override
public WebhookMessageModel<T> mapRow(ResultSet rs, int rowNum) throws SQLException {
T invoicingMessage = messageRowMapper.mapRow(rs, rowNum);
WebhookMessageModel<T> webhookMessageModel = new WebhookMessageModel<>();
webhookMessageModel.setMessage(invoicingMessage);
webhookMessageModel.setHookId(rs.getLong(HOOK_ID));
webhookMessageModel.setUrl(rs.getString(URL));
webhookMessageModel.setPrivateKey(rs.getString(PRIV_KEY));
return webhookMessageModel;
}
}

View File

@ -8,14 +8,8 @@ import dev.vality.woody.thrift.impl.http.event.ServiceEventLogListener;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import javax.servlet.GenericServlet;
import javax.servlet.Servlet;
import javax.servlet.ServletConfig;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.*;
import javax.servlet.annotation.WebServlet;
import java.io.IOException;
@WebServlet("/hook")

View File

@ -1,48 +0,0 @@
package dev.vality.hooker.endpoint;
import dev.vality.damsel.webhooker.WebhookMessageServiceSrv;
import dev.vality.woody.api.event.CompositeServiceEventListener;
import dev.vality.woody.thrift.impl.http.THServiceBuilder;
import dev.vality.woody.thrift.impl.http.event.HttpServiceEventLogListener;
import dev.vality.woody.thrift.impl.http.event.ServiceEventLogListener;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import javax.servlet.GenericServlet;
import javax.servlet.Servlet;
import javax.servlet.ServletConfig;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.annotation.WebServlet;
import java.io.IOException;
@WebServlet("/message")
@Slf4j
@RequiredArgsConstructor
public class WebhookMessageServiceServlet extends GenericServlet {
private final WebhookMessageServiceSrv.Iface requestHandler;
private Servlet thriftServlet;
@Override
public void init(ServletConfig config) throws ServletException {
log.info("Hooker servlet init.");
super.init(config);
thriftServlet = new THServiceBuilder()
.withEventListener(
new CompositeServiceEventListener<>(
new ServiceEventLogListener(),
new HttpServiceEventLogListener()
)
)
.build(WebhookMessageServiceSrv.Iface.class, requestHandler);
}
@Override
public void service(ServletRequest req, ServletResponse res) throws ServletException, IOException {
log.info("Start new request to WebhookMessageServiceServlet.");
thriftServlet.service(req, res);
}
}

View File

@ -1,18 +0,0 @@
package dev.vality.hooker.handler;
import dev.vality.geck.filter.Filter;
import dev.vality.hooker.model.EventInfo;
import dev.vality.hooker.model.Message;
/**
* Created by inal on 24.11.2016.
*/
public interface Handler<C, M extends Message> {
default boolean accept(C change) {
return getFilter().match(change);
}
void handle(C change, EventInfo eventInfo);
Filter getFilter();
}

View File

@ -2,17 +2,14 @@ package dev.vality.hooker.handler;
import dev.vality.geck.filter.Filter;
import dev.vality.hooker.model.EventInfo;
import dev.vality.hooker.model.InvoicingMessageKey;
import dev.vality.hooker.model.Message;
import java.util.Map;
public interface Mapper<C, M extends Message> {
default boolean accept(C change) {
return getFilter().match(change);
}
M handle(C change, EventInfo eventInfo, Map<InvoicingMessageKey, M> storage);
M map(C change, EventInfo eventInfo);
Filter getFilter();
}

View File

@ -1,31 +1,24 @@
package dev.vality.hooker.handler.poller.customer;
package dev.vality.hooker.handler.customer;
import dev.vality.geck.filter.Filter;
import dev.vality.geck.filter.PathConditionFilter;
import dev.vality.geck.filter.condition.IsNullCondition;
import dev.vality.geck.filter.rule.PathConditionRule;
import dev.vality.hooker.dao.impl.CustomerDaoImpl;
import dev.vality.hooker.dao.impl.CustomerQueueDao;
import dev.vality.hooker.dao.impl.CustomerTaskDao;
import dev.vality.hooker.model.CustomerMessageEnum;
import dev.vality.hooker.model.EventType;
import org.springframework.stereotype.Component;
/**
* Created by inalarsanukaev on 12.10.17.
*/
@Component
public class CustomerBindingFailedHandler extends NeedReadCustomerEventHandler {
public class CustomerBindingFailedMapper extends NeedReadCustomerEventMapper {
private EventType eventType = EventType.CUSTOMER_BINDING_FAILED;
private Filter filter =
new PathConditionFilter(new PathConditionRule(eventType.getThriftPath(), new IsNullCondition().not()));
public CustomerBindingFailedHandler(CustomerDaoImpl customerDao,
CustomerQueueDao customerQueueDao,
CustomerTaskDao customerTaskDao) {
super(customerDao, customerQueueDao, customerTaskDao);
public CustomerBindingFailedMapper(CustomerDaoImpl customerDao) {
super(customerDao);
}
@Override

View File

@ -1,4 +1,4 @@
package dev.vality.hooker.handler.poller.customer;
package dev.vality.hooker.handler.customer;
import dev.vality.damsel.payment_processing.CustomerChange;
import dev.vality.geck.filter.Filter;
@ -6,29 +6,21 @@ import dev.vality.geck.filter.PathConditionFilter;
import dev.vality.geck.filter.condition.IsNullCondition;
import dev.vality.geck.filter.rule.PathConditionRule;
import dev.vality.hooker.dao.impl.CustomerDaoImpl;
import dev.vality.hooker.dao.impl.CustomerQueueDao;
import dev.vality.hooker.dao.impl.CustomerTaskDao;
import dev.vality.hooker.model.CustomerMessage;
import dev.vality.hooker.model.CustomerMessageEnum;
import dev.vality.hooker.model.EventType;
import org.springframework.stereotype.Component;
/**
* Created by inalarsanukaev on 12.10.17.
*/
@Component
public class CustomerBindingStartedHandler extends NeedReadCustomerEventHandler {
public class CustomerBindingStartedMapper extends NeedReadCustomerEventMapper {
private EventType eventType = EventType.CUSTOMER_BINDING_STARTED;
private Filter filter =
new PathConditionFilter(new PathConditionRule(eventType.getThriftPath(), new IsNullCondition().not()));
public CustomerBindingStartedHandler(CustomerDaoImpl customerDao,
CustomerQueueDao customerQueueDao,
CustomerTaskDao customerTaskDao) {
super(customerDao, customerQueueDao, customerTaskDao);
public CustomerBindingStartedMapper(CustomerDaoImpl customerDao) {
super(customerDao);
}
@Override

View File

@ -1,31 +1,24 @@
package dev.vality.hooker.handler.poller.customer;
package dev.vality.hooker.handler.customer;
import dev.vality.geck.filter.Filter;
import dev.vality.geck.filter.PathConditionFilter;
import dev.vality.geck.filter.condition.IsNullCondition;
import dev.vality.geck.filter.rule.PathConditionRule;
import dev.vality.hooker.dao.impl.CustomerDaoImpl;
import dev.vality.hooker.dao.impl.CustomerQueueDao;
import dev.vality.hooker.dao.impl.CustomerTaskDao;
import dev.vality.hooker.model.CustomerMessageEnum;
import dev.vality.hooker.model.EventType;
import org.springframework.stereotype.Component;
/**
* Created by inalarsanukaev on 12.10.17.
*/
@Component
public class CustomerBindingSucceededHandler extends NeedReadCustomerEventHandler {
public class CustomerBindingSucceededMapper extends NeedReadCustomerEventMapper {
private EventType eventType = EventType.CUSTOMER_BINDING_SUCCEEDED;
private Filter filter =
new PathConditionFilter(new PathConditionRule(eventType.getThriftPath(), new IsNullCondition().not()));
public CustomerBindingSucceededHandler(CustomerDaoImpl customerDao,
CustomerQueueDao customerQueueDao,
CustomerTaskDao customerTaskDao) {
super(customerDao, customerQueueDao, customerTaskDao);
public CustomerBindingSucceededMapper(CustomerDaoImpl customerDao) {
super(customerDao);
}
@Override

View File

@ -1,14 +1,12 @@
package dev.vality.hooker.handler.poller.customer;
package dev.vality.hooker.handler.customer;
import dev.vality.damsel.payment_processing.CustomerChange;
import dev.vality.geck.filter.Filter;
import dev.vality.geck.filter.PathConditionFilter;
import dev.vality.geck.filter.condition.IsNullCondition;
import dev.vality.geck.filter.rule.PathConditionRule;
import dev.vality.hooker.dao.impl.CustomerDaoImpl;
import dev.vality.hooker.dao.impl.CustomerQueueDao;
import dev.vality.hooker.dao.impl.CustomerTaskDao;
import dev.vality.hooker.exception.DaoException;
import dev.vality.hooker.handler.Mapper;
import dev.vality.hooker.model.CustomerMessage;
import dev.vality.hooker.model.CustomerMessageEnum;
import dev.vality.hooker.model.EventInfo;
@ -16,16 +14,10 @@ import dev.vality.hooker.model.EventType;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Component;
/**
* Created by inalarsanukaev on 12.10.17.
*/
@Component
@RequiredArgsConstructor
public class CustomerCreatedHandler extends AbstractCustomerEventHandler {
public class CustomerCreatedMapper implements Mapper<CustomerChange, CustomerMessage> {
private final CustomerDaoImpl customerDao;
private final CustomerQueueDao customerQueueDao;
private final CustomerTaskDao customerTaskDao;
private EventType eventType = EventType.CUSTOMER_CREATED;
private Filter filter =
new PathConditionFilter(new PathConditionRule(eventType.getThriftPath(), new IsNullCondition().not()));
@ -36,23 +28,17 @@ public class CustomerCreatedHandler extends AbstractCustomerEventHandler {
}
@Override
protected void saveEvent(CustomerChange cc, EventInfo eventInfo) throws DaoException {
public CustomerMessage map(CustomerChange cc, EventInfo eventInfo) throws DaoException {
dev.vality.damsel.payment_processing.CustomerCreated customerCreatedOrigin = cc.getCustomerCreated();
CustomerMessage customerMessage = new CustomerMessage();
customerMessage.setEventId(eventInfo.getEventId());
customerMessage.setEventTime(eventInfo.getEventCreatedAt());
customerMessage.setSequenceId(eventInfo.getSequenceId());
customerMessage.setChangeId(eventInfo.getChangeId());
customerMessage.setType(CustomerMessageEnum.CUSTOMER);
customerMessage.setPartyId(customerCreatedOrigin.getOwnerId());
customerMessage.setEventType(eventType);
customerMessage.setCustomerId(customerCreatedOrigin.getCustomerId());
customerMessage.setSourceId(customerCreatedOrigin.getCustomerId());
customerMessage.setShopId(customerCreatedOrigin.getShopId());
Long messageId = customerDao.create(customerMessage);
if (messageId != null) {
customerMessage.setId(messageId);
customerQueueDao.createWithPolicy(messageId);
customerTaskDao.create(messageId);
}
return customerMessage;
}
}

View File

@ -1,31 +1,24 @@
package dev.vality.hooker.handler.poller.customer;
package dev.vality.hooker.handler.customer;
import dev.vality.geck.filter.Filter;
import dev.vality.geck.filter.PathConditionFilter;
import dev.vality.geck.filter.condition.IsNullCondition;
import dev.vality.geck.filter.rule.PathConditionRule;
import dev.vality.hooker.dao.impl.CustomerDaoImpl;
import dev.vality.hooker.dao.impl.CustomerQueueDao;
import dev.vality.hooker.dao.impl.CustomerTaskDao;
import dev.vality.hooker.model.CustomerMessageEnum;
import dev.vality.hooker.model.EventType;
import org.springframework.stereotype.Component;
/**
* Created by inalarsanukaev on 12.10.17.
*/
@Component
public class CustomerDeletedHandler extends NeedReadCustomerEventHandler {
public class CustomerDeletedMapper extends NeedReadCustomerEventMapper {
private EventType eventType = EventType.CUSTOMER_DELETED;
private Filter filter =
new PathConditionFilter(new PathConditionRule(eventType.getThriftPath(), new IsNullCondition().not()));
public CustomerDeletedHandler(CustomerDaoImpl customerDao,
CustomerQueueDao customerQueueDao,
CustomerTaskDao customerTaskDao) {
super(customerDao, customerQueueDao, customerTaskDao);
public CustomerDeletedMapper(CustomerDaoImpl customerDao) {
super(customerDao);
}
@Override

View File

@ -1,31 +1,24 @@
package dev.vality.hooker.handler.poller.customer;
package dev.vality.hooker.handler.customer;
import dev.vality.geck.filter.Filter;
import dev.vality.geck.filter.PathConditionFilter;
import dev.vality.geck.filter.condition.IsNullCondition;
import dev.vality.geck.filter.rule.PathConditionRule;
import dev.vality.hooker.dao.impl.CustomerDaoImpl;
import dev.vality.hooker.dao.impl.CustomerQueueDao;
import dev.vality.hooker.dao.impl.CustomerTaskDao;
import dev.vality.hooker.model.CustomerMessageEnum;
import dev.vality.hooker.model.EventType;
import org.springframework.stereotype.Component;
/**
* Created by inalarsanukaev on 12.10.17.
*/
@Component
public class CustomerReadyHandler extends NeedReadCustomerEventHandler {
public class CustomerReadyMapper extends NeedReadCustomerEventMapper {
private EventType eventType = EventType.CUSTOMER_READY;
private Filter filter =
new PathConditionFilter(new PathConditionRule(eventType.getThriftPath(), new IsNullCondition().not()));
public CustomerReadyHandler(CustomerDaoImpl customerDao,
CustomerQueueDao customerQueueDao,
CustomerTaskDao customerTaskDao) {
super(customerDao, customerQueueDao, customerTaskDao);
public CustomerReadyMapper(CustomerDaoImpl customerDao) {
super(customerDao);
}
@Override

View File

@ -1,49 +1,33 @@
package dev.vality.hooker.handler.poller.customer;
package dev.vality.hooker.handler.customer;
import dev.vality.damsel.payment_processing.CustomerChange;
import dev.vality.hooker.dao.impl.CustomerDaoImpl;
import dev.vality.hooker.dao.impl.CustomerQueueDao;
import dev.vality.hooker.dao.impl.CustomerTaskDao;
import dev.vality.hooker.exception.DaoException;
import dev.vality.hooker.handler.Mapper;
import dev.vality.hooker.model.CustomerMessage;
import dev.vality.hooker.model.CustomerMessageEnum;
import dev.vality.hooker.model.EventInfo;
import dev.vality.hooker.model.EventType;
import lombok.RequiredArgsConstructor;
/**
* Created by inalarsanukaev on 12.10.17.
*/
@RequiredArgsConstructor
public abstract class NeedReadCustomerEventHandler extends AbstractCustomerEventHandler {
public abstract class NeedReadCustomerEventMapper implements Mapper<CustomerChange, CustomerMessage> {
protected final CustomerDaoImpl customerDao;
private final CustomerQueueDao customerQueueDao;
private final CustomerTaskDao customerTaskDao;
@Override
protected void saveEvent(CustomerChange cc, EventInfo eventInfo) throws DaoException {
//getAny any saved message for related invoice
public CustomerMessage map(CustomerChange cc, EventInfo eventInfo) throws DaoException {
CustomerMessage message = getCustomerMessage(eventInfo.getSourceId());
if (message == null) {
throw new DaoException("CustomerMessage for customer with id " + eventInfo.getSourceId() + " not exist");
}
message.setEventType(getEventType());
message.setType(getMessageType());
message.setEventId(eventInfo.getEventId());
message.setEventTime(eventInfo.getEventCreatedAt());
message.setSequenceId(eventInfo.getSequenceId());
message.setChangeId(eventInfo.getChangeId());
modifyMessage(cc, message);
Long messageId = customerDao.create(message);
if (messageId != null) {
message.setId(messageId);
customerQueueDao.createWithPolicy(messageId);
customerTaskDao.create(messageId);
}
return message;
}
protected CustomerMessage getCustomerMessage(String customerId) {

View File

@ -1,4 +1,4 @@
package dev.vality.hooker.handler.poller.invoicing;
package dev.vality.hooker.handler.invoicing;
import dev.vality.damsel.payment_processing.InvoiceChange;
import dev.vality.damsel.payment_processing.InvoicePayment;
@ -7,11 +7,7 @@ import dev.vality.geck.filter.PathConditionFilter;
import dev.vality.geck.filter.condition.IsNullCondition;
import dev.vality.geck.filter.rule.PathConditionRule;
import dev.vality.hooker.dao.InvoicingMessageDao;
import dev.vality.hooker.model.EventType;
import dev.vality.hooker.model.InvoicingMessage;
import dev.vality.hooker.model.InvoicingMessageEnum;
import dev.vality.hooker.model.InvoicingMessageKey;
import dev.vality.hooker.model.PaymentStatusEnum;
import dev.vality.hooker.model.*;
import dev.vality.hooker.service.HellgateInvoicingService;
import org.springframework.stereotype.Component;

View File

@ -1,4 +1,4 @@
package dev.vality.hooker.handler.poller.invoicing;
package dev.vality.hooker.handler.invoicing;
import dev.vality.damsel.domain.Invoice;
import dev.vality.damsel.payment_processing.InvoiceChange;
@ -7,19 +7,13 @@ import dev.vality.geck.filter.PathConditionFilter;
import dev.vality.geck.filter.condition.IsNullCondition;
import dev.vality.geck.filter.rule.PathConditionRule;
import dev.vality.hooker.exception.DaoException;
import dev.vality.hooker.model.EventInfo;
import dev.vality.hooker.model.EventType;
import dev.vality.hooker.model.InvoiceStatusEnum;
import dev.vality.hooker.model.InvoicingMessage;
import dev.vality.hooker.model.InvoicingMessageEnum;
import dev.vality.hooker.model.InvoicingMessageKey;
import dev.vality.hooker.handler.Mapper;
import dev.vality.hooker.model.*;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;
import java.util.Map;
@Component
public class InvoiceCreatedMapper extends AbstractInvoiceEventMapper {
public class InvoiceCreatedMapper implements Mapper<InvoiceChange, InvoicingMessage> {
private EventType eventType = EventType.INVOICE_CREATED;
@ -28,8 +22,7 @@ public class InvoiceCreatedMapper extends AbstractInvoiceEventMapper {
@Override
@Transactional
public InvoicingMessage buildEvent(InvoiceChange ic, EventInfo eventInfo,
Map<InvoicingMessageKey, InvoicingMessage> storage) throws DaoException {
public InvoicingMessage map(InvoiceChange ic, EventInfo eventInfo) throws DaoException {
Invoice invoiceOrigin = ic.getInvoiceCreated().getInvoice();
InvoicingMessage message = new InvoicingMessage();
message.setEventTime(eventInfo.getEventCreatedAt());
@ -38,7 +31,7 @@ public class InvoiceCreatedMapper extends AbstractInvoiceEventMapper {
message.setType(InvoicingMessageEnum.INVOICE);
message.setPartyId(invoiceOrigin.getOwnerId());
message.setEventType(eventType);
message.setInvoiceId(invoiceOrigin.getId());
message.setSourceId(invoiceOrigin.getId());
message.setShopId(invoiceOrigin.getShopId());
message.setInvoiceStatus(InvoiceStatusEnum.lookup(invoiceOrigin.getStatus().getSetField().getFieldName()));
return message;

View File

@ -1,4 +1,4 @@
package dev.vality.hooker.handler.poller.invoicing;
package dev.vality.hooker.handler.invoicing;
import dev.vality.damsel.domain.InvoicePaymentRefund;
import dev.vality.damsel.payment_processing.InvoiceChange;
@ -8,11 +8,7 @@ import dev.vality.geck.filter.PathConditionFilter;
import dev.vality.geck.filter.condition.IsNullCondition;
import dev.vality.geck.filter.rule.PathConditionRule;
import dev.vality.hooker.dao.InvoicingMessageDao;
import dev.vality.hooker.model.EventType;
import dev.vality.hooker.model.InvoicingMessage;
import dev.vality.hooker.model.InvoicingMessageEnum;
import dev.vality.hooker.model.InvoicingMessageKey;
import dev.vality.hooker.model.RefundStatusEnum;
import dev.vality.hooker.model.*;
import org.springframework.stereotype.Component;
@Component

View File

@ -1,4 +1,4 @@
package dev.vality.hooker.handler.poller.invoicing;
package dev.vality.hooker.handler.invoicing;
import dev.vality.damsel.payment_processing.InvoiceChange;
import dev.vality.geck.filter.Filter;
@ -6,11 +6,7 @@ import dev.vality.geck.filter.PathConditionFilter;
import dev.vality.geck.filter.condition.IsNullCondition;
import dev.vality.geck.filter.rule.PathConditionRule;
import dev.vality.hooker.dao.InvoicingMessageDao;
import dev.vality.hooker.model.EventType;
import dev.vality.hooker.model.InvoicingMessage;
import dev.vality.hooker.model.InvoicingMessageEnum;
import dev.vality.hooker.model.InvoicingMessageKey;
import dev.vality.hooker.model.RefundStatusEnum;
import dev.vality.hooker.model.*;
import org.springframework.stereotype.Component;
@Component

View File

@ -1,4 +1,4 @@
package dev.vality.hooker.handler.poller.invoicing;
package dev.vality.hooker.handler.invoicing;
import dev.vality.damsel.domain.InvoicePayment;
import dev.vality.damsel.payment_processing.InvoiceChange;
@ -7,11 +7,7 @@ import dev.vality.geck.filter.PathConditionFilter;
import dev.vality.geck.filter.condition.IsNullCondition;
import dev.vality.geck.filter.rule.PathConditionRule;
import dev.vality.hooker.dao.InvoicingMessageDao;
import dev.vality.hooker.model.EventType;
import dev.vality.hooker.model.InvoicingMessage;
import dev.vality.hooker.model.InvoicingMessageEnum;
import dev.vality.hooker.model.InvoicingMessageKey;
import dev.vality.hooker.model.PaymentStatusEnum;
import dev.vality.hooker.model.*;
import org.springframework.stereotype.Component;
@Component

View File

@ -1,4 +1,4 @@
package dev.vality.hooker.handler.poller.invoicing;
package dev.vality.hooker.handler.invoicing;
import dev.vality.damsel.payment_processing.InvoiceChange;
import dev.vality.geck.filter.Filter;
@ -6,11 +6,7 @@ import dev.vality.geck.filter.PathConditionFilter;
import dev.vality.geck.filter.condition.IsNullCondition;
import dev.vality.geck.filter.rule.PathConditionRule;
import dev.vality.hooker.dao.InvoicingMessageDao;
import dev.vality.hooker.model.EventType;
import dev.vality.hooker.model.InvoicingMessage;
import dev.vality.hooker.model.InvoicingMessageEnum;
import dev.vality.hooker.model.InvoicingMessageKey;
import dev.vality.hooker.model.PaymentStatusEnum;
import dev.vality.hooker.model.*;
import org.springframework.stereotype.Component;
@Component

View File

@ -1,4 +1,4 @@
package dev.vality.hooker.handler.poller.invoicing;
package dev.vality.hooker.handler.invoicing;
import dev.vality.damsel.payment_processing.InvoiceChange;
import dev.vality.geck.filter.Filter;
@ -6,11 +6,7 @@ import dev.vality.geck.filter.PathConditionFilter;
import dev.vality.geck.filter.condition.IsNullCondition;
import dev.vality.geck.filter.rule.PathConditionRule;
import dev.vality.hooker.dao.InvoicingMessageDao;
import dev.vality.hooker.model.EventType;
import dev.vality.hooker.model.InvoiceStatusEnum;
import dev.vality.hooker.model.InvoicingMessage;
import dev.vality.hooker.model.InvoicingMessageEnum;
import dev.vality.hooker.model.InvoicingMessageKey;
import dev.vality.hooker.model.*;
import org.springframework.stereotype.Component;
@Component

View File

@ -1,35 +1,26 @@
package dev.vality.hooker.handler.poller.invoicing;
package dev.vality.hooker.handler.invoicing;
import dev.vality.damsel.payment_processing.InvoiceChange;
import dev.vality.hooker.dao.InvoicingMessageDao;
import dev.vality.hooker.exception.DaoException;
import dev.vality.hooker.exception.NotFoundException;
import dev.vality.hooker.model.EventInfo;
import dev.vality.hooker.model.EventType;
import dev.vality.hooker.model.InvoicingMessage;
import dev.vality.hooker.model.InvoicingMessageEnum;
import dev.vality.hooker.model.InvoicingMessageKey;
import dev.vality.hooker.handler.Mapper;
import dev.vality.hooker.model.*;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import java.util.Map;
@Slf4j
@RequiredArgsConstructor
public abstract class NeedReadInvoiceEventMapper extends AbstractInvoiceEventMapper {
public abstract class NeedReadInvoiceEventMapper implements Mapper<InvoiceChange, InvoicingMessage> {
private final InvoicingMessageDao messageDao;
@Override
protected InvoicingMessage buildEvent(InvoiceChange ic, EventInfo eventInfo,
Map<InvoicingMessageKey, InvoicingMessage> storage) throws DaoException {
public InvoicingMessage map(InvoiceChange ic, EventInfo eventInfo) throws DaoException {
InvoicingMessage message;
InvoicingMessageKey messageKey = getMessageKey(eventInfo.getSourceId(), ic);
try {
message = storage.get(messageKey);
if (message == null) {
message = messageDao.getInvoicingMessage(messageKey);
}
message = message.copy();
} catch (NotFoundException e) {
log.warn(e.getMessage());

View File

@ -1,20 +0,0 @@
package dev.vality.hooker.handler.poller.customer;
import dev.vality.damsel.payment_processing.CustomerChange;
import dev.vality.hooker.exception.DaoException;
import dev.vality.hooker.handler.Handler;
import dev.vality.hooker.model.CustomerMessage;
import dev.vality.hooker.model.EventInfo;
/**
* Created by inalarsanukaev on 07.04.17.
*/
public abstract class AbstractCustomerEventHandler implements Handler<CustomerChange, CustomerMessage> {
@Override
public void handle(CustomerChange c, EventInfo eventInfo) throws DaoException {
saveEvent(c, eventInfo);
}
protected abstract void saveEvent(CustomerChange cc, EventInfo eventInfo) throws DaoException;
}

View File

@ -1,23 +0,0 @@
package dev.vality.hooker.handler.poller.invoicing;
import dev.vality.damsel.payment_processing.InvoiceChange;
import dev.vality.hooker.exception.DaoException;
import dev.vality.hooker.handler.Mapper;
import dev.vality.hooker.model.EventInfo;
import dev.vality.hooker.model.InvoicingMessage;
import dev.vality.hooker.model.InvoicingMessageKey;
import java.util.Map;
public abstract class AbstractInvoiceEventMapper implements Mapper<InvoiceChange, InvoicingMessage> {
@Override
public InvoicingMessage handle(InvoiceChange ic, EventInfo eventInfo,
Map<InvoicingMessageKey, InvoicingMessage> storage) throws DaoException {
return buildEvent(ic, eventInfo, storage);
}
protected abstract InvoicingMessage buildEvent(InvoiceChange ic, EventInfo eventInfo,
Map<InvoicingMessageKey, InvoicingMessage> storage)
throws DaoException;
}

View File

@ -2,11 +2,10 @@ package dev.vality.hooker.listener;
import dev.vality.damsel.payment_processing.CustomerChange;
import dev.vality.damsel.payment_processing.EventPayload;
import dev.vality.geck.serializer.kit.json.JsonHandler;
import dev.vality.geck.serializer.kit.tbase.TBaseProcessor;
import dev.vality.hooker.handler.Handler;
import dev.vality.hooker.handler.poller.customer.AbstractCustomerEventHandler;
import dev.vality.hooker.handler.Mapper;
import dev.vality.hooker.model.CustomerMessage;
import dev.vality.hooker.model.EventInfo;
import dev.vality.hooker.service.MessageService;
import dev.vality.machinegun.eventsink.MachineEvent;
import dev.vality.sink.common.parser.impl.MachineEventParser;
import lombok.RequiredArgsConstructor;
@ -23,48 +22,39 @@ import java.util.List;
public class CustomerMachineEventHandler implements MachineEventHandler {
private final MachineEventParser<EventPayload> parser;
private final List<AbstractCustomerEventHandler> pollingEventHandlers;
private final List<Mapper<CustomerChange, CustomerMessage>> customerEventMappers;
private final MessageService<CustomerMessage> customerMessageService;
@Override
@Transactional
public void handle(List<MachineEvent> machineEvents, Acknowledgment ack) {
for (MachineEvent machineEvent : machineEvents) {
EventPayload payload = parser.parse(machineEvent);
if (!payload.isSetCustomerChanges()) {
return;
}
List<CustomerChange> changes = payload.getCustomerChanges();
for (int i = 0; i < changes.size(); ++i) {
preparePollingHandlers(changes.get(i), machineEvent, i);
}
machineEvents.forEach(me -> {
EventPayload payload = parser.parse(me);
if (payload.isSetCustomerChanges()) {
handleChanges(me, payload);
}
});
ack.acknowledge();
}
private void preparePollingHandlers(CustomerChange cc, MachineEvent machineEvent, int i) {
pollingEventHandlers.stream()
.filter(handler -> handler.accept(cc))
private void handleChanges(MachineEvent me, EventPayload payload) {
for (int i = 0; i < payload.getCustomerChanges().size(); ++i) {
CustomerChange customerChange = payload.getCustomerChanges().get(i);
handleChange(me, customerChange, i);
}
}
private void handleChange(MachineEvent me, CustomerChange customerChange, int i) {
customerEventMappers.stream()
.filter(handler -> handler.accept(customerChange))
.findFirst()
.ifPresent(handler -> processEvent(handler, cc, machineEvent, i));
.ifPresent(handler -> {
log.info("Start to handle event {}", customerChange);
var eventInfo = new EventInfo(me.getCreatedAt(), me.getSourceId(), me.getEventId(), i);
CustomerMessage message = handler.map(customerChange, eventInfo);
if (message != null) {
customerMessageService.process(message);
}
private void processEvent(Handler pollingEventHandler, Object cc, MachineEvent machineEvent, int i) {
long id = machineEvent.getEventId();
try {
log.info("We got an event {}", new TBaseProcessor()
.process(machineEvent, JsonHandler.newPrettyJsonInstance()));
EventInfo eventInfo = new EventInfo(
machineEvent.getEventId(),
machineEvent.getCreatedAt(),
machineEvent.getSourceId(),
machineEvent.getEventId(),
i
);
pollingEventHandler.handle(cc, eventInfo);
} catch (Exception e) {
log.error("Error when poller handling with id {}", id, e);
});
}
}
}

View File

@ -2,12 +2,11 @@ package dev.vality.hooker.listener;
import dev.vality.damsel.payment_processing.EventPayload;
import dev.vality.damsel.payment_processing.InvoiceChange;
import dev.vality.hooker.handler.Mapper;
import dev.vality.hooker.model.EventInfo;
import dev.vality.hooker.model.InvoicingMessage;
import dev.vality.hooker.model.InvoicingMessageKey;
import dev.vality.hooker.service.BatchService;
import dev.vality.hooker.service.HandlerManager;
import dev.vality.hooker.utils.KeyUtils;
import dev.vality.hooker.service.MessageService;
import dev.vality.machinegun.eventsink.MachineEvent;
import dev.vality.sink.common.parser.impl.MachineEventParser;
import lombok.RequiredArgsConstructor;
@ -16,47 +15,47 @@ import org.springframework.kafka.support.Acknowledgment;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@Slf4j
@Component
@RequiredArgsConstructor
public class InvoicingMachineEventHandler implements MachineEventHandler {
private final HandlerManager handlerManager;
private final List<Mapper<InvoiceChange, InvoicingMessage>> handlers;
private final MachineEventParser<EventPayload> parser;
private final BatchService batchService;
private final MessageService<InvoicingMessage> invoicingMessageService;
@Override
@Transactional
public void handle(List<MachineEvent> machineEvents, Acknowledgment ack) {
List<InvoicingMessage> messages = new ArrayList<>();
Map<InvoicingMessageKey, InvoicingMessage> localCache = new HashMap<>();
machineEvents.forEach(me -> {
EventPayload payload = parser.parse(me);
if (payload.isSetInvoiceChanges()) {
for (int i = 0; i < payload.getInvoiceChanges().size(); ++i) {
InvoiceChange invoiceChange = payload.getInvoiceChanges().get(i);
int j = i;
handlerManager.getHandler(invoiceChange).ifPresent(handler -> {
log.info("Start to handle event {}", invoiceChange);
InvoicingMessage message = handler.handle(invoiceChange,
new EventInfo(null, me.getCreatedAt(), me.getSourceId(), me.getEventId(), j),
localCache);
if (message != null) {
localCache.put(KeyUtils.key(message), message);
messages.add(message);
handleChanges(me, payload);
}
});
}
}
});
if (!localCache.isEmpty()) {
batchService.process(messages);
}
ack.acknowledge();
}
private void handleChanges(MachineEvent me, EventPayload payload) {
for (int i = 0; i < payload.getInvoiceChanges().size(); ++i) {
InvoiceChange invoiceChange = payload.getInvoiceChanges().get(i);
handleChange(me, invoiceChange, i);
}
}
private void handleChange(MachineEvent me, InvoiceChange invoiceChange, int i) {
handlers.stream()
.filter(handler -> handler.accept(invoiceChange))
.findFirst()
.ifPresent(handler -> {
log.info("Start to handle event {}", invoiceChange);
var eventInfo = new EventInfo(me.getCreatedAt(), me.getSourceId(), me.getEventId(), i);
InvoicingMessage message = handler.map(invoiceChange, eventInfo);
if (message != null) {
invoicingMessageService.process(message);
}
});
}
}

View File

@ -1,285 +0,0 @@
package dev.vality.hooker.logging;
import lombok.extern.slf4j.Slf4j;
import okhttp3.Connection;
import okhttp3.Headers;
import okhttp3.Interceptor;
import okhttp3.MediaType;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.RequestBody;
import okhttp3.Response;
import okhttp3.ResponseBody;
import okhttp3.internal.http.HttpHeaders;
import okio.Buffer;
import okio.BufferedSource;
import java.io.EOFException;
import java.io.IOException;
import java.nio.charset.Charset;
import java.util.concurrent.TimeUnit;
/**
* An OkHttp interceptor which logs request and response information. Can be applied as an
* {@linkplain OkHttpClient#interceptors() application interceptor} or as a {@linkplain
* OkHttpClient#networkInterceptors() network interceptor}. <p> The format of the logs created by
* this class should not be considered stable and may change slightly between releases. If you need
* a stable logging format, use your own interceptor.
*/
@Slf4j
public final class HttpLoggingInterceptor implements Interceptor {
private static final Charset UTF8 = Charset.forName("UTF-8");
private final Logger logger;
private volatile Level level = Level.BASIC;
public HttpLoggingInterceptor() {
this(Logger.DEFAULT);
}
public HttpLoggingInterceptor(Logger logger) {
this.logger = logger;
}
/**
* Returns true if the body in question probably contains human readable text. Uses a small sample
* of code points to detect unicode control characters commonly used in binary file signatures.
*/
static boolean isPlaintext(Buffer buffer) {
try {
Buffer prefix = new Buffer();
long byteCount = buffer.size() < 64 ? buffer.size() : 64;
buffer.copyTo(prefix, 0, byteCount);
for (int i = 0; i < 16; i++) {
if (prefix.exhausted()) {
break;
}
int codePoint = prefix.readUtf8CodePoint();
if (Character.isISOControl(codePoint) && !Character.isWhitespace(codePoint)) {
return false;
}
}
return true;
} catch (EOFException e) {
return false; // Truncated UTF-8 sequence.
}
}
public Level getLevel() {
return level;
}
/**
* Change the level at which this interceptor logs.
*/
public HttpLoggingInterceptor setLevel(Level level) {
if (level == null) {
throw new NullPointerException("level == null. Use Level.NONE instead.");
}
this.level = level;
return this;
}
@Override
public Response intercept(Chain chain) throws IOException {
Level level = this.level;
Request request = chain.request();
if (level == Level.NONE) {
return chain.proceed(request);
}
boolean logBody = level == Level.BODY;
boolean logHeaders = logBody || level == Level.HEADERS;
RequestBody requestBody = request.body();
boolean hasRequestBody = requestBody != null;
Connection connection = chain.connection();
StringBuilder message = new StringBuilder("--> \n")
.append(request.method())
.append(' ').append(request.url())
.append(connection != null ? " " + connection.protocol() : "").append('\n');
if (!logHeaders && hasRequestBody) {
message.append(" (").append(requestBody.contentLength()).append("-byte body)").append('\n');
}
//logger.log(requestStartMessage);
if (logHeaders) {
if (hasRequestBody) {
// Request body headers are only present when installed as a network interceptor. Force
// them to be included (when available) so there values are known.
if (requestBody.contentType() != null) {
message.append("Content-Type: ").append(requestBody.contentType()).append('\n');
}
if (requestBody.contentLength() != -1) {
message.append("Content-Length: ").append(requestBody.contentLength()).append('\n');
}
}
Headers headers = request.headers();
for (int i = 0, count = headers.size(); i < count; i++) {
String name = headers.name(i);
// Skip headers from the request body as they are explicitly logged above.
if (!"Authorization".equalsIgnoreCase(name)) {
message.append(name).append(": ").append(headers.value(i)).append('\n');
}
}
if (!logBody || !hasRequestBody) {
message.append("\n--> END ").append(request.method());
} else if (bodyEncoded(request.headers())) {
message.append("\n--> END ").append(request.method()).append(" (encoded body omitted)");
} else {
message.append('\n');
Buffer buffer = new Buffer();
requestBody.writeTo(buffer);
Charset charset = UTF8;
MediaType contentType = requestBody.contentType();
if (contentType != null) {
charset = contentType.charset(UTF8);
}
if (isPlaintext(buffer)) {
message.append(buffer.readString(charset));
message.append("\n--> END ").append(request.method())
.append(" (").append(requestBody.contentLength()).append("-byte body)");
} else {
message.append("\n--> END ").append(request.method()).append(" (binary ")
.append(requestBody.contentLength()).append("-byte body omitted)");
}
}
}
logger.log(message.toString());
long startNs = System.nanoTime();
Response response;
try {
response = chain.proceed(request);
} catch (Exception e) {
logger.log("<-- HTTP FAILED: " + e);
throw e;
}
long tookMs = TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - startNs);
ResponseBody responseBody = response.body();
long contentLength = responseBody.contentLength();
message = new StringBuilder();
String bodySize = contentLength != -1 ? contentLength + "-byte" : "unknown-length";
message.append("<-- \n")
.append(response.code())
.append(response.message().isEmpty() ? "" : " " + response.message())
.append(' ').append(response.request().url())
.append(" (").append(tookMs).append("ms").append((!logHeaders ? ", " + bodySize + " body" : "") + ')')
.append('\n');
if (logHeaders) {
Headers headers = response.headers();
for (int i = 0, count = headers.size(); i < count; i++) {
message.append(headers.name(i)).append(": ").append(headers.value(i)).append('\n');
}
if (!logBody || !HttpHeaders.hasBody(response)) {
message.append("\n<-- END HTTP");
} else if (bodyEncoded(response.headers())) {
message.append("\n<-- END HTTP (encoded body omitted)");
} else {
message.append('\n');
BufferedSource source = responseBody.source();
source.request(Long.MAX_VALUE); // Buffer the entire body.
Buffer buffer = source.buffer();
Charset charset = UTF8;
MediaType contentType = responseBody.contentType();
if (contentType != null) {
charset = contentType.charset(UTF8);
}
if (!isPlaintext(buffer)) {
message.append("\n<-- END HTTP (binary ").append(buffer.size()).append("-byte body omitted)");
return response;
}
if (contentLength != 0) {
message.append(buffer.clone().readString(charset));
}
logger.log(message.append("\n<-- END HTTP (").append(buffer.size()).append("-byte body)").toString());
}
}
return response;
}
private boolean bodyEncoded(Headers headers) {
String contentEncoding = headers.get("Content-Encoding");
return contentEncoding != null && !contentEncoding.equalsIgnoreCase("identity");
}
public enum Level {
/**
* No logs.
*/
NONE,
/**
* Logs request and response lines.
* <p>
* <p>Example:
* <pre>{@code
* --> POST /greeting http/1.1 (3-byte body)
*
* <-- 200 OK (22ms, 6-byte body)
* }</pre>
*/
BASIC,
/**
* Logs request and response lines and their respective headers.
* <p>
* <p>Example:
* <pre>{@code
* --> POST /greeting http/1.1
* Host: example.com
* Content-Type: plain/text
* Content-Length: 3
* --> END POST
*
* <-- 200 OK (22ms)
* Content-Type: plain/text
* Content-Length: 6
* <-- END HTTP
* }</pre>
*/
HEADERS,
/**
* Logs request and response lines and their respective headers and bodies (if present).
* <p>
* <p>Example:
* <pre>{@code
* --> POST /greeting http/1.1
* Host: example.com
* Content-Type: plain/text
* Content-Length: 3
*
* Hi?
* --> END POST
*
* <-- 200 OK (22ms)
* Content-Type: plain/text
* Content-Length: 6
*
* Hello!
* <-- END HTTP
* }</pre>
*/
BODY
}
public interface Logger {
/**
* A {@link Logger} defaults output appropriate for the current platform.
*/
Logger DEFAULT = log::info;
void log(String message);
}
}

View File

@ -5,16 +5,12 @@ import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.ToString;
/**
* Created by inalarsanukaev on 13.10.17.
*/
@Data
@AllArgsConstructor
@NoArgsConstructor
@ToString
public class CustomerMessage extends Message {
private CustomerMessageEnum type;
private String customerId;
private String bindingId;
public boolean isBinding() {

View File

@ -3,9 +3,6 @@ package dev.vality.hooker.model;
import lombok.Getter;
import lombok.Setter;
/**
* Created by inalarsanukaev on 14.11.17.
*/
@Getter
@Setter
public class CustomerQueue extends Queue {

View File

@ -6,7 +6,6 @@ import lombok.Data;
@Data
@AllArgsConstructor
public class EventInfo {
private Long eventId;
private String eventCreatedAt;
private String sourceId;
private Long sequenceId;

View File

@ -1,38 +1,26 @@
package dev.vality.hooker.model;
import dev.vality.hooker.dao.WebhookAdditionalFilter;
import dev.vality.hooker.retry.RetryPolicyType;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.*;
import java.util.Set;
/**
* Created by jeckep on 13.04.17.
*/
@Data
@Builder
@ToString
@AllArgsConstructor
@NoArgsConstructor
public class Hook {
private long id;
private String partyId;
private String topic;
@ToString.Exclude
private Set<WebhookAdditionalFilter> filters;
private String url;
@ToString.Exclude
private String pubKey;
@ToString.Exclude
private String privKey;
private boolean enabled;
private double availability;
private RetryPolicyType retryPolicyType;
@Override
public String toString() {
return "Hook{" +
"id=" + id +
", topic=" + topic +
", partyId='" + partyId + '\'' +
", url='" + url + '\'' +
'}';
}
}

View File

@ -5,17 +5,12 @@ import lombok.NoArgsConstructor;
import lombok.ToString;
import org.springframework.beans.BeanUtils;
/**
* Created by inalarsanukaev on 07.04.17.
*/
@NoArgsConstructor
@Data
@ToString
public class InvoicingMessage extends Message {
private InvoicingMessageEnum type;
private String invoiceId;
private InvoiceStatusEnum invoiceStatus;
private String paymentId;
private PaymentStatusEnum paymentStatus;

View File

@ -12,7 +12,7 @@ public enum InvoicingMessageEnum {
PAYMENT("payment"),
REFUND("refund");
private String value;
private final String value;
public static InvoicingMessageEnum lookup(String v) {
return Arrays.stream(values()).filter(value -> v.equals(value.getValue())).findFirst()

View File

@ -1,10 +1,6 @@
package dev.vality.hooker.model;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.ToString;
import lombok.*;
@Data
@AllArgsConstructor

View File

@ -3,9 +3,6 @@ package dev.vality.hooker.model;
import lombok.Getter;
import lombok.Setter;
/**
* Created by inalarsanukaev on 14.11.17.
*/
@Getter
@Setter
public class InvoicingQueue extends Queue {

View File

@ -3,17 +3,14 @@ package dev.vality.hooker.model;
import lombok.Getter;
import lombok.Setter;
/**
* Created by inalarsanukaev on 20.11.17.
*/
@Getter
@Setter
public class Message {
private Long id;
private Long eventId;
private Long sequenceId;
private Integer changeId;
private String eventTime;
private String sourceId;
private String partyId;
private String shopId;
private EventType eventType;

View File

@ -1,14 +0,0 @@
package dev.vality.hooker.model;
import dev.vality.swag_webhook_events.model.DigitalWalletDetails;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.Setter;
@Getter
@Setter
@AllArgsConstructor
public class PaymentToolDetailsDigitalWallet
extends dev.vality.swag_webhook_events.model.PaymentToolDetailsDigitalWallet {
private DigitalWalletDetails digitalWalletDetails;
}

View File

@ -1,6 +1,5 @@
package dev.vality.hooker.model;
import dev.vality.hooker.retry.RetryPolicyRecord;
import lombok.Data;
import lombok.NoArgsConstructor;
@ -9,5 +8,4 @@ import lombok.NoArgsConstructor;
public class Queue {
private long id;
private Hook hook;
private RetryPolicyRecord retryPolicyRecord;
}

View File

@ -1,15 +0,0 @@
package dev.vality.hooker.model;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.Setter;
import lombok.ToString;
@AllArgsConstructor
@Getter
@Setter
@ToString
public class Task {
long messageId;
long queueId;
}

View File

@ -0,0 +1,11 @@
package dev.vality.hooker.model;
import lombok.Data;
@Data
public class WebhookMessageModel<T extends Message> {
private T message;
private Long hookId;
private String url;
private String privateKey;
}

View File

@ -1,36 +0,0 @@
package dev.vality.hooker.retry;
import dev.vality.hooker.dao.SimpleRetryPolicyDao;
import dev.vality.hooker.retry.impl.simple.SimpleRetryPolicy;
import dev.vality.hooker.retry.impl.simple.SimpleRetryPolicyRecord;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
/**
* Created by jeckep on 18.04.17.
*/
@Service
@RequiredArgsConstructor
public class RetryPoliciesService {
private final SimpleRetryPolicy simpleRetryPolicy;
private final SimpleRetryPolicyDao simpleRetryPolicyDao;
public RetryPolicy getRetryPolicyByType(RetryPolicyType type) {
if (RetryPolicyType.SIMPLE.equals(type)) {
return simpleRetryPolicy;
} else {
throw new UnsupportedOperationException("Retry policy for type: " + type.toString() + " not found");
}
}
public void update(RetryPolicyRecord record) {
if (RetryPolicyType.SIMPLE.equals(record.getType())) {
simpleRetryPolicyDao.update((SimpleRetryPolicyRecord) record);
} else {
throw new UnsupportedOperationException(
"Retry policy DAO for type: " + record.getType().toString() + " not found");
}
}
}

View File

@ -1,12 +0,0 @@
package dev.vality.hooker.retry;
/**
* Created by jeckep on 17.04.17.
*/
public interface RetryPolicy<T> {
boolean shouldDisable(T record);
RetryPolicyType getType();
void updateFailed(T record);
}

View File

@ -1,12 +0,0 @@
package dev.vality.hooker.retry;
/**
* Created by jeckep on 18.04.17.
*/
public abstract class RetryPolicyRecord {
public abstract boolean isFailed();
public abstract void reset();
public abstract RetryPolicyType getType();
}

View File

@ -1,38 +0,0 @@
package dev.vality.hooker.retry;
import dev.vality.hooker.retry.impl.simple.SimpleRetryPolicyRecord;
import java.sql.ResultSet;
import java.sql.SQLException;
/**
* Created by jeckep on 17.04.17.
*/
public enum RetryPolicyType {
/*
* Первая и самая простая политика переотправки.
* Если хук не отвечает или отвечает с ошибкой,
* пробуем 4 раза с интервалами 30сек, 5мин, 15мин, ас опять послать
* неотправленное сообщение в этот хук. При этом очередь сообщений для хука копится.
* После первой удачной отправки, после неудачной, счетчик неудачных попыток сбрасывается.
* */
SIMPLE {
@Override
public RetryPolicyRecord build(ResultSet rs) throws SQLException {
SimpleRetryPolicyRecord record = new SimpleRetryPolicyRecord();
record.setQueueId(rs.getLong("id"));
record.setMessageType(rs.getString("message_type"));
record.setFailCount(rs.getInt("fail_count"));
record.setLastFailTime(rs.getLong("last_fail_time"));
record.setNextFireTime(rs.getLong("next_fire_time_ms"));
return record;
}
public SimpleRetryPolicyRecord cast(RetryPolicyRecord record) {
return (SimpleRetryPolicyRecord) record;
}
};
public abstract RetryPolicyRecord build(ResultSet rs) throws SQLException;
}

View File

@ -1,40 +0,0 @@
package dev.vality.hooker.retry.impl.simple;
import dev.vality.hooker.retry.RetryPolicy;
import dev.vality.hooker.retry.RetryPolicyType;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Component;
/**
* Created by jeckep on 17.04.17.
*/
@Component
@RequiredArgsConstructor
public class SimpleRetryPolicy implements RetryPolicy<SimpleRetryPolicyRecord> {
private long[] delays = {30, 300, 900, 3600,
3600, 3600, 3600, 3600, 3600, 3600, 3600, 3600, 3600, 3600,
3600, 3600, 3600, 3600, 3600, 3600, 3600, 3600, 3600, 3600,
3600, 3600, 3600, 3600
}; //in seconds
@Override
public RetryPolicyType getType() {
return RetryPolicyType.SIMPLE;
}
@Override
public void updateFailed(SimpleRetryPolicyRecord record) {
record.setFailCount(record.getFailCount() + 1);
record.setLastFailTime(System.currentTimeMillis());
if (record.getFailCount() <= delays.length) {
record.setNextFireTime(record.getLastFailTime() + (delays[record.getFailCount() - 1] * 1000));
}
}
@Override
public boolean shouldDisable(SimpleRetryPolicyRecord rp) {
return rp.getFailCount() > delays.length;
}
}

View File

@ -1,41 +0,0 @@
package dev.vality.hooker.retry.impl.simple;
import dev.vality.hooker.retry.RetryPolicyRecord;
import dev.vality.hooker.retry.RetryPolicyType;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import lombok.ToString;
@ToString
@AllArgsConstructor
@NoArgsConstructor
@Getter
@Setter
public class SimpleRetryPolicyRecord extends RetryPolicyRecord {
public static RetryPolicyType type = RetryPolicyType.SIMPLE;
private Long queueId;
private String messageType;
private Integer failCount;
private Long lastFailTime;
private Long nextFireTime;
@Override
public boolean isFailed() {
return failCount > 0;
}
@Override
public void reset() {
failCount = 0;
lastFailTime = null;
nextFireTime = null;
}
@Override
public RetryPolicyType getType() {
return type;
}
}

View File

@ -1,27 +0,0 @@
package dev.vality.hooker.scheduler;
import dev.vality.hooker.model.Message;
import dev.vality.hooker.model.Queue;
import dev.vality.hooker.service.MessageProcessor;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler;
import javax.annotation.PostConstruct;
import java.util.stream.IntStream;
@Slf4j
@RequiredArgsConstructor
public class MessageScheduler<M extends Message, Q extends Queue> {
private final int threadPoolSize;
private final int delayMillis;
private final MessageProcessor<M, Q> messageProcessor;
private final ThreadPoolTaskScheduler executorService;
@PostConstruct
public void init() {
IntStream.range(0, threadPoolSize).forEach(i ->
executorService.scheduleWithFixedDelay(messageProcessor, delayMillis));
}
}

View File

@ -1,77 +0,0 @@
package dev.vality.hooker.scheduler;
import com.fasterxml.jackson.databind.ObjectMapper;
import dev.vality.hooker.model.Message;
import dev.vality.hooker.model.Queue;
import dev.vality.hooker.model.QueueStatus;
import dev.vality.hooker.model.Task;
import dev.vality.hooker.service.EventService;
import dev.vality.hooker.service.FaultDetectorService;
import dev.vality.hooker.service.PostSender;
import dev.vality.hooker.service.crypt.Signer;
import dev.vality.hooker.service.err.PostRequestException;
import dev.vality.swag_webhook_events.model.Event;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.http.HttpStatus;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
@Slf4j
@RequiredArgsConstructor
public class MessageSender<M extends Message, Q extends Queue> {
private final int connectionPoolSize;
private final int timeout;
private final Signer signer;
private final EventService<M> eventService;
private final ObjectMapper objectMapper;
private final FaultDetectorService faultDetector;
public List<QueueStatus> send(Map<Long, List<Task>> scheduledTasks, Map<Long, Q> queuesMap,
Map<Long, M> messagesMap) {
PostSender postSender = new PostSender(connectionPoolSize, timeout);
List<QueueStatus> queueStatuses = new ArrayList<>();
for (Map.Entry<Long, List<Task>> entry : scheduledTasks.entrySet()) {
Long queueId = entry.getKey();
List<Task> tasks = entry.getValue();
Q queue = queuesMap.get(queueId);
QueueStatus queueStatus = new QueueStatus();
queueStatus.setQueue(queue);
M currentMessage = null;
try {
for (Task task : tasks) {
long messageId = task.getMessageId();
M message = messagesMap.get(messageId);
currentMessage = message;
Event event = eventService.getEventByMessage(message);
final String messageJson = objectMapper.writeValueAsString(event);
final String signature = signer.sign(messageJson, queue.getHook().getPrivKey());
faultDetector.startRequest(queue.getHook().getId(), message.getEventId());
int statusCode =
postSender.doPost(queue.getHook().getUrl(), message.getId(), messageJson, signature);
if (statusCode != HttpStatus.SC_OK) {
String wrongCodeMessage = String.format(
"Wrong status code: %d from merchant, we'll try to resend it. Message with id: %d %s",
statusCode, message.getId(), message);
log.info(wrongCodeMessage);
faultDetector.errorRequest(queue.getHook().getId(), message.getEventId());
throw new PostRequestException(wrongCodeMessage);
}
faultDetector.finishRequest(queue.getHook().getId(), message.getEventId());
queueStatus.getMessagesDone().add(message.getId());
}
queueStatus.setSuccess(true);
} catch (Exception e) {
if (currentMessage != null) {
log.warn("Couldn't send message with id {} {} to hook {}. We'll try to resend it",
currentMessage.getId(), currentMessage, queue.getHook(), e);
}
queueStatus.setSuccess(false);
}
queueStatuses.add(queueStatus);
}
return queueStatuses;
}
}

View File

@ -0,0 +1,16 @@
package dev.vality.hooker.service;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import java.util.Map;
@Service
@RequiredArgsConstructor
public class AdditionalHeadersGenerator {
public static final String SIGNATURE_HEADER = "Content-Signature";
public Map<String, String> generate(String signature) {
return Map.of(SIGNATURE_HEADER, "alg=RS256; digest=" + signature);
}
}

View File

@ -1,42 +0,0 @@
package dev.vality.hooker.service;
import dev.vality.hooker.dao.impl.InvoicingMessageDaoImpl;
import dev.vality.hooker.dao.impl.InvoicingQueueDao;
import dev.vality.hooker.dao.impl.InvoicingTaskDao;
import dev.vality.hooker.dao.impl.MessageIdsGeneratorDaoImpl;
import dev.vality.hooker.model.InvoicingMessage;
import dev.vality.hooker.model.Message;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import java.util.List;
import java.util.stream.Collectors;
@Service
@Slf4j
@RequiredArgsConstructor
public class BatchService {
private final InvoicingMessageDaoImpl invoicingMessageDao;
private final InvoicingQueueDao invoicingQueueDao;
private final InvoicingTaskDao invoicingTaskDao;
private final MessageIdsGeneratorDaoImpl messageIdsGeneratorDao;
public void process(List<InvoicingMessage> messages) {
log.info("Start processing of batch, size={}", messages.size());
List<Long> ids = messageIdsGeneratorDao.get(messages.size());
List<Long> eventIds = messageIdsGeneratorDao.get(messages.size());
for (int i = 0; i < messages.size(); ++i) {
messages.get(i).setId(ids.get(i));
messages.get(i).setEventId(eventIds.get(i));
}
invoicingMessageDao.saveBatch(messages);
List<Long> messageIds = messages.stream().map(Message::getId).collect(Collectors.toList());
int[] queueBatchResult = invoicingQueueDao.saveBatchWithPolicies(messageIds);
log.info("Queue batch size={}", queueBatchResult.length);
int taskInsertResult = invoicingTaskDao.save(messageIds);
log.info("Task insert size={}", taskInsertResult);
log.info("End processing of batch");
}
}

View File

@ -12,13 +12,7 @@ import dev.vality.hooker.exception.RemoteHostException;
import dev.vality.hooker.model.CustomerMessage;
import dev.vality.hooker.utils.HellgateUtils;
import dev.vality.hooker.utils.TimeUtils;
import dev.vality.swag_webhook_events.model.CustomerBindingFailed;
import dev.vality.swag_webhook_events.model.CustomerBindingStarted;
import dev.vality.swag_webhook_events.model.CustomerBindingSucceeded;
import dev.vality.swag_webhook_events.model.CustomerCreated;
import dev.vality.swag_webhook_events.model.CustomerDeleted;
import dev.vality.swag_webhook_events.model.CustomerReady;
import dev.vality.swag_webhook_events.model.Event;
import dev.vality.swag_webhook_events.model.*;
import dev.vality.woody.api.flow.WFlow;
import dev.vality.woody.api.trace.ContextUtils;
import lombok.RequiredArgsConstructor;
@ -38,17 +32,17 @@ public class CustomerEventService implements EventService<CustomerMessage> {
try {
Customer customer = woodyFlow.createServiceFork(() -> {
addWoodyContext();
return customerClient.get(message.getCustomerId(),
return customerClient.get(message.getSourceId(),
HellgateUtils.getEventRange(message.getSequenceId().intValue()));
}
).call();
return resolveEvent(message, customer)
.eventID(message.getEventId().intValue())
.eventID(message.getId().intValue())
.occuredAt(TimeUtils.toOffsetDateTime(message.getEventTime()))
.topic(Event.TopicEnum.CUSTOMERSTOPIC);
} catch (CustomerNotFound e) {
throw new NotFoundException("Customer not found, invoiceId=" + message.getCustomerId());
throw new NotFoundException("Customer not found, invoiceId=" + message.getSourceId());
} catch (Exception e) {
throw new RemoteHostException(e);
}
@ -93,6 +87,6 @@ public class CustomerEventService implements EventService<CustomerMessage> {
.findFirst()
.orElseThrow(() ->
new NotFoundException(String.format("Customer binding not found, customerId=%s, bindingId=%s",
message.getCustomerId(), message.getBindingId())));
message.getSourceId(), message.getBindingId())));
}
}

View File

@ -1,20 +0,0 @@
package dev.vality.hooker.service;
import dev.vality.hooker.dao.impl.CustomerDaoImpl;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
@Component
@Slf4j
@RequiredArgsConstructor
public class CustomerLastEventService {
private final CustomerDaoImpl customerDao;
public Long getLastEventId() {
Long custLastEventId = customerDao.getMaxEventId();
log.info("Get last event id = {}", custLastEventId);
return custLastEventId;
}
}

View File

@ -1,13 +0,0 @@
package dev.vality.hooker.service;
public interface FaultDetectorService {
double getRate(long hookId);
void startRequest(long hookId, long eventId);
void finishRequest(long hookId, long eventId);
void errorRequest(long hookId, long eventId);
}

View File

@ -1,89 +0,0 @@
package dev.vality.hooker.service;
import dev.vality.damsel.fault_detector.Error;
import dev.vality.damsel.fault_detector.FaultDetectorSrv;
import dev.vality.damsel.fault_detector.Finish;
import dev.vality.damsel.fault_detector.Operation;
import dev.vality.damsel.fault_detector.OperationState;
import dev.vality.damsel.fault_detector.ServiceConfig;
import dev.vality.damsel.fault_detector.ServiceStatistics;
import dev.vality.damsel.fault_detector.Start;
import dev.vality.geck.common.util.TypeUtil;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import javax.annotation.PostConstruct;
import java.time.LocalDateTime;
import java.time.ZoneOffset;
import java.util.List;
@Slf4j
@Service
@RequiredArgsConstructor
public class FaultDetectorServiceImpl implements FaultDetectorService {
private final FaultDetectorSrv.Iface faultDetector;
@Value("${service.fault-detector.slidingWindow}")
private Long slidingWindowMillis;
@Value("${service.fault-detector.operationTimeLimit}")
private Long operationTimeLimit;
private ServiceConfig serviceConfig;
@PostConstruct
public void init() {
serviceConfig = new ServiceConfig()
.setSlidingWindow(slidingWindowMillis)
.setOperationTimeLimit(operationTimeLimit);
}
@Override
public double getRate(long hookId) {
try {
List<ServiceStatistics> statistics = faultDetector.getStatistics(List.of(buildServiceId(hookId)));
return statistics.isEmpty() ? 0 : statistics.get(0).getFailureRate();
} catch (Exception e) {
log.error("Error in FaultDetectorService when getStatistics", e);
return 0;
}
}
@Override
public void startRequest(long hookId, long eventId) {
registerOperation(hookId, eventId, OperationState.start(new Start(getNow())));
}
@Override
public void finishRequest(long hookId, long eventId) {
registerOperation(hookId, eventId, OperationState.finish(new Finish(getNow())));
}
@Override
public void errorRequest(long hookId, long eventId) {
registerOperation(hookId, eventId, OperationState.error(new Error(getNow())));
}
private void registerOperation(long hookId, long eventId, OperationState operationState) {
Operation operation = new Operation()
.setOperationId(String.valueOf(eventId))
.setState(operationState);
try {
faultDetector.registerOperation(buildServiceId(hookId), operation, serviceConfig);
} catch (Exception e) {
log.error("Error in FaultDetectorService when registerOperation", e);
}
}
private String getNow() {
return TypeUtil.temporalToString(LocalDateTime.now(ZoneOffset.UTC));
}
private String buildServiceId(long id) {
return "hooker-" + id;
}
}

View File

@ -1,7 +1,8 @@
package dev.vality.hooker.service;
import dev.vality.damsel.payment_processing.InvoiceChange;
import dev.vality.hooker.handler.poller.invoicing.AbstractInvoiceEventMapper;
import dev.vality.hooker.handler.Mapper;
import dev.vality.hooker.model.InvoicingMessage;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
@ -12,9 +13,9 @@ import java.util.Optional;
@RequiredArgsConstructor
public class HandlerManager {
private final List<AbstractInvoiceEventMapper> handlers;
private final List<Mapper<InvoiceChange, InvoicingMessage>> handlers;
public Optional<AbstractInvoiceEventMapper> getHandler(InvoiceChange change) {
public Optional<Mapper<InvoiceChange, InvoicingMessage>> getHandler(InvoiceChange change) {
return handlers.stream().filter(handler -> handler.accept(change)).findFirst();
}
}

View File

@ -11,23 +11,7 @@ import dev.vality.hooker.exception.RemoteHostException;
import dev.vality.hooker.model.InvoicingMessage;
import dev.vality.hooker.utils.HellgateUtils;
import dev.vality.hooker.utils.TimeUtils;
import dev.vality.swag_webhook_events.model.Event;
import dev.vality.swag_webhook_events.model.Invoice;
import dev.vality.swag_webhook_events.model.InvoiceCancelled;
import dev.vality.swag_webhook_events.model.InvoiceCreated;
import dev.vality.swag_webhook_events.model.InvoiceFulfilled;
import dev.vality.swag_webhook_events.model.InvoicePaid;
import dev.vality.swag_webhook_events.model.Payment;
import dev.vality.swag_webhook_events.model.PaymentCancelled;
import dev.vality.swag_webhook_events.model.PaymentCaptured;
import dev.vality.swag_webhook_events.model.PaymentFailed;
import dev.vality.swag_webhook_events.model.PaymentProcessed;
import dev.vality.swag_webhook_events.model.PaymentRefunded;
import dev.vality.swag_webhook_events.model.PaymentStarted;
import dev.vality.swag_webhook_events.model.Refund;
import dev.vality.swag_webhook_events.model.RefundCreated;
import dev.vality.swag_webhook_events.model.RefundFailed;
import dev.vality.swag_webhook_events.model.RefundSucceeded;
import dev.vality.swag_webhook_events.model.*;
import lombok.RequiredArgsConstructor;
import org.apache.thrift.TException;
import org.springframework.stereotype.Service;
@ -45,7 +29,7 @@ public class InvoicingEventService
@Override
public Event getEventByMessage(InvoicingMessage message) {
return resolveEvent(message, getInvoiceByMessage(message))
.eventID(message.getEventId().intValue())
.eventID(message.getId().intValue())
.occuredAt(TimeUtils.toOffsetDateTime(message.getEventTime()))
.topic(Event.TopicEnum.INVOICESTOPIC);
}
@ -60,41 +44,34 @@ public class InvoicingEventService
try {
return invoicingClient.get(
HellgateUtils.USER_INFO,
message.getInvoiceId(),
message.getSourceId(),
HellgateUtils.getEventRange(message.getSequenceId().intValue())
);
} catch (InvoiceNotFound e) {
throw new NotFoundException("Invoice not found, invoiceId=" + message.getInvoiceId());
throw new NotFoundException("Invoice not found, invoiceId=" + message.getSourceId());
} catch (TException e) {
throw new RemoteHostException(e);
}
}
private Event resolveEvent(InvoicingMessage m, dev.vality.damsel.payment_processing.Invoice invoiceInfo) {
switch (m.getEventType()) {
case INVOICE_CREATED:
return new InvoiceCreated()
return switch (m.getEventType()) {
case INVOICE_CREATED -> new InvoiceCreated()
.invoice(getSwagInvoice(invoiceInfo))
.eventType(Event.EventTypeEnum.INVOICECREATED);
case INVOICE_STATUS_CHANGED:
return resolveInvoiceStatusChanged(m, invoiceInfo);
case INVOICE_PAYMENT_STARTED:
return new PaymentStarted()
case INVOICE_STATUS_CHANGED -> resolveInvoiceStatusChanged(m, invoiceInfo);
case INVOICE_PAYMENT_STARTED -> new PaymentStarted()
.invoice(getSwagInvoice(invoiceInfo))
.payment(getSwagPayment(m, invoiceInfo))
.eventType(Event.EventTypeEnum.PAYMENTSTARTED);
case INVOICE_PAYMENT_STATUS_CHANGED:
return resolvePaymentStatusChanged(m, invoiceInfo);
case INVOICE_PAYMENT_REFUND_STARTED:
return new RefundCreated()
case INVOICE_PAYMENT_STATUS_CHANGED -> resolvePaymentStatusChanged(m, invoiceInfo);
case INVOICE_PAYMENT_REFUND_STARTED -> new RefundCreated()
.invoice(getSwagInvoice(invoiceInfo))
.payment(getSwagPayment(m, invoiceInfo))
.refund(getSwagRefund(m, invoiceInfo));
case INVOICE_PAYMENT_REFUND_STATUS_CHANGED:
return resolveRefundStatusChanged(m, invoiceInfo);
default:
throw new UnsupportedOperationException("Unknown event type " + m.getEventType());
}
case INVOICE_PAYMENT_REFUND_STATUS_CHANGED -> resolveRefundStatusChanged(m, invoiceInfo);
default -> throw new UnsupportedOperationException("Unknown event type " + m.getEventType());
};
}
private Invoice getSwagInvoice(dev.vality.damsel.payment_processing.Invoice invoiceInfo) {
@ -134,7 +111,7 @@ public class InvoicingEventService
.findFirst()
.orElseThrow(
() -> new NotFoundException(
String.format("Payment not found, invoiceId=%s, paymentId=%s", message.getInvoiceId(),
String.format("Payment not found, invoiceId=%s, paymentId=%s", message.getSourceId(),
message.getPaymentId())
)
);
@ -194,7 +171,7 @@ public class InvoicingEventService
() -> new NotFoundException(
String.format(
"Refund not found, invoiceId=%s, paymentId=%s, refundId=%s",
m.getInvoiceId(), m.getPaymentId(), m.getRefundId()
m.getSourceId(), m.getPaymentId(), m.getRefundId()
)
)
);
@ -205,15 +182,11 @@ public class InvoicingEventService
Invoice swagInvoice = getSwagInvoice(invoiceInfo);
Payment swagPayment = getSwagPayment(message, invoiceInfo);
Refund swagRefund = getSwagRefund(message, invoiceInfo);
switch (message.getRefundStatus()) {
case PENDING:
return new RefundCreated().invoice(swagInvoice).payment(swagPayment).refund(swagRefund);
case SUCCEEDED:
return new RefundSucceeded().invoice(swagInvoice).payment(swagPayment).refund(swagRefund);
case FAILED:
return new RefundFailed().invoice(swagInvoice).payment(swagPayment).refund(swagRefund);
default:
throw new UnsupportedOperationException("Unknown refund status " + message.getRefundStatus());
}
return switch (message.getRefundStatus()) {
case PENDING -> new RefundCreated().invoice(swagInvoice).payment(swagPayment).refund(swagRefund);
case SUCCEEDED -> new RefundSucceeded().invoice(swagInvoice).payment(swagPayment).refund(swagRefund);
case FAILED -> new RefundFailed().invoice(swagInvoice).payment(swagPayment).refund(swagRefund);
default -> throw new UnsupportedOperationException("Unknown refund status " + message.getRefundStatus());
};
}
}

View File

@ -1,118 +0,0 @@
package dev.vality.hooker.service;
import dev.vality.hooker.dao.HookDao;
import dev.vality.hooker.dao.MessageDao;
import dev.vality.hooker.dao.QueueDao;
import dev.vality.hooker.dao.TaskDao;
import dev.vality.hooker.exception.DaoException;
import dev.vality.hooker.model.Message;
import dev.vality.hooker.model.Queue;
import dev.vality.hooker.model.QueueStatus;
import dev.vality.hooker.model.Task;
import dev.vality.hooker.retry.RetryPoliciesService;
import dev.vality.hooker.retry.RetryPolicy;
import dev.vality.hooker.retry.RetryPolicyRecord;
import dev.vality.hooker.scheduler.MessageSender;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.transaction.support.TransactionTemplate;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
@Slf4j
@RequiredArgsConstructor
public class MessageProcessor<M extends Message, Q extends Queue> implements Runnable {
private static final double UPDATE_PROBABILITY = 0.25;
private final HookDao hookDao;
private final TaskDao taskDao;
private final QueueDao<Q> queueDao;
private final MessageDao<M> messageDao;
private final RetryPoliciesService retryPoliciesService;
private final TransactionTemplate transactionTemplate;
private final FaultDetectorService faultDetector;
private final MessageSender<M, Q> messageSender;
@Override
public void run() {
transactionTemplate.execute(k -> {
process();
return null;
});
}
private void process() {
Map<Long, List<Task>> scheduledTasks = taskDao.getScheduled();
log.debug("scheduledTasks {}", scheduledTasks);
if (scheduledTasks.entrySet().isEmpty()) {
return;
}
Set<Long> queueIds = scheduledTasks.keySet();
Map<Long, Q> queuesMap =
queueDao.getWithPolicies(queueIds).stream().collect(Collectors.toMap(Queue::getId, q -> q));
Set<Long> messageIds = scheduledTasks.values().stream().flatMap(Collection::stream).map(Task::getMessageId)
.collect(Collectors.toSet());
Map<Long, M> messagesMap =
messageDao.getBy(messageIds).stream().collect(Collectors.toMap(Message::getId, m -> m));
List<QueueStatus> queueStatuses = messageSender.send(scheduledTasks, queuesMap, messagesMap);
queueStatuses.forEach(queueStatus -> {
try {
Queue queue = queueStatus.getQueue();
queueStatus.getMessagesDone().forEach(id -> taskDao.remove(queue.getId(), id));
if (queueStatus.isSuccess()) {
done(queue);
} else {
fail(queue);
}
} catch (DaoException e) {
log.error("DaoException error when remove sent messages. " +
"It's not a big deal, but some messages may be re-sent: {}",
queueStatus.getMessagesDone(), e);
}
});
}
private void done(Queue queue) {
if (queue.getRetryPolicyRecord().isFailed()) {
RetryPolicyRecord record = queue.getRetryPolicyRecord();
record.reset();
updatePolicy(record);
updateAvailability(queue);
} else {
if (Math.random() < UPDATE_PROBABILITY) {
updateAvailability(queue);
}
}
}
private void fail(Queue queue) {
log.warn("Queue {} failed.", queue.getId());
RetryPolicy retryPolicy = retryPoliciesService.getRetryPolicyByType(queue.getHook().getRetryPolicyType());
RetryPolicyRecord retryPolicyRecord = queue.getRetryPolicyRecord();
retryPolicy.updateFailed(retryPolicyRecord);
updatePolicy(retryPolicyRecord);
updateAvailability(queue);
if (retryPolicy.shouldDisable(retryPolicyRecord)) {
queueDao.disable(queue.getId());
taskDao.removeAll(queue.getId());
log.warn("Queue {} was disabled according to retry policy.", queue.getId());
}
}
private void updatePolicy(RetryPolicyRecord record) {
retryPoliciesService.update(record);
log.info("Queue retry policy has been updated {}", record);
}
private void updateAvailability(Queue queue) {
//double rate = faultDetector.getRate(queue.getHook().getId());
//hookDao.updateAvailability(queue.getHook().getId(), rate);
//log.info("Hook {} availability has been updated to {}", queue.getHook().getId(), rate);
}
}

View File

@ -0,0 +1,41 @@
package dev.vality.hooker.service;
import dev.vality.hooker.converter.WebhookMessageBuilder;
import dev.vality.hooker.dao.MessageDao;
import dev.vality.hooker.model.Message;
import dev.vality.swag_webhook_events.model.Event;
import dev.vality.webhook.dispatcher.WebhookMessage;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
@Slf4j
@RequiredArgsConstructor
public class MessageService<T extends Message> {
private final MessageDao<T> messageDao;
private final EventService<T> eventService;
private final WebhookMessageBuilder webhookMessageBuilder;
private final WebhookKafkaProducerService webhookKafkaProducerService;
public void process(T message) {
log.info("Start processing of message {}", message);
Long id = messageDao.save(message);
String sourceId = message.getSourceId();
if (id != null) {
message.setId(id);
var webhookModels = messageDao.getWebhookModels(id);
if (!webhookModels.isEmpty()) {
log.info("Processing {} webhook(s)", webhookModels.size());
Event event = eventService.getEventByMessage(message);
webhookModels.forEach(w -> {
Long hookId = w.getHookId();
Long parentEventId = messageDao.getParentId(hookId, sourceId, id);
WebhookMessage webhookMessage = webhookMessageBuilder.build(w, event, sourceId, parentEventId);
log.info("Try to send webhook to kafka: {}", webhookMessage);
webhookKafkaProducerService.send(webhookMessage);
log.info("Webhook to kafka was sent: sourceId={}", webhookMessage.getSourceId());
});
}
}
log.info("End processing of message {}", sourceId);
}
}

View File

@ -1,57 +0,0 @@
package dev.vality.hooker.service;
import dev.vality.hooker.logging.HttpLoggingInterceptor;
import lombok.extern.slf4j.Slf4j;
import okhttp3.ConnectionPool;
import okhttp3.MediaType;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.RequestBody;
import okhttp3.Response;
import java.io.IOException;
import java.util.concurrent.TimeUnit;
@Slf4j
public class PostSender {
public static final MediaType JSON = MediaType.parse("application/json; charset=utf-8");
public static final String SIGNATURE_HEADER = "Content-Signature";
public static final long RESPONSE_MAX_LENGTH = 4096L;
private final OkHttpClient httpClient;
public PostSender(int connectionPoolSize, int timeout) {
OkHttpClient.Builder httpBuilder = new OkHttpClient.Builder();
if (log.isDebugEnabled()) {
HttpLoggingInterceptor httpLoggingInterceptor = new HttpLoggingInterceptor(message -> log.debug(message));
httpLoggingInterceptor.setLevel(HttpLoggingInterceptor.Level.BODY);
httpBuilder.addInterceptor(httpLoggingInterceptor);
}
ConnectionPool connectionPool = new ConnectionPool(2 * connectionPoolSize, 5, TimeUnit.MINUTES);
this.httpClient = httpBuilder
.connectionPool(connectionPool)
.retryOnConnectionFailure(false)
.connectTimeout(timeout, TimeUnit.SECONDS)
.writeTimeout(timeout, TimeUnit.SECONDS)
.readTimeout(timeout, TimeUnit.SECONDS)
.build();
}
public int doPost(String url, long messageId, String paramsAsString, String signature) throws IOException {
log.info("Sending message with id {}, {} to hook: {} ", messageId, paramsAsString, url);
RequestBody body = RequestBody.create(JSON, paramsAsString);
final Request request = new Request.Builder()
.url(url)
.addHeader(SIGNATURE_HEADER, "alg=RS256; digest=" + signature)
.post(body)
.build();
try (Response response = httpClient.newCall(request).execute()) {
log.info("Response from hook: messageId: {}, code: {}; body: {}", messageId, response.code(),
response.body() != null ? response.peekBody(RESPONSE_MAX_LENGTH).string() : "<empty>");
return response.code();
}
}
}

View File

@ -0,0 +1,42 @@
package dev.vality.hooker.service;
import dev.vality.kafka.common.exception.KafkaProduceException;
import dev.vality.webhook.dispatcher.WebhookMessage;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.kafka.core.KafkaTemplate;
import org.springframework.stereotype.Service;
@Slf4j
@Service
@RequiredArgsConstructor
public class WebhookKafkaProducerService {
private final KafkaTemplate<String, WebhookMessage> kafkaTemplate;
@Value("${kafka.topic.webhook-dispatcher.name}")
private String topicName;
@Value("${kafka.topic.webhook-dispatcher.produce.enabled}")
private boolean producerEnabled;
public void send(WebhookMessage webhookMessage) {
if (producerEnabled) {
sendWebhook(webhookMessage);
}
}
private void sendWebhook(WebhookMessage webhookMessage) {
try {
kafkaTemplate.send(topicName, webhookMessage.getSourceId(), webhookMessage).get();
} catch (InterruptedException e) {
log.error("InterruptedException command: {}", webhookMessage, e);
Thread.currentThread().interrupt();
throw new KafkaProduceException(e);
} catch (Exception e) {
log.error("Error while sending command: {}", webhookMessage, e);
throw new KafkaProduceException(e);
}
}
}

View File

@ -1,10 +1,6 @@
package dev.vality.hooker.service;
import dev.vality.damsel.webhooker.LimitExceeded;
import dev.vality.damsel.webhooker.Webhook;
import dev.vality.damsel.webhooker.WebhookManagerSrv;
import dev.vality.damsel.webhooker.WebhookNotFound;
import dev.vality.damsel.webhooker.WebhookParams;
import dev.vality.damsel.webhooker.*;
import dev.vality.hooker.dao.HookDao;
import dev.vality.hooker.model.Hook;
import dev.vality.hooker.utils.HookConverter;

View File

@ -1,47 +0,0 @@
package dev.vality.hooker.service;
import dev.vality.damsel.webhooker.SourceNotFound;
import dev.vality.damsel.webhooker.WebhookMessageServiceSrv;
import dev.vality.damsel.webhooker.WebhookNotFound;
import dev.vality.hooker.dao.HookDao;
import dev.vality.hooker.dao.impl.CustomerTaskDao;
import dev.vality.hooker.dao.impl.InvoicingTaskDao;
import dev.vality.hooker.model.Hook;
import dev.vality.swag_webhook_events.model.Event;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.thrift.TException;
import org.springframework.stereotype.Service;
@Service
@Slf4j
@RequiredArgsConstructor
public class WebhookMessageService implements WebhookMessageServiceSrv.Iface {
private final HookDao hookDao;
private final InvoicingTaskDao invoicingTaskDao;
private final CustomerTaskDao customerTaskDao;
@Override
public void send(long hookId, String sourceId) throws TException {
log.info("Start creating tasks for sending hooks for hookId={}, invoiceId={}", hookId, sourceId);
Hook hook = hookDao.getHookById(hookId);
if (hook == null) {
log.warn("Webhook with id={} not found", hookId);
throw new WebhookNotFound();
}
int count;
if (hook.getTopic().equals(Event.TopicEnum.INVOICESTOPIC.getValue())) {
count = invoicingTaskDao.save(hookId, sourceId);
} else if (hook.getTopic().equals(Event.TopicEnum.CUSTOMERSTOPIC.getValue())) {
count = customerTaskDao.create(hookId, sourceId);
} else {
throw new RuntimeException("Unknown webhook type " + hook.getTopic());
}
if (count < 1) {
log.warn("No tasks created for hookId={} and invoiceId={}", hookId, sourceId);
throw new SourceNotFound();
}
log.info("Tasks has been created. Count={} for hookId={}, invoiceId={}", count, hookId, sourceId);
}
}

View File

@ -4,13 +4,7 @@ import dev.vality.hooker.service.err.UnknownCryptoException;
import org.springframework.stereotype.Component;
import java.nio.charset.StandardCharsets;
import java.security.InvalidKeyException;
import java.security.KeyFactory;
import java.security.KeyPairGenerator;
import java.security.NoSuchAlgorithmException;
import java.security.PrivateKey;
import java.security.Signature;
import java.security.SignatureException;
import java.security.*;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.KeySpec;
import java.security.spec.PKCS8EncodedKeySpec;

View File

@ -1,17 +0,0 @@
package dev.vality.hooker.service.err;
public class PostRequestException extends Exception {
public PostRequestException(Throwable cause) {
super(cause);
}
public PostRequestException(String errMessage) {
super(errMessage);
}
@Override
public String getMessage() {
String message = getCause() != null ? getCause().getMessage() : super.getMessage();
return "Unknown error during request to merchant execution. \n" + message;
}
}

View File

@ -1,28 +1,6 @@
package dev.vality.hooker.utils;
import dev.vality.damsel.webhooker.CustomerBindingEvent;
import dev.vality.damsel.webhooker.CustomerBindingFailed;
import dev.vality.damsel.webhooker.CustomerBindingStarted;
import dev.vality.damsel.webhooker.CustomerBindingSucceeded;
import dev.vality.damsel.webhooker.CustomerCreated;
import dev.vality.damsel.webhooker.CustomerDeleted;
import dev.vality.damsel.webhooker.CustomerEventFilter;
import dev.vality.damsel.webhooker.CustomerEventType;
import dev.vality.damsel.webhooker.CustomerStatusReady;
import dev.vality.damsel.webhooker.EventFilter;
import dev.vality.damsel.webhooker.InvoiceCreated;
import dev.vality.damsel.webhooker.InvoiceEventFilter;
import dev.vality.damsel.webhooker.InvoiceEventType;
import dev.vality.damsel.webhooker.InvoicePaymentCreated;
import dev.vality.damsel.webhooker.InvoicePaymentEventType;
import dev.vality.damsel.webhooker.InvoicePaymentRefundChange;
import dev.vality.damsel.webhooker.InvoicePaymentRefundCreated;
import dev.vality.damsel.webhooker.InvoicePaymentRefundStatus;
import dev.vality.damsel.webhooker.InvoicePaymentRefundStatusChanged;
import dev.vality.damsel.webhooker.InvoicePaymentStatus;
import dev.vality.damsel.webhooker.InvoicePaymentStatusChanged;
import dev.vality.damsel.webhooker.InvoiceStatus;
import dev.vality.damsel.webhooker.InvoiceStatusChanged;
import dev.vality.damsel.webhooker.*;
import dev.vality.hooker.dao.WebhookAdditionalFilter;
import dev.vality.hooker.model.EventType;
import dev.vality.swag_webhook_events.model.Event;

View File

@ -7,7 +7,7 @@ public class KeyUtils {
public static InvoicingMessageKey key(InvoicingMessage message) {
return InvoicingMessageKey.builder()
.invoiceId(message.getInvoiceId())
.invoiceId(message.getSourceId())
.paymentId(message.getPaymentId())
.refundId(message.getRefundId())
.type(message.getType())

View File

@ -11,8 +11,6 @@ import dev.vality.swag_webhook_events.model.*;
import java.nio.charset.StandardCharsets;
import java.util.Base64;
import java.util.Objects;
import java.util.Optional;
public class PaymentToolUtils {
@ -25,14 +23,14 @@ public class PaymentToolUtils {
.lastDigits(paymentTool.getBankCard().getLastDigits())
.cardNumberMask(
paymentTool.getBankCard().getBin() + "******" + paymentTool.getBankCard().getLastDigits())
.tokenProvider(
Optional.ofNullable(TokenProviderUtil.getTokenProviderName(paymentTool.getBankCard()))
.map(PaymentToolDetailsBankCard.TokenProviderEnum::fromValue)
.orElse(null)
)
.tokenProvider(paymentTool.getBankCard().getTokenProviderDeprecated() != null
? PaymentToolDetailsBankCard.TokenProviderEnum.fromValue(
paymentTool.getBankCard().getTokenProviderDeprecated().name())
: null)
.paymentSystem(PaymentSystemUtil.getPaymentSystemName(paymentTool.getBankCard()))
.issuerCountry(paymentTool.getBankCard().getIssuerCountry() != null
? paymentTool.getBankCard().getIssuerCountry().name() : null)
? paymentTool.getBankCard().getIssuerCountry().name()
: null)
.bankName(paymentTool.getBankCard().getBankName())
.detailsType(PaymentToolDetails.DetailsTypeEnum.PAYMENTTOOLDETAILSBANKCARD);
} else if (paymentTool.isSetPaymentTerminal()) {
@ -41,7 +39,6 @@ public class PaymentToolUtils {
TerminalPaymentUtil.getTerminalPaymentProviderName(paymentTool.getPaymentTerminal())))
.detailsType(PaymentToolDetails.DetailsTypeEnum.PAYMENTTOOLDETAILSPAYMENTTERMINAL);
} else if (paymentTool.isSetDigitalWallet()) {
//TODO Bump swag-webhook-events api
LegacyDigitalWalletProvider walletProvider = LegacyDigitalWalletProvider.valueOf(
DigitalWalletUtil.getDigitalWalletName(paymentTool.getDigitalWallet()));
if (walletProvider == LegacyDigitalWalletProvider.qiwi) {

View File

@ -63,7 +63,11 @@ kafka:
id: mg-events-customer
enabled: false
concurrency: 1
topic:
webhook-dispatcher:
name: webhook-dispatcher-topic
produce:
enabled: true
info:
version: '@version@'
responsible: Inal Arsanukaev
@ -77,18 +81,6 @@ service:
customer:
url: http://hellgate:8022/v1/processing/customer_management
networkTimeout: 5000
fault-detector:
url: http://fault-detector:8022/v1/
networkTimeout: 5000
slidingWindow: 60000
operationTimeLimit: 3000
cache:
invoice:
size: 10000
# connection timeout
merchant.callback.timeout: 10
limit:
perShop: 10
@ -106,19 +98,6 @@ spring.datasource:
data-source-properties:
reWriteBatchedInserts: true
db:
jdbc:
tr_timeout: 300000
message:
scheduler:
delay: 1000
invoicing:
threadPoolSize: 10
customer:
threadPoolSize: 3
clean:
scheduler:
cron: "0 0 * * * *"

Some files were not shown because too many files have changed in this diff Show More